ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自主导航小车CAN通信实战:从STM32到ROS联调解析

自主导航小车CAN通信实战:从STM32到ROS联调解析 做自主导航小车很多人把注意力全放在上层的SLAM建图、路径规划算法上结果车子一上电轮子不动电机乱抖甚至底盘直接“失联”。我跟你讲八成问题都出在底层通信尤其是CAN这一环。这期就以“自主导航—CAN通信”为主线把从硬件接线、协议设计到ROS端联调、故障排查的完整链路捋一遍。这套方案是我在真实小车项目里反复验证过的跟着走能少踩很多坑。1. 底层通信方案选型为什么必须是CAN1.1 先搞清楚底盘要传什么数据自主导航小车的底盘本质是一个分布式实时控制系统。STM32作为下位机要同时管电机驱动器、编码器、陀螺仪/IMU如果接在CAN上、转向舵机等好几个节点。每个节点都在持续产生数据比如电机转速反馈、电流环状态、IMU的角速度和加速度、电池电压等。同时上位机通常是装了ROS的树莓派或Jetson还要实时下发速度指令、转向指令、急停指令。这个场景有3个硬性要求实时性速度指令从ROS发出到电机响应整个链路最好控制在10ms以内否则导航算法算得再准底盘执行不到位车子就会画龙。可靠性底盘工作在电机换向、PWM调速的强干扰环境总线电平稍不稳定就会导致数据错乱。多点互联电机、编码器、IMU、舵机分属不同节点用点对点通信比如UART直连根本接不过来。CAN总线就是为此设计的。它属于多主总线任何节点都可以主动发消息自带优先级仲裁和错误检测机制。我们用的经典CAN 2.0A标准帧11位ID数据段最长8字节1Mbps速率下单条报文传输时间不到100微秒实时性绰绰有余。1.2 CAN和UART、I2C、SPI到底差在哪很多初学者会惯性思维直接用STM32的UART连蓝牙模块或者无线串口不也能控制小车吗确实能但分场景。通信方式传输距离抗干扰能力多节点支持实时性适用场景UART15米以内弱点对点中短距离调试、蓝牙透传I2C1米以内弱有限受地址限制低板载传感器连接SPI10米以内弱有限靠片选高板载高速Flash、SD卡CAN40米以上强差分信号理论110个实际几十个高车载、机器人底盘、工业控制I2C和SPI是芯片间通信用的根本没考虑长线传输和多点互联不适合电机这种会抖动的负载。UART虽然简单但两个UART口只能接一个设备而且它是单端信号电机启动瞬间的压降和浪涌很容易把数据打飞。CAN是差分信号CAN_H和CAN_L一对双绞线信号以电压差形式传输共模干扰被直接抵消掉抗干扰能力天生就强。这也是为什么汽车OBD诊断、工业PLC总线几乎默认CAN。1.3 这套小车的通信链路拓扑我们小车的实际拓扑并不复杂但设计思路值得参考。整条链路分成两层上层ROS侧Jetson或者树莓派跑ROS的SLAM和导航栈。它没有原生CAN控制器所以通过USB-CAN适配器接入CAN网络相当于CANopen主站。下层执行侧STM32作为底盘主控同时承担IMU数据采集、电机控制、编码器计数的任务以CAN节点身份挂在总线上。如果电机驱动器本身支持CAN控制那STM32就只做管理节点指令仲裁后发给驱动器。数据流向是这个样子的ROS导航栈发出几何速度指令/cmd_vel→ USB-CAN适配器把指令打包成CAN报文 → 总线广播 → STM32接收解析 → 通过内部PWM/GPIO控制电机驱动器 → 编码器计数反馈 → 打包成CAN报文回传 → ROS订阅/odom→ 里程计数据喂给AMCL和move_base。这整个过程在10ms周期内跑完CAN就是那条“通信血管”。2. 硬件接线与基础配置别小看这些细节2.1 需要的硬件清单这套方案里我用的典型硬件是STM32F103C8T6或者F407F407自带两个CAN外设更宽裕TJA1050 CAN收发器芯片如果是F407板载了就省事USB-CAN分析仪我用的是USBCAN-II型兼容SocketCAN120Ω终端电阻两个CAN网络两端各一个这是标配双绞线若干或直接用屏蔽双绞线硬件连接的核心是STM32的CAN控制器引脚CAN_RX、CAN_TX接到TJA1050TJA1050再以差分线形式接出CAN_H和CAN_L。USB-CAN适配器一端接USB到上位机一端同样接CAN_H和CAN_L。2.2 终端电阻与波特率两个最容易犯错的地方CAN总线两端必须接120Ω终端电阻这不是可选项是硬性要求。它的作用是在高速信号传输时吸收反射波防止信号在总线末端形成振铃。如果缺失最典型的故障就是通信时好时坏低速波特率可能还凑合一上500kbps或1Mbps就直接失联。我踩过的坑是这样的早期图省事只在下位机这边接了一个120Ω想着分析仪那边也许自带结果实测CAN报文偶尔能收到偶尔全是错误帧。拿示波器一看CAN_H/CAN_L的差分波形在末端出现了明显的过冲和回沟信号质量一塌糊涂。后来在分析仪端又补了一个120Ω电阻通信立刻稳定错误帧归零。波特率选择上我建议统一用500kbps或者1Mbps。不管上层跑的是SocketCAN还是直接裸收发两边的波特率必须一致。STM32的波特率由APB1外设时钟、预分频器和BS1/BS2时间段共同决定。以F103为例CAN外设挂在APB1上时钟频率36MHz。要配置成500kbps可以按这个思路算CAN总线的位时间 1 / 波特率 1 / 500000 2微秒。如果预分频器设为9即把36MHz除以9得到4MHz的CAN时钟CAN时钟周期0.25微秒那么位时间就是2微秒 / 0.25微秒 8个时间量子。这8个时间量子可以分配为SYNC_SEG占1个、BS1占5个、BS2占2个。实际在STM32CubeMX里直接选500kbps它会自动帮你算但理解这个计算过程有助于排查问题。2.3 CAN ID规划让报文结构一眼可读CAN 2.0A标准帧的ID是11位也就是说最多可以定义2048种不同的消息ID。但对于小车来说完全不需要铺满反而要精打细算。我推荐一套ID分配方案直接抄作业CAN ID方向含义数据内容0x101上位机→STM32速度控制指令线速度、角速度0x102上位机→STM32急停灯/状态控制刹车标志、灯控0x201STM32→上位机电机编码器反馈左轮速度、右轮速度0x202STM32→上位机IMU姿态数据Yaw角、角速度0x203STM32→上位机电池电压/系统状态电压值、故障码ID有两个考虑维度一是优先级ID值越小总线仲裁优先级越高。车速指令和IMU姿态属于关键数据应该用小ID0x101、0x201保证它们在总线拥塞时也能第一时间被发出。二是可读性按照功能划分ID段上位机和下位机写代码时一目了然。3. 协议设计与DBC文件从裸数据到结构化消息3.1 为什么必须设计“数据帧结构”刚上手的朋友常常直接在CAN发送函数里填8个字节比如“0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00”想当然地认为第一个字节就是线速度。这种写法在单机调试时能用但等整套系统跑起来多个节点互相发数据再打日志调试时会非常痛苦因为你对着十六进制裸数据根本不知道哪个字节代表哪个物理量。所以需要一套协议规范——本质上就是把原始字节映射成有意义的物理量。DBC文件CAN Database就是这套映射的标准描述。3.2 DBC文件里到底写了什么DBC文件是Vector公司定义的CAN协议描述格式可以用文本打开核心是“BO_”开头定义报文“SG_”开头定义信号。比如我们定义速度控制指令这条报文BO_ 257 VehicleSpeedControl: 8 Vector__XXX SG_ LinearSpeed : 0|161- (0.001,0) [0|6.5535] m/s Receiver SG_ AngularSpeed : 16|161- (0.001,0) [-3.2768|3.2767] rad/s Receiver我来逐块拆解让新手也能看懂BO_ 257257是十进制ID对应十六进制的0x101。VehicleSpeedControl报文名方便人读。8报文数据长度8字节。SG_一个信号的开始。LinearSpeed信号的名字。0|161-这句是关键。0表示这个信号在数据场中的起始位bit 016表示信号长度16位即2个字节1表示采用Intel字节序小端序-表示数据是有符号的。(0.001,0)因子和偏移量。物理值 原始值 × 因子 偏移量。也就是说原始值12345表示的实际线速度是12.345 m/s。[0|6.5535]物理值的取值范围。m/s物理单位。Receiver接收方可以留空或者写节点名。同一行格式也适用于角速度只是起始位变成了16占16位有符号。有了DBC文件上层工具可以自动把解析后的物理量显示出来或者直接用canmatrix库在Python里解析。我自己调试时常常先把所有报文写进DBC再用candump配合can-utils抓包把数据喂给一个解析脚本打印出来的就是“LinearSpeed0.532m/s, AngularSpeed0.01rad/s”这种可读信息。3.3 数据打包与.CPP里的实际对应DBC定义好了STM32端发送时要按这个格式打包。比如发送IMU姿态数据yaw角偏航角是有符号数// STM32端发送0x202报文 uint8_t can_tx_data[8]; int16_t yaw_raw (int16_t)(yaw_deg * 100.0f); // 扩大100倍变成整数 can_tx_data[0] yaw_raw 0xFF; can_tx_data[1] (yaw_raw 8) 0xFF;注意这里用了小端序低字节在前和高位字节在后正好对应DBC里的1。3.4 周期与心跳机制设计CAN是广播式总线每个节点都得想清楚自己多久发一次数据。我们的设计原则是速度控制指令5ms或者10ms周期越短控制越平滑。IMU姿态20ms周期50Hz导航算法够用。编码器反馈10ms周期作为里程计来源。电池电压/系统状态500ms周期慢速上报即可。心跳机制的加入是关键优化。STM32端每100ms发送一条在线指示报文比如ID 0x205上位机一旦超过200ms没收到就判定下位机离线立即触发急停逻辑。这个机制在真机调试时救过我一次。当时小车跑到一半电机驱动器过热保护停机但CAN总线上没有明确错误报文ROS还在傻乎乎地继续发布速度指令。有了心跳检测上位机立刻察觉停止了指令下发总算没有造成更严重的硬件事故。4. 上下位机联调从SocketCAN到STM32收发关键步骤4.1 上位机侧让Linux识别CAN接口树莓派或Jetson跑了Ubuntu ROS后插入USB-CAN适配器系统通常会识别成一个网络接口。使用SocketCAN标准接口配置步骤是# 查看适配器对应的网络接口名一般是can0 ip link show # 配置波特率并启动 sudo ip link set can0 type can bitrate 500000 sudo ip link set can0 up # 验证接口状态 ip -details link show can0看到state UP就表示CAN物理层已经正常了。此时可以用candump先裸抓一下总线上的报文确认环境配置正确candump can0正常情况下STM32端已经在持续发送心跳包和数据包candump会不断刷出原始数据帧。如果什么都抓不到或者全是错误帧先别急着查代码回头检查终端电阻和波特率八成是物理层的问题。4.2 ROS端写一个CAN转ROS话题的节点ROS端需要一个节点把CAN报文翻译成ROS话题同时反过来把/cmd_vel速度指令打包发到CAN总线上。我写过一个精简版的示例核心逻辑如下import can import rospy from geometry_msgs.msg import Twist bus can.interface.Bus(channelcan0, bustypesocketcan) def cmd_vel_callback(msg): linear_raw int(msg.linear.x * 1000) angular_raw int(msg.angular.z * 1000) data [ linear_raw 0xFF, (linear_raw 8) 0xFF, angular_raw 0xFF, (angular_raw 8) 0xFF, 0, 0, 0, 0 ] frame can.Message(arbitration_id0x101, datadata, is_extended_idFalse) bus.send(frame) rospy.init_node(can_bridge) rospy.Subscriber(/cmd_vel, Twist, cmd_vel_callback) rospy.spin()这里的核心逻辑就是把ROS的线速度和角速度转成CAN数据按我们DBC里定义的因子0.001打包。注意数据格式线速度判断符号、取绝对值、再扩展成无符号整数存储细节不要出错错了整车方向都会反。接收方向也是同理读取0x201报文解析出左右轮速度组成nav_msgs/Odometry消息发布。注意里程计要用轮速积分还要处理航向角融合这些后续讲SLAM时再展开。4.3 STM32端CAN外设初始化与收发中断STM32端重点看CAN外设的配置。我以STM32F103为例说明关键点。先看初始化。用STM32CubeMX直接生成配置即可注意这几项波特率选500kbps对应预分频、BS1、BS2在代码里自动生成。开启CAN接收中断FIFO0消息挂起中断。开启CAN发送邮箱空中断如果希望发送反馈。接收中断处理的核心逻辑要精简不能在里面做复杂计算void CAN1_RX0_IRQHandler(void) { CAN_Receive(CAN1, CAN_FIFO0, rxMsg); switch (rxMsg.StdId) { case 0x101: // 速度控制指令 // 解析线速度、角速度更新目标速度变量 target_linear (int16_t)(rxMsg.Data[0] | (rxMsg.Data[1] 8)); target_angular (int16_t)(rxMsg.Data[2] | (rxMsg.Data[3] 8)); break; case 0x102: // 急停控制 emergency_stop (rxMsg.Data[0] 0x01); break; default: break; } }发送侧也不要直接在主循环里盲目发送。推荐的做法是采用“定时发送”策略配合一个10ms定时器中断// 在TIM中断回调或定时任务里每10ms执行一次 static void send_motor_feedback(void) { uint8_t data[8]; int16_t left_speed_raw (int16_t)(left_speed_mps * 1000.0f); int16_t right_speed_raw (int16_t)(right_speed_mps * 1000.0f); data[0] left_speed_raw 0xFF; data[1] (left_speed_raw 8) 0xFF; data[2] right_speed_raw 0xFF; data[3] (right_speed_raw 8) 0xFF; CAN_TxHeaderTypeDef txHeader; txHeader.StdId 0x201; txHeader.DLC 8; txHeader.IDE CAN_ID_STD; txHeader.RTR CAN_RTR_DATA; // 发送到邮箱等待发送完成 // ... }实测下来STM32F103的CAN外设配合标准库或者HAL库都很稳定重点是保证中断服务函数尽量短别在里面做打印、延时之类的操作否则会直接导致CAN消息丢失。5. 实战故障排查CAN突然连不上的排查全路径5.1 故障类型一突然一个报文都收不到这是最常遇见的问题。热词里“stm32 can通信突然连不上”基本就是这类。排查路线要从物理层往上层走顺序不要乱第一步检查物理层。CAN_H和CAN_L是否接反。接反后接收端会一直收到错误帧严重时总线直接关闭。这点可以通过示波器查看波形极性判断如果没有示波器就先把收发线对调试试。终端电阻是否在位。用万用表测量CAN_H和CAN_L之间的电阻正常情况应该在60Ω左右两个120Ω并联。如果量出来是120Ω说明有一端没接量出来是开路或者0Ω说明线路有问题。第二步检查波特率。总线上一旦有多个节点且波特率不一致总线的错误计数器会很快飙升节点进入Bus-Off状态表现为完全无法通信。把两边都配置成统一的500kbps然后重启所有节点。第三步检查软件配置。STM32端CAN过滤器是否配置正确。过滤器配置太严格比如只接收特定ID的报文会导致其他报文全被硬件过滤掉。CAN总线是否被初始化成静默模式或环回模式。很多人配置CAN外设时没注意选了Loopback模式调试之后忘了切回Normal模式结果上位机永远收不到数据。5.2 故障类型二通信时好时坏时不时的错误帧这种比彻底失联更折磨人。我遇到过的一种典型情况是CAN报文正常运行一段时间后突然出现大段错误帧随后自动恢复。排查重点放在CAN_H/CAN_L的线缆质量和走线方式上。电机驱动器的PWM大电流线如果和CAN双绞线平行走线会产生强烈的电磁干扰直接影响CAN收发器的差分信号。解决办法是把CAN线用屏蔽双绞线屏蔽层单端接地同时尽量远离电机电源线。还有一种是CAN收发器供电不稳。TJA1050的VCC如果挂着电机电源上电机启动瞬间电压跌落收发器的电平阈值就不稳定了自然会产生错误帧。要给CAN收发器单独用稳压芯片供电并且加退耦电容。5.3 排查清单速查表把整个排查过程整理成一张表方便现场对照故障现象优先排查项检查方法解决措施完全收不到数据终端电阻缺失万用表测CAN_H/CAN_L间阻值应约60Ω两端各接120Ω完全收不到数据波特率不一致逐节点核对配置统一为500kbps完全收不到数据CAN_H/CAN_L接反观察错误帧计数对调两根线完全收不到数据过滤器设置错误检查CAN过滤器配置改为接收所有ID间歇性通信失败线缆受干扰示波器观察波形毛刺换屏蔽双绞线远离动力线间歇性通信失败收发器供电不足万用表量收发器VCC波动独立稳压供电通信正常但全是错误帧终端电阻阻值漂移万用表精确测量更换精密电阻发送后无反馈发送邮箱满检查发送完成标志使用中断发送并在失败时重传5.4 数据分析现场用candump抓包定位有一回小车在导航测试时频繁出现车轮顿挫我原以为是电机PID参数有问题但在上位机用candump一抓发现CAN总线上出现了间隔性的错误帧每隔几百毫秒就有一次。顺着时间戳比对电机电流波形发现错误帧总是出现在电机换向瞬间。这基本坐实了干扰源是电机电磁噪声。后来把CAN线换成屏蔽双绞线屏蔽层单端接地并且让CAN线走远离电机的路径顿挫立即消失。所以说排查CAN问题不能只盯代码。逻辑层面的错误比如ID不匹配、数据格式错反而不难找难的是物理层的“隐性故障”。如果手头有逻辑分析仪或者示波器优先量波形比盲改代码高效十倍。6. 个人经验补充真正让通信系统稳定的几个习惯手头这个自主导航项目做到现在踩的CAN相关的坑比SLAM调参还多。这里把最底层的几条经验写出来算是给后来人的一点提醒。第一硬件设计阶段就留好CAN调试口。PCB上无论如何都要预留一个独立的CAN调试排针方便直接接CAN分析仪。不要想着靠软件打印日志CAN总线上的错误帧、ACK错误、Bus-Off事件普通日志根本反映不出来。第二协议设计越早越省事。小车刚搭起来就赶紧把DBC文件定了哪怕是初版也会让后面省不少事。后面每加一个报文就更新DBC并同步给上位机和下位机严禁出现“先写代码后补文档”的状态。第三CAN的失败重传机制一定要做。尤其在总线上节点较多时发送邮箱满、仲裁失败都是家常便饭。在STM32端的发送函数里加上返回状态判断发送失败就重试不要默默丢弃数据。一次速度指令丢了可能感知不到十次丢了小车就会抖动甚至失控。第四整车测试前先做一个CAN压力测试。用candump连续记录10分钟总线报文同时让电机在最大负载下正反转切换。这个测试能暴露大部分物理层的连接隐患。如果通车测试时再发现通信问题定位难度会翻好几倍。后续这个自主导航项目我还会继续分享里程计融合、move_base参数整定、SLAM建图实操这些内容。CAN这部分是底盘的地基地基稳了上层才能跑得踏实。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进