在嵌入式开发中,CAN总线是一种非常常见的通信方式,尤其是在汽车电子、工业控制还有机器人领域。很多刚接触STM32的朋友,听到CAN可能会觉得有点复杂,其实说白了它就像是一个多个人能同时说话、但又不会打架的通信网络。今天我们就来聊聊怎么在STM32上用CAN总线,以及遇到问题该怎么排查。

一、CAN总线是个啥玩意儿?

简单说,CAN总线就是一条数据线,很多设备都能挂在这条线上,大家轮流发言。但跟平时串口一对一的聊天不一样,CAN总线上的任何一个设备都能随时发送消息,而且不会因为冲突导致数据丢失。它的核心机制是“仲裁”,就是谁的消息优先级高谁先发,低优先级的会自动后退等待。这种设计特别适合实时性要求高的场景,比如汽车里刹车信号肯定要比车窗信号优先级高。

在STM32系统里,CAN外设通常集成在芯片内部,我们只需要通过两个引脚(CAN_TX和CAN_RX)连接外部的CAN收发器芯片(比如TJA1050),就能把单片机的逻辑电平转成差分信号,然后挂到CAN总线上。

二、在STM32上怎么用CAN?

用STM32控制CAN,最省事的方法是用STM32CubeMX生成初始化代码,然后配合HAL库来开发。下面我们先说说硬件连接。

2.1 硬件连接

你需要准备:一块STM32开发板(比如F103C8T6),一个CAN收发器模块(比如TJA1050模块),以及两个120欧姆的终端电阻。连接方式很简单:

  • STM32的CAN_RX(比如PB8)接收发器的RXD
  • STM32的CAN_TX(比如PB9)接收发器的TXD
  • 收发器的CANH和CANL分别接到总线上的CANH和CANL
  • 在总线的两端各接一个120欧姆电阻到CANH和CANL之间

这里要注意:终端电阻一定要接,没有的话信号会反射,导致通信不稳定。

2.2 软件配置

用CubeMX打开对应芯片,在“Connectivity”选项里找到CAN,使能它。然后设置波特率,常见的是500kbps或250kbps。注意时钟配置:CAN的时钟源一般是APB1,需要算好分频因子。比如STM32F103的APB1是36MHz,要得到500kbps,需要设置分频为9,时间量子段等参数按标准设置就行。CubeMX会自动计算,我们只要把Prescaler改成9,然后点“Resolve”看看是否500k。

别忘了配置中断:CAN接收中断和错误中断都勾上,方便后面调试。生成代码后,我们还需要在main.c里添加业务逻辑。

三、实际案例:写个简单的收发程序

下面我们用STM32F103 + HAL库实现一个循环:每隔1秒发送一帧数据,同时接收任何其他设备发来的消息,并把接收到的内容打印到串口。

技术栈:STM32F103 + HAL库 + CubeMX + Keil MDK

/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "string.h"
/* USER CODE END Includes */

/* 定义CAN发送消息结构体 */
CAN_TxHeaderTypeDef TxHeader;
uint8_t TxData[8];
uint32_t TxMailbox;

/* 定义CAN接收消息结构体 */
CAN_RxHeaderTypeDef RxHeader;
uint8_t RxData[8];

/* 串口打印用的缓冲 */
char print_buf[100];

/* 初始化CAN滤波器:允许接收所有ID */
void CAN_Filter_Init(void)
{
    CAN_FilterTypeDef filter;
    filter.FilterBank = 0;                    // 使用滤波器组0
    filter.FilterMode = CAN_FILTERMODE_IDMASK; // 标识符屏蔽模式
    filter.FilterScale = CAN_FILTERSCALE_32BIT; // 32位
    filter.FilterIdHigh = 0x0000;             // 标准ID高16位
    filter.FilterIdLow = 0x0000;              // 标准ID低16位
    filter.FilterMaskIdHigh = 0x0000;         // 屏蔽码全部为0,接收所有ID
    filter.FilterMaskIdLow = 0x0000;
    filter.FilterFIFOAssignment = CAN_RX_FIFO0; // 关联到FIFO0
    filter.FilterActivation = ENABLE;
    HAL_CAN_ConfigFilter(&hcan, &filter);
}

/* 发送一帧CAN数据 */
void CAN_Send_Message(uint32_t id, uint8_t *data, uint8_t len)
{
    TxHeader.IDE = CAN_ID_STD;          // 使用标准ID
    TxHeader.StdId = id;                // 消息ID
    TxHeader.RTR = CAN_RTR_DATA;        // 数据帧
    TxHeader.DLC = len;                 // 数据长度(1-8)
    TxHeader.TransmitGlobalTime = DISABLE;

    // 将数据复制到发送缓冲区
    memcpy(TxData, data, len);

    // 将消息放入发送邮箱
    if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, TxData, &TxMailbox) != HAL_OK)
    {
        printf("发送失败!\n");
    }
}

/* 接收一帧CAN数据(在中断中调用) */
void CAN_Receive_Message(void)
{
    if (HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &RxHeader, RxData) == HAL_OK)
    {
        // 格式化打印接收到的消息
        sprintf(print_buf, "收到ID: 0x%03X, 长度: %d, 数据: ", RxHeader.StdId, RxHeader.DLC);
        for (int i = 0; i < RxHeader.DLC; i++)
        {
            char tmp[4];
            sprintf(tmp, "%02X ", RxData[i]);
            strcat(print_buf, tmp);
        }
        printf("%s\n", print_buf);
    }
}

int main(void)
{
    /* HAL库初始化,系统时钟配置等(CubeMX已生成) */
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();
    MX_CAN_Init();

    /* 初始化CAN滤波器 */
    CAN_Filter_Init();

    /* 启动CAN外设,并启用中断 */
    if (HAL_CAN_Start(&hcan) != HAL_OK)
    {
        printf("CAN启动失败!\n");
        Error_Handler();
    }
    if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
    {
        printf("CAN接收中断使能失败!\n");
        Error_Handler();
    }

    printf("CAN总线示例开始运行...\n");

    uint8_t msg_count = 0;
    while (1)
    {
        // 每秒发送一帧数据:ID=0x123,内容为"Hello"加上计数器
        uint8_t send_data[8] = {0};
        send_data[0] = 'H';
        send_data[1] = 'e';
        send_data[2] = 'l';
        send_data[3] = 'l';
        send_data[4] = 'o';
        send_data[5] = msg_count;
        CAN_Send_Message(0x123, send_data, 6);
        printf("发送第 %d 帧数据\n", msg_count);
        msg_count++;

        HAL_Delay(1000);
    }
}

/* CAN接收中断服务函数 */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    if (hcan->Instance == CAN1) // 确定是CAN1的中断
    {
        CAN_Receive_Message();
    }
}

上面的代码注释已经写得很清楚了。核心就是先初始化滤波器(这里设成接收所有ID,方便调试),然后在主循环里定时发送,接收则通过中断回调函数来处理。注意:实际项目中滤波器要按需配置,否则总线负载会很高。

四、常见故障怎么排查?

刚接触CAN的时候,十有八九会遇到通信不通的问题。别慌,我们一步步来。

4.1 硬件问题

首先确认接线:CAN_TX/CAN_RX有没有接反?收发器的VCC和GND有没有供电?终端电阻有没有接?如果没接电阻,信号会有回波,数据包可能会出错。可以用示波器看CANH和CANL之间的差分电平:空闲时应该是2.5V左右,发送时会在1.5V和3.5V之间摆动。没有示波器的话,可以找个CAN分析仪(比如USBCAN)插到总线上,看看能不能收到自己发的帧。如果收不到,先检查收发器供电和引脚连接。

还有一点:总线上至少要两个节点才能工作(一个发送,一个接收)。如果你的板子单独测试,可以用CAN分析仪当作另一个节点,或者用另一块开发板。

4.2 软件问题

软件问题最常见的就是波特率不匹配。总线上所有设备的波特率必须完全一样,差一点点都不行。确认自己的配置:用CubeMX重新检查时钟树,确保CAN的时钟正确。也可以用示波器测量CAN_TX引脚的波形,看看位时间是否符合。

另外,检查滤波器配置:如果滤波器设置太严格,可能会把想要的ID过滤掉。测试阶段可以像我上面代码那样,把屏蔽码全设为0,接收所有ID。

还有中断优先级:CAN接收中断的优先级不能比系统滴答中断低太多,否则可能导致消息丢失。一般设成中等优先级就行。

五、应用场景和优缺点

CAN总线最常见的应用就是汽车:发动机控制单元、ABS、车身控制模块等都用CAN通信。在工业自动化中,也常用于传感器数据采集和执行器控制,比如变频器、伺服驱动器之间通过CANbus交换指令。

优点很明显:

  • 多主通信,不需要主机轮询,实时性好
  • 错误检测机制强大(CRC、位填充、应答机制等),数据可靠性高
  • 物理层抗干扰能力强,适合恶劣环境
  • 总线利用率高,理论上最多可以挂110个节点

缺点也有:

  • 通信速率受距离限制,比如500kbps通常只能跑几十米
  • 数据帧最大只有8字节,传输大容量数据效率低(CAN FD解决了,但STM32很多型号不支持)
  • 协议相对复杂,初次上手需要理解仲裁和位时序

六、注意事项

  1. 终端电阻必须接:而且只接两端各一个120欧姆,不能多个或没有。
  2. CAN_H和CAN_L不能接反:接反了不会损坏,但通信完全不通。
  3. 波特率必须一致:如果总线上有多个节点,确保所有节点使用相同的波特率。
  4. ID设计要注意优先级:0的优先级最高,数字越小优先级越高。重要的消息用小的ID。
  5. 接收中断处理要快:不能在中断里做复杂操作,否则会丢帧。可以只把数据存到队列里,主循环再处理。
  6. 使用CAN分析仪辅助调试:如果你手头没有CAN分析仪,可以先用两块STM32互相通信测试。

七、总结

CAN总线在STM32上实现起来其实并不难,关键是理解它的仲裁原理和参数配置。通过CubeMX可以省去很多手动计算的过程,但底层细节还是要懂一点,否则出了问题不知道怎么排查。上面给的示例代码可以直接用,但实际项目里要记得修改滤波器和处理错误中断。当你看到两块板子通过CAN成功传输数据时,那种成就感还是很爽的。

希望这篇文章能帮你把CAN总线这块石头搬开,下次遇到CAN问题不会再挠头了。