基于CanFestival和STM32的CANopen电机控制实现
2026/9/10 2:24:10 网站建设 项目流程

简介:一套基于CanFestival开源协议栈的电机控制固件工程,面向需要为伺服、直流或步进电机驱动器接入CANopen总线,实现通信、参数配置与运动控制的嵌入式开发者,适合有一定STM32、CCS或类C嵌入式基础的读者。压缩包共306个文件,以C源文件和H头文件为主,承担协议栈核心逻辑与接口定义;辅以makefile构建脚本、CCS工程配置文件、XML/Launch调试部署文件,方便在不同IDE和调试器中直接编译烧录。整包仅861KB,结构紧凑、模块划分清晰。已有653人学习下载。资源深入覆盖SDO对象字典交互、PDO过程数据传输、NMT网络管理、任务调度与消息队列等CanFestival关键机制,并包含电机控制相关的主从站示例代码。读者可据此快速理解CANopen通信流程,也能直接在CCS等环境下进行移植、裁剪和二次开发。作为一套可运行的固件参考,能大幅缩短从零搭建CANopen电机控制栈的周期,适合项目原型验证、教学实验和产品化前的快速评估。

1. 从 Firmwares-master 目录名到 CanFestival 电机控制:这条技术链路解决什么问题

第一次拿到Firmwares-master这个目录名时,不要急着把它当成一个普通的固件源码包。日常做机器人关节或分布式电机驱动时,很多厂商的固件工程会直接用这个名字作为仓库根目录,里面塞着 bootloader、应用固件、驱动库和协议栈源码。真正值得关注的不是目录结构本身,而是藏在里面的 CanFestival——一个能在 MCU 上完整跑起来的 CANopen 协议栈,以及它如何承载电机控制这条业务主线。

CanFestival 不是电机库,它解决的是「多电机、同一条 CAN 总线、既要周期控制又要参数读写」的问题。你可以用它在 STM32 上通过 SDO 读写电机驱动器的模式字和 PID 参数,用 PDO 以 1 kHz 频率周期发送位置环指令,同时通过心跳报文监测每个关节是否掉线。这个方案适合正在从「裸 CAN 发帧控电机」升级到「总线化节点管理」的嵌入式工程师,也适合刚接触 CANopen 协议栈、想少走弯路的学生开发者。它的核心价值在于:把电机控制从发报文上升为建立一张分布式网络的节点管理。

2. CanFestival 的协议栈架构与电机控制的映射关系:先把原理立住

2.1 CanFestival 在固件工程里的目录角色

Firmwares-master这种命名习惯的工程里,CanFestival 通常以独立子目录出现,比如canfestival/或者lib_canfestival/。里面一般按src/include/drivers/组织源码。这个布局不是随便分的,协议栈的核心文件必须从业务代码里隔离出来,因为你以后要换 MCU 或者升级电机固件时,只需要替换drivers/下的文件,不需要动协议栈主体。

常见的 CanFestival 源码结构中,几个关键文件我要单独拎出来说。objacces.c负责对象字典的读写访问,它是整个 CANopen 协议栈的心脏;sdo.c是 SDO 服务的实现,处理电机参数的读写请求;pdo.c处理 PDO 的映射与收发;lss.c负责从站节点 ID 分配。而applicfg.hcanfestival.h是桥接头文件,里面做了大量与 MCU 相关的宏定义。上手时最容易犯的错是直接把整个协议栈拉进工程而不看applicfg.h里的配置,结果因为一个宏定义不对,TIMER 中断就进不去。

2.2 对象字典是电机的「全局寄存器表」

CANopen 之所以适合电机控制,核心就在于对象字典(Object Dictionary,OD)把电机的所有状态抽象成了索引。对电机控制而言,你关心的是这几个索引区间:0x18000x18FF是 TPDO 通信参数,0x1A000x1AFF是 TPDO 映射参数,0x16000x16FF是 RPDO 映射参数。而0x2000之后通常是厂商自定义区,电机厂商会把控制模式、电流环 PID、位置环参数放在这个区域。

这个设计的实际意义在于:当你要给电机下发一个新的速度环 Kp 时,不需要自己定义一套私有通信协议,只需要通过 SDO 向对象字典的某个索引写入浮点值即可。比如对达妙这类智能电机,0x2000附近往往就是厂商定义的参数区。我一般会先在ObjDict.h里把对象字典声明出来,再在ObjDict.c里补充初始值。下面这段代码是从对象字典文件中摘出的关键模式:

