ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CANfestival移植实战:STM32F1上实现CANopen对象字典与PDO/SDO调试

CANfestival移植实战:STM32F1上实现CANopen对象字典与PDO/SDO调试 简介基于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.cSDO快速/分段传输否src/pdo.cPDO收发与映射否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系列时钟配置为72MHzAPB1为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是0x180nodeID协议栈给出的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通常配置为72MHzAPB136MHz */ 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; /* APB136MHz: 36M/(8*(162))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。如果总线链路距离长、分支多把采样点前移会更稳。下表是APB136MHz时的常用配置目标波特率CAN_PrescalerBS1(tq)BS2(tq)采样点1Mbit/s46277.8%500kbit/s86277.8%250kbit/s166277.8%125kbit/s326277.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工具能看到0x700nodeID的心跳但任何PDO都没有。原因往往是主站没有发NMT指令。NMT命令用CAN ID 0x000发送两字节分别是命令和节点ID进入操作状态的命令是0x010x000: 01 05 进入操作状态节点5NMT的0x000是广播ID不支持按节点过滤。如果CAN滤波器的掩码设置只放行0x180nodeID那么NMT、SYNC、心跳都会丢失这是新手最常踩的坑。4.2 PDO映射的字节序和映射长度陷阱PDO是CANopen里性能最好的数据通道。TPDO1的默认COB-ID是0x180nodeIDRPDO1是0x200nodeID。要真正传输数据必须在OD的映射区配置好映射条目。TPDO1映射区是0x1A00RPDO1是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请求格式如下发送: 0x600nodeID 40 00 10 00 00 00 00 00 接收: 0x580nodeID 43 00 10 00 XX XX XX XX0x40表示读请求后面依次是索引低字节、索引高字节、子索引和4字节保留。0x43表示读成功后4字节是0x1000的值。如果节点回复0x4F说明该索引在对象字典里不存在回复0x80说明子索引无效。两种情况都先回od.c确认条目是否被裁剪掉。CANfestival对SDO超时的处理依赖底层定时器。如果主站发的分段SDO请求超过协议栈容忍时间节点会回复中止传输码0x05040000timeout。这类问题常见于调试器挂起时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配置所以三秒内看总线应有以下内容初始化后短暂出现0x700nodeID的帧数据0x00进入预操作后同一个COB-ID的数据变成0x7F节点ID为5时应看到ID为0x705、DLC1的心跳帧主站发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都会被丢掉。调试期把滤波器全部置为直通模式产品阶段再按需收紧。本文还有配套的精品资源点击获取
返回列表