STM32F103R HAL CAN 通信实战 with Copilot
本教程基于 STM32F103R + STM32F1 HAL 库,讲解如何完成 CAN 总线通信配置、调试和问题排查,适合用于博客记录。
说明:本文档参考工程目录
Core/Src/的main.c、can.c、can_app.c、Core/Inc/can.h、Core/Inc/can_app.h的实际实现。
1. 目标平台与硬件
- MCU: STM32F103R (STM32F103RB / STM32F103RBT6)
- 开发环境: STM32CubeMX + HAL 库
- 传输链路: CAN1 on PB8/PB9 with TJA1050,USART1 on PA9/PA10 用于调试打印
- 测试工具:
pcanview、canlogserver、USB-TTL 串口
2. 关键问题与修复结论
本次工程里遇到的核心问题是:
- CAN 波特率没有显式标注为
500kbps。 - CAN 中断配置不完整,虽然激活了 HAL CAN 事件通知,但没有启用对应 NVIC 向量。
- 发送队列设计中,
CAN TX FAIL是因为发送回调不执行,导致发送挂起后队列溢出。 - 串口调试打印被宿主机的抓包命令占用,导致观察到串口不可见。
最终修复点:
- 在
Core/Src/can.c中显式配置500kbps波特率 - 在
Core/Src/can_app.c中启用USB_HP_CAN1_TX_IRQn、USB_LP_CAN1_RX0_IRQn、CAN1_SCE_IRQn - 在
Core/Src/main.c中增加启动串口提示和心跳打印
3. 软件配置
3.1 Core/Src/can.c: CAN 初始化配置
Core/Src/can.c 中 MX_CAN_Init() 的核心配置如下:
c
CAN_HandleTypeDef hcan1;
#define CAN_PRESCALER_500KBPS 6U
#define CAN_BS1_500KBPS CAN_BS1_8TQ
#define CAN_BS2_500KBPS CAN_BS2_3TQ
void MX_CAN_Init(void)
{
hcan1.Instance = CAN1;
/* 36MHz APB1 / (6 * (1+8+3)) = 500kbps */
hcan1.Init.Prescaler = CAN_PRESCALER_500KBPS;
hcan1.Init.Mode = CAN_MODE_NORMAL;
hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan1.Init.TimeSeg1 = CAN_BS1_500KBPS;
hcan1.Init.TimeSeg2 = CAN_BS2_500KBPS;
hcan1.Init.TimeTriggeredMode = DISABLE;
hcan1.Init.AutoBusOff = ENABLE;
hcan1.Init.AutoWakeUp = ENABLE;
hcan1.Init.AutoRetransmission = ENABLE;
hcan1.Init.ReceiveFifoLocked = DISABLE;
hcan1.Init.TransmitFifoPriority = DISABLE;
if (HAL_CAN_Init(&hcan1) != HAL_OK)
{
Error_Handler();
}
}
3.2 Core/Src/can_app.c: CAN 驱动逻辑和中断启用
Can1_Init() 负责配置滤波器、启动 CAN 外设,并启用中断:
c
void Can1_Init(void)
{
CAN_FilterTypeDef sFilterConfig = {0};
sFilterConfig.FilterBank = 0;
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK;
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT;
sFilterConfig.FilterIdHigh = 0x0000;
sFilterConfig.FilterIdLow = 0x0000;
sFilterConfig.FilterMaskIdHigh = 0x0000;
sFilterConfig.FilterMaskIdLow = 0x0000;
sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0;
sFilterConfig.FilterActivation = ENABLE;
if (HAL_CAN_ConfigFilter(&hcan1, &sFilterConfig) != HAL_OK)
{
Can1_ErrorHandler(&hcan1);
return;
}
if (HAL_CAN_Start(&hcan1) != HAL_OK)
{
Can1_ErrorHandler(&hcan1);
return;
}
if (HAL_CAN_ActivateNotification(&hcan1,
CAN_IT_RX_FIFO0_MSG_PENDING |
CAN_IT_TX_MAILBOX_EMPTY |
CAN_IT_ERROR_WARNING |
CAN_IT_ERROR_PASSIVE |
CAN_IT_BUSOFF |
CAN_IT_LAST_ERROR_CODE) != HAL_OK)
{
Can1_ErrorHandler(&hcan1);
return;
}
HAL_NVIC_SetPriority(USB_HP_CAN1_TX_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USB_HP_CAN1_TX_IRQn);
HAL_NVIC_SetPriority(USB_LP_CAN1_RX0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
HAL_NVIC_SetPriority(CAN1_SCE_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(CAN1_SCE_IRQn);
g_Can1_TxQueue.Head = g_Can1_TxQueue.Tail = g_Can1_TxQueue.Count = 0;
g_Can1_RxQueue.Head = g_Can1_RxQueue.Tail = g_Can1_RxQueue.Count = 0;
g_Can1_IsTxPending = false;
}
这一步非常重要:HAL 的通知必须配合 NVIC 中断使能,否则发送完成回调不会触发。
3.3 Core/Src/main.c: 主流程与调试输出
主循环里使用 USART1 输出心跳和 CAN 发送状态:
c
if ((now - last_heartbeat) >= 500)
{
last_heartbeat = now;
HAL_GPIO_TogglePin(D1_GPIO_Port, D1_Pin);
int n = snprintf(uart_buf, sizeof(uart_buf), "HB %lu\r\n", (unsigned long)now);
HAL_UART_Transmit(&huart1, (uint8_t*)uart_buf, (uint16_t)n, 50);
}
if ((now - last_can_send) >= 1000)
{
last_can_send = now;
Can1_Msg_t tx = {0};
tx.Id = 0x123;
tx.DLC = 2;
tx.IDE = 0;
tx.RTR = 0;
tx.Data[0] = (uint8_t)(now & 0xFF);
tx.Data[1] = (uint8_t)((now>>8) & 0xFF);
if (Can1_SendMsg(&tx, 100) != HAL_OK)
{
int n = snprintf(uart_buf, sizeof(uart_buf), "CAN TX FAIL ID=0x%03X DLC=%u\r\n", (unsigned)tx.Id, tx.DLC);
HAL_UART_Transmit(&huart1, (uint8_t*)uart_buf, (uint16_t)n, 100);
}
else
{
int n = snprintf(uart_buf, sizeof(uart_buf), "CAN TX ID=0x%03X DLC=%u\r\n", (unsigned)tx.Id, tx.DLC);
HAL_UART_Transmit(&huart1, (uint8_t*)uart_buf, (uint16_t)n, 100);
}
}
在启动阶段打印:
c
int n = snprintf(uart_buf, sizeof(uart_buf), "BOOT\r\nCAN INIT OK 500KBPS\r\n");
HAL_UART_Transmit(&huart1, (uint8_t*)uart_buf, (uint16_t)n, 200);
这样可以快速观察设备是否正常上电并进入了 CAN 初始化。
4. 主要代码结构说明
4.1 软件队列设计
can_app.c 里实现了简易的 TX/RX 环形队列:
Can1_EnqueueTxMsg()Can1_DequeueTxMsg()Can1_ProcessTxQueue()
发送时:
c
HAL_StatusTypeDef Can1_SendMsg(Can1_Msg_t *pTxMsg, uint32_t Timeout)
{
(void)Timeout;
if (Can1_EnqueueTxMsg(pTxMsg) != HAL_OK)
{
return HAL_ERROR;
}
Can1_ProcessTxQueue();
return HAL_OK;
}
如果上一次发送还在挂起,下一条报文会继续保留在队列里,直到发送完成回调释放 g_Can1_IsTxPending。
4.2 发送完成回调
回调实现为:
c
void HAL_CAN_TxMailbox0CompleteCallback(CAN_HandleTypeDef *hcan)
{
if (hcan->Instance != hcan1.Instance) return;
g_Can1_IsTxPending = false;
Can1_ProcessTxQueue();
}
并将 Mailbox1、Mailbox2 的回调都指向该函数,保证任一邮箱完成都能继续处理后续队列。
5. 验证与调试
5.1 串口调试
为避免宿主机抓包命令占用串口,先确保没有下面这种命令在运行:
bash
stdbuf -oL cat /dev/ttyUSB0 | stdbuf -oL sed -u 's/^/[UART] /' | tee build/serial_log.txt
正确方式是直接打开 /dev/ttyUSB0 查看输出,如果串口正常,应看到:
text
BOOT
CAN INIT OK 500KBPS
HB 500
HB 1000
...
5.2 pcanview 验证 CAN
使用以下命令检查 pcanview 是否能看到报文:
bash
timeout 6 pcanview -b 500000 -l /tmp/pcanview_check.log /dev/pcanusb32
正常情况下,pcanview 会显示 CycleTime / ID / Type / Data,例如:
ID = 0x123DLC = 2Data = EB 03
如果出现 empty,说明总线层面未收到任何报文,需检查物理连接。
6. 常见故障与排查
6.1 CAN TX FAIL 仍然出现
这通常是:
HAL_CAN_TxMailbox*CompleteCallback未执行- NVIC 中断没有使能
- 发送邮箱卡住导致队列堆满
本工程修复后,关键在于启用下列 NVIC 中断:
c
HAL_NVIC_EnableIRQ(USB_HP_CAN1_TX_IRQn);
HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
HAL_NVIC_EnableIRQ(CAN1_SCE_IRQn);
6.2 pcanview empty 但串口打印 CAN TX ID=...
说明 MCU 端发出了报文,但总线物理链路有问题:
- CAN_H/CAN_L 是否接反?
- 是否有 120Ω 终端电阻?
- 你的 CAN 设备与 PCAN adapter 是否在同一根总线上?
6.3 串口没有输出
先确认 /dev/ttyUSB0 没被别的进程占用。
使用 fuser 检查:
bash
fuser -v /dev/ttyUSB0
如果被占用,关闭相关抓包程序后再重新打开串口。
7. 代码文件清单
本教程涉及的关键源码:
Core/Src/main.cCore/Src/can.cCore/Src/can_app.cCore/Inc/can.hCore/Inc/can_app.h
8. 结语
本工程最终验证流程为:
- 先保证串口调试通畅
- 让 MCU 打印
BOOT、CAN INIT OK 500KBPS - 确认
Can1_Init()已启用 CAN NVIC 中断 - 用
pcanview观察ID=0x123报文 - 若出现错误,查看串口
CAN ERR码并检查物理总线
这样就能把 STM32F103R 的 HAL CAN 通信稳定运行起来,并实现 500kbps 的总线验证记录。