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,把报文索引传进去,然后协议栈自己去读接收缓冲区。
所以我们需要做两件事:
- 在CAN接收中断里,把收到的报文存到一个环形缓冲区或直接存到CANopenNode的接收队列
- 调用
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系统中的位置。你看,它就像一座桥,连接了硬件和协议栈。
4.6 常见问题与调试技巧
移植过程中,我遇到过几个典型问题,列出来供你参考:
- 发送失败:检查CAN总线是否接好,终端电阻是否匹配(120Ω)。我曾经因为忘记焊终端电阻,折腾了一下午。
- 接收不到报文:先确认过滤器配置是否正确。可以用CAN分析仪发一帧标准ID=0x100的数据帧,看中断是否触发。
- 协议栈卡死:多半是
CO_CANsend返回了错误码,但上层没有处理。建议在发送失败时加一个重试机制。
调试小技巧:在 CO_CANsend 和接收回调里加GPIO翻转,用示波器看波形。发送时拉高一个引脚,接收完成时拉低另一个引脚。这样能直观看到收发是否正常,比看日志快多了。
好了,CAN驱动接口的移植就讲到这里。你只要把 CO_CANsend 和接收回调填好,CANopenNode就能在STM32上收发报文了。下一步,我们会把时钟和定时器也接进来,让协议栈真正跑起来。