1. 协议设计先想清楚:从裸报文到“业务语言”
前阵子有朋友问我,他手里的CAN节点已经能正常收发报文了,波特率、终端电阻、收发器都没问题,可一旦把两个以上设备挂在总线上,数据就乱套。A发出去的电压值,B读出来是错的;偶尔想给某个节点单独下发配置,其他节点也一起响应。他问我是不是滤波器设置不对,我说你先别急着调滤波器,你缺的是一个真正属于自己的自定义协议。
这个问题太典型了。很多人把CAN通信理解成“能发能收就算通”,但实际上CAN只是给你提供了一条最底层的、带仲裁能力的报文搬运管道。它能保证的是:你塞进去的8个字节,只要通过了CRC校验,对端就能原样拿到。但它完全不负责解释这8个字节是什么意思、该发给谁、对方没收到怎么办、收到的是不是最新数据。这些全是协议层的事。
所以“CAN自定义协议如何设计”,本质上是回答这么一个问题:如何在CAN这8字节小包上,建立一套适合你自己业务的语言规则。这套规则要覆盖数据格式、节点寻址、错误发现、时序约定,甚至要考虑到总线仲裁和时钟误差这些物理层约束。
这篇文章会把自定义协议从0到1完整拆一遍,包括帧ID怎么规划、8字节怎么布局、校验和重传怎么做、位时序误差怎么算、工程调试中有哪些坑。适合正在做车载、工业控制、机器人、仪器仪表里CAN通信开发的工程师,也适合刚入门想搞懂CAN协议本质的嵌入式学习者。
2. 逐字段打磨:CAN自定义协议的具体设计要点
2.1 帧ID规划:优先级、过滤和路由本来就是一件事
很多人设计协议时,第一个纠结的就是CAN ID怎么分配。这个纠结非常值得,因为ID在CAN协议里身兼数职:它决定报文优先级、决定接收方如何过滤、在工程上还承担路由表的作用。
先说优先级。标准帧的ID是11位,扩展帧是29位。ID数值越小,仲裁时优先级越高。数据类报文一般要给一个比较低的ID,控制类报文优先级中等,故障类和安全类报文必须给最高优先级。比如我做电机控制器的时候,转速、电流这类周期性数据帧ID我习惯定义在0x200~0x2FF段,控制指令帧放到0x100~0x1FF段,急停和故障帧直接压到0x001~0x00F。这样哪怕总线上数据帧疯狂抢占,急停帧也能在几个微秒内抢到总线。
再说过滤和路由。CAN控制器的硬件过滤器按ID段匹配,所以你的ID规划必须考虑“能否用掩码把一组相关报文框进来”。我习惯把ID拆成两段:高几位当“报文类型/源地址”,低几位当“目标地址”。比如用标准帧11位,可以这样分:bit10~bit8表示优先级和类型,bit7~bit3表示源节点地址,bit2~bit0表示目标节点地址或服务类型。这样接收节点只要配置一个掩码,就能把所有发给自己的帧收进来,其他帧直接硬件过滤掉,节约CPU时间。
还有一个经常被忽略的细节:同一条总线上,尽量让周期性报文的ID不要重叠,更要保证任何两个帧的ID不冲突。因为CAN仲裁不是“谁先发谁赢”,而是“谁ID小谁赢”。两帧同时发送时,ID小的会毫发无损地继续传输,ID大的那帧直接被总线“干掉”,下次重发。如果两帧ID完全相同,那就会发生位错误,直接报错帧。设计列表时建议列一张完整的ID分配表,宁可留空段,不要强行填满。
2.2 用满8字节:数据字段布局与字节序
CAN数据场的长度是0到8字节(CAN FD最长64字节),经典CAN下,绝大多数协议都是采用8字节定长的标准格式。为什么要定长?因为8字节定长让接收方处理逻辑最简单——一个周期信号到了,直接按偏移取数,不需要判断“这帧里面放几个字段”。
字段布局的第一原则是:把最核心、最需要实时性的数据放在前面。CAN的数据传输是从第0字节到第7字节按顺序发的,虽然在一帧内部延迟差只有几十微秒,但对于同步要求极高的多轴协调控制,前排字段的相位差就是要更小一点。
第二原则是:一个物理量尽量不要跨字节边界存储时发生混乱。比如一个16位电压值,占用两个字节,你必须在协议文档里明确规定是大端还是小端。这里我强烈建议:车载和工业场景,统一用大端(高字节在前)。原因是CAN报文在总线上的字节顺序是从第0字节开始逐字节传输,而且很多CAN分析工具展示报文时习惯把第0字节放在最左边。你打开CANalyzer或者周立功的上位机,看一段十六进制报文,左起第一字节就是byte0,用大端解析时,uint16就是把byte0左移8位或上byte1。这样人工排查报文时非常直观。
第三原则:预留字段一定要有明确填充值。那些暂时没定义的位,发送方必须填0,接收方不要对它们做任何业务判断,只做保留。这一点看似简单,实际上很多协议后期扩展出问题都是因为预留位被当成“空闲位”随意填了垃圾数据,导致新旧版本设备不兼容。
第四原则是DLC。如果只有两个字节的有效数据,DLC到底是写2还是8?我建议:除非总线负载率极其紧张,一律写8。因为CAN帧无论数据字段是1字节还是8字节,帧头、CRC、ACK这些开销是固定的。在经典CAN里,数据长度为8的帧也就比长度为1的帧多了大约13个位的传输时间。相比这点时间开销,固定DLC带来的解析简化完全值得。你不需要在接收端写“若DLC=2则取字节0~1,若DLC=5则取字节0~4”这样的分支逻辑,直接把8字节全部接收,未定义的字节当保留字节处理即可。
2.3 填充、计数与校验:不显眼的字段决定稳定性
在协议里留出“帧计数器”和“校验字段”,很多人觉得没必要。我一开始做项目时也觉得,CAN帧本身有CRC15校验,硬件都帮你查了错误,软件层再搞校验不是多余吗?踩过坑之后才明白,这完全是两码事。
CAN物理层的CRC15校验,解决的是“传输过程中位被干扰”的问题。它不解决“发送的是旧数据”和“字节顺序对但业务逻辑错”的问题。比如传感器节点每隔100ms发一次温度数据,第5帧发的是50℃这个值,主控已经处理了;接着第6帧因为某种原因没发出来,第7帧才把最新的52℃发出来,从CAN底层看,这两帧都是合法的、CRC都通过,主控无法区分这到底是不是最新数据。这时候帧计数器就有用了。约定每发一帧计数器加1,接收方发现计数值跳变,就能知道“中间丢了一帧”,从而触发重发请求或同步刷新策略。
校验字段方面,推荐使用累加和、CRC8或CRC16。经典CAN一帧最多8字节,留给校验的空间不大,一般用一个字节做校验。我常用的做法是:字节0放帧头和命令,字节1放帧计数,字节2~6放业务数据,字节7放前面7个字节的异或和或CRC8。用异或和很简单,代码量少,适合MCU资源紧张的项目;CRC8抗突发错误能力更强,适合对数据可靠性要求高的场合。如果项目里数据长度超过8字节,那就是多帧传输问题,要引入序列号、总帧数、帧序号、包类型(首包/续包),复杂度上一个台阶,后面会专门讲。
校验字段还有一个容易被忽略的作用:它帮你区分“这帧数据到底是新发的,还是上一帧的重复”。在总线上一模一样的两帧数据,如果连校验值都一样,接收方很难识别这是新数据。加入帧计数后,这个问题迎刃而解。
2.4 超时与重复策略:把“不通信”也算进协议
协议设计到最后,很多人会忽略一条最关键的原则:协议的最终目的不是“让所有报文都通”,而是“让系统在异常情况下可预期地降级”。
车身控制器和发动机控制器之间有100个信号,其中98个是周期性发送的,2个是事件触发的。协议文档写得很清楚每个信号的意义,但你没写“如果收不到这个周期帧该怎么办”。结果就是发动机转速这个核心信号一丢失,整车控制器还在傻乎乎地等下一帧,期间输出转矩指令毫无变化,最后驾驶体验直接拉垮。
设计协议时,每个周期类报文都要定义一个“超时阈值”和“超时动作”。比如某个转速帧约定每10ms发一次,超时阈值设为50ms(连续丢5帧判超时),超时后主控要进入“转速信号无效”逻辑,转速值标记为无效,同时把默认的转矩指令降到安全值。这个超时时间不能太短,因为CAN总线在高负载时会偶尔丢帧,连续丢一两帧就触发降级,误报率太高;也不能太长,否则系统反应太迟钝。我一般取发送周期的5到10倍。
事件类报文,比如故障报警、按钮按下,这类报文要约定“重复发送次数和间隔”。如果故障节点只发一次故障帧,接收方万一没收到,故障就无声无息地消失了。正确的做法是:事件触发后按20ms间隔连发3次,接收方收到第一帧就响应,后两帧用于确认和冗余。接收方还要在一个固定时间窗口内没有收到任何跟某个节点相关的帧时,判定该节点离线,向应用层上报“节点失联”。
3. 时间参数与仲裁:把物理层限制变成设计约束
3.1 位时序和采样点:为什么波特率越高越要抠时钟误差
自定义协议不只是“数据怎么组织”,你定义完字段后,要面对的总线物理层问题一点不比数据层少。最典型的就是波特率和时钟误差的博弈。
每个CAN节点里都有一个晶振或时钟源,用来产生CAN控制器的位时序。比如你配置500kbps波特率,总线速度为每比特2微秒,控制器会把2微秒拆成若干个时间段(TQ),分别用于同步段、传播段、相位缓冲段1和相位缓冲段2。采样点就落在这几个段的交界处。理想状态下,所有节点的采样点应该在同一个位置,但实际晶振都有误差,普通晶振误差一般是±50ppm到±100ppm,便宜的陶瓷谐振器甚至到±0.3%。
当总线上有100个节点,每个节点自己的位时间都有一点偏差,经过一段时间累积,采样点就会慢慢漂移。如果某个节点采样点正好落在位边沿附近,它就可能采到错误电平,然后报CRC错误或填充错误,甚至进入总线关闭状态。
这也是为什么“波特率越高,越要关注时钟误差”。500kbps时一个位只有2μs,相位缓冲段总共也就1μs左右;到了1Mbps,一个位只有1μs,采样点的容差空间直接减半。在这种高速率下,如果总线两端节点的时钟误差一个正一个负,最高支持的总线长度会急剧缩短。
设计自定义协议时,必须根据实际晶振精度来设定采样点位置。业内经典的推荐是采样点位于位时间的75%~87.5%之间,常规配置选80%,可靠性优先的场合选87.5%。具体计算方式:假设总线波特率500kbps,APB1外设时钟36MHz,那么一个位时间需要的TQ数就是72,采样点80%意味着采样点位于第58个TQ处。如果采样点设得太靠前(比如55%),总线上信号反射和跳变毛刺还没稳定,就容易误采;如果太靠后,留给相位缓冲段2的时间不够,重同步能力会变弱。
3.2 重同步机制:用同步段换稳定性
你可能会问,既然晶振都有误差,CAN又是怎么做到100个节点还能稳定通信的?这就得说到CAN的硬件重同步机制。
CAN每个节点在接收总线数据时,会不停地把本地的位时间与接收到的位边沿做比较。如果发现本地位时间慢了(总线的边沿比自己预期的来得早),就把相位缓冲段1拉长一点;如果发现本地位时间快了,就把相位缓冲段2拉长一点。这样每一帧传输过程中,每个节点的时钟都在被源源不断地“校准”。
这个机制的关键在于同步跳转宽度(SJW),它规定了单次重同步最多能调整多少个TQ。SJW设得越大,节点能容忍的时钟偏差越大,但也意味着抗干扰能力下降,因为在噪声环境下,它会把毛刺误当成有效边沿去重同步,反而加剧位错误。一般SJW设为1到4个TQ,常用2。
理解这个机制对协议设计的启示是:你在自定义协议里约定波特率和采样点后,必须把SJW等参数同步下发给所有节点,不能只配波特率而忽略SJW。我在调试中见过太多次“明明波特率一致,但两个不同厂家的设备死活通不上”的情况,最后排查发现是一个SJW配了1,另一个配了4,两者在总线边沿抖动时行为差异太大。协议驱动开发时,建议把位的各项参数(TQ总数、采样点位置、SJW)写进移植文档里,别只写波特率。
3.3 仲裁机制:优先级不是“想设几就设几”
CAN的自定义协议还有一个经常被低估的部分,就是仲裁机制对消息实时性的影响。
CAN的仲裁遵循“显性位优先”原则,就是说逻辑0(显性)能覆盖逻辑1(隐性)。这也是为什么ID越小优先级越高。但很多人不知道的是,数据帧和远程帧也有仲裁优先级区别,而且数据帧优先于远程帧。另外,标准帧和扩展帧混用时,标准帧的ID在仲裁时占优。
设计协议时,帧优先级分配的功力体现在“高优先级报文的ID和低优先级报文的ID要有足够间隔”。如果一个急停帧的ID是0x010,而一个普通数据帧的ID是0x011,两者虽然差了1,但在总线上同时竞争时,急停帧只赢一个bit。实际传输中,这两位ID的差异意味着仲裁窗口极短,对接收方才说几乎无法在ID段就看出差别,需要等到整个仲裁段结束后才能识别高优先级帧。但如果两者的ID差得足够大(比如0x001和0x200),从第一个bit开始就能立即区分。
另外,CAN的仲裁还直接影响协议里的“应答”和“重传”设计。两个节点同时发数据,低优先级帧会在仲裁段输掉后自动重发,这是硬件行为,协议层不需要管。但你要知道,一个设计不当的自定义协议,可能导致高负载下的“优先级反转”——一个中等优先级的周期帧频繁重发,把高优先级帧的时延拉长。解决思路是尽量让周期帧和时间关键帧的ID段错开,同时控制总线上周期帧的数量,这一点在下一节详说。
3.4 帧间隔与负载率:协议吞吐量的隐性天花板
CAN总线的吞吐量上限,不只是“波特率除以一帧总比特数”这么简单。每个节点在发送下一帧之前,必须等待帧间隔和应答/错误处理。这些开销虽然不是协议里的显式字段,但它们决定了一条总线上实际能跑多少帧。
以经典CAN标准帧为例,一帧完整的数据帧(数据长度8字节)大约要占110个位(包括SOF、仲裁段、控制段、数据段、CRC段、ACK段、EOF、IFS)。500kbps下,理论最快帧率约4500帧/秒。但这是理论极限,实际设计协议时,总线负载率最好控制在30%~50%以下。负载率超过70%以后,仲裁碰撞急剧增加,高优先级帧的平均时延会变得很不稳定,低优先级帧可能一连几个周期都发不出去。
比如你要设计一个底盘域控制器的协议,8个传感器每个周期10ms发一帧,每帧128位,总线上负载率大致是8×128位/10ms÷500kbps≈20.5%,看起来不高。但如果你再加两个100ms一次的诊断大包(每包4帧,共32字节)、一个事件型故障帧(偶发但一次连发3帧),负载率会涨到30%左右,这还在安全范围内。如果继续加节点或者缩短周期,就要重新核算了。
还有一个常常被忽略的“时延抖动”指标。协议设计时不仅要考虑平均时延,还要考虑最坏时延。比如一个高优先级帧在最坏情况下,要等当前低优先级帧传完(最多约130位)、再等紧接着的另一个低优先级帧开始传输,所以最坏等待时间是两个低优先级帧的传输时间总和。如果低优先级帧还带重传机制,最坏时延会进一步拉长。这个数字要作为协议设计里的“实时性预算”写进需求文档。
4. 帧格式与多帧传输:一个可复用的实际模板
4.1 单帧协议的经典布局示例
这里给出一个我在多个项目里用过的经典8字节单帧模板,你可以直接参考甚至拿来改:
字节位置 | 字段名 | 长度 | 说明 0 | Frame Type | 4bit | 0x1=普通数据帧,0x2=控制帧,0x3=故障帧,0x4=配置帧 0 | Reserved | 4bit | 预留,填0 1 | Message ID | 8bit | 应用层消息ID,用于区分这个帧承载的是电压/电流/温度等 2 | Sequence/Frame Counter | 8bit | 帧计数,每发一次累加1 3~6 | Data Field | 32bit | 业务数据区,按需要在里面拆出多个子字段 7 | Checksum | 8bit | 字节0~6的异或和或CRC8
这里把Message ID和应用层指令放到了数据场里,而不是用CAN ID去映射每一种信号。这样设计的好处是CAN ID只承担分配和过滤功能,同一帧可以承载不同业务数据,协议扩展时不需要重新规划ID段。坏处是多占了一个字节,且接收方要软件过滤一次Message ID,会增加几微秒CPU开销。如果你的MCU处理能力很强,总线类型多,这种软件路由的方式灵活得多。
对于更简单的场景,可以直接把8字节全留给业务数据,校验单独放在CAN控制器的CRC里,帧计数也不做,此时协议确实更轻量,但代价是丢帧检测、乱序恢复能力都很弱。我一般只在这种场景下使用:总线固定为点对点(一个主机一个从机),数据周期固定,并且主机对从机有极强的实时状态监控。
4.2 数据字段怎么拆:位域还是字节对齐
前面提到数据区是32bit,那里面怎么放多个物理量?两个方案:位域紧凑打包和字节对齐。
位域打包,就是把一个12位的电压值、一个10位的电流值、一个8位的温度值紧挨着放,总32位(12+10+8+2预留)。优点是节省字节,缺点是可读性差、多平台移植时要小心位域的内存布局跟编译器相关(大端小端、分配顺序都可能不一样)。
字节对齐,就是不管物理量大小,每个量都从字节边界开始放。比如uint16电压占2字节、uint16电流占2字节、uint8温度占1字节、uint8状态占1字节,总共6字节,还有2字节预留。启示就是解析代码很好写,不容易出错,代价是数据密度低。
我个人强烈推荐:在MCU资源充足的情况下,优先用字节对齐。省下的那几个位,对CAN总线来说几乎不构成吞吐量压力(一帧最多省最多几个字节);但带来的代码简洁性和跨端一致性,价值远高于那点带宽。如果实在要做位域,建议用uint8_t数组+位移宏来手动拼位,不用C语言内置的bit-field语法,避免编译器差异。
4.3 多帧传输:分包、重组与超时处理
当单次业务数据超过8字节,就要用到CAN多帧传输。最常见的一个场景是OTA固件升级,一包固件可能有几KB,甚至几MB。多帧传输方案有很多种,这里介绍最简单实用的MTU+总长度方案。
发送方先把整个数据块拆成若干8字节单元,最后一包不足8字节的做填充。协议格式建议用固定10字节首包:前两字节是总长度(表示整个数据块有多少字节),后面6字节存放第一个数据分片;从第二包开始全部是纯数据分片,每个分片8字节,最后一包用DLC指明实际字节数。
接收方要做三件事:解析总长度、申请缓冲或直接写入存储区、校验“分片序号是否连续”。连续序号可以用CAN ID的低字节放序号,也可以用数据首字节放序号,我习惯用后者,因为这样CAN ID完全保留给过滤和路由。
多帧传输里最麻烦的是“中途丢包”和“接收缓冲区溢出”。处理策略:接收方如果发现序号跳变,放弃整包并回发一个“请求重传”的控制帧,发送方收到后重新发全包。这个策略很粗暴,但绝对有效,缺点是浪费带宽,但考虑到多帧传输本来就用于非实时场景,完全能接受。
超时方面,约定“整个多帧传输过程最长300ms,每两个分片间隔最长50ms”。如果超时,接收方丢弃已收数据,恢复接收空闲状态。这个超时时间要根据总线上其他流量来定,不能太短,否则重传频繁,也不能太长,否则接收缓冲区一直占着影响其他业务。
5. 总线时序与波形诊断:从设计文档到真机验证
5.1 波形测量:眼见为实
协议设计得再完备,不在示波器上看到真实波形就等于纸上谈兵。我每次调CAN自定义协议,第一步不是跑协议代码,而是拿示波器(或带波形分析功能的CAN分析仪)看总线上的实际电平。
CAN总线有两条线:CANH和CANL,差分电压在隐性时约为0V(CANH和CANL都在2.5V附近),显性时CANH抬到3.5V、CANL降到1.5V,差约2V。用示波器同时抓CANH和CANL,能直接看到每一位的电平变化和边沿位置。把波形放大到单bit级别,你能清楚地看到采样点是否落在正确位置,以及是否有信号反射导致电平振铃。
如果波形上出现不稳定的毛刺或者边沿抖动明显,你要意识到这不是“协议层”能解决的问题,而是物理层问题。常见原因包括:终端电阻接的不对(应该总线两端各一个120Ω)、分支线路过长、地电位差过大、线缆类型不对等。在自定义协议设计阶段就先把物理层调干净,后面跑逻辑会省一半时间。
5.2 用分析仪验证协议字段
工程上,我习惯用一个支持“自定义协议解析”的CAN分析仪(CANalyzer、周立功CANTest、野火的分析仪、开源的BUSMASTER都行),把我设计的DBC文件或者自定义解析脚本加载进去,然后把真实的报文按字段解析成物理量。
这一步能同时验证两件事:一是发送端的字节序和位域填充是否符合设计,二是接收端按同样规则解析后能否还原出发送端原始值。我见过不少协议翻车案例,都是“发送端把高低字节写反了,但测试时只看接收缓冲区的hex值,没做物理量比对”,结果两个人都觉得没问题。用解析后的物理量比对,当场就能发现大小端、缩放系数、偏移量的问题。
5.3 一致性测试:不只测功能,还要测异常
自定义协议上线前,除了测正常收发,我强烈建议跑一轮侧重异常场景的一致性测试。测试用例不用很复杂,但覆盖面要够:
第一个用例是“错误帧注入”。在总线上故意制造一个位错误(很多分析仪支持主动发送错误帧),观察协议栈能否正确识别并恢复。正确表现是:错误帧不导致整个节点失去同步,节点在几个帧周期内能恢复正常通信。
第二个用例是“冷启动时序”。把总线上所有节点同时上电,或者随机错开上电,观察协议是否还能按设计的优先级和时序正常运行。CAN本身支持在总线上热插拔添加节点,但很多自定义协议里的初始化握手流程在“先上电的节点等后上电节点”时会卡住。设计时一定要约定:任何节点在单独上电时,不依赖其他节点回应也能进入待机状态,而不是死等初始化握手完成。
第三个用例是“干扰环境下的鲁棒性”。在实验室里用CAN干扰仪或用示波器注入毛刺信号,观察协议的错误恢复机制是否有效。这一项对车载、工业现场尤其重要,因为现场电磁环境远比实验室恶劣。
5.4 精选问题排查:两个常见但隐蔽的坑
我在多个CAN项目里遇到过同一个坑:所有节点的波特率配置看起来一样,分析仪也能正常通信,但两台设备直连时就是会周期性报错。最后排查发现,这两个节点的采样点一个设置在65%,一个设置在85%,波特率一致,但采样点差异太大,导致在边沿附近抖动时两者对位电平的判断不一致,于是频繁产生位错误。解决办法是统一约束所有节点的采样点配置,最好在同一份配置向导里下发。
另一个坑是“终端电阻和级联影响”。有些设备内置了120Ω终端电阻,你又在总线上外接了两个120Ω,结果等效阻抗变成40Ω,收发器驱动能力跟不上,总线电平振幅变小,远端节点采样点误判。做自定义协议时,一定要在硬件设计手册里明确:总线上的节点哪些默认打开内置终端电阻、哪些需要外接、哪些是中间节点。这些看似不是协议问题,但它直接影响协议能否稳定运行,我宁可在这里啰嗦也要讲一遍。
6. 工具、参考协议和移植落地建议
做CAN自定义协议设计时,手头趁手的工具能极大提升效率。最基础的是CAN分析仪,推荐至少有一台带记录功能的分析仪,能把整车或整条产线的报文录下来回放,这在分析偶发错误帧时等于有了一台“黑匣子”。其次是示波器,模拟示波器就够了,带宽50MHz以上,能抓到位级波形。软件工具方面,周立功的CANTest和ZCANPRO、Vector的CANalyzer(贵但功能全)、开源的BUSMASTER都可以备着。
自定义协议并不是从零发明。行业里已经沉淀了几套成熟协议,比如CANopen(基于对象字典和PDO/SDO,侧重于设备描述和网络管理)、J1939(侧重于重型车辆的动力总成和部件通信)、UDS/ISO 14229(侧重于诊断)。如果你的项目能直接套这些协议,就别自己设计,省时省力。但如果项目需求比较特殊(比如大量私有传感器数据、简单的主从控制、小批量产品),自定义协议反而更灵活。我的经验是:协议栈的扩展性和可读性完全靠文档和代码注释,跟选不选自定义协议没有必然关系。哪怕选CANopen,你也要针对PDO映射、对象字典条目做大量自定义,工作量并不小。
移植自定义协议时,建议把协议解析逻辑做成平台无关的C文件,不直接依赖具体MCU的CAN寄存器。所有对外接口精简为:初始化、发送帧、接收帧、超时处理、错误回调。这样以后换MCU、换CAN控制器,只需要把底层接口重新实现一遍,上层业务逻辑完全不动。顺便提一句,很多CAN控制器硬件自带多个邮箱和过滤器,你要根据同一时间可能接收多少种不同类型的帧来合理分配邮箱,防止“邮箱被占满导致新帧丢失”。这在多帧传输时尤其明显——建议至少预留一个高优先级邮箱给故障帧,这是我在项目里踩过的一次教训。
最后再说一个容易被忽视的细节:协议文档里要包含一条“版本号”。在协议首包的某个固定位置放协议版本字节,比如V1.0填0x10。这样当新旧版本设备混装在同一总线上时,双方能通过版本号识别兼容性,而不是等到通信失败才去抓瞎。我在模块化项目中吃过亏:升级了A模块的协议,忘了同步B模块的解析代码,结果整条产线全部报错,排查了一个下午,最后发现就是协议版本不匹配。
CAN自定义协议设计,最重要的不是代码怎么写,而是你在写代码之前有没有把数据布局、ID规划、时序约束、异常处理全部想清楚。只要你把上面这些点都考虑到位了,剩下的工作其实就是在草稿纸上画一张表格,然后照着填空编码。希望这篇文章能帮你把协议设计的第一版做扎实。