📚 学习目标:读完本文,你将掌握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引脚连接说明
VCC5V电源
GND地
TXD连接STM32的CAN_TX(如PA12)
RXD连接STM32的CAN_RX(如PA11)
CANHCAN总线高电平线,与板子上的CAN_H连接
CANLCAN总线低电平线,与板子上的CAN_L连接
S接地下拉(normal模式),悬空/高电平:standby
NC悬空即可

硬件连接

画板

⚠️ 提醒

  1. 终端电阻:CAN总线两端必须各接一个120Ω终端电阻
  2. 共地:所有节点必须共地
  3. 线序: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. 一个位时间 = 1 / 500k = 2μs
  2. 42MHz / Prescaler = CAN时钟频率
  3. 假设Prescaler = 6,则CAN时钟 = 7MHz
  4. 一个Tq = 1 / 7MHz = 142.86ns
  5. 一个位时间需要 = 2μs / 142.86ns = 14 Tq
  6. 采样点80%,则 PBS1结束位置 = 14 × 0.8 = 11 Tq
  7. 所以: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代码跑通了吗?
  • 遇到了什么问题?
  • 需要哪些功能的代码示例?

欢迎在评论区留言,我会及时回复!关注我,获取完整代码资源。


专栏目录:

  1. ✅ 【CAN通信入门】万字长文带你理解CAN:从汽车"减肥"说起
  2. ✅ 【CAN通信进阶】物理层:两根线如何传输0和1?
  3. ✅ 【CAN通信进阶】数据链路层(上):帧结构全解析
  4. ✅ 【CAN通信进阶】数据链路层(下):仲裁机制与错误处理
  5. ✅ 【CAN通信实战】STM32 HAL库开发:从配置到调试(本文)
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