尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

自主导航小车CAN通信实战:STM32驱动、DBC解析与ROS接入指南

自主导航小车CAN通信实战:STM32驱动、DBC解析与ROS接入指南 干这行最常被问的一个问题就是导航算法算出了速度、角速度到底怎么落到轮子上我早期做ROS小车的时候也踩过这个坑——SLAM建图、路径规划、自主导航仿真都跑通了结果一接真车底盘控制指令下不去电机不转查了半天发现是通信链路的问题。从那时候起我就意识到自主导航这套系统里底盘通信和上层算法一样重要。而提到真实机器人底盘通信绕不开的就是CAN通信。这篇是这个系列的第4篇专门讲我在自主导航小车里接CAN通信的实战经验。内容覆盖CAN协议本身、STM32端驱动怎么写、DBC文件怎么用、以及ROS端怎么把总线数据接入导航链路。适合正在做ROS小车自主导航、想搞懂底盘通信的开发者也适合被“CAN突然连不上”折磨的人。我尽量用大白话把每个环节讲透代码和踩坑记录都放在后面你可以直接照着抄。1. 自主导航小车里CAN到底干的是什么活1.1 控制链路里CAN的位置先看一条完整的自主导航控制链路激光雷达/相机采集环境信息跑SLAM建图然后路径规划算法算出一条无碰撞路径再转换成底盘速度指令最后由电机驱动执行。这条链路里上层计算用的基本都是ROS、以太网、USB这类接口但到了底盘内部尤其是多个电机控制器、电池管理系统、遥控器接收机、甚至举升机构之间CAN总线几乎是标配。为什么是CAN我举一个真实场景你的导航节点发了一帧“左转、速度0.5m/s”的指令这条指令要同时到达左右两个电机控制器。如果用独立串口分别连那得接两根线、两个串口节点还得处理两边不同步的问题。而CAN本身就是一根差分总线双线并接所有节点控制器A和控制器B同时收到同一帧数据天然同步。而且CAN是广播式多主总线任何一个节点都能主动发数据不依赖主机轮询。还有一个更核心的优势实时性。CAN的仲裁机制允许低ID帧在总线冲突时毫秒级抢占总线这意味着你底盘上的急停指令、电机故障帧可以靠ID优先级插队而不是排队等主机问。对于自主导航这种对安全性和反馈质量有要求的场景这个特性非常要紧。1.2 CAN和串口、以太网、485的取舍我见过不少同学纠结一个问题到底用串口、RS485、以太网还是CAN我自己的选型经验是这么看的直接上对比表。通信方式最大波特率常见多主能力抗干扰布线成本典型场景串口UART115200-921600不支持一主一从较弱低板间调试、传感器直连RS48510Mbps短距离半双工靠主机控制较强差分线中工业现场、多点采集CAN1Mbps经典CAN天然多主仲裁强差分线中车载、机器人底盘、电机控制以太网100M/1000M支持需协议栈中较高上位机、激光雷达、视觉CAN不是最快的但它在实时性、多主能力、抗干扰这三点上达到了一个很好的平衡。对于底盘这种节点不多、数据量不大但实时性要求高的场景CAN几乎是行业默认答案。串口便宜但抗干扰和扩展性差RS485虽然也是差分但半双工多主需要主机调度通信周期拉长到了自主导航需要快速闭环的时候就有点吃力。以太网当然好但成本高、协议栈复杂而且对实时性要求严格的场合还得搞时间同步远没有CAN这么皮实。说白了CAN在这个系统里的角色就是“底盘神经系统”。它不负责算负责把大脑的决策准时、可靠地传给肌肉再把肌肉的状态反馈给大脑。2. 硬件选型与接线不要在这上面栽跟头2.1 STM32 CAN外设与收发器选型搞自主导航底盘MCU这边最常见的选择就是STM32。F103上带的是bxCAN外设支持CAN 2.0A/BF405、F427这些带FDCAN支持更高带宽。大多数电机控制器走的还是经典CAN所以我这边用的就是F103的bxCAN稳定性足够资料也多。但STM32内部集成的是CAN控制器不是收发器。控制器出来的是TXD/RXD这种数字信号真正上总线把差分电平拉起来的是外部收发器芯片最常用的几款是TJA10505V供电经典工规收发器适合12V/24V车辆电源系统节点多、线长的场景表现稳。SN65HVD2303.3V供电和很多3.3V单片机电平直连不用电平转换适合开发板和小型底盘我用得最多的就是这款。TJA1051TJA1050的改进版带待机模式低功耗设计里好用。选型时重点看供电电压和你STM32系统电压是否匹配。如果MCU是3.3V收发器是5V的TJA1050TXD那根线不能直连中间需要电平转换否则长期跑会有隐患。我自己常用的方案是SN65HVD2303.3V直连Pico、STM32、ESP32都能带非常省事。还有一个容易忽略的点CAN控制器和收发器之间最好串联一个小电阻比如22Ω串在TXD路径上可以抑制振铃和过冲。这个对高速CAN尤其重要1Mbps速率下波形要是毛刺大节点靠近总线中间很容易采样出错。2.2 终端电阻与总线上那几个常见坑CAN物理层有一条铁律总线两端各接一个120Ω终端电阻。这个电阻的作用是把总线阻抗匹配到大约60Ω抑制信号反射。很多人做实验时随手把两个120Ω都插在一个节点上另外一端不接或者干脆一个都不接结果就是通信时好时坏、距离一长就丢帧、波特率一高就报错。我建议拿万用表在总线两端量一下CANH和CANL之间的阻抗正常应该是60Ω左右。如果量出来是120Ω说明只接了一个终端电阻如果是40Ω以下说明某个节点又额外并联了电阻负载过重信号摆幅不够。接线还有几个细节CANH接CANHCANL接CANL不要接反。两根线同时互换倒没事但只接反一根总线直接罢工。一定要接CAN_GND或参考地不要指望只靠CANH/CANL两根差分线就能长距离通信。收发器共模范围有限两个节点电位差太大隐性电平会漂通信会变得极不稳定。线的粗细尽量统一双绞线最佳没有条件也至少把CANH/CANL绞在一起能明显降低外部干扰。节点数量和线长也说说经验值经典CAN 500kbps速率下我跑过大概20米、8个节点的底盘线束稳定。再长了建议降到250kbps或者上CAN FD不然信号衰减会给你颜色看。我之前在一个项目上把线束走到30米500kbps直接丢帧改成250k就正常了。3. 报文格式与ID规划总线上的“规矩”3.1 标准帧与扩展帧怎么选CAN协议帧格式很多人觉得枯燥但这是通信的基础我尽量讲得形象一点。你可以把总线想象成一条“双向单车道”谁都能上车发言但遇到两个人同时讲话时谁的ID数值小谁先走。这就是CSMA/CA非破坏性仲裁机制。CAN 2.0标准帧的结构大致是帧起始、仲裁段带11位ID、控制段含DLC数据长度、数据段0-8字节、CRC、ACK、EOF。扩展帧则在仲裁段多了18位扩展ID总共29位ID。选哪个我自己的建议是能标准帧就标准帧。理由很简单标准帧帧头短总线利用率高效率更好而且绝大多数底盘设备只支持标准帧你定义扩展ID反而可能兼容不了。只有在ID不够用、或者要兼容某些特殊设备时才考虑扩展帧。数据段最多8字节这个也强调一下。有些新手愣是要在CAN里发100字节的结构体那思路就不对CAN设计出来就是跑控制类短消息的大数据走以太网或者串口别硬塞。3.2 手写一份底盘通信ID表真正到写代码之前先要把总线上跑哪些报文、ID是多少、谁发谁收、数据怎么定义全部定下来。这一步做扎实了后面省一半的调试时间。我以自己一套典型的自主导航差速底盘为例通信逻辑大致如下报文ID标准帧报文名发送方接收方周期内容0x100VCU_CMD主控STM32电机控制器20ms模式、转向、速度指令0x110VCU_STATUS主控STM32电机控制器100ms急停、使能、故障码0x181MOTOR1_FB左电机控制器主控STM3210ms实测速度、电流、状态0x182MOTOR2_FB右电机控制器主控STM3210ms实测速度、电流、状态0x201BATTERY_INFOBMS电池管理主控STM32500ms电压、SOC、温度0x301REMOTE_CTRL遥控接收机主控STM3220ms摇杆油门、转向通道ID规划原则有两个一是“重要且紧急的报文ID小一点”比如0x100控制指令、0x110急停状态ID越小优先级越高保证延时最低二是周期性反馈类的ID分一个区间比如0x180到0x1FF都留给定速反馈三是每个设备尽量只占固定段位BMS 0x200、遥控0x300后续再扩设备直接按区段加就行不用重构表格。这套ID表你可以直接参考也可以按自己的设备手册改。但记住一点ID表写进程序之前先写进文档最好再做成DBC文件这个下面展开别只在代码里裸写数字。不然过两个月你自己都看不懂那一堆硬编码0x100、0x181到底在传什么。3.3 DBC文件详解为什么不是写文档而是写dbc说到DBC很多人听过但没真正理解。DBC不是一套通信协议而是对CAN报文中信号的一种描述文件一行行文本定义了“哪个报文、哪个字节、哪些位、代表什么物理量、单位是什么、缩放系数多少”。它相当于给底层总线数据套了一层“翻译层”没有这层翻译你看到0x181数据场88 13 00 00 00 00 00 00根本不知道是速度13m/s还是电流88A。我常用的一套底盘DBC片段长这样BO_ 256 VCU_CMD: 8 Vector__XXX SG_ DrvDir : 0|11 (1,0) [0|1] Receiver SG_ DrvSpd : 1|121 (0.1,0) [0|409.5] m/s Receiver SG_ DrvMode : 16|31 (1,0) [0|5] Receiver BO_ 385 MOTOR1_FB: 8 Vector__XXX SG_ M1_Speed : 0|161 (0.01,0) [0|655.35] m/s Receiver SG_ M1_Current : 16|161- (0.1,0) [0|3276.7] A Receiver解释一下这些字段BO_定义一条报文256是十进制ID对应0x100VCU_CMD是报文名8是数据场字节数。SG_定义信号DrvDir是信号名0|11表示从第0位开始占1位1表示小端字节序整数表示无符号数(1,0)是缩放系数和偏移量[0|1]是物理范围单位是空字符串。比如DrvSpd1|121的意思是信号从第1位开始长度12位小端序无符号整数扩大0.1倍后才是物理速度。那么原始值350翻译出来就是35.0单位是0.1m/s。为什么要用缩放系数而不是直接发35因为很多电机控制器的内部速度值是整数只有乘上0.1或0.01才能表达出小数点精度。如果不做这层映射你用浮点在CAN里传一个速度包8字节都塞不下而且浮点字节序在不同芯片间还有兼容性问题。你自己写DBC的时候用文本编辑器就能做也可以用canoediter这类工具图形化操作。我更喜欢用文本直接写因为可控性强改起来快。写好DBC之后就可以用cantoolsPython库直接加载解析后面ROS节点里读报文、发指令都非常顺手。4. STM32端CAN驱动一步一步写明白4.1 波特率怎么算500kbps背后的时钟数学STM32端跟CAN打交道第一步就是初始化外设并配置波特率。这一步看似简单但波特率不对是“突然连不上”的最大原因之一。很多人直接复制别人代码Prescaler填个8TimeSeg1填个6结果换了一块主频不同的板子就废了。CAN波特率公式是Baud CAN外设时钟 / (Prescaler × (1 TimeSeg1 TimeSeg2))比如STM32F103APB1最大36MHzCAN外设时钟默认就是36MHz。要配500kbps可以取Prescaler8TimeSeg16TimeSeg22代入36,000,000 / (8 × (1 6 2)) 36M / (8 × 9) 500kbps这里1TimeSeg1TimeSeg2一共9个时间量子其中同步段占1个传播段相位段1TimeSeg1占6个相位段2TimeSeg2占2个。采样点位置在(16)/9≈77.8%采样点靠后一些对长线传输更友好。如果你跑250kbps一般可以把Prescaler改成16其他不变正好250k。我调试时习惯在初始化里把CanTxHeaderTypeDef和CanRxHeaderTypeDef的ID类型、DLC都先打印出来看一下确认配置进寄存器了再接总线。HAL库里的配置代码大致是CAN_HandleTypeDef hcan; hcan.Instance CAN1; hcan.Init.Prescaler 8; hcan.Init.Mode CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth CAN_SJW_1TQ; hcan.Init.TimeSeg1 CAN_BS1_6TQ; hcan.Init.TimeSeg2 CAN_BS2_2TQ; hcan.Init.TimeTriggeredMode DISABLE; hcan.Init.AutoBusOff ENABLE; hcan.Init.AutoWakeUp ENABLE; hcan.Init.AutoRetransmission ENABLE; hcan.Init.ReceiveFifoLocked DISABLE; hcan.Init.TransmitFifoPriority DISABLE; HAL_CAN_Init(hcan); HAL_CAN_Start(hcan); HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING);这里有两个容易被忽略的小细节。一个是AutoBusOff我通常打开总线错误长时间累积导致进入Bus Off状态时硬件能自动恢复如果你不开进入Bus Off后必须软件干预否则通信直接“死掉”。另一个是AutoRetransmission打开后发送失败会自动重发虽然会占用总线但在控制类底盘里一帧指令丢了可能意味着几十毫秒的失控所以宁可让它重发也不要静默丢失。4.2 初始化与发送接收代码配置完初始化接下来就是发送和接收。发送的核心是配置发送邮箱。CAN_TxHeaderTypeDef txHeader; uint8_t txData[8] {0}; txHeader.IDE CAN_ID_STD; txHeader.StdId 0x100; txHeader.DLC 8; txHeader.RTR CAN_RTR_DATA; txHeader.TransmitGlobalTime DISABLE; uint32_t mailbox; HAL_CAN_AddTxMessage(hcan, txHeader, txData, mailbox);HAL_CAN_AddTxMessage会把报文放进3个发送邮箱之一马上开始发送。这里有个坑如果你连续高频发送3个邮箱都满了函数会返回HAL_BUSY而你丢掉了当前帧。我一般会在调用前检查一下发送邮箱的空闲状态或者用环形队列缓存待发指令避免导航指令在高峰期被丢弃。底盘控制这种东西你可以容忍晚几毫秒但不能容忍指令直接消失。接收用中断比较省心。比如我想收电机反馈报文0x181就在回调里解析void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData); if (rxHeader.StdId 0x181) { int16_t rawSpeed (rxData[0] | (rxData[1] 8)); float actualSpeed rawSpeed * 0.01f; // 存入全局变量供速度闭环使用 } }注意0x181的数据是小端序低字节在前所以speed data[0] | (data[1] 8)而不是反过来。DBC里定义的小端序信号解码方式就是这样的。4.3 发送失败、邮箱满、中断接收这些细节实际开发中我遇到最多的问题之一是中断回调里处理耗时太长导致后续CAN帧来不及接收。CAN接收FIFO深度只有3帧你在回调里做大量浮点运算、加日志、甚至调delayFIFO就会溢出帧就丢了。我的经验是回调里只做拷贝和最小化解析把真正的业务逻辑放到主循环或RTOS任务里处理。另一个常见问题是发送失败后数据对不齐。比如你期望底盘20ms收一次指令结果你代码里一个分支没走到10ms发一次、另一个分支30ms发一次电机会忽快忽慢甚至抖震。这种情况不是波特率问题是控制周期不稳。我自己做完发送函数后会在总线侧用CAN分析仪统计真实周期如果发现抖动大就去查调度逻辑。5. ROS端接入让导航算法和底盘“对上话”5.1 Linux下把CAN接口跑起来STM32那边把数据从总线收发处理好了接下来是把CAN接入ROS导航系统。在Linux下最常用的方式是借助SocketCAN——它把CAN抽象成了类似以太网的网络接口。你的虚拟CAN接口叫can0你甚至可以像ping一样去操作它。先载入驱动配置波特率并启用接口sudo modprobe can sudo modprobe can_raw sudo ip link set can0 type can bitrate 500000 sudo ip link set can0 up执行完ifconfig看一眼能看到can0挂着UP状态。如果用的是CAN转USB设备比如USBCAN大部分Linux内核自带驱动插上就会出can0或can1。这时候用candump就能直接看总线上的裸报文candump can0这个命令在我看来是排查“CAN没反应”的第一利器。如果candump能看到数据但你的ROS节点没反应问题在上层如果连candump都一片寂静问题在物理层或配置层你就不要浪费精力去查ROS代码了。5.2 一个轻量CAN到ROS节点怎么写有了can0接口接下来就是写CAN到ROS的桥接节点。我推荐用python-can配合cantools解析DBC代码量很少逻辑也很清晰。import can import cantools import rospy from geometry_msgs.msg import Twist db cantools.database.load_file(chassis.dbc) bus can.interface.Bus(channelcan0, bustypesocketcan) def send_cmd(linear, angular): # 根据底盘模型左右轮速度 线速度 ± 角速度 * 轮距/2 left_speed linear - angular * 0.3 / 2 right_speed linear angular * 0.3 / 2 msg db.get_message_by_name(VCU_CMD) data msg.encode({ DrvDir: 1 if linear 0 else 0, DrvSpd: int(abs(linear) * 10), DrvMode: 2, }) tx_msg can.Message(arbitration_idmsg.frame_id, datadata, is_extended_idFalse) bus.send(tx_msg) def cmd_vel_cb(twist): send_cmd(twist.linear.x, twist.angular.z) rospy.init_node(can_bridge) rospy.Subscriber(/cmd_vel, Twist, cmd_vel_cb) # 接收电机反馈并发布实测速度 while not rospy.is_shutdown(): for rx_msg in bus: if rx_msg.arbitration_id 0x181: decoded db.decode_message(MOTOR1_FB, rx_msg.data) rospy.loginfo(left speed: %f, decoded[M1_Speed]) # 更新odom、发布状态等这里有两个关键点。第一cmd_vel里的线速度单位是m/s角速度是rad/s而底层驱动里速度信号的缩放系数可能是0.01m/s或者0.1m/s你必须在桥接节点里完成单位换算别指望车能自动“理解”ROS单位。第二差速底盘的左右轮速度分解公式涉及轮距也就是左轮和右轮之间的距离不同车轴距不一样这个参数要在配置文件里显式写清楚不要硬编码在代码里。我就是因为偷懒硬编码轮距后来换了辆底盘调试了整整一下午才发现导航指令是反的。5.3 闭环验证用导航仿真测试CAN链路很多同学做“ROS小车自主导航仿真”的时候整条链路都在Gazebo里跑cmd_vel发给虚拟底盘永远发现不了CAN通信的问题。我的建议是即使没有真实小车也要把通信层拉通测试一遍。方法很简单PC上用SocketCAN创建一个虚拟CAN接口vcan0然后让ROS桥接节点监听vcan0再通过canutils发送模拟的底盘反馈帧看桥接节点能不能正确解析并发布出odom话题。这样可以在没有硬件的情况下验证DBC解析对不对、坐标系转换对不对。如果有一套真实的底盘和USBCAN工具就把导航节点的cmd_vel直接发给桥接节点同时用candump抓CAN侧的报文。你可以对照看ROS侧期望的速度值和CAN侧实际编码出来的原始数据是否匹配。我做过很多次这种闭环验证最常见的问题是线性/角速度的符号取反、单位少乘了10、左右轮速度对调。这些在Gazebo里测不出来只能靠真实的CAN抓包对比。6. 故障排查“CAN通信突然连不上”的玄学与科学6.1 常见问题速查表我把这几年做CAN通信踩过的坑整理成了速查表排查问题时按顺序过一遍能覆盖90%的问题。现象可能原因排查与解决完全无通信candump无数据波特率不匹配、总线未up、CANH/CANL接反先用candump -L看是否有任何帧检查ip link set can0 up时好时坏短距离正常长距离丢帧缺少终端电阻或阻抗不匹配万用表量CANH-CANL应为60Ω左右发送失败HAL_CAN_AddTxMessage返回BUSY发送邮箱满、发送周期过快加发送队列降频检查是否AutoRetransmission关闭接收漏帧电机反馈跳变FIFO溢出、中断处理过长回调只拷贝数据快速退出进入Bus Off后恢复不了AutoBusOff没开启初始化里打开或者软件主动恢复CAN调试工具连不上指示灯异常不共地、CAN_GND没接确保所有节点共地检查收发器供电上电正常过一会儿死掉急停/错误帧风暴总线被占用CAN分析仪看错误帧检查是否有节点发错ID6.2 一次真正的排查实录说一个我印象很深的排查过程。当时一台自主导航车在测试场上电时一切正常跑了大概5分钟后底盘突然“失去响应”遥控器也控制不了就像整条总线被什么东西掐断了。下意识判断是硬件坏了但拆开检查线缆、收发器、节点电阻都没问题。最后我用USBCAN分析仪并接到总线上发现那一段时间总线上出现了大量的错误帧持续了大约2秒然后总线进入Bus Off状态。进一步看错误帧的内容发现有一个节点在一瞬间发了大量填充位错误的帧。根源竟然是给电机控制器供电的24V电源在启动瞬间有个回落控制器那边电流采样出现毛刺导致CAN控制器计算出错触发错误计数超限把自己“踢出”了总线。这种“突然连不上”的最终解决办法是给总线增加错误帧计数监控一旦某个节点连续出错就自动复位该节点同时在CAN控制器初始化时开启AutoBusOff并确保Bus Off后能自动重新进入总线。从那以后我所有的STM32工程里AutoBusOff基本都是默认开启的。经验教训是CAN“突然连不上”不要只盯着软件要拿分析仪看错误帧错误帧会告诉你到底谁在“捣乱”。6.3 最好用的排查工具清单搞CAN排除故障工具选对能省一半时间。逻辑分析仪/示波器看物理层波形CANH和CANL之间的差分电平和采样点一测便知。USBCAN分析仪最实用插电脑就能抓包、发帧还能统计错误帧。我用的是一款百来块钱的稳定性完全够用。candump/cansendLinux命令行工具排查SocketCAN层问题时离不开。CANtest、canoediter等PC软件灌DBC文件直接以物理量的方式查看总线数据比看16进制舒服太多。另外提一个设备问题同时接两个USBCAN分析仪或者USBCAN和STM32调试板同时挂在总线上时注意终端电阻的数量。很多USBCAN盒子内部默认带了120Ω终端电阻而STM32开发板上一般也有再加你手动接的总线阻抗会被拉到很低信号幅度不足出现“只要插上分析仪就不能通信”的诡异现象。遇到这种情况把其中一端多余的终端电阻去掉就行。最后再分享一个小技巧调试CAN通信时我习惯先把所有业务逻辑剥离只留一个最小收发程序总线侧用最便宜的USBCAN和设备对发固定报文。确认底层通了再逐步往上加ROS节点、加控制算法。每一层都验证过再叠加。这样即使后面出问题也能很快定位到具体在哪一层而不是对着几千行代码瞎猜。CAN这门通信技术虽然已经“上了年纪”但在自主导航底盘这个场景里它依然是最可靠、最划算的选择。希望这篇从硬件到协议、从STM32到ROS的完整梳理能帮你少走一点弯路。如果你正在做自主导航小车、正在被CAN通信折磨不妨把文中的代码和排查表直接抄下来跑通了再回来聊聊你遇到的新坑。
返回列表