4、CANopenNode移植第一步:移植CAN驱动接口

好,咱们正式开始动手移植。很多初学者一上来就盯着CANopenNode的协议栈看,结果越看越懵。我的建议是——先别管协议,先把车造好。车是什么?就是CAN收发器这层。

CANopenNode要跑起来,底层必须有两个接口:CAN_SendMsg 和 CAN_ReceiveMsg。说白了,一个负责把报文发出去,一个负责把报文收进来。你想想看,如果这两条路都不通,上面协议栈写得再漂亮也是白搭。

核心原则:移植CAN驱动接口,不是让你重写CANopenNode,而是让你把STM32的HAL库或LL库的收发函数,包装成CANopenNode能调用的样子。

4.1 先搞清楚CANopenNode要什么

我刚开始移植的时候,也犯过傻——直接去翻CANopenNode的源码,想找到它到底怎么调用CAN的。后来发现,其实它留了几个钩子函数,你只要填上就行。

打开CANopenNode的源码目录,找到 CANopen.c 或 CO_driver.h,你会看到类似这样的声明:

/* CAN发送接口 */
int16_t CO_CANsend(CO_CANmodule_t *CANmodule, CO_CANtx_t *buffer);

/* CAN接收回调(由中断或轮询调用) */
void CO_CANreceive(CO_CANmodule_t *CANmodule, uint16_t index);

嗯,这里要注意:CO_CANsend 是CANopenNode主动调用的发送函数,而 CO_CANreceive 是底层收到报文后,通知协议栈的回调入口。

所以我们的任务很明确:

  • 把 CO_CANsend 映射到 STM32 的 CAN 发送函数
  • 在 CAN 接收中断里调用 CO_CANreceive

4.2 移植CAN_SendMsg

先看发送。CANopenNode传给你的 CO_CANtx_t 结构体里,包含了报文ID、数据长度、数据内容,还有是否是远程帧等标志。你需要把这些信息拆出来,塞进STM32的CAN发送邮箱。

我个人习惯用STM32的HAL库,因为可读性好,调试方便。下面是一个典型的实现:

int16_t CO_CANsend(CO_CANmodule_t *CANmodule, CO_CANtx_t *buffer)
{
    CAN_TxHeaderTypeDef txHeader;
    uint32_t txMailbox;
    uint8_t data[8];
    uint8_t i;

    /* 检查CAN模块是否已启用 */
    if (!CANmodule->enabled) return -1;

    /* 填充发送头 */
    txHeader.StdId = buffer->ident & 0x7FF;      // 11位标准ID
    txHeader.ExtId = 0;
    txHeader.IDE = CAN_ID_STD;
    txHeader.RTR = buffer->rtr ? CAN_RTR_REMOTE : CAN_RTR_DATA;
    txHeader.DLC = buffer->DLC;
    txHeader.TransmitGlobalTime = DISABLE;

    /* 复制数据 */
    for (i = 0; i < buffer->DLC; i++) {
        data[i] = buffer->data[i];
    }

    /* 调用HAL库发送 */
    if (HAL_CAN_AddTxMessage(&hcan1, &txHeader, data, &txMailbox) != HAL_OK) {
        return -1;  // 发送失败
    }

    return 0;  // 发送成功
}

避坑指南:我曾经在这里栽过跟头——忘记检查 CANmodule->enabled 标志。结果在CAN模块还没初始化完成时,协议栈就尝试发送报文,导致死锁。所以,第一行一定要做这个检查。

4.3 移植CAN_ReceiveMsg

接收比发送稍微复杂一点,因为涉及中断。CANopenNode的接收机制是这样的:底层收到一帧报文后,调用 CO_CANreceive,把报文索引传进去,然后协议栈自己去读接收缓冲区。

所以我们需要做两件事:

  1. 在CAN接收中断里,把收到的报文存到一个环形缓冲区或直接存到CANopenNode的接收队列
  2. 调用 CO_CANreceive 通知协议栈

