【CAN通信实战】STM32 HAL库开发:从配置到调试
实战指南
📚 学习目标:读完本文,你将掌握STM32 CAN的完整配置方法,能够编写发送和接收代码,理解波特率计算,并能够排查常见问题。适合做毕设和项目开发。
⏱️ 预计学习时间:30-40分钟
📋 前置知识:建议先阅读前四篇文章,了解CAN理论基础。需要基本的STM32 HAL库使用经验。
1. 写在前面
本文是《CAN通信从入门到实战》专栏的第 5 篇,前面四篇主要讲了理论,这一篇开始上手 STM32F407 的实战。本文将以STM32F407 + TJA1042收发器为例,手把手教你如何配置和使用CAN通信。
这是一篇实践文章,实现一个简单的can demo,将bcan1和bcan2通过can总线连接,相互发送can报文。
2. 实验环境
- MCU:野火霸天虎 STM32F407ZGT6 开发板(板载 TJA1042)
- CAN收发器:TJA1042
- 调试工具:FireDAP
- 开发工具:STM32CubeMX + VsCode + CMake
- 开发环境:windows上安装的Ubuntu系统(WSL安装的)
3. 硬件连接
3.1. TJA1042 引脚连接(板子上已经集成好)
无需连接,使用的bcan2(PB12和PB13),为了方便测试,我又另外购买了一款TJA1042的收发器,通过杜邦线与板子连接
| TJA1042引脚 | 连接说明 |
|---|---|
| VCC | 5V电源 |
| GND | 地 |
| TXD | 连接STM32的CAN_TX(如PA12) |
| RXD | 连接STM32的CAN_RX(如PA11) |
| CANH | CAN总线高电平线,与板子上的CAN_H连接 |
| CANL | CAN总线低电平线,与板子上的CAN_L连接 |
| S | 接地下拉(normal模式),悬空/高电平:standby |
| NC | 悬空即可 |
硬件连接

