STM32 HAL库串口接收不定长数据与DMA传输故障定位 - 第3节

3.1 NVIC中断控制器——CPU的“紧急事务处理中心”

聊到UART中断接收,绕不开NVIC。NVIC是什么?说白了就是Cortex-M内核里的一个中断管家。它负责决定:哪个中断来了、该不该响应、响应完要不要切去更高优先级的活儿。

我刚开始用STM32时,总觉得NVIC就是个“配优先级的地方”,配完就忘了。直到有一次,两个外设同时触发中断,系统直接卡死——我才意识到,NVIC的优先级分组、抢占优先级和子优先级,不是随便填的。

3.1.1 NVIC核心概念速览

概念 说明 我的经验
中断通道 每个外设中断对应一个通道号,如USART1_IRQn 查手册时注意通道号别写错,我吃过这个亏
抢占优先级 数值越小,优先级越高,可打断低优先级中断 一般把UART中断设高一点,避免丢字节
子优先级 同抢占优先级下,数值越小越先响应 同组中断用子优先级区分,够用就行
优先级分组 决定抢占位和子优先位的分配比例 推荐用NVIC_PriorityGroup_2,4位抢占+4位子优先

关键点:NVIC的优先级分组必须在系统初始化时设置一次,中途改会出问题。我习惯在HAL_Init()之后立刻调用HAL_NVIC_SetPriorityGrouping()。

3.1.2 NVIC配置三步走

  1. 设置优先级分组——HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2)
  2. 配置具体中断通道——HAL_NVIC_SetPriority(USART1_IRQn, 0, 0)
  3. 使能中断通道——HAL_NVIC_EnableIRQ(USART1_IRQn)

嗯,就这么简单。但简单归简单,顺序别搞反。先设分组,再配通道,最后使能。我曾经在项目里把使能写在了分组前面,结果中断响应乱套,查了半天才发现。

3.2 HAL_UART_Receive_IT函数原理——启动中断接收的“开关”

这个函数,是HAL库提供的“一键开启中断接收”接口。你调用它,库就会帮你配好UART的接收中断,并准备好缓冲区。

3.2.1 函数原型与参数

HAL_StatusTypeDef HAL_UART_Receive_IT(
    UART_HandleTypeDef *huart,
    uint8_t *pData,
    uint16_t Size
);
参数 含义 注意
huart UART句柄指针 确保句柄已初始化
pData 接收缓冲区地址 缓冲区要足够大,别溢出
Size 期望接收的字节数 收到Size个字节后触发回调

小技巧:如果你只想每次收1个字节,就把Size设为1。这样每来一个字节,回调函数就执行一次。我常用这种方式做“单字节中断接收”。

3.2.2 函数内部干了什么

说白了,HAL_UART_Receive_IT做了三件事:

  • 把pData和Size存到huart的成员变量里
  • 使能UART的接收非空中断(RXNEIE)
  • 返回HAL_OK

你想想看,它其实没真正“收”数据,只是把“收数据”的准备工作做好了。真正收数据,是在中断服务函数里完成的。

3.3 中断回调函数HAL_UART_RxCpltCallback——数据到手的“通知铃”

这个回调函数,是HAL库留给用户的“钩子”。当UART接收完指定数量的字节后,库会自动调用它。你只需要在这个函数里写“数据到了,接下来怎么办”的逻辑。

3.3.1 回调函数的典型写法

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)
    {
        // 收到一个字节,存到环形缓冲区
        rx_buffer[rx_index++] = rx_byte;
        // 重新启动接收,准备收下一个字节
        HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
    }
}

注意:回调函数是在中断上下文执行的。不要在里边做耗时操作,比如printf、延时、复杂计算。我见过有人直接在回调里写LCD刷新,结果系统卡成PPT。

3.3.2 回调函数的触发条件

触发条件只有一个:UART接收到了你设定的Size个字节。如果你设Size=1,那每来一个字节就触发一次。如果你设Size=10,那收满10个字节才触发一次。

我个人习惯用Size=1做单字节接收,这样灵活性最高。你可以根据协议头判断数据长度,再动态调整接收策略。

3.4 单字节中断接收实战——从零搭一个能跑的例子

光说不练假把式。咱们直接上手,写一个完整的单字节中断接收程序。

3.4.1 硬件准备

  • STM32F103C8T6开发板(其他型号同理)
  • USB转串口模块(CH340或CP2102)
  • 杜邦线若干

3.4.2 代码实现

第一步:定义全局变量

uint8_t rx_byte;          // 接收缓冲区(单字节)
uint8_t rx_flag = 0;      // 接收完成标志

第二步:初始化UART和NVIC

void MX_USART1_UART_Init(void)
{
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    HAL_UART_Init(&huart1);

    // NVIC配置
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
}

第三步:启动中断接收

HAL_UART_Receive_IT(&huart1, &rx_byte, 1);

第四步:编写回调函数

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)
    {
        rx_flag = 1;  // 标记收到数据
        // 可以在这里处理数据,或者存到缓冲区
        HAL_UART_Receive_IT(&huart1, &rx_byte, 1); // 重新启动
    }
}

第五步:在主循环中处理

while(1)
{
    if(rx_flag)
    {
        rx_flag = 0;
        // 处理收到的rx_byte
        // 比如:HAL_UART_Transmit(&huart1, &rx_byte, 1, 100);
    }
}

避坑指南:我曾经在回调函数里直接处理数据,没重新调用HAL_UART_Receive_IT。结果只收到第一个字节,后面的全丢了。记住:回调里一定要重新启动接收,否则中断只触发一次。

3.5 本章知识体系图

UART中断接收机制知识体系 NVIC中断控制器 优先级分组 抢占优先级 子优先级 中断使能 HAL_UART_Receive_IT 设置缓冲区指针 设置接收字节数 使能RXNE中断 HAL_UART_RxCpltCallback 收到指定字节数后 自动调用

3.6 本章小结

这一章我们走通了UART中断接收的完整链路:从NVIC的优先级配置,到HAL_UART_Receive_IT的启动原理,再到回调函数的数据处理。单字节中断接收是最基础、最常用的方式,也是后续实现不定长接收的基石。

我个人建议,初学者先把单字节中断跑通,再考虑更复杂的DMA或空闲中断。基础打牢了,后面遇到问题才能快速定位。

最后提醒一句:调试中断程序时,多用串口打印关键变量的值。我每次调试都会在回调里加一个GPIO翻转,用示波器看中断响应频率,比猜靠谱多了。