ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CAN自定义协议设计指南:从帧格式到位时序的工程实践

CAN自定义协议设计指南:从帧格式到位时序的工程实践 今天聊一个我实际项目里反复折腾过的话题CAN 自定义协议怎么设计。很多人踩过坑CAN 收发器接上了波特率也配到 500k两个板子都能进中断可数据一过来全是乱码又或者表面看着是通的跑两天突然报 Bus Off整个系统就像老式电话断线一样卡住。这些问题大多不是 CAN 硬件本身的问题而是应用层协议设计没想清楚。我自己做过车载仪表、工业控制器和机器人通信板从最开始的“能发就行”到后来老老实实把协议拆成帧格式、字节序、超时机制、错误恢复中间交了不少学费。这篇按我实际做项目时的思考顺序来写先想清楚要解决什么问题再定报文结构然后配置控制器和物理层最后用上位机和示波器去验证。内容偏工程实践适合正在做 STM32、嵌入式 Linux 或工控设备的开发者参考。1. 拿到一个 CAN 项目别急着写代码1.1 自定义协议到底解决什么问题很多场合下CAN 控制器只负责把数据从 A 点搬到 B 点。这句话听起来很基础但它是理解自定义协议的前提。CAN 底层已经帮你搞定了帧起始、CRC 校验、应答、错误检测和重发所以你要做的不是去发明一套新的传输机制而是要回答几个应用层问题这一帧是发给谁的里面装的是什么这个值是多少伏、多少度、多少转速如果对端超过多久没回算不算故障举个例子你有一个温度传感器节点要把 65.3 摄氏度的数据发给主控。CAN 物理层不关心 65.3 怎么表示控制器也不关心这个数据有没有业务含义。你可以在数据场里写 0x028D十进制 653然后和主控约定“第0字节为温度整数部分第1字节为小数部分偏移量0分辨率0.1”。这种约定就是自定义协议。所以自定义协议解决的核心问题是在 CAN 提供的可靠数据通道之上把“字节”变成“信息”。它要覆盖三件事帧格式约定、信号编码规则、节点间通信策略。1.2 协议分层思路底层交给控制器应用层自己定设计 CAN 自定义协议时最好在脑子里有一个分层模型。物理层和数据链路层由 CAN 收发器和控制器处理你不需要去操作电平的每个细节你需要关注的是应用层和一部分传输层逻辑。我习惯把自定义协议分成四块帧 ID 分配规则定义这个 ID 代表什么类型、优先级多高、发给哪个功能域。数据场布局一个字节里放几个信号、信号从第几位开始、长度多少、是否带符号、缩放系数怎么算。通信策略周期发送还是事件触发是否需要应答需要握手还是广播。错误与状态管理节点上电后如何处理 Bus Off多久能重新恢复诊断信息如何上报。这四块都定了协议才算完整。如果只定义“第0字节是电压”那只能算做了一半。传输层如果没做系统一旦出现总线错误或丢帧你根本不知道数据对不对、该不该信。1.3 标准帧还是扩展帧什么时候必须用扩展帧CAN 2.0A 标准帧的 ID 是 11 位CAN 2.0B 扩展帧是 29 位。有人觉得扩展帧“看起来更高级”喜欢什么项目都用扩展帧这是没必要的。11 位 ID 能表示 0x000 到 0x7FF也就是最多 2048 个不同 ID。大多数工业控制网络、中小型设备这个数量级完全够用。你一个节点如果需要 ID 同时承载源地址、目标地址、消息类型、优先级11 位确实容易不够分但多数设备没这么复杂把 ID 合理拆分能塞下常用信号。扩展帧真正有用的场景是多个不同子系统要接入同一条总线并且需要大范围寻址时。比如整车网络里动力系统、车身系统、娱乐系统各自有 ID 规划如果都挤在 11 位里很容易冲突。另外使用 CANopen、J1939 这类成熟协议时很多 ID 含义已经被协议占用了你自定义部分可能只能用扩展帧去扩展。经验之谈优先用标准帧。ID 资源不够再上扩展帧不要一上来就把协议复杂度提上去。因为扩展帧波特率时序、过滤配置、上位机解析都要多留一份心思。2. 报文怎么布置才算“好设计”2.1 CAN ID 不只是地址它同时是优先级CAN 一个很独特的机制是总线仲裁。多个节点同时发送时ID 数值越小的帧优先级越高。这个特性直接决定了自定义协议里 ID 不能乱分。我有一次设计设备控制协议把“心跳报文”的 ID 定成了 0x100把“急停状态”定成了 0x200。结果某个异常场景下心跳报文持续占用总线急停反而被不断延后。虽然不至于完全发不出去但延迟明显变大。后来改成急停 ID 0x010、心跳 ID 0x300才算合理。合理的 ID 设计是把实时性要求高的信号放到低数值区间。比如0x000 - 0x07F最高优先级安全、急停、故障。0x080 - 0x0FF控制指令速度、位置、模式切换。0x100 - 0x1FF状态采集关键反馈。0x200 - 0x3FF辅助数据、参数配置、厂商自定义。如果采用源地址消息类型的组合也要尽量把类型放在 ID 高位这样才能保证“类型”决定优先级而不是“谁发的谁就高”。2.2 数据场布局字节序、缩放、符号位、CRC数据场最多 8 个字节自定义协议里最有发挥空间也最容易出错的地方就在这。第一字节序。CAN 报文在物理链路上是一个字节一个字节传的但多字节数值在内存里有大端和小端两种解释方式。我比较建议在协议里明确统一使用小端Little-Endian因为 STM32、x86 这类主流平台都是小端解析时不需要做转换。但注意有些老式 DSP、CAN 分析仪软件默认是大端显示导致你看到 0x0102 和实际传输的 0x02 0x01 对不上。这种情况下一定要在协议文档里写清楚“所有多字节字段按小端传输即低字节在前。”第二缩放。浮点数在 CAN 里尽量别直接用 IEEE 754 的 float 四个字节去传。原因很简单不同编译器对浮点内存布局、对齐方式很少保证一致而且直接传 float 不利于上位机显示和故障排查。更稳妥的做法是计算好分辨率和偏移量用整数传输。比如电压范围 0 到 36V精度 0.1V那就可以用 0 到 360 的整数表示加上偏移 0。接收方拿到原始值 326换算成 32.6V。第三符号位。整数信号可能是负值比如温度 -40 度。两种方案一种是用有符号整数直接补码传输另一种是用偏移量把所有温度加 40传 0 到 200。我建议温度、角度这类本身有物理意义的用有符号数但要在协议里标明 bit 位数和符号扩展方式。第四CRC 校验。虽然 CAN 数据链路层已经有 CRC但那是校验“传输错误”不是校验“应用层错误”。如果两个节点对信号定义不一致或者数据在中间被某个网关错误转发接收方很难发现。所以在关键报文的最后两个字节里加自己的 CRC8 或 CRC16是个好习惯。CRC 多项式不用很复杂CRC8 多项式 0x31 在很多场景里够用。2.3 心跳、超时和序号让对端知道节点“活着”自定义协议里最容易被忽略的部分是“对端是否还正常工作”。CAN 不像 TCP没有自动建立的连接。节点默认就是往外发帧发完就结束。所以协议里要有心跳帧。发送方周期性发送一个很小的报文比如每 100ms 发一次数据场里带设备状态和累计计数。接收方如果超过 300ms 没收到心跳就认为节点离线或故障。计数器的设计有个细节不要每次从 0 开始而是连续加一到 255 后回绕。接收方可以通过“计数器有没有变”来判断数据是不是新帧。有时候总线很繁忙接收方可能连续收到两帧内容一样的报文如果只比较数据内容就会以为是重复帧实际上可能是两帧不同时刻的相同状态。带上序号就能区分。超时处理和序号要成对出现。接收方保存“最近一次收到序号”如果新序号和旧序号相同可能是发送方卡死或者重发了如果序号跳跃说明中间丢帧需要做丢帧统计和报警。3. 控制器配置与位时序别让协议死在物理层3.1 波特率、采样点、SJW、BS1/BS2 到底怎么配自定义协议设计得再好如果 CAN 控制器采样点配得不对线上就会出现偶发错误帧尤其是线缆较长、节点数较多时。CAN 每一位的时间由四段组成同步段SYNC_SEG、传播段PROP_SEG、相位缓冲段1BS1和相位缓冲段2BS2。采样点就在 BS1 和 BS2 交界处。STM32 的库函数里经常只让你填 BS1、BS2 和 SJW背后的传播段被并到 BS1 里了。采样点推荐范围是 75% 到 85%。常见的 500kbps我一般会配成预分频 Prescaler 2假设 APB1 时钟 36MHzCAN 时钟 36MHz。BS1 9 TQBS2 6 TQSJW 1 TQ。每一位总 TQ 1 9 6 16采样点 (1 9) / 16 62.5%。等一下这个采样点偏低。我更常用的 500k 配置是Prescaler 4CAN 时钟 36MHz则位时间 36MHz / 500k 72 TQ。实际上 STM32F103 的 CAN 外设是挂载在 APB1 上的如果 APB1 是 36MHz想得到 500k预分频设 4位时间 18 TQ。如果 BS1 13BS2 4SJW 1采样点 (113)/18 77.8%比较理想。SJW 的作用是补偿总线上的相位偏差。线缆越长、节点时钟误差越大SJW 需要适度增大一般取 1 到 4 TQ。SJW 不是越大越好过大会让控制器对噪声更敏感反而增加误码。实际项目中如果不是特殊恶劣环境1 到 2 就够。3.2 STM32F103 的 CAN 初始化实例很多入门项目用的是 STM32F103我用 HAL 库写一个最小初始化配置方便直接对照。注意这是最常见的一套做法具体引脚和时钟要按你的板子调。CAN_HandleTypeDef hcan; void MX_CAN_Init(void) { hcan.Instance CAN1; hcan.Init.Prescaler 4; hcan.Init.Mode CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth CAN_SJW_1TQ; hcan.Init.TimeSeg1 CAN_BS1_13TQ; hcan.Init.TimeSeg2 CAN_BS2_4TQ; hcan.Init.TimeTriggeredMode DISABLE; hcan.Init.AutoBusOff ENABLE; hcan.Init.AutoWakeUp DISABLE; hcan.Init.AutoRetransmission ENABLE; hcan.Init.ReceiveFifoLocked DISABLE; hcan.Init.TransmitFifoPriority DISABLE; if (HAL_CAN_Init(hcan) ! HAL_OK) { Error_Handler(); } }几个配置点要注意AutoBusOff 开不开我建议开。自动恢复可以让控制器在 Bus Off 后主动重新进入 Bus On减少人工干预。但如果你的系统需要严格的故障记录也可以关闭然后自己在错误中断里做处理。AutoRetransmission 默认开启是好事。CAN 协议本身就支持发送失败后自动重发这个功能可以保留但如果做时间触发类协议建议关闭避免同一帧反复插队影响后续帧。ReceiveFifoLocked 默认关闭新的报文会覆盖旧报文。对大多数场景更合适。发送和接收在使用 HAL 时发送比较简单直接阻塞或中断发送一帧CAN_TxHeaderTypeDef txHeader; uint8_t txData[8] {0}; uint32_t mailbox; txHeader.DLC 8; txHeader.IDE CAN_ID_STD; txHeader.RTR CAN_RTR_DATA; txHeader.ExtId 0; txHeader.StdId 0x123; HAL_CAN_AddTxMessage(hcan, txHeader, txData, mailbox);接收端一般用中断加回调函数。先启动接收中断HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING);然后在回调里解析void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData); // 在这里按自定义协议解析 rxData }有一个很容易踩的坑回调里不要做耗时操作比如打印、Flash 写、复杂计算。CAN 中断频率可能很高回调里耗时太长会导致 FIFO 溢出丢帧。正确做法是把原始数据拷贝到自己的环形缓冲区然后在主循环里处理。3.3 收发缓冲、过滤器、中断与轮询如何选自定义协议里节点可能同时收发很多种 ID。STM32 CAN 控制器自带的硬件过滤器很好用它能让你只接收关心的帧其他帧直接丢弃减轻 CPU 负担。比如我只要接收 ID 0x100 到 0x10F 的帧可以配置成掩码模式屏蔽掉 ID 低 4 位固定高 7 位。这样过滤规则简单也不占用额外 SRAM。缺点是灵活性不够。如果节点要接收多个不同类型、不连续的 ID推荐用列表模式把关心的 ID 一个个列进去。中断和轮询的选择上接收我强烈建议用中断或 DMA轮询很容易丢帧。发送则看场景。高速周期发送可以放在主循环或定时器回调里如果系统非常复杂也可以用发送邮箱中断来确认发送完成。我见过一个反例项目里用了轮询接收主循环里做了一大堆电机控制逻辑结果 CAN 接收中断没开报文来的时候主循环正忙于其他计算FIFO 溢出整个系统像抽风一样。后来改成 FIFO 中断接收加环形缓冲问题立刻消失。4. 自定义报文的上位机解析与波形验证4.1 上位机怎么解析DBC、自定义脚本还是裸看写完协议烧进板子接下来就是验证。最直接的工具是 CAN 分析仪上位机软件。你会在软件里看到很多行报文每行有时间戳、ID、DLC 和数据。如果协议里没有定义任何解析规则你只能对着原始字节发呆。我的习惯是前期调试用裸看看原始字节对不对协议稳定后马上做解析表。解析表可以用 DBC 文件也可以用脚本。DBC 是汽车领域非常通用的格式CANoe、PCAN、周立功的软件大多支持。工业或者个人项目里我觉得 Python 脚本更快。写个简单的字典定义每个 ID 里各信号的起始 bit、长度、缩放、偏移量然后读日志批量解析非常方便。比如温度信号起始 bit 为 0长度 12缩放 0.1偏移 0有符号。解析代码很简单def parse_temp(raw): val raw 0x0FFF if val 0x0800: val - 0x1000 return val * 0.1有了这个基础再配合 CSV 导出和图表你就能快速发现某个信号在特定工况下是否跳变、是否有超范围数据。4.2 用逻辑分析仪/示波器看 CAN 波形上位机解析能看到协议层的语义但协议层显示正常不代表物理层正常。总线电平、隐形/显性位、采样点合理性这些必须靠示波器或逻辑分析仪确认。CAN 总线是差分信号CANH 和 CANL 之间的电压差决定了逻辑电平。显性位对应逻辑 0差分电压约为 2V隐性位对应逻辑 1差分电压约为 0V。示波器最好接成差分测量或者直接看 CANH-CANL 波形。如果手里只有单端探头也可以分别看 CANH 和 CANL但相位和噪声判断会麻烦一些。我调试一个 1Mbps 的 CAN 总线时示波器上看到波形上升沿缓得像上坡结果发现终端电阻被放到了 120Ω但节点数太多等效阻抗偏低。后来重新计算总线负载把终端电阻调整成合理的匹配方案波形立刻变干净。这个事让我意识到协议层永远看不出来物理层的问题两个环节都得验证。4.3 错误帧、Bus Off 与恢复策略CAN 总线出现错误时会有错误帧。分析仪上通常显示为总线错误或 CRC 错误。错误帧不一定是协议问题更常见的是波特率不一致、采样点不对、总线干扰、终端不匹配。Bus Off 的机制是这样的节点发送错误计数器达到 256 时控制器会自动离线。这是 CAN 硬件的一种保护策略它防止一个损坏节点持续污染总线。Bus Off 后控制器不再参与通信直到硬件或软件执行恢复流程。自定义协议里Bus Off 恢复策略很重要。简单粗暴的做法是硬件自动恢复立刻重新上线。但如果在强干扰下反复 Bus Off就会反复恢复、反复打断其他节点通信。更稳妥的做法是软件检测到 Bus Off 后先等待一段时间再请求恢复。如果连续恢复失败多次就拉高故障级别亮灯或上报。5. 实战血泪CAN 调试里最常见的坑5.1 初始化失败和打不开串口先查这几样很多人在电脑上接到“can not open com port”或设备管理器里找不到设备。先不要怀疑代码先查这几项USB 转 CAN 工具的驱动装没装串口号是不是被其他软件占用。波特率是不是和分析仪软件一致。这个看起来低级但真的能折腾半天。板子的 CAN 收发器供电是否正常TJA1050、MCP2551 这类芯片都有 VCC 引脚不供电相当于断线。初始化失败也要看有没有接终端电阻。CAN 总线至少需要两端各一个 120Ω 电阻没有终端电阻时通信极不稳定甚至完全不通。我有个习惯硬件回来先不跑完整协议先写一个回环测试。把 CAN 控制器的 Loopback 模式打开自发自收。如果回环能过说明控制器配置没问题再改成正常模式做板间通信。这样一层层缩小问题范围比一把梭快很多。5.2 仲裁不是 bug但你得会看很多人第一次看到总线上两个节点同时发送以为系统出故障了。其实 CAN 仲裁是设计特性。仲裁时多个节点同时发送ID 小的先赢得总线ID 大的那个节点自动转为接收模式等下一次总线空闲再重发。你看分析仪上的时间戳有些帧可能不是周期性出现的就是被仲裁延迟了。如果你的协议里心跳帧过多或者两个节点的发送周期太接近总线负载会很高低优先级帧被延迟得非常明显。这时候看分析仪只能看到“为什么这个帧没按时来”实际上是在总线上排队。5.3 终端电阻、共地、线缆长度的影响CAN 总线的距离和速度相关。低速 10kbps 能拉几公里高速 1Mbps 最多几十米。线缆越长信号反射越严重终端电阻要求越严格。终端电阻有两个作用一是匹配阻抗减少反射二是提供隐性电平的偏置。CAN 收发器在隐性位时总线电平靠电阻网络稳定在一个固定电位没有终端电阻隐性位容易漂移导致采样错误。共地问题也很常见。CAN 是差分信号理论上不依赖地线但收发器本身需要参考电位。如果两个节点电源隔离了但地没有连通CANH 和 CANL 的共模电压可能超出收发器允许范围出现一些“时好时坏”的通信问题。长距离传输时最好用隔离收发器比如 ISO1050 配合隔离电源。6. 把自定义协议做成“能演进”的协议6.1 版本号与向后兼容嵌入式产品经常有固件升级协议也可能要改。不改版本号的协议早晚变成一团乱麻。我自己的习惯是在设备上电后的第一帧或者诊断请求应答里包含协议版本号。比如数据场第 0 字节固定为版本号 0x01。接收方可以根据版本号决定用哪套解析表也方便排查“明明是同一批硬件为什么老设备和新设备相互读不懂”。另一个好习惯是预留位。数据场 DLC 用不到 8 字节时剩余字节不要随意填 0 或者不填建议统一填一个固定模式比如 0xAA方便在分析仪上判断这一帧到底有没有发完整。协议升级时预留字节可以用来扩展新功能。6.2 协议文档、信号表与 DBC 维护自定义协议裸奔一时爽过两个月没人看得懂就难受了。协议文档里至少要有节点表、报文 ID 分配表、信号定义表。每个信号要写清楚所属报文 ID、起始 bit、bit 长度、字节序、符号、缩放系数、偏移量、物理范围、初始值、无效值。这些信息最好用表格维护也可以导出成 DBC。DBC 文件在团队协作里价值很大尤其是有多个供应商或多人同时开发时。给每个信号起一个有意义的名字比如 “BMS_MaxVoltage” 而不是 “Signal1”后期维护成本会低很多。没有 DBC 工具时用 Markdown 表格维护也一样核心是信息完整、统一存放、有版本记录。6.3 回到一个最小可行协议最后说一个实在的建议。不要一上来就设计一个功能全包的协议。CAN 自定义协议最重要的是保持简单。一个最小可用自定义协议至少要包含一个固定的帧头或固定 ID 识别让接收方知道这是什么消息。明确的字节序和信号缩放规则。关键信号带 CRC 和计数器。心跳帧和超时判断。有了这些系统已经能稳定工作了。等真正需要诊断、固件升级、网络管理时再往协议里加功能。你加的每一条规则都要有人在未来维护。协议越简单出问题的概率越低排查起来也越容易。我在实际项目里吃过最大的亏就是一开始把协议设计得“功能强大”结果调试时根本分不清是硬件问题、控制器配置问题还是协议逻辑问题。后来把协议砍到刚好满足需求再逐块加回来系统才稳定下来。如果你现在正卡在 CAN 通信不畅、报文解析不对或者总线时不时报错不妨回头把协议文档重新整理一遍重点看 ID 分配、字节序、采样点这三处。大多数问题都藏在这三个看似简单的地方。
返回列表