Apollo如何新增一個新的CAN裝置
阿新 • • 發佈:2019-02-14
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介面。而
- 本文將大體分為下面兩部分:
- 以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。每一個檔案對應一個解析類,每一個解析類解析一種報文。在前文我們講到,解析類繼承
釋出
多個裝置
- 如果我們有需要接入多個同一個型別的裝置,例如多個大陸毫米波雷達,這時候有兩種方式:
- 在同一個CAN口接入不同裝置號的多個裝置
- 例如conti_radar,如果要同一個CAN口接入多個conti_radar,可以提前將多個conti_radar刷成不同的裝置號,然後在
ContiRadarMessageManager::Parse()
中,根據裝置號進入不同的解析分支。
- 例如conti_radar,如果要同一個CAN口接入多個conti_radar,可以提前將多個conti_radar刷成不同的裝置號,然後在
- 一個CAN口僅接入一個裝置,也就是用多個CAN口來接入多個裝置。