Brilliantwxx头像
关注
【​​​​STM32】 USART 中断模式数据收发:从原理到源码级解析封面图

【​​​​STM32】 USART 中断模式数据收发:从原理到源码级解析


  STM32 USART 中断模式数据收发:从原理到源码级解析  

一、为什么用中断模式?

串口数据收发常见三种模式:轮询(阻塞)、中断(非阻塞)、DMA。

对比维度轮询模式中断模式
CPU 参与方式死等标志位,期间 CPU 被占用硬件事件触发,CPU 空闲时干别的
实时性依赖轮询周期,可能丢数据字节到达立刻响应
代码复杂度简单但低效需要回调函数,稍复杂
适用场景简单单字节交互高频、不定长数据收发

核心结论:

        中断模式比轮询高效得多,是非阻塞收发的标准做法。

        本例用 UART 中断模式完成数据的接收、处理(大小写互换)、回传,并用 LED 指示传输状态。


二、核心机制:USART 如何连上 NVIC?

2.1 中断源的两种类型

  • 外部中断(EXTI):处理来自 GPIO 引脚的外部信号,需经过 EXTI 外设。
  • 片上外设中断:USART、定时器、I2C、SPI 等外设自带中断模块,直接接到 Cortex-M3 的 NVIC。

关键认知:USART 这类片上外设并不需要经过 EXTI,EXTI 是专门处理外部中断的。

2.2 标志位 → 中断请求 → NVIC

USART 内部有 USART 中断控制 单元:

        当 状态寄存器(SR)中对应标志位被置位(收到数据 RXNE=1、发送寄存器空 TXE=1),且 USART 中断被使能,USART 就会向 NVIC 发起中断请求。

中断号:USART1_IRQn = 37(stm32f1xx.h 中枚举定义)。

2.3 两个开关(最重要的结论)

让 USART 以中断模式工作,必须开启两个开关:

开关内容在哪里开
① USART 内部中断CR1 中的 RXNEIE / TXEIE 等中断使能位HAL 库(__HAL_UART_ENABLE_IT)
② NVIC 中断USART1 的全局中断使能CubeMX 配置 / HAL_NVIC_EnableIRQ

缺一不可!CubeMX 勾选 "USART1 global interrupt" 后,NVIC 会自动生成使能代码。


三、实验目标与硬件分析

3.1 实验目标

  1. 串口配置:波特率 115200bps、8 位数据、奇校验(Odd)、停止位 1、无流控。
  2. 用串口中断模式接收上位机发来的测试数据。
  3. 用串口中断模式把数据回传到上位机。
  4. 用 LED 指示灯闪烁表示串口正在进行数据传输。

3.2 原理图:为什么需要 CH340K?

开发板串口电路使用 USB 转 USART 芯片 CH340K(等效"USB 转 UART TTL"模块):

笔记本电脑(USB 口)──USB──> CH340K ──TTL──> STM32(USART1)
                           Tx ───────> Rx
                           Rx <─────── Tx
                           GND ────── GND(共地)

原因:

        现代电脑(尤其笔记本)没有原生串口;

        MCU 只认识 TTL 电平串口信号,二者无法直接对话,需要 CH340K 这个"翻译官"。对软件应用来讲,电脑看到的是"串口",其实是 USB 转出来的。


四、CubeMX 工程配置(逐步)

4.1 仿真器接口:SWD

        SYS → Debug 选择 Serial Wire。

        ⚠️ 若 CubeMX 错误配置或禁止了 debug 接口(JTAG/SWD),烧录完程序后将无法再通过调试器连接目标 STM32。

4.2 时钟:8MHz HSE → 72MHz

        RCC 的 HSE 选 Crystal / Ceramic Resonator(外部晶振);

        开发板是 8MHz 晶振,配置到系统最大频率 72MHz:

时钟路径配置结果
HSE8 MHz8 MHz
PLL 倍频×9PLLCLK = 72 MHz
SYSCLK选 PLLCLK72 MHz
AHB 预分频(HCLK)/172 MHz
APB1 预分频(PCLK1)/236 MHz
APB2 预分频(PCLK2)/172 MHz

