资讯中心

STM32上CANopenNode移植与RTOS适配实战指南

📅 2026/9/28 17:33:03
STM32上CANopenNode移植与RTOS适配实战指南
1. 为什么要在 STM32 上折腾 CANopenNode如果你做过工业控制、伺服驱动或者运动控制相关的项目大概率绕不开 CANopen 这个协议。它基于 CAN 总线在欧美工控领域几乎是标配国内汇川、步科、台达这些厂商的伺服驱动器也都支持。但问题在于很多朋友第一次接触 CANopen 的时候面对厚厚一本 DS301 协议文档往往不知道从哪里下手。我最初做的一个项目是用 STM32F407 控制三台伺服电机做多轴联动上位机通过串口发指令STM32 作为 CANopen 主站去调度三个从站。当时考虑过几个方案一是自己从零写 CANopen 协议栈二是用商业协议栈三是用开源的 CANopenNode。自己写不现实DS301 加上 DSP402 的协议内容太多没有几个月根本搞不定商业协议栈授权费动辄几万块小项目根本扛不住。最后选了 CANopenNode原因很简单代码干净、移植方便、社区活跃而且它本身就是为嵌入式场景设计的RAM 和 Flash 占用都很小。CANopenNode 的核心价值在于它把 CANopen 协议里那些繁琐的状态机、对象字典、PDO/SDO 传输机制都封装好了你只需要提供底层的 CAN 收发接口和定时器剩下的协议处理它自己搞定。在 STM32 上部署最典型的方式是裸机跑主循环但如果你项目里还有别的任务要处理比如串口通信、传感器采集、逻辑控制那就需要上 RTOS 来做任务调度。这也是为什么标题里专门提到了 RTOS 适配——实际项目里CANopen 很少单独存在它总是和其他功能模块共存。这篇文章适合谁看如果你手上有 STM32 的板子想快速把 CANopen 跑起来不管是做主站还是从站不管是用裸机还是 FreeRTOS这篇内容都能给你一套可以直接抄作业的方案。我会从工程结构、底层驱动对接、对象字典配置、RTOS 任务划分这几个维度把踩过的坑和验证过的做法都讲清楚。2. CANopenNode 的工程结构与移植思路拆解2.1 源码目录里哪些文件必须留、哪些可以砍CANopenNode 的源码结构其实很清晰但第一次拿到手的时候一堆 .c 和 .h 文件堆在一起容易让人犯迷糊。我一般会先把源码目录过一遍按功能模块分类核心协议层CANopen.c、CO_SDO.c、CO_PDO.c、CO_NMT_Heartbeat.c、CO_SYNC.c、CO_EMCY.c、CO_TIME.c这些是协议栈的骨架一个都不能少。对象字典层CO_OD.c、CO_OD.h这两个文件是根据你的 OD 配置自动生成的后面会讲怎么生成。驱动接口层CO_driver.h、CO_driver_target.h这是你需要自己实现的硬件抽象层也是移植的关键。存储层CO_storage.c、CO_storageBlank.c如果你需要掉电保存参数就要用到这部分不需要的话可以用 Blank 版本。额外功能CO_LEDs.c、CO_trace.c这些是可选的LED 指示和调试追踪按需添加。我一般会在 STM32 工程里建一个CANopenNode分组把核心协议层和对象字典层加进去驱动接口层单独放在Port分组里。这样结构清晰后面维护也方便。注意CANopenNode 的版本更新比较频繁不同版本的文件名和接口可能有差异。我用的比较稳的是 v1.3 和 v2.0 这两个大版本v2.0 对 RTOS 的支持更好建议新项目直接上 v2.0。2.2 移植的核心思路把硬件相关的部分抽出来CANopenNode 的设计哲学就是“协议与硬件分离”它定义了一套CO_driver.h接口你只需要在 STM32 上实现这几个函数CO_CANmodule_init()初始化 CAN 外设配置波特率、过滤器。CO_CANsend()把 CANopen 协议层要发的数据塞进 CAN 发送邮箱。CO_CANrxBufferInit()注册接收回调当收到特定 COB-ID 的报文时调用协议层的处理函数。CO_CANinterrupt()在 CAN 接收中断里调用把收到的报文分发给对应的处理函数。这套接口设计的好处是协议层完全不关心你用的是 STM32 的 bxCAN 还是别的什么 CAN 控制器它只管调用你实现的函数。所以移植的核心工作就是把这几个函数用 STM32 HAL 库或者标准库实现一遍。我个人的习惯是先不管协议层先把 CAN 的收发调通。用回环模式测试发一帧收一帧确认硬件和驱动没问题再去对接 CANopenNode。这样出问题的时候排查范围小不会一头雾水。2.3 对象字典的生成别手写用工具对象字典是 CANopen 的灵魂它定义了设备所有的参数、数据和通信对象。手写 OD 文件几乎不可能因为一个稍微完整一点的从站OD 条目就有上百个。CANopenNode 官方提供了一个 OD 编辑器工具你可以用 Excel 表格定义好索引、子索引、数据类型、访问权限然后一键生成CO_OD.c和CO_OD.h。我一般会先在 Excel 里把 OD 规划好比如索引子索引名称数据类型访问权限说明0x10000Device TypeUNSIGNED32RO设备类型0x10010Error RegisterUNSIGNED8RO错误寄存器0x10180Identity ObjectRECORDRO厂商信息0x20000Control WordUNSIGNED16RW控制字0x20010Status WordUNSIGNED16RO状态字规划好之后用工具生成代码直接替换工程里的CO_OD.c和CO_OD.h。这样既不容易出错后期改 OD 也方便。实操心得OD 编辑器生成的代码里CO_OD.c会包含一个CO_OD_ROM和CO_OD_RAM结构体前者存常量后者存变量。如果你用的是 STM32可以把CO_OD_ROM放到 Flash 里CO_OD_RAM放到 RAM 里节省 RAM 空间。3. STM32 底层驱动对接与实操要点3.1 CAN 外设初始化波特率和过滤器怎么配STM32 的 bxCAN 外设配置起来不算复杂但有几个参数必须算清楚。首先是波特率CANopen 默认支持 10k、20k、50k、125k、250k、500k、800k、1M 这几种。以 500k 为例假设 APB1 时钟是 42MHz预分频器设为 6则 CAN 时钟为 7MHz一个位时间分成 14 个时间份额其中 BS110BS23SJW1这样采样点就在 10/14 的位置约 71.4%符合 CANopen 推荐的采样点范围。用 HAL 库配置的代码大概长这样hcan1.Instance CAN1; hcan1.Init.Prescaler 6; hcan1.Init.Mode CAN_MODE_NORMAL; hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; hcan1.Init.TimeSeg1 CAN_BS1_10TQ; hcan1.Init.TimeSeg2 CAN_BS2_3TQ; hcan1.Init.TimeTriggeredMode DISABLE; hcan1.Init.AutoBusOff ENABLE; hcan1.Init.AutoWakeUp DISABLE; hcan1.Init.AutoRetransmission ENABLE; hcan1.Init.ReceiveFifoLocked DISABLE; hcan1.Init.TransmitFifoPriority DISABLE;过滤器配置是另一个容易踩坑的地方。CANopen 的报文 COB-ID 分布是有规律的比如 NMT 是 0x000SYNC 是 0x080EMCY 是 0x080NodeIDTPDO1 是 0x180NodeIDRPDO1 是 0x200NodeIDSDO 是 0x600/0x580NodeID。我一般会配置两组过滤器一组接收所有 CANopen 相关的报文另一组接收广播报文。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;这里我把掩码全设为 0意思是接收所有报文。实际项目里可以根据需要缩小范围减少中断频率。3.2 接收中断里的处理逻辑CAN 接收中断是 CANopenNode 和硬件交互的关键环节。当 CAN 控制器收到一帧报文中断触发你需要把报文从邮箱里读出来然后调用CO_CANinterrupt()或者直接调用对应的接收回调。我一般的做法是在中断里只做最少的处理读报文、判断 COB-ID、调用回调函数。不要在中断里做耗时操作比如打印日志、操作 Flash这些都应该放到主循环或者 RTOS 任务里。void CAN1_RX0_IRQHandler(void) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; HAL_CAN_GetRxMessage(hcan1, CAN_RX_FIFO0, rxHeader, rxData); CO_CANrxMsg_t rxMsg; rxMsg.ident rxHeader.StdId; rxMsg.DLC rxHeader.DLC; memcpy(rxMsg.data, rxData, 8); CO_CANinterrupt(CANopenNode, rxMsg); }注意CO_CANinterrupt()这个函数在 v2.0 版本里可能改名叫CO_CANrxInterrupt()或者类似的名字具体看你的版本。另外中断优先级要设置合理不要高于系统 tick 中断否则可能影响 RTOS 调度。3.3 定时器与时间基准CANopen 协议里有很多和时间相关的功能比如心跳生产、SDO 超时、SYNC 周期。CANopenNode 需要一个毫秒级的时间基准你可以用 STM32 的 SysTick 或者一个通用定时器来提供。我一般会在 SysTick 中断里调用CO_tick()或者类似函数让协议栈自己处理时间相关的逻辑。如果你用的是 FreeRTOSSysTick 已经被 RTOS 占用了那就需要另外开一个定时器比如 TIM6 或 TIM7配置成 1ms 中断。void TIM6_DAC_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim6, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_FLAG(htim6, TIM_FLAG_UPDATE); CO_tick(CANopenNode); } }这个 1ms 的时间基准很重要如果不准心跳周期会漂移SDO 传输可能超时SYNC 同步也会出问题。我实测下来用内部 RC 振荡器做时钟源的话时间基准误差比较大建议用外部晶振。4. RTOS 适配任务划分与优先级设计4.1 裸机 vs RTOS什么时候该上 RTOS如果你的项目里只有 CANopen 一个功能裸机跑主循环完全够用。主循环里调用CO_process()中断里处理 CAN 收发简单直接没有任务切换的开销。但实际项目往往不是这样。比如我做过的一个项目STM32 要同时处理CANopen 主站调度、串口和上位机通信、ADC 采集电流电压、PWM 输出控制电机、OLED 显示状态。这么多任务裸机跑起来就很吃力任何一个任务阻塞都会影响其他任务。这时候上 RTOS 就是必然选择。FreeRTOS 在 STM32 上跑得很稳资源占用也小我一般用它。任务划分的思路是CANopen 协议处理单独一个任务优先级设高一点串口通信一个任务优先级中等传感器采集和显示一个任务优先级低一点。这样保证 CANopen 的实时性同时其他任务也能正常运行。4.2 CANopen 任务的具体实现在 FreeRTOS 里跑 CANopenNode核心是把CO_process()放到一个独立任务里然后根据协议栈的状态决定是否需要延时。void CANopen_Task(void *argument) { CO_NMT_reset_cmd_t reset CO_RESET_NOT; uint16_t timeout 0; while (1) { reset CO_process(CANopenNode, 1, timeout); if (reset CO_RESET_COMM) { // 通信复位重新初始化 } else if (reset CO_RESET_APP) { // 应用复位重启系统 NVIC_SystemReset(); } if (timeout 0) { vTaskDelay(pdMS_TO_TICKS(timeout)); } else { taskYIELD(); } } }这里的关键是CO_process()的第二个参数它表示是否要处理同步窗口。如果你用了 SYNC就传 1没用就传 0。timeout是协议栈告诉你的下次处理时间如果大于 0就延时如果等于 0说明有紧急事件要处理让出 CPU 但不延时。实操心得CO_process()的调用周期直接影响 CANopen 的实时性。我一般把这个任务设成 1ms 周期优先级比串口任务高比中断低。实测下来500k 波特率下PDO 传输延迟可以控制在 2ms 以内。4.3 中断与任务的同步CAN 接收中断和 CANopen 任务之间需要同步。中断里收到报文后不能直接调用协议层的处理函数因为那可能会阻塞。正确的做法是中断里把报文放到一个队列里任务里从队列取出来处理。// 中断里 BaseType_t xHigherPriorityTaskWoken pdFALSE; xQueueSendFromISR(canRxQueue, rxMsg, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 任务里 CO_CANrxMsg_t rxMsg; if (xQueueReceive(canRxQueue, rxMsg, 0) pdTRUE) { CO_CANinterrupt(CANopenNode, rxMsg); }这样做的好处是中断处理时间极短不会影响其他中断的响应。队列的长度根据你的总线负载来定我一般设 16 或 32足够应对突发流量。4.4 优先级配置的坑FreeRTOS 的任务优先级和中断优先级是两套体系容易搞混。STM32 的中断优先级数值越小优先级越高FreeRTOS 的任务优先级数值越大优先级越高。而且 FreeRTOS 有一个configMAX_SYSCALL_INTERRUPT_PRIORITY的配置高于这个优先级的中断不能调用 FreeRTOS 的 API。我踩过的坑是CAN 接收中断的优先级设得太高结果在中断里调用xQueueSendFromISR()的时候触发了断言失败。后来把 CAN 中断优先级调到configMAX_SYSCALL_INTERRUPT_PRIORITY以下问题解决。注意如果你用的是 STM32CubeMX 生成的 FreeRTOS 工程它默认会把 SysTick 和 PendSV 的优先级设成最低这是正确的。但 CAN 中断的优先级需要你自己配置建议设成 5 或 6如果优先级分组是 4 位的话确保低于configMAX_SYSCALL_INTERRUPT_PRIORITY。5. 常见问题与排查技巧实录5.1 CANopen 通信不上怎么一步步排查这是最常见的问题我一般按以下顺序排查硬件层面CAN_H 和 CAN_L 有没有接反终端电阻有没有接120 欧姆的终端电阻必须接在总线两端中间节点不需要接。用万用表量一下 CAN_H 和 CAN_L 之间的电阻应该是 60 欧姆左右两个 120 欧姆并联。波特率主站和从站的波特率必须一致。我遇到过好几次主站设的 500k从站出厂默认是 1M怎么都通不上。用示波器或者 CAN 分析仪看一下波形确认波特率。过滤器配置STM32 的 CAN 过滤器如果配错了报文根本进不了接收 FIFO。可以先配成接收所有报文确认能收到再缩小范围。节点 IDCANopen 的节点 ID 决定了 COB-ID如果两个节点 ID 冲突通信会乱。确认每个节点的 ID 唯一。NMT 状态从站上电后默认是 Pre-operational 状态只有收到 NMT 启动命令后才会进入 Operational 状态才会发送 PDO。如果你发现 SDO 能通但 PDO 不通大概率是 NMT 状态不对。5.2 SDO 传输超时怎么办SDO 传输超时通常有几个原因一是从站没有响应可能是节点 ID 不对或者从站没上电二是 SDO 缓冲区太小数据太长传不完三是时间基准不准导致超时判断错误。我一般会先用 CAN 分析仪抓包看看 SDO 请求发出去没有从站有没有回复。如果请求发出去了但从站没回复检查从站的 OD 里有没有对应的索引。如果从站回复了但主站没收到检查主站的过滤器配置。实操心得CANopenNode 的 SDO 超时时间默认是 1000ms可以通过修改CO_SDO_TIMEOUT来调整。如果总线负载很高可以适当加大这个值。5.3 心跳丢失和节点保护心跳是 CANopen 里用来监控节点状态的机制。主站通过心跳消费者来监控从站如果从站心跳丢失主站会触发心跳事件。我遇到过从站心跳周期设得太短比如 10ms结果总线负载太高心跳报文丢包主站误判从站掉线。解决办法是合理设置心跳周期一般 100ms 到 500ms 比较合适。如果节点多可以适当加大周期降低总线负载。另外心跳消费者和心跳生产者要配对使用主站要配置心跳消费者的超时时间一般是心跳周期的 3 倍。5.4 常见问题速查表问题现象可能原因排查方法解决方案完全通信不上硬件接线错误检查 CAN_H/CAN_L 和终端电阻重新接线补上 120 欧姆电阻能收不能发CAN 发送邮箱满检查发送失败计数增加发送超时处理降低发送频率SDO 超时从站无响应用分析仪抓包检查节点 ID 和 OD 配置PDO 不更新NMT 状态不对检查 NMT 状态机发送 NMT 启动命令心跳丢失总线负载过高检查总线负载率加大心跳周期减少 PDO 数量时间基准漂移时钟源不准测量实际心跳周期改用外部晶振5.5 调试工具和技巧调试 CANopen 的时候一个 CAN 分析仪是必不可少的。我用的比较多的是 USB-CAN 分析仪配合上位机软件可以实时抓包、解析 CANopen 报文。如果没有分析仪也可以用 STM32 的串口打印调试信息把关键的 COB-ID 和数据打印出来。另外CANopenNode 自带了一个CO_trace模块可以把协议栈的内部状态输出到串口或者内存缓冲区方便分析。我一般在调试阶段会打开这个功能确认没问题后再关掉节省资源。注意调试信息不要打印太频繁否则会影响实时性。我一般只在关键节点打印比如 NMT 状态切换、SDO 传输完成、心跳超时这些事件。6. 从站和主站的配置差异6.1 从站配置OD 是核心从站的配置工作主要集中在 OD 上。你需要定义好所有的通信对象和应用对象包括通信对象0x1000-0x1FFF 是通信子协议区定义了设备类型、错误寄存器、厂商 ID、SYNC 参数、心跳参数、PDO 映射等。应用对象0x2000-0x5FFF 是厂商自定义区你可以在这里定义控制字、状态字、目标位置、实际位置等应用相关的参数。从站的 PDO 映射是关键它决定了哪些数据通过 PDO 传输。TPDO 是从站发给主站的RPDO 是主站发给从站的。映射的时候要注意数据长度和字节对齐比如一个 UNSIGNED32 占 4 个字节映射到 PDO 里要占 4 个字节的位置。6.2 主站配置调度和监控主站的工作是调度和监控从站。你需要实现 NMT 主站功能发送启动、停止、复位命令实现心跳消费者监控从站状态实现 SDO 客户端配置从站参数实现 PDO 收发和从站交换数据。主站的 OD 相对简单主要定义一些通信参数和全局变量。但主站的逻辑比较复杂需要处理多个从站的状态机处理通信错误和恢复。我一般会在主站里维护一个从站状态表记录每个从站的 NMT 状态、心跳状态、错误计数。当某个从站心跳丢失时触发报警或者自动恢复流程。6.3 主从站通信的时序问题主从站通信的时序很重要。比如主站发送 NMT 启动命令后从站需要一定时间才能进入 Operational 状态这时候主站如果立刻发 PDO从站可能还没准备好。我一般会在 NMT 启动后延时 100ms 再开始 PDO 通信。另外SYNC 同步的时序也要注意。主站发送 SYNC 后从站会在 SYNC 窗口内更新 PDO 数据。如果主站的 SYNC 周期太短从站可能来不及处理导致数据不同步。我一般把 SYNC 周期设在 1ms 到 10ms 之间根据实际需求调整。7. 性能优化与资源占用分析7.1 RAM 和 Flash 占用CANopenNode 在 STM32F407 上的资源占用我实测下来大概是这样的模块Flash 占用RAM 占用核心协议层约 12KB约 2KB对象字典约 4KB约 1KB驱动接口约 2KB约 0.5KBFreeRTOS约 6KB约 4KB合计约 24KB约 7.5KB这个占用对于 STM32F4071MB Flash192KB RAM来说完全不是问题。即使是 STM32F10364KB Flash20KB RAM也能跑得起来只是要精简一些功能。7.2 中断延迟和任务切换开销CAN 接收中断的延迟直接影响 CANopen 的实时性。我实测下来STM32F407 在 168MHz 主频下CAN 中断的响应时间大约是 1-2 微秒加上 FreeRTOS 的任务切换开销总的延迟在 5 微秒左右。这个延迟对于 500k 波特率位时间 2 微秒来说完全够用。如果对实时性要求更高可以把 CANopen 任务设成最高优先级或者用裸机跑减少任务切换开销。7.3 总线负载率的控制总线负载率是 CANopen 网络设计的重要指标。负载率太高会导致报文延迟增加甚至丢包。我一般会把负载率控制在 30% 以下留出足够的余量。计算负载率的方法是统计单位时间内总线上传输的位数除以总线带宽。比如 500k 波特率1 秒内最多传输 500000 位。如果实际传输了 150000 位负载率就是 30%。降低负载率的方法有减少 PDO 数量、加大 PDO 传输周期、提高波特率、优化心跳周期。我一般会先用分析仪测一下实际负载率再决定怎么优化。8. 实际项目中的经验总结8.1 版本选择稳定比新功能重要CANopenNode 的版本更新比较快但我一般不会追最新版。新版本可能引入新的 bug或者接口变化导致移植工作量增加。我一般选一个稳定的版本比如 v1.3 或 v2.0然后在项目里固定下来不轻易升级。如果非要用新版本建议先在开发板上验证确认没问题再放到正式项目里。8.2 代码结构分层清晰便于维护我在项目里一般会把代码分成三层硬件层、协议层、应用层。硬件层负责 CAN 驱动、定时器、GPIO协议层就是 CANopenNode应用层是具体的业务逻辑。三层之间通过接口函数通信不要跨层调用。这样分层的好处是换硬件平台的时候只需要改硬件层协议层和应用层不用动。我试过把同样的 CANopenNode 代码从 STM32F407 移植到 STM32F103只改了硬件层的几个函数半天就搞定了。8.3 测试策略先回环再单机最后联调测试 CANopen 的时候我一般分三步走回环测试把 CAN 设成回环模式自己发自己收确认驱动和协议栈的基本功能正常。单机测试用 CAN 分析仪模拟从站测试主站的 NMT、SDO、PDO 功能。联调测试接上真实的从站测试完整的通信流程和异常处理。每一步都要充分测试不要跳过。我见过有人直接上真实设备结果通信不上排查了半天发现是终端电阻没接。8.4 文档和注释别偷懒CANopen 的配置项很多OD 条目、PDO 映射、心跳参数这些东西如果不写注释过两个月自己都看不懂。我一般会在 OD 表格里详细标注每个条目的含义、数据类型、默认值、访问权限生成的代码里也保留注释。另外项目里的关键配置比如波特率、节点 ID、心跳周期最好写在一个配置文件里不要散落在各个源文件中。这样改配置的时候只改一个地方就行。8.5 后续扩展方向CANopenNode 跑通之后还可以继续扩展。比如加上 CANopen 的紧急报文EMCY处理实现故障报警加上时间戳功能实现精确的时间同步加上存储功能实现参数掉电保存。如果项目里有多条 CAN 总线还可以实现 CANopen 网关把一条总线上的数据转发到另一条总线。这些扩展功能 CANopenNode 都支持只需要在 OD 里配置好然后在应用层实现相应的逻辑。我个人在实际操作中的体会是CANopenNode 的移植本身不难难的是对 CANopen 协议的理解和调试。协议里的状态机、对象字典、PDO 映射这些概念需要花时间消化。但只要跑通一次后面再做类似的项目就轻车熟路了。我建议新手先从最简单的从站做起配置几个 PDO跑通通信再逐步增加功能。不要一上来就搞复杂的多轴联动那样容易受挫。

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案