⚠️ 提醒
- 终端电阻:CAN总线两端必须各接一个120Ω终端电阻
- 共地:所有节点必须共地
- 线序:CAN_H和CAN_L不能接反
4. STM32CubeMX 配置
4.1. 使能CAN外设
在Connectivity中找到CAN1/CAN2,选择Activated。
4.2. 配置CAN参数
基本配置:
- Mode:normal
- Prescaler (for Time Quantum):分频系数,用于计算波特率
- Time Quanta in Bit Segment 1:对应PBS1
- Time Quanta in Bit Segment 2:对应PBS2
- Time for one Bit:自动计算,等于 1 + PBS1 + PBS2
波特率计算示例(500kbps,80MHz系统时钟):
假设:
- 系统时钟:42MHz(Can挂载在APB1上,APB1设置的是42MHz)
- 目标波特率:500kbps
- 采样点:80%
计算过程:(后续考虑使用脚本自动实现)
- 一个位时间 = 1 / 500k = 2μs
- 42MHz / Prescaler = CAN时钟频率
- 假设Prescaler = 6,则CAN时钟 = 7MHz
- 一个Tq = 1 / 7MHz = 142.86ns
- 一个位时间需要 = 2μs / 142.86ns = 14 Tq
- 采样点80%,则 PBS1结束位置 = 14 × 0.8 = 11 Tq
- 所以:SS=1, PBS1=10, PBS2=3
CubeMX配置:
- Prescaler: 6
- Time Quanta in Bit Segment 1: 10
- Time Quanta in Bit Segment 2: 3
- ReSynchronization Jump Width: 1
4.3. 中断配置
在NVIC Settings中使能TX,RX,SCE中断。
5. 代码实现
5.1. CAN初始化
/**
* @brief CAN初始化函数
* @param None
* @retval HAL_StatusTypeDef
*/
void MX_CAN1_Init(void)
{
/* 初始化hcan1结构体成员,cubemx自动生成 */
/* USER CODE BEGIN CAN1_Init 2 手动添加*/
/**
* @brief CAN1初始化完成后的处理
* 启动CAN外设并激活接收中断
* 1. 配置CAN过滤器
* 2. 启动CAN外设
* 3. 开启can相关中断
*/
CAN_Filter_Config(&hcan1, 0);
HAL_CAN_Start(&hcan1);
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_TX_MAILBOX_EMPTY | // 发送队列为空时出触发TX中断
CAN_IT_RX_FIFO0_MSG_PENDING | //FIFO 0接收到消息,触发RX中断
CAN_IT_ERROR | //只要发生错误,就会触发中断
CAN_IT_BUSOFF); //发生BUSOFF,触发SCE中断
/* CAN_IT_LAST_ERROR_CODE //最近一次错误代码捕获中断
CAN_IT_ERROR_WARNING //错误警告(错误计数超过 96)
CAN_IT_ERROR_PASSIVE //被动错误(错误计数超过 127)
等,按需开启,这些错误对应的都是SCE中断
*/
/* USER CODE END CAN1_Init 2 */
}
void MX_CAN2_Init(void)
{
//内容与can1类似,不再赘述
}
需要手动实现CAN_Filter_Config的代码,实现过滤器,定义哪些id的can报文可以被收到。
void CAN_Filter_Config(CAN_HandleTypeDef *hcan, uint8_t filterBank) {
CAN_FilterTypeDef sFilterConfig;
// 配置过滤器参数,哪些ID的can报文可以触发接受中断
sFilterConfig.FilterBank = filterBank; // 使用过滤器组0
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式,根据掩码匹配
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 32位过滤器比例
sFilterConfig.FilterIdHigh = 0x0000; // 验证ID高16位,0x0000表示不过滤高位
sFilterConfig.FilterIdLow = 0x0000; // 验证ID低16位,0x0000表示不过滤低位
//并未配置过滤器,所有报文均可收到
sFilterConfig.FilterMaskIdHigh = 0x0000; // 掩码高16位,0x0000表示不检查ID的高16位
sFilterConfig.FilterMaskIdLow = 0x0000; // 掩码低16位,0x0000表示不检查ID的低16位
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; // 过滤后的报文存入FIFO 0
sFilterConfig.FilterActivation = ENABLE; // 启用过滤器
sFilterConfig.SlaveStartFilterBank = 14; // 从过滤器组14开始作为从属CAN
// 应用过滤器配置
if (HAL_CAN_ConfigFilter(canHandle, &sFilterConfig) != HAL_OK) {
Error_Handler(); //后续考虑使用uart输出错误信息
}
}
⚠️注意:此处的FilterBank必须要大于SlaveStartFilterBank,SlaveStartFilterBank的作用是将0-SlaveStartFilterBank分给can1,SlaveStartFilterBank-28分给can2
5.2. CAN发送函数
本demo中我开启了freertos,在**StartDefaultTask**中周期调用发送函数(如果你不用 FreeRTOS,也可以在 while(1) 里用 HAL_Delay 周期调用 CANx_Send_Test,逻辑是一样的。)
void CAN1_Send_Test(void) {
CAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
uint32_t TxMailbox;
uint32_t err;
TxHeader.StdId = 0x123; // 标准ID
TxHeader.RTR = CAN_RTR_DATA; // 数据帧
TxHeader.IDE = CAN_ID_STD; // 标准格式
TxHeader.DLC = 8; // 8字节
TxHeader.TransmitGlobalTime = DISABLE;
// 请求发送,数据会直接回环到接收区
if(HAL_OK != HAL_CAN_AddTxMessage(&hcan1, &TxHeader, TxData, &TxMailbox))
{
Error_Handler();
}
}
void CAN2_Send_Test(void) {
CAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8] = {0x12, 0x23, 0x34, 0x45, 0x56, 0x67, 0x78, 0x89};
uint32_t TxMailbox;
uint32_t err;
TxHeader.StdId = 0x124; // 标准ID
TxHeader.RTR = CAN_RTR_DATA; // 数据帧
TxHeader.IDE = CAN_ID_STD; // 标准格式
TxHeader.DLC = 8; // 8字节
TxHeader.TransmitGlobalTime = DISABLE;
// 请求发送,数据会直接回环到接收区
if(HAL_OK != HAL_CAN_AddTxMessage(&hcan2, &TxHeader, TxData, &TxMailbox))
{
Error_Handler();
}
}
5.3. CAN接收中断方式
接收到总线的报文后,若符合过滤器条件,则会触发RX中断
// 在stm32f4xx_it.c中
void CAN1_RX0_IRQHandler(void)
{
HAL_CAN_IRQHandler(&hcan1);
}
void CAN2_RX0_IRQHandler(void)
{
HAL_CAN_IRQHandler(&hcan2);
}
//最终都会走到此函数中,在HAL库中被定义为weak类型,用户需要根据需求自己实现
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];
if (hcan->Instance == CAN1)
{
// 从 FIFO 0 提取报文
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData);
// 验证数据:如果是我们发的 ID 0x124,就点个灯或串口打印输出
if (RxHeader.StdId == 0x124)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
}
}
if (hcan->Instance == CAN2)
{
// 从 FIFO 0 提取报文
HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData);
// 验证数据:如果是我们发的 ID 0x123,就点个灯或串口打印输出
if (RxHeader.StdId == 0x123)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
}
}
}
5.4. 错误中断
当总线发生错误时,会触发SCE中断
void CAN1_SCE_IRQHandler(void)
{
HAL_CAN_IRQHandler(&hcan1);
}
void CAN2_SCE_IRQHandler(void)
{
HAL_CAN_IRQHandler(&hcan2);
}
//用户自己手动实现weak函数
void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan)
{
// 在这里调用 GetError
uint32_t err = HAL_CAN_GetError(hcan);
// 判断具体错误类型
if (err & HAL_CAN_ERROR_BOF)
{
// 发生了离线错误 (Bus-Off)
// 可以在这里执行恢复逻辑:如 HAL_CAN_Start(hcan);
}
if (err & HAL_CAN_ERROR_STF)
{
// 填充错误 (Stuff Error)
}
// 清除错误信息
}
5.5. 测试结果
在vscode通过FireDAP进行调试,在报文接受函数中添加断点,可以看到分别进入到can1和can2的接受函数,且接收内容符合预期。


