3. 开发环境搭建:STM32CubeMX配置CAN外设、FreeRTOS任务创建、CANopenNode移植步骤
好,咱们直接进入正题。这一章我带你亲手把开发环境搭起来。说白了,就是让硬件能跑CAN,让系统能跑多任务,再把CANopenNode这个协议栈塞进去。这三件事做完,后面的心跳和SDO调试才有地方落脚。
我在项目里见过不少朋友,代码写了大半才发现CAN配置不对,或者FreeRTOS任务优先级搞反了,回头再改特别痛苦。所以这一章咱们一步一个脚印,把基础夯结实。
3.1 STM32CubeMX配置CAN外设
我个人习惯先用CubeMX把外设和时钟生成好,省得自己手写寄存器。你想想看,STM32的CAN外设其实挺复杂的,光过滤器就有32个组,用手配容易漏。
打开CubeMX,选好你的芯片型号(我这边用STM32F407VG举例)。然后按下面几步来:
- 配置时钟树: CAN外设挂在APB1总线上,我一般把APB1时钟设为42MHz。CAN需要的是42MHz的8分频,也就是5.25MHz的CAN时钟,这样能方便地得到常用波特率。
- 使能CAN1: 在Pinout & Configuration里找到Connectivity → CAN1,勾选激活。
- 配置CAN参数: 在Parameter Settings里,我习惯这样设:
- Prescaler (分频系数):6
- Time Quanta in Bit Segment 1:13
- Time Quanta in Bit Segment 2:2
- SJW (同步跳转宽度):1
- 配置GPIO: CAN1的TX和RX引脚,我习惯用PB9和PB8。记得把GPIO模式设为Alternate Function,速度选High。
- 配置NVIC: 使能CAN1的RX0中断和SCE中断。优先级我一般设成5,别太低也别太高。
配置完别忘了生成代码。CubeMX会帮你把CAN的初始化函数 MX_CAN1_Init() 和GPIO配置都写好。嗯,这里要注意,它生成的CAN过滤器默认是全部放行的,咱们做从站可以先不改。
3.2 FreeRTOS任务创建
CANopenNode是个事件驱动的协议栈,但底层需要轮询或者中断配合。我建议用FreeRTOS来管理任务,这样协议栈的定时器、报文收发、应用逻辑都能分开跑,互不干扰。
CubeMX里已经集成了FreeRTOS,你只需要在Middleware里选FREERTOS,然后选CMSIS_V2接口。我个人习惯用CMSIS_V2,因为它跟CubeMX生成的代码配合得最好。
咱们至少需要创建三个任务:
| 任务名称 | 功能描述 | 推荐栈大小 | 优先级 |
|---|---|---|---|
CANopenNode_Task |
运行CANopenNode的主循环,处理SDO、PDO、心跳等 | 512 words | osPriorityNormal (2) |
CAN_TX_Task |
从发送队列取报文,调用CAN硬件发送 | 256 words | osPriorityAboveNormal (3) |
App_Control_Task |
应用逻辑,比如控制LED、读取传感器 | 256 words | osPriorityBelowNormal (1) |
为什么CAN_TX_Task的优先级要高于CANopenNode_Task?我在项目里吃过亏。有一次发送任务优先级设低了,结果协议栈拼命往队列里塞报文,发送却来不及处理,队列直接爆了。你想想看,CAN总线上的报文发送是有实时性要求的,优先级高一点才稳。
在CubeMX的Task and Queues页面里,按上面的表格添加任务。代码生成后,你会在 app_freertos.c 里看到任务函数的骨架。咱们后面再往里填内容。
3.3 CANopenNode移植步骤
好,重头戏来了。CANopenNode的移植,说白了就是把它提供的模板文件跟你的硬件和系统对接上。我移植过好几个平台,STM32 + FreeRTOS是最顺手的组合。
首先,从GitHub上下载CANopenNode源码。我建议用v1.3或者更新的稳定版。把下面这些文件复制到你的工程里:
CANopen.c和CANopen.h—— 核心协议栈CO_driver.c和CO_driver.h—— 硬件抽象层,这个需要你改CO_SDOserver.c和CO_SDOserver.h—— SDO服务器CO_Emergency.c和CO_Emergency.h—— 紧急报文CO_NMT_Heartbeat.c和CO_NMT_Heartbeat.h—— 心跳和NMTCO_OD.c和CO_OD.h—— 对象字典,这个需要你根据设备描述文件生成
接下来,重点修改 CO_driver.c。这个文件是协议栈和硬件的桥梁。你需要实现以下几个关键函数:
| 函数名 | 你需要做的事 |
|---|---|
CO_CANsend() |
把CAN报文放入FreeRTOS的发送队列,而不是直接调用HAL_CAN_Transmit。这样不会阻塞协议栈。 |
CO_CANrxMsg_t 结构体 |
把HAL的CAN接收结构体映射到CANopenNode的接收结构体。主要是ID、DLC、Data的拷贝。 |
CO_TIMER_IRQHandler() |
用一个1ms的定时器中断来调用这个函数。CANopenNode的定时器精度就靠它了。 |
CO_LockQueue() / CO_UnlockQueue() |
用FreeRTOS的临界区或者互斥量来保护队列操作。我习惯用 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL()。 |
我曾经在移植时犯过一个低级错误——忘了实现 CO_TIMER_IRQHandler()。结果心跳报文死活不发,我还以为是波特率配错了,折腾了两天。后来发现是定时器没跑,协议栈的时间戳一直是0。嗯,这种坑踩过一次就记住了。
最后,在 CANopenNode_Task 的任务函数里,调用 CO_CANopen_Init() 初始化协议栈,然后在循环里调用 CO_CANopen_Process()。大概长这样:
void CANopenNode_Task(void *argument)
{
// 初始化CANopenNode
CO_CANopen_Init(&CO, &CAN1_Handle, &CO_OD, CO_TMR_IRQHandler);
for(;;)
{
// 处理协议栈
CO_CANopen_Process(&CO);
// 延时1ms,别让任务占满CPU
osDelay(1);
}
}
osDelay(1) 这个延时很关键。如果不加,任务会疯狂轮询,浪费CPU。加了之后,任务每1ms跑一次,刚好匹配CANopenNode的定时器精度。我试过5ms,心跳会抖动;试过0.5ms,CPU占用率飙到30%以上。1ms是最佳平衡点。
3.4 整体架构一览
说了这么多,咱们用一张图把整个系统的脉络理清楚。你一看就明白各个模块是怎么配合的。
从这张图你能看到,整个系统是分层设计的。应用层只管自己的逻辑,不关心CAN报文怎么发;协议栈层负责解析和组包;驱动层做硬件适配。每一层各司其职,出了问题也容易定位。
- ☐ CAN外设初始化成功,波特率500kbps
- ☐ FreeRTOS三个任务已创建,栈大小合理
- ☐ CO_driver.c中CO_CANsend()已对接发送队列
- ☐ 1ms定时器中断已使能,调用CO_TIMER_IRQHandler()
- ☐ CAN接收中断中调用CO_CANrxMsg_t的回调
- ☐ CANopenNode_Task中循环调用CO_CANopen_Process()
做完这些,你的开发环境就搭好了。下一件事就是让从站先跑起来,发个心跳报文看看。不过那是后面章节的内容了,咱们先把这一步走稳。