STM32中的CAN总线详解:从原理到实战

前言:为什么CAN总线是嵌入式通信的"硬通货"?

在嵌入式通信领域,CAN(Controller Area Network)总线凭借其高可靠性实时性多节点通信能力,成为汽车电子、工业控制、智能设备等领域的"标配"。想象一下:一辆汽车中有几十个ECU(电子控制单元),从发动机控制到车窗调节,都需要实时交换数据------CAN总线正是为这种多节点、高干扰环境设计的通信协议。

STM32几乎全系列都集成了高性能CAN控制器(bxCAN),支持标准帧/扩展帧、中断/DMA传输、灵活的滤波功能,完美适配工业级应用。本文将从CAN总线的基本原理讲起,深入解析STM32的CAN外设结构、配置方法、通信流程和实战案例,帮助你彻底掌握STM32 CAN通信开发。

一、CAN总线基础:为什么它能在恶劣环境中可靠通信?

在学习STM32的CAN外设前,我们需要先理解CAN总线的核心特性------这是掌握后续内容的基础。

1.1 CAN总线的核心优势

CAN总线由博世公司在1980年代为汽车电子开发,经过30多年发展,已成为国际标准(ISO 11898),其核心优势包括:

  • 多主通信:总线上的每个节点都可主动发送数据,无需中央控制器,避免单点故障;
  • 非破坏性仲裁:多个节点同时发送时,通过ID优先级仲裁,优先级高的节点继续发送,低优先级的自动退让,不会破坏数据;
  • 差分信号传输:通过CAN_H和CAN_L两根线传输差分信号,抗电磁干扰能力极强(适合工业和汽车环境);
  • 远距离传输:速率125kbps时传输距离可达500m,满足大多数工业场景;
  • 错误检测与自动重传:内置CRC校验、位填充、应答机制,确保数据可靠传输,错误帧会自动重传。

1.2 CAN总线的基本概念

(1)帧结构:CAN数据的"包装格式"

CAN总线通过"帧"传输数据,最常用的是数据帧(用于传输有效数据),其结构如下(标准帧):

字段 长度(位) 功能描述
起始位 1 帧开始标志(低电平)
仲裁 11 包含标准ID(11位),用于仲裁
控制场 6 包含数据长度(DLC,0~8字节)
数据场 0~64 有效数据(0~8字节,扩展帧支持更多)
CRC场 16 循环冗余校验,检测数据错误
应答场 2 接收节点确认收到正确数据
结束位 7 帧结束标志(高电平)

扩展帧与标准帧的区别是仲裁场包含29位ID(11位标准ID+18位扩展ID),支持更多节点和更复杂的ID规划。

(2)位时序:如何保证不同节点的同步?

CAN总线是异步通信,节点间没有统一的时钟线,通过位时序实现同步。每个位被分为4个时间段:

  • 同步段(SS):用于同步各节点时钟,长度1TQ(Time Quantum,时间量子);
  • 传播段(PS):补偿信号传输延迟,长度1~8TQ;
  • 相位缓冲段1(PBS1):可延长,用于重同步,长度1~8TQ;
  • 相位缓冲段2(PBS2):等于PBS1或信号传播时间,长度1~8TQ。

波特率计算:波特率 = 1 / (总TQ),其中总TQ = SS + PS + PBS1 + PBS2(通常总TQ=8~25)。

例如:STM32的CAN时钟为36MHz,若总TQ=18,则波特率=36MHz / 18 = 2Mbps。

1.3 CAN节点的硬件组成

一个完整的CAN节点包括:

  • MCU中的CAN控制器(如STM32的bxCAN):负责帧的组装、发送、接收和错误检测;
  • CAN收发器(如SN65HVD230):将控制器输出的TTL电平转换为CAN总线的差分信号(CAN_H/CAN_L);
  • 终端电阻(120Ω):接在总线两端,匹配阻抗,防止信号反射。

典型电路 :STM32的CAN_TX(如PB6)和CAN_RX(如PB5)连接到SN65HVD230的TXD和RXD,SN65HVD230的CAN_H和CAN_L接总线,两端各接120Ω电阻。

二、STM32的CAN外设:bxCAN控制器的强大之处