/* ObjDict.c 中对象字典条目示例 */ const indextable ObjDict_Index[] = { /* 0x1600: RPDO1 映射参数,把电机控制字映射到 PDO */ { 0x1600, 0, 0, { &ObjDict_obj_0x1600 } }, /* 0x1A00: TPDO1 映射参数,把电机状态字与位置值映射出去 */ { 0x1A00, 0, 0, { &ObjDict_obj_0x1A00 } }, /* 0x2000: 厂商自定义区,读写电机速度环 Kp */ { 0x2000, 0, 0, { &ObjDict_obj_0x2000 } } };

对象字典在编译期就申请好了内存,每个条目通过subindexaccessType控制读写权限。电机控制里最容易忽略的是映射参数的字节长度:PDO 最多映射 8 字节,如果电机控制字 + 目标位置 + 速度一共超过 8 字节,就得拆成多个 PDO 或者降低映射精度。我见过不少人在0x1600里映射了 10 个字节,编译不报错但运行起来 PDO 一直不触发,原因就是总长度溢出了。

2.3 SDO 与 PDO 的分工:电机控制里的两条通信通道

电机控制场景下,SDO 和 PDO 的分工非常明确。SDO 走的是确认式应答,适合控制模式切换、参数配置、零点标定这类低频但需要保证可靠的操作。PDO 走的是无应答的周期广播,适合位置、速度、力矩指令这种每毫秒刷新一次的数据。是否使用 SDO 去设置电机模式,可以在任何时间执行,因为 CANopen 协议本身不限制 SDO 与 PDO 同时工作。

主站 从站(电机节点) | SDO 写索引 0x2000 | |--------------------------->| | SDO 响应(成功/失败) | |<---------------------------| | RPDO1(8 字节控制字) | |===>===>===>===>===>===>===>| 周期发送,无应答 | TPDO1(8 字节状态回传) | |<===<===<===<===<===<===<===| 周期上传,无应答

实际编码时,我习惯把电机使能、失能、修改 PID 参数放在 SDO 通道,把位置环目标值和速度前馈放在 PDO 通道。这样即使 PDO 因为总线繁忙丢了一帧,下一周期也能补上,而关键参数操作不会丢。SDO 的应答超时要单独做定时器管理,电机从站如果死机 500 ms 没有响应,主站应当主动进入错误状态而不是盲目重发。

3. 在 STM32 上移植 CanFestival 到电机控制器:最小可运行工程

3.1 从 Firmwares-master 中挑选协议栈文件

移植的第一步不是写代码,而是明确「哪些文件进工程、哪些文件不进工程」。CanFestival 的源码会带一个示例目录,里面有针对不同板卡的驱动。电机控制工程里,我一般只拷贝这几部分:src/下整个协议栈核心、include/下的头文件、drivers/下对应 MCU 的 can 和 timer 驱动。至于examples/里的测试程序,参考可以,不要直接编进工程,因为测试程序往往跟特定板卡深度绑定。

# 复制协议栈核心源码与驱动(主机侧操作) mkdir -p firmware/src mkdir -p firmware/inc cp -r canfestival/src/*.c firmware/src/ cp -r canfestival/include/*.h firmware/inc/ cp -r canfestival/drivers/stm32_can /firmware/src/drv_can.c cp -r canfestival/drivers/stm32_timer /firmware/src/drv_timer.c

拷贝完以后,需要确认工程里是否定义了CANOPEN_BIG_ENDIAN等宏。大部分电机主控是 STM32F4、F1 或 G4 系列,小端模式居多,不需要额外处理。但如果是整车控制器用了别家的主控芯片,字节序问题会让 PDO 里的电机位置出现「高低字节颠倒」的怪现象——位置值偶尔对、偶尔不对,加 1 变成 256,排查起来很隐蔽。

3.2 can 驱动层适配:先跑通发送再调接收

协议栈的核心不关心具体 MCU 的 CAN 外设怎么初始化,它只调用canSend()canReceive()两个接口。因此移植的核心工作就是实现这两个函数。