USART1 挂在 APB2 总线上,时钟源 72MHz。

4.3 USART1 基本参数

参数配置值说明
ModeAsynchronous异步通信模式
Hardware Flow ControlDisable不用硬件流控,只用 RX/TX
Baud Rate115200 Bits/s常见波特率
Word Length9 Bits (including Parity)8 位数据 + 1 位校验
ParityOdd奇校验
Stop Bits11 位停止位
Data DirectionReceive and Transmit收发模式
Over Sampling16 Samples过采样,默认即可

过采样(OverSampling):

        USART 接收器在一个位周期内对信号采样的次数,用于判断该位的逻辑状态。

4.4 引脚复用(使能串口后自动配置)

引脚信号GPIO 模式说明
PA9USART1_TXAlternate Func.(复用推挽)发送
PA10USART1_RXInput mode(下拉)接收

4.5 中断配置(NVIC)

        USART1 的 NVIC Settings 勾选 USART1 global interrupt,抢占优先级 0、子优先级 0。系统 NVIC 页面会自动出现 USART1 的中断信息。

4.6 LED 指示(PF8 / PF9)

        PF8、PF9 配置为 GPIO Output Push Pull(推挽输出),默认输出 High(灯熄灭,低电平点亮)。

        原理图中推挽、开漏都行。

        本例:PF8 绿灯指示收发状态,PF9 红灯指示接收溢出异常。

4.7 项目配置与生成

        Project Name 自定义;Toolchain/IDE 选 MDK-ARM(V5.32)。

        点击 GENERATE CODE 生成工程。


五、两个核心 API:函数签名与行为

5.1 HAL_UART_Receive_IT —— 中断模式接收

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart,
                                      uint8_t *pData,
                                      uint16_t Size);
参数含义
huartUART 句柄,需处于接收就绪状态
pData接收缓冲区起始地址(9 位无校验按 uint16_t*,其余按 uint8_t*)
Size接收的数据元素个数(不是字节数)
返回值含义
HAL_OK成功启动中断接收
HAL_BUSY当前有接收正在进行(RxState != READY)
HAL_ERROR入参非法(pData==NULL 或 Size==0)

这个函数做了什么?

核心是"设定一个接收计划,然后使能中断,让硬件在后台完成接收":

  1. 设定目标:告诉句柄数据放哪(pData)、收多少(Size);
  2. 开启中断:使能接收中断 RXNE,硬件随时响应新数据;
  3. 等待完成:每收到一字节,自动进入中断服务程序,HAL 库内部存入缓冲区并递减剩余计数,收满后触发用户层回调。

5.2 HAL_UART_Transmit_IT —— 中断模式发送

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart,
                                       const uint8_t *pData,
                                       uint16_t Size);
  • huart:UART 句柄;pData:待发送缓冲区指针;Size:待发送数据元素数量。
  • 字长与校验说明:Parity 无校验且 WordLength 为 9 位时按 u16 元素处理;其他情形按 u8。

做了什么?

  1. 设定计划:把 pData 和 Size 存入句柄内部变量;
  2. 开启中断:使能发送数据寄存器空中断 TXEIE;
  3. 中断触发:TDR 空则立即触发 TXE 中断,CPU 在 HAL 库中逐字节搬运,全部发完触发用户层回调。

5.3 关键认知(务必记住)

        上面的"发送"和"接收"并不是真的在收发数据,只是设定了计划、开启了中断。

        真正的收发发生在 ISR 被调用、回调执行时——数据流先经过 HAL 库内部,再到你的应用层回调函数。


六、完整工程代码(含详细注释)

6.1 全局变量与缓冲区

#include <stdbool.h>

/* USER CODE BEGIN PM */
#define BUFFER_SIZE 128
/* USER CODE END PM */

