3. 开发环境搭建:STM32CubeMX配置CAN外设、FreeRTOS任务创建、CANopenNode移植步骤

好,咱们直接进入正题。这一章我带你亲手把开发环境搭起来。说白了,就是让硬件能跑CAN,让系统能跑多任务,再把CANopenNode这个协议栈塞进去。这三件事做完,后面的心跳和SDO调试才有地方落脚。

我在项目里见过不少朋友,代码写了大半才发现CAN配置不对,或者FreeRTOS任务优先级搞反了,回头再改特别痛苦。所以这一章咱们一步一个脚印,把基础夯结实。

本章核心目标: 完成一个可运行的CANopenNode从站工程,包含CAN硬件初始化、FreeRTOS多任务调度、以及协议栈的移植与启动。

3.1 STM32CubeMX配置CAN外设

我个人习惯先用CubeMX把外设和时钟生成好,省得自己手写寄存器。你想想看,STM32的CAN外设其实挺复杂的,光过滤器就有32个组,用手配容易漏。

打开CubeMX,选好你的芯片型号(我这边用STM32F407VG举例)。然后按下面几步来:

  1. 配置时钟树: CAN外设挂在APB1总线上,我一般把APB1时钟设为42MHz。CAN需要的是42MHz的8分频,也就是5.25MHz的CAN时钟,这样能方便地得到常用波特率。
  2. 使能CAN1: 在Pinout & Configuration里找到Connectivity → CAN1,勾选激活。
  3. 配置CAN参数: 在Parameter Settings里,我习惯这样设:
    • Prescaler (分频系数):6
    • Time Quanta in Bit Segment 1:13
    • Time Quanta in Bit Segment 2:2
    • SJW (同步跳转宽度):1
    这样算下来,波特率 = 42MHz / (6 * (1 + 13 + 2)) = 500kbps。这是CANopen最常用的速率。
  4. 配置GPIO: CAN1的TX和RX引脚,我习惯用PB9和PB8。记得把GPIO模式设为Alternate Function,速度选High。
  5. 配置NVIC: 使能CAN1的RX0中断和SCE中断。优先级我一般设成5,别太低也别太高。
小技巧: 波特率算出来不对怎么办?我教你个土办法——用示波器抓CAN TX引脚的波形,看一个bit的宽度。500kbps的bit宽度应该是2微秒,差太多就回去调分频系数。

配置完别忘了生成代码。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 里看到任务函数的骨架。咱们后面再往里填内容。

注意: FreeRTOS的栈大小单位是word(4字节),不是byte。512 words = 2048 bytes,对于CANopenNode的主循环来说刚刚够。别设太小,否则会栈溢出,现象就是程序跑着跑着就挂了。

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 —— 心跳和NMT
  • CO_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 整体架构一览

说了这么多,咱们用一张图把整个系统的脉络理清楚。你一看就明白各个模块是怎么配合的。

CANopenNode从站软件架构图 CANopenNode从站软件架构 应用层 (App_Control_Task) LED控制 / 传感器读取 / 电机控制 / 用户逻辑 CANopenNode协议栈 (CANopenNode_Task) NMT / SDO / PDO / 心跳 / 紧急报文 / 对象字典 CO_CANopen_Process() 每1ms轮询一次 驱动抽象层 (CO_driver.c) CO_CANsend() / CAN接收中断 / CO_TIMER_IRQHandler() CAN硬件 (STM32 CAN1) HAL_CAN_Transmit_IT / HAL_CAN_Receive_IT 定时器 (TIM6) 1ms中断 → CO_TIMER_IRQHandler() CAN总线 (500kbps)

从这张图你能看到,整个系统是分层设计的。应用层只管自己的逻辑,不关心CAN报文怎么发;协议栈层负责解析和组包;驱动层做硬件适配。每一层各司其职,出了问题也容易定位。

移植检查清单:
  • ☐ CAN外设初始化成功,波特率500kbps
  • ☐ FreeRTOS三个任务已创建,栈大小合理
  • ☐ CO_driver.c中CO_CANsend()已对接发送队列
  • ☐ 1ms定时器中断已使能,调用CO_TIMER_IRQHandler()
  • ☐ CAN接收中断中调用CO_CANrxMsg_t的回调
  • ☐ CANopenNode_Task中循环调用CO_CANopen_Process()

做完这些,你的开发环境就搭好了。下一件事就是让从站先跑起来,发个心跳报文看看。不过那是后面章节的内容了,咱们先把这一步走稳。