2. 开发环境搭建:STM32CubeMX工程配置、FreeRTOS集成、CANopenNode源码移植

说实话,嵌入式开发里最磨人的往往不是写代码,而是搭环境。尤其是要把 FreeRTOS 和 CANopenNode 塞进一个 STM32 工程里,稍不留神就踩坑。这一章我带你一步步走通,把那些容易翻车的地方提前指出来。

2.1 工具链准备

先列一下我常用的工具版本。不一定非得一模一样,但别差太远,否则有些坑你查都查不到。

工具 推荐版本 备注
STM32CubeMX 6.10+ 代码生成器,版本太老可能没有 FreeRTOS 中间件
STM32CubeIDE 1.15+ 或者用 Keil / IAR,但 IDE 版本要匹配
FreeRTOS CMSIS-RTOS v2 封装 CubeMX 直接集成,省去手动移植
CANopenNode v4.0+ GitHub 官方仓库,注意选稳定分支
MCU STM32F407 / F103 / G4 系列 至少 64KB RAM + 256KB Flash
个人习惯:我一般用 STM32CubeIDE 做主力,调试器直接集成,省得来回切软件。如果你用 Keil,记得把 FreeRTOS 的堆栈配置调大一点,Keil 默认给得很抠门。

2.2 STM32CubeMX 工程配置

打开 CubeMX,选好你的芯片型号。我以 STM32F407VGT6 为例,因为它的 CAN 外设有两个,RAM 也够用。

2.2.1 时钟树配置

CAN 外设对时钟精度要求比较高。我建议直接用外部晶振,HSE 配到 168MHz。为什么?因为 CAN 的位时序需要精确的时钟源,内部 RC 振荡器漂移太大,跑 CANopen 容易丢帧。

HSE: 8MHz → PLL → SYSCLK: 168MHz
APB1: 42MHz (CAN 挂在这个总线上)
APB2: 84MHz
我曾经踩过的坑:有次图省事用了内部 HSI,结果 CAN 总线在高温下频繁报错。后来换成外部晶振,问题全消。所以,别省这个晶振钱。

2.2.2 CAN 外设配置

CAN 的配置参数直接决定了通信的稳定性。我一般这样设:

  • 模式:Normal 模式(调试时先用 Loopback)
  • 波特率:250 kbps(CANopen 默认推荐值)
  • 时间片:16 个时间量子,采样点设在 87.5%
  • FIFO:使能 FIFO0 和 FIFO1,每个深度 3 个邮箱
  • 中断:使能接收中断、错误中断、状态变化中断

波特率计算有个公式,我直接给你一个现成的:

CAN 波特率 = APB1 时钟 / (BRP + 1) / (TS1 + TS2 + 3)
以 42MHz APB1、250kbps 为例:
BRP = 5, TS1 = 13, TS2 = 2
→ 42M / 6 / 18 = 388.89k? 不对,重算:
实际:42M / 6 / 28 = 250k (TS1=13, TS2=2, Sync=1 → 总 = 16)
关键点:采样点位置最好在 80%~87.5% 之间。CANopen 对时序容差要求高,采样点太靠前或靠后都会增加误码率。

2.2.3 GPIO 与中断优先级

CAN 的 TX/RX 引脚要配成复用功能。另外,别忘了把 CAN 接收中断的优先级设高一点。FreeRTOS 里中断优先级分组要统一,我习惯用 4 位抢占优先级。

HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 5, 0);
HAL_NVIC_SetPriority(CAN1_RX1_IRQn, 5, 0);
HAL_NVIC_SetPriority(CAN1_SCE_IRQn, 5, 0);

2.3 FreeRTOS 集成

CubeMX 集成的 FreeRTOS 是 CMSIS-RTOS v2 封装层。说白了,就是给你包了一层标准 API,底层还是 FreeRTOS 内核。

2.3.1 在 CubeMX 中启用 FreeRTOS

  1. Pinout & Configuration → Middleware → FREERTOS
  2. Interface 选 CMSIS_V2
  3. Tasks and Queues 里添加你的任务

我一般至少建三个任务:

任务名 栈大小 优先级 功能
CANopenTask 1024 osPriorityHigh 处理 CANopen 协议栈心跳、PDO、SDO
AppTask 512 osPriorityNormal 用户应用逻辑,比如读传感器、控制输出
DebugTask 256 osPriorityLow 串口打印调试信息,不影响实时性
我建议:CANopenTask 的栈给 1024 字节其实有点紧。如果你开了很多 PDO 映射,建议加到 1536。我遇到过栈溢出导致任务莫名其妙挂掉的情况,查了两天才发现是栈不够。

2.3.2 配置 FreeRTOS 内核参数

在 CubeMX 的 FreeRTOS 配置页里,有几个参数必须改:

  • TOTAL_HEAP_SIZE:至少 16KB,我一般给 32KB
  • USE_PREEMPTION:Enabled
  • CPU_CLOCK_HZ:跟你的 SYSCLK 一致,168000000
  • TICK_RATE_HZ:1000(1ms 一个 tick,CANopen 的定时器精度够用)