/* USER CODE BEGIN PV */
volatile static uint16_t rx_index = 0;      // 接收缓冲区当前索引位置
static uint8_t rx_buffer[BUFFER_SIZE];      // 接收缓冲区
static uint8_t tx_buffer[BUFFER_SIZE];      // 发送缓冲区(为可读性用两个)
/* USER CODE END PV */

6.2 main:初始化并启动第一次接收

int main(void)
{
    HAL_Init();
    SystemClock_Config();      // CubeMX 生成:时钟配置
    MX_GPIO_Init();            // CubeMX 生成:GPIO 配置
    MX_USART1_UART_Init();     // CubeMX 生成:USART1 配置(含中断使能)

    /* 关键:启动第一次中断接收,收 1 个字节到 rx_buffer[0] */
    HAL_UART_Receive_IT(&huart1, &rx_buffer[0], 1);

    while (1)
    {
        /* 主循环空闲,CPU 可做其他事 */
    }
}

        课件中 main 主体省略为 ......,但第一次接收必须在 main 中启动,否则永远收不到数据。每收到一字节后,回调里会再次调用 HAL_UART_Receive_IT 形成接收闭环。

6.3 数据处理:大小写互换

// 把收到的行数据,处理完写入到发送缓冲区
static void Handler_Data(void)
{
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_RESET); // 点亮绿灯:处理数据中

    // 做测试用(模拟耗时,让闪灯效果更明显)
    // volatile int cnt = 5000000;
    // while (cnt--);

    uint16_t rx_total = rx_index + 1;   // 含换行符在内的整行长度
    for (int i = 0; i < rx_total; i++)
    {
        if (rx_buffer[i] >= 'a' && rx_buffer[i] <= 'z')   // 小写 → 大写
        {
            tx_buffer[i] = rx_buffer[i] - 32;
        }
        else if (rx_buffer[i] >= 'A' && rx_buffer[i] <= 'Z') // 大写 → 小写
        {
            tx_buffer[i] = rx_buffer[i] + 32;
        }
        else
        {
            tx_buffer[i] = rx_buffer[i];   // 其他字符原样保留
        }
    }
}

6.4 接收完成回调 HAL_UART_RxCpltCallback

/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        // ① 溢出检查:接收超过缓冲区大小
        if (rx_index > BUFFER_SIZE - 1)
        {
            rx_index = 0;                                 // 恢复初始
            HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, GPIO_PIN_RESET); // 点亮红灯:溢出异常
            return;                                       // 直接返回,不再开启读中断
        }

        // ② 收到换行符:一行结束
        if (rx_buffer[rx_index] == '\n')
        {
            Handler_Data();                                            // 处理数据(大小写转换)
            HAL_UART_Transmit_IT(&huart1, tx_buffer, rx_index + 1);   // 开启中断发送,回传整行
            rx_index = 0;                                             // 复位,重新接收
        }
        else
        {
            rx_index++;   // 一次收一个字节,索引后移
        }

        // ③ 继续开启读中断(接收闭环)
        HAL_UART_Receive_IT(huart, &rx_buffer[rx_index], 1);
    }
}

6.5 发送完成回调 HAL_UART_TxCpltCallback

// 发送可是一次发多字节,底层全部发完才会触发该回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_SET); // 熄灭绿灯
    }
}

6.6 数据流逻辑小结

上位机发送一行(如 abcdEFG1234\n)
  ↓ 逐字节中断接收(每 1 字节触发一次 USART1 中断)
  ↓ 存入 rx_buffer,rx_index 递增
  ↓ 遇到 '\n'  →  Handler_Data() 大小写互换写入 tx_buffer
  ↓             →  HAL_UART_Transmit_IT() 开启中断发送
  ↓             →  rx_index = 0 复位
逐字节 TXE 中断发送 → 发完触发 HAL_UART_TxCpltCallback → 熄灭绿灯
上位机收到:ABCDefg1234\n(完成一次回环)

七、源码级解析:两条线

核心就两条线:调用线(软件使能) 和 回调线(中断驱动)。

