简介:基于CANfestival的CANopen协议在STM32F1系列单片机上的实现,是一份面向嵌入式开发工程师的完整工程资源,解决CANopen协议栈在STM32F1平台下的移植与集成问题。资源共931个文件,压缩包大小28.8MB,包含大量C语言源码与头文件(.c/.h)、编译生成的目标文件(.o/.d)、汇编启动文件(.s)、Keil工程配置(.uvprojx)以及烧录文件(.hex)等,涵盖从协议栈底层CAN驱动到NMT、SDO、PDO服务的完整实现。已有392人学习。通过这套代码,开发者可掌握对象字典配置、PDO映射、心跳报文等核心机制,并基于canopend、objdict等模块快速搭建自己的CANopen从站节点。资源内还附带调试辅助文件与说明文档,便于对照学习工程结构和排错,适合具备一定STM32基础、希望深入CAN总线通信的研发人员。
1. CANfestival与STM32F1的CANopen实现:从源码包到节点上线
CANopen在工控设备里几乎是默认配置,驱动器、IO模块、编码器都靠它把数据挂到总线上。但真正把它从零跑起来的人并不多,因为协议栈本身有体量,再加上对象字典、NMT状态机、PDO和SDO这些概念叠在一起,初次接触的人很容易卡在“协议栈到底怎么进单片机”这一步。这篇文章围绕CANfestival在STM32F1系列单片机上的落地方案,从CANopen的层级结构、源码组织、bxCAN外设适配,一直讲到PDO映射和SDO调试。适合需要把CANopen从站在F1上落地、而不是只看协议文档的嵌入式工程师。
这里有个反直觉的结论:CANopen的复杂度不在CAN控制器上,而在对象字典和状态机。STM32F1的bxCAN只负责收发帧,NMT状态切换、心跳计时、SDO分段传输全部在CPU上跑。所以移植工作的大部分是把CANfestival的接口和定时器、中断对齐,而不是研究CAN硬件本身。
2. CANopen协议层级结构与CANfestival在STM32F1上的适配边界
CANopen不是一种新总线,它是建立在CAN之上的应用层协议。ISO 11898定义物理层和数据链路层,CANopen在之上固定了COB-ID分配、8字节数据域语义、对象字典,以及NMT、PDO、SDO、EMCY这些通信对象。CANfestival就是按CiA 301规范实现的开源协议栈,它把CAN控制器抽象成少数几个收发函数,上层跑完整的从站逻辑。
2.1 CANopen层级结构:STM32F1只负责到数据链路层
在CANfestival视角里,STM32F1的bxCAN承担“收发帧”这一件事。CAN报文只是一条11位标准帧或29位扩展帧,CANopen约定统一使用29位扩展帧,COB-ID放到ExtId里,8字节数据原样放进Data。因此适配层要做的事非常有限:把协议栈给出的Message结构体翻译成bxCAN的CanTxMsg,再把接收中断里的CanRxMsg翻译回Message。
后面的NMT状态切换、SDO分段、PDO映射、心跳计时全部由协议栈核心代码在MCU上完成。这一结论直接决定了工程组织方式:移植时不需要修改sdo.c、pdo.c这类核心文件,只需要保证三个基础能力可用——CAN帧发送、CAN帧接收、周期定时器。这三件事在STM32F1上分别对应bxCAN的发送邮箱与FIFO,以及任意一个通用定时器。
2.2 CANfestival源码包的关键目录与移植边界
CANfestival源码包的结构各版本基本一致,移植时主要用到下面这些文件。注意src目录是平台无关的核心逻辑,不要改动。
| 目录/文件 | 作用 | 移植时是否修改 |
|---|---|---|
| src/nmtSlave.c | 从站NMT状态机 | 否 |
| src/sdo.c | SDO快速/分段传输 | 否 |
| src/pdo.c | PDO收发与映射 | 否 |
| src/timer.c | 软件定时器列表 | 否,需要外部TimeDispatch驱动 |
| drivers | 板级驱动示例 | 参考,按平台重写 |
| examples | 示例工程 | 参考,不直接用 |
常见做法是复制一份examples里最接近的工程,然后把其中的CAN驱动替换成STM32F1的bxCAN实现。我一般会单独建一个can_platform.c,只向协议栈暴露发送函数和一个接收回调,不把业务代码混进去。
移植的边界在于Timer和CAN的接入方式。CANfestival内部通过TimeDispatch()驱动所有周期性事务,这个函数必须在固定周期tick里被调用,典型值是10ms。如果你用STM32F1的TIM3做10ms中断,那这个中断里只做一件事:调用TimeDispatch()。不要再塞其他业务逻辑,否则协议栈的时序会漂移。
2.3 对象字典裁剪:OD大小直接决定Flash占用
对象字典是CANopen的核心数据结构。每个索引都有一组属性:类型、权限、保存标志和回调函数指针。CANfestival生成的OD本质上是一张大表,放到Flash还是RAM取决于定义方式。
源码包自带的示例OD通常包含完整的SDO服务器、心跳、错误寄存器等条目,条目多,编译后体积就大。对于STM32F1,特别是F103C8这种Flash只有64KB的型号,最常见的问题是编译链接时Flash溢出。我建议在od.c里只保留业务需要的对象:
- 通信区(1000h段)至少保留0x1000、0x1001、0x1005、0x1016、0x1017、0x1018;
- 制造商区(2000h段)按需增加变量,例如电压、温度、状态字,每个变量占1个OD条目;
- 不加LSS时,可以不编译lss.c,同时把OD里相关索引注释掉;
- 不需要动态修改波特率时,0x1006相关逻辑也可以删。
对象字典的存储布局直接影响地址计算。CANfestival的OD数组里每个条目包含指向数据的指针,所以数据本身放在哪里都行。常见错误是给数据和OD条目使用独立数组时长度不匹配,导致SDO读写时指针越界、总线复位或进入HardFault。调试时如果发现SDO读某个索引立刻卡死,先检查这个OD条目对应的数据变量是否真的存在,再看类型、长度与读写回调里的memcpy长度是否一致。
3. 在STM32F1系列单片机上移植CANfestival的操作步骤
这一章直接给操作序列。假设你手上已经有Keil MDK工程,MCU是STM32F103系列,时钟配置为72MHz,APB1为36MHz。
3.1 建立工程:把CANfestival源码加入stm32f1工程
新建一个Middleware/CANopen目录,把src和include整体拷入,然后在工程里添加以下源文件(按实际使用删减):
nmtSlave.c sdo.c pdo.c sync.c lifegrd.c emcy.c objacces.c timer.c只做从站时不需要添加nmtMaster.c;未启用LSS时不添加lss.c,否则会白白消耗Flash。把include目录加入编译头文件路径后,再定义两个编译宏:
#define CANOPEN_BIG_ENDIAN 0 #define NO_DEBUGCANOPEN_BIG_ENDIAN指定OD数据的字节序。STM32F1默认小端,置0即可。只有当你整条链路里有一个大端主站或PLC,且OD里保存了16位以上的数据时,才需要考虑字节序问题。NO_DEBUG用于关掉协议栈内部的调试打印,省掉重定向printf的麻烦。
3.2 编写bxCAN底层:canopen_canSend与接收中断
CANfestival要求平台提供两个点:发送函数和接收回调。发送函数一般是canopen_canSend,接收回调在CAN接收中断里调用。以下代码基于STM32标准外设库。
#include "data.h" #include "can_platform.h" #include "stm32f10x_can.h" unsigned char canopen_canSend(CAN_PORT notused, Message *m) { CanTxMsg TxMessage; uint8_t i; TxMessage.ExtId = m->cob_id; /* CANopen固定使用29位扩展帧 */ TxMessage.IDE = CAN_Id_Extended; TxMessage.RTR = m->rtr ? CAN_RTR_REMOTE : CAN_RTR_DATA; TxMessage.DLC = m->len; for (i = 0; i < m->len && i < 8; i++) TxMessage.Data[i] = m->data[i]; if (CAN_Transmit(CAN1, &TxMessage) != CAN_TxStatus_Ok) return 0; return 1; }m->cob_id必须来自协议栈内部的COB-ID计算,不要自己额外加偏移。例如TPDO1的默认COB-ID是0x180+nodeID,协议栈给出的Message结构体里已经是最终值,底层直接透传即可。
接收中断同样在can_platform.c里实现:
void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; Message msg; CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); msg.cob_id = RxMessage.ExtId; msg.len = RxMessage.DLC; msg.rtr = (RxMessage.RTR == CAN_RTR_REMOTE) ? 1 : 0; memcpy(msg.data, RxMessage.Data, msg.len); canopen_canReceive(&msg); }bxCAN的接收中断优先级不能和定时器中断同级。CANopen的SDO分段传输要求接收中断不能被长时间阻塞,否则主站会因超时重传,总线上出现大量错误帧。通常把CAN1_RX0中断设为高于业务定时器、低于系统节拍的优先级。
3.3 定时器与TimeDispatch:心跳和同步都靠它
CANfestival的lifegrd、SYNC、PDO定时器全部挂在TimeDispatch()上。用TIM3做一个10ms周期中断的写法如下:
void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM3, TIM_IT_Update); TimeDispatch(); /* 驱动CANfestival软件定时器 */ } }TimeDispatch每调用一次,协议栈内部所有定时器列表都会减少对应tick。若你的tick是10ms,那么0x1017心跳周期配置为100时,实际心跳间隔是1秒。10ms对大多数从站应用足够;如果SDO大块数据分段传输较多,建议把tick缩短到5ms,但CPU占用会上升。
3.4 初始化顺序与最小启动代码
初始化顺序有个易错点:必须先初始化协议栈再启动CAN通信,否则节点在未就绪时收到NMT帧会直接忽略。最小启动代码大致如下:
int main(void) { SystemClock_Config(); /* F1通常配置为72MHz,APB1=36MHz */ CAN_GPIO_Config(); /* CAN1_RX/CAN1_TX复用推挽 */ CAN_Config(); /* 波特率500kbit/s */ TIM3_Tick_Init(); /* 10ms周期中断 */ CANopen_Node_Init(); /* 初始化OD与状态机 */ setNodeId(0x05); /* 节点ID必须在进入NMT运行前设置 */ setState(Pre_operational); /* 上电默认进入预操作 */ __enable_irq(); while (1) { /* 应用层轮询,和协议栈运行互不阻塞 */ } }setNodeId的调用时机有讲究。CANfestival内部很多COB-ID都是根据nodeID在启动时计算的,如果在协议栈运行后改nodeID,已注册的PDO/SDO映射不会自动更新。上电后收到NMT Reset也要保持nodeID不变的前提下重新走一遍初始化。
CAN波特率配置用标准库举个例子:
CAN_InitStructure.CAN_Prescaler = 8; /* APB1=36MHz: 36M/(8*(1+6+2))=500k */ CAN_InitStructure.CAN_BS1 = CAN_BS1_6tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_Init(CAN1, &CAN_InitStructure);Prescaler取8、位时间合计9tq。如果总线链路距离长、分支多,把采样点前移会更稳。下表是APB1=36MHz时的常用配置:
| 目标波特率 | CAN_Prescaler | BS1(tq) | BS2(tq) | 采样点 |
|---|---|---|---|---|
| 1Mbit/s | 4 | 6 | 2 | 77.8% |
| 500kbit/s | 8 | 6 | 2 | 77.8% |
| 250kbit/s | 16 | 6 | 2 | 77.8% |
| 125kbit/s | 32 | 6 | 2 | 77.8% |
注意APB1不是36MHz时这套值全部失效。计算式是 f_APB1 / (Prescaler * (1 + BS1 + BS2)),改时钟前先按公式反推。
4. NMT、PDO与SDO的参数设置和常见坑位
4.1 NMT状态机:为什么从站一直停在预操作状态
CANopen从站上电默认进入Pre_operational。预操作状态下,SDO和心跳能工作,但PDO完全被禁用。这是CiA 301的明文规定,意图是防止设备配置未完成就向外输出过程数据。
调试时最常见的现象是:用USB-CAN工具能看到0x700+nodeID的心跳,但任何PDO都没有。原因往往是主站没有发NMT指令。NMT命令用CAN ID 0x000发送,两字节分别是命令和节点ID,进入操作状态的命令是0x01:
0x000: 01 05 (进入操作状态,节点5)NMT的0x000是广播ID,不支持按节点过滤。如果CAN滤波器的掩码设置只放行0x180+nodeID,那么NMT、SYNC、心跳都会丢失,这是新手最常踩的坑。
4.2 PDO映射的字节序和映射长度陷阱
PDO是CANopen里性能最好的数据通道。TPDO1的默认COB-ID是0x180+nodeID,RPDO1是0x200+nodeID。要真正传输数据,必须在OD的映射区配置好映射条目。TPDO1映射区是0x1A00,RPDO1是0x1600,每个映射条目是一个32位值:
bit31..16 = 映射对象索引 bit15..8 = 映射对象子索引 bit7..0 = 映射位长度例如把0x2000子索引01的16位数据映射到TPDO1,写入0x1A00子索引01,内容应为0x20000110。很多人的第一反应是用memcpy把一个uint32_t塞进SDO数据,结果主站读出来是颠倒的。原因是SDO数据域按字节传输,小端字节序下必须手动拼:
uint32_t mapValue = (0x2000u << 16) | (0x01u << 8) | 16u; uint8_t mapBytes[4]; mapBytes[0] = (uint8_t)(mapValue); mapBytes[1] = (uint8_t)(mapValue >> 8); mapBytes[2] = (uint8_t)(mapValue >> 16); mapBytes[3] = (uint8_t)(mapValue >> 24);映射位长度之和必须能被8整除,且总位数不能超过CAN帧的数据域。常见错误是映射三段8位加一段6位,共30位,协议栈在处理时丢弃不足一字节的位,主站解析时对不上。保持所有映射长度是8的倍数是最省事的做法。
4.3 SDO读写与超时参数
SDO承担配置类大块数据的上传下载,速度慢但可靠。读对象0x1000的快速SDO请求格式如下:
发送: 0x600+nodeID 40 00 10 00 00 00 00 00 接收: 0x580+nodeID 43 00 10 00 XX XX XX XX0x40表示读请求,后面依次是索引低字节、索引高字节、子索引和4字节保留。0x43表示读成功,后4字节是0x1000的值。如果节点回复0x4F,说明该索引在对象字典里不存在;回复0x80,说明子索引无效。两种情况都先回od.c确认条目是否被裁剪掉。
CANfestival对SDO超时的处理依赖底层定时器。如果主站发的分段SDO请求超过协议栈容忍时间,节点会回复中止传输码0x05040000(timeout)。这类问题常见于调试器挂起时CPU停止,TimeDispatch不再被调用。排查时先确认TIM3中断在调试暂停期间是否还执行。
SDO的字节序也值得单独说。CANopen标准规定SDO数据字段按小端传输,但很多PLC主站把16位数据按大端解释。如果总线上的值总是对不上,先不要怀疑CAN底层,先在SDO层确认0x1000的4字节值和OD变量的排列顺序一致。真要大端可以改CANOPEN_BIG_ENDIAN宏,但要在首次编译前定好,运行期改动无效。
提示:调试CANopen从站时,先不要怀疑硬件故障。绝大多数情况是NMT没进操作状态,或者SDO请求里索引字节写反了。
5. 用CAN报文收发验证CANopen移植是否成功
5.1 三秒抓帧法:只看心跳和0x1000就能确认协议栈活着
把USB-CAN工具接到总线上,滤波器设置为接收全部帧,观察上电瞬间。一个正常的从站节点要做的第一件事是发布心跳(按0x1017配置),所以三秒内看总线,应有以下内容:
- 初始化后短暂出现0x700+nodeID的帧,数据0x00;
- 进入预操作后同一个COB-ID的数据变成0x7F;
- 节点ID为5时,应看到ID为0x705、DLC=1的心跳帧;
- 主站发NMT 01 05后,该节点心跳数据变成0x05,说明进入操作状态。
这三点都满足,移植从协议栈角度已经成功。再发一次SDO读0x1000确认对象字典通路:发送ID 0x605、数据40 00 10 00 00 00 00 00,收到0x585且前四字节是43 00 10 00,说明OD读取路径也通了。
5.2 用GPIO翻转定位接收中断路径
如果SDO请求发出后没有任何响应,先确认接收中断有没有触发。常见做法是在CAN接收中断回调里翻转一个GPIO,用示波器看脉冲:
void USB_LP_CAN1_RX0_IRQHandler(void) { GPIOB->ODR ^= GPIO_PIN_12; /* 观察点:进中断即翻转 */ CanRxMsg RxMessage; Message msg; CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); ... }有脉冲但没响应,说明报文到了协议栈但处理失败,查OD条目和NMT状态;没脉冲,说明bxCAN的中断没进,或者硬件滤波器把帧丢了。
F103的bxCAN有28个滤波器组,如果初始化时把滤波器设为屏蔽位模式且掩码全为0,会放行所有帧。如果把掩码设置成只放行PDO的COB-ID,那么NMT的0x000、SYNC的0x080都会被丢掉。调试期把滤波器全部置为直通模式,产品阶段再按需收紧。
本文还有配套的精品资源,点击获取