ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ROS小车自主导航中的CAN通信实战:从数据帧到故障排查

ROS小车自主导航中的CAN通信实战:从数据帧到故障排查 做自主导航小车的时候我第一个想吐槽的是明明ROS和导航算法都已经很成熟了真正让我熬夜的却是底盘那根不起眼的CAN总线。标题里写的“自主导航--4.CAN通信”说白了就是整个系统里最底层、最容易被忽略、但一坏就全车瘫痪的环节。导航算法再漂亮最后还是要通过它把速度指令送到电机轮子上去。这篇文章我就围绕自己在ROS小车上做CAN通信的完整经历来写从为什么非用CAN不可到数据帧怎么定义、代码怎么走读、踩了哪些坑一次讲透。如果你正在做ROS小车自主导航仿真或者准备把仿真代码搬到真车底盘又恰好被CAN通信折腾过那这篇内容应该能帮你省下不少查资料和试错的时间。1. 为什么自主导航要选CAN而不是串口或以太网1.1 从整车架构看CAN的位置自主导航系统通常分三块感知层雷达、相机、决策层工控机或树莓派跑ROS、执行层电机驱动板、舵机、IMU等。ROS负责做SLAM建图、路径规划、避障但它算出来的结果不是直接驱动电机的信号而是“线速度 角速度”这样的抽象指令比如cmd_vel。真正把这些指令变成物理运动需要一个可靠的通信通道连接主控和底盘执行器。CAN就是在这个位置出现的。可以说CAN是决策层和执行层之间的“神经束”。1.2 CAN相比串口和以太网的几个关键优势第一是多主结构。串口是主从模式一问一答主控忙不过来或者某个传感器没回话整条链路就等。CAN是多主总线任何一个节点都能主动发数据主控不用轮询这在实时控制场景下太重要了。第二是仲裁机制。CAN本身的CSMA/CA机制能保证多个节点同时发数据时优先级最高的先发而且不会破坏数据。这个在底盘的场景里很实用比如电机驱动器要上报“过流保护”这种紧急故障优先级高可以随时打断正常的指令帧。第三是抗干扰和线束成本。CAN用差分信号传输CAN_H和CAN_L两根线拧在一起抗共模干扰能力远超TTL串口。小车上电机多电磁环境差我试过用串口在电机启动瞬间出现毛刺乱码CAN就稳定得多。另外总线式拓扑意味着所有传感器只需要就近挂在两根线上不用每个设备都拉一堆线到主控。第四是实时性确定。CAN的帧长度有限数据最多8字节波特率可以做到1Mbps在500kbps下总线负载不高的情况下一个数据帧从发出到被接收的延迟非常稳定这在50Hz甚至100Hz的控制周期下很友好。注意不是说CAN能替代一切。它数据长度有限传图像、传点云完全不行。但在底盘控制、传感器状态上报这类短报文场景它几乎是工业界和机器人领域都在用的默认答案。2. CAN数据帧到底长什么样如何拆解2.1 数据帧的完整结构我把标准数据帧按位拆开来说别被那个“标准/扩展”的概念吓到。一个标准数据帧的报文从开始到结束依次是字段位数作用SOF帧起始1同步信号由高到低的跳变仲裁段1211位ID 1位RTR控制段6IDE位 保留位 4位DLC数据长度数据段0~64位真正要传的数据0到8字节CRC段1615位CRC校验码 分隔符ACK段2接收确认EOF7帧结束标志最核心的是仲裁段里的ID11位标准帧 / 29位扩展帧和数据段。ID决定了这个帧是谁发的、优先级有多高数据段里才是具体的物理量比如速度、转角、轮速。2.2 仲裁机制和ID优先级的选择仲裁机制很容易理解多个节点同时往总线发数据时每个节点一位一位地把自己的ID发出去谁在比较时先输出显性电平逻辑0谁就赢了。所以ID的数值越小优先级越高。这点在定义底盘协议时特别关键。我的做法是故障上报类帧用最小的ID比如0x000~0x00F因为故障信息必须第一时间发出去控制指令帧居中比如0x010~0x0FF周期状态上报帧优先级最低用大一点的ID比如0x100之后。一开始我把状态上报帧的ID设成了0x001结果发现每次电机驱动器急停发送速度就会被低优先级的状态帧卡住晚了几毫秒到执行器按那个速度跑车是刹不住的。后来反过来了故障最优先状态上报让路。2.3 波特率和采样点的设置CAN通信波特率设置不对最常见的结果就是总线全报错一个帧都收不到。我们小车用的波特率一般是500kbps也就是每秒传输500k个位。波特率不是像给串口设115200那样随便写个数字就完的底层要换算成位时间。波特率由四段组成同步段(SYNC_SEG)、传播段(PROP_SEG)、相位缓冲段1(PHASE_SEG1)、相位缓冲段2(PHASE_SEG2)加起来就是一个位时间。拿STM32的bxCAN举例如果CAN外设时钟是36MHz位时间 1 / 500kbps 2us tq * (1 BS1 BS2)预设tq 250ns则位时间有8个tq。取BS1 6个tqBS2 1个tq同步段1个tq。采样点位置就落在(1 6) / 8 82.5%处标准CAN的理想采样点一般在75%到87.5%这个值就比较稳。经验采样点太靠后或太靠前长距离总线容易采到跳变边沿附近的数据误码率飙升。如果手头有示波器直接看CAN_H和CAN_L的差分波形能直观评估上升沿和下降沿的畸变程度。3. CAN通信代码走读从SocketCAN到ROS的cmd_vel3.1 Linux环境下SocketCAN的基本操作在树莓派、工控机这类Linux系统上最常用的CAN操作接口叫SocketCAN它把CAN总线的节点抽象成了一个虚拟网络接口比如can0操作方式跟操作socket差不多。先把CAN接口启动起来波特率设成500k# 设置波特率并启动 sudo ip link set can0 up type can bitrate 500000 # 查看CAN设备是否正常 sudo ip -details link show can0如果手头有USB转CAN适配器插上去之后在/sys/class/net/下面通常会自动出现can0。没有硬件的时候可以用系统自带的功能创建虚拟CAN接口后面专门讲。启动之后用candump监听总线上的帧用cansend从命令行发送测试帧# 监听所有报文 candump can0 # 发送一个标准帧ID0x010data01 02 03 04 05 06 07 08 cansend can0 010#0102030405060708candump的输出格式是接口 帧ID#数据比如can0 010 [8] 01 02 03 04 05 06 07 08这个基础操作别看简单调试通信时最先用它验证物理链路和驱动程序是否正常非常有用。3.2 ROS端CAN节点整体设计在ROS小车自主导航仿真的项目里CAN通信不是裸操作控制器而是要把CAN收发逻辑封装成一个ROS节点。这个节点负责两件事订阅cmd_vel线速度/角速度把速度指令转换成CAN数据帧发到底盘。接收底盘通过CAN发回来的轮速/里程计状态解析成odom话题输出。可以理解成ROS世界和物理世界之间的翻译官。导航栈只认cmd_vel和odom底盘只认CAN报文CAN节点把两者拼起来。我用Python写过一版快速原型核心部分是这样的import can import rospy from std_msgs.msg import Float64 from geometry_msgs.msg import Twist from sensor_msgs.msg import JointState class CanNode: def __init__(self): rospy.init_node(can_bridge) self.bus can.Bus(interfacesocketcan, channelcan0, bitrate500000) rospy.Subscriber(/cmd_vel, Twist, self.cmd_vel_callback) self.odom_pub rospy.Publisher(/odom_raw, JointState, queue_size1) rospy.Timer(rospy.Duration(1/50), self.timer_callback) def cmd_vel_callback(self, msg): # 线速度和角速度转成16位整型 linear int(msg.linear.x * 1000) angular int(msg.angular.z * 1000) # 组帧: ID0x010, 数据段4字节 data [ (linear 8) 0xFF, linear 0xFF, (angular 8) 0xFF, angular 0xFF, ] frame can.Message( arbitration_id0x010, dlc4, datadata ) self.bus.send(frame) def timer_callback(self, durationNone): # 从总线上读取帧这里简化为轮询 # 实际场景可以放在单独线程阻塞读取 frame self.bus.recv(timeout0.01) if frame is None: return if frame.arbitration_id 0x110: linear_speed ((frame.data[0] 8) | frame.data[1]) / 1000.0 # 然后封装成 odom 发出去代码里要特别留意量化因子。ROS的cmd_vel是浮点数底盘的CAN数据是整数二者之间需要一个统一的比例系数。我用的是0.001也就是说发送的时候乘1000收回来的时候除以1000。如果你用1.0的因子精度会很差。3.3 帧ID和数据栏定义先把协议定清楚代码写来写去最终落地靠的是一张协议表。我在做小车底盘时定义了下面这套规则你自己做项目也可以直接参考帧ID方向名称DLC数据定义0x010主控 → 底盘速度指令4字节0~1线速度(int16单位0.001m/s)字节2~3角速度(int16单位0.001rad/s)0x011主控 → 底盘转向指令2字节0~1目标转向角度(int16单位0.01°)0x100底盘 → 主控状态信息6字节0电源电压字节1控制器温度字节2~3错误码0x110底盘 → 主控轮速反馈8字节0~1左轮速度字节2~3右轮速度字节4~5左轮编码器累计值字节6~7右轮编码器累计值0x001底盘 → 主控故障上报2字节0故障码字节1故障级别这里分享一个我的惨痛教训一开始我把“主控发给底盘”和“底盘发给主控”的帧都随手排了ID结果调试的时候用candump一看全混在一起光靠眼睛根本分不清哪条指令被底盘执行了、哪条状态是底盘发上来的。后来我强制规定所有主控到底盘的帧ID范围固定从0x010开始所有底盘到主控的帧从0x100开始。中间隔开一个量级用candump肉眼过滤就能省下很多事。3.4 数据段校验别把校验位抠掉很多自己做的小项目CAN数据帧不带校验位因为CAN硬件本身有CRC15校验应用层就免了。但这个认识有代价——CAN的CRC只能防“总线传输错误”防不了“应用层数据定义错乱”比如底盘发回一个帧的ID是对的但数据段里左右轮速写反了。我在协议里加了一个简单的校验字节放在最后一字节算法采用和校验把前面所有字节加起来取低8位。def append_checksum(data): return data [sum(data) 0xFF]接收端收到帧之后先算一遍校验对不上就直接丢弃而不是把错误数据拿去算里程计。否则可能造成ROS里的odom突然跳变导航规划直接觉得定位崩了。4. 导航联调实战CAN帧和ROS数据如何相互转换4.1 从CAN轮速到里程计odom的推算做自主导航ROS里move_base导航规划器依赖一个可靠的话题叫odom里程计。里程计数据从哪里来在真车上就是从CAN回传的轮速和编码器数据自己算出来。已知左轮线速度v_left右轮线速度v_right轮距d左右轮中心距离那么车体的线速度和角速度用下面的公式v (v_left v_right) / 2 ω (v_right - v_left) / d有了v和ω再按时间dt累加就能得到车体在全局坐标系下的位姿变化delta_x v * cos(theta) * dt delta_y v * sin(theta) * dt delta_theta ω * dt这段逻辑可以直接写在CAN节点里把CAN帧解析出来的左右轮速换算成odom消息发出去。要注意CAN帧里的轮速单位一般不是m/s而是“编码器脉冲数/秒”或“mm/s”需要按轮径比例换算。比如底盘发回来的是“毫米每秒”你就得除以1000转成米每秒否则导航算法会认为车速放大了一千倍路径跟踪立刻发疯。4.2 控制频率的匹配问题我调试时经常发现ROS导航栈发送cmd_vel的频率是10Hz但底盘电机驱动器的期望控制频率是100Hz。如果直接把cmd_vel简单转发给电机驱动器车子会一顿一顿的因为10Hz实在不够平滑。正确做法是在CAN节点里加一个平滑缓冲。接收cmd_vel到本节点的回调后不是立刻发到总线上而是先把目标速度缓存下来在另一个高频循环比如100Hz里用简易的斜坡函数把当前速度逐步逼近目标速度每一拍再发CAN帧rate rospy.Rate(100) while not rospy.is_shutdown(): self.current_linear (self.target_linear - self.current_linear) * 0.2 self.current_angular (self.target_angular - self.current_angular) * 0.2 send_can_velocity(self.current_linear, self.current_angular) rate.sleep()这里0.2就是平滑系数系数越大越激进越接近直接追踪系数越小越柔和但跟踪越滞后。工程师需要根据自己的车体和执行器的响应速度去调。我做的是一个载重较小的底盘0.15~0.25之间比较合适电机不会抖转弯也不会太突兀。4.3 没有真车时怎么仿真CAN通信ROS小车自主导航仿真一般都在Gazebo里做Gazebo里没有真CAN硬件但通信节点又需要真实的底层数据路径。这里我的办法是用虚拟CAN接口 vcan。创建vcansudo modprobe vcan sudo ip link add dev vcan0 type vcan sudo ip link set up vcan0关键点来了vcan和can0的设备名一样操作方式一样candump、cansend都能用但它是内存里模拟的没有真实电气信号。你在ROS容器或者宿主机上跑CAN节点把channel参数从can0改成vcan0逻辑一模一样。有vcan之后Gazebo仿真的底盘控制完全可以通过“ROS节点 → vcan0 → 另一个模拟底盘节点 → odom”的路径打通。这样既能测试协议帧定义、ID规划、解析逻辑又不用每次编译都动用真电机开发效率高好几倍。我建议所有没有真车条件的同学都用这套流程把CAN通信逻辑先跑顺。5. CAN通信常见故障与排查技巧实录5.1 总线上一片错误帧什么有效报文都收不到现象candump can0时输出连续的错误帧或者用ip -details link show can0看到error-active变成error-passive甚至bus-off。排查顺序查终端电阻。CAN总线两端需要各接一个120Ω电阻。我有一个偷懒经历直接拿一根杜邦线短接CAN_H和CAN_L当测试没有接电阻结果总线上到处是错误帧。拿万用表量CAN_H和CAN_L之间的阻抗正常情况下应该在60Ω左右两个120Ω并联如果量出来是无穷大说明有一端电阻没接。查波特率一致性。两个节点波特率一个是500k一个是250k谁能收谁的都收不到。很多USB转CAN适配器默认是500k如果底盘控制器是250k挂上去立刻全是错帧。这个最先确认最简单的办法是发一帧测试报文用另一种波特率听看有没有ACK回显。查CAN_H和CAN_L有没有接反。这个错误最蠢但最容易犯。特别是不同厂家的线颜色不一样不统一。接反后错误帧数量巨大而且不分主从。查地线是否共地。CAN总线虽然靠差分信号传输但是各个节点最好仍然共用一个参考地。如果节点间地电位差太大即使差分信号也无法正常工作。我在车上接了两个隔离电源模块就出现了间歇性丢帧后来把主控底和底盘底连到一起解决。5.2 报文能收到但周期忽快忽慢现象接收底盘电机反馈帧用candump看有时候10ms有时候50ms才来一帧。优先怀疑两个原因第一CAN节点接收线程被阻塞了。在ROS节点里如果recv()在一个线程里处理而主线程被其他同步调用卡住接收缓冲就会积压导致“伪周期抖动”。解决办法是单独开线程接收CAN帧或者用select()加超时处理。第二总线上有其他高优先级帧占用了带宽。如果你车上的故障诊断帧设计得太频繁比如每1ms发一次而后续命令帧要等它走完那低优先级帧自然要排队看起来就是周期被拖慢。用candump -ta can0 -l抓一秒钟的日志统计每个ID的出现次数就能看出是谁在抢占总线。5.3 BUS OFF 了怎么办CAN控制器检测到发送错误计数器超过256会进入BUS OFF状态此节点彻底不参与通信。真车调试中出现BUS OFF意味着不是某个软件bug而是硬件层面出了大问题。恢复方法是在代码里加自动恢复指令比如用STM32时调用CAN_CancelAutoRetransmit或相应的bxCAN测离线恢复寄存器在检测到BUS OFF后延时一段时间重新初始化CAN控制器。但这里要提醒一点别完全依赖自动恢复要解决根源。BUS OFF八成是CAN_H/CAN_L短路、地环流过大、线束长度过长导致信号质量太差。如果代码里只是把错误清零然后重新初始化问题还会复现而且复现周期可能越来越短。我调试中见过最无语的一次电机驱动器的接插件进水短路导致CAN_H和电源地轻微短路系统跑几分钟就BUS OFF一次。最后是拿万用表一个线束一个线束排查才找到。5.4 帧内容一直不对比如收到的速度和实际差两倍这种问题一般是“单位换算”或“字节序”问题。单位换算比如协议里定义速度单位是0.01 m/s但发送端按0.001 m/s计算那收到的数值永远偏小。我习惯在节点启动时把“标定系数”做成一个参数启动时从launch文件里读调试时不必重新编译代码。字节序CAN数据段本身没有规定大小端完全看协议怎么写。假如发送端按大端高字节在前组帧接收端按小端低字节在前解析那数据就会错得离谱。比如 0x1234 发过来两种解析结果大端解析: 0x1234 4660 小端解析: 0x3412 13330我实际踩过这个坑当时左右轮速都差了好几倍导航程序直接判断为打滑把车速限制到几乎为零。排查方法是发一个固定值帧比如发01 02 03 04然后用Pythonint.from_bytes(data, little)和big各解一遍看哪个结果对得上。最后一个小技巧调试CAN通信时我习惯在ROS节点里加一个debug_flag开启后把每个CAN帧同时保存成CSV文件帧ID、DLC、原始数据、时间戳都存下来。真车调试跑一圈回来用Pandas一拉数据所有协议问题都能直观看到比现场盲猜高效太多。这个小习惯帮我扛过了无数个“莫名其妙”的调车下午。
返回列表