how_to_add_a_new_can_device
- 在自动机调试的领域,使用CAN接口的应用非常广泛,这一块,在Apollo中也有所体现,今天我们就来结合代码分析一下Apollo中的CAN数据交互流程,以及分享如何在Apollo中添加一个新的CAN设备。
- 首先,普及一下相关的知识点,普通的IPC或者PC是不具有CAN口,也就是不能直接和CAN设备通信,所以他们之间需要用到转换设备,将CAN协议转换成适配IPC的协议,例如PCIe、USB等。像Kvaser PCIEcan 4×HS就是一个CAN转PCIe设备,俗称CAN卡,后面的4xHS代表其可以“一拖四”的带有四个CAN口,它需要插在主板上的PCIe接口。而kvaser leaf light HS v2是一个CAN转USB设备,其只有一个CAN口,是插在USB口即可。
- 本文将大体分为下面两部分:
- 以Apollo/conti_radar为例,聊一下Apollo中CAN设备的相关流程及代码。
- 以Apollo/conti_radar为例,讲解Apollo中增加新的CAN设备的基本流程。
框架理解
- 直接看代码跳来跳去很繁琐,我总结归纳了一段整体流程的伪代码,希望可以帮助你有一个基本的流程印象。
整体流程伪代码:
struct CanFrame {
/// Message id
uint32_t id;
/// Message length
uint8_t len;
/// Message content
uint8_t data[8];
/// Time stamp
struct timeval timestamp;
}
//-- 这里的AA、BB、CC分别是不同的消息msg_id,这个是根据设备的数据手册来定义的。比如0x301之类的;
//-- dosth_AA等都是收到相应的消息后的处理逻辑代称;在Apollo中,我们一般在这里调用AdapterManager::Publish_XXX_Topic来发送ROS消息。
ContiRadarMessageManager::Parse(msg_id, data, length){
switch(msg_id){
case AA:{ dosth_AA; } // Publish_AA_Topic
case BB:{ dosth_BB; } // Publish_BB_Topic
case CC:{ dosth_CC; } // Publish_CC_Topic
}
}
Kvaser_MSG recv_frames_;
kvaserCanClient::Receive(std::vector<CanFrame> *frames,int32_t *frame_num){
canReadWait(handler, &recv_frames_.id,&recv_frames_.data,&recv_frames_.len, &recv_frames_.flag,&recv_frames_.time,&recv_frames_.time_out);
CanFrame cf;
cf.id = recv_frames_.id;
cf.len = recv_frames_.len;
std::memcpy(cf.data, recv_frames_.data, recv_frames_.len);
frames->push_back(cf)
}
APOLLO_MAIN(){
// 1. init and start the can card hardwarejiekou
//-- can_client: ESD/Kvaser
can_client_->Start();
// 2. start receive first then send
//-- can_receiver_.Start();
std::unique_ptr<std::thread> thread_;
MessageManager<SensorType> *pt_manager_;
//-- 解析conti_radar_conf.pb.txt
GetProtoFromFile(FLAGS_sensor_conf_file,&conti_radar_conf_);
can_type_ = conti_radar_conf_.can_conf().can_card_parameter();
can_client_ = can_factory->CreateCANClient(can_type_); //-- 这里以kvaser为例
//-- 这是很重要的报文解析类的管理类
ContiRadarMessageManager *pt_manager_ = new ContiRadarMessageManager();
Run in std::thread{
wile(IsRunning()){
can_client_->Recieve(&buf);
for (const auto &frame : buf) {
uint8_t len = frame.len;
uint32_t uid = frame.id;
//-- 报文解析类进行报文解析以及ROS的publish等操作
pt_manager_->Parse(uid, data, len);
}
}
}
}
流程图
- Apollo中对单独CAN设备的数据读取是单独开辟的进程,在/modules/drivers/radar/conti_radar/main.cc中的APOLLO_MAIN为程序入口。接下来调用了ContiRadarCanbus::Start()来启动CAN客户端模块以及CAN数据读取模块。
- CAN客户端:指Apollo中对各CAN转换设备(如kvaser)的驱动层,用来从各个转换设备中读取数据,并转换成相应的格式的过程。
- 数据读取:指对从CAN转换设备中读取到的一串8字节的字符串解析成不同字段的数值这么一个过程。在Apollo中,将数据解析后,会转换成proto的形式,借助ROS的topic-message机制,来Publish出去。关于ROS这块在下图中有所呈现,具体也可参考How to advertise and subscribe a topic
报文解析
- 既然是操作CAN设备,当然最重要的就是CAN报文解析。报文解析就是将CAN传上来的8字节串解析成不同字段数值。例如:
void ClusterListStatus600::Parse(const std::uint8_t* bytes, int32_t length, ContiRadar* conti_radar) const {
auto status = conti_radar->mutable_cluster_list_status();
status->set_near(near(bytes, length));
status->set_far(far(bytes, length));
status->set_meas_counter(meas_counter(bytes, length));
status->set_interface_version(interface_version(bytes, length));
auto counter = status->near() + status->far();
conti_radar->mutable_contiobs()->Reserve(counter);
}
这里是conti_radar下对msg_id为600的报文的解析。其中bytes是CAN收到的8字节,64位的报文消息。我们将其解析并给ContiRadar* conti_radar中的对应成员赋值。这里的ContiRadar是一种proto消息格式,可以理解为Apollo中的ROS消息格式,这在下文的proto部分有详细解释。
- 因为CAN报文的种类较多,Apollo抽象了一个协议解析的基类,放在/modules/drivers/canbus/can_comm/protocol_data.h内的class ProtocolData。我们在继承这个基类后,重写其virtual void Parse()用来做报文解析操作即可。一般,每个类型的CAN报文都新建一个对应的解析类,一般放在/modules/metoak_stereo/protocol/下,如下图就是conti_radar的相关解析类:
- 因为解析类太多,肯定还需要一个类来进行管理。Apollo也抽象了一个基类class MessageManager,放在/modules/drivers/canbus/can_comm/message_manager.h内。像conti_radar就实现了
class ContiRadarMessageManager : public MessageManager<ContiRadar>
来继承它。如果你想添加对某一种报文的解析,需要在/protocol/下实现了对应的解析类后,在ContiRadarMessageManager中注册对应的解析类,这个操作,我们一般放在其构造函数内,参考conti_radar:
ContiRadarMessageManager::ContiRadarMessageManager() {
AddRecvProtocolData<RadarState201, true>();
AddRecvProtocolData<ClusterListStatus600, true>();
AddRecvProtocolData<ClusterGeneralInfo701, true>();
AddRecvProtocolData<ClusterQualityInfo702, true>();
AddRecvProtocolData<ObjectExtendedInfo60D, true>();
AddRecvProtocolData<ObjectGeneralInfo60B, true>();
AddRecvProtocolData<ObjectListStatus60A, true>();
AddRecvProtocolData<ObjectQualityInfo60C, true>();
}
这里的AddRecvProtocolData就是一个解析类的注册过程,本质上将解析类push_back到一个vector,在vector的上层用map来根据msg_id做一个映射的管理,在解析的时候,根据msg_id,来find对应的解析类,然后调用其parse()函数进行解析。这一块是在ContiRadarMessageManager::GetMutableProtocolDataById()中做的。
整个的报文解析这一套可以理解为一个简单工厂模式。
- 上述就是对整个CAN设备相关模块的一个代码解析,包括整体流程分析和解析类的分析。下面且看如何添加一个新的CAN设备。
添加设备
- 对整体框架有了一个基本的了解后,接下来试试实操,看看如何在Apollo中添加一个新的CAN设备。在这里,我们以元橡(metoak)的双目模块为例,具体的讲一讲。
工作目录
- 要添加一个新的CAN设备,一般我们在/modules/drivers/下对应的目录添加文件夹,就在/modules/drivers下添加一个/stereo/metoak_stereo/的目录,这个就是我们的工作目录了。
- 这个目录下,你需要酌情新建一些如/conf/,/protocol/等目录,conf是存放配置文件,protocol用来存放对CAN报文解析的相关类,这里的protocol目录下的代码是我们的主要工作量所在。这里可以参考conti_radar的相关文件结构。
程序入口
- 在/metoak_stereo/下新建一个main.cc,程序入口当然是APOLLO_MAIN(MetoakStereoCanbus);
整体框架
- 上面的MetoakStereoCanbus我们放在metoak_stereo_canbus.h中声明,基本如下:
class MetoakStereoCanbus : public apollo::common::ApolloApp { apollo::common::Status Init() override; apollo::common::Status Start() override; void Stop() override; };
- 在这里,最重要的是重写上述三个函数,这也是整个模块的控制开关。
在上述的Init()中,是根据/conf/下的配置文件配置我们的can_client以及can_receiver。这里的具体写法可参考ContiRadarCanbus::Init()。对can_client和can_receiver我们都不需要修改其代码,只需要仿照现有代码调用接口即可,顺着Apollo的框架逻辑来。 - 这里需要注意,在初始化can_receiver时,传入的sensor_message_manager是一个很重要的部分——CAN报文解析。这一块也是我们的主要工作量所在,详见下文。
报文
- 我们最重要的工作将放在如下两点:
- 根据设备的CAN报文数据手册定义我们的proto。
- 根据数据手册,实现CAN报文的解析代码。
- proto
- 在这里,我们可参考conti_radar的proto,在*/modules/drivers/proto/conti_radar.proto*。这个proto是Apollo修改ROS后的消息格式,类同ROS中的*.msg*文件,这里需要我们根据设备的CAN数据手册,浓缩出一份proto格式。对proto的相关理解可参考Google Protocol Buffer 的使用和原理
- 报文解析
- 对CAN报文的解析代码,我们放在*/metoak_stereo/protocol/下,参考/modules/drivers/radar/conti_radar/protocol/下的文件,其文件名后面的数字就是当前解析类对应的报文帧ID,也就是msg_id。每一个文件对应一个解析类,每一个解析类解析一种报文。在前文我们讲到,解析类继承
class ProtocolData
,具体就不赘述了,参考conti_radar*吧。
- 对CAN报文的解析代码,我们放在*/metoak_stereo/protocol/下,参考/modules/drivers/radar/conti_radar/protocol/下的文件,其文件名后面的数字就是当前解析类对应的报文帧ID,也就是msg_id。每一个文件对应一个解析类,每一个解析类解析一种报文。在前文我们讲到,解析类继承
发布
- 解析了报文之后涉及到ROS的消息发布,之前说过,这里就不赘述了,参考How to advertise and subscribe a topic和How to add a new message
多个设备
- 如果我们有需要接入多个同一个类型的设备,例如多个大陆毫米波雷达,这时候有两种方式:
- 在同一个CAN口接入不同设备号的多个设备
- 例如conti_radar,如果要同一个CAN口接入多个conti_radar,可以提前将多个conti_radar刷成不同的设备号,然后在
ContiRadarMessageManager::Parse()
中,根据设备号进入不同的解析分支。
- 例如conti_radar,如果要同一个CAN口接入多个conti_radar,可以提前将多个conti_radar刷成不同的设备号,然后在
- 一个CAN口仅接入一个设备,也就是用多个CAN口来接入多个设备。