简介:这是一份基于STM32平台的CANopen协议栈开源实现——Festival3.0(CanFestival),面向嵌入式开发者、自动化工程师及学习现场总线技术的爱好者,帮助在工业级项目中快速搭建符合CiA规范的CANopen通信节点。资源包共65个文件,以C源码和头文件为主,另有EDS对象字典、OD配置及Keil工程配置文件,压缩包仅228KB,结构清晰便于查阅。目前已有2103人学习。源码覆盖NMT网络管理、SDO服务、PDO实时传输和紧急报文等核心模块,并通过main.c中的示例程序展示初始化、主循环及中断处理的完整流程。学习者可按示例配置自己的对象字典与PDO映射,理解STM32 HAL库下CAN外设的驱动细节,并借助附带的工程模板快速移植到实际产品中,大幅缩短CANopen功能的开发周期。 一直关注工业通信这块的朋友应该对CANopen不陌生,这个协议在运动控制、医疗设备、特种车辆上用得极广。今天聊的是一个很实在的东西:把开源的CANopen协议栈Festival 3.0移植到STM32上。这套东西我前前后后折腾过几个项目,从最初的能跑通到后来真正敢用在产品上,踩了不少坑,也积累了一些可复现的经验。这篇不会去搬协议规范文档,就按实际操作顺序,把从拿到源码到设备能正常跑起来的关键环节捋一遍,希望对正准备在STM32上集成CANopen的朋友有点参考价值。
1. 为什么选CANopen以及为什么是Festival 3.0
1.1 CANopen在工业场景中的定位
先聊个基础问题:总线上有一堆传感器、驱动器、IO模块,到底怎么让它们配合工作?传统的RS485方案通常是自定义一帧报文格式,主站轮询从站,但这会导致一个很头疼的问题——每换一个设备供应商,就得重写一遍通信协议,而且总线利用率低,实时性也难保证。
CANopen本质上是在CAN物理层之上定义了一套标准化的对象字典和通信机制。每个设备节点都有唯一的Node ID,通过SDO进行对象字典的读写,通过PDO进行实时的过程数据传输,通过NMT管理节点状态机,通过心跳报文实现故障诊断。这套机制把“设备之间怎么通信”这件事标准化了,不同厂商的设备只要都支持CANopen,基本可以实现即插即用。
我之前做过一个多轴运动控制系统,原来用自定义协议,加一个新类型的驱动器,从通信层到应用层都要改代码。换成CANopen之后,新的驱动器只要配置好对象字典,主站侧几乎不用动。
1.2 Festival 3.0相比其他开源协议栈的差异化优势
工业通信领域的开源CANopen协议栈主要有几个选择,比如CanFestival、CANopenNode等等。Festival 3.0是目前我个人在用且比较推荐的一个版本,有几个比较具体的点:
首先是代码结构清晰,数据链路层驱动和应用层逻辑分离得比较彻底。这对于ST系列单片机移植来说是件好事,你只需要修改底层的CAN驱动接口,上层协议逻辑基本不用动。
其次是它对于对象字典的支持比较完备,涵盖了PDO、SDO、NMT、SYNC、EMCY等常用通信对象。在运动控制这种需要多节点高速交换数据的场景下,这些功能全部需要用到。
再有一点是它的许可证相对友好,可以应用到商业项目中而不必担心知识产权问题。
配置方式上,Festival 3.0支持通过EDS文件或直接代码定义对象字典。对于系统性比较强的项目,直接代码定义反而更灵活。有些工程师喜欢在电脑上用EDS编辑器把整个对象字典梳理好然后导入,而有些喜欢直接在C代码里修改结构体数组。我的经验是:项目初期用EDS方式快速搭建原型,进入量产阶段则切换到代码定义方式,方便统一版本管理。
2. STM32硬件基础与CAN通信前置条件
2.1 使用STM32的CAN外设需要准备什么
这套方案基于STM32的bxCAN外设。使用STM32时,需要的资源并不复杂:一个CAN控制器外设、一个CAN收发器芯片(比如TJA1050或者SN65HVD230)、一只120Ω终端电阻。
注意STM32的CAN外设时钟来源是APB1,在配置系统时钟时,要先确认APB1的时钟频率,然后根据想要的CAN波特率计算分频系数和时间段参数。我实际调试中经常碰到的问题就是:CAN波特率计算错误导致总线通信时好时坏,而且这种问题非常难以排查,因为看起来代码都是对的,示波器波形也正常。
以STM32F103为例,APB1是36MHz,如果目标是1Mbps波特率,那就要设置预分频器为4,时间段1为8个时间单元,时间段2为7个时间单元,同步跳转宽度为1。如果目标是500kbps,则预分频器改为8,时间段不变。这里有一个非常重要的概念:CAN总线的位时序要求采样点位置相对靠后(大约在75%~85%),否则在长线传输时抗干扰能力会下降很多。
/* CAN init structure */ CAN_InitTypeDef CAN_InitStructure; CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 = CAN_BS2_7tq; CAN_InitStructure.CAN_Prescaler = 4; // 36MHz / (1+8+7) / 4 = 1Mbps2.2 硬件层面的排查清单
在这里想多提醒一句,很多人上来就写代码,结果通信失败一大半原因是硬件问题。CAN总线调试时,有几点值得提前排查:
一是终端电阻必须加在物理总线两端。我之前在一个测试环境中只在主站端加了终端电阻,从站没加,测试距离约30米,结果误码率很高。后面在从站端也并联了终端电阻,问题立刻消失。120Ω终端电阻不是可选项,它是CAN物理层正常工作的重要条件。
二是CAN收发器的VREF引脚不要悬空。部分型号的收发器需要将VREF引脚连接一个电容到地,用于内部参考电压稳定。忘记接的话通信也会出现随机错误。
三是CAN_H和CAN_L不要接反。这个虽然看起来是低级错误,但在没有经验的新手项目中发生率还挺高。
完成以上硬件准备后,下一步就是整个工作最关键的部分:移植协议栈。
3. Festival 3.0源码结构与关键层解析
3.1 源码目录结构与模块职责
拿到Festival 3.0源码后,第一件事是理解它的目录结构。不要一上来就打开工程文件到处找main函数,它不是一个为特定MCU定制的工程,而是一个协议栈库,需要你自己组织和编写应用层代码。
源码核心目录包括:
- src:协议栈核心实现,包括CANopen协议对象字典管理、PDO/SDO通信逻辑、NMT状态机、紧急报文处理、同步报文处理等功能。
- include:核心头文件,定义了协议栈对外接口、数据类型、宏常量等。
- ports:移植层代码,针对不同硬件平台的底层接口实现。比如需要适配的对象字典函数、CAN驱动接口等。
- objdictgen:对象字典生成工具。用它能生成一个C文件,内部定义了该设备节点的全部对象字典信息。
用一层层剥的方式理解,这套协议栈最核心的结构体就是s_static_OD_Data_t,它保存了对象的索引、子索引、数据类型、读写属性、值指针等信息。应用层的操作对象就是通过索引和子索引来访问这些数据。
这里想强调一个关键点:千万不要试图去改写协议栈中的对象字典结构体,除非你确实理解它为什么会这样设计。之前见过有开发者为了让某个驱动器支持非标对象,直接改了协议栈内部的核心结构,导致SDO通信崩溃。正确做法是在对象字典配置阶段就要把所需对象规划清楚。
3.2 对象字典设计的工程方法
对象字典是CANopen设备的灵魂。简单来说,它就是设备内所有可被总线访问的数据的集合,通过16位索引和8位子索引来寻址。
在设计对象字典时,经验值是要提前规划好以下几类区域:
- 0x1000系列:设备类型、错误寄存器、厂商设备名、硬件版本、软件版本等。
- 0x1400-0x1BFF系列:接收PDO和发送PDO的通信参数、映射参数。
- 0x2000系列:厂商自定义对象区。
- 0x6000系列:通常用于过程数据对象。
在Festival 3.0中,对象字典是以一个结构体数组的形式存储的。每个条目描述了索引、子索引、访问类型、数据类型、值域范围等信息。这种方式优点是可读性好、修改灵活,缺点是占用Flash空间相对较大,但以STM32的容量来说绰绰有余。
我通常的做法是先把设备需要的所有参数列一个Excel表格,分类、索引、子索引、数据类型、读写属性、默认值、单位都标注清楚,然后按照这个表格来配置对象字典。这样可以避免在项目后期参数遗漏时再来改对象字典结构,那会对整个工程造成比较大的影响。
3.3 底层CAN驱动适配层设计
协议栈的硬件抽象层需要实现一批接口函数,简单来说就是以下几个关键函数:
- canSend:将CAN报文发送出去。
- canReceive:从CAN接收缓冲区读取报文。
- canInit:初始化CAN控制器外设和中断。
在STM32上实现canSend的核心逻辑是填充CAN_TxMsg结构体,然后调用CAN_Transmit函数。
unsigned char canSend(CAN_PORT notused, Message *m) { CanTxMsg TxMessage; TxMessage.StdId = m->cob_id; TxMessage.IDE = CAN_Id_Standard; TxMessage.RTR = CAN_RTR_Data; TxMessage.DLC = m->len; memcpy(TxMessage.Data, m->data, m->len); return CAN_Transmit(CAN1, &TxMessage) == CAN_TxStatus_Failed ? 0 : 1; }这里要注意,在实际产品中需要对发送缓冲区进行管理。如果上位机频繁发送同步帧,而底层CAN发送需要等待发送完成,我们就会在canSend里面加一个超时或者重传机制。
接收方向也要注意中断优先级配置。CAN接收中断必须设置为较高优先级,否则如果低优先级任务大量占用CPU时间,CAN接收中断被频繁延迟,会出现覆盖的情况。
void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMessage; Message m; CAN_Receive(CAN1, CAN_FIFO0, &RxMessage); /* 将接收的数据拷贝到协议栈可以访问的结构体中 */ m.cob_id = RxMessage.StdId; m.len = RxMessage.DLC; memcpy(m.data, RxMessage.Data, m.len); canDispatch(&m); }4. 实操移植步骤
4.1 从搭建工程到完成协议栈集成的五个步骤
下面把整个移植过程拆成五个步骤,每一步都给出可复制可执行的操作建议。
第一步:准备基础工程
直接使用STM32CubeMX生成一个基础工程,配置好时钟树、GPIO、USART串口和CAN外设。注意CAN的GPIO引脚需要配置为复用推挽输出,同时使能AFIO时钟。
第二步:将Festival源码加入工程
将Protocol源码目录复制到工程目录下,在Keil或者GCC工程中添加对应源文件。注意头文件的引用路径设置,建议将src和include目录都添加到头文件搜索路径中。
第三步:配置STM32的CAN外设
优先在main函数中先做CAN外设初始化,然后将底层CAN驱动封装成协议栈要求的接口函数。这里的重点是理解canDispatch函数:协议栈内部通过这个调度函数来处理接收到的报文,你需要将CAN接收中断中的报文一步步转化为Message结构体,然后调用canDispatch。
第四步:配置对象字典
这一步需要根据你的设备需求来定制。比如做一个CANopen转串口的网关设备,你需要定义设备名称、设备类型、错误寄存器、心跳生产周期,还要配置几个自定义对象区用于缓存串口数据。
第五步:编写初始化代码和应用代码
在main函数中调用协议栈的初始化函数,启动心跳发送任务,然后在主循环或RTOS任务中周期性地调用协议栈的定时处理函数。
4.2 一个最小可运行示例的初始化代码
以下是初始化流程的一个简化示例。注意其中要注意的是初始化顺序不能颠倒。
/* 初始化CAN硬件 */ CAN_Config(); /* 初始化协议栈对象字典 */ initNodes(); /* 初始化节点状态 */ setNodeId(&master_objdict, 0x01); setState(&master_objdict, Initialisation); /* 启动协议栈心跳和节点管理 */ startNode(&master_objdict); setState(&master_objdict, Operational);在实际项目中,从Initialisation到Operational的状态切换必须符合CANopen规范。不要在节点没有完成初始化配置就直接切换到Operational状态。比如一个带编码器的伺服驱动器,必须在Operational之前把编码器参数、PID系数、回零参数全部配置完成,否则电机上的动作模式会出现不可预知的异常。
4.3 如何让对象字典动态扩展
Festival 3.0的一大特色就是支持运行时动态扩展对象字典条目。比如在Modbus转CANopen网关中,不同的Modbus寄存器地址可能需要对应不同的对象字典条目。
它的实现方式是在对象字典结构体数组中预留一个扩展区段,然后通过调用相应的函数动态地添加条目。
// 动态添加一个对象字典条目 UNS32 localDict[] = { 0x2000, // 索引 0x00, // 子索引 UNS32_TYPE, // 数据类型 ... };不过要提醒大家,动态扩展对象字典一般是有性能和内存开销的,对于时序要求严格的PDO数据推荐使用静态定义方式,动态扩展可以放在配置参数区或者诊断数据区。
5. 应用层调试、边缘案例及优化思路
5.1 使用CAN分析仪+P python来调试
当协议栈移植完成进入联调阶段时,最简单高效的方式是用CAN分析仪配合Python的python-can库快速验证协议栈工作状态。因为你能看到总线上具体的报文内容,对比协议规范就可以快速定位是发送方还是接收方的问题。
用Python发送一个SDO报文读取0x1000设备类型:
import can bus = can.interface.Bus(bustype='canalystii', channel=0, bitrate=1000000) # 发送SDO读取0x1000 msg = can.Message(arbitration_id=0x601, data=[0x40, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00], is_extended_id=False) bus.send(msg) response = bus.recv(timeout=1) print(response.data)关于python-can,老版本的import方式是import can,某些新版本是import canopen,这是两个不同的库。如果执行时遇到名称冲突,建议统一使用import canopen这种库,它自带对象字典解析能力,可以方便地把总线上另一个节点的数据读到本地DataFrame里。
5.2 Framework层优化思路——PDO映射与心跳
在体育运动的实时性要求场景里,PDO是核心。一个驱动器的PDO1在出厂时可能默认映射的是状态字和控制字,如果你的应用需要在1ms内周期性地发送目标位置、目标速度、实际位置、实际速度这四个数据,而PDO1只有8字节的长度怎么办?需要进行映射优化。
映射的方法是定义PDO的映射参数对象,比如0x1A00子索引1映射到0x6040(控制字),子索引2映射到0x607A(目标位置)。在Festival 3.0中,你只需要将对象字典中对应PDO映射的条目修改为相应的索引和子索引,并在设备重启后发送LSS请求使从站重新加载对象字典。
另一个需要关注的是心跳报文。在设备断线检测中,心跳它是最关键的一环。最稳妥的做法是设置比超时时间多一倍安全余量的心跳周期。因为CANopen主站中一般有按超时时间判定断线的逻辑,如果心跳周期设置成1000ms,超时时间也恰好是1000ms,那么总线上某瞬间稍微有点延迟就导致主站误判断线。
5.3 性能分析与代码级优化
Festival 3.0在低端STM32F103上跑1Mbps总线还会有不少余量。但有一个细节值得注意:canDispatch函数在接收中断中被调用,如果在中断里做大量对象字典访问操作,中断响应时间会变长,这对系统实时性有很大影响。
优化方案有两种:
方案A:接收中断中仅做数据拷贝,将消息放入环形缓冲区,然后在主循环中调用canDispatch处理。这种方法实现简单,但需要处理缓冲区满的边界条件。
方案B:接收中断中直接调用canDispatch,但将协议栈的定时处理函数Timer轮询周期调短,确保数据尽快被处理。这个方法响应实时性好,但需要接受中断代码执行时间较长的事实。
我实际测试过两种方案,在500kbps波特率、CAN中断频率较高的场景下,用方案A加双缓冲区可以保持更稳定的接收时序。如果你项目中CAN通信负担比较重,可以考虑方案A。
6. 常见问题排查与工程避坑
6.1 通信失败的经典问题
下面这个表格是实际项目中比较常见的几类问题,建议直接保存下来排查时对照使用。
| 现象 | 原因 | 解决方式 |
|---|---|---|
| 总线完全无数据 | 终端电阻丢失 | 总线两端各并联120Ω电阻 |
| 波特率错误 | 分频系数或时间段计算错误 | 用示波器对比测试波特率;用CAN分析仪重新校准 |
| 偶发通信错误导致总线恢复 | 采样点位置偏移 | 调整BS1/BS2比例至采样点约80%位置 |
| 冷启动后协议栈无响应 | 初始化顺序不对 | 先初始化硬件,再初始化协议栈,最后启动节点 |
| 发送失败 | 发送缓冲区满 | 对发送函数加超时机制;提高发送中断优先级 |
| 一上电节点就进入Bus-Off | CAN_H和CAN_L接反 | 用万用表或示波器检查差分信号 |
| 节点报错但不断线 | 无心跳报文或心跳周期过大 | 配置心跳报文周期并检查心跳发送定时处理 |
6.2 经验总结与持续优化建议
最后分享几个比较个人化的经验。移植协议栈这东西,看起来是写代码,但真正决定项目成败的其实是前期的对象字典规划和后期的稳定性测试。
第一,对象字典的地址规划一定要从全局考虑,特别是那些需要多个产品线共用的设备。你可以先做一个统一的变量命名手册,把每一个Index、每一个SubIndex对应的物理量、读写权限、数据类型、缩放系数都定义清楚,后续哪怕是换人维护这个项目也不会断层。
第二,建议在开发阶段就引入自动化测试。在CI环境中跑Python脚本模拟主站,自动发送SDO遍历读走设备的所有参数,这样每次代码变更后都能快速回归。很多问题不是功能逻辑问题,而是修改A模块导致B模块的偏移量变了。
第三,如果你想在这个方案基础上不断扩展功能,建议将协议栈的版本用Git子模块方式管理,不要直接拷贝源码到工程内。否则升级协议栈时,你会发现自己早就改得面目全非,升级变成重写。
总的来说,Festival 3.0在STM32上的移植并没有想象中那么神秘。它本质就是一个标准化的通信协议库,只要把底层CAN驱动适配好,把对象字典设计清楚,剩下的就是在应用逻辑里把数据映射到正确的位置。按照上面这几个步骤走下来,你的设备应该能正常地在CANopen总线上跑起来了。如果过程中遇到其他问题,也可以拿着你的报错截图和数据手册一起再讨论。
本文还有配套的精品资源,点击获取