简介:CANopen在STM32F103上的从机移植源码,是一套面向工业自动化与嵌入式开发者的从机节点实现方案,解决CANopen协议栈从零移植、对象字典配置、SDO/PDO通信联调等问题。压缩包共244个文件、3.59MB,核心代码由62个头文件与49个C源文件构成,覆盖CAN模块波特率与滤波器配置、NMT网络管理、SDO服务端、PDO映射、心跳报文、错误处理等关键环节,同时附带Keil工程文件、备份工程、AXF/MAP链接信息及编译产物,可直接在MDK中打开并对照学习;内容预览提到的sdo.c、stm32f10x_tim.c等源文件,恰好对应对象字典服务与定时器中断实现,便于定位功能模块。源码角色为CANopen从机,可配合主站完成节点启动、参数配置、周期性/非周期性数据交换和在线状态监控,移植思路清晰。已有935人学习下载,尤其适合正在做从机移植、协议栈裁剪或现场调试的开发者,作为代码范例与排错参考;初学者也可借此理解从机在上电、预操作、运行等状态间的切换及对象字典在通信中的桥梁作用。 CANopen这个协议栈,搞嵌入式的小伙伴应该不陌生。工业现场设备联网、伺服控制、传感器采集,到处都有它的身影。我这次要分享的,就是把CANopen从站协议栈——Canfestival(圈里人俗称festival)移植到STM32F103上的完整过程。这块板子大家都熟,资源够用又便宜,拿来跑从站协议刚刚好。准备这篇文章之前,我在网上翻了翻相关热词,发现不少人在问“CANOPEN festival”、“STM32F103移植”、“从机”这些话题,今天就把我之前做过的一个方案从头到尾捋一遍。
无论你是刚接触CANopen的小白,还是已经写过几个设备节点的老手,这篇博文都会给你一些可以参考的东西。我这个人写东西不喜欢绕弯子,直接说结论:Canfestival + STM32F103 + 从机节点,这套组合做下来,成本低、见效快,而且网上开源的参考代码非常多,踩坑也基本都有前人趟过了。
1. 项目整体认知:这不是一个简单的协议栈搬运
先花点时间把这件事的本质讲透。很多人一上来就开始下载源码、建工程、改引脚,折腾半天发现连心跳报文都发不出去。问题的根源在于,你没有弄清楚CANopen从站在系统中到底扮演什么角色。
CANopen是基于CAN总线的应用层协议,从站节点的定位就是被主站管理和调度的设备。主站负责网络管理(NMT)、参数配置(SDO)和过程数据交换(PDO)。从站的职责是维护自己的对象字典(Object Dictionary,OD),响应主站的各种请求,并且按照预设的传输方式(同步、异步、事件触发)上报自己的状态和数据。
Canfestival这个名字经常被简写成festival,它是一个开源免费的CANopen协议栈实现,支持从站和主站功能。它最核心的价值,不是帮你把CAN收发器的电平信号变成数据帧,而是把协议栈的复杂逻辑,包括NMT状态机、心跳、节点守护、SDO服务、PDO映射、SYNC同步、紧急报文(EMCY),全部封装成一套可调用的API。你要做的,就是把它和设备底层的CAN驱动以及一个时基定时器对接起来。
STM32F103的优势在于,它有bxCAN外设,基本就是为CAN应用量身定做的,支持标准帧和扩展帧,硬件滤波也不用额外占用CPU资源。配合标准外设库(比如经典的V3.5版本),写CAN驱动也不算费劲。所以这个项目真正的难点,不在于CAN驱动本身,而在于怎么把Canfestival的状态机和你的硬件正确“粘”在一起。
1.1 从站节点的整体框架梳理
我先画一个虚拟的层次图,帮助大家理解整个项目的结构。
最底层是STM32F103的硬件,包括CAN收发器芯片(比如TJA1050)和微控制器。往上一层是CAN外设驱动,负责收发报文、处理中断、配置滤波器。再往上就是Canfestival协议栈,它内部包含了对象字典、SDO服务、PDO服务、NMT状态机这些模块。最顶层,才是你自己的应用代码,比如采集传感器的数据、控制电机、读取开关状态。
这个架构意味着,你往Canfestival里填充数据是和协议栈交互的过程,它替你管理好所有和CAN总线相关的协议逻辑。你需要做的,就是提供两个底层的支撑:一个是时基(Timer),Canfestival内部所有超时判断、心跳周期、PDO事件周期,都依赖于这个时钟源;另一个是CAN收发接口,Canfestival需要把你的对象字典数据封装成CAN报文发出去,同时从CAN总线把收到的报文喂给协议栈。
1.2 为什么选择Canfestival而不是其他协议栈
市面上CANopen从站协议栈不少,有商业收费的,也有开源的。商业协议栈比如CANopen Stack、emCAN,稳定性好,技术支持到位,但价格不菲,而且源码往往是库文件的形式,出了问题不好定位。开源的除了Canfestival,还有些轻量级的实现,但论功能完整度、社区活跃度、资料丰富度,Canfestival应该说是首选。
Canfestival经过了很长时间的迭代,虽然代码风格偏老,但逻辑是严谨的,很多工业产品就是把Canfestival经过大量测试后跑在了自己的板子上。另外一个好处是,Canfestival附带一个叫Objdictedit的GUI工具,可以用可视化的方式编辑对象字典,自动生成C源码,这对于管理大量映射对象来说非常方便。
选型的时候还有一个隐形成本需要考虑,就是学习成本。Canfestival的概念比较多,刚上手会觉得繁琐,但它做得好的地方在于,所有协议相关的状态转换都有固定的入口,你不需要完全搞懂每一行协议代码,就能先跑起来一个能收发数据的节点。这比从零开始自己写一个CANopen从站要节省大量时间,更适合做产品原型或者项目预研。
2. 移植前的准备工作:源码、硬件和工具链
2.1 源码版本和目录结构解读
Canfestival的源码可以从SourceForge或者GitHub上找到,版本上我自己用过3.x版本,网上流传比较多的是CanFestival-3-10。下载下来之后,你会看到几个核心目录:
src:协议栈的完整源代码,包含了canfestival.c(核心初始化)、objacces.c(对象字典访问)、sdo.c(SDO服务)、pdo.c(PDO服务)、nmt.c(网络管理)、lss.c(层设置服务)等。include:对应的头文件。drivers:官方提供的一些硬件驱动模板,比如针对不同MCU的CAN驱动示例,里面有can_std.c这类文件可以参考。examples:官方示例,里面有很多针对不同硬件平台的工程模板。objdictgen:独立的Python工具,用来生成对象字典的C语言描述文件。
移植的时候,你真正要改动的地方很少。核心的协议栈代码基本不用动,你只需要新增一个针对STM32F103的驱动接口文件,并且把对象字典配置文件(通常叫TestSlave.c和TestSlave.h)放进工程。
2.2 硬件连接和最小系统确认
STM32F103的CAN外设使用PB8和PB9引脚时,需要重映射到CAN1;更常用的是PA11(CAN_RX)和PA12(CAN_TX),这组是默认的CAN1引脚,不需要重映射,适合大多数场景。
硬件上必须注意,CAN控制器输出的TX/RX是TTL电平,不能直接连接到总线,中间必须加CAN收发器芯片。常用的是TJA1050、SN65HVD230这些,它们负责把TTL电平转换成CAN总线的差分信号。如果你的板子是从网上买的最小系统板,通常需要自己外接一个CAN收发器模块。连线很简单:
- STM32F103的PA12(CAN_TX)连接到收发器的TXD引脚。
- PA11(CAN_RX)连接到收发器的RXD引脚。
- 收发器的CANH和CANL分别连接到总线上的CAN_H和CAN_L。
- 总线两端要各接一个120欧姆的终端电阻,这是CAN通信稳定的基础。如果只有两个节点,两端各接一个;如果节点多,只要保证总线两端有有终端电阻即可。
2.3 工程搭建和文件添加的注意点
我建议在STM32标准外设库V3.5的基础上搭建工程。新建一个项目文件夹,把Canfestival的src目录下的协议栈源码全部添加进来,注意排除掉main.c和测试相关的文件,只保留协议栈本体。
然后新建你自己的驱动文件,比如canfestival_driver.c和canfestival_driver.h,用来实现Canfestival和STM32F103之间的桥梁。具体需要实现哪几个函数,后面我会详细说。再把Objdictedit生成的对象字典文件ObjectDictionary.c和ObjectDictionary.h拷进工程,这样编译就能通过了。
一个容易踩的坑是编译器设置。Canfestival的源码比较老,有些地方用到了旧的C语法,比如隐式函数声明在严格编译模式下会报错。我在用Keil MDK编译时,是把C99模式关掉的,个别警告直接忽略,确保能正常编过。如果你用IAR或者GCC,也可能需要调整一下优化等级,我遇到过-O2下协议栈定时器回调不稳定的现象,后来换成了-O1就正常了。
3. 核心细节:Canfestival移植的关键步骤
3.1 时间基座的实现
CANopen协议栈非常依赖时间。心跳报文的周期、节点守护的超时、SDO块传输的超时、PDO事件定时器的触发,全部靠一个Tick来驱动。Canfestival在源码里抽象了一个定时器接口,需要你提供两个基础函数:
void setTimer(TIMER_HANDLE handle, uint32_t value):这个函数用于设置一个定时器,当到达value毫秒时,Canfestival希望得到通知。TIMER_HANDLE getElapsedTime(void):这个函数返回从上次设置定时器到现在已经过去了多少毫秒。
实际上,Canfestival内部会对这两个函数做封装,你只需要定时调用它的TimeDispatch函数来检查是否有定时器超时。
我的做法是使用STM32F103的SysTick定时器,配置成1毫秒中断一次。在中断服务函数里,维护一个全局的毫秒计数变量,并调用Canfestival的TimeDispatch()。这里有一个细节:TimeDispatch不能被1毫秒中断频繁调用导致主循环卡死,所以我在中断里只置了一个标志位,在主循环里再判断这个标志并调用TimeDispatch,避免在中断上下文中执行过于复杂逻辑。
具体代码如下,这里给出核心部分:
static volatile uint32_t g_ulMsCounter = 0; void SysTick_Handler(void) { g_ulMsCounter++; } uint32_t getElapsedTime(void) { return g_ulMsCounter; } void setTimer(TIMER_HANDLE handle, uint32_t value) { // 这里需要把value值保存起来,用于getElapsedTime计算 }更简便的方法是直接使用Canfestival提供的宏,内部会调用你实现的这两个API。需要注意的是,getElapsedTime返回值不能溢出,uint32_t在系统连续运行49天左右会翻转,这对大多数应用来说足够了。
3.2 CAN驱动对接
CAN驱动是另一个必须要实现的关键接口。Canfestival在协议栈上层调用canSend这个函数,把要发送的CAN报文交给底层驱动。你需要在驱动文件里实现一个类似下面的函数:
unsigned char canSend(CAN_PORT notused, Message *m) { CanTxMsg TxMessage; TxMessage.StdId = m->cob_id; // 标准帧ID TxMessage.ExtId = 0; TxMessage.IDE = CAN_Id_Standard; TxMessage.RTR = (m->rtr == 0) ? CAN_RTR_Data : CAN_RTR_Remote; TxMessage.DLC = m->len; memcpy(TxMessage.Data, m->data, m->len); // 调用标准库函数发送 if (CAN_Transmit(CAN1, &TxMessage) != CAN_TxStatus_Failed) { return 1; } return 0; }接收端,在CAN接收中断中把收到的报文帧封装成Canfestival的Message结构体,然后调用canDispatch(&canOpen_objdict_Data, &message)来分发报文。这条分发链就是协议栈的生命线,所有从总线上来的请求都会通过这个函数进入状态机。
驱动文件还有初始化函数,比如配置CAN波特率、滤波器、中断优先级。波特率这块需要特别注意,CANopen协议默认通讯波特率是250kbps或1Mbps,看你主站的设置。STM32F103的bxCAN波特率计算公式是:BRP分频器的值 = 外设时钟 / ( (1 + BS1 + BS2) * 波特率 )。就以APB1外设时钟36MHz,目标波特率250kbps为例,设置CAN_SJW = CAN_SJW_1tq,CAN_BS1 = CAN_BS1_13tq,CAN_BS2 = CAN_BS2_2tq,CAN_Prescaler = 9,算下来差不多就是250k。如果你发现通信不稳定,除了查总线连接,优先检查这三组参数是否和主站配置一致。
3.3 对象字典的生成和加入工程
对象字典是整个CANopen从站的灵魂,它定义了这个设备有哪些对象、每个对象是只读还是可读写、对应的数据类型是什么。用Canfestival自带的Objdictedit工具可以图形化编辑这些内容。
具体操作是:打开Objdictedit,新建或者打开它自带的模板(比如DS301标准从站模板),然后添加你的自定义对象。每个对象都有一个16位的索引(Index)和一个8位的子索引(SubIndex)。比如你需要在对象字典里增加一个存放温度值的对象,给它分配索引0x2001,子索引0x00,类型是UNSIGNED16。保存后,Objdictedit会生成对应的ObjectDictionary.c和ObjectDictionary.h。
你不需要看懂每一个数组元素的意思,但要能定位关键的几个位置。生成的文件中,最重要的数组是OD_Objdict_TestSlave,它把所有对象按索引排列起来,Canfestival在启动时通过这个数组来实例化对象字典。你只需把生成的文件加入工程,然后在slave_ObjectDictionary.c里调用初始化和加载函数即可。
4. 从机功能的实操实现与演示
4.1 节点初始化流程
一旦协议栈和驱动准备就绪,从机节点的初始化就有章法了。我在main函数里做的操作顺序,直接影响后续能否正常上线。
第一步,初始化硬件时钟、SysTick定时器、CAN外设GPIO、CAN外设本身和中断。确保CAN外设已经进入正常模式。
第二步,调用initTimer()函数,把Canfestival的定时器系统跑起来。
第三步,设置节点ID和波特率。这里需要修改两个地方:一个是对象字典中关于设备标识的对象值;另一个是Canfestival的节点初始化函数,通常涉及一个宏定义SDO_LIS或者调用setNodeId之类的函数。比较直接的做法是在TestSlave.c的TestSlave_Init中,把nodeId赋值成你期望的从站地址,比如2、3、4或者更高的,范围在1到127之间。
第四步,调用协议栈的初始化函数initCANopen、_TestSlave_Initialisation(具体名字看你生成的对象字典工程),加载对象字典并完成协议栈的就绪。
unsigned char nodeId = 0x02; // 本节点Node ID canopen_node_t *node = NULL; node = CANopen_Init(nodeId, &canOpen_objdict_Data);第五步,进入主循环。主循环里做两件事:一是调用canopen_loop之类的处理函数,让协议栈能及时处理接收和发送的报文;二是执行你自己的应用逻辑,比如读取传感器值,然后更新到对象字典对应条目。
4.2 心跳报文和上线过程
CANopen从站想要让主站知道自己在总线上活着,最常用的方式是发送心跳报文(Heartbeat)。这个报文的数据长度固定为1个字节,内容是从站的当前状态值(如Operational状态对应0x05)。心跳报文的COB-ID是0x700 + NodeID,这算是CANopen协议的基础常识。
Canfestival里启用心跳很简单,只需要在对象字典里配置好心跳生产周期(通常索引0x1017,类型为UNSIGNED16),并设置心跳消费者周期(索引0x1016,类型为UNSIGNED32)可以暂时不配,因为这是主站那边管的事情。配置完成后,协议栈会按照你设定的周期,自动通过canSend发送心跳帧。
我在实际测试中发现一个坑:如果你配置的心跳周期是1000ms,但底层CAN发送失败导致连续几个心跳都没发出去,主站那边可能会认为你掉线,从而让整个网络进入错误状态。所以底层的驱动发送函数务必加上重试或者缓冲机制,提高发送成功率。
4.3 PDO和SDO通信的代码级演示
PDO用于实时过程数据的传输,特点是速度快、数据量大,但不带确认机制。SDO用于参数配置,特点是可靠,基于请求-响应模式。
先看PDO。典型的数据发送流程是:你更新对象字典中映射到TPDO(发送PDO)的数据,然后触发PDO发送。比如说你定义了一个TPDO1,映射对象0x2001和0x2002,传输类型为异步。那么当你写了一个新数据到0x2001时,需要调用类似下面的代码:
UNS16 value = getSensorValue(); writeLocalDict(&canOpen_objdict_Data, 0x2001, 0, &value, sizeof(value)); SendPDOevent(&canOpen_objdict_Data, 1); // 触发TPDO1发送PDO的COB-ID是协议中规划好的,比如TPDO1的COB-ID一般是0x180 + NodeID。主站配置成接收这个ID就能拿到数据。
再看SDO。SDO通信分为上传(从站发数据给主站)和下载(主站写数据到从站)。这部分在Canfestival里几乎不需要你手动写,因为协议栈已经把SDO服务端的逻辑实现完了。你只需要实现一个TestSlave_OD_Read和TestSlave_OD_Write之类的回调函数,这两个函数在对象字典被读取或写入时被调用,适合做额外的处理,比如写了一个配置参数后,马上应用到硬件。
举个例子,你在对象字典里定义了一个索引0x2100表示“LED闪烁周期”。当主站通过SDO往这个地址写值时,Canfestival会更新对象字典,如果你在写回调里做了额外的操作,就能立刻改变实际LED的闪烁频率。这在实际项目中非常实用。
ODCallbackReturn_t TestSlave_OD_Write(CO_Data_t *d, const indextable *idx, UNS8 bSubindex) { if (idx->index == 0x2100) { // 读取新值,应用到硬件 UNS32 newVal = *(UNS32 *)idx->pData; Motor_SetSpeed(newVal); } return OD_SUCCESSFUL; }注意这里的回调只是其中一种实现方式,协议栈的具体接口默认是ODCallbackReturn_t writeLocalDict等,实际名称要看你的版本。但思路是共通的。
5. 调试工具与常见问题排查实录
5.1 用什么工具观察CANopen通信
调试CANopen最理想的是用专门的CANopen调试上位机工具,比如PCAN系列配PCAN-View,或者USBCAN分析仪配上第三方上位机软件。你不需要太高级的设备,只要能正常收发CAN报文,能看COB-ID和数据的工具就行。
我调试时用的是USBCAN-I搭配一个开源的CAN调试助手,重点观察三类报文:
- 看有没有节点上线报文(Boot-up message,COB-ID为
0x700 + NodeID,数据为0x00)。 - 看心跳报文是否周期稳定。
- 看PDO报文是否能在数据更新时正常发出。
如果看不到Boot-up报文,那问题基本出在协议栈初始化没完成,或者对象字典损坏导致初始化卡住。这种情况下我推荐加串口打印,在关键初始化函数和CAN发送函数里加打印信息,快速定位卡死在哪一步。
5.2 常见问题速查表
我把这几年的踩坑整理成一张表,大家可以对照着排查:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 上电后没有任何CAN报文输出 | 定时器未初始化,或者CAN发送函数配置了错误引脚 | 检查SysTick是否启动,用示波器看CAN_TX引脚是否有波形 |
| 能发Boot-up,但心跳不规律 | 心跳周期配置错误,或者getElapsedTime返回值异常 | 检查对象字典0x1017是否配置正确;在TTL中打印getElapsedTime返回值 |
| 主站能发现从站,但SDO读写超时 | SDO配置有误,或者对象字典索引未匹配 | 用CAN分析仪确认SDO请求ID是否为主站发出的0x600 + NodeID,响应ID是否为0x580 + NodeID |
| PDO数据一直收不到 | 映射未配置或触发方式错误 | 检查TPDO映射的对象字典索引;确认传输类型是否为同步或者事件触发 |
| 波特率通信不上 | BRP、BS1、BS2配置不当 | 用示波器测试CAN_TX波形的位时间,计算实际波特率 |
5.3 几个独家避坑心得
首先,对象字典的位宽和数据对齐是个大坑。Canfestival使用了UNS32、UNS16这些自定义类型,如果你在主控端用标准uint32_t直接拷贝,短数据类型的内存布局可能对不上,会导致读写值变成乱的。建议所有协议栈的类型都走它自己的定义,不要混用。
其次,CAN接收缓冲区要设置足够深。STM32F103的bxCAN有3个发送邮箱和多个接收FIFO,如果应用层处理不及时,接收报文会被丢弃。在中断里收到报文后,建议立刻拷贝到自己的环形缓冲区,再由主循环处理。
第三,关于复位后能不能自动上线。STM32F103复位后,CAN外设的寄存器值是默认状态,如果你在初始化时忘了重新配置CAN外设,可能总线上两个节点都通不了。我习惯在协议栈初始化前,先执行一次CAN_DeInit(CAN1),确保外设回到一个明确的初始状态。
6. 更深一步:主站的交互与状态控制
很多时候大家做完从站,发现能发心跳了,但不知道怎么和主站完整交互。这里补充一些实操层面的信息,方便你快速搭建一个测试环境。
如果你没有现成的CANopen主站设备,可以在PC上用上位机软件模拟,比如把USBCAN接到电脑,然后跑一个支持CANopen主站的上位机。这个上位机主要做三件事:发送NMT命令、SDO读写、接收PDO数据。你不需要自己开发主站代码,用现成的调试工具就能验证从站的大部分功能。
常见的NMT命令帧ID是0x000,数据第一个字节是命令字,第二个字节是节点ID。比较实用的几个命令:
0x01 0x02:进入Operational状态(节点ID=2的时候),从站开始正常发送PDO。0x02 0x02:进入Stop状态,从站停止PDO,但心跳仍继续。0x80 0x02:进入Pre-operational状态,允许SDO访问,不发送PDO。
调试SDO的时候,建议先用SDO读一下对象字典的0x1000(设备类型)或者0x1018(设备标识),如果能正常返回数据,说明SDO通道是通的。
有一次我调试一个从站,主站上位机始终显示节点不在线,把波特率、节点ID查了个遍都没发现问题。后来用CAN分析仪抓包,发现报文里确实有Boot-up,但ID是0x702而不是我配置的0x701。原因是在对象字典生成的时候,Node ID的默认值和我在初始化代码里设置的没有同步,导致协议栈内部用的ID和对象字典里保存的不一致。解决的方法就是确保初始化代码里传入的nodeId和对象字典中相关设备的标识保持一致。
7. 移植过程中的个人体会
说实话,CANopen从站移植,技术壁垒没有想象中那么高,但它考验的是你对整个系统架构的理解能力和调试的耐心。我一开始也想着能不能不读协议文档直接开干,结果每次都被各种各样奇怪的超时问题折磨到深夜。后来沉下心来,把对象字典、NMT状态机、心跳机制这几个概念彻底吃透,再回头去看Canfestival的源码,很多看不懂的地方就自然通了。
最后再分享一个小经验,就是在上位机里做全流程的联调的时候,一定要先从单节点开始,不要一上来就挂多个节点。CANopen网络的排错顺序是:硬件链路、波特率一致性、节点ID冲突、对象字典映射、应用逻辑。按照这个顺序排查,能省下不少无用功。这个方案后续如果想扩展,可以在同一个STM32F103上把主站功能也加进来,或者将Canfestival移植到其他更强大的MCU上,让节点具备EtherCAT转CANopen网关的功能。这条路走通了,很多现场问题都能迎刃而解。
本文还有配套的精品资源,点击获取