尧图精选

嵌入式CAN总线自定义协议设计全攻略:从帧结构到采样点避坑指南

🕒 发布时间:2026/9/12 19:26:06 📁 来源:尧图网络
做嵌入式这么多年CAN总线项目接触了不少从车载BMS到工业设备互联最让我头疼的不是硬件电路反而是协议怎么定义。早期接过一个项目两块单板都能正常收发示波器上波形也漂亮可设备一上电就是疯狂报错帧排查了大半天最后发现是报文ID和字节序两边各定各的根本没有形成一份可以共同遵守的协议。这种问题其实在协议设计阶段花十分钟就能避免。这篇文章把我这些年做CAN自定义协议的设计方法完整梳理一遍覆盖物理层基础、帧结构、ID规划、数据编码、波特率采样点计算、CAN FD对协议的影响、调试手段和常见坑。无论你是刚接触CAN的新手还是被通信问题折磨过的老开发都能从这里找到可以直接照搬的套路。我自己一直遵循一个原则协议在设计阶段花的时间越多后面联调和排查省的时间就越多。1. 协议设计前必须搞清楚的基础从波形到帧格式1.1 物理层与电平变化CAN总线电压差是怎么来的很多人设计CAN协议时容易犯一个错误上来就直接画报文帧结构完全忽略物理层。实际上物理层决定了总线能不能稳定通信连带着也会影响协议里的重传策略和超时判断。CAN总线物理上只有两根线CANH和CANL靠差分电压传输信号。总线空闲时两条线都在2.5V左右差分电压接近0V这个状态叫隐性位发送显性位时收发器会把CANH拉高到3.5V左右CANL拉低到1.5V左右差分电压约为2V。接收端不看单根线的绝对电压只看CANH减CANL的差值所以抗共模干扰能力很强地电位漂移影响也不大。我在实测中经常用示波器直接夹在CANH和CANL之间看差分波形这是最直观的排查手段。如果隐性电平不在2.5V附近或者显性压差不到1.5V基本可以判定收发器或者供电有问题。总线两端各接一个120欧姆终端电阻目的是匹配传输线阻抗、减少信号反射。如果终端电阻缺失高速率下波形会产生振铃错误帧率会明显上升。1.2 帧格式标准帧、扩展帧、远程帧与DLCCAN协议栈的帧格式是标准化的但很多人对其中的细节理解模糊。自定义协议用到最多的是数据帧标准帧有11位ID扩展帧有29位ID数据场最多8字节。DLC字段表示数据长度取值范围0到8接收端会严格按DLC裁剪数据。远程帧在实际项目中我很少用它本身不带数据段靠ID请求对方发送数据。听起来很方便但远程帧在实际总线上容易引发优先级反转和时序不确定的问题很多上层协议栈甚至不推荐使用。自定义协议设计时尽量绕开远程帧统一用周期上报和事件上报来替代。扩展帧和标准帧不能混用在一个报文里ID虽然可以相同但仲裁时候的位序列不同会被当成两个不同报文。协议设计时最好全项目统一标准帧因为29位ID对大多数应用来说都用不满反而增加解析复杂度。1.3 仲裁机制为什么ID越小优先级越高CAN的仲裁机制是它区别于RS485、以太网的重要特性。多个节点同时发送时总线会逐位比较ID显性位会覆盖隐性位发送隐性位但读到显性位的节点会立即退出仲裁转为接收状态。因为ID前面越多的显性位就越晚退出所以ID数值越小优先级越高。实际设计协议时我会把实时性要求最高的报文放在最低的ID段。比如电机控制器的使能报文ID设为0x001电池管理系统的电压报文设为0x0A1车身状态报文放到0x300以后。这样即使总线上突发大量报文关键控制信号也不会被阻塞。1.4 位时序与SJW采样点为什么如此关键CAN总线上没有独立的时钟线所有节点靠同步段和重同步来对齐位时间。一个位时间在STM32里被划分为SYNC_SEG、BS1、BS2三段采样点位于BS1和BS2交界处。BS1可以理解为一个位时间的“前半场”BS2是“后半场”接收节点在交界处采样总线电平。SJW同步跳跃宽度的作用是当节点检测到总线边沿与本地时序有偏差时允许调整的最大时间量子数。SJW太小长距离传输或温漂大的场景下容易失步SJW太大抗干扰能力会下降。常规配置SJW1如果总线拓扑比较复杂、最远节点距离超过几十米可以适当增加到2。采样点的选择对通信稳定性影响极大。推荐采样点落在75%到90%之间常见标准是87.5%。采样点太后容易采到下一个位的边沿采样点太前抗干扰能力不足采样到信号毛刺的概率增加。后面第三章我会详细介绍具体怎么计算。2. 自定义协议的设计步骤从信号清单到完整报文2.1 第一步把信号清单列成表格协议不是拍脑袋画出来的而是先从需求里把要传的信号全部挖出来。做过三五个CAN项目后你会发现信号清单这一步几乎决定了后面所有设计工作的质量。我做协议前一定会先整理一张表格明确每个信号的名称、物理单位、范围、精度、发送周期和实时性要求。电压可能范围0到1000V精度0.1V温度范围-40到125摄氏度精度1摄氏度SOC范围0到100%精度1%。把这些梳理清楚后才能决定每个信号占用几个字节、有没有必要用偏移量。这类表格看似繁琐但它的价值在于让大家在动手写代码前就把需求对齐。项目里不同工程师对同一个信号的理解经常有偏差比如“电流”到底是有符号还是无符号、单位是安培还是毫安如果不提前定义后面联调就是灾难。2.2 第二步报文ID规划与优先级分配ID规划是协议设计里最能体现水平的环节也是最容易后期返工的地方。我常用的做法是把ID空间按功能分区高字节表示消息类别低字节表示具体报文。比如0x00到0x0F留给系统管理和网络管理0x10到0x2F给动力系统0x30到0x4F给车身系统0x50到0x7F给诊断。分区后还要在分区内部按周期和实时性排列。周期越短、实时性越高的报文ID越小。比如电机使能是10ms周期ID用0x11整车状态100ms周期ID用0x12。这样即使报文重叠高优先级报文也能先发出去。ID一旦在量产项目里定了基本不能改因为所有节点和诊断工具都要跟着变。所以设计ID时最好预留一段空间比如0x500到0x7FF留作未来扩展不要一上来就把空间占满。2.3 第三步数据段编码与字节序CAN协议标准里没有规定数据场里的字节序这一块完全由应用层自己定义。常用的有小端模式和大端模式小端的意思是低字节在前大端是高字节在前。绝大多数ARM单片机的内部数据就是小端协议如果也定义成小端代码里直接取地址拷贝就行省去字节序转换的麻烦。多字节信号还需要处理符号和偏移。以总电流为例范围-1000A到1000A精度0.1A原始数据范围是-10000到10000超出16位无符号范围可以把电流本身转成有符号16位或者加一个偏移量做无符号存储。我习惯用无符号加偏移比如电流偏移327680表示-3276.8A65535表示3276.7A这样解析代码简单还顺便兼容了“0xFFFF表示传感器故障”这类异常标志。数据场里每个bit最好都有明确含义不要留空洞。能按字节对齐就按字节对齐虽然浪费一点空间但解析效率和可读性会好很多。位域packed的方式能省则省除非总线负载特别紧张否则别为了省两个字节把一个信号拆得七零八落。2.4 第四步报文类型与发送周期设计报文按发送方式可以分为周期报文、事件报文和应答报文。周期报文按照固定时间间隔发送用于连续状态量比如电压、电流、温度事件报文只在信号变化或故障发生时发送用于开关量、报警应答报文则是对诊断命令或配置命令的回复。周期设计有一个关键点事件报文必须配套接收超时检查。如果接收端超过比如500ms没有收到事件报文应该认为链路异常或对端失联进入安全状态。不要因为事件报文平时不发就省略超时判断否则节点出现硬故障时接收端完全不知道。周期报文的周期也要考虑总线负载率。500kbps的CAN总线理论负载能力是每秒50000位左右实际上要留安全余量控制在50%以下比较稳妥。一个100ms周期的8字节报文加上帧头帧尾和填充位大约占用130位左右算下来单帧负载约0.026%一条总线上跑几十个报文完全没有问题。2.5 实例一个BMS自定义协议报文设计空讲不好理解我直接给出一套实际项目中用过的BMS报文设计。电压、温度、SOC和状态标志是动力系统最核心的几组信号。第一帧0x0A1100ms周期8字节。Byte0和Byte1放总电压小端单位0.1V直接用无符号16位整数Byte2和Byte3放总电流小端偏移32768单位0.1A0xFFFF表示电流传感器故障Byte4放SOC范围0到200对应0%到100%Byte5放最高单体温度Byte6放最低单体温度都用偏移40存储0对应-40摄氏度160对应120摄氏度Byte7放状态位bit0是充电状态bit1是放电状态bit2是故障标志bit3是继电器状态。第二帧0x0A2500ms周期8字节专门放单体电压。Byte0到Byte7依次存放8个单体电压的低字节和高字节一条总线上如果有多个BMS从板就通过ID区分从板序号。实际上单体电压数量多的话一帧放不下就得拆成多帧ID继续递增。这套设计的思路是核心实时信号用短周期、小ID保障控制链路大数据量的单体电压信息用长周期通过多帧轮询发送。解析时对照协议文档很直观调试效率明显高很多。3. 波特率与采样点计算用STM32F103算给你看3.1 波特率公式与参数约束CAN波特率不是随便填的它由外设时钟、预分频器和位时间三部分组成。以STM32F103为例CAN外设挂载的APB1时钟最高36MHz然后经过预分频得到时间量子时钟最后每个位时间由1个同步段加BS1加BS2组成。波特率计算公式是波特率 外设时钟 / (预分频值 * (1 BS1 BS2))。采样点 (1 BS1) / (1 BS1 BS2)。这里最容易踩的坑是STM32的BS1和BS2配置范围有限制BS1是4位寄存器最大15BS2是3位寄存器最大7。所以不是任意组合都能算出目标波特率很多教程里随手写的参数其实并不合法。3.2 500kbps采样点最佳配置拿最常用的500kbps来举例。APB1时钟36MHz如果预分频设为9得到4MHz时间量子时钟每个位时间需要8个时间量子也就是1 BS1 BS2 8。设BS1 6BS2 1采样点 (1 6) / 8 87.5%正好落在推荐区间。对应的代码配置是这样的CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_Prescaler 9; // 预分频9 CAN_InitStructure.CAN_SJW CAN_SJW_1tq; // 同步跳跃宽度1个时间量子 CAN_InitStructure.CAN_BS1 CAN_BS1_6tq; // BS1 6 CAN_InitStructure.CAN_BS2 CAN_BS2_1tq; // BS2 1 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq;这个配置在绝大多数工业场景下都很稳。如果你的总线拓扑特别长或者干扰特别大可以把采样点往90%附近调比如BS1 7BS2 0不合法因为BS2最小是1所以要保持SP 87.5%以上只能选BS1 6BS2 1这种组合。3.3 其他常用波特率配置对照表实际项目里250kbps和1Mbps也常用我把几组常用配置直接整理出来方便照抄。还是基于APB1 36MHz来计算。波特率预分频BS1BS2采样点实际波特率1Mbps930? 不合法1Mbps435? 不对重新算这里我直接给出几组验证过的值。1Mbps36M / 4 9MHz位时间9个tqBS1 5BS2 31 5 3 9采样点 6/9 66.7%偏低。1Mbps标准87.5%需要1 BS1 BS2 8BS1 6BS2 1那么时间量子时钟 8MHz预分频 36M / 8M 4.5不整除。所以APB1 36MHz下严格的87.5%采样点对1Mbps来说做不到工业上常用配置是预分频3时间量子12MHz位时间12个tqBS1 9BS2 2采样点 10/12 83.3%也算在75%~90%的安全区间内。250kbps的话36M / 9 4MHz位时间需要16个tqBS1 13BS2 2采样点 14/16 87.5%很标准。预分频也可以取182MHz时钟8个tqBS1 6BS2 1采样点同样是87.5%。我把常用配置列一张表目标波特率外设时钟预分频BS1BS2采样点1Mbps36MHz39283.3%500kbps36MHz96187.5%250kbps36MHz913287.5%125kbps36MHz1615288.9%每次新建项目我会先确认外设时钟再按表验证参数最后用示波器量一下总线上的实际位宽确认。500kbps下一个位应该是2微秒量出来如果差很多说明配置有问题。4. CAN FD来了协议设计要跟着变4.1 CAN FD与CAN 2.0的核心差异CAN FD和经典CAN最直观的差别是数据场变长了最多可以到64字节而经典CAN只有8字节。仲裁部分波特率不变但从BRS位开始数据段和CRC段可以切换到更高速率最高能做到几Mbps甚至更高。这个可变速率特性很关键。仲裁段低速是为了保证多个节点仲裁的可靠性数据段高速是为了提高传输效率。一帧CAN FD报文既能享受经典CAN那种仲裁机制又能把大量用户数据快速搬到总线上这是CAN FD对协议设计最大的改变。另外CAN FD的CRC校验更强有17位和21位两种还会对填充位做CRC保护。这意味着CAN FD帧的数据完整性比经典CAN更可靠在安全相关场景里更有优势。4.2 对自定义协议设计的影响CAN FD出现后以前需要拆成3到4帧才能发完的数据现在一帧就能搞定。比如固件升级一个64字节的CAN FD帧能承载大量程序数据升级效率和成功率都大幅提升。协议设计时信号打包的颗粒度可以更大不再为8字节限制绞尽脑汁。数据段波特率提高以后单个报文在总线上占用的时间变短了总线空余时间变多可以容纳更多低优先级报文或者增加诊断报文的发送频率。但同时要注意如果总线上混跑经典CAN和CAN FD经典CAN节点收到CAN FD帧会报错因为它们不识别新帧格式。4.3 兼容性设计老节点怎么办混合网络中如果确实有老节点一个折中方案是让CAN FD节点工作在“CAN FD tolerant”模式发送时用经典CAN帧接收时能收CAN FD帧。更稳妥的做法是按功能分区支持CAN FD的子系统内部用FD通信跨子系统的关键信号仍然用经典CAN兼容帧。我在协议文档里会明确标注每个报文是经典CAN帧还是CAN FD帧。如果一帧里既有必须兼容老节点的关键信号又有大量扩展数据就把关键信号放在前8字节后面的扩展数据用另一条CAN FD报文发送。这样老节点读不到扩展数据也不会影响它的核心控制逻辑。5. 协议调试与验证用工具把帧一层层剥开5.1 调试工具怎么选CAN调试工具我用过不少简单好用的是PCAN-View界面简洁抓帧速度快适合现场快速验证。ZLG的CANTest和CANPro功能更全支持报文回放、脚本触发、错误帧统计。CANalyzer功能非常强大但价格也高大部分项目用不到那个层级。开源方案里USB-CAN适配器加Python-can库也是很好的组合。特别是做协议自动化测试的时候写一个小脚本循环发帧、校验接收帧比手动点上位机高效太多。我做回归测试时经常用这个方法。5.2 实测步骤从回环模式到真实总线新板子第一次调CAN我不会直接接到总线上而是先让CAN外设工作在回环模式。回环模式下数据只在本机内部兜一圈不需要收发器也能验证MCU的CAN控制器配置是否正确。回环通了以后再把收发器接上用自发自收模式验证物理链路。最后才接入真实总线多节点联调。接入真实总线后第一件事是抓一帧看帧ID、DLC、数据域是否符合协议。如果看到一堆错误帧优先怀疑波特率不一致用示波器量CANH和CANL之间的差分波形一个位的时间宽度就能反推出实际波特率。500kbps的位宽2微秒如果量出来2.2微秒说明某一边配置的和预期不符。5.3 报文解析与大小端核对技巧解析报文我习惯先手动解一帧再和工具自动解析结果对照。这里有个经验上位机显示的数据通常是原始字节如果解析出来数值明显离谱比如总电压显示65535V多半是字节序反了或者偏移量没加。写Python解析脚本也很简单先把帧数据按字节拆开再用struct模块的unpack处理大小端和符号。多字节信号我会在协议文档里同时标注起始字节、长度、字节序、缩放因子、偏移量脚本解析时严格按这个元数据算能避免很多人为错误。6. CAN自定义协议常见坑与排查速查表6.1 硬件层典型故障硬件层的问题通常最隐蔽但排查起来也最有章可循。总线完全不通信先量收发器供电、看STB或RS引脚电平是否正常。很多CAN收发器模块有standby引脚悬空时可能进入待机模式收发功能被关掉这是新板子最容易犯的错。终端电阻问题也很常见。短距离直连两块板子可以只在其中一端接120欧姆但只要总线长度超过几米或者节点数超过两个必须两端都接120欧姆。这个我吃过亏以为短距离调试不接也行结果高速率下波形反射严重错误帧率降不下来。6.2 协议层典型故障协议层最大问题永远是两边对不上。ID不一致、DLC不一致、字节序不一致、缩放因子不一致这些在协议文档缺失或更新不及时时反复出现。解决这类问题我有个笨但有效的办法联调时先从协议里挑一帧最简单的固定报文比如状态字或者版本号手动算一帧数据让设备发出来再用上位机抓帧对照。这一帧对上了再逐步扩展到浮点、负数、多字节信号。一次只验证一组信号出了问题定位就是分钟级。6.3 排查顺序建议遇到CAN通信问题时我习惯按这个顺序排查先看工具能不能收到帧收不到先查物理层能收到但全是错误帧查波特率和采样点错误帧很少但周期不稳定查终端电阻和线缆质量帧内容和期望值对不上查协议里的ID、DLC、字节序和偏移量。Bus Off频繁出现时除了查物理层还要检查应用层的恢复策略。总线关闭后的恢复策略一般有两种一种是等待硬件自动恢复速度快但干扰持续时容易反复震荡另一种是应用层检测到Bus Off后主动重新初始化CAN外设恢复清晰可控。安全相关项目建议用第二种同时通过一个网络管理报文把错误状态上报给主控。我建议每个节点在协议里预留一个状态报文哪怕只占一个字节用来上报发送错误计数、接收错误计数和Bus Off状态。一次两个节点莫名其妙丢帧就是靠这个状态报文定位到是从节点采样点配置太靠前导致的。类似这种问题如果没有状态上报靠示波器抓偶发波形真的能抓到怀疑人生。最后再说一下我的习惯。协议设计完后我会把所有信号定义导入DBC文件作为项目交付物的一部分。DBC文件不只是给CANalyzer用的它还是团队沟通的基准。芯片可以换硬件工程师可以换但DBC文件和协议文档保持一致这个项目就算换三拨人也不怕接不上。CAN底层机制已经非常成熟工程师真正比拼的是协议设计层面的规范程度。项目开始前花半天时间把信号矩阵、ID分配表、协议文档建好后面能省下几周调Bug的时间。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →