问题现象

通过串口助手发送十六进制帧数据 AA BB E8 EE FF,单片机无响应,无法接收和解析数据。printf 可以正常输出(说明串口发送正常),但串口接收中断似乎未触发或回调未执行。


问题原因分析(按排查顺序)

问题1:编译错误 — USART_Private 前向声明导致无法访问成员

错误信息

System\DeBUG.c(186): error:  #393: pointer to incomplete class type is not allowed
   	        (unsigned int)&(Debug.Private->USART_InitStructure),

原因

  • Function.h 中只有 typedef struct USART_Private USART_Private; 前向声明
  • USART_Private 的完整定义放在了 Function.c 内部
  • DeBUG.c 包含 Function.h 但看不到结构体成员定义,因此访问 Debug.Private->USART_InitStructure 时报错

修复:将 USART_Private 的完整结构体定义从 Function.c 移到 Function.h 中。


问题2:核心根因 — UART 句柄地址不匹配,导致 HAL 回调不触发

这是本次排查最关键的发现。

数据流对照

CubeMX 生成的中断处理(Core/Src/stm32l4xx_it.c

void USART3_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart3);   // ← 固定传入 &huart3
}

之前错误的做法(Function.cCreate_USART

this.Private = (USART_Private*)malloc(sizeof(USART_Private));
// 自己分配了一个 UART_HandleTypeDef 副本
this.Private->USART_InitStructure.Instance = USART3;
// ...
HAL_UART_Receive_IT(&(this.Private->USART_InitStructure), (uint8_t *)RecvData, 1);
//  ↑ 启动中断时用的是自己分配的句柄,地址 ≠ &huart3
为什么这会导致接收失败

HAL 库的内部工作原理:

  1. HAL_UART_Receive_IT(huart, ...) 会设置 huart->RxState = HAL_UART_STATE_BUSY_RX
  2. 串口收到数据触发 USART3_IRQHandler
  3. USART3_IRQHandler 调用 HAL_UART_IRQHandler(&huart3)
  4. HAL 内部检查 &huart3RxState 是否为 BUSY_RX
  5. 由于我们启动中断用的是另一个句柄huart3RxState 从未被设置为 BUSY_RX
  6. HAL 认为没有待处理的接收,回调函数永远不会被调用
如何发现的

通过在 Create_USART 和回调函数中加入调试打印,对比两个地址:

[Init] USART handle addr=0x20000588    ← 我们自己分配的句柄

USART3_IRQHandler 中调用的是 &huart3(地址不同),两个地址不一致 → 永远不匹配 → 回调不执行。


最终解决方案

重构思路

不再自己分配 UART_HandleTypeDef 副本,直接使用 CubeMX 生成的全局变量 huart3

这样保证:

  • USART3_IRQHandlerHAL_UART_IRQHandler(&huart3) 的句柄
  • Create_USARTHAL_UART_Receive_IT(&huart3, ...) 的句柄
  • 两者是同一个地址,HAL 状态判断正常

具体改动

1. Function.h — 结构体和函数签名
typedef struct USART_Private {
    IRQn_Type            usart_IRQ;
    USART_TypeDef       *USART;
    UART_HandleTypeDef  *huart;    // ← 改为指针,指向外部传入的句柄
} USART_Private;

// Create_USART 第一个参数改为 UART_HandleTypeDef*
USART_BUS Create_USART(UART_HandleTypeDef *huart,
                        USART_TypeDef *USARTx,
                        uint32_t BaudRate,
                        void(*Reset)(void),
                        uint8_t* RecvData);
2. Function.cCreate_USART 实现
USART_BUS Create_USART(UART_HandleTypeDef *huart,
                        USART_TypeDef *USARTx,
                        uint32_t BaudRate,
                        void(*Reset)(void),
                        uint8_t* RecvData) {
    IRQn_Type USART_TRQ;
    // ... 根据 USARTx 设置 USART_TRQ 和时钟

    USART_BUS this;
    this.Private = (USART_Private*)malloc(sizeof(USART_Private));
    this.Private->USART    = USARTx;
    this.Private->usart_IRQ = USART_TRQ;
    this.Private->huart    = huart;   // 保存外部句柄指针

    // 直接用外部传入的 huart 配置
    huart->Instance        = USARTx;
    huart->Init.BaudRate   = BaudRate;
    // ...
    HAL_UART_Init(huart);
    HAL_NVIC_SetPriority(USART_TRQ, 3, 3);
    HAL_NVIC_EnableIRQ(USART_TRQ);

    // 初始化状态结构
    this.USART_RecvReset = Reset;
    memset(this.Status.RecvDataBuf, 0, RecvDataSize);
    this.Status.RecvDataLen  = 0;
    this.Status.RecvDataFlag = RecvHandleFinish;

    // 关键:用外部传入的 huart 启动中断接收
    HAL_UART_Receive_IT(huart, (uint8_t *)RecvData, 1);

    return this;
}
3. DeBUG.c — 传入正确句柄
void DebugUSART_Init(uint32_t bound)
{
    DEBUG_GPIO_CLK_ENABLE();
    // 传入 CubeMX 生成的 &huart3,而不是自己分配的副本
    Debug = Create_USART(&huart3, USART3, bound, RecvTypeReset, &RecvData);
    USE_USART(Debug)->USART_RecvReset();
}
4. DeBUG.c — 回调函数中的句柄比较
void Debug_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    UART_HandleTypeDef *expected = Debug.Private->huart;

    if(huart == expected) {      // ← 现在地址一致,条件成立
        printf("[IRQ] handle MATCH ...\n");
        HAL_UART_Receive_IT(expected, (uint8_t *)&RecvData, 1);
        RecvHexFrame();          // 状态机解析
    }
}