/* drv_can.c 中 STM32 bxCAN 发送实现 */ #include "canfestival.h" #include "stm32f4xx_hal.h" extern CAN_HandleTypeDef hcan; /* 已在 HAL 层初始化 */ UNS8 canSend(CAN_PORT notused, Message *m) { CAN_TxHeaderTypeDef txHeader; uint8_t data[8]; uint32_t i; /* 从协议栈 Message 结构体取 CAN ID、数据和长度 */ txHeader.IDE = CAN_ID_STD; /* 电机控制统一使用标准帧 */ txHeader.RTR = CAN_RTR_DATA; txHeader.DLC = m->len; txHeader.StdId = m->cob_id; /* 直接使用协议栈算出的 COB-ID */ for (i = 0; i < m->len; i++) { data[i] = m->data[i]; } if (HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &txMailbox) != HAL_OK) { return 1; /* 发送失败,协议栈会处理错误 */ } return 0; }

canSend()里有一个容易被忽略的细节:m->cob_id是协议栈内部已经组装好的完整 COB-ID,不需要再手动拼接节点 ID 和功能码。如果你自己在外面又做了一次(node_id << 7) | func,发送出来的报文 ID 会整体左移错位,从站根本不会响应。另外注意标准帧的标志位,很多电机驱动器只支持标准帧,扩展帧即使 ID 一样也不会被识别。

接收方向建议用 CAN 接收中断配合 FIFO。电机工作在 1 kHz 控制周期时,总线报文的到达时间基本可以预测,不需要复杂的消息队列框架,直接在中断服务函数里调用canDispatch()即可。

void CAN_RX_IRQHandler(void) { CAN_RxHeaderTypeDef rxHeader; uint8_t data[8]; Message msg; if (HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &rxHeader, data) != HAL_OK) { return; } msg.cob_id = rxHeader.StdId; msg.rtr = rxHeader.RTR; msg.len = rxHeader.DLC; memcpy(msg.data, data, runtime); canDispatch(&msg); /* 协议栈内部按 OD 分发 */ }

3.3 定时器驱动:1 ms 心跳决定控制周期

CanFestival 的任务调度全部依赖定时器驱动,电机控制的项目里我一般把定时器中断频率固定在 1 kHz。这正好匹配很多关节电机的位置环刷新率——PDO 的控制指令以这个频率发出去,电机的电流环内部单独闭环,不占用总线资源。

/* drv_timer.c 中 1ms 定时中断 */ void TIM_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); TimeDispatch(); /* CanFestival 的核心时间调度,驱动 PDO 周期发送 */ } }

定时器必须先于协议栈启动。TimerInit()初始化定时器硬件,setTimer()设置超时值,getElapsedTime()读取时间流逝。三者配合不好,最典型的现象是心跳报文不发、PDO 不按照预定的周期发射。TimeDispatch()中断频率过高或过低,会直接影响电机的控制响应。把 1 kHz 定时器启动放在initialisation()函数里,并确保它的优先级高于 CAN 接收中断,才能在 PDO 到达时及时触发协议栈处理。

4. 在 CanFestival 下实现电机位置/速度/力矩控制:从帧格式到参数调优

4.1 关节电机的控制帧格式与模式字

达妙、MJB 这类的智能关节电机,内部已经集成了 FOC 驱动器和编码器,对外通过 CAN 接收控制指令。这类电机通常有一个「混合控制模式」,即位置、速度、力矩同时下发,电机内部做优先级仲裁。在 CanFestival 框架下,最自然的做法就是把这几个目标值映射进同一个 RPDO。

从实践看,达妙电机的控制指令是 8 字节定长报文:第 1 字节是控制字,第 2 字节是模式字,后面 2 字节是目标位置,接着 2 字节是目标速度,最后 2 字节是目标力矩。控制字0xFC表示使能,0xFD表示失能,0xFE表示保存零位。模式字0x0E是混控模式,0x10是位置速度模式,0x12是速度模式。

字节偏移字段说明示例值
0控制字使能/失能/保存零位0xFC
1模式字混控/位置/速度0x0E
2-3目标位置0.01 度/LSB,有符号0x1000
4-5目标速度0.1 rpm/LSB0x026C
6-7目标力矩电流环目标,单位取决于驱动器0x0000

这里的位置单位映射是排查问题的高频区。有人直接发送弧度值进去,电机转得飞快;有人发送度值但没乘 100,电机只转一点点。我一般会在协议栈外面定义一个封装层,把浮点控制量在发 PDO 之前统一转换成整数 LSB。

4.2 初始化电机:SDO 完成模式切换

电机上电后默认处于失能状态,必须先通过 SDO 设置运行模式,再使能。这体现了 CANopen 状态机的好处——NMT 状态、电机使能状态、对象字典设置三者之间有清晰的先后顺序。

