3. 开发环境搭建:STM32CubeMX配置、Keil/IAR工程建立、CANopenNode源码移植
说实话,很多初学者卡在CANopen开发的第一步,不是协议看不懂,而是环境没搭对。我见过有人花了一周时间,最后发现是时钟配置错了,CAN怎么都收不到数据。所以这一章,咱们把地基打牢。
我会带着你走完三个步骤:CubeMX生成底层代码 → 建立Keil/IAR工程 → 移植CANopenNode源码。每一步我都会告诉你哪些坑可以绕过去。
3.1 整体流程概览
先看一张图,把整个搭建过程串起来。你心里有个谱,后面操作就不容易乱。
3.2 STM32CubeMX配置要点
我个人习惯用CubeMX 6.x以上版本,界面更友好。打开软件后,先选好你的芯片型号。我以STM32F407VGT6为例,这个型号在CANopen开发中很常见。
3.2.1 时钟树配置
CAN外设对时钟精度要求很高。CANopen协议规定,位时间误差不能超过1.5%。所以时钟源必须选准。
| 时钟项 | 推荐配置 | 说明 |
|---|---|---|
| HSE | 8MHz 外部晶振 | 精度高,温度漂移小 |
| PLL | 168MHz (HCLK) | F407最高主频 |
| APB1 | 42MHz | CAN外设挂载在APB1上 |
| APB2 | 84MHz | GPIO、TIM等 |
3.2.2 CAN外设配置
在Pinout & Configuration页面,找到Connectivity → CAN1。勾选Activate,然后配置参数。
这里有一个关键参数:Bit Timings。CANopen默认通信速率为125kbps、250kbps或1Mbps。我建议你从125kbps开始,兼容性最好。
以125kbps为例,APB1时钟42MHz,需要满足:
CAN时钟 = APB1 / Prescaler
位时间 = (Sync_Seg + BS1 + BS2) * CAN时钟周期
波特率 = 1 / 位时间
推荐参数:
Prescaler = 12
Sync_Seg = 1
BS1 = 8
BS2 = 7
采样点 = (1 + 8) / (1 + 8 + 7) = 56.25% → 接近CANopen推荐的56.3%
3.2.3 中断与GPIO配置
CANopenNode需要两个中断:
- CAN接收中断:RX0中断,优先级设为5(不要用0,留给系统滴答定时器)
- 定时器中断:用于CANopen心跳和同步报文,我习惯用TIM3,优先级设为6
GPIO方面,CAN_TX和CAN_RX引脚需要配置为复用功能。以F407为例:
- CAN1_TX → PB9 (AF9)
- CAN1_RX → PB8 (AF9)
别忘了在GPIO设置里把输出速度调到High,否则高速通信时会丢包。
3.3 生成Keil/IAR工程
CubeMX配置完成后,点击Project Manager选项卡。这里有两个选择:
| IDE | Toolchain/IDE | 我的建议 |
|---|---|---|
| Keil MDK-ARM | MDK-ARM V5.32+ | 国内用户多,资料好找 |
| IAR EWARM | EWARM V8.50+ | 编译优化更好,调试功能强 |
我个人更倾向Keil,因为CANopenNode的官方示例大部分是基于Keil的。但如果你用IAR,也没问题,后面移植步骤完全一样。
生成代码前,记得勾选:
- Generate peripheral initialization as a pair of '.c/.h' files per peripheral
- Copy all used libraries into the project folder
点击Generate Code,等几秒钟,工程就出来了。
3.4 CANopenNode源码移植
这是本章的重头戏。CANopenNode是一个开源的CANopen协议栈,代码结构清晰,但初次接触会觉得文件很多。别怕,我带你理一遍。
3.4.1 源码目录结构
从GitHub下载CANopenNode源码后,你会看到这样的目录:
CANopenNode/
├── CANopen.c # 主协议栈文件
├── CANopen.h
├── CO_driver.c # 硬件抽象层(需要移植)
├── CO_driver.h
├── CO_Emergency.c # 紧急报文
├── CO_NMT_Heartbeat.c # 网络管理与心跳
├── CO_PDO.c # 过程数据对象
├── CO_SDO.c # 服务数据对象
├── CO_SDOserver.c
├── CO_SYNC.c # 同步报文
├── CO_TIME.c # 时间戳
├── CO_LSS.c # 层设置服务(可选)
├── 301/ # CiA 301标准相关
├── 302/ # CiA 302标准相关
├── 305/ # CiA 305标准相关
└── example/ # 示例工程
你需要移植的核心文件只有两个:CO_driver.c 和 CO_driver.h。其他文件直接复制到工程里,不需要修改。
3.4.2 移植步骤
我总结了一个四步法,你照着做就行:
- 复制源码:把CANopenNode根目录下的所有.c和.h文件复制到你的工程文件夹,建议放在Middlewares/CANopenNode/下。
- 添加文件到工程:在Keil里,把.c文件全部添加到工程组里。我习惯新建一个"CANopen"组,专门放这些文件。
- 配置包含路径:在C/C++选项卡里,添加CANopenNode的路径到Include Paths。
- 修改CO_driver.c:这是唯一需要你动手改的文件。
3.4.3 CO_driver.c关键函数实现
我挑几个最重要的函数,给你看看具体怎么写:
// 1. CAN发送函数
int16_t CO_CANsend(CO_CANmodule_t *CANmodule, CO_CANtx_t *buffer) {
CAN_TxHeaderTypeDef txHeader;
uint32_t txMailbox;
txHeader.StdId = buffer->ident;
txHeader.ExtId = 0;
txHeader.IDE = CAN_ID_STD;
txHeader.RTR = CAN_RTR_DATA;
txHeader.DLC = buffer->DLC;
txHeader.TransmitGlobalTime = DISABLE;
// 调用HAL库发送
if (HAL_CAN_AddTxMessage(&hcan1, &txHeader, buffer->data, &txMailbox) != HAL_OK) {
return -1; // 发送失败
}
return 0; // 成功
}
// 2. CAN接收回调(在CAN接收中断中调用)
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef rxHeader;
uint8_t rxData[8];
if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData) == HAL_OK) {
// 构造CANopenNode需要的接收结构体
CO_CANrxMsg_t msg;
msg.ident = rxHeader.StdId;
msg.DLC = rxHeader.DLC;
memcpy(msg.data, rxData, 8);
// 调用CANopenNode的接收处理函数
CO_CANrxBufferRead(&msg);
}
}
// 3. 定时器初始化(用于心跳和同步)
void CO_TIM_init(CO_TIM_t *TIM) {
// 使用TIM3,1ms中断一次
htim3.Instance = TIM3;
htim3.Init.Prescaler = 168 - 1; // 168MHz / 168 = 1MHz
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = 1000 - 1; // 1MHz / 1000 = 1ms
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim3);
HAL_TIM_Base_Start_IT(&htim3);
}
3.4.4 主函数调用流程
移植完成后,主函数的调用顺序是这样的:
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN1_Init();
MX_TIM3_Init();
// CANopenNode初始化
CO_ReturnError_t err;
CO_NMT_reset_cmd_t reset = CO_RESET_NOT;
CO_CANmodule_t CANmodule;
CO_NMT_t NMT;
CO_SDO_t SDO;
CO_PDO_t PDO;
CO_EM_t EM;
CO_SYNC_t SYNC;
// 调用初始化函数
err = CO_init(&CANmodule, &NMT, &SDO, &PDO, &EM, &SYNC,
&reset, 0x01); // 0x01是节点ID
if (err != CO_ERROR_NO) {
Error_Handler(); // 初始化失败
}
// 启动CAN
HAL_CAN_Start(&hcan1);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
// 主循环
while(1) {
// CANopenNode主循环处理
CO_process(&CANmodule, &NMT, &SDO, &PDO, &EM, &SYNC);
// 你的应用代码
// ...
delay_ms(1); // 至少1ms循环一次
}
}
3.5 验证移植是否成功
工程编译通过后,怎么知道移植对了?我教你一个快速验证方法:
- 烧录程序,用CAN分析仪(比如PCAN-USB或周立功)监听总线
- 上电后,应该能看到节点发送Boot-up报文(COB-ID = 0x700 + NodeID)
- 如果配置了心跳,还能看到周期性的心跳报文(COB-ID = 0x700 + NodeID)
看到这两个报文,说明你的移植成功了。如果没看到,先检查硬件连接,再用示波器看CAN_TX引脚有没有波形。
3.6 本章小结
到这一步,你已经有了一个完整的CANopenNode工程模板。这个模板会贯穿整个课程,后面的实验都基于它。我建议你把这个工程备份一份,万一改坏了还能恢复。
嗯,环境搭建确实有点繁琐,但这是最值得花时间的一步。地基打牢了,后面写应用代码就顺风顺水。