2.4 CANopenNode 源码移植

CANopenNode 的移植,说白了就是把它那套协议栈塞进你的 FreeRTOS 工程里。核心工作就三块:定时器、CAN 驱动、对象字典。

2.4.1 获取源码

从 GitHub 上 clone 下来:

git clone https://github.com/CANopenNode/CANopenNode.git
cd CANopenNode
git checkout v4.0

把以下目录复制到你的工程里:

  • CANopenNode/stack/ → 协议栈核心代码
  • CANopenNode/example/ → 参考实现,尤其是 CO_driver.h 和 CO_driver.c
  • CANopenNode/301/ → CiA 301 标准对象

2.4.2 移植定时器接口

CANopenNode 需要一个 1ms 的硬件定时器。我直接用 FreeRTOS 的软件定时器,省一个硬件外设。

// 在 CO_driver.c 中实现
void CO_timer_init(void) {
    osTimerDef(timer_canopen, CO_timer_callback);
    canopen_timer = osTimerCreate(osTimer(timer_canopen), osTimerPeriodic, NULL);
    osTimerStart(canopen_timer, 1); // 1ms 周期
}

void CO_timer_callback(void const *arg) {
    CO_TMR_TICK(); // CANopenNode 的 tick 函数
}
注意:CO_TMR_TICK() 必须在定时器中断或高优先级任务里调用。如果你放在低优先级任务里,CANopen 的同步和心跳会抖动,严重时会导致主站报错。

2.4.3 移植 CAN 驱动

CANopenNode 通过 CO_CANsend() 和 CO_CANreceive() 两个函数跟硬件交互。你需要把 STM32 的 HAL 库封装进去。

// 发送
CO_ReturnError_t CO_CANsend(CO_CANmodule_t *CANmodule, CO_CANtx_t *buffer) {
    CAN_TxHeaderTypeDef header;
    header.StdId = buffer->ident;
    header.IDE = CAN_ID_STD;
    header.DLC = buffer->DLC;
    header.RTR = CAN_RTR_DATA;
    
    if (HAL_CAN_AddTxMessage(&hcan1, &header, buffer->data, &tx_mailbox) != HAL_OK) {
        return CO_ERROR_TX_OVERFLOW;
    }
    return CO_ERROR_NO;
}

// 接收(在 CAN 接收中断里调用)
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
    CAN_RxHeaderTypeDef header;
    uint8_t data[8];
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &header, data);
    
    CO_CANrxMsg_t msg;
    msg.ident = header.StdId;
    msg.DLC = header.DLC;
    memcpy(msg.data, data, 8);
    CO_CANreceive(CANmodule, &msg);
}

2.4.4 对象字典配置

对象字典是 CANopen 的灵魂。你需要根据你的 I/O 从站功能,定义哪些对象是可读的、可写的、映射到 PDO 的。

我一般用 CO_OD.h 和 CO_OD.c 来管理。一个简单的数字量输入从站,对象字典大概长这样:

索引 子索引 名称 类型 访问
0x1000 0 设备类型 UNSIGNED32 只读
0x1017 0 心跳生产者时间 UNSIGNED16 读写
0x1400 1 接收 PDO 通信参数 UNSIGNED32 读写
0x6000 1~8 数字量输入 UNSIGNED8 只读
0x6200 1~8 数字量输出 UNSIGNED8 读写
核心思路:对象字典就是你的 I/O 点和协议栈之间的翻译官。你写应用代码时,直接读写对象字典里的变量,CANopenNode 会自动帮你处理 PDO 映射和 SDO 访问。

2.5 整体架构图

下面这张图把整个软件架构串起来了。你对照着看,心里就有谱了。

CANopenNode 工业 I/O 从站软件架构 硬件层 STM32F4 MCU | CAN 收发器 | 数字量 I/O 电路 | 外部晶振 驱动层 HAL CAN 驱动 | FreeRTOS 定时器 | GPIO 控制 | 串口调试 中间件层 FreeRTOS 内核 | CMSIS-RTOS v2 API | CANopenNode 协议栈 应用层 对象字典管理 | PDO 映射 | SDO 处理 | 用户 I/O 逻辑 数据流向:硬件中断 → 驱动回调 → CANopenNode 协议栈 → 对象字典 → 用户任务

2.6 编译与验证

所有代码写完后,先编译一遍。如果报错,八成是路径没加对。检查一下:

  • CANopenNode 的 .c 文件有没有全部加入编译列表
  • CO_driver.h 里的宏定义是否跟你的 MCU 匹配
  • FreeRTOS 的堆大小是否够用

编译通过后,烧录到板子上。用 CAN 分析仪抓一下,看看有没有发心跳帧。如果看到 0x700 + 节点 ID 的报文,恭喜你,移植成功了。

调试小技巧:如果心跳没出来,先检查定时器是否在跑。在 CO_timer_callback 里加个 GPIO 翻转,用示波器看 1ms 周期对不对。我遇到过定时器优先级被其他中断抢占导致周期不准的情况。