ARTICLE DETAIL

资讯详情

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

STM32F4上CANOpen移植实战:从零搭建到避坑指南

STM32F4上CANOpen移植实战:从零搭建到避坑指南 1. 为什么要在STM32F4上跑CANOpen而不是自己撸一套协议做过工业控制的朋友大概率都遇到过这个场景一台设备需要和伺服驱动器、远程IO模块或者上位机PLC通信总线用的是CAN但通信协议五花八门。有人用自定义的帧格式有人用Modbus RTU转CAN还有人直接上CANOpen。如果你选了自己撸一套协议短期看确实自由度高但一旦设备数量上去、节点类型变多你就会发现——设备状态管理、心跳监控、PDO数据映射、SDO参数读写这些东西全得自己从头写而且每换一个厂家的设备协议适配就得重来一遍。CANOpen的价值就在这里。它是一套标准化的应用层协议底层跑在CAN总线上定义了一套完整的通信对象和状态机。伺服驱动器、变频器、编码器、远程IO只要标了CANOpen理论上你拿同一套代码就能对接不同厂家的设备。对于STM32F4这种带双CAN控制器的芯片来说跑CANOpen几乎是工业项目的标配需求。但问题来了CANOpen协议栈的移植尤其是从零开始在STM32F4上搭一套能跑起来的CANOpen坑非常多。我自己前前后后做过四五个CANOpen项目从最早的CANfestival到后来的商业协议栈踩过的坑包括但不限于定时器中断优先级冲突导致心跳报文丢失、PDO映射参数配置错误导致数据错位、NMT状态机切换不成功、SDO分段传输超时等等。这篇文章就是把这些经验整理出来手把手带你在STM32F4上完成CANOpen移植同时把那些容易翻车的地方提前标出来。这篇文章适合谁看如果你已经有STM32F4的基础会用CubeMX配置外设能看懂CAN的基本帧格式那就可以直接往下看。如果你连CAN的显性电平和隐性电平都还没搞清楚建议先去补一下CAN总线的基础知识不然移植过程中遇到问题会很难定位。注意本文以CANfestival协议栈为例进行讲解这是目前开源社区里用得最多的CANOpen协议栈之一。商业协议栈的移植思路类似但API和配置方式会有差异。2. 移植前的硬件与软件环境盘点2.1 STM32F4的CAN外设能力确认STM32F4系列比如最常见的STM32F407通常带两个CAN控制器也就是CAN1和CAN2。这两个控制器共享滤波器组CAN1用滤波器0-13CAN2用滤波器14-27。这一点在配置的时候要特别注意如果你两个CAN口都要用滤波器的分配不能搞错。CAN控制器的时钟来源是APB1总线STM32F407的APB1默认是42MHz。计算波特率的时候CAN的位时序寄存器CAN_BTR里有个BRP分频系数加上BS1和BS2段的时间份额最终波特率 APB1时钟 / (BRP × (1 BS1 BS2))。举个例子你要跑500kbpsAPB1是42MHz那BRP可以取6BS1取5BS2取2算下来42M / (6 × 8) 875kHz不对得重新算。实际上常用配置是BRP6BS15BS22总时间份额152842M/(6×8)875k这不对。正确的500k配置BRP6BS15BS2242M/(6×(152))42M/48875k还是不对。让我重新算42MHz / 500kHz 84所以BRP × (1BS1BS2) 84。如果BRP6那(1BS1BS2)14BS111BS22这样就是6×1484波特率正好500k。采样点位置在(111)/14 ≈ 85.7%这个采样点偏高一般推荐75%-80%。所以更常用的配置是BRP7(1BS1BS2)12BS19BS22采样点(19)/1283.3%还是偏高。BRP6BS110BS23总份额14采样点11/1478.6%这个比较合适。所以最终配置BRP6BS110BS23SJW1。这些参数在CubeMX里可以直接填但你要知道背后的计算逻辑不然出了问题不知道怎么调。2.2 CANfestival协议栈的获取与目录结构CANfestival的源码可以从它的官方仓库获取版本建议用3.0以上。下载下来之后目录结构大概是这样的CANfestival-3/ ├── include/ # 头文件目录 ├── src/ # 协议栈核心源码 ├── drivers/ # 硬件驱动适配层 │ ├── can_stm32/ # STM32的CAN驱动可能需要自己适配 │ └── timer_stm32/ # STM32的定时器驱动 ├── examples/ # 示例代码 └── objdict/ # 对象字典相关工具移植的核心工作就是把drivers目录下的驱动适配到你的STM32F4平台上同时配置好对象字典。CANfestival本身是平台无关的它通过几个回调函数和硬件打交道CAN发送、CAN接收、定时器获取当前时间、定时器启动。你只需要把这几个函数实现好协议栈就能跑起来。2.3 定时器资源的分配策略CANfestival需要一个硬件定时器来驱动协议栈的时间管理包括心跳报文周期、SDO超时、节点保护时间等。在STM32F4上我一般用TIM2或者TIM3因为它们挂载在APB1上和CAN的时钟源一致配置起来比较方便。定时器的中断频率建议设为1ms一次。这个频率不能太低否则心跳报文的周期精度不够也不能太高否则中断开销太大影响主循环。1ms是一个比较平衡的选择。在定时器中断里你只需要调用一个函数来更新协议栈的时间基准比如TimeDispatch()或者类似的接口。提示定时器中断的优先级一定要设置好。如果CAN接收中断的优先级低于定时器中断在高负载情况下可能会出现CAN报文丢失。建议CAN接收中断优先级高于定时器中断但不要设成最高留一点余地给系统其他关键中断。3. 从零搭建CANOpen节点的完整操作链路3.1 CubeMX里的CAN外设配置要点打开CubeMX先配置CAN1。模式选Normal波特率参数按前面算的填Prescaler6BS110BS23SJW1。自动重传要打开否则总线仲裁失败后不会自动重发。接收FIFO用FIFO0就够了中断里使能RX FIFO0 pending中断。滤波器的配置是个容易翻车的地方。CANOpen用的是11位标准帧功能码加节点ID的格式。比如NMT报文的功能码是0x000节点ID是1的话COB-ID就是0x001。心跳报文的功能码是0x700节点ID是1的话就是0x701。所以滤波器的掩码模式要能覆盖这些ID范围。最简单的做法是配置一个全通过的滤波器接收所有报文然后在软件里根据COB-ID过滤。这样做的好处是不会漏掉任何报文缺点是CPU要处理所有总线上的帧。如果总线负载不高比如只有几个节点全通过完全没问题。如果总线负载很高那就需要精细配置滤波器只接收本节点相关的COB-ID。// 全通过滤波器配置示例 CAN_FilterTypeDef filter; filter.FilterBank 0; filter.FilterMode CAN_FILTERMODE_IDMASK; filter.FilterScale CAN_FILTERSCALE_32BIT; filter.FilterIdHigh 0x0000; filter.FilterIdLow 0x0000; filter.FilterMaskIdHigh 0x0000; filter.FilterMaskIdLow 0x0000; filter.FilterFIFOAssignment CAN_RX_FIFO0; filter.FilterActivation ENABLE; HAL_CAN_ConfigFilter(hcan1, filter);3.2 CANfestival驱动层的适配实现CANfestival的驱动适配主要实现四个函数canSend()、canReceive()、getElapsedTime()、setTimer()。下面逐个说。canSend()负责把协议栈要发的报文通过CAN控制器发出去。CANfestival传进来的参数是一个Message结构体里面有cob_id、len、data。你只需要把这些数据填到CAN发送邮箱里就行。注意发送的时候要处理邮箱满的情况如果三个发送邮箱都满了要么等待要么返回错误让协议栈重试。UNS8 canSend(CAN_PORT notused, Message *m) { CAN_TxHeaderTypeDef txHeader; uint32_t txMailbox; txHeader.StdId m-cob_id; txHeader.ExtId 0; txHeader.IDE CAN_ID_STD; txHeader.RTR CAN_RTR_DATA; txHeader.DLC m-len; txHeader.TransmitGlobalTime DISABLE; if (HAL_CAN_AddTxMessage(hcan1, txHeader, m-data, txMailbox) ! HAL_OK) { return 0xFF; // 发送失败 } return 0; }canReceive()是在CAN接收中断里调用的把收到的报文转成CANfestival的Message格式然后调用canDispatch()交给协议栈处理。这里要注意canDispatch()的执行时间不能太长否则会阻塞中断。如果协议栈处理逻辑比较重建议在中断里只做数据拷贝把实际处理放到主循环里。void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; Message msg; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData); msg.cob_id rxHeader.StdId; msg.len rxHeader.DLC; memcpy(msg.data, rxData, rxHeader.DLC); canDispatch(TestMaster_Data, msg); }getElapsedTime()返回从某个基准点到现在经过的时间单位是微秒。你可以用一个全局变量在定时器中断里累加然后这个函数直接返回这个变量的值。setTimer()用来设置一个定时器在指定的时间后触发回调。CANfestival用这个来实现心跳周期和SDO超时。你可以在定时器中断里检查是否到了设定的时间到了就调用对应的回调函数。3.3 对象字典的生成与配置对象字典是CANOpen的核心它定义了节点所有的参数和数据结构。CANfestival提供了一个叫objdictedit的工具可以图形化编辑对象字典然后生成C代码。对于从站节点最基本的对象字典条目包括索引子索引名称说明0x10000Device Type设备类型0x10010Error Register错误寄存器0x10170Producer Heartbeat Time心跳周期0x10180-4Identity Object厂商ID、产品码等0x18000-5TPDO1 Communication ParameterTPDO1通信参数0x1A000-8TPDO1 Mapping ParameterTPDO1映射参数0x14000-5RPDO1 Communication ParameterRPDO1通信参数0x16000-8RPDO1 Mapping ParameterRPDO1映射参数心跳周期设成1000ms也就是0x1017写1000。TPDO1的传输类型一般设成0xFF表示事件触发或者设成1-240表示同步周期。映射参数里把你要发送的数据对象映射进去比如0x2000子索引0是一个16位的模拟量输入那就把0x2000:00映射到TPDO1的第一个位置。注意对象字典的索引分配要遵循CANOpen的规范。0x1000-0x1FFF是通信参数区0x2000-0x5FFF是制造商自定义区0x6000-0x9FFF是标准设备子协议区。自己定义的数据对象放在0x2000以后不要占用标准区的索引。4. 那些让我熬夜的坑排查链路与修复方案4.1 心跳报文不发送从NMT状态机查起第一次移植完最常遇到的问题是节点不上线心跳报文死活发不出来。我当时的排查过程是这样的先确认NMT状态机是否进入了Operational状态。CANOpen节点上电后默认是Pre-operational状态在这个状态下只有SDO和心跳能工作PDO是不发的。要让节点进入Operational需要主站发一条NMT命令或者节点自己配置成自动进入Operational。用CAN分析仪抓包看看总线上有没有NMT报文。如果没有那就是主站的问题如果有NMT报文但节点没反应那就是节点的问题。检查canDispatch()函数里对NMT报文的处理确认NMT_Start_Node()或者类似的函数被正确调用了。还有一个隐蔽的坑心跳报文的COB-ID配置。0x1017设置的是心跳周期但心跳报文的COB-ID是0x700 节点ID。如果你的节点ID是1那心跳COB-ID就是0x701。有些协议栈实现里心跳COB-ID是自动计算的但有些需要手动配置。确认一下你的对象字典里0x1017的值是否正确以及协议栈是否在定时器里正确触发了心跳发送。4.2 PDO数据错位映射参数与字节序的陷阱PDO数据错位是另一个高频问题。现象是主站收到的数据和自己期望的对不上比如温度值跑到了位置值的位置上。根因通常是映射参数配置错误。TPDO的映射参数0x1A00里每个条目是一个32位的值高16位是索引低16位是子索引和长度。比如你要映射0x2000:00这个16位对象映射值就是0x20000010。如果你写成了0x20000008那就变成了8位长度数据就会错位。字节序也是个大坑。CANOpen规定多字节数据用小端模式但有些厂家的设备用大端。如果你和第三方设备通信发现数据高低字节反了先检查字节序。CANfestival默认是小端如果你的对象字典里的数据是大端存储的需要在映射的时候做转换。// 正确的映射值计算 // 索引0x2000子索引0x00长度16位 uint32_t mappingValue (0x2000 16) | (0x00 8) | 0x10; // 结果是0x200000104.3 SDO传输超时分段传输的坑SDO用来读写对象字典对于超过4字节的数据需要用分段传输。分段传输的坑在于超时时间的设置和握手流程。CANfestival里SDO的超时时间默认是几百毫秒如果从站响应慢可能会超时。你可以在对象字典里调整SDO超时参数或者直接在协议栈源码里改默认值。分段传输的握手流程是客户端发初始化请求服务端回应然后客户端发数据段服务端确认直到数据发完。如果中间某一步的确认丢了整个传输就会卡住。排查的时候用CAN分析仪看SDO报文序列确认每一步的握手是否完整。还有一个容易忽略的点SDO的COB-ID。默认情况下SDO请求的COB-ID是0x600 节点ID响应是0x580 节点ID。如果你改了节点ID但没改SDO的COB-ID配置SDO就会失败。4.4 定时器中断优先级导致的通信不稳定这个问题最隐蔽现象是通信时好时坏总线负载高的时候特别容易出问题。根因是定时器中断和CAN接收中断的优先级配置不当。如果定时器中断优先级高于CAN接收中断当定时器中断频繁触发时CAN接收中断会被延迟处理导致接收FIFO溢出报文丢失。STM32F4的NVIC优先级分组默认是4位抢占优先级和0位子优先级或者2位抢占和2位子优先级。建议把CAN接收中断的抢占优先级设得比定时器中断高比如CAN接收设为1定时器设为2。这样CAN报文来了能优先处理定时器中断晚一点执行不影响功能。// CAN接收中断优先级设置 HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 1, 0); HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn); // 定时器中断优先级设置 HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn);5. 移植完成后的验证与调试手段5.1 用CAN分析仪抓包验证通信流程移植完成后第一件事是抓包验证。把CAN分析仪接到总线上观察节点上电后的报文序列。正常的流程应该是节点上电发送心跳报文如果配置了主站发NMT命令让节点进入Operational然后TPDO开始周期性发送。如果心跳报文周期不对检查0x1017的值和定时器中断的频率。如果TPDO数据不对检查映射参数和实际数据源。如果SDO读写失败检查COB-ID和超时配置。抓包的时候要注意时间戳CANOpen对时间比较敏感。心跳周期偏差超过10%就可能被主站判定为掉线。5.2 用Python快速搭建CANOpen上位机测试没有商业CANOpen主站软件的时候可以用Python的canopen库快速搭一个测试上位机。安装很简单pip install canopen然后写一个简单的脚本扫描网络上的节点读取对象字典import canopen network canopen.Network() network.connect(channelcan0, bustypesocketcan) # 扫描节点1到10 for node_id in range(1, 11): node network.add_node(node_id) try: node.load_configuration() print(fNode {node_id} found) except Exception as e: print(fNode {node_id} not responding: {e}) # 读取节点1的设备类型 node network.add_node(1) device_type node.sdo[0x1000].raw print(fDevice type: {hex(device_type)})这个脚本可以快速验证节点的SDO通信是否正常。如果SDO能读到数据说明基本的CANOpen通信已经通了。5.3 长时间运行稳定性测试的观察指标短时间跑通不代表没问题CANOpen节点需要长时间运行稳定性测试。我一般会跑至少24小时观察以下几个指标总线错误计数STM32F4的CAN控制器有错误计数器可以通过CAN_ESR寄存器读取。如果错误计数持续增长说明总线质量有问题或者波特率不匹配。心跳丢失次数主站记录心跳报文的接收情况如果出现丢失检查节点的定时器中断是否被其他高优先级中断阻塞。内存泄漏如果协议栈里用了动态内存分配长时间运行后检查堆栈使用情况。CANfestival默认不用动态内存但如果你自己加了功能要注意。CPU负载在定时器中断里翻转一个GPIO用示波器看波形频率可以估算CPU的负载情况。如果负载超过70%需要考虑优化代码或者提高主频。6. 从能跑到好用几个值得做的优化6.1 对象字典的裁剪与内存优化CANfestival默认生成的对象字典包含了很多用不到的标准条目这些条目会占用Flash和RAM。如果你的STM32F4资源紧张可以裁剪掉不需要的条目。裁剪的原则是只保留通信必需的和实际用到的对象。比如节点保护相关的对象0x100C、0x100D如果不用可以删掉一些标准化的错误码定义如果不用也可以删。但要注意有些主站会读取特定的对象来判断节点类型删之前确认主站的行为。裁剪之后要重新生成对象字典代码并更新协议栈的配置。裁剪过度可能导致主站无法识别节点所以建议先保留完整的对象字典跑通再逐步裁剪。6.2 中断里的处理策略快进快出CAN接收中断里只做最必要的事情读取报文、拷贝数据、设置标志位。实际的协议栈处理放到主循环里。这样做的好处是中断响应快不会因为协议栈处理耗时导致其他中断被延迟。具体实现方式是在中断里把报文存到一个环形缓冲区主循环里从缓冲区取报文并调用canDispatch()。环形缓冲区的大小根据总线负载来定一般32或64个条目就够了。// 环形缓冲区定义 #define CAN_RX_BUFFER_SIZE 64 typedef struct { Message msgs[CAN_RX_BUFFER_SIZE]; volatile uint16_t head; volatile uint16_t tail; } CanRxBuffer; CanRxBuffer rxBuffer; // 中断里写入 void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { // ... 读取报文 ... uint16_t next (rxBuffer.head 1) % CAN_RX_BUFFER_SIZE; if (next ! rxBuffer.tail) { rxBuffer.msgs[rxBuffer.head] msg; rxBuffer.head next; } // 如果缓冲区满了丢弃最老的报文或记录错误 } // 主循环里处理 void mainLoop(void) { while (rxBuffer.tail ! rxBuffer.head) { Message msg rxBuffer.msgs[rxBuffer.tail]; rxBuffer.tail (rxBuffer.tail 1) % CAN_RX_BUFFER_SIZE; canDispatch(TestMaster_Data, msg); } }6.3 心跳周期的动态调整有些应用场景下心跳周期需要根据总线负载动态调整。总线负载低的时候心跳可以快一点负载高的时候慢一点。CANfestival本身不支持动态调整但你可以通过修改对象字典0x1017的值来实现。修改的时候要注意心跳周期的改变不是立即生效的需要等到下一个心跳周期才会用新值。如果你在心跳回调里修改周期要确保不会造成死循环或者周期震荡。我在一个项目里做过这样的优化总线负载超过60%时心跳周期从1000ms自动调整到2000ms负载降到40%以下时再调回1000ms。这样既保证了低负载时的实时性又避免了高负载时心跳报文加剧总线拥塞。6.4 错误处理与恢复机制工业现场环境恶劣CAN总线受干扰是常态。节点需要具备错误检测和自动恢复能力。STM32F4的CAN控制器在总线错误时会进入错误被动状态甚至总线关闭状态。总线关闭后需要软件干预才能恢复。你可以在CAN错误中断里检测总线关闭状态然后执行恢复流程等待一段时间重新初始化CAN控制器重新加入总线。void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan) { uint32_t error HAL_CAN_GetError(hcan); if (error HAL_CAN_ERROR_BOF) { // 总线关闭执行恢复 HAL_CAN_Stop(hcan); HAL_Delay(100); HAL_CAN_Start(hcan); HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING); } }同时协议栈层面也要处理通信中断的情况。如果心跳超时或者SDO连续失败节点应该记录错误并尝试重新建立通信。CANfestival提供了一些错误回调接口你可以在这些回调里实现自己的恢复逻辑。7. 一些零散但重要的经验节点ID的分配要提前规划好不要等到现场调试的时候才发现两个节点ID冲突。CANOpen的节点ID范围是1-1270是广播地址。建议在项目初期就做一个节点ID分配表每个设备对应一个唯一的ID。CAN总线的终端电阻不能忘。120欧姆的终端电阻要接在总线的两端中间节点不要接。我见过太多因为终端电阻没接导致通信不稳定的案例排查半天最后发现是硬件问题。线缆的选择也有讲究。CAN_H和CAN_L要用双绞线屏蔽层单端接地。线径不要太细长距离通信时线径不够会导致信号衰减。如果通信距离超过100米波特率要相应降低。对象字典的备份很重要。每次修改对象字典后把生成的C文件和配置文件一起备份。现场调试的时候如果发现对象字典有问题可以快速回滚到上一个版本。如果项目里同时用了FreeRTOSCANOpen的任务优先级要设置合理。CANOpen的处理任务优先级不能太低否则会被其他任务阻塞导致通信超时。建议CANOpen任务优先级高于普通的应用任务但低于系统关键任务。最后说一个调试技巧在协议栈的关键函数入口和出口翻转GPIO用示波器或者逻辑分析仪观察波形可以直观地看到协议栈的执行时间和调用频率。这个方法比打印日志快得多而且不影响实时性。我在排查SDO超时问题的时候就是靠这个方法定位到了某个函数执行时间过长的问题。
返回列表