STM32的CAN控制器称为bxCAN(Basic Extended CAN),支持CAN 2.0A/B标准,不同系列(F1/F4/L4)的CAN外设功能略有差异,但核心结构一致。

2.1 bxCAN控制器的核心特性

  • 支持标准帧(11位ID)和扩展帧(29位ID)
  • 3个发送邮箱:可缓存3帧待发送数据,支持优先级发送;
  • 2个接收FIFO:每个FIFO有3级深度,可缓存3帧接收数据,减轻CPU负担;
  • 灵活的滤波功能:14个滤波器组,支持屏蔽位模式和列表模式,精准过滤目标报文;
  • 多种中断源:发送完成、接收FIFO满、错误警告等,支持中断和DMA传输;
  • 总线错误管理:检测总线错误(位错误、CRC错误等),自动进入错误状态。

2.2 bxCAN的硬件结构

理解bxCAN的结构有助于后续配置,核心模块包括:

(1)发送部分:3个发送邮箱

发送邮箱(Tx Mailbox)是发送数据的"缓冲区",每个邮箱包含:

  • 标识符寄存器(TXID):存储标准/扩展ID;
  • 数据长度和数据寄存器(TXDLR、TXDATA):存储数据长度和有效数据;
  • 控制寄存器(TXCTRL):配置发送优先级、帧类型等。

发送流程:当邮箱状态为"空"时,CPU填充邮箱数据,设置"发送请求",CAN控制器会自动 arbitration(仲裁)并发送,发送完成后邮箱状态变为"空"。

(2)接收部分:2个FIFO + 14个滤波器
  • 接收FIFO:FIFO0和FIFO1,用于缓存接收的有效报文。当FIFO中的报文数达到阈值(如3帧),会触发中断;
  • 滤波器组:共14个(STM32F103),每个滤波器可配置为屏蔽位模式(按ID掩码过滤)或列表模式(精确匹配ID),过滤后的报文才会存入FIFO。
(3)波特率发生器

根据输入时钟(APB1时钟,最高36MHz for F1)和配置的位时序参数(预分频器、同步段、相位段),生成CAN总线需要的波特率。

三、STM32 CAN配置步骤:从CubeMX到代码实现

本节以STM32F103C8T6STM32CubeMX 6.6.0为例,结合HAL库,详细讲解CAN通信的配置流程。

3.1 硬件准备

  • 开发板:STM32F103C8T6最小系统板;

  • CAN收发器:TJA1050模块(带120Ω终端电阻,可通过跳线选择);

  • 接线:STM32的PA11(CAN_RX)接TJA1050的RXD,PA12(CAN_TX)接TJA1050的TXD,TJA1050的CAN_H和CAN_L接总线(若单节点测试,可短接CAN_H和CAN_L,或接2个节点形成回路)。

3.2 CubeMX配置步骤

步骤1:新建工程,选择芯片

打开CubeMX,搜索"STM32F103C8T6",创建新工程。

步骤2:配置时钟树

CAN外设挂载在APB1总线上,需确保APB1时钟正确(最高36MHz for F1):

  1. 配置RCC:HSE选择"Crystal/Ceramic Resonator"(8MHz外部晶振);
  2. 配置PLL:PLLMUL=×9,使系统时钟=72MHz;
  3. APB1 Prescaler=×2,使APB1时钟=36MHz(满足CAN时钟需求)。
步骤3:配置CAN外设
  1. 引脚配置:在Pinout视图中,将PA11配置为"CAN_RX",PA12配置为"CAN_TX";
  2. 配置CAN模式:在Configuration→Connectivity→CAN中,设置:
    • Mode:"Normal"(正常模式,收发数据)或"Loopback"(回环模式,用于自测,发送的报文会自己接收);
    • Prescaler(预分频器):根据波特率需求设置,例如"6"(后续计算波特率);
    • 位时序参数(Bit Timing):
      • Sync Jump Width (SJW):"1tq";
      • Time Segment 1 (BS1):"8tq";
      • Time Segment 2 (BS2):"3tq";
        (总TQ = Prescaler × (SJW+BS1+BS2) = 6×(1+8+3)=72 → 波特率=36MHz/72=500kbps)
步骤4:配置中断(可选)