/* 通过 SDO 给电机节点设置混控模式 */ void set_motor_control_mode(uint8_t node_id, uint8_t mode) { uint8_t data[4]; /* 索引 0x2000,子索引 0x01,写入模式字 */ data[0] = 0x00; /* 子索引 */ data[1] = mode; /* 模式值 */ data[2] = 0x00; data[3] = 0x00; /* 构建 SDO 写请求报文并发送 */ Message msg; msg.cob_id = 0x600 + node_id; msg.rtr = 0; msg.len = 8; msg.data[0] = 0x22; /* SDO 加速下载请求:带 4 字节数据 */ msg.data[1] = 0x00; /* 索引低位 */ msg.data[2] = 0x20; /* 索引高位 */ msg.data[3] = 0x00; /* 子索引,SDO 数据标识 */ msg.data[4] = mode; msg.data[5] = 0x00; msg.data[6] = 0x00; msg.data[7] = 0x00; canSend(0, &msg); }

SDO 写请求报文结构里,0x22是加速下载请求,表示「我要写 4 字节数据」。msg.data[1]msg.data[2]组合成对象字典索引,msg.data[3]是子索引。发送完以后要等待从站的 SDO 回复,回复的 COB-ID 是0x580 + node_id,回复报文里的 abort code 会告诉你写入失败的具体原因——最常出现的是0x06090030,表示超出对象字典范围。

设置完模式再使能,顺序乱了会出现「使能成功但不转」或者「转一圈就报错」。因为电机驱动器的模式字和使能位在 CANopen 对象字典里是分开的两个条目,必须先确保模式被正确保存,再接控制指令。

4.3 PDO 周期发送:控制循环里的关键代码

电机控制的核心循环就是每 1 ms 执行一次 PDO 发送。

/* 1kHz 定时中断里,周期发送 RPDO1 到电机节点 */ void motor_control_loop(uint8_t node_id, uint16_t pos, uint16_t spd, uint16_t tor) { Message pdo_msg; uint8_t *data = pdo_msg.data; /* 构造 8 字节 PDO 数据:控制字+模式字+位置+速度+力矩 */ data[0] = MOTOR_CTRL_ENABLE; /* 0xFC 使能 */ data[1] = MOTOR_MODE_HYBRID; /* 0x0E 混控模式 */ data[2] = pos >> 8; /* 位置高字节 */ data[3] = pos & 0xFF; /* 位置低字节 */ data[4] = spd >> 8; /* 速度高字节 */ data[5] = spd & 0xFF; /* 速度低字节 */ data[6] = tor >> 8; /* 力矩高字节 */ data[7] = tor & 0xFF; /* 力矩低字节 */ /* RPDO1 的 COB-ID 为 0x200 + node_id */ pdo_msg.cob_id = 0x200 + node_id; pdo_msg.len = 8; pdo_msg.rtr = 0; canSend(0, &pdo_msg); }

整个电机控制循环里的这两个参数需要单独调:速度和力矩的高低字节发送。达妙这类小电机的位置精度是 0.01 度,意味着 36000 就能表达一整圈,但这个精度配合 FOC 电流环足够了。速度值用的是 0.1 rpm,如果直接发 0 表示位置环模式下不限制速度,发太大反而会让电机出现震荡。

PDO 周期发送的「锚点」应该在定时器中断里直接调,不要通过消息队列转一手。转一手会让周期出现几十微秒的抖动,高速转动时会听到明显噪声。

4.4 TPDO 状态回传:位置和电流的闭环验证

电机状态回传通过 TPDO 实现。在对象字典里配置 TPDO1 映射位置、速度和电流值,从站会按照主站配置的周期发送。主站这边的处理不复杂,但要注意一点:从站 TPDO 的周期由从站自己决定,主站不能强制改变,除非通过 SDO 去修改从站 0x1800 里的事件时间和禁止时间。

收到 TPDO 后,解析位置值是闭环控制的前提。位置值通常占 2 字节,按 LSB 转换成实际角度前,要确认它是单圈的还是多圈的。多圈位置有符号位,单圈位置往往 0 到 36000 循环。做关节控制时我一般用多圈模式,省去每次上电找零位的麻烦。

5. 把 CanFestival 电机控制做成可靠固件的几个工程技巧

5.1 下电前先保存电机零位与限位参数

