第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 里,按这个顺序初始化:

  1. HAL库初始化(时钟、GPIO、CAN)
  2. 启动CAN外设(HAL_CAN_Start(&hcan1))
  3. 使能CAN接收中断(HAL_CAN_ActivateNotification)
  4. 调用 initTimer() 启动CanFestival的定时器(用于心跳、同步等)
  5. 调用 setNodeId() 设置从站节点ID
  6. 调用 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 移植后的验证方法

移植完别急着写应用。先做三件事验证:

  1. NMT状态机验证:用CAN分析仪发NMT命令(0x00 + 节点ID),看从站能否从Pre-op切换到Operational
  2. 心跳报文验证:从站进入Operational后,应该周期性发送心跳(COB-ID 0x700 + NodeID)
  3. SDO读写验证:读对象字典0x1000(设备类型),应该返回你预设的值
我常用的工具:PCAN-View或CANtact。发NMT命令时注意——命令字是0x01(启动)不是0x80(进入Pre-op)。我见过有人把这两个搞反,从站一直复位。

2.5 本章知识结构图

下面这张图总结了协议栈移植的核心流程和关键接口:

CANopen协议栈移植核心流程 CanFestival 移植路径 canDriver.c → canSend() / canDispatch() initTimer() → 硬件定时器驱动 setNodeId() + initNMT() → 状态机启动 Lely CANopen 移植路径 can_write() / can_read() → 硬件收发 co_init() → 全局初始化 co_node_create() + co_node_start() 验证:NMT命令 → 心跳 → SDO读写 💡 移植要点 • 驱动层是唯一需要修改的部分,协议核心代码不要动 • CAN中断优先级建议设为最低(防止影响实时任务),但不要关全局中断 • 定时器精度至少1ms,心跳周期通常设为100ms~1000ms

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。等调通了再按需细化。