航底盤通信:CAN總線原理與實車調(diào)試實戰(zhàn))
做自主導(dǎo)航這些年底盤通信一直是我覺著最“容易翻車”又最不被人重視的一環(huán)。很多人在仿真里把路徑規(guī)劃調(diào)得很順一放到實車上就各種原地打轉(zhuǎn)、輪速跳變、電機抖動大概率罪魁禍首都藏在通信鏈路里。這個系列寫到第4篇專門把CAN通信拎出來說透——它是ROS小車從“大腦”到“四肢”之間的那條神經(jīng)系統(tǒng)跑通了它SLAM建圖、自主導(dǎo)航里那些跟里程計相關(guān)的算法才有發(fā)揮的空間。這篇文章適合兩類人一類是自己搭ROS小車、底盤還沒完全調(diào)通的極客和工程師另一類是看過不少ROS教程但一直沒搞明白cmd_vel話題到底是怎么變成車輪轉(zhuǎn)動的入門玩家。我盡量用實際項目里能直接用上的代碼和命令講少講虛的。1. 為什么自主導(dǎo)航的底盤通信繞不開CAN1.1 導(dǎo)航鏈路里CAN到底站在哪一層先捋一下自主導(dǎo)航系統(tǒng)的數(shù)據(jù)流傳感器激光雷達、IMU、相機把環(huán)境信息送給SLAM定位模塊定位結(jié)果送給全局/局部路徑規(guī)劃器規(guī)劃器算出速度指令也就是ROS里的geometry_msgs/Twist消息。這個速度指令會經(jīng)過底盤驅(qū)動節(jié)點最終變成電機控制信號。CAN通信就承擔了“底盤驅(qū)動節(jié)點到電機驅(qū)動器/編碼器”之間的物理鏈路。換句話說導(dǎo)航算法的每個決策最后都要壓進一幀CAN報文里發(fā)出去再從另一幀CAN報文里收回來做反饋。整個系統(tǒng)里這一層離硬件最近也最容易出幺蛾子。我以前見過不少搞視覺導(dǎo)航的團隊算法層面跑得很好但底盤通信用的是劣質(zhì)USB串口線加簡陋的私有協(xié)議跑幾分鐘就死一次機最后還懷疑是算法參數(shù)沒調(diào)好。實際上問題根本不在導(dǎo)航而在底層通信的可靠性。1.2 和串口比CAN贏在哪輸在哪現(xiàn)在DIY底盤最常用的通信方案就兩種串口UART和CAN總線。串口布線簡單代碼也簡單但有幾個硬傷半雙工通信大部分情況或者主從一問一答的模式帶寬浪費嚴重抗干擾能力弱電機大電流啟動時波形亂跳數(shù)據(jù)就花掉沒有錯誤檢測和自動重發(fā)機制如果不自己實現(xiàn)協(xié)議層的話丟一幀數(shù)據(jù)根本不知道。CAN總線就不一樣它是真正的多主機總線任何一個節(jié)點都可以主動發(fā)消息硬件層面自帶CRC校驗、位填充、錯誤識別和自動重發(fā)一幀數(shù)據(jù)在總線上傳歪了控制器會自動處理應(yīng)用層基本無感。這對實時控制來說是質(zhì)的差別。但CAN也不是沒短板。它最高速率也就1Mbps標準幀跟動輒千兆的以太網(wǎng)沒法比而且收發(fā)器、終端電阻、線束要求比串口講究調(diào)試門檻略高。這些代價換來的可靠性在實際機器人項目里是完全值得的。1.3 選型結(jié)論與適用邊界我的建議很明確凡是帶電機驅(qū)動、編碼器反饋的移動底盤只要控制器支持CAN外設(shè)首選CAN如果是攝像頭云臺、機械臂關(guān)節(jié)這種數(shù)據(jù)量不大但實時性高的場合CAN照樣合適只有極短距離、簡單低速場景才考慮串口湊合。后面講的所有內(nèi)容默認的硬件組合是主控比如Jetson或者x86工控機通過USB轉(zhuǎn)CAN適配器接一條1米以內(nèi)的CAN總線總線上掛著底盤STM32控制板、電機驅(qū)動器、編碼器節(jié)點。操作系統(tǒng)是Ubuntu 20.04/22.04 ROS1/ROS2內(nèi)核自帶SocketCAN驅(qū)動。這套組合是社區(qū)里最常見、也最容易被默認配置坑到的場景我會按真實翻車路徑來寫。2. 硬件準備從USB到總線的一點經(jīng)驗2.1 工具選型USB轉(zhuǎn)CAN適配器怎么挑USB轉(zhuǎn)CAN模塊市面上各種各樣的都有幾十塊的、幾百塊的、上千塊的。我的經(jīng)驗是調(diào)試階段可以買便宜的實車跑起來之后建議換帶隔離的。便宜的模塊多用CH340串口轉(zhuǎn)CAN芯片方案功能上沒啥問題但抗干擾和隔離性能弱電流大的時候容易把USB口都帶崩。實車跑起來之后電機驅(qū)動和電池電源的干擾是持續(xù)存在的沒有隔離的話輕則掉幀重則燒USB接口。我自己用的是CANable這種開源方案的板子固件切到slcan模式之后在Linux下會被識別成串口設(shè)備然后通過slcand命令掛載成can0接口如果固件切到gs_usb模式內(nèi)核會直接識別成一個原生SocketCAN設(shè)備。兩種模式我都用過實際效果差別不大但gs_usb模式更省心因為不需要額外的daemon。另一個容易被忽略的點是總線供電。CAN收發(fā)器需要3.3V或者5V供電有的USB適配器從USB取電有的需要外接電源一定要看說明書確認。我踩過一次坑模塊只接了USB線總線上的從設(shè)備供電倒是正常但收發(fā)器電平不對導(dǎo)致怎么調(diào)都收不到ACK折騰了兩天才發(fā)現(xiàn)是供電問題。2.2 Linux下把CAN接口“拉起來”不管用什么USB轉(zhuǎn)CAN模塊在Linux下最終要做的事都是把它注冊成一個網(wǎng)絡(luò)接口比如can0然后配置波特率、拉起來。這里用到的就是內(nèi)核的SocketCAN協(xié)議棧一套專門為CAN總線設(shè)計的網(wǎng)絡(luò)層實現(xiàn)。以gs_usb模式為例插上設(shè)備后先看看內(nèi)核有沒有識別lsusb # 應(yīng)該能看到類似 Gs_usb 的設(shè)備 dmesg | tail -20 # 能看到創(chuàng)建了新的網(wǎng)絡(luò)接口的日志比如 can0如果設(shè)備被識別成了ttyUSB那就是slcan模式需要先把固件切到gs_usb模式或者直接用slcand掛載sudo slcand -o -c -s6 0x01 -d /dev/ttyUSB0 can0-s6表示波特率500kbps不同的s參數(shù)對應(yīng)不同的速率具體看slcand的文檔。原生SocketCAN接口的話配置命令是# 設(shè)置500kbps波特率 sudo ip link set can0 down sudo ip link set can0 type can bitrate 500000 sudo ip link set can0 up注意順序先down再改參數(shù)再up改參數(shù)之前如果接口沒down會直接報錯。另外每次重啟之后這些配置都會丟所以建議把這幾條命令寫進一個systemd service或者啟動腳本里別手動敲因為每次開機都手動配置太容易忘。檢查接口狀態(tài)可以用ip -details -statistics link show can0這個命令能看到波特率、環(huán)回模式、錯誤計數(shù)、發(fā)送接收統(tǒng)計排查問題的時候特別好用。如果看到state UP說明接口正常拉起如果看到state DOWN或者BUS-OFF就要排查了。2.3 終端電阻和線纜布線的老生常談CAN總線規(guī)范要求在總線兩端各接一個120歐姆終端電阻。這個細節(jié)我在項目里幫人排查過無數(shù)次十次里有七次通信異常其實是終端電阻的問題。具體來說如果總線上只有兩個節(jié)點比如一個USB適配器、一個底盤控制板那就在兩邊各開一個120歐如果總線上有三個以上節(jié)點只在物理最遠的兩個節(jié)點上各接一個120歐中間的節(jié)點不要接。判斷方法很簡單斷電狀態(tài)下用萬用表量CAN_H和CAN_L之間的電阻正常應(yīng)該是60歐左右兩個120歐并聯(lián)如果量到120歐說明只接了一端如果量到40歐以下說明某個節(jié)點重復(fù)接了終端電阻。線纜方面CAN總線用雙絞線不要太細至少24AWG以上絞距盡量均勻。布線要遠離電機電源線和PWM信號線別圖省事捆在一起??偩€長度超過幾米的話絞合和質(zhì)量就更關(guān)鍵不然反射會嚴重影響波形質(zhì)量高速率下尤其明顯。3. 通信協(xié)議設(shè)計直接決定調(diào)試效率3.1 一幀CAN報文到底長什么樣搞CAN通信第一步就是理解幀結(jié)構(gòu)。標準CAN 2.0A幀由這么幾部分組成11位標識符ID決定幀的優(yōu)先級和用途數(shù)據(jù)長度碼DLC表示數(shù)據(jù)段有多少字節(jié)最多8字節(jié)最多8字節(jié)的數(shù)據(jù)段各種校驗位和位填充機制由硬件自動處理但我們要知道它能自動發(fā)現(xiàn)錯誤。11位ID是個很寶貴的設(shè)計資源怎么分配ID、怎么約定數(shù)據(jù)段格式直接決定后期調(diào)試的復(fù)雜度。有些現(xiàn)成協(xié)議比如CANopenID分配和對象字典都規(guī)范化了但很多做機器人底盤的人還是喜歡自定義一套簡單協(xié)議因為CANopen的學(xué)習(xí)成本和使用復(fù)雜度對DIY項目來說有點過重。我自己是自定義協(xié)議派不是說CANopen不好而是很多底盤就那么幾個電機、幾個傳感器自定義協(xié)議加上簡單的校驗代碼比套CANopen框架清爽得多調(diào)試時出問題了也更好定位。3.2 我們用的報文分配與ID規(guī)劃我常用的一套底盤協(xié)議分配方式供參考方向幀ID名稱數(shù)據(jù)段內(nèi)容上位機→底盤0x101速度控制指令[0xAA] [使能] [左輪速度高8位] [左輪速度低8位] [右輪速度高8位] [右輪速度低8位] [校驗] [0x55]底盤→上位機0x181底盤狀態(tài)反饋[模式] [左輪編碼器高8位] [左輪編碼器低8位] [右輪高8位] [右輪低8位] [電壓] [校驗] [0x55]底盤→上位機0x182里程計原始數(shù)據(jù)[左輪累計脈沖高16位...] [右輪累計脈沖...] [...]幀ID選0x101、0x181這類數(shù)字是因為它們在11位ID范圍內(nèi)數(shù)值上二進制干擾少調(diào)試時一眼能看明白。0x101控制幀用0xAA 0x55做幀頭和幀尾中間第7字節(jié)做校驗可以過濾掉大部分雜波。校驗算法我習(xí)慣用最簡單的異或把第0到第5字節(jié)逐字節(jié)異或結(jié)果放到第6字節(jié)。數(shù)據(jù)段長度固定8字節(jié)雖然浪費了一兩個字節(jié)但解析簡單、不需要動態(tài)長度判斷對MCU代碼來說少很多分支。速度值用有符號int16表示單位是毫米每秒。比如要讓左輪以0.5米/秒的速度轉(zhuǎn)那就LeftSpeed 500高字節(jié)是0x01低字節(jié)是0xF4。有符號的好處是正負天然表示正反轉(zhuǎn)不用額外定義方向位。3.3 波特率計算從時鐘樹算到總線波特率是CAN通信最容易出問題的地方兩邊不一致表現(xiàn)就是總線上Error Frame滿天飛數(shù)據(jù)完全不通。所以這塊要徹底講清楚。以STM32F103為例CAN外設(shè)掛在APB1總線上PCLK1最高36MHzSYSCLK72MHz時APB1預(yù)分頻2。CAN波特率由三個參數(shù)決定預(yù)分頻器BRP、時間段BS1、時間段BS2外加一個固定的同步段SJW。公式是波特率 PCLK1 / (BRP * (1 BS1 BS2))我常用500kbps配置是BRP4BS19BS28。代入算一下波特率 36000000 / (4 * (1 9 8)) 36000000 / 72 500000Hz這個配置的采樣點位置大約是(1 9) / (1 9 8) 10 / 18 ≈ 83.3%在CAN總線里是比較推薦的采樣點位置能容忍一定程度的線纜傳播延遲和節(jié)點時鐘偏差。Linux側(cè)SocketCAN設(shè)置波特率就是開頭寫的bitrate 500000。兩邊必須嚴格一致。有些USB轉(zhuǎn)CAN模塊的默認波特率是250kbps或者1Mbps如果底盤STM32側(cè)燒的是500kbps固件兩邊對不上一上電就是一堆錯誤幀。排查這個問題最快的辦法是用示波器看CAN_H和CAN_L之間的波形但沒示波器的話就先確認兩側(cè)配置數(shù)值別想當然。3.4 協(xié)議定義寫在哪DBC還是頭文件協(xié)議定義的管理方式不同項目做法不一樣。汽車行業(yè)標準做法是寫DBC文件然后用工具生成代碼機器人開源社區(qū)更常見的是直接在代碼里用結(jié)構(gòu)體和宏定義。我自己的項目兩種都試過DBC文件適合有現(xiàn)成工具鏈的情況比如用PCAN的CANdb或者用Python的cantools庫解析DBC開發(fā)和驗證協(xié)議時很直觀但DBC的語法本身也是學(xué)習(xí)成本而且很多MCU側(cè)的自動生成代碼質(zhì)量一般最后還是手工微調(diào)。個人項目我更推薦直接在頭文件里定義清晰的結(jié)構(gòu)體然后用Python腳本在PC端做同樣結(jié)構(gòu)體的解析兩邊共用同一份協(xié)議文檔哪怕就是Markdown表格就行。關(guān)鍵在于文檔要寫清楚每個幀的ID、方向、DLC、每個字節(jié)的含義、單位、范圍、校驗方式。不然過兩個月回來自己都看不懂自己寫的協(xié)議。4. 代碼走讀從Twist到輪速再把狀態(tài)傳回來4.1 ROS側(cè)節(jié)點話題數(shù)據(jù)變成CAN幀在ROS側(cè)底盤驅(qū)動節(jié)點的核心工作就是訂閱cmd_vel把線速度和角速度拆分成左右輪速度然后打包成CAN幀發(fā)出去。先看一個簡化的拆分公式// 雙輪差速底盤輪距W輪半徑R v_left v - w * W / 2 v_right v w * W / 2 left_rpm v_left / (2 * PI * R) * 60 right_rpm v_right / (2 * PI * R) * 60把左右輪的速度值轉(zhuǎn)換成毫米每秒填進0x101幀的數(shù)據(jù)段用SocketCAN的send接口發(fā)出去。Linux下發(fā)送CAN幀的標準方法是創(chuàng)建RAW socket#include linux/can.h #include linux/can/raw.h #include sys/socket.h #include net/if.h int s; struct sockaddr_can addr; struct can_frame frame; s socket(PF_CAN, SOCK_RAW, CAN_RAW); strcpy(ifr.ifr_name, can0); ioctl(s, SIOCGIFINDEX, ifr); addr.can_family AF_CAN; addr.can_ifindex ifr.ifr_ifindex; bind(s, (struct sockaddr *)addr, sizeof(addr)); // 填充一幀 frame.can_id 0x101; frame.can_dlc 8; frame.data[0] 0xAA; frame.data[1] 0x01; // enable frame.data[2] (left_speed_mm 8) 0xFF; frame.data[3] left_speed_mm 0xFF; frame.data[4] (right_speed_mm 8) 0xFF; frame.data[5] right_speed_mm 0xFF; frame.data[6] check_sum; frame.data[7] 0x55; write(s, frame, sizeof(frame));注意在ROS2里cmd_vel的發(fā)布頻率通常由上層規(guī)劃器決定但底盤驅(qū)動節(jié)點最好自己做頻率控制。我的做法是單獨起一個控制線程固定20ms周期發(fā)送即使沒有新的cmd_vel消息也重復(fù)發(fā)送最后一幀指令或者發(fā)送零速保證底盤不因為收不到指令而失控。4.2 底盤側(cè)CAN幀變成電機PWMMCU側(cè)我以STM32為例的核心邏輯寫在CAN接收中斷里void CAN1_RX0_IRQHandler(void) { CanRxMsg RxMsg; CAN_Receive(CAN1, CAN_FIFO0, RxMsg); if (RxMsg.StdId 0x101) { if (RxMsg.Data[0] 0xAA RxMsg.Data[7] 0x55) { // 校驗 uint8_t sum 0; for (int i 0; i 6; i) sum ^ RxMsg.Data[i]; if (sum RxMsg.Data[6]) { int16_t left (RxMsg.Data[2] 8) | RxMsg.Data[3]; int16_t right (RxMsg.Data[4] 8) | RxMsg.Data[5]; // 調(diào)用PID控制器輸出PWM占空比 } } } }收到的速度指令是目標速度要讓輪子真的轉(zhuǎn)到這個速度必須在MCU里做閉環(huán)控制。我用的是增量式PID周期10ms。底盤驅(qū)動板從編碼器讀到的實際轉(zhuǎn)速作為反饋和目標速度做差然后PID輸出疊加到PWM占空比上。扭矩大、負載變化劇烈的場景限幅和積分分離非常關(guān)鍵不然起步會猛沖、停機會有頓挫。4.3 閉環(huán)反饋輪速怎么回到導(dǎo)航節(jié)點導(dǎo)航算法需要里程計里程計原始數(shù)據(jù)是輪速。所以底盤MCU要把編碼器數(shù)據(jù)通過CAN幀發(fā)回上位機上位機驅(qū)動節(jié)點再計算位姿變化發(fā)布odom話題。MCU側(cè)定時器每50ms讀一次編碼器累計脈沖左右輪各4字節(jié)int32連同電壓、故障狀態(tài)一起打包用0x182幀發(fā)給上位機。上位機解析之后結(jié)合上一幀的時間間隔計算瞬時線速度和角速度v (left_delta right_delta) * wheel_circumference / 2 / dt w (right_delta - left_delta) / wheel_base / dt然后調(diào)用tf變換發(fā)布odom→base_link的位姿。這里有個關(guān)鍵點里程計角速度積分對輪距誤差極其敏感。輪距標定不準小車在導(dǎo)航模式下就會走著走著偏航循環(huán)打轉(zhuǎn)。我一般用“右轉(zhuǎn)5圈、左轉(zhuǎn)5圈回到起點”的辦法實測標定輪距誤差能控制在毫米級。4.4 超時保護和掉線處理總線通信不是永遠可靠的所以協(xié)議里一定要有超時保護。我的方案是MCU側(cè)設(shè)置一個軟看門狗每隔100ms檢查是否收到0x101幀如果連續(xù)10個周期沒收到就把目標速度清零電機急停。上位機側(cè)也是同理如果200ms沒收到0x181或0x182幀就把底盤標記為lost在車體面板亮警告燈導(dǎo)航節(jié)點直接進入STOP狀態(tài)。別小看這個設(shè)計。實車運行時USB線松動、USB轉(zhuǎn)CAN模塊固件卡死、上位機負載過高導(dǎo)致調(diào)度延遲都可能讓幾幀數(shù)據(jù)遲遲到不了。如果沒有超時保護小車會繼續(xù)執(zhí)行最后的指令直接撞墻。我自己吃過這個虧所以才把超時保護放在協(xié)議里而不是放在導(dǎo)航算法里。5. 調(diào)試實錄那幾次讓人想砸鍵盤的故障5.1 現(xiàn)象一can0起不來或者起來之后馬上被內(nèi)核拉黑表現(xiàn)是執(zhí)行sudo ip link set can0 up type can bitrate 500000之后立刻報錯或者起來之后ip -details link show can0里能看到大量error幀再過一會接口自己變成BUS-OFF。排查思路先看dmesg有沒有內(nèi)核報錯比如“failed to restart device”之類的日志。最常見的兩個原因一是波特率不匹配總線上其他節(jié)點已經(jīng)以別的速率在跑新節(jié)點一上去就把總線帶亂了二是終端電阻缺失信號反射嚴重錯誤幀率居高不下。解決方式就是對照第2.3節(jié)和第3.3節(jié)的內(nèi)容逐項檢查。另一個容易踩的坑是某些USB轉(zhuǎn)CAN適配器在電腦休眠喚醒之后會掉線can0接口消失。如果機器人是長時間自動運行建議在驅(qū)動節(jié)點里做個心跳監(jiān)測發(fā)現(xiàn)can0不存在就把整個適配器重新初始化。5.2 現(xiàn)象二電機抖動速度忽大忽小車速在低速時一頓一頓的像有人在一下一下踩油門。這個現(xiàn)象我排查過一次最后發(fā)現(xiàn)是PID周期和CAN指令周期“打架”了。底盤MCU的PID控制周期是10ms但上位機的控制指令周期是50ms因為規(guī)劃頻率低。PID每10ms跑一次但目標值只有每50ms才更新一次于是目標值斷崖式跳躍每次跳變PID都會超調(diào)表現(xiàn)出來就是抖動。解決辦法有三個任選一是上位機把指令周期縮短到和PID周期一致20ms以內(nèi)二是MCU側(cè)做速度指令平滑濾波比如一階低通把階躍變化抹平三是PID參數(shù)調(diào)松一點降低超調(diào)。我實際用的是第二種一階濾波系數(shù)0.6左右起步和剎車都柔和很多。5.3 現(xiàn)象三里程計漂移導(dǎo)航時原地轉(zhuǎn)圈導(dǎo)航模式下小車會不斷偏航最后繞圈或者直接撞墻。除了輪距標定的問題還有一個隱蔽原因是編碼器脈沖解析有誤。比如MCU側(cè)用的是定時器正交編碼器模式但代碼里配置了錯誤的計數(shù)方向左右輪都往一個方向偏里程積分出來就是一個大弧線。排查辦法是用手推車走一條直線打印左右輪累計脈沖數(shù)如果兩個數(shù)差異很大說明編碼器安裝或解析有問題。還有一個容易被忽視的點編碼器脈沖數(shù)除以輪速采樣周期換算出來的線速度在低轉(zhuǎn)速時因為量化誤差大會非常毛糙。低速時里程計不準是正常的但導(dǎo)航算法不認這個說法所以上位機側(cè)在發(fā)布odom時最好對速度做一次濾波或者干脆用高分辨率編碼器別為了省錢用那種每圈只有幾十個脈沖的霍爾編碼器。5.4 現(xiàn)象四高負載丟幀導(dǎo)航突然原地轉(zhuǎn)圈有一次我在Jetson上同時跑激光雷達建圖、目標檢測算法和導(dǎo)航規(guī)劃結(jié)果CAN通信頻繁丟幀底盤偶發(fā)抽搐。用candump一看總線上幾乎每隔幾秒就有一個Error Frame。這是典型的USB傳輸延遲和CPU調(diào)度延遲疊加導(dǎo)致的問題。SocketCAN的驅(qū)動在應(yīng)用層收不到數(shù)據(jù)時會丟包而Jetson上實時性本來就不強高負載時用戶態(tài)程序遲遲拿不到數(shù)據(jù)CAN控制器FIFO就溢出了。對癥手段把底盤驅(qū)動節(jié)點設(shè)為高優(yōu)先級線程減少其他進程爭搶USB轉(zhuǎn)CAN適配器換用實時性能更好的方案如果真的需要超大負載并行考慮把底盤通信挪到一個專門的小控制板比如ESP32或者STM32上通過以太網(wǎng)和上位機通信讓CAN總線只在小控制板下面轉(zhuǎn)這就屬性能架構(gòu)調(diào)整的范疇了。5.5 問題排查速查表現(xiàn)象優(yōu)先檢查項大概率原因can0無法updmesg、內(nèi)核對設(shè)備識別狀態(tài)USB適配器固件異常或供電不足總線上大量error幀波特率一致性、終端電阻速率不匹配或120歐電阻缺失電機抖動PID周期與指令周期指令頻率和PID頻率差距過大里程計漂移編碼器方向、脈沖數(shù)計數(shù)方向配置錯誤或輪距不準高負載丟幀CPU負載、USB調(diào)度驅(qū)動線程優(yōu)先級過低或USB鏈路瓶頸CAN控制器BUS-OFF總線短路或強干擾線纜布線問題或隔離缺失這套表里的每一項我都在實車上踩過坑有些甚至反復(fù)踩了好幾次。最后分享一個最不起眼但最有效的排查技巧調(diào)試CAN通信時先把上位機算法全停掉只留一個candump手動用cansend給底盤發(fā)速度指令。如果這一步能穩(wěn)定控制底盤問題就出在上層如果這一步都做不通那就是底層硬件和配置的鍋。用這種“切層排查法”代替滿系統(tǒng)瞎猜定位問題的速度快十倍。做自主導(dǎo)航這幾年越來越覺得很多玄學(xué)問題到最后都是物理和配置問題——先把通信這層夯實了導(dǎo)航算法才有底氣在實車上跑起來。