从零搭建STM32 CAN总线网络:Keil5烧录与多节点通信实战指南
你有没有遇到过这样的场景?在做一个分布式控制系统时,多个设备之间需要实时交换数据,但串口通信距离短、抗干扰差,以太网又太复杂、成本高。这时候,CAN总线就成了解决问题的“黄金方案”。
本文将带你一步步使用Keil5完成STM32固件烧录,并配置片上CAN控制器,最终实现多个STM32节点通过CAN总线稳定通信。整个过程不依赖复杂的操作系统或协议栈,适合嵌入式初学者和中级开发者快速上手。
我们不会堆砌术语,而是像一位老工程师手把手教你:怎么接线、怎么写代码、怎么调通第一个CAN帧,以及那些只有踩过坑才知道的“隐藏技巧”。
为什么是STM32 + Keil5 + CAN?
先说结论:这套组合在中小规模工业控制中几乎是“性价比之王”。
- STM32多数型号自带硬件CAN控制器,无需外加协处理器;
- Keil5(MDK)对ARM Cortex-M系列支持完善,调试体验流畅;
- CAN总线支持多主、抗干扰强、传输距离远(最长可达1km),非常适合工厂、车载等复杂环境。
更重要的是,这三个技术点可以无缝衔接——你在Keil里写的代码,一键下载到STM32后就能驱动CAN收发器发送第一帧数据。
那么问题来了:如何让这一切真正跑起来?
第一步:用Keil5把程序烧进STM32
别小看这一步,很多初学者卡在这里——编译通过了,但板子没反应。其实关键不在代码,而在烧录配置是否正确。
烧录的本质是什么?
简单说,就是把你写的C语言程序变成一串二进制机器码,写进STM32的Flash里,并让它从复位向量开始执行。
这个过程依赖三个要素:
1. 正确的工程设置(芯片型号、时钟)
2. 可靠的物理连接(ST-Link or J-Link)
3. 合理的启动模式(BOOT引脚)
实操流程(基于ST-Link + STM32F103C8T6)
硬件连接
- ST-Link → STM32 板- SWDIO → PA13
- SWCLK → PA14
- GND → GND
- 3.3V → 3.3V(可选供电)
Keil5工程配置
- Project → Options for Target → Debug- 选择
ST-Link Debugger - Utilities → Settings
- 勾选 “Use Memory Layout from Target Dialog”
- 选择编程算法(如 STM32F1xx 64KB Flash)
- 选择
关键检查项
- ✅ BOOT0 = 0(从主Flash启动)
- ✅ 目标板供电正常(推荐外部稳压源)
- ✅ 没有短路或反接
⚠️ 常见坑点:如果你发现“No target connected”,大概率是电压不匹配或者SWD线路接触不良。不要强行烧录!
如何验证烧录成功?
最简单的办法是在main()函数开头点亮一个LED:
int main(void) { HAL_Init(); SystemClock_Config(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_5; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &gpio); while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); HAL_Delay(500); // 每半秒闪一次 } }只要LED开始闪烁,说明程序已经成功运行 —— 你的开发链路打通了第一步!
第二步:让STM32说出“CAN语言”
现在轮到核心功能:配置STM32的CAN控制器。
STM32的CAN模块叫bxCAN(Basic Extended CAN),它不是软件模拟,而是独立运行的硬件外设。这意味着一旦初始化完成,发送和接收几乎不需要CPU干预。
CAN通信的核心参数:波特率怎么算?
这是最容易出错的地方。所有节点必须完全一致地配置位时间,否则谁也收不到谁的数据。
假设我们要设置500kbps波特率,APB1时钟为36MHz(常见于STM32F1系列),该怎么设置?
计算公式:
Bit Rate = APB1_CLK / (Prescaler × (1 + BS1 + BS2))其中:
- Prescaler:分频系数
- BS1(Time Segment 1):传播段 + 相位缓冲段1
- BS2(Time Segment 2):相位缓冲段2
- 同步跳转宽度 SJW ≤ min(BS1, BS2)
示例配置(500kbps):
| 参数 | 值 |
|---|---|
| APB1_CLK | 36 MHz |
| Prescaler | 9 |
| BS1 | 8 Tq |
| BS2 | 7 Tq |
| 总位时间 | 1 + 8 + 7 = 16 Tq |
| 实际波特率 | 36,000,000 / (9×16) = 500,000 bps ✅ |
这些值对应HAL库中的结构体成员:
hcan.Init.Prescaler = 9; hcan.Init.BS1 = CAN_BS1_8TQ; hcan.Init.BS2 = CAN_BS2_7TQ; hcan.Init.SJW = CAN_SJW_1TQ;💡 小贴士:采样点建议设在75%~85%之间(本例为(1+8)/16 ≈ 56%,偏低)。若通信不稳定,可适当增加BS1长度。
第三步:编写CAN初始化代码(基于HAL库)
下面是一段经过验证的CAN1初始化函数,适用于STM32F1系列:
CAN_HandleTypeDef hcan; void MX_CAN_Init(void) { hcan.Instance = CAN1; hcan.Init.Mode = CAN_MODE_NORMAL; // 正常模式 hcan.Init.AutoBusOff = ENABLE; // 自动离线恢复 hcan.Init.AutoWakeUp = DISABLE; hcan.Init.TimeTriggeredMode = DISABLE; hcan.Init.ReceiveFifoLocked = DISABLE; hcan.Init.TransmitFifoPriority = DISABLE; // 波特率设置(500kbps) hcan.Init.Prescaler = 9; hcan.Init.SJW = CAN_SJW_1TQ; hcan.Init.BS1 = CAN_BS1_8TQ; hcan.Init.BS2 = CAN_BS2_7TQ; if (HAL_CAN_Init(&hcan) != HAL_OK) { Error_Handler(); } // 配置过滤器:接收所有标准帧 CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank = 0; sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh = 0x0000; // ID掩码高位 sFilterConfig.FilterIdLow = 0x0000; sFilterConfig.FilterMaskIdHigh = 0x0000; // 掩码全0 → 全部通过 sFilterConfig.FilterMaskIdLow = 0x0000; sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; sFilterConfig.FilterActivation = ENABLE; if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK) { Error_Handler(); } // 启动CAN并启用中断 if (HAL_CAN_Start(&hcan) != HAL_OK) { Error_Handler(); } if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) { Error_Handler(); } }🔍 关键解读:
-FilterMaskId=0表示“不管ID是多少都接收”,适合调试阶段。
- 使用FIFO0中断而非轮询,避免浪费CPU资源。
- 开启自动离线恢复(ABOM),防止总线异常导致节点锁死。
别忘了在NVIC中使能CAN中断:
HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 0, 0); HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);并在中断服务函数中处理接收事件:
void USB_LP_CAN1_RX0_IRQHandler(void) { HAL_CAN_IRQHandler(&hcan); } // 回调函数(需定义) 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) { // 在这里处理收到的数据! if (rxHeader.StdId == 0x101 && rxData[0] == 0x01) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 点亮LED } } }第四步:组网!构建一个多节点CAN系统
现在我们已经有能力发送和接收CAN帧了,下一步是让多个STM32节点互联通信。
物理连接要点
- 所有节点共用一对差分线:CAN_H和CAN_L
- 使用TJA1050或MCP2551等CAN收发器芯片进行电平转换
- 总线两端各加一个120Ω终端电阻,中间节点不接!
[Node A] -----(CAN_H/L)------ [Node B] │ 120Ω │ 120Ω │ (GND)❗ 错误示范:有人为了“更可靠”在每个节点都加上120Ω电阻,结果总阻抗严重失配,通信失败。
软件层面的设计思路
- 统一波特率:所有节点必须使用相同的Prescaler、BS1、BS2。
- 规划ID空间:比如:
- 0x100~0x1FF:传感器数据
- 0x200~0x2FF:控制命令
- 0x300~0x3FF:心跳包/状态上报 - 避免频繁重传:设置合理的NART(No Automatic Retransmission)策略。
发送一帧数据试试看
void send_temperature(float temp) { CAN_TxHeaderTypeDef txHeader; uint8_t txData[2]; uint32_t txMailbox; txHeader.StdId = 0x101; // 标准ID txHeader.RTR = CAN_RTR_DATA; // 数据帧 txHeader.IDE = CAN_ID_STD; // 标准帧 txHeader.DLC = 2; // 2字节数据 txHeader.TransmitGlobalTime = DISABLE; int value = (int)(temp * 10); // 25.3°C → 253 txData[0] = value >> 8; txData[1] = value & 0xFF; if (HAL_CAN_AddTxMessage(&hcan, &txHeader, txData, &txMailbox) != HAL_OK) { Error_Handler(); } }当你调用send_temperature(25.3f),其他节点就会收到这条消息,并可根据ID做出响应。
实战案例:三节点温控系统
设想一个简单应用场景:
- Node A:采集温度(每隔1秒发一次)
- Node B:根据温度决定是否加热
- Node C:HMI显示当前温度和状态
它们都连接在同一根CAN总线上,没有任何主控协调。
Node A(温度采集)
while (1) { float temp = read_ds18b20(); // 假设读取温度 send_temperature(temp); HAL_Delay(1000); }Node B(加热控制)
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { ... if (rxHeader.StdId == 0x101) { float temp = ((rxData[0] << 8) | rxData[1]) / 10.0f; if (temp < 20.0f) { send_heater_cmd(HEATER_ON); // 发送加热指令 } } }Node C(显示界面)
// 同时监听温度和加热状态 if (rxHeader.StdId == 0x101) update_temp_display(temp); if (rxHeader.StdId == 0x201) update_heater_icon(status);整个系统去中心化,任何一个节点故障不影响其余部分运行,可靠性极高。
常见问题与调试技巧
Q1:为什么收不到任何消息?
- ✅ 检查终端电阻是否只在两端接入
- ✅ 所有节点波特率配置是否完全一致
- ✅ CAN收发器VCC是否正常供电
- ✅ 是否启用了接收中断并正确注册回调
Q2:偶尔丢帧怎么办?
- 提高采样点位置(调整BS1/BS2比例)
- 检查是否有强干扰源靠近通信线(如电机、继电器)
- 使用屏蔽双绞线(STP)
Q3:节点突然“失联”?
- 查看TEC(发送错误计数器)是否超过96(警告级别)
- REC > 127 可能已进入“被动错误”状态
- 加入状态监控任务定期打印CAN_ErrorCodeGet()
调试利器推荐
- PCAN-View或CANalyzer:抓取总线数据,分析通信流程
- 逻辑分析仪:观察CAN_H/L波形质量
- 串口透传:将CAN报文转发到串口,方便日志输出
写在最后:这套技术能走多远?
你可能会问:“这只是一个简单的CAN通信,有什么特别的?”
它的特别之处在于:简单、可靠、可扩展。
- 不需要RTOS也能实现实时通信;
- 几个GPIO + 一片TJA1050就能组网;
- 即使未来升级为CAN FD,底层架构依然可用。
无论是做毕业设计、工业改造,还是开发智能农业设备,这套“Keil5 + STM32 + CAN”的组合都能成为你手中的一张王牌。
掌握它,你就掌握了在复杂电磁环境中构建稳健通信系统的底层能力。
如果你正在尝试搭建自己的CAN网络,欢迎在评论区留言交流——我们一起解决每一个“收不到帧”的深夜难题。