若需要通过中断处理收发,需配置NVIC:

  1. 在Configuration→NVIC中,勾选"CAN_RX0_IRQn"(FIFO0接收中断)和"CAN_TX_IRQn"(发送完成中断);
  2. 设置中断优先级(如抢占优先级1,子优先级0)。
步骤5:生成代码

设置工程路径和IDE(如MDK-ARM),点击"Generate Code"生成初始化代码。

3.3 HAL库CAN核心函数解析

生成的代码中,CAN相关核心函数位于can.cstm32f1xx_hal_can.h,主要包括:

(1)初始化函数:MX_CAN_Init()

自动生成的初始化函数,配置CAN模式、波特率、滤波器等(后续需手动完善滤波器配置):

c 复制代码
CAN_HandleTypeDef hcan;

static void MX_CAN_Init(void)
{
  hcan.Instance = CAN1;
  hcan.Init.Prescaler = 6;
  hcan.Init.Mode = CAN_MODE_NORMAL; // 正常模式
  hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
  hcan.Init.TimeSeg1 = CAN_BS1_8TQ;
  hcan.Init.TimeSeg2 = CAN_BS2_3TQ;
  hcan.Init.TimeTriggeredMode = DISABLE;
  hcan.Init.AutoBusOff = ENABLE; // 自动退出总线关闭状态
  hcan.Init.AutoRetransmission = ENABLE; // 自动重传
  hcan.Init.ReceiveFifoLocked = DISABLE;
  hcan.Init.TransmitFifoPriority = DISABLE;
  if (HAL_CAN_Init(&hcan) != HAL_OK)
  {
    Error_Handler();
  }
}
(2)滤波器配置函数:CAN_Filter_Config()

滤波器需要手动配置,用于过滤目标ID的报文,示例:

c 复制代码
void CAN_Filter_Config(void)
{
  CAN_FilterTypeDef can_filter_st;
  can_filter_st.FilterActivation = ENABLE; // 使能滤波器
  can_filter_st.FilterMode = CAN_FILTERMODE_IDMASK; // 屏蔽位模式
  can_filter_st.FilterScale = CAN_FILTERSCALE_32BIT; // 32位滤波器
  
  // 配置滤波器ID和掩码(只接收ID=0x123的标准帧)
  can_filter_st.FilterIdHigh = 0x123 << 5; // 标准ID的高16位(左移5位是因为标准ID占11位)
  can_filter_st.FilterIdLow = 0x0000;
  can_filter_st.FilterMaskIdHigh = 0xFFF << 5; // 掩码高16位(全1表示严格匹配)
  can_filter_st.FilterMaskIdLow = 0x0000;
  
  can_filter_st.FilterBank = 0; // 使用第0个滤波器组
  can_filter_st.FilterFIFOAssignment = CAN_FILTER_FIFO0; // 匹配的报文存入FIFO0
  if(HAL_CAN_ConfigFilter(&hcan, &can_filter_st) != HAL_OK)
  {
    Error_Handler();
  }
}

滤波器配置说明

  • 屏蔽位模式:FilterId是目标ID,FilterMaskId是掩码,掩码为1的位必须严格匹配,为0的位可忽略;
  • 列表模式:FilterId是需要匹配的ID列表,只有完全匹配的ID才会被接收;
  • 32位/16位模式:32位模式可同时过滤标准帧和扩展帧,16位模式适合单独过滤。
(3)发送函数:CAN_Send_Message()

封装发送流程,填充报文并启动发送:

c 复制代码
uint8_t CAN_Send_Message(uint32_t id, uint8_t *data, uint8_t len)
{
  CAN_TxHeaderTypeDef tx_header;
  uint8_t tx_mailbox; // 存储使用的发送邮箱编号
  
  // 配置发送头部
  tx_header.StdId = id; // 标准ID
  tx_header.ExtId = 0; // 扩展ID(不使用)
  tx_header.RTR = CAN_RTR_DATA; // 数据帧
  tx_header.IDE = CAN_ID_STD; // 标准帧
  tx_header.DLC = len; // 数据长度(0~8)
  tx_header.TransmitGlobalTime = DISABLE;
  
  // 等待发送邮箱空闲
  if(HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0)
  {
    return 1; // 邮箱满,发送失败
  }
  
  // 发送数据
  if(HAL_CAN_AddTxMessage(&hcan, &tx_header, data, &tx_mailbox) != HAL_OK)
  {
    return 2; // 发送失败
  }
  
  return 0; // 发送成功
}
(4)接收函数:CAN_Receive_Message()

