ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从站协议栈Canfestival在STM32F103上的移植实战

从站协议栈Canfestival在STM32F103上的移植实战 简介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 DictionaryOD响应主站的各种请求并且按照预设的传输方式同步、异步、事件触发上报自己的状态和数据。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总线相关的协议逻辑。你需要做的就是提供两个底层的支撑一个是时基TimerCanfestival内部所有超时判断、心跳周期、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.cSDO服务、pdo.cPDO服务、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更常用的是PA11CAN_RX和PA12CAN_TX这组是默认的CAN1引脚不需要重映射适合大多数场景。硬件上必须注意CAN控制器输出的TX/RX是TTL电平不能直接连接到总线中间必须加CAN收发器芯片。常用的是TJA1050、SN65HVD230这些它们负责把TTL电平转换成CAN总线的差分信号。如果你的板子是从网上买的最小系统板通常需要自己外接一个CAN收发器模块。连线很简单STM32F103的PA12CAN_TX连接到收发器的TXD引脚。PA11CAN_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_1tqCAN_BS1 CAN_BS1_13tqCAN_BS2 CAN_BS2_2tqCAN_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 messageCOB-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 NodeIDPDO数据一直收不到映射未配置或触发方式错误检查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状态节点ID2的时候从站开始正常发送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网关的功能。这条路走通了很多现场问题都能迎刃而解。本文还有配套的精品资源点击获取
返回列表