在嵌入式开发中,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很多型号不支持)
- 协议相对复杂,初次上手需要理解仲裁和位时序
六、注意事项
- 终端电阻必须接:而且只接两端各一个120欧姆,不能多个或没有。
- CAN_H和CAN_L不能接反:接反了不会损坏,但通信完全不通。
- 波特率必须一致:如果总线上有多个节点,确保所有节点使用相同的波特率。
- ID设计要注意优先级:0的优先级最高,数字越小优先级越高。重要的消息用小的ID。
- 接收中断处理要快:不能在中断里做复杂操作,否则会丢帧。可以只把数据存到队列里,主循环再处理。
- 使用CAN分析仪辅助调试:如果你手头没有CAN分析仪,可以先用两块STM32互相通信测试。
七、总结
CAN总线在STM32上实现起来其实并不难,关键是理解它的仲裁原理和参数配置。通过CubeMX可以省去很多手动计算的过程,但底层细节还是要懂一点,否则出了问题不知道怎么排查。上面给的示例代码可以直接用,但实际项目里要记得修改滤波器和处理错误中断。当你看到两块板子通过CAN成功传输数据时,那种成就感还是很爽的。
希望这篇文章能帮你把CAN总线这块石头搬开,下次遇到CAN问题不会再挠头了。
Comments