从FIFO接收报文(查询方式):

c 复制代码
uint8_t CAN_Receive_Message(uint32_t *id, uint8_t *data, uint8_t *len)
{
  CAN_RxHeaderTypeDef rx_header;
  
  // 检查FIFO0是否有数据
  if(HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &rx_header, data) != HAL_OK)
  {
    return 1; // 接收失败
  }
  
  *id = rx_header.StdId; // 获取接收的ID
  *len = rx_header.DLC; // 获取数据长度
  return 0; // 接收成功
}
(5)中断服务函数

若使用中断接收,需实现中断服务函数和回调函数:

c 复制代码
// FIFO0接收中断服务程序
void CAN1_RX0_IRQHandler(void)
{
  HAL_CAN_IRQHandler(&hcan);
}

// 接收回调函数
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
  uint32_t id;
  uint8_t data[8], len;
  
  // 接收数据
  if(CAN_Receive_Message(&id, data, &len) == 0)
  {
    // 处理接收的数据(如打印)
    printf("收到ID:0x%X, 数据:", id);
    for(uint8_t i=0; i<len; i++)
    {
      printf("%02X ", data[i]);
    }
    printf("\r\n");
  }
}

四、实战案例:STM32 CAN节点通信测试

本节通过两个案例(回环测试和双节点通信),验证CAN配置的正确性。

4.1 回环模式测试(单节点)

回环模式下,STM32发送的CAN报文会被自己接收,无需外部节点,适合调试:

步骤1:修改CAN模式为回环

MX_CAN_Init()中,将hcan.Init.Mode改为CAN_MODE_LOOPBACK

