STM32F103VET6 CAN 双板通信实战:从配置到代码实现
STM32F103VET6 CAN 双板通信实战:从配置到代码实现
在工业控制和汽车电子领域,CAN(Controller Area Network)总线凭借其高可靠性和多主特性,成为最常用的通信协议之一。本文将带你一步步在 STM32F103VET6 上实现两块开发板之间的 CAN 通信,包含硬件连接、HAL 库配置、过滤器设置及中断接收的完整代码。
一、硬件准备与连接
在编写代码之前,物理连接是通信成功的基础。CAN 总线是差分信号,必须严格遵循以下接线规则:
该图来自@Embedd1000010出处
1. 接线图
你需要两块 STM32F103VET6 开发板(或一块板子连接 USB-CAN 分析仪),以及 CAN 收发器(如 TJA1050 模块)。
| 板子 A (或分析仪) | 连接线 | 板子 B (或分析仪) |
|---|---|---|
| CAN_H | <------> | CAN_H |
| CAN_L | <------> | CAN_L |
| GND | <------> | GND(必须共地) |
2. 终端电阻
CAN 总线要求在物理链路的两端并联120Ω电阻以消除信号反射。
- 注意:如果你的开发板板载了 120Ω 电阻,无需额外添加;如果没有,请在两块板子的 CAN_H 和 CAN_L 之间各焊一个 120Ω 电阻。
二、软件架构与流程
根据你提供的流程图,CAN 通信的初始化逻辑非常清晰。在main函数中,我们需要严格按照以下顺序执行:
- CAN 初始化:配置波特率、模式(正常/环回)。
- 过滤器初始化:决定接收哪些 ID 的数据。
- 启动 CAN:进入正常工作模式。
- 开启接收中断:让 CPU 在收到数据时自动处理,不阻塞主循环。
- 发送数据:在
while(1)中循环发送。
三、核心代码实现
以下是基于 STM32 HAL 库的完整实现代码。
1. 头文件与全局变量
首先定义全局的句柄和报文结构体。TxHeader和RxHeader是 HAL 库收发函数的核心参数。
#include "main.h" #include <stdio.h> #include <string.h> CAN_HandleTypeDef hcan; UART_HandleTypeDef huart1; // 用于 printf 打印调试 /* CAN 报文结构体 */ CAN_TxHeaderTypeDef TxHeader; CAN_RxHeaderTypeDef RxHeader; uint8_t RxData[8]; // 接收数据缓存 uint32_t TxMailbox; // 发送邮箱号 char MsgBuffer[50]; // 打印缓存 /* 函数声明 */ void CAN_Filter_Config(void); void CAN_Start_Init(void); void CAN_Send_Msg(uint8_t *data, uint8_t len);2. CAN 初始化(波特率配置)
在MX_CAN_Init()中,波特率是最关键的参数。双板通信必须保证波特率完全一致。
static void MX_CAN_Init(void) { hcan.Instance = CAN1; hcan.Init.Prescaler = 9; // 波特率分频系数 hcan.Init.Mode = CAN_MODE_NORMAL; // 正常模式(调试时可用环回模式 CAN_MODE_LOOPBACK) hcan.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan.Init.TimeSeg1 = CAN_BS1_6TQ; // 时间段1 hcan.Init.TimeSeg2 = CAN_BS2_1TQ; // 时间段2 // 波特率计算公式: 36MHz / (Prescaler * (1 + BS1 + BS2)) // 36000000 / (9 * (1 + 6 + 1)) = 500Kbps hcan.Init.TimeTriggeredMode = DISABLE; hcan.Init.AutoBusOff = DISABLE; hcan.Init.AutoWakeUp = DISABLE; hcan.Init.AutoRetransmission = ENABLE; // 开启自动重传 hcan.Init.ReceiveFifoLocked = DISABLE; hcan.Init.TransmitFifoPriority = DISABLE; if (HAL_CAN_Init(&hcan) != HAL_OK) { Error_Handler(); } }3. 过滤器配置
CAN 过滤器决定了单片机“愿意”接收哪些数据。以下代码配置为32位掩码模式,接收所有标准 ID 数据。
void CAN_Filter_Config(void) { CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank = 0; // 过滤器组0 sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式 sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 32位尺度 sFilterConfig.FilterIdHigh = 0x0000; // ID高16位 sFilterConfig.FilterIdLow = 0x0000; // ID低16位 sFilterConfig.FilterMaskIdHigh = 0x0000; // 掩码高16位 (0表示不关心) sFilterConfig.FilterMaskIdLow = 0x0000; // 掩码低16位 sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0;// 匹配后存入 FIFO0 sFilterConfig.FilterActivation = ENABLE; // 激活 sFilterConfig.SlaveStartFilterBank = 14; // 仅用于互联型产品,F103可忽略 if (HAL_CAN_ConfigFilter(&hcan, &sFilterConfig) != HAL_OK) { Error_Handler(); } }4. 启动与中断使能
这一步将 CAN 外设置于工作状态,并打开接收中断,允许 CPU 响应收到的数据。
void CAN_Start_Init(void) { // 1. 启动 CAN if (HAL_CAN_Start(&hcan) != HAL_OK) { Error_Handler(); } // 2. 开启接收中断 (FIFO 0 消息挂起中断) if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK) { Error_Handler(); } // 3. 配置 NVIC 中断优先级 (通常在 stm32f1xx_it.c 中处理,但需在此使能) HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 1, 0); HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn); }5. 数据发送函数
封装一个发送函数,方便在while(1)中调用。
void CAN_Send_Msg(uint8_t *data, uint8_t len) { TxHeader.StdId = 0x111; // 标准 ID TxHeader.ExtId = 0x0; // 扩展 ID (不使用) TxHeader.IDE = CAN_ID_STD; // 使用标准 ID TxHeader.RTR = CAN_RTR_DATA; // 数据帧 TxHeader.DLC = len; // 数据长度 TxHeader.TransmitGlobalTime = DISABLE; // 发送数据 if (HAL_CAN_AddTxMessage(&hcan, &TxHeader, data, &TxMailbox) != HAL_OK) { printf("发送失败!\r\n"); } }6. 接收中断回调
当 CAN 总线收到数据且通过过滤器后,硬件会自动触发中断,并执行此回调函数。
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { if (hcan->Instance == CAN1) { // 获取数据 HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData); printf("---> 接收到数据!\r\n"); printf("ID: 0x%X, DLC: %d\r\n", RxHeader.StdId, RxHeader.DLC); printf("Data: %02X %02X %02X %02X\r\n", RxData[0], RxData[1], RxData[2], RxData[3]); printf("<---\r\n"); } } // 中断服务程序 (需放在 stm32f1xx_it.c 或 main.c 中) void CAN1_RX0_IRQHandler(void) { HAL_CAN_IRQHandler(&hcan); }四、主函数逻辑
将上述模块在main函数中串联起来。
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_CAN_Init(); MX_USART1_UART_Init(); // 初始化串口用于 printf /* CAN 配置流程 */ CAN_Filter_Config(); // 配置过滤器 CAN_Start_Init(); // 启动并开启中断 uint8_t tx_data[4] = {0x01, 0x02, 0x03, 0x04}; uint32_t tick = 0; printf("CAN 双板通信测试开始...\r\n"); while (1) { // 每 500ms 发送一次数据 if (HAL_GetTick() - tick > 500) { tick = HAL_GetTick(); tx_data[0]++; // 数据自增,方便观察 CAN_Send_Msg(tx_data, 4); } } }五、常见问题排查
- 收不到数据?
- 检查 GND 是否连接(共地是必须的)。
- 检查波特率是否一致(两块板子必须完全相同)。
- 检查过滤器配置,建议先配置为全接收(掩码全 0)测试通断。
- printf 不打印?
- 确保重定向了
_write或fputc函数到 USART1。
- 确保重定向了
- 报错
HAL_CAN_ActivateNotification?- 确保在
stm32f1xx_it.c中添加了CAN1_RX0_IRQHandler中断入口函数。
- 确保在
通过以上步骤,你应该能成功实现 STM32F103 之间的双向数据通信。
