1. 程式人生 > >Apollo如何新增一個新的CAN裝置

Apollo如何新增一個新的CAN裝置

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口即可。
  • 本文將大體分為下面兩部分:
    1. 以Apollo/conti_radar為例,聊一下Apollo中CAN裝置的相關流程及程式碼。
    2. 以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資料讀取模組。
  1. CAN客戶端:指Apollo中對各CAN轉換裝置(如kvaser)的驅動層,用來從各個轉換裝置中讀取資料,並轉換成相應的格式的過程。
  2. 資料讀取:指對從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*吧。
釋出
多個裝置
  • 如果我們有需要接入多個同一個型別的裝置,例如多個大陸毫米波雷達,這時候有兩種方式:
  1. 在同一個CAN口接入不同裝置號的多個裝置
    • 例如conti_radar,如果要同一個CAN口接入多個conti_radar,可以提前將多個conti_radar刷成不同的裝置號,然後在ContiRadarMessageManager::Parse()中,根據裝置號進入不同的解析分支。
  2. 一個CAN口僅接入一個裝置,也就是用多個CAN口來接入多個裝置。