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 |
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
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)
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
- Pinout & Configuration → Middleware → FREERTOS
- Interface 选 CMSIS_V2
- Tasks and Queues 里添加你的任务
我一般至少建三个任务:
| 任务名 | 栈大小 | 优先级 | 功能 |
|---|---|---|---|
| CANopenTask | 1024 | osPriorityHigh | 处理 CANopen 协议栈心跳、PDO、SDO |
| AppTask | 512 | osPriorityNormal | 用户应用逻辑,比如读传感器、控制输出 |
| DebugTask | 256 | osPriorityLow | 串口打印调试信息,不影响实时性 |
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.cCANopenNode/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 函数
}
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 | 读写 |
2.5 整体架构图
下面这张图把整个软件架构串起来了。你对照着看,心里就有谱了。
2.6 编译与验证
所有代码写完后,先编译一遍。如果报错,八成是路径没加对。检查一下:
- CANopenNode 的
.c文件有没有全部加入编译列表 CO_driver.h里的宏定义是否跟你的 MCU 匹配- FreeRTOS 的堆大小是否够用
编译通过后,烧录到板子上。用 CAN 分析仪抓一下,看看有没有发心跳帧。如果看到 0x700 + 节点 ID 的报文,恭喜你,移植成功了。