ARTICLE DETAIL

资讯详情

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

CAN自定义协议设计从入门到实战:ID规划、数据场布局与错误恢复

CAN自定义协议设计从入门到实战:ID规划、数据场布局与错误恢复 做CAN通信的工程师几乎都会遇到这么一天手头设备用的MCU带CAN控制器总线也搭好了收发器波形拿示波器看完全正常但两边设备就是各说各话——A发的数据B收不到或者收到了也解析得乱七八糟。问题几乎都出在同一件事上没人规定报文里这些字节到底是什么意思。CAN协议栈只保证这8个字节能从一个节点送到另一个节点至于字节里装的是电压、转速还是故障码它一概不管。这就是自定义协议要干的事。这篇文章我会从CAN自带机制讲起把ID分配、仲裁、数据场布局、位时序、Bus Off恢复这些点串起来最后用一个完整的传感器采集链路做例子把自定义协议从需求到报文再到代码的完整过程走一遍。适合刚接触CAN通信的嵌入式工程师也适合已经在用CAN但每次都是问领导要一页协议文档、从来没自己设计过协议的朋友。1. 先搞明白你要设计的并不是CAN本身而是报文里装什么、怎么装很多新手把自定义协议想成需要修改CAN底层的东西其实完全不是。CAN的物理层和数据链路层已经被ISO 11898、CAN 2.0A/B这些标准定死了你能自由发挥的空间只在应用层这一个层面上。1.1 协议层和物理层是两回事别混在一起CAN总线的电气特性——差分电压、终端电阻、显性隐性电平——这些属于物理层跟你的协议没有半点关系。两根线上电压多少伏、怎么形成差分信号是收发器芯片管的事你设计协议时完全不用操心。数据链路层负责把你要发的数据打包成标准帧、加CRC校验、处理仲裁和应答这部分由CAN控制器硬件完成。你真正要设计的是帧结构里这些字段怎么用一个标准CAN数据帧显性部分依次是帧起始SOF1个显性位、仲裁域标准帧是11位ID加RTR位、控制域IDE位、DLC数据长度代码、数据域0~8字节、CRC域、ACK域、EOF帧结束。也就是说CAN控制器给了你两个自由发挥的入口仲裁域里的11位或29位ID数据域里的0~8个字节自定义协议本质就是回答两个问题ID怎么编码数据字节怎么编排所有你看到的CANopen、J1939、DeviceNet不过是把这两件事做了标准化而已。搞清楚了这一点再看任何协议都不会觉得玄。1.2 自定义协议必须回答的三个问题第一个问题谁能说话也就是节点如何分辨接收到的ID是自己感兴趣的。CAN没有类似UDP的端口号、也没I2C的设备地址全靠ID本身来过滤。硬件验收滤波器比如STM32的FIFO过滤器能按ID范围或掩码筛选报文协议设计时必须留出ID分区的余地。第二个问题一句话怎么说完8个字节要表达电压12.34V电流5.67A温度45.2℃状态正常这些信息每一段怎么放、用什么单位、需不需要偏移和缩放全得先说清楚。比如温度传感器输出0~100℃对应ADC值0~4095是直接发原始ADC值还是转成0.1℃为单位这些约定就是协议的核心。第三个问题说错了怎么办报文丢失、节点掉线、数据冲突怎么发现怎么恢复。CAN控制器本身有CRC和ACK机制这是数据链路层的保障但收发双方的状态同步、超时判断、出错重发策略都需要在协议层面自行约定。想明白这三个问题协议设计就有骨架了。2. ID和仲裁自定义协议的第一张设计图纸在CAN总线上每帧报文的ID不仅是身份的标识更直接参与总线仲裁。仲裁机制是CAN相对其他总线最独特的地方不理解仲裁机制设计ID后面的麻烦会接踵而至。2.1 要理解ID设计先理解总线仲裁怎么工作CAN总线是线与结构总线空闲时所有节点都能发起发送。如果两个节点同时开始发送就靠ID逐位仲裁分胜负ID较小显性位更多的报文赢得仲裁ID较大的节点自动转为接收等总线空闲再重新发送。举个最直白的例子节点A要发ID0x100的报文节点B要发ID0x200的报文两帧同时上总线。因为0x100比0x200小在ID的第8位最高有效位比较时A的位是0显性B的位是1隐性显性电平覆盖隐性电平A直接赢B老老实实等下一轮。这个机制的代价是ID越小优先级越高。所以设计ID时最重要的原则就是——把时间关键的报文放在小的ID号段上。比如控制类报文、故障类报文要有最高的优先级周期性传感器数据次之配置类、诊断类报文优先级最低。2.2 标准帧还是扩展帧优先级不要做无谓的牺牲11位标准ID和29位扩展ID主要区别就是ID容量。扩展帧能容纳的节点和报文类型更多但它的优先级和带宽占用都稍逊。29位ID的仲裁域更长一帧报文在总线上占用的时间也略多。对于大部分中小型项目十几个节点几十条报文11位标准帧完全够用。只有在节点多、报文类型多、需要兼容某些国际标准协议时才考虑扩展帧。值得提醒的是很多人图省事直接选扩展帧结果ID排序、过滤和仲裁复杂度都上去了却只用了几十个ID得不偿失。我自己做过一个项目最开始定成扩展帧最后全部ID加起来不超过30个后来改回标准帧过滤配置和调试工具都清爽很多。2.3 ID分配策略按报文类型划分还是按节点划分这是设计ID时最关键的分歧点。按节点划分每个节点分一段ID比如节点1用0x100~0x13F节点2用0x140~0x17F。这种方式的优点是出问题好排查——一看ID就知道是哪个节点发的缺点是同一类型的报文分散在各段接收端要配置多条过滤规则。按报文类型划分比如所有转速类报文集中在0x100~0x11F所有温度类报文集中在0x120~0x13F。优点是接收逻辑统一、过滤规则简明缺点是调试时需要额外的映射关系表才能定位节点。我的习惯是小系统少于5个节点按节点分大系统多于10个节点按类型分中等规模用混合方式——高优先级段0x000~0x0FF给控制/状态类报文中段给数据采集低端给配置和诊断。下面是一个例子ID段报文类型典型用途优先级等级0x000~0x0FF实时控制/故障电机控制指令、急停、故障上报最高0x100~0x1FF核心状态量转速、电流、电池电压高0x200~0x2FF传感器采集温度、湿度、气压中0x300~0x3FF配置/诊断参数读写、固件信息低2.4 一个接收过滤的实际配置注意点ID定完后一定要重新算一遍过滤器的掩码和模式。ST的bxCAN支持两种过滤器模式标识符列表模式和掩码模式。列表模式要求收到的ID必须精确匹配掩码模式则只关注ID中受掩码保护的位。如果按类型分段后想收0x2xx的所有报文掩码应该设置为0x700意思就是低8位不关心只匹配高3位的0x2。这个设计要在ID规划时同步考虑否则出现两条报文ID分配不规律掩码过滤就没法统一只能靠软件逐条判断白白增加CPU负载。3. 数据场布局8个字节怎么安排才科学ID解决的是这个报文是谁、要干什么数据场解决的是里面每个bit代表什么物理量的问题。8个字节看着不多设计不好解析代码会写得非常痛苦。3.1 信号定义的三要素缩放、偏移、字节序假设你要发送一个温度值传感器量程-40℃~125℃精度要求0.1℃。如果直接发浮点数8个字节只够装两个浮点浪费得离谱。正确做法是用整型数加缩放因子表示浮点值。比如用有符号16位整数表示温度1个bit代表0.1℃范围就是-3276.8℃~3276.7℃完全够用。接收端拿到原始值乘以0.1就是实际温度。这就引出了信号定义的三个关键要素缩放因子Factor原始值乘以该因子得到物理值偏移Offset物理值 原始值 × Factor Offset字节序Byte Order高字节在前Big Endian还是低字节在前Little Endian字节序是新手最容易踩坑的地方。比如发送一个0x1234这个16位值Intel格式小端低字节在前发送时第一个字节是0x34第二个是0x12Motorola格式大端则相反。协议文档里必须明确写清字节序收发两端用不同的解析工具时尤其要注意。我的经验是除非整个团队统一用大端否则新项目一律推荐小端因为大多数MCU原生就是小端省去转换代码。3.2 用DBC文件把协议固化下来数据场设计好后一定要形成DBC文件。DBC是CAN通信领域通用的数据库格式几乎所有CAN工具CANalyzer、PCAN-View、周立功CANTest等都支持导入。它描述的是哪个报文ID含有哪个信号、信号在数据场里的开始位、长度、缩放、偏移、取值范围、单位等信息。下面是一个简单的DBC片段VERSION NS_ : NS_DESC_ CM_ BA_DEF_ BA_ VAL_ BU_ BO_TX_BU_ BA_DEF_DEF_ BA_DEF_REL_ BA_DEF_SGT_ BA_DEF_REL_ BA_DEF_SGT_ BA_DEF_DEF_REL_ BA_DEF_DEF_SGT_ BU_ BO_ SG_ EV_ ENVVAR_ SGT_ SGTYPE_ SIG_ VAL_ BS_: BU_: Node1 Node2 Node3 BO_ 256 MotorSpeed: 8 Node1 SG_ Speed_Actual : 0|161- (0.1,0) [0|8000] rpm Node2,Node3 SG_ Temp_Motor : 16|81- (1,-20) [-40|125] degC Node2,Node3在这个例子里0|161-的意思是从数据场的第0位开始长度16位1代表Intel格式小端-代表无符号数。这样CAN工具解析报文时就能直接显示转速是123.4rpm而不是一个莫名其妙的0x04D2。在项目一开始就维护DBC文件比什么都强。我见过不少团队协议都在工程师脑子里换个人接手就全乱套。DBC文件不仅是文档还能直接从工具里导出C语言结构体、Simulink模型是协议设计里最划算的一笔投入。3.3 数据场里如何放多帧数据帧ID和数据类型当一条报文的8个字节无法满足需求常见做法是多帧组合。有两种思路第一种是把一条长数据拆成多帧每帧数据域的第一个字节作为帧序列号比如0代表首帧1、2代表后续帧接收端按序列号重组。第二种是把同一类型的数据每帧放一个数据段比如一条报文叫电池信息第二段放电压、电流第三段放SOC、SOH通过报文ID区分通过帧内偏移组合数据。第一种适合大块数据比如固件升级要传几K字节第二种适合周期性采集类数据。如果项目里两种需求都有建议把数据类型标识放数据域第一个字节后面再跟数据。这样报文ID就不用拆得过于零碎过滤规则也简单。4. 位时序与采样点协议稳定性的隐藏地基很多人设计完ID和数据场就以为协议搞定了结果一上总线数据率稍微高一点就疯狂报错、丢帧示波器看波形又是好的。问题很可能出在位时序参数上——波特率、采样点、SJW这些参数设置不对导致节点在错误的时间点采样总线电平把本来正确的位判错。4.1 波特率不是想定多少就定多少CAN波特率范围虽然宽最高1MbpsCAN FD更高但实际取值要考虑总线长度、节点数量、收发器性能、MCU时钟精度。经验法则波特率越高总线长度越短。1Mbps下总线长度建议不超过40米125kbps下可以到500米左右具体数值跟线缆质量、节点数都有关系。选波特率时还要考虑所有节点的晶振/时钟误差。CAN控制器每个位都要采样如果两个节点时钟偏差太大采样点就会不断漂移。低成本MCU用内部RC做时钟源时精度一般只有1%~2%高波特率下很容易出错。正规做法是用外部晶振误差控制在0.5%以内。4.2 采样点为什么重要一个数学层面的解释CAN每个位时间分四个段同步段SYNC_SEG、传播段PROP_SEG、相位缓冲段1PHASE_SEG1、相位缓冲段2PHASE_SEG2。采样点出现在PHASE_SEG1和PHASE_SEG2的交界处。采样点的位置直接决定了容错能力。推荐采样点设置在75%~85%也就是说在一个位的后半段采样。这样做的原因信号的跳变沿集中在位的前半部分采样点靠后能避开跳变边沿减少误判概率。具体计算时位时间 (BRP分频 SYNC_SEG PROP_SEG PHASE_SEG1 PHASE_SEG2) × Tq。比如STM32F103APB1时钟36MHz配1Mbps波特率可以这样配// CAN_BTR寄存器设置采样点约80% // BRP3 得到 36MHz/(31)9MHz即Tq111ns // 位时间 9 Tq采样点 (16)/9 ≈ 78% // SJW1 CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_6tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler 3;为什么BS1取6、BS2取2因为采样点(SYNC_SEG BS1) / (SYNC_SEG BS1 BS2) (16)/(162) ≈ 78%落在推荐区间。不同波特率下要重新计算不能照搬一组参数到处用。另外同一总线上所有节点的采样点必须一致否则高速时必然出错。4.3 SJW同步跳跃宽度不要忽略的小参数SJW用于在总线出现边沿时调整采样点补偿节点间的时钟偏差。SJW越大能容忍的偏差越大但过大也会降低抗干扰能力。一般取1~2 Tq即可不要把SJW设得过小否则总线上一旦出现噪声引起的边沿跳动同步能力不足就会连续采样错误。做总线测试时如果发现偶发CRC错误先排查SJW和BS1/BS2再查硬件。4.4 和can not open com port这类问题的区分用USB-CAN适配器调试时经常会遇到can not open com port或者打开串口失败的情况。这不是CAN总线问题而是USB转串口驱动、设备占用这类PC端问题。排查思路一般是先看设备管理器认没认到设备再确认COM口有没有被其他软件占用最后检查波特率设置是否和固件匹配。这类问题反复出现的话建议直接把调试工具的驱动统一更新到最新版能省不少时间。5. 错误处理与Bus Off恢复策略协议要能扛揍CAN控制器自带五类错误检测机制位错误、填充错误、CRC错误、格式错误、应答错误。这些机制能把错误帧检测出来但如何统计错误、何时进入Bus Off、如何恢复默认行为不一定适合你的应用场景。5.1 CAN控制器的错误状态机每个CAN节点都有两个错误计数器发送错误计数器TEC和接收错误计数器REC。根据计数值划分三个状态错误主动Error ActiveTEC和REC都小于128节点正常发送和接收错误被动Error Passive至少一个计数器大于127节点只能发被动错误标志Bus OffTEC大于255节点完全退出总线不发送任何报文关键点在这里默认情况下Bus Off后控制器要等待128次总线空闲11个连续隐性位才会恢复如果不去干预节点可能长时间掉线。而自定义协议如果依赖这个节点持续上报状态这段时间就是一个巨大的空窗期。5.2 协议层面的恢复策略设计我的做法是在协议里明确约定三种处理第一快速恢复机制。比如STM32的bxCAN在CAN_ESR里的BOFF位被硬件置1后软件可以主动重新初始化CAN外设把TEC/REC清零立即恢复发送。但注意如果总线本身短路或一直被干扰快速恢复会导致反复Bus Off不断冲击总线。所以更稳的做法是先检测错误原因如果只是瞬时干扰快速恢复没问题如果是持续故障则停止恢复并进入故障状态上报。第二节点状态上报。协议里应该规定每个节点周期性发送心跳报文或状态报文接收端如果在规定时间内收不到某节点的状态就判定该节点离线。这段超时时间要跟心跳周期匹配一般取3~5个周期时长。这样总线上任何节点掉线整条链路都能快速感知。第三错误类型区分。不是所有错误都要走严重处理。ACK错误发出去的报文没人应答通常只是目标节点暂时不在线填充错误和CRC错误说明总线数据传输被干扰需要关注物理层。协议文档里建议写明不同错误码对应的处理动作而不是让每个节点各自发挥。5.3 硬件上的冗余思路更高可靠性的应用比如车用域控制器还会在物理层面做冗余双CAN通道主通道故障时切到备份通道或者总线末端加两个滤波器、两个终端电阻。协议设计时要考虑这种切换机制比如主备通道上的报文ID可以完全相同接收端靠通道标识来区分数据来源。6. CAN FD数据量大时要不要升级协议如果你在设计新协议时发现8个字节经常不够用、波特率又被长度限制就该考虑CAN FDCAN with Flexible Data-rate了。6.1 CAN FD和经典CAN的三个核心区别第一个区别是数据场长度从最多8字节提升到最多64字节大数据包的传输效率大幅提升。第二个区别是波特率可变仲裁段可以保持原来的中低速保证兼容性数据段可以用更高的速率传输比如仲裁段1Mbps、数据段5Mbps。第三个区别是数据场多了CRC保护特别是超过16字节的数据场用了17位或21位CRC误检率更低。注意CAN FD不能和经典CAN混布在同一总线上互通——物理层虽然一样但帧格式有区别。好消息是经典CAN节点能容忍CAN FD帧存在即收到不认识的帧格式就当作错误帧或忽略但不会解析。所以你要升级到CAN FD所有节点都得支持或者用网关做协议转换。6.2 什么时候该上CAN FD判断标准很简单单帧数据超过8字节且总线负载率接近饱和或者控制周期太紧需要更高吞吐。比如电池管理系统BMS要上报几百个电芯电压、温度用经典CAN要占大量帧和ID换成CAN FD一帧就能塞下。又比如域控制器之间交换摄像头数据经典CAN的带宽完全不够CAN FD才勉强能满足。反过来如果只是几个传感器周期上报经典CAN完全够用没必要为了新而上CAN FD因为FD模式下数据段的波特率增加对线缆、接插件、终端匹配的要求也更高出了问题排查难度更大。6.3 CAN FD协议设计的额外注意事项在数据场布局上CAN FD除了长度增加还可以按64字节连续排布信号量很大建议按功能域分批定义并保持DBC同步更新。另外CAN FD要求显性位和隐性位的边沿时间严格一致尤其在数据段因此布线时对分支长度更敏感终端电阻的处理也更讲究。位时序方面CAN FD的采样点设置和经典CAN类似但数据段的位时间更短对SJW和BS1/BS2的时间精度要求更高。如果使用较老的MCU只支持经典CAN硬件不兼容要换带CAN FD控制器的型号。7. 一个完整例程三节点传感器采集协议从需求到代码理论部分讲了不少下面用一个实际的例子串起来。假设我要做一套环境监测系统三个传感器节点采集温湿度、气压、光照通过CAN总线上报给一个主控节点主控节点做显示和存储。7.1 需求分析和ID规划通信需求三个采集节点各自周期性100ms周期上报温湿度和气压光照节点上报光照强度主控节点可以下发采集频次设置命令。ID分配就按之前说的混合策略报文ID方向优先级采集频次设置命令0x100主控 → 传感器高节点1温湿度/气压0x200传感器1 → 主控中节点2温湿度/气压0x201传感器2 → 主控中节点3温湿度/气压0x202传感器3 → 主控中光照上报0x210光照节点 → 主控中心跳帧0x300所有节点 → 主控低采集命令用0x100比上报数据小所以能抢占优先级主控下发的命令不会被数据帧堵住。心跳帧ID最大代表最低优先级不影响主业务。7.2 帧结构定义以节点1的温湿度/气压上报为例数据场8字节这样安排字节偏移长度含义缩放/偏移单位取值范围0~116位温度原始值×0.1 / 0℃-400~12502~316位湿度原始值×0.1 / 0%RH0~10004~516位气压原始值×1 / 0Pa30000~11000068位节点状态位无-0x00~0xFF78位预留无-0x00为什么温度用-400~1250而不是-40~125因为放大10倍后-40.0℃对应-400125.0℃对应1250用有符号16位整型存绰绰有余。如果直接用浮点得占4个字节一帧根本塞不下三个量。7.3 关键代码实现发送和接收发送端STM32 HAL库示例核心逻辑// 组包把传感器数据填入CAN发送缓冲区 typedef struct { int16_t temp; // 单位0.1℃ uint16_t humi; // 单位0.1%RH uint32_t pressure; // 单位Pa uint8_t status; } SensorData_t; #define CAN_ID_NODE1_TEMP 0x200 void CAN_SendSensorData(CAN_HandleTypeDef *hcan, SensorData_t *data) { CAN_TxHeaderTypeDef txHeader; uint8_t txData[8]; uint32_t mailbox; txHeader.ExtId 0; txHeader.StdId CAN_ID_NODE1_TEMP; txHeader.IDE CAN_ID_STD; txHeader.RTR CAN_RTR_DATA; txHeader.DLC 8; // 小端模式低字节在前 txData[0] >void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; SensorData_t sensor; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData); if (rxHeader.StdId CAN_ID_NODE1_TEMP) { // 解析小端还原 sensor.temp (int16_t)(rxData[0] | (rxData[1] 8)); sensor.humi (uint16_t)(rxData[2] | (rxData[3] 8)); sensor.pressure (uint32_t)rxData[4] | ((uint32_t)rxData[5] 8) | ((uint32_t)rxData[6] 16); sensor.status rxData[7]; // 缩放还原物理值 float tempDegC sensor.temp * 0.1f; float humiRH sensor.humi * 0.1f; float pressurePa (float)sensor.pressure; // 业务处理显示/存储/判断是否超限 ProcessSensorData(sensor, tempDegC, humiRH, pressurePa); } }注意这里我把temp定义成int16_t这样负数零下温度也能正确处理。很多人会想当然地把温度定义成uint16_t结果零下温度解析出来是个巨大的正数排查半天才发现是符号位丢了。协议文档里每个有符号信号都必须显式标注有符号。7.4 主控节点的过滤配置主控节点只关心三路传感器数据和光照数据可以配置两个过滤器过滤器0掩码模式掩码0x7F0只匹配ID高5位为0x20x200~0x2FF这样0x200、0x201、0x202、0x210都能收进来过滤器1列表模式精确匹配0x100命令应答如果需要这个过滤配置依赖ID分配的一致性——如果当初ID乱排0x210和0x200之间其他ID没有规律掩码过滤就会收到一堆无关报文。这也是为什么前面反复强调ID规划要从全局出发。7.5 组包解析的通用技巧代码写多了会发现报文解析本质上是位操作游戏。为了减少错误我有几个习惯第一所有组包/解析代码用一个单独模块封装对外暴露结构体业务层不准直接碰txData和rxData。这样协议变动时只需改这一个文件。第二在协议升级时保留兼容性。比如旧版本温度字段从单字节升级到双字节新解析代码要能处理旧帧通常的做法是给帧加版本号字段。我见过最无语的情况是协议改了一版老设备没升级固件结果主控收到旧格式解析出来全是乱码还愣是排查了半天硬件。第三写个小工具做协议验证。只靠逻辑分析仪看原始16进制数据太痛苦用Python写个简单的CAN报文解析脚本或者把DBC导入CAN工具直接在PC上解析实时报文效率完全不是一个量级。8. 调试工具与总线测量协议上线前的最后一道关协议设计完代码写完并不代表能直接稳定运行。上线前的调试和测量这关必须过否则上了产线或现场才开始排错成本高得多。8.1 必备的调试工具和信号波形观察最少要准备一个USB-CAN适配器比如周立功、PCAN这类百元到千元不等、CAN调试助手软件、示波器。示波器用来看总线物理波形调试助手用来做协议层的发送接收验证。把示波器探头接在CAN_H和CAN_GND之间能看到显性位约2.5V、隐性位约2.0V的波形这就是差分信号的基础。如果示波器上看到的波形幅值不够、边沿太缓、或者有振铃先别急着查协议优先解决物理层。常见的电压异常如下现象可能原因处理方向CAN_H对GND电压恒为0V收发器没供电或线路断路查供电、查接线CAN_H和CAN_L短路线缆破损、接插件问题分段排查差分幅值小于1.5V终端电阻缺失、线路过长检查120Ω终端电阻波形边沿振铃明显分支过长、节点过多改善布线、加终端8.2 总线负载率预留余量是必要的总线负载率是协议设计时容易被忽略的指标。计算方法很简单一帧报文总位时间 × 周期发送次数 / 波特率。比如100ms周期、8字节数据、1Mbps下一帧大约130位单个节点占用0.13%100个这样的节点就是13%。工程上建议最大负载率控制在50%以下因为负载率太高总线上的冲突和重发会急剧增加实时性腰斩。8.3 实测中的玄学错误八成是电源地的问题实际项目中CAN通信不稳定最常见的原因其实不是协议而是地。示波器看CAN_H和CAN_L波形一路都是标准的但总线依旧偶发错误。这时候查节点间的共地情况多个节点必须共地CAN差分信号虽然抗共模干扰但共模电压超过收发器耐受范围通常±12V左右一样收不到。我曾遇到一个案例两个节点相距50米电源是各自开关电源没有共地。一到深夜电压波动大通信就开始丢帧。后来给两个节点的GND加了一条粗线问题立刻消失。所以设计协议和布线的同时一定要把电源和地线的规划考虑进去。9. 协议文档的维护写清楚才能走得远最后说一个很多工程师都不重视、但实际最能省事的环节协议文档。9.1 一张协议表该包含什么每个报文至少要有报文ID十六进制和含义、发送周期或事件触发条件、发送节点、接收节点、DLC、每个信号的名字、起始位、长度、字节序、缩放因子、偏移、单位、取值范围。这些信息千万别写在微信聊天记录或者口头约定里一定要进版本管理。我自己维护的项目协议文档跟代码放同一个仓库每次改了协议代码和文档一起提交review的时候也一目了然。这比文档写了一份代码早就改成另一版的混乱状态强太多。9.2 版本兼容性策略协议升级时尽量做到向后兼容。如果需要变更某条报文的数据场布局优先使用新ID而不是改老ID老节点收不到新ID也不会报错。如果必须复用同一个ID则在数据场里加一个协议版本号字段接收端先读版本号再决定解析逻辑。9.3 测试用例也要沉淀协议设计完成后把关键测试用例写下来每条报文的正常解析、边界值比如温度最小值和最大值、错误帧触发、节点掉线检测、Bus Off恢复、总线负载率测试。这些用例既是验收标准也是以后改协议时的回归测试基础。没有测试用例做保障的协议改一次就慌一次。我自己在写完CAN协议代码之后一般会先在测试板上把节点离线的场景模拟一遍热插拔、短路CAN_H和CAN_L、总线只留一个节点发报文不去应答看看协议能不能按设计兜底。这些场景看着土但很多现场问题就是这么暴露出来的。协议设计这个事说难不难说简单也不简单。难在它需要你把数据链路层的机制、应用层的需求、可维护性、可测试性全都想清楚不难在一旦把ID、数据场、时序、错误恢复这些组件分别理顺剩下的就是拼装。我做了这么多年的CAN通信项目最深的体会就是一份好的自定义协议是让所有人省时间的协议——不只是收发两端还包括三个月后回来改bug的你。
返回列表