STM32F103R HAL CAN 通信实战 with Copilot

STM32F103R HAL CAN 通信实战 with Copilot

本教程基于 STM32F103R + STM32F1 HAL 库,讲解如何完成 CAN 总线通信配置、调试和问题排查,适合用于博客记录。

说明:本文档参考工程目录 Core/Src/main.ccan.ccan_app.cCore/Inc/can.hCore/Inc/can_app.h 的实际实现。


1. 目标平台与硬件

  • MCU: STM32F103R (STM32F103RB / STM32F103RBT6)
  • 开发环境: STM32CubeMX + HAL 库
  • 传输链路: CAN1 on PB8/PB9 with TJA1050,USART1 on PA9/PA10 用于调试打印
  • 测试工具: pcanviewcanlogserver、USB-TTL 串口

2. 关键问题与修复结论

本次工程里遇到的核心问题是:

  1. CAN 波特率没有显式标注为 500kbps
  2. CAN 中断配置不完整,虽然激活了 HAL CAN 事件通知,但没有启用对应 NVIC 向量。
  3. 发送队列设计中,CAN TX FAIL 是因为发送回调不执行,导致发送挂起后队列溢出。
  4. 串口调试打印被宿主机的抓包命令占用,导致观察到串口不可见。

最终修复点:

  • Core/Src/can.c 中显式配置 500kbps 波特率
  • Core/Src/can_app.c 中启用 USB_HP_CAN1_TX_IRQnUSB_LP_CAN1_RX0_IRQnCAN1_SCE_IRQn
  • Core/Src/main.c 中增加启动串口提示和心跳打印

3. 软件配置

3.1 Core/Src/can.c: CAN 初始化配置

Core/Src/can.cMX_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();
}

并将 Mailbox1Mailbox2 的回调都指向该函数,保证任一邮箱完成都能继续处理后续队列。


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 = 0x123
  • DLC = 2
  • Data = 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.c
  • Core/Src/can.c
  • Core/Src/can_app.c
  • Core/Inc/can.h
  • Core/Inc/can_app.h

8. 结语

本工程最终验证流程为:

  1. 先保证串口调试通畅
  2. 让 MCU 打印 BOOTCAN INIT OK 500KBPS
  3. 确认 Can1_Init() 已启用 CAN NVIC 中断
  4. pcanview 观察 ID=0x123 报文
  5. 若出现错误,查看串口 CAN ERR 码并检查物理总线

这样就能把 STM32F103R 的 HAL CAN 通信稳定运行起来,并实现 500kbps 的总线验证记录。

内容由Raptor Mini from Copilot自动生成,内容后续作者将进行补充

相关推荐
拾-光2 小时前
【无标题】
单片机
himobrinehacken2 小时前
Windows调试技巧:从Hello到I Love C++
stm32·单片机·嵌入式硬件
2301_805962933 小时前
ESP32模组烧录失败排查解决方法
单片机·嵌入式硬件
时空自由民.4 小时前
RGB Image Sensor 和 RGB LCD Scren 的HSYNC和VSYNC的区别,以及ESP32 RGB LCD外设工作原理介绍
单片机
RFID舜识物联网5 小时前
耐高温RFID技术如何解决汽车涂装车间管理难题?
大数据·人工智能·嵌入式硬件·物联网·安全·信息与通信
笨笨饿5 小时前
#55_NE595脉冲电路
stm32·单片机·硬件工程
2301_805962935 小时前
ESP32之esptool.py 常用快捷命令
stm32·单片机·嵌入式硬件
NULL指向我5 小时前
TMS320F28379D笔记1:主控-从核双核架构认识
笔记·单片机
sweetone5 小时前
用一个电阻及一段胶带修复 VORWERK (福维克) THERMOMIX(美善品) TM5-1食品料理机 不工作故障
经验分享·单片机·嵌入式硬件