Apollo之Canbus处理来自Control的Cmd
简述canbus
apollo项目中canbus模块的主要作用是接收Control模块发布的指令,然后将指令解析为CAN协议报文与车辆的ECU交互,且得到指令的反馈信息,并将反馈结果发布为车辆底盘信息(Chassis_datail)。
先看一下apollo的整体框架图
Canbus模块之解析Command
在apollo项目中有两处canbus源码,modules/canbus 这一部分的canbus主要是对Vehicle(车辆)控制的实现,modules/driver/canbus是实现数据通信主要的模块。这篇文章主要是canbus如何处理Command,这两块的canbus源码就不仔细分析了,有疑惑的可以参考这篇博文https://blog.****.net/davidhopper/article/details/79176505。apollo的modules各模块初始化操作大致相同,我也是通过这篇文章来学习的canbus模块。接下来进入正题“处理Command”,先看我画的流程图。
再推荐一篇博文https://blog.****.net/liu3612162/article/details/81981388
比较清晰的阐述了canbus模块中主要文件夹的含义。
1、canbus.cc中注册回调函数
在modules/canbus/canbus.cc的Init()函数中
if (!FLAGS_receive_guardian) {
AdapterManager::AddControlCommandCallback(&Canbus::OnControlCommand, this);
} else {
AdapterManager::AddGuardianCallback(&Canbus::OnGuardianCommand, this);
}
AddControlCommandCallback()、AddGuardianCallback(),在这里注册两个回调函数。当Control模块指令发布之后就调用这个两个函数,通过匹配然后调用OnControlCommand()、OnGuarddianCommand(),假设接受来自Control的刹车指令,然后我们调用OnControlCommand()函数。
2、进入处理指令的接口
void Canbus::OnControlCommand(const ControlCommand &control_command)
if (vehicle_controller_->Update(control_command) != ErrorCode::OK)
can_sender_.Update();
在这个函数中通过调用两次Update()函数,第一个Update()是解析指令控制车辆并修改协议类型数据,第二个Update()是更新修改了的协议类型数据,并Send数据。而这也是这块的核心之处,先看第一个Update()。
2.1修改协议类型数据
第一个Update()函数在modules/canbus/vehicle/vehicle_controller.cc
VehicleController::Update(const ControlCommand &command)
在这个函数中先根据指令选择driving mode,然后根据driving mode来修改协议类型数据,还是假设接收到的是刹车(Brake)指令
Brake(control_command.brake());
void CidiT7Controller::Brake(double pedal) {
if (!(driving_mode() == Chassis::COMPLETE_AUTO_DRIVE ||
driving_mode() == Chassis::AUTO_SPEED_ONLY)) {
AINFO << "The current drive mode does not need to set acceleration.";
return;
}
brake_60_->set_pedal(pedal);
}
Brake60 *Brake60::set_pedal(double pedal) {
pedal_cmd_ = pedal;
if (pedal_cmd_ < 1e-3) {
disable_boo_cmd();
} else {
enable_boo_cmd();
}
return this;
}
很清晰的看到用计算的pedal来更新协议类数据成员pedal_cmd_,在这里更新了自定义的协议类型数据
看源码的时候经常能看到一个protocolData这个类型,这个叫协议数据类型。其实很好理解,以int为协议来代表的是整数,以char为协议代表的字符,那这个protocolData就是我们以这个类型为协议来替代车辆行驶的具体操作,只是这个比较抽象我们需要专门定义一个类来代表这个操作。举个例子,在modules/canbus/vehicle/brake_60.h 这个brake_60类的操作就是刹车,
private:
double pedal_cmd_ = 0.0;
bool boo_cmd_ = false;
bool pedal_enable_ = false;
bool clear_driver_override_flag_ = false;
bool ignore_driver_override_ = false;
int32_t watchdog_counter_ = 0.0;
Control模块的cmd其实就是为了改变这几个协议类的成员变量。
2.2 更新并发送
这个职责是第二个Update()函数来做的。更新很好理解修改了的协议类成员变量修改完需要更新之后才可以起作用。看代码
void Brake60::UpdateData(uint8_t *data) {
set_pedal_p(data, pedal_cmd_);
set_boo_cmd_p(data, boo_cmd_);
set_enable_p(data, pedal_enable_);
set_clear_driver_override_flag_p(data, clear_driver_override_flag_);
set_watchdog_counter_p(data, watchdog_counter_);
}
上面的UpdateDate()的操作正是protocol_data_->UpdateData(can_frame_to_update_.data)来调用的。
Update()调用的是modules/driver/canbus/can_sender.h的Update()
template <typename SensorType>
void SenderMessage<SensorType>::Update() {
if (protocol_data_ == nullptr) {
AERROR << "Attention: ProtocolData is nullptr!";
return;
}
protocol_data_->UpdateData(can_frame_to_update_.data);
std::lock_guard<std::mutex> lock(mutex_);
can_frame_to_send_ = can_frame_to_update_;
}
第二个Update()核心之后就在于把更新之后的协议类数据通过can_frame_to_update_赋值给can_frame_to_send_;
首先CanFrame是什么
struct CanFrame {
/// Message id
uint32_t id;
/// Message length
uint8_t len;
/// Message content
uint8_t data[8];
/// Time stamp
struct timeval timestamp;
这是一个结构体,CAN数据线上传递的就是CanFrame.data[],那幅图里也有体现。我们需要通过updateData()来把自己定义协议类型数据转化成可以在CAN数据线上传输的类型。那更新好了的can_frame_to_send_又是怎么发送出去的呢
3 发送CanFrame
这个操作是上图的Sender完成的,知道Sender在之后就更能理解了这两处的canbus各自的职责是什么了。Sender是modules/driver/canbus/can_common/can_sender.h。就是一个粘合剂,连接上层canbus.cc与下层esd_can_client的。这个模块的Init()与Start()是在上层canbus.cc Init()与Start()的时候就调用了。看canbus.cc的代码在canbus.cc的Init()中
if (can_receiver_.Init(can_client_.get(), message_manager_.get(),
canbus_conf_.enable_receiver_log()) != ErrorCode::OK) {
return OnError("Failed to init can receiver.");
}
AINFO << "The can receiver is successfully initialized.";
if (can_sender_.Init(can_client_.get(), canbus_conf_.enable_sender_log()) !=
ErrorCode::OK) {
return OnError("Failed to init can sender.");
}
AINFO << "The can sender is successfully initialized.";
// 2. start receive first then send
if (can_receiver_.Start() != ErrorCode::OK) {
return OnError("Failed to start can receiver.");
}
AINFO << "Can receiver is started.";
// 3. start send
if (can_sender_.Start() != ErrorCode::OK) {
return OnError("Failed to start can sender.");
}
在modules/driver/canbus/can_common/can_sender.h的start()函数中
is_running_ = true;
thread_.reset(new std::thread([this] { PowerSendThreadFunc(); }));
显然在canbus这个模块一开始就开始t了Sender的start()模块,在Sender里专门有一个线程去负责发送message。我们去看这个接口PowerSendThreadFunc()
这个接口里核心就是调用了
if (can_client_->SendSingleFrame(can_frames) != common::ErrorCode::OK)
在这里调用esd_can_client来发送CanFrame。
4、反馈底盘信息
这个操作是在canbus.cc中执行的,向Control反馈以让控制中心及时了解指令的执行情况
// 5. set timer to triger publish info periodly
const double duration = 1.0 / FLAGS_chassis_freq;
timer_ = AdapterManager::CreateTimer(ros::Duration(duration),
&Canbus::OnTimer, this);
void Canbus::OnTimer(const ros::TimerEvent &) {
PublishChassis();
if (FLAGS_enable_chassis_detail_pub) {
PublishChassisDetail();
}
}
在canbus模块一开始就会注册一个定时器OnTimer,它会定时向Control 发布chassis_detail。
补充
1、其实再往下会看到apollo里Send()其实是调用了canwrite()函数,这个函数是CAN卡提供的别人已经把自己的读取函数做了编译封装成库你直接调用就可以。
在apollo/third_party/can_card_library/esd_can/include/ntcan.h 名字起的很专业 第三方库 CAN卡 esd客户端的ntcan.h
2、canbus模块还有很多很深奥的地方,比如它对要收发的message的处理,即 modules/driver/canbus/can_common/message_manager.h 、还有Sender.h有一个SenderMessage类来预处理、它的所有自定义协议类型数据都继承了类protocol_data(modules/driver/canbus/can_common/protocol_data.h)它里面有两个非常重要的函数,一个是我们说到的UpdateData(),还有一个prase(),用来把CanFrame解析成协议类型数据,prase()会在Receive()的时候会大有作为。