电机固件里最容易忽略的是「参数保存」这一步。很多智能电机通过 SDO 收到的参数只在 RAM 中生效,断电即失。批量小电机时你会发现,同一个 ID 的电机重新上电后 PID 参数变了,就是因为没有发送参数保存指令。CANopen 协议栈里没有标准的参数保存机制,这是厂商自定义功能,通常由电机驱动器的控制字或专门的 SDO 对象实现。

在 CanFestival 应用层里,我会在收到「系统下电」的 GPIO 信号后,先让主站发送保存零位指令,再等待电机回传完成响应,最后才切断继电器电源。如果没有独立的掉电检测电路,至少要在定时器中断里判断控制指令是否连续丢失超过 100 ms,超时就把所有电机的 PDO 数据里的控制字置为失能状态,避免电机在通讯中断时保持力矩输出,带着正在组装的手臂继续动作。如果你在总线上带了多个电机,用统一的 0x700 心跳报文来判断通讯链路是最简单的方法。

5.2 实时抢断与总线优先级划分

电机控制对总线时延比较敏感,当同一条 CAN 总线上挂了关节电机、底盘电机和传感器,就要重新梳理 COB-ID 的分配规则。注意 RPDO 和 TPDO 的 COB-ID 中间预留区域不要和电机厂商默认值冲突。CAN 报文优先级由 ID 大小决定,ID 越小优先级越高,所以关节位置控制指令要用较小的 ID 范围,传感器状态反馈用较大的 ID 范围。

电机 PDO 指令: 0x180 ~ 0x1FF (最高优先级) 电机状态回传: 0x280 ~ 0x2FF 底盘电机控制: 0x380 ~ 0x3FF 传感器心跳: 0x700 ~ 0x7FF

CAN 总线仲裁的「非破坏性」特性在此时体现得淋漓尽致——当关节控制指令和底盘控制指令同时抢占总线时,ID 小的关节指令能无损坏地赢得总线,底盘指令自动重发等待。

5.3 电机报错与 CANopen 错误帧的区分

电机驱动器的错误状态和 CANopen 协议栈的错误状态是两回事。电机报 E_TEMP 过温,通过 TPDO 里的状态字回传;而协议栈报「心跳超时」则是从站节点逻辑上掉线。排查问题时分清这两点,可以少走很多弯路。过温报警时电机会主动限制输出力矩,但 CanFestival 的 NMT 状态可能仍然是 Operational,心跳也正常。如果直接用心跳判断电机是否健康,会漏掉过温保护。正确做法是把 TPDO 里的电机状态字、电流值、温度值全部放进对象字典,由主站应用层做阈值判断。

6. 用 CAN 日志抓包验证整条控制链路:最后一个调试技巧

整条链路是否跑通,凭指示灯判断是不够的。准备一个 USB-CAN 分析仪,把总线上的报文导出来,核对三条核心报文:RPDO 控制指令、TPDO 状态回传、心跳报文。用candump工具可以直接在终端观察。

# 抓取 CAN 总线报文,过滤电机节点 1 的相关报文 candump can0 -t d -L | grep -E "(180|280|700)"

正常工作时,你应该看到 RPDO 以固定周期刷屏,TPDO 的频率与从站配置一致,心跳每 100 ms 左右出现一次。报文格式里有几个细节要留意:RPDO 的 DLC 必须为 8,如果从站配置的 PDO 长度为 4,剩余字节会被填充;TPDO 的位置值是否随电机转动连续变化,而不是突变到 0 再跳变;心跳报文如果出现「跳变」,说明从站协议栈跑飞过,不是简单的 CAN 通讯问题。

另一个实用操作是验证 SDO 读写是否成功。通过 CAN 分析仪抓包看 SDO 的回复报文,0x60开头的是正常回复,0x80开头的是异常回复。异常回复携带的 4 字节错误码指向具体的失败原因:0x06010000表示不支持该索引,0x06040041表示数据长度不匹配,0x08000000表示一般性错误。出现错误后按索引去查对象字典定义,多半是字典没有导出或者子索引写错。报文的周期统计还能帮你估算总线负载,简化算一度电机控制比较常见的负载水平:1 kHz 周期下,主站发 8 字节 PDO + 从站回 8 字节 TPDO + 心跳,总线上大约占用 20% 带宽,留给传感器和其他电机还有余量。留出余量后才能保证每个关节的控制指令都能在要求的周期内被实时响应。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询