4. 搭建STM32 CANopenNode开发环境
好,咱们进入第四章。这一章我打算把 STM32 的 HAL 库 CAN 驱动好好封装一下。说白了,就是给 CANopenNode 准备一套趁手的底层接口。你想想看,如果底层驱动写得稀烂,上层协议栈再牛也跑不稳。
我个人习惯是把 CAN 相关的操作全部封装到一个 can_driver.c 和 can_driver.h 里。这样后期移植或者换芯片,只需要改这两个文件,上层代码基本不动。
4.1 HAL库CAN初始化:别漏了时间片
初始化这块,核心就是配置 CAN 的时序参数。STM32 的 CAN 外设用的是 位时间(Bit Time) 的概念,分成 Sync_Seg、Prop_Seg、Phase_Seg1、Phase_Seg2 四段。我刚开始接触的时候,总觉得这东西很玄乎,后来发现其实就是算一个公式:
CAN 波特率 = APB1 时钟 / (BRP + 1) / (tBS1 + tBS2 + 1)
举个例子,如果 APB1 是 42MHz,我想跑 500kbps,那可以这么配:
hcan.Instance = CAN1;
hcan.Init.Prescaler = 6; // BRP = 6 → 分频后 7MHz
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_8TQ; // tBS1 = 8
hcan.Init.TimeSeg2 = CAN_BS2_6TQ; // tBS2 = 6
// 7MHz / (8 + 6 + 1) = 7MHz / 15 ≈ 466kHz → 接近 500k
我曾经踩过的坑: 采样点位置。CAN 总线要求采样点通常在 75%~85% 之间。上面这个配置采样点在 (1 + 8) / (1 + 8 + 6) = 9/15 = 60%,太靠前了。后来我改成 TimeSeg1=10, TimeSeg2=4,采样点 (1+10)/(1+10+4)=11/15≈73%,总线就稳了。
初始化函数里,我一般还会顺手把过滤器设为旁路模式,等后面再单独配置:
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.AutoBusOff = DISABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = ENABLE; // 我习惯开启自动重发
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
4.2 中断配置:别让CPU空转
CAN 通信如果靠轮询,CPU 基本就废了。所以必须用中断。我一般开三个中断:
- 接收中断(FIFO0/FIFO1 消息挂起) —— 来报文了立刻通知我
- 发送完成中断(TX Mailbox 空) —— 发完了我好释放缓冲区
- 错误中断(总线错误、状态变化) —— 出问题了我得知道
在 HAL 库里,使能中断的代码很简单:
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_TX_MAILBOX_EMPTY);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_ERROR);
然后记得在 stm32f4xx_it.c 里把中断服务函数加上:
void CAN1_RX0_IRQHandler(void) {
HAL_CAN_IRQHandler(&hcan);
}
void CAN1_TX_IRQHandler(void) {
HAL_CAN_IRQHandler(&hcan);
}
void CAN1_SCE_IRQHandler(void) {
HAL_CAN_IRQHandler(&hcan);
}
一个小技巧: 我习惯在中断优先级上把 CAN 接收中断设得比发送中断高一点。因为接收不及时会导致 FIFO 溢出丢帧,发送晚一点问题不大。
4.3 发送/接收函数封装:别裸调HAL
直接调 HAL_CAN_AddTxMessage 和 HAL_CAN_GetRxMessage 当然可以,但不够灵活。我封装了两个核心函数:
发送函数
uint8_t CAN_SendMessage(uint32_t id, uint8_t *data, uint8_t len, uint8_t is_ext) {
CAN_TxHeaderTypeDef tx_header;
uint32_t tx_mailbox;
tx_header.StdId = (is_ext) ? 0 : id;
tx_header.ExtId = (is_ext) ? id : 0;
tx_header.IDE = (is_ext) ? CAN_ID_EXT : CAN_ID_STD;
tx_header.RTR = CAN_RTR_DATA;
tx_header.DLC = len;
if (HAL_CAN_AddTxMessage(&hcan, &tx_header, data, &tx_mailbox) != HAL_OK) {
return 0; // 发送失败
}
return 1;
}
这里我加了一个 is_ext 参数,因为 CANopen 标准帧和扩展帧混用的情况很常见。CANopen 的 NMT、SDO 用标准帧,但有些厂家自定义对象字典会用扩展帧。
接收函数(回调方式)
接收我不用轮询,直接在 HAL 的回调里处理:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) {
CAN_RxHeaderTypeDef rx_header;
uint8_t rx_data[8];
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data);
// 这里直接丢给 CANopenNode 的接收处理函数
canopen_node_receive(rx_header.StdId, rx_data, rx_header.DLC);
}
注意: 回调函数里不要做耗时操作。我见过有人直接在回调里做 CRC 校验、写 EEPROM,结果导致中断嵌套过深,系统直接挂掉。回调里只做数据搬运,处理逻辑放到任务循环里。
4.4 过滤器配置:别让CPU被垃圾报文淹没
CAN 总线是广播式的,所有节点都能收到所有报文。如果不加过滤,CPU 会被无关报文频繁打断。STM32 的 CAN 控制器有 28 个过滤器组,每个组可以配置为 列表模式 或 掩码模式。
我个人习惯用 掩码模式,因为 CANopen 的 COB-ID 是有规律的,可以用掩码一次性匹配一组 ID:
CAN_FilterTypeDef filter;
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = 0x0000; // 标准帧 ID 高 16 位
filter.FilterIdLow = 0x0000; // 标准帧 ID 低 16 位
filter.FilterMaskIdHigh = 0x0000; // 掩码全 0 → 接收所有
filter.FilterMaskIdLow = 0x0000;
filter.FilterFIFOAssignment = CAN_RX_FIFO0;
filter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &filter);
上面这个配置是接收所有报文,适合调试阶段。实际项目中,我会根据 CANopen 节点 ID 来设置掩码:
| 功能 | COB-ID 范围 | 掩码策略 |
|---|---|---|
| NMT 管理 | 0x000 | 精确匹配 |
| PDO 通信 | 0x180 ~ 0x1FF | 掩码高 5 位 |
| SDO 通信 | 0x580 ~ 0x5FF | 掩码高 5 位 |
| 心跳报文 | 0x700 ~ 0x77F | 掩码高 5 位 |
我曾经犯过的错: 过滤器配置完忘了调用 HAL_CAN_Start(&hcan),结果中断死活不触发。查了半天才发现 CAN 外设根本没启动。嗯,这种低级错误犯一次就够了。
4.5 整体封装结构图
下面这张图是我自己项目里用的驱动分层结构,你可以参考一下:
你看,这样分层之后,CANopenNode 协议栈完全不知道底层是 STM32 还是别的芯片。它只管调 CAN_SendMessage(),剩下的封装层搞定。这就是我强调的 解耦。
4.6 初始化完整流程
最后,我把整个初始化流程串起来,你照着这个顺序写基本不会错:
- 使能时钟:
__HAL_RCC_CAN1_CLK_ENABLE()以及 GPIO 时钟 - 配置 GPIO: CAN_RX 设为输入浮空,CAN_TX 设为复用推挽输出
- 配置 NVIC: 设置 CAN 中断优先级并使能
- 调用 HAL_CAN_Init(): 传入配置好的时序参数
- 配置过滤器: 根据项目需求设置掩码或列表
- 使能中断通知:
HAL_CAN_ActivateNotification() - 启动 CAN:
HAL_CAN_Start(&hcan)—— 这一步最容易忘 - 注册回调: 在
HAL_CAN_RxFifo0MsgPendingCallback里连接协议栈
我的习惯: 我会在初始化最后加一个自检——发一个空帧给自己,如果能收到,说明 CAN 收发通路是通的。这个自检函数我放在 can_driver_self_test() 里,调试时特别有用。