ARTICLE DETAIL

资讯详情

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

STM32 CAN双结点通信实战:从位时序到Bus Off恢复的完整调试指南

STM32 CAN双结点通信实战:从位时序到Bus Off恢复的完整调试指南 P3这个项目我给自己定的目标很明确用两块STM32F103板子搭一套最小可用的CAN双结点控制系统。说白了就是让结点A和结点B通过CAN总线互相收发控制指令和状态数据。这件事听起来好像就是两个板子互相发消息但真正把位时序、采样点、仲裁、Bus Off恢复这些细节全部理清楚再让两边的数据一帧不丢地稳定跑起来我前后折腾了小两周。这篇文章就把整个调试过程、关键配置和踩过的坑完整记录下来给正准备用CAN做双结点通信的嵌入式开发者一个可以直接抄作业的参考。1. 为什么P3要做成CAN双结点控制1.1 双结点是CAN通信的最小闭环P3是我这个系列项目的第三阶段。前面P1、P2分别解决了基础外设和单板自检到P3自然而然地要面对一个问题两块控制器之间怎么可靠地交换数据。这时候摆在桌面上的方案有好几个UART、RS485、I2C、SPI、CAN我最后选了CAN不是因为CAN听起来高级而是因为双结点控制这个场景它最合适。CAN总线最核心的几个特性恰好就是双结点控制需要的多主结构两个结点都可以主动发数据不需要主机轮询、非破坏性仲裁两个结点同时发的时候不会冲突优先级低的自动让路、差分物理层抗干扰能力强适合工业现场、完善的错误检测机制错误帧、Bus Off状态都能被感知和处理。对比一下传统方案就很直观方案多主能力仲裁机制抗干扰传输距离适用场景UART不支持无弱十几米板间点对点RS485需要主机调度无硬件仲裁较强上千米仪表总线、ModbusI2C半双工多主但复杂有仲裁但信号弱弱几米板内器件通信CAN全多主非破坏性逐位仲裁强可达1km车载、工业控制双结点是整个CAN通信体系里最小的闭环。一个发送一个接收这只是最表面的一层完整的闭环还包含两个结点同时发送时的仲裁、出现错误帧时的处理、某个结点掉线后另一个结点的反应。这些在单结点或者纯接收场景里根本验证不到。所以我把P3定义为双结点控制本质上是要把CAN通信的完整链路打通。1.2 硬件架构和结点角色怎么分我的硬件选型很朴素主控用STM32F103C8T6收发器用TJA1050总线上两端各接一个120Ω终端电阻。选STM32F103的原因不多说了bxCAN外设集成度够高生态资料丰富最关键的是我手头正好有现成的板子。TJA1050是经典的5V供电CAN收发器驱动能力强市场上各种模块化板子很多调试起来非常方便。两个结点的角色我做了应用层区分结点A是主控周期发送控制指令帧结点B是执行结点收到指令后执行动作并周期性回报状态帧。注意这个主从关系只是应用层约定CAN物理层和多主协议本身并没有规定谁是主机两个结点随时都可以往总线上发数据。应用层的主从划分是为了业务流程清晰协议层的多主能力才是CAN真正的价值所在。整体架构很简单两板之间两根线CANH和CANL共地两边各接一个终端电阻。结点A用PA12作为CAN_TXPA11作为CAN_RX结点B同样配置。就这么一个看似简单的连接后面让我排查了整整一天原因后面细说。2. CAN双结点通信的地基差分物理层与收发器电路2.1 总线上的0和1不是电平高低而是差分电压很多从UART、SPI转过来的人最开始都会犯一个思维惯性错误以为CAN总线上的逻辑0和逻辑1也是靠某个引脚的高低电平来表示的。实际上CAN总线用的是差分信号通信的关键在CANH和CANL两根线之间的电压差而不是某一条线对地的绝对电压。以TJA1050这种5V供电的收发器为例当总线处于显性电平逻辑0时收发器内部的驱动管会把CANH拉到约3.5V、CANL拉到约1.5V两根线之间的差分电压约为2V。当总线处于隐性电平逻辑1时收发器释放总线CANH和CANL都回到约2.5V差分电压约为0V。所以你在示波器上看到的CAN波形如果单看CANH或者CANL对地电压好像是方波上下跳动但真正承载信息的是V(CANH)-V(CANL)这个差值。这个过程可以理解成一个跷跷板显性位时跷跷板两端被强制拉开隐性位时两端松开回到平衡。总线上的终端电阻在这里起了关键作用隐性状态时正是靠它把总线拉到2.5V附近的共模电平。这也是为什么忘了接终端电阻总线波形会变得非常难看。回到热搜里那个问题CAN总线的电压差是怎么改变的本质就是收发器内部驱动MOS管的导通和关断改变了总线驱动状态从而改变CANH和CANL之间的电位差。差分信号带来的最大好处是抗共模干扰。工业现场电机启停、变频器工作都会在地线上灌入大量共模噪声这种噪声是同时叠加在CANH和CANL两根线上的。既然是共模做减法之后就被自然抵消了。这是CAN总线能在电磁环境恶劣的场合活下去的根本原因也是它和UART这种单端信号最本质的区别。2.2 收发器选型与双结点接线细节收发器这块市面上最常见的是TJA1050、SN65HVD230和MCP2551这三款。选型的时候主要看供电电压和应用环境收发器供电电压特点注意点TJA10505V驱动能力强经典与3.3V MCU需要电平兼容处理SN65HVD2303.3V适合3.3V MCU直连驱动能力稍弱MCP25515VMicrochip经典方案同样存在电平匹配问题STM32F103的IO是3.3V电平直接用5V供电的TJA1050时TXD和RXD引脚存在电平匹配问题。很多现成的CAN模块板子已经做了电平转换直接用没问题但如果自己画板子务必确认收发器TXD能否被3.3V高电平正常驱动RXD输出的高电平会不会反向灌进MCU的IO。SN65HVD230这类3.3V收发器就没这个烦恼只是总线驱动能力和抗干扰性能略逊一筹。终端电阻是最容易被忽略的细节。CAN规范要求总线的物理两端各接一个120Ω电阻注意是物理两端。双结点系统里两个结点就是总线的两端所以每个结点靠近收发器的地方各放一个120Ω电阻整个总线等效阻抗约60Ω。这里有个经典误区如果用的是那种板载了120Ω电阻的开发板你又在外部加了一个120Ω就会导致总线匹配电阻偏小信号反射反而更严重。调试前务必确认每个结点的终端电阻状态要么用板载要么外接不要重复。另一个容易出问题的是地线。CAN是差分信号没错但收发器本身还是需要参考地的。近距离调试时两个板子共地问题不大一旦距离超过几米或者两个板子各自用独立电源供电地电位差就可能超出收发器共模输入范围轻则波形畸变重则烧毁收发器。工程上的做法是使用隔离收发器比如ISO1050配合隔离电源把总线和MCU侧在电气上完全隔开。我的P3项目因为距离短直接共地解决。顺带提一下CAN和RS485复用差分接口的问题。两者确实都是差分信号也都用120Ω终端匹配但电平标准和协议完全不同。CAN是CANH/CANL显性隐性电平RS485是A/B逻辑电平定义不一样。硬件上如果做复用一般通过跳线帽或者模拟开关切换收发器芯片同时还要切换终端电阻连接方式。我自己不太建议在同一块电路板上直接复用除非接口位置非常紧张否则老老实实分开走调试时能省很多心。3. 位时序和波特率把500kbps采样点算明白3.1 一个bit由四段组成BS1/BS2不是随便填的CAN通信里一个bit的时间不是简单的一个时钟周期而是被切分成了好几段。这个设计是CAN能容忍时钟偏差、能在长线上稳定通信的关键。STM32F103的bxCAN把一位时间Time QuantumTQ分成了四段同步段Sync_Seg、传播段Prop_Seg并入BS1、相位缓冲段1BS1、相位缓冲段2BS2。实际上F1的寄存器里把传播段和相位缓冲段1合并成了BS1一个字段。同步段固定是1个TQ用来同步总线上的各个结点BS1可以配置为1~16个TQ主要用来补偿收发器延迟、总线传播延迟以及吸收时钟偏差BS2可以配置为1~8个TQ主要用来补偿相位误差SJW同步跳跃宽度可以配置为1~4个TQ它不参与位时间的长度作用是在重新同步时允许采样点往前或往后跳跃的最大宽度采样点的位置就在BS1和BS2的交界处。之所以强调采样点不能太靠前也不能太靠后是因为采样点离位跳变沿太近时如果总线上的时钟有偏差或者信号有延迟就容易采到跳变过程中的不稳定电平导致错误帧。业内一般推荐采样点放在75%~80%的位置留出足够的相位缓冲余量。拿BS1/BS2 延迟和早到 时钟处理有什么要求这个问题来说实际上是一个权衡总线越长、波特率越高信号传播延迟在一位时间里的占比就越大这时候BS1应该适当加大把采样点往后推让信号有足够时间稳定下来。反过来如果结点时钟精度较差SJW就要配得大一些给重新同步留更多空间。但SJW也不是越大越好过大的SJW在强干扰环境下可能会把采样点跳到一个错误的相位反而增加采样错误概率。3.2 500kbps配置计算实例STM32F103的CAN外设挂载在APB1总线上系统时钟72MHz时APB1时钟是36MHz。波特率计算公式是BaudRate PCLK1 / (Prescaler * (1 BS1 BS2))注意BS1和BS2在这里指的都是实际TQ数。我要配置500kbps目标位时间是2us。时钟频率36MHz选预分频Prescaler4得到TQ时钟9MHz也就是每个TQ约111.1ns。每个bit需要18个TQ9MHz / 500kbps 18。然后分配同步段1个TQBS1取13个TQBS2取4个TQ采样点 (1 13) / 18 ≈ 77.8%正好落在推荐的75%~80%区间。这里必须多说一句HAL库的填参陷阱。ST的标准库和HAL库在时序参数上填的都是实际TQ数比如BS1给13、BS2给4库函数内部会自动把寄存器值设为TQ数减1。很多人不知道这个关系直接按寄存器值反推填参结果算出来的波特率完全不对。调试时如果怀疑波特率配置建议把CAN_BTR寄存器读出来对照参考手册一个个bit拆开看比对着代码猜高效得多。为方便大家直接抄我把F103在36MHz APB1时钟下的常用配置列个表波特率预分频TQ时钟每bit TQ数BS1BS2采样点1Mbps218MHz1813477.8%500kbps49MHz1813477.8%250kbps84.5MHz1813477.8%125kbps162.25MHz1813477.8%这组配置的好处是TQ数恒定为18采样点一致改波特率只需要动预分频。HAL库里的初始化代码大致长这样CAN_HandleTypeDef hcan1; hcan1.Instance CAN1; hcan1.Init.Prescaler 4; // 36MHz / 4 9MHz TQ时钟 hcan1.Init.Mode CAN_MODE_NORMAL; // 正常模式不是回环 hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; // 同步跳跃宽度1个TQ hcan1.Init.TimeSeg1 CAN_BS1_13TQ; // BS1实际13个TQ hcan1.Init.TimeSeg2 CAN_BS2_4TQ; // BS2实际4个TQ hcan1.Init.TimeTriggeredMode DISABLE; hcan1.Init.AutoBusOff ENABLE; hcan1.Init.AutoWakeUp DISABLE; hcan1.Init.AutoRetransmission ENABLE; hcan1.Init.ReceiveFifoLocked DISABLE; hcan1.Init.TransmitFifoPriority DISABLE; if (HAL_CAN_Init(hcan1) ! HAL_OK) { Error_Handler(); }实际项目中我一般会把波特率做成宏预分频、BS1、BS2都一起定义方便后期调整。如果总线上有多个结点波特率必须完全一致这个一致性不仅指最终的速率一致还要求采样点尽量一致否则长距离传输时容易出现错误帧。3.3 初始化失败排查顺序CAN初始化失败这个报错我估计每个玩过STM32 CAN的人都被它折磨过。根据我的经验90%的初始化失败不是硬件问题而是软件配置时序问题。排查顺序非常重要我踩过坑之后的固定套路如下。第一步查时钟。CAN外设的时钟源是APB1没有使能CAN1时钟一切白搭。在CubeMX里勾了CAN1外设之后会默认使能时钟但如果是纯寄存器或者标准库裸配置很容易漏掉RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE)。第二步查GPIO复用。STM32F103的CAN1引脚固定在PA11和PA12但这两个引脚不是默认就是CAN功能的必须配置为复用功能。这里有个非常隐蔽的坑CAN_TXPA12要配置为复用推挽输出而CAN_RXPA11应该配置为上拉输入或者浮空输入。很多人习惯性地把两个引脚都配成复用推挽导致RX引脚状态不对CAN初始化直接报错。第三步查时序参数。HAL_CAN_Init返回HAL_ERROR最常见的原因是Prescaler、TimeSeg1、TimeSeg2组合非法。比如TimeSeg1给0或者TimeSeg2给0寄存器根本写不进去。还有SJW的值不能超过BS2的实际TQ数这个约束很多人不知道。第四步验证初始化是否真正成功。配置完成后用示波器测量TXD引脚PA12总线空闲时TXD应该保持高电平。如果初始化成功但总线上有异常波形八成是模式配错了比如不小心配成了SILENT模式发送功能被禁用。4. 数据怎么才算真正发通报文、仲裁与收发代码4.1 报文结构与字节序CAN标准帧11位ID的报文结构分成几个段帧起始、仲裁段ID RTR位、控制段IDE位 DLC、数据段0~8字节、CRC段、ACK段、帧结束。双结点控制这种短报文的场景标准帧完全够用没必要上扩展帧增加总线负担。这里要特别强调一下报文解析的一个基本意识CAN协议本身只定义了帧格式并没有规定数据段里的字节代表什么意思。0x101这个ID后面跟的8个字节到底哪个是控制字、哪个是目标值高位、哪个是低位完全是应用层自己约定的。所以做双结点通信前第一件事不是写代码而是写一份协议表把所有会用到的ID、方向、周期、数据字节布局全部定义清楚。字节序是协议定义里最容易出问题的地方。CAN报文里如果涉及多字节整数比如转速值0x1234小端Intel格式发送时低字节在前数据段就是34 12大端Motorola格式发送时高字节在前数据段就是12 34。总线上传输的字节序必须由通信双方共同约定否则A结点以低字节在前发出去B结点如果按高字节在前解析读出来的就是0x3412错得毫无头绪。举个具体的解析例子结点A周期发送ID0x101DLC8Data01 60 00 00 00 00 00 00。如果协议约定Data[0]是控制字01表示启动Data[1]和Data[2]是目标值小端存储那么目标值就是0x006096。用CAN分析仪抓包时看到的是原始字节流能不能转换成有意义的业务数据完全取决于协议表。4.2 仲裁机制两个结点同时发送时谁赢CAN最迷人的机制就是非破坏性仲裁。两个结点同时往总线上发数据时不是靠随机退避来解决冲突而是从ID的最高位开始逐位比较。因为显性电平逻辑0会覆盖隐性电平逻辑1所以ID数值越小优先级越高会在仲裁段获胜继续发送ID数值大的结点在发现总线电平与自己发送的电平不一致时自动转为接收状态。举一个双结点场景的例子结点A发送ID0x101二进制是00100000001结点B发送ID0x201二进制是01000000001。从最高位开始比较第9位从高位往下数结点A发送的是0结点B发送的是1当A发的显性电平覆盖了B发的隐性电平时B立刻知道自己的优先级不够立即停止发送转为接收。这个仲裁过程完全由硬件完成不需要软件介入而且几乎不浪费总线时间。如果要验证仲裁机制在双结点系统里真的有效可以做一个实验把两个结点的发送周期都设为极小值比如500us让它们尽可能频繁地同时发送。另一端用一个USB-CAN分析仪挂在总线上抓包统计两个ID的帧数比例。只要两个ID不同低ID的帧数会明显占优但高ID的帧也不会完全消失因为两个结点很难做到每一个发送周期都严格同步。这里有一个致命的反面案例如果两个结点的ID设置成了完全相同的值仲裁的时候双方都认为自己在发送无法判定输赢最终会触发位错误机制产生错误帧两个结点的发送都会失败。所以双结点项目中ID必须保证唯一这是写进协议第一步的事。4.3 收发代码HAL库双结点Demo代码层面我习惯把CAN收发拆成三个模块初始化、发送、接收。初始化部分除了CAN外设本身还需要配置滤波器。双结点最简单的方式是接收所有报文滤波配置如下CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank 0; sFilterConfig.FilterMode CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh 0x0000; sFilterConfig.FilterIdLow 0x0000; sFilterConfig.FilterMaskIdHigh 0x0000; sFilterConfig.FilterMaskIdLow 0x0000; sFilterConfig.FilterFIFOAssignment CAN_RX_FIFO0; sFilterConfig.FilterActivation ENABLE; if (HAL_CAN_ConfigFilter(hcan1, sFilterConfig) ! HAL_OK) { Error_Handler(); } if (HAL_CAN_Start(hcan1) ! HAL_OK) { Error_Handler(); }屏蔽寄存器全0表示掩码不检查任何位所有ID都能进FIFO。如果你的双结点系统里两个结点间有多条报文明确定义了ID还是建议用掩码过滤一下只接收自己关心的报文减少中断频率。发送端我用的是HAL_CAN_AddTxMessageCAN_TxHeaderTypeDef txHeader; uint8_t txData[8] {0x01, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; uint32_t txMailbox; txHeader.ExtId 0; txHeader.IDE CAN_ID_STD; txHeader.RTR CAN_RTR_DATA; txHeader.DLC 8; txHeader.StdId 0x101; if (HAL_CAN_AddTxMessage(hcan1, txHeader, txData, txMailbox) ! HAL_OK) { // 3个发送邮箱都满了需要做异常处理 }接收端建议走中断或者FIFO回调不要在中断里做耗时操作。我是把收到的原始数据存到全局数组置一个标志位主循环里再来解析void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; if (hcan-Instance CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData); // 根据rxHeader.StdId分发表把rxData拷贝到对应缓冲区并置位 } }接收回调里要注意FIFO溢出问题。F1的CAN每个FIFO只有3个报文深度如果中断响应不及时或者主循环处理不过来新来的报文会直接丢弃。高负载场景下建议把接收中断优先级调高同时缩短报文处理链路。双结点Demo的完整逻辑是结点A的main循环里每10ms发送一次0x101控制帧发送完之后检查是否有收到0x102状态帧结点B在收到0x101之后执行对应的业务动作同时每100ms主动发送一次0x102状态帧回报当前状态。两边各自维护一个总线错误计数器出错次数超阈值就进入自恢复流程。这个Demo跑通之后CAN双结点控制的基本闭环就算建立了。5. 实测翻车现场Bus Off恢复、初始化失败与波形排查5.1 Bus Off恢复策略Bus Off是CAN控制器的一种自我保护状态。每个结点内部有两个错误计数器发送错误计数器TEC和接收错误计数器REC。当TEC超过255时结点判定自己已经无法正常参与总线通信自动进入Bus Off离线状态不再往总线上发任何数据避免污染总线。双结点系统里Bus Off的影响比多结点更残酷一个结点离线后另一个结点发的数据因为没有结点回复ACK会持续报错很快也会被拖入错误状态两个结点一起哑火。Bus Off的常见诱因包括总线短路或者断路、波特率不匹配、忘记接终端电阻导致波形反射严重、外部强电磁干扰。在F103的HAL库里Bus Off的状态可以在错误回调里捕获void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan) { uint32_t errorCode HAL_CAN_GetError(hcan); if (errorCode HAL_CAN_ERROR_BOFF) { // 进入Bus Off执行恢复流程 HAL_CAN_Stop(hcan); // 等待总线恢复稳定必要时延时 HAL_CAN_Start(hcan); } }恢复策略我建议分两步走先做被动恢复也就是停掉CAN外设再重新启动如果短时间内连续多次进入Bus Off说明总线上存在持续性的问题光靠软件恢复没有意义应该主动上报异常状态亮故障灯或者记录日志而不是盲目循环重启。我见过有人写了一个死循环自动重启CAN的代码结果电容器开关干扰一上来两个结点就陷入重启-再报错-再重启的死循环整个系统彻底瘫痪。恢复之前先判断外部干扰是否已经消失这个逻辑比恢复动作本身更重要。5.2 三个真实案例初始化失败、收不到数据、偶发帧错误案例一初始化一直失败。现象是HAL_CAN_Init返回HAL_ERROR程序直接掉进Error_Handler。排查过程我按前面说的顺序过了一遍时钟已使能GPIO配置用的是CubeMX自动生成没动过看起来没问题。最后一行行对照寄存器才发现我把Prescaler配成了0。CAN的预分频值在HAL库中填写的是实际分频系数而底层寄存器里存的是分频系数减1所以Prescaler0在寄存器层面是合法的但实际TQ时钟直接变成了36MHz位时间计算全乱套。这类配置似懂非懂导致的初始化失败光看代码很难发现正确做法是把CAN_BTR寄存器值打印出来逐bit对照参考手册验证。案例二结点A发送成功但结点B收不到。A的发送函数返回HAL_OK说明报文已经送到了发送邮箱但B那边一直进不了接收回调。排查链路是这样的先确认波特率配置两边一致确实都是500kbps再检查终端电阻两个结点各有一个板载120Ω没有重复外接然后用逻辑分析仪挂在CANH和CANL之间抓波形发现波形本身是正常的说明物理层没问题。最后回过头查滤波器配置才发现B结点的滤波器掩码配错了只接收0x200到0x2FF区间的ID而A发送的0x101根本不在这个范围内。这个案例非常有代表性波形正常不代表能通信软件过滤同样会拦截报文。排查收不到数据的问题一定要把物理层通没通和应用层放没放行分开来判断。案例三偶发错误帧通信一阵正常一阵报错。这是最折磨人的情况。用CAN分析仪持续抓报发现错误帧没有固定规律但波特率越高错误越频繁。后来把总线两端的线从普通跳线换成了双绞线错误率立刻下降了一大截。再把采样点从默认的75%调整到80%错误帧完全消失。这个案例说明了两个问题CAN总线对线材和布线是有要求的普通排线在1Mbps下就是不行双绞线是底线采样点配置不是一个能通就行的参数它直接影响总线在恶劣条件下的稳定性。整理一下这三个案例的排查要点现象排查方向根因解决方案初始化失败时钟→GPIO→时序参数→读寄存器预分频填0对照BTR寄存器逐bit验证发送正常收不到波特率→终端→波形→滤波器滤波器掩码拦截了ID放宽掩码或精确配置偶发错误帧线材→采样点→波特率普通线采样点不合适换双绞线、调整采样点5.3 经典CAN、CANFD和网口的选型边界调试完P3之后我顺手研究了一下CANFD和CAN的差异因为这个话题在社区里讨论度很高。经典CAN的波特率上限是1Mbps数据段最多8字节CANFD在仲裁段仍然使用经典CAN的波特率保证兼容性但数据段可以切换到最高8Mbps数据长度最大64字节CRC校验也更加强悍。从协议角度看CANFD是向下兼容的总线上允许经典CAN帧和CANFD帧共存但前提是控制器本身支持CANFD。STM32F103的bxCAN不支持CANFD需要上G431、G474或者带FDCAN外设的芯片。到底选经典CAN还是CANFD我的建议很简单如果业务场景是周期性的短报文控制比如电机转速指令、状态机切换、传感器数值上报经典CAN的8字节数据段绰绰有余没必要为了CANFD去换主控如果涉及固件升级、日志上传、大批量标定数据这类数据量动不动几十上百KBCANFD的64字节数据段能大幅减少总线占用这时候才值得为了它升级硬件。至于CAN和网口以太网的对比经常有人问为什么工业控制不用以太网。以太网的优势是带宽大、生态完善但它的介质访问机制本质上是冲突检测加退避CSMA/CD虽然现代交换机全双工消除了冲突域但端到端的延迟仍然不可预计。CAN的仲裁是确定性的优先级高的帧延迟是可控的这对运动控制和实时控制系统来说是硬指标。一句话总结就是CAN用来传控制指令网口用来传大数据两者在工业场景里通常是互补关系而不是替代关系。6. 从双结点到多结点ID规划与协议演进6.1 多结点时的ID规划双结点系统里ID怎么定都没太大压力只要两个结点不重号就行。但如果你像我一样P3的双结点只是第一步后面还要挂更多的传感器、执行器、显示面板那ID规划就成了协议设计里优先级最高的一件事。临时随便分配ID的后果等结点多起来再回头改协议牵一发而动全身所有结点的代码和滤波表都要跟着动。我的习惯做法是把11位标准帧ID切分成几段来用。高位段表示功能域或者优先级级别中位段表示设备地址低位段表示命令类型。比如可以这样约定ID段比特位含义示例故障/紧急域0x000~0x0FF最低ID最高优先级0x001 紧急停机控制域0x100~0x1FF主控到执行结点0x101 转速指令状态上报域0x200~0x2FF执行结点到主控0x201 状态回报参数配置域0x300~0x3FF低速、可延迟0x301 参数写入这种分层的好处是故障帧和紧急指令永远拥有总线上的最高优先级不管其他结点在发什么总线仲裁都会保证故障帧先发出去周期性状态帧放在中间优先级保证实时性的同时不会把总线带宽全部占完参数配置这种不要求实时性的报文放在最高ID段总线繁忙时自动让路晚一点发也无所谓。滤波器配置也要跟着ID规划走。每个结点不再接收所有报文而是用掩码只接收与自己相关的那几个ID集合。比如执行结点只接收0x100~0x1FF的控制帧和0x300~0x3FF的参数配置帧剩下的统统不关心这样可以显著降低中断频率和CPU负载。广播帧的ID也建议在规划阶段就预留出来比如0x7FF统一作为对时或者同步广播帧。6.2 先把协议文档写清楚再考虑CANopenP3项目做到最后我最大的体会是双结点通信的代码其实不难写难的是把协议文档写得让未来的自己和合作的同事都能一眼看懂。我现在的习惯是动手写代码之前先输出一张完整的总线报文汇总表内容包括报文ID、报文方向主控到执行/执行到主控/双向、发送周期、DLC、每个字节的字段名、字段类型uint8/int16/枚举、缩放系数、字节序。这张表就是整个通信系统的宪法代码只是宪法的实现。等结点数真正多起来自定协议暴露出维护成本的时候就可以考虑上标准化的应用层协议了。工业领域最常用的是CANopen它的核心是对象字典Object Dictionary所有通信都是围绕对象字典来读写的。SDO用于参数配置PDO用于周期性实时过程数据还有心跳报文Heartbeat来监控各个结点的在线状态。CANopen的好处是协议框架现成生态成熟但学习成本也不低对于三五个结点的系统来说自定协议反而更灵活高效。我在P3里坚持用自定协议就是为了把底层机制先吃透。CAN总线的位时序、仲裁、错误处理这些底层机制搞明白了后面不管上CANopen还是J1939都只是套一层应用层壳的问题。双结点控制是CAN世界里最小的一张完整拼图拼好了后面所有跟CAN打交道的项目心里都不会再发怵。
返回列表