7.1 调用线:HAL_UART_Transmit_IT 内部实现

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart,
                                       const uint8_t *pData, uint16_t Size)
{
    /* 检查发送进程是否已在进行中 */
    if (huart->gState == HAL_UART_STATE_READY)   // gState = 空闲状态
    {
        /* 参数校验:指针不能为空,长度不能为 0 */
        if ((pData == NULL) || (Size == 0U))
        {
            return HAL_ERROR;
        }
        /* 保存发送参数到句柄中 */
        huart->pTxBuffPtr  = pData;   // 指向要发送的数据
        huart->TxXferSize  = Size;    // 总发送长度
        huart->TxXferCount = Size;    // 剩余待发送字节数(中断里递减)
        /* 清除错误码,标记状态为"发送忙碌" */
        huart->ErrorCode = HAL_UART_ERROR_NONE;
        huart->gState = HAL_UART_STATE_BUSY_TX;
        /* 使能发送数据寄存器空中断(TXE)——一旦使能,TDR 为空立即触发中断 */
        __HAL_UART_ENABLE_IT(huart, UART_IT_TXE);
        return HAL_OK;
    }
    else
    {
        return HAL_BUSY;   // 串口正忙(可能正在发送或接收)
    }
}
__HAL_UART_ENABLE_IT 宏:三选一写寄存器
#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__) \
    ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX) ? ((__HANDLE__)->Instance->CR1 |= ((__INTERRUPT__) & UART_IT_MASK)) : \
     ((((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX) ? ((__HANDLE__)->Instance->CR2 |= ((__INTERRUPT__) & UART_IT_MASK)) : \
     ((__HANDLE__)->Instance->CR3 |= ((__INTERRUPT__) & UART_IT_MASK))))

原理:

        每个 UART_IT_XXX 是 32 位值——高 4 位(>>28)标识属于 CR1/CR2/CR3 哪个寄存器,低 16 位(& UART_IT_MASK)是寄存器内的对应位。

        宏按高 4 位选择寄存器并置位。

#define UART_IT_TXE  ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_TXEIE)) // TXE 中断
#define USART_CR1_TXEIE_Pos  (7U)
#define USART_CR1_TXEIE_Msk  (0x1UL << USART_CR1_TXEIE_Pos)  // 0x00000080
#define USART_CR1_TXEIE      USART_CR1_TXEIE_Msk

7.2 调用线:HAL_UART_Receive_IT → UART_Start_Receive_IT

HAL_StatusTypeDef UART_Start_Receive_IT(UART_HandleTypeDef *huart,
                                        uint8_t *pData, uint16_t Size)
{
    /* 保存接收参数(供中断服务函数使用) */
    huart->pRxBuffPtr  = pData;   // 指向接收缓冲区
    huart->RxXferSize  = Size;    // 总共要接收的字节数
    huart->RxXferCount = Size;    // 剩余待接收字节数(递减到 0 表示收完)
    /* 清除错误码,状态改为"接收忙碌" */
    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* 开启了奇偶校验 → 使能奇偶校验错误中断(PE) */
    if (huart->Init.Parity != UART_PARITY_NONE)
    {
        __HAL_UART_ENABLE_IT(huart, UART_IT_PE);
    }
    /* 使能错误中断(帧错误、噪声错误、溢出错误 ORE/NE/FE) */
    __HAL_UART_ENABLE_IT(huart, UART_IT_ERR);
    /* 使能接收数据寄存器非空中断(RXNE)——最关键的一步! */
    __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);

    return HAL_OK;
}
#define UART_IT_RXNE ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_RXNEIE))
#define USART_CR1_RXNEIE_Pos  (5U)
#define USART_CR1_RXNEIE_Msk  (0x1UL << USART_CR1_RXNEIE_Pos) // 0x00000020
#define USART_CR1_RXNEIE      USART_CR1_RXNEIE_Msk

7.3 USART_CR1 关键中断使能位

位名称含义相关中断宏
bit5RXNEIE接收数据寄存器非空中断使能UART_IT_RXNE
bit6TCIE发送完成中断使能UART_IT_TC
bit7TXEIE发送数据寄存器空中断使能UART_IT_TXE
bit8PEIE奇偶校验错误中断使能UART_IT_PE

7.4 MspInit:引脚 + NVIC(CubeMX 生成)

void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (huart->Instance == USART1)
    {
        __HAL_RCC_USART1_CLK_ENABLE();   // 使能 USART1 时钟
        __HAL_RCC_GPIOA_CLK_ENABLE();    // 使能 GPIOA 时钟

        /* PA9  ------> USART1_TX */
        GPIO_InitStruct.Pin   = GPIO_PIN_9;
        GPIO_InitStruct.Mode  = GPIO_MODE_AF_PP;        // 复用推挽输出
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

        /* PA10 ------> USART1_RX */
        GPIO_InitStruct.Pin  = GPIO_PIN_10;
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;         // 输入模式
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

        /* USART1 interrupt Init */
        HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);        // 优先级 0/0
        HAL_NVIC_EnableIRQ(USART1_IRQn);                // 开关②:使能 NVIC 中断
    }
}
// USART1_IRQn = 37

7.5 调用线结论

  • 中断方式收发函数,并不是收发数据,而是使能 USART 内部的中断标志位;
  • 让 USART 以中断模式工作需要两个开关:USART 自身中断开关(CR1 使能位)+ NVIC 的 USART 中断开关。

7.6 回调线:中断服务例程链路

  • USART 的收发寄存器各自只能收发一个字节,所以中断触发的单位就是字节;
  • 无论是收还是发,中断号一样、优先级一样、执行的中断服务程序一样——收发回调的是同一个中断服务例程 USART1_IRQHandler;
  • 收发共用同一个 ISR、优先级相同,中断处理函数之间不会互相抢占嵌套,只会交叉执行。

启动文件 startup_stm32f103xe.s 中:

DCD     USART1_IRQHandler   ; USART1

中断服务函数(stm32f1xx_it.c):

void USART1_IRQHandler(void)
{
    HAL_UART_IRQHandler(&huart1);   // HAL 库统一处理:读标志、搬数据、调回调
}

  HAL_UART_IRQHandler 内部会检查 RXNE、TXE、ORE、NE、FE、PE 等标志,执行数据搬运(DR 读到缓冲区 / 缓冲区字节写入 DR),收满/发完再调用用户回调(HAL_UART_RxCpltCallback / HAL_UART_TxCpltCallback)。

        中断里先过 HAL 库,再到应用层回调。


7.7 完整调用链总结

【调用线】应用层 HAL_UART_Receive_IT / Transmit_IT
   → HAL 库保存缓冲区指针 + 计数 + 状态置 BUSY
   → 使能 USART 中断位(开关①:RXNEIE/TXEIE...)
   →(开关② NVIC 使能已由 CubeMX 配置好)

【回调线】硬件事件(收到字节 RXNE=1 / TDR 空 TXE=1)
   → USART 请求 NVIC → Cortex-M3 响应
   → USART1_IRQHandler → HAL_UART_IRQHandler
   → 数据搬运 → 用户回调 RxCpltCallback / TxCpltCallback
   → 应用层处理 / 继续调用 IT 函数形成闭环

八、核心知识点总结(面试/考试考点)

  1. USART 是片上外设中断,直接连 NVIC,不经过 EXTI。
  2. 中断模式两个开关:USART 内部中断使能(CR1 位)+ NVIC 全局中断使能,缺一不可。
  3. HAL_UART_Receive_IT / Transmit_IT 只是"设定计划 + 使能中断",不真正搬数据;真正的收发发生在 ISR 回调链路。
  4. 收发共用同一个中断服务例程(USART1_IRQHandler),中断号 37,按字节触发。
  5. 回调执行顺序:硬件 → NVIC → ISR → HAL_UART_IRQHandler → 应用层回调。
  6. 中断内部不要调用 HAL_Delay();闪灯效果不明显可用 while(x) 模拟耗时观察。