下面是一个基于HAL库中断回调的实现:

/* HAL库的接收中断回调 */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    CAN_RxHeaderTypeDef rxHeader;
    uint8_t rxData[8];
    CO_CANrxMsg_t *msg;
    uint16_t index;

    if (hcan->Instance != CAN1) return;

    /* 从硬件读取报文 */
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData);

    /* 查找CANopenNode中对应的接收通道 */
    index = CO_CANrxMsg_read(&rxHeader.StdId, rxHeader.DLC, rxData);

    /* 如果找到有效通道,通知协议栈 */
    if (index != 0xFFFF) {
        CO_CANreceive(&CANmodule1, index);
    }
}

重要提醒:中断服务函数里不要做耗时操作。我曾经在中断里直接处理CANopenNode的OD(对象字典)访问,结果导致中断延迟过高,丢帧严重。正确的做法是:中断里只做报文接收和通知,具体处理交给主循环或任务。

4.4 初始化CAN模块

接口移植完了,别忘了初始化。CANopenNode需要一个 CO_CANmodule_t 结构体来描述CAN硬件。你需要调用 CO_CANmodule_init() 来配置它。

这里有一个典型的初始化流程:

步骤 操作 说明
1 HAL_CAN_Init() 配置CAN波特率、模式等硬件参数
2 HAL_CAN_Start() 启动CAN通信
3 HAL_CAN_ActivateNotification() 使能接收中断
4 CO_CANmodule_init() 初始化CANopenNode的CAN模块对象
5 CO_CANmodule_set() 配置接收过滤器(可选)

代码示例:

void CANopen_Init(void)
{
    /* 第1步:初始化STM32的CAN硬件 */
    HAL_CAN_Init(&hcan1);

    /* 第2步:启动CAN */
    HAL_CAN_Start(&hcan1);

    /* 第3步:使能接收中断 */
    HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

    /* 第4步:初始化CANopenNode的CAN模块 */
    CO_CANmodule_init(&CANmodule1, (void*)&hcan1, CO_CAN_BAUDRATE_250K, 0);

    /* 第5步:配置接收过滤器(只接收标准帧) */
    CO_CANmodule_set(&CANmodule1, CO_CAN_FILTER_STANDARD, 0);
}

4.5 整体架构图

下面这张图展示了CAN驱动接口在整个CANopenNode系统中的位置。你看,它就像一座桥,连接了硬件和协议栈。

CANopenNode 驱动接口架构 应用层(用户代码) CANopenNode 协议栈 CO_CANsend() / CO_CANreceive() (本章移植的接口) STM32 HAL CAN 驱动 CAN 收发器 + 总线 说明:CO_CANsend 和 CO_CANreceive 是 CANopenNode 与硬件之间的唯一桥梁 移植好这两个接口,协议栈就能在 STM32 上跑起来了

4.6 常见问题与调试技巧

移植过程中,我遇到过几个典型问题,列出来供你参考:

  • 发送失败:检查CAN总线是否接好,终端电阻是否匹配(120Ω)。我曾经因为忘记焊终端电阻,折腾了一下午。
  • 接收不到报文:先确认过滤器配置是否正确。可以用CAN分析仪发一帧标准ID=0x100的数据帧,看中断是否触发。
  • 协议栈卡死:多半是 CO_CANsend 返回了错误码,但上层没有处理。建议在发送失败时加一个重试机制。

调试小技巧:在 CO_CANsend 和接收回调里加GPIO翻转,用示波器看波形。发送时拉高一个引脚,接收完成时拉低另一个引脚。这样能直观看到收发是否正常,比看日志快多了。

好了,CAN驱动接口的移植就讲到这里。你只要把 CO_CANsend 和接收回调填好,CANopenNode就能在STM32上收发报文了。下一步,我们会把时钟和定时器也接进来,让协议栈真正跑起来。