步骤2:主函数代码
c 复制代码
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init(); // 初始化串口(用于打印信息)
  MX_CAN_Init();
  
  // 配置滤波器
  CAN_Filter_Config();
  
  // 启动CAN
  if(HAL_CAN_Start(&hcan) != HAL_OK)
  {
    Error_Handler();
  }
  
  // 使能FIFO0接收中断
  if(HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
  {
    Error_Handler();
  }
  
  uint8_t send_data[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
  uint8_t send_cnt = 0;
  
  while (1)
  {
    // 每1秒发送一次数据
    if(send_cnt >= 100)
    {
      if(CAN_Send_Message(0x123, send_data, 8) == 0)
      {
        printf("发送成功: ID=0x123, 数据:11 22 33 44 55 66 77 88\r\n");
      }
      else
      {
        printf("发送失败\r\n");
      }
      send_cnt = 0;
    }
    HAL_Delay(10);
    send_cnt++;
  }
}
测试结果

程序运行后,串口会打印"发送成功"和"收到数据"的信息,说明回环通信正常。

4.2 双节点通信(多节点)

准备两个STM32节点(Node A和Node B),配置相同波特率(500kbps),实现双向通信:

  • Node A:发送ID=0x123的报文,接收ID=0x456的报文;
  • Node B:发送ID=0x456的报文,接收ID=0x123的报文。
Node A核心代码(发送0x123,接收0x456)
c 复制代码
// 滤波器配置(接收0x456)
can_filter_st.FilterIdHigh = 0x456 <<5;
can_filter_st.FilterMaskIdHigh = 0xFFF<<5;

// 主循环发送0x123
while(1)
{
  CAN_Send_Message(0x123, send_data, 8);
  HAL_Delay(1000);
}
Node B核心代码(发送0x456,接收0x123)
c 复制代码
// 滤波器配置(接收0x123)
can_filter_st.FilterIdHigh = 0x123 <<5;
can_filter_st.FilterMaskIdHigh = 0xFFF<<5;

// 主循环发送0x456
while(1)
{
  CAN_Send_Message(0x456, send_data, 8);
  HAL_Delay(1000);
}
测试结果

两个节点通过CAN总线连接后,Node A会收到Node B发送的0x456报文,Node B会收到Node A发送的0x123报文,实现双向通信。

五、CAN高级特性:中断、DMA与错误处理

5.1 中断与DMA:提高通信效率

  • 中断方式:适合报文数量少、实时性要求高的场景,接收/发送完成后立即触发中断,CPU及时处理;
  • DMA方式:适合大量报文传输,通过DMA直接将数据搬运到内存,减少CPU干预(仅部分STM32系列支持,如F4)。

中断配置补充:除了接收FIFO中断,还可配置以下中断:

  • CAN_IT_TX_MAILBOX_EMPTY:发送邮箱空(可用于连续发送);
  • CAN_IT_ERROR:总线错误(用于错误处理);
  • CAN_IT_WAKEUP:休眠唤醒(低功耗场景)。

5.2 CAN错误处理:保证总线可靠性

CAN控制器会检测总线错误,并根据错误计数进入不同状态:

  • 主动错误状态:错误较少,可正常发送错误帧,通知其他节点;
  • 被动错误状态:错误较多,只能接收,发送前需等待总线空闲;
  • 总线关闭状态 :错误严重,无法参与通信,需软件复位(HAL_CAN_ResetError)恢复。

错误处理示例

c 复制代码
void CAN_Error_Handler(void)
{
  if((hcan.Instance->ESR & CAN_ESR_BOFF) != 0) // 检测到总线关闭
  {
    printf("CAN总线关闭,尝试恢复...\r\n");
    HAL_CAN_ResetError(&hcan); // 复位错误状态
    HAL_CAN_Start(&hcan); // 重新启动CAN
  }
}

// 在主循环中定期检查
while(1)
{
  if(HAL_CAN_GetError(&hcan) != HAL_CAN_ERROR_NONE)
  {
    CAN_Error_Handler();
  }
  // ... 其他代码 ...
}

六、常见问题与解决方案:避坑指南

6.1 通信失败:波特率不匹配

现象:发送报文后,接收方收不到,或收到乱码。

原因

  • 两个节点的波特率计算错误,导致时钟不同步;
  • 位时序参数(BS1、BS2、SJW)配置不一致。

解决方案

  1. 重新计算波特率:确保APB1时钟 / (Prescaler × (SJW+BS1+BS2))在两个节点完全一致;
  2. 推荐位时序参数:对于500kbps,可使用Prescaler=6, SJW=1, BS1=8, BS2=3(总TQ=12,36MHz/6/12=500kbps)。

6.2 接收不到报文:滤波器配置错误

现象:总线有数据,但本节点收不到。

原因

  • 滤波器未使能(FilterActivation=DISABLE);
  • 滤波器ID或掩码配置错误,目标ID被过滤;
  • FIFO溢出(报文过多未及时处理,导致新报文被丢弃)。

解决方案

  1. 检查滤波器使能状态,确保FilterActivation=ENABLE
  2. 用回环模式测试滤波器:发送目标ID,若能收到,说明滤波器配置正确;
  3. 及时处理FIFO数据,避免溢出(可增加FIFO满中断)。

6.3 总线错误:硬件或接线问题

现象:频繁进入总线错误状态,甚至总线关闭。

原因

  • CAN_H和CAN_L接反或短路;
  • 终端电阻缺失或阻值错误(应为120Ω);
  • 总线长度过长,超出对应波特率的最大距离;
  • 电源干扰(未接地或纹波过大)。

解决方案

  1. 用万用表检查CAN_H和CAN_L是否短路,接线是否正确;
  2. 确保总线两端各接一个120Ω终端电阻;
  3. 降低波特率(如从1Mbps降为500kbps),延长传输距离;
  4. 加强电源滤波,确保接地良好。

七、扩展

  • CAN FD:支持更高数据速率(8Mbps)和更长数据帧(64字节),适合大数据量传输;
  • CANopen协议:在CAN基础上的高层协议,定义了标准化的通信对象和设备模型;
  • 多节点网络管理:学习如何设计CAN网络的ID分配、优先级规划和故障诊断。

掌握STM32 CAN通信,能为工业控制、汽车电子等领域的开发打下坚实基础------这是嵌入式工程师进阶的重要技能。建议结合实际硬件多做测试,尤其是滤波器配置和错误处理,才能真正理解CAN总线的精髓。