易踩坑提醒

  • CubeMX 禁掉 SWD → 芯片无法再次烧录;
  • 忘记在 main 里启动第一次 HAL_UART_Receive_IT → 永远收不到数据;
  • 缓冲区溢出未处理 → 数据错乱;
  • 中断里做耗时操作 → 丢失后续字节(本例每字节都触发一次中断)。

面试题

一、基础概念类

1. 轮询、中断、DMA 三种收发模式有什么区别?

模式CPU 参与效率适用
轮询死等标志位,阻塞低简单单字节交互
中断事件触发,空闲可做别的高高频 / 不定长数据
DMA硬件搬运,CPU 完全释放最高大批量连续数据

2. USART 属于哪一类中断源?为什么不经过 EXTI?

  • 属于片上外设中断(自带中断模块,直接连 Cortex-M3 的 NVIC)。
  • EXTI 只负责外部引脚信号的中断;USART 是芯片内部外设,信号在片内走,无需 EXTI 中转。

3. 让 USART 以中断模式工作需要哪 "两个开关"?缺一个会怎样?

  • ① USART 内部中断开关:CR1 寄存器的 RXNEIE/TXEIE 等使能位(HAL 库 __HAL_UART_ENABLE_IT 设置)。
  • ② NVIC 中断开关:USART1_IRQn(=37)全局使能(CubeMX 勾选 / HAL_NVIC_EnableIRQ)。
  • 缺①:标志位置位但不产生中断请求;
  • 缺②:请求到了 NVIC 也不响应 → 中断永远不执行。

4. 为什么说中断触发的单位是字节?

         USART 的发送 / 接收数据寄存器(TDR/RDR)各只能存 1 个字节,

        所以每收 / 发一个字节就会置位一次 RXNE/TXE 并触发一次中断。


5. HAL_UART_Receive_IT 真的在接收数据吗?

  • 不是。它只做三件事:保存缓冲区指针和计数 → 状态置 BUSY → 使能 RXNE 中断。
  • 真正收数据发生在 ISR 回调链路:硬件收到字节 → NVIC → USART1_IRQHandler → HAL_UART_IRQHandler → 应用层回调。

6. 接收和发送共用中断服务函数吗?

  • 共用。收发的中断号一样(USART1_IRQn=37)、优先级一样,都进 USART1_IRQHandler。
  • 因为共用且同级,中断处理函数之间不会抢占嵌套,只会交叉执行。

7. 开发板为什么需要 CH340K 芯片?

  • 电脑(尤其笔记本)没有原生串口,而 STM32 只认 TTL 电平串口信号,二者无法直接通信。
  • CH340K 做 USB ↔ UART TTL 转换 "翻译官";对软件来讲电脑看到的是 "串口"。

8. 为什么中断回调里不能调用 HAL_Delay ()?

  • HAL_Delay 依赖 SysTick 中断,在串口中断里调用会引入嵌套 / 优先级问题。
  • 阻塞式延时期间,同优先级的串口中断无法及时响应,后续字节会丢失。

二、源码 / 寄存器类

9. __HAL_UART_ENABLE_IT 宏的工作原理?

  • 每个 UART_IT_XXX 中断号是 32 位值:高 4 位(>>28)标识该位属于 CR1/CR2/CR3 哪个寄存器,低 16 位(& UART_IT_MASK)是寄存器内对应位。
  • 宏用三目运算符按高 4 位选寄存器,再对选中寄存器做 |= 置位。

10. 为什么 UART_IT_TXE = (1<<28) | 0x80?

  • 高 4 位 1 = UART_CR1_REG_INDEX,表示 TXE 使能位在 CR1;
  • 低 16 位 0x80 = USART_CR1_TXEIE(CR1 的 bit7,发送数据寄存器空中断使能)。

11. UART_Start_Receive_IT 做了哪些事?

  1. 保存 pRxBuffPtr / RxXferSize / RxXferCount(剩余计数,中断里递减);
  2. RxState = BUSY_RX;
  3. 开了奇偶校验 → 使能 PE 中断;
  4. 使能 ERR 错误中断(ORE/NE/FE);
  5. 使能 RXNE 接收中断(最关键)。