我也额外购买了usb-can工具,等工具到了之后会使用专业的工具查看总线数据,进一步验证数据是否在总线上符合预期的发送。后续会更新
新买的usbcan fd工具到达后,连接板子的canh/canl到usbcanfd的can1上,tja1042的canh/canl到usbcanfd的can2上,打开设备设置好波特率,即可看到can1/can2上有can数据,并且在usbcan上发送数据,在板子上也可以接收到。



6. 常见问题排查
6.1. 接收不到数据
检查清单:
- 终端电阻是否正确连接(120Ω)
- CAN_H和CAN_L是否接反
- 波特率是否匹配
- 过滤器配置是否正确
- 是否启动了CAN(HAL_CAN_Start)
6.2. 采样点配置不当
现象:波形正常但CRC错误
解决:调整BS1和BS2的比例,使采样点位于位时间的75%-80%,保证收发双方波特率相同
7. 总结
通过本文,我们学习了:
✅ STM32 CAN配置:CubeMX配置、波特率计算
✅ 发送代码:完整的发送函数实现
✅ 接收代码:轮询和中断两种方式
✅ 常见问题:排查方法和解决方案
✅ 调试技巧:使用工具和添加日志
8. 下篇预告
代码写好了,但调试时还是遇到各种问题?下一篇《【CAN通信实战】调试技巧:那些文档没写的坑》 将分享:
- 示波器/逻辑分析仪的使用
- 常见Bug的排查方法
- 采样点配置的实战经验
- 终端电阻问题的诊断
后续我也会基于此代码为基础,慢慢扩展其功能,包括但不限于:autosar风格分层、增加Json配置和解析脚本、自动解析DBC脚本等,若大家有好的想法,也欢迎留言讨论。
敬请期待!
9. 互动时间
- 你的CAN代码跑通了吗?
- 遇到了什么问题?
- 需要哪些功能的代码示例?
欢迎在评论区留言,我会及时回复!关注我,获取完整代码资源。
专栏目录:
- ✅ 【CAN通信入门】万字长文带你理解CAN:从汽车"减肥"说起
- ✅ 【CAN通信进阶】物理层:两根线如何传输0和1?
- ✅ 【CAN通信进阶】数据链路层(上):帧结构全解析
- ✅ 【CAN通信进阶】数据链路层(下):仲裁机制与错误处理
- ✅ 【CAN通信实战】STM32 HAL库开发:从配置到调试(本文)
更多推荐


所有评论(0)