修复后的验证输出

System Run
[Init] huart=0x200004EC, Instance=0x40004800

(发送 AA BB E8 EE FF)

[IRQ] huart=0x200004EC, expect=0x200004EC, byte=AA   ← 匹配!
[IRQ] handle MATCH, byte=AA, state=0
[FSM] enter state=0, byte=AA, len=0, flag=F4
[FSM] header1 OK -> STATE_WAIT_HEADER2

[IRQ] huart=0x200004EC, expect=0x200004EC, byte=BB
[IRQ] handle MATCH, byte=BB, state=1
[FSM] enter state=1, byte=BB, len=0, flag=F4
[FSM] header2 OK -> STATE_RECEIVE_DATA

[IRQ] huart=0x200004EC, expect=0x200004EC, byte=E8
[IRQ] handle MATCH, byte=E8, state=2
[FSM] enter state=2, byte=E8, len=0, flag=F4
[FSM] stored E8, new len=1

[IRQ] huart=0x200004EC, expect=0x200004EC, byte=EE
[IRQ] handle MATCH, byte=EE, state=2
[FSM] tail1 OK -> STATE_WAIT_TAIL2

[IRQ] huart=0x200004EC, expect=0x200004EC, byte=FF
[IRQ] handle MATCH, byte=FF, state=3
[FSM] tail2 OK -> DONE! flag=RecvFinish, total len=1

======= E8 ======   ← USER_Loop 检测到 RecvFinish,打印数据字节

修改的文件清单

文件 改动摘要
MDK-ARM/System/Function.h USART_Private 结构体移到此处,新增 UART_HandleTypeDef *huart 成员;Create_USART 签名增加第一个参数 UART_HandleTypeDef *huart
MDK-ARM/System/Function.c Create_USART 改为直接使用外部传入的 huart,不再自己分配 UART_HandleTypeDef 副本
MDK-ARM/System/DeBUG.c DebugUSART_Init 调用改为 Create_USART(&huart3, USART3, bound, RecvTypeReset, &RecvData);回调函数用 Debug.Private->huart 进行句柄比较

经验总结

  1. 不要和 CubeMX "抢"句柄。CubeMX 生成的 USARTx_IRQHandler 固定传入 &huartx,自己再分配副本会导致地址不匹配,HAL 状态判断失败。

  2. HAL 中断回调匹配机制HAL_UART_IRQHandler(huart) 内部会检查传入的 huart 指针的 RxState 是否为 BUSY_RX,如果启动中断接收时用的是另一个 huart,状态不会被设置,回调永远不触发。

  3. 在中断回调中加入调试打印(对比句柄地址)是快速定位此类问题的有效手段。

  4. 前向声明的结构体只能传指针,如果需要在其他文件中访问其成员,必须将完整定义放到头文件中。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