第2章:STM32 CANopen协议栈移植——基于CanFestival与Lely实战
协议栈移植,说白了就是把CANopen的“灵魂”塞进STM32的“身体”里。很多初学者拿到源码就懵了——几千个文件,从哪下手?
我刚开始做移植时也踩过这个坑。后来发现,核心就三件事:时钟配置、对象字典挂载、CAN接口对接。今天咱们就用CanFestival和Lely两个主流协议栈,手把手走一遍流程。
2.1 移植前的准备工作
动手之前,先把家底清点清楚。你需要:
- 硬件:STM32F103/F407开发板(带CAN收发器,比如TJA1050)
- 软件:STM32CubeMX生成HAL库工程 + Keil/IAR
- 协议栈源码:CanFestival 3.x 或 Lely CANopen 2.x
我的习惯:先用CubeMX把CAN外设调通——发一帧、收一帧,确认硬件链路没问题。协议栈移植最怕“硬件没通就往上堆软件”,排查起来很痛苦。
2.2 CanFestival协议栈移植
CanFestival是开源界的老牌协议栈,结构清晰,但代码风格偏“学术”。移植时抓住它的驱动层接口就行。
2.2.1 文件结构梳理
CanFestival的核心文件分三层:
| 层级 | 关键文件 | 作用 |
|---|---|---|
| 应用层 | ObjDict.c / ObjDict.h | 对象字典,由EDSG工具生成 |
| 协议核心 | lifegrd.c / lss.c / nmtSlave.c | NMT状态机、心跳、LSS等 |
| 驱动层 | canDriver.c / canDriver.h | 硬件CAN收发,需要你重写 |
移植时,你只需要动驱动层。其他文件基本不用改。
2.2.2 驱动接口实现
打开 canDriver.c,找到这几个函数:
// 发送一帧CAN报文
unsigned char canSend(CAN_PORT port, Message *m) {
// 将Message结构体拆成CAN帧
CAN_TxHeaderTypeDef txHeader;
uint8_t txData[8];
txHeader.StdId = m->cob_id;
txHeader.DLC = m->len;
memcpy(txData, m->data, m->len);
// 调用HAL_CAN_AddTxMessage
return HAL_CAN_AddTxMessage(&hcan1, &txHeader, txData, &TxMailbox) == HAL_OK;
}
// CAN接收中断回调(在stm32f1xx_it.c中调用)
void CAN1_RX0_IRQHandler(void) {
CAN_RxHeaderTypeDef rxHeader;
uint8_t rxData[8];
HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &rxHeader, rxData);
// 组装成Message结构体,压入协议栈接收队列
Message msg;
msg.cob_id = rxHeader.StdId;
msg.len = rxHeader.DLC;
memcpy(msg.data, rxData, msg.len);
canDispatch(&msg); // CanFestival核心调度函数
}
我曾经踩过的坑:CanFestival的
Message结构体里,cob_id是UNS32类型,而HAL库的StdId是uint32_t。直接赋值没问题,但如果你用了扩展帧(29位ID),记得把IDE标志位也设上。我有一回忘了设,从站死活进不了Operational状态,查了两天才发现。
2.2.3 初始化流程
在 main.c 里,按这个顺序初始化:
- HAL库初始化(时钟、GPIO、CAN)
- 启动CAN外设(
HAL_CAN_Start(&hcan1)) - 使能CAN接收中断(
HAL_CAN_ActivateNotification) - 调用
initTimer()启动CanFestival的定时器(用于心跳、同步等) - 调用
setNodeId()设置从站节点ID - 调用
initNMT()初始化NMT状态机
int main(void) {
HAL_Init();
SystemClock_Config();
MX_CAN1_Init();
HAL_CAN_Start(&hcan1);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
initTimer(); // 启动协议栈定时器
setNodeId(0x05); // 节点ID设为5
initNMT(); // 进入Pre-operational状态
while(1) {
// 主循环里调用协议栈轮询函数
// CanFestival没有显式的poll函数,靠定时器中断驱动
// 但建议加一个简单的看门狗喂狗操作
}
}
2.3 Lely CANopen协议栈移植
Lely是后起之秀,代码用C++写的(但提供C接口),设计更现代。它的移植思路和CanFestival类似,但驱动层更薄。
2.3.1 核心差异
| 对比项 | CanFestival | Lely |
|---|---|---|
| 语言 | 纯C | C++(提供C接口) |
| 驱动层 | 需要实现canSend/canDispatch | 需要实现can_read/can_write |
| 定时器 | 内部定时器,需提供硬件时钟 | 外部传入时间戳,更灵活 |
| 对象字典 | 静态数组,编译时确定 | 支持动态注册,运行时修改 |
2.3.2 驱动接口实现
Lely的驱动接口更简洁。你需要实现两个函数:
// 在lely_can.c中
#include <lely/co/can.h>
// CAN发送
int can_write(struct can_ctx *ctx, struct can_msg *msg) {
CAN_TxHeaderTypeDef txHeader;
txHeader.StdId = msg->id;
txHeader.DLC = msg->len;
memcpy(txData, msg->data, msg->len);
return HAL_CAN_AddTxMessage(&hcan1, &txHeader, txData, &TxMailbox);
}
// CAN接收(在中断中调用)
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef rxHeader;
uint8_t rxData[8];
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData);
struct can_msg msg;
msg.id = rxHeader.StdId;
msg.len = rxHeader.DLC;
memcpy(msg.data, rxData, msg.len);
can_read(&msg); // Lely的接收入口
}
关键区别:Lely的
can_msg结构体里,id字段已经包含了IDE标志位(扩展帧时第29位为1)。CanFestival则是分开的cob_id和rtr/ide位。移植时务必看清楚。
2.3.3 初始化流程
Lely的初始化更“面向对象”一些:
#include <lely/co/init.h>
int main(void) {
HAL_Init();
SystemClock_Config();
MX_CAN1_Init();
HAL_CAN_Start(&hcan1);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
// Lely初始化
co_init(); // 全局初始化
co_node_t *node = co_node_create(0x05); // 创建节点,ID=5
co_node_start(node); // 启动节点,进入Pre-operational
while(1) {
// Lely需要定期调用co_process()
co_process(node);
// 建议10ms调用一次
HAL_Delay(10);
}
}
2.4 移植后的验证方法
移植完别急着写应用。先做三件事验证:
- NMT状态机验证:用CAN分析仪发NMT命令(0x00 + 节点ID),看从站能否从Pre-op切换到Operational
- 心跳报文验证:从站进入Operational后,应该周期性发送心跳(COB-ID 0x700 + NodeID)
- SDO读写验证:读对象字典0x1000(设备类型),应该返回你预设的值
我常用的工具:PCAN-View或CANtact。发NMT命令时注意——命令字是0x01(启动)不是0x80(进入Pre-op)。我见过有人把这两个搞反,从站一直复位。
2.5 本章知识结构图
下面这张图总结了协议栈移植的核心流程和关键接口:
2.6 常见移植问题与排查
移植过程中,你大概率会遇到下面这些问题:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 从站不响应NMT命令 | CAN过滤器没配置,过滤掉了NMT报文(COB-ID 0x000) | 检查HAL_CAN_ConfigFilter,设为接收所有ID |
| 心跳不发送 | 定时器中断没启动,或心跳生产者时间(0x1017)没配置 | 用逻辑分析仪看定时器引脚是否有脉冲 |
| SDO读写超时 | 对象字典索引/子索引不对,或SDO服务器未使能 | 用CAN分析仪抓SDO请求和响应,看COB-ID是否正确 |
| 进入Operational后自动复位 | 看门狗没喂,或心跳消费者超时 | 检查主循环中是否调用了喂狗函数 |
特别提醒:CAN过滤器的配置是“隐形杀手”。很多移植教程只字不提,但如果你没设好过滤器,协议栈根本收不到任何报文。我一般这样配——
CAN_FILTER_FIFO0,掩码设为0,接收所有ID。等调通了再按需细化。