12. 一次完整中断回调的执行顺序?

硬件收到字节 → RXNE=1 → USART 请求 NVIC
→ Cortex-M3 响应 → USART1_IRQHandler
→ HAL_UART_IRQHandler(读标志、搬数据)
→ 数据搬完 → HAL_UART_RxCpltCallback(应用层回调)

口诀:硬件 → NVIC → ISR → HAL 库 → 应用回调。


13. gState 和 RxState 是什么?

  • 句柄里的两个独立状态机:gState 管发送、RxState 管接收,初始 READY,启动后置 BUSY_TX / BUSY_RX。
  • 所以收发可以同时进行,互不干扰;忙时再调用返回 HAL_BUSY。

14. 接收缓冲区索引 rx_index 为什么要加 volatile?

        它在中断回调里被修改,而主循环 / 其他代码可能读取它。

  volatile 防止编译器优化时把它缓存到寄存器,保证每次访问都读内存最新值。


三、工程实践类

15. 如果 main 里忘记调用 HAL_UART_Receive_IT(&huart1, &rx_buffer[0], 1) 会发生什么?

  • 永远收不到数据。因为回调里每次只重新使能 "下一次" 接收,第一次启动必须在 main 完成 —— 否则接收中断根本没被打开。

16. 缓冲区溢出如何处理?(本示例的做法)

  • 回调开头检查 rx_index > BUFFER_SIZE - 1:溢出则复位索引、点亮红灯(PF9)指示异常、直接 return 不再开启读中断,防止越界写。

17. 本例配置 "9 位数据 + 奇校验" 怎么理解?Size 参数是什么单位?

  • 9 Bits = 8 位数据 + 1 位校验(Word Length 含校验位);
  • 校验开启时按 u8 元素计数;若 "无校验 + 9 位字长",则按 u16 元素计数,pData 要按 uint16_t* 用。

18. 收到什么信号判定 "一行数据" 结束?

  • 收到 '\n'(换行符)。回调里判断 rx_buffer[rx_index] == '\n',成立则说明整行收完 → 处理数据(大小写互换)→ HAL_UART_Transmit_IT 回传 → rx_index = 0 复位。

19. 两个 LED 分别干什么?

  • PF8 绿灯:数据处理中点亮、发送完成熄灭(指示收发进行中);
  • PF9 红灯:接收溢出异常时点亮告警。

四、进阶拓展类

20. 如果上位机一次发 100 字节,中断会触发几次?

  • 触发 100 次(每字节一次 RXNE 中断),回调里逐字节存缓冲区。
  • 这正是字节级中断的特点:代码简单,但高频时 CPU 开销大;大批量场景应换 DMA + 空闲中断。

21. 中断模式下如何避免丢字节?(要点)

  • 回调里别做耗时操作(别 HAL_Delay、别大循环),尽快重新使能下一次接收;
  • 缓冲区要够大并做溢出保护;
  • 提高 NVIC 抢占优先级,避免被低优先级中断长期打断。

22. NVIC 抢占优先级和子优先级怎么理解?

  • 抢占优先级:能打断低抢占级的中断(本例分组 4 位全给抢占,USART1 为 0,最高);
  • 子优先级:同抢占级时决定响应先后,不能打断同级。
  • 本例 USART1 配置抢占 0、子 0,即最高优先级,保证串口响应及时。

23. 串口参数 "115200, 8O1" 是什么意思?

  • 波特率 115200bps;8 位数据;O = Odd 奇校验(9 位帧含校验位);1 位停止位。与 "8N1"(无校验)的帧格式不同,两端必须一致才能通信。

24. 如果把中断收发改成 DMA 收发,核心差别在哪?

  • 中断:每字节触发一次 ISR,CPU 逐字节搬运;
  • DMA:配置好源 / 目的 / 长度后,硬件自动搬运,搬完只触发一次完成中断,CPU 开销最小,适合大数据量连续传输。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2602_95649725/article/details/166738804

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--