第二章:开发环境搭建——STM32CubeMX工程配置、FreeRTOS集成、CANopenNode源码移植
好,咱们直接进入正题。这一章要干三件事:配好STM32CubeMX工程、把FreeRTOS请进来、再把CANopenNode源码移植进去。这三步走完,你的开发环境就算立住了。我个人习惯先把硬件资源理清楚,再动手配软件,这样不容易翻车。
2.1 STM32CubeMX工程配置——别小看这一步
很多人觉得CubeMX就是点点鼠标,没啥技术含量。嗯,我刚开始也这么想,直到有一次在项目里因为时钟树配错了,CAN总线死活起不来,排查了两天才发现是时钟源选错了。从那以后,我再也不敢轻视这一步。
2.1.1 新建工程与芯片选型
打开CubeMX,点击“New Project”。在芯片选择框里输入你用的型号,比如STM32F407ZGT6。选好之后双击,进入配置界面。
这里有个小技巧:我建议你直接按芯片型号搜索,别用系列筛选。因为有些型号的封装和资源差异很大,搜型号最准。
2.1.2 时钟树配置——CAN的命脉
CAN外设对时钟要求比较严格。CAN协议要求位时间精度在1%以内,说白了就是时钟不能乱配。
我一般这样配:
- HSE:8MHz外部晶振(根据你板子实际来)
- PLL:倍频到168MHz(F4系列常见主频)
- APB1:42MHz(CAN外设挂在APB1上)
配好之后,记得在“Clock Configuration”页面看一眼CAN的时钟源是不是APB1。我曾经见过有人配了168MHz主频,但APB1分频器设错了,CAN时钟变成了21MHz,结果波特率怎么算都不对。
2.1.3 CAN外设配置
在“Pinout & Configuration”页面,找到“Connectivity”下的“CAN1”。勾选“Activated”。
参数设置如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Bit Timings | Prescaler=6, BS1=13, BS2=2 | 对应250kbps(42MHz时钟下) |
| Mode | Normal | 正常模式,调试时可选Loopback |
| Interrupt | Enabled | 开启接收中断 |
你想想看,如果波特率配错了,CAN总线上的节点互相听不懂,那还玩什么?所以这一步务必仔细。
2.1.4 GPIO引脚配置
CAN的TX和RX引脚需要手动映射。以STM32F407为例:
- CAN1_TX → PB9
- CAN1_RX → PB8
在Pinout视图里,直接点击PB9,选择“CAN1_TX”。PB8同理。配好之后,引脚颜色会变成绿色,表示已分配。
2.2 FreeRTOS集成——让系统跑起来
CANopenNode需要多任务支持,FreeRTOS是最佳搭档。CubeMX对FreeRTOS的支持已经很成熟了,咱们直接用它生成。
2.2.1 在CubeMX中启用FreeRTOS
在“Pinout & Configuration”页面,找到“Middleware”下的“FREERTOS”。勾选“Activated”。
接口选择“CMSIS_V2”。为什么选这个?因为CMSIS_V2是ARM官方标准接口,兼容性好。我早期用过V1版本,后来发现V2在任务通知和消息队列上更强大,就全面迁移了。
2.2.2 任务创建与配置
在“Task and Queues”选项卡里,添加以下任务:
| 任务名 | 栈大小 | 优先级 | 功能 |
|---|---|---|---|
| CANopenNodeTask | 512 | osPriorityNormal | 处理CANopen协议栈 |
| AppTask | 256 | osPriorityBelowNormal | 用户应用逻辑 |
栈大小怎么定?我一般先给个保守值,跑起来后用FreeRTOS的uxTaskGetStackHighWaterMark()函数查看实际使用量,再调整。这样既省内存又不会溢出。
2.2.3 生成代码与工程导入
点击“Project” → “Generate Code”。生成后,用你喜欢的IDE打开(我习惯用Keil MDK)。
打开后,你会看到CubeMX已经帮你生成了FreeRTOS的初始化代码。在main.c里,找到MX_FREERTOS_Init()函数,里面就是任务创建的地方。
2.3 CANopenNode源码移植——重头戏来了
CANopenNode是一个开源的CANopen协议栈实现,代码质量很高。但移植起来还是有一些坑的,我一个个说。
2.3.1 源码获取与目录结构
从GitHub上下载CANopenNode源码。解压后,你需要的核心文件在CANopenNode/目录下:
CO_driver.h— 硬件抽象层接口CO_CAN.h— CAN驱动接口CO_timer.h— 定时器接口CO_Emergency.h— 紧急报文处理CO_SDO.h— SDO协议CO_PDO.h— PDO协议
把这些文件复制到你的工程目录下。我习惯在工程里建一个ThirdParty/CANopenNode文件夹,专门放第三方库。
2.3.2 硬件抽象层适配——最磨人的部分
CANopenNode通过CO_driver.h里的宏定义和函数接口来操作硬件。你需要实现以下几个关键函数:
// CAN发送函数
int16_t CO_CANsend(CAN_Type *CANptr, CO_CANtx_t *buffer);
// CAN接收回调
void CO_CANrxMsg_t(CO_CANrx_t *rx, const uint32_t ident, const uint8_t *data);
// 定时器相关
uint32_t CO_timerGetTimeUs(void);
void CO_timerSetTimeUs(uint32_t timeUs);
我记得第一次移植时,在CO_CANsend函数里忘了处理发送完成标志,结果CAN报文发不出去,排查了半天。后来发现是硬件发送完成中断没清,导致后续报文被阻塞。
CO_timerGetTimeUs时,直接用SysTick的计数器值。但SysTick是24位的,溢出后时间会回绕。CANopenNode内部依赖连续的时间戳,回绕会导致协议栈崩溃。解决方案是用一个32位的变量来扩展时间范围。
2.3.3 配置对象字典——CANopen的灵魂
对象字典(Object Dictionary)是CANopen的核心。你需要根据你的设备功能,定义支持的对象。比如一个简单的数字量输入设备:
// 对象字典条目示例
const CO_OBJ_TYPE *CO_OD_ROM[] = {
&CO_TYPEOF(CO_OD_1000), // 设备类型
&CO_TYPEOF(CO_OD_1001), // 错误寄存器
&CO_TYPEOF(CO_OD_1017), // 心跳生产者时间
&CO_TYPEOF(CO_OD_6000), // 数字量输入
NULL
};
对象字典的配置很繁琐,但别怕。CANopenNode提供了一个CO_OD.h模板文件,你可以在里面按需修改。我一般先复制一份模板,然后删掉不需要的对象,只保留自己设备要用的。
2.3.4 集成到FreeRTOS任务中
在CANopenNodeTask里,循环调用CANopenNode的主循环函数:
void CANopenNodeTask(void *argument) {
CO_NMT_reset_cmd_t reset = CO_NMT_RESET_COMMAND_RESET_APPLICATION;
CO_ReturnError_t err;
// 初始化CANopenNode
err = CO_CANopenNode_Init(&canopenNode, &canDriver, &timerDriver, &objectDictionary, reset);
if (err != CO_ERROR_NO) {
// 初始化失败处理
Error_Handler();
}
for (;;) {
// 处理CANopen协议栈
CO_CANopenNode_process(&canopenNode);
// 延时,避免CPU占用过高
osDelay(1);
}
}
这里有个细节:osDelay(1)的延时时间要根据你的CAN波特率来调整。250kbps下,1ms的延时是合理的。如果波特率更高,比如1Mbps,建议改成osDelay(0)或者用taskYIELD()让出CPU。
CO_CANopenNode_process前后加两个GPIO翻转,用示波器看任务执行时间。如果执行时间超过1ms,说明协议栈处理不过来,需要优化或者降低CAN负载。
2.4 验证与调试——跑起来才算数
移植完成后,先别急着写应用逻辑。先做几个基本验证:
- 心跳报文:用CAN分析仪看设备是否按配置的时间发送心跳
- SDO读写:尝试读取对象字典1000h,看返回值是否正确
- NMT状态切换:发送NMT命令,看设备状态是否正常切换
如果心跳报文出不来,先检查CAN物理层。用示波器看CAN_H和CAN_L的差分信号,正常应该是2.5V左右。如果电平不对,检查收发器供电和终端电阻。
嗯,到这里,你的开发环境就搭好了。下一章咱们开始写第一个CANopen应用——让一个LED灯通过CAN总线控制亮灭。别看简单,这里面把CANopen的核心机制都走了一遍。