【STM32】 DMA 源码分析与实战(下):DMA 收发状态机与 HAL 分层

DMA 收发状态机与 HAL 分层

1. DMA 收发状态机

1.1 为什么要引入状态机?

从应用角度我们已经知道,比较推荐用**"中断修改标志位,主循环进行逻辑处理"的开发方式。为了让代码设计更具健壮性和可扩展性**,我们引入状态机处理。

引入状态机理由(FSM Logic):

条目 准则 理由
总纲 用状态流代替线性逻辑 **实现收发时序解耦,**确保硬件资源(如缓冲区)在确定状态下被安全访问
1. 嵌套风险规避 (Stack Safety) 严禁在中断内调用耗时或依赖其他中断的函数(如 HAL_Delay) 防止优先级冲突导致永久死锁, 降低深层嵌套引发的栈溢出风险
2. 逻辑归位准则 (Context Separation) 中断负责"事件通知", 主循环负责"逻辑决策" **保证 CPU 在干净的上下文中处理复杂任务,**提高系统的鲁棒性和可维护性

1.2 状态机核心代码

状态枚举与全局变量(USER CODE 区域):

cpp 复制代码
/* USER CODE BEGIN PTD */
typedef enum {
    USART_STATE_IDLE,        // 空闲:可以发起接收
    USART_STATE_RXING,       // 接收中:USART+DMA 正在收数据
    USART_STATE_DATA_READY,  // 数据就绪:收到了一包,可以处理
    USART_STATE_TXING,       // 发送中:USART+DMA 正在发数据
    USART_STATE_ERROR        // 错误:需要恢复
} usart_state_t;
/* USER CODE END PTD */

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

/* USER CODE BEGIN PV */
uint8_t rx_buffer[BUFFER_SIZE];
uint8_t tx_buffer[BUFFER_SIZE];
volatile usart_state_t g_state = USART_STATE_IDLE;   // 全局状态,初始 IDLE
/* USER CODE END PV */

void TransformCase(uint8_t *dst, uint8_t *src, uint16_t len);

main 初始化后(USER CODE 2):先发一条测试提示,验证串口链路:

复制代码
// for test
const char *tips = "This Is State Machine Test\n";
HAL_UART_Transmit_DMA(&huart1, tips, strlen(tips));
HAL_Delay(1000);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, GPIO_PIN_SET);  // 关灯

主循环:switch(g_state) 状态机:

cpp 复制代码
while (1)
{
    switch (g_state)
    {
        case USART_STATE_IDLE:                       // ① 空闲:尝试启动接收
            if (HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE) == HAL_OK)
                g_state = USART_STATE_RXING;         //    启动成功 → 接收中
            else
                g_state = USART_STATE_ERROR;         //    启动失败 → 错误
            break;

        case USART_STATE_RXING:                      // ② 接收中:什么都不做
            // 说明USART+DMA正在进行接收数据中
            // 如果还有其他事情,就可以让CPU在这里完成
            break;

        case USART_STATE_DATA_READY:                 // ③ 数据就绪:业务逻辑处理
            TransformCase(tx_buffer, rx_buffer, BUFFER_SIZE);  // 大小写转换
            HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8);            // 翻转 LED
            if (HAL_UART_Transmit_DMA(&huart1, tx_buffer, BUFFER_SIZE) == HAL_OK)
                g_state = USART_STATE_TXING;         //    发送启动成功 → 发送中
            else
                g_state = USART_STATE_ERROR;
            break;

        case USART_STATE_TXING:                      // ④ 发送中:什么都不做
            // 说明USART+DMA正在进行发送数据中
            // 如果还有其他事情,就可以让CPU在这里完成
            break;

        case USART_STATE_ERROR:                      // ⑤ 错误:恢复现场
            HAL_UART_DMAStop(&huart1);               // 停止DMA,重置相关配置寄存器
            __HAL_UART_CLEAR_OREFLAG(&huart1);       /* 必须手动清除溢出标志,否则无法重启 */
            HAL_Delay(300);                          // 给点时间,让重启生效
            g_state = USART_STATE_IDLE;              // 重新设置为空闲,重新开始
            break;

        default:
            break;
    }
}

三个回调:中断只改状态,不做业务(逻辑归位准则的直接体现):

cpp 复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 中断触发,说明数据接收完成了
        g_state = USART_STATE_DATA_READY;   // 通知主循环:数据就绪
    }
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 中断触发,说明数据发送完成了,也就完成了一次收发,重新回到IDLE状态
        g_state = USART_STATE_IDLE;
    }
}

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 触发错误了,也是修改错误标志位,main的while循环统一处理
        g_state = USART_STATE_ERROR;
    }
}

TransformCase 与之前完全一致(ctype.h 大小写互转,含空指针/长度为 0 校验)。

1.3 状态迁移图

整个收发过程用状态机表达,比"标志位 + if 堆叠"清晰得多------每个时刻系统处于且只处于一个确定状态:

1.4 异常演练:验证状态机真的能"扛住"错误

正常测试结果:串口助手(COM4 / 115200 / 8 位 / Odd 奇校验 / 1 停止位 / None)先收到 This Is State Machine Test,随后每次发送 abcdBIT 都回显转换后的 ABCDbit,循环往复。

如何测试异常?

步骤 内容
方法 在 while(1) 的 USART_STATE_DATA_READY 分支里加一个长延时
操作 使用上位机以高频率(例如每 1ms 发一包)连续发送数据
现象 CPU 被 Delay 堵住,且 DMA 是普通模式(回调之后会关闭 DMA),无法及时处理 USART 数据,串口硬件迅速产生溢出(ORE)
验证点 观察程序是否能跳进 USART_STATE_ERROR,执行清除动作,并在延时结束后恢复正常收发

测试代码(其他代码不变):

cpp 复制代码
int first = 0;   // 只在最开始测试一次,后续方便查看恢复之后的结果
while (1)
{
    switch (g_state)
    {
        case USART_STATE_IDLE:
            if (HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE) == HAL_OK)
                g_state = USART_STATE_RXING;
            else
                g_state = USART_STATE_ERROR;
            break;

        case USART_STATE_RXING:
            break;

        case USART_STATE_DATA_READY:
            // 如果我们在该状态让程序长时间处于休眠状态:即便收到数据也一直不处理,
            // 同时让 comtool 持续发数据;因为是普通模式 DMA,收完即关闭,
            // 就会导致 USART 接收过载,进而触发错误
            if (first == 0)
                HAL_Delay(2000);          // ← 人为制造"来不及处理"的窗口
            // 业务逻辑处理
            TransformCase(tx_buffer, rx_buffer, BUFFER_SIZE);
            HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8);
            // 启动发送
            if (HAL_UART_Transmit_DMA(&huart1, tx_buffer, BUFFER_SIZE) == HAL_OK)
                g_state = USART_STATE_TXING;
            else
                g_state = USART_STATE_ERROR;
            break;

        case USART_STATE_TXING:
            break;

        case USART_STATE_ERROR:
            if (huart1.Instance->SR & UART_FLAG_ORE)   // 确认是 ORE 问题
            {
                if (first == 0) {
                    // 让灯闪烁20次,表示进入到了异常
                    first = 1;
                    int cnt = 20;
                    while (cnt--) {
                        HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_8);
                        HAL_Delay(200);
                    }
                }
                HAL_UART_DMAStop(&huart1);             // 停止DMA,重置相关配置寄存器
                __HAL_UART_CLEAR_OREFLAG(&huart1);     /* 必须手动清除溢出标志,否则无法重启 */
                HAL_Delay(300);                        // 给点时间,让重启生效
                g_state = USART_STATE_IDLE;            // 重新设置为空闲,重新开始
            }
            break;

        default:
            break;
    }
}

配合测试的一个小技巧:在 DMA1_Channel5_IRQHandler 的 USER CODE 0 区加一句:

cpp 复制代码
void DMA1_Channel5_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel5_IRQn 0 */
  HAL_Delay(500); // 让DMA接收中断慢一点,暂时不执行后续关闭操作,从而让usart有机会报错
  /* USER CODE END DMA1_Channel5_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_usart1_rx);
  ...
}

调整优先级,让系统滴答能够正确嵌套,进而不会卡死:

NVIC 界面中把 DMA1 channel5 global interrupt 的 Preemption Priority 设为 15(最低),保证 SysTick(Time base)能抢占/正常嵌套,否则中断里 HAL_Delay 依赖的 SysTick 被同优先级卡住会造成死等。

测试结果:

程序运行中会看到灯爆闪(20 次),爆闪之后,也能恢复到最开始接收的正确逻辑。

细节 1:__HAL_UART_CLEAR_OREFLAG 这个函数去掉,如果也能恢复该怎么理解?------主要是因为 HAL 库在其他模块可能帮你清除了,但从代码的健壮性角度,显式清除比较妥当。

细节 2:HAL_UART_DMAStop 是做什么的?

复制代码
HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);
项 内容
参数 huart:UART 句柄指针,包含串口配置信息和状态
返回值 HAL_OK(成功停止)/ HAL_ERROR(停止出错)/ HAL_BUSY(硬件忙)
核心逻辑 ① 禁用 USART 硬件的 DMA 使能: 往 CR3 写位,关闭 DMAR(接收 DMA 使能)和 DMAT(发送 DMA 使能)
核心逻辑 ② 终止 DMA 通道: 调用 HAL_DMA_Abort 强制停止关联的发送和接收 DMA 通道
核心逻辑 ③ 恢复软件状态: 将 huart->RxState 和 huart->gState 重置为 HAL_UART_STATE_READY

一句话:

将 DMA 通道和 USART 的相关状态寄存器恢复成为初始状态,方便重新启动;

主要应用场景就是出错后进行状态恢复。

2. 附录

2.1 其他片上外设的 DMA 怎么理解?(触类旁通)

对片上外设来说,中断 ISR 函数回调到用户层的 weak 函数,与 DMA ISR 函数回调到用户层的 weak 函数,函数名是一样的(比如 HAL_UART_RxCpltCallback)。

对比结果(节选核心):

外设 事件类型 中断模式(IT)用户 Weak 函数 是否相同 备注
USART 接收完成 HAL_UART_RxCpltCallback() ✅ 完全相同
USART 发送完成 HAL_UART_TxCpltCallback() ✅ 完全相同
USART 接收半完成 --- ❌ DMA 独有
USART 发送半完成 --- ❌ DMA 独有
USART 错误 HAL_UART_ErrorCallback() ✅ 完全相同
USART 唤醒 HAL_UART_WakeupCallback() ✅ 完全相同
SPI 接收完成 HAL_SPI_RxCpltCallback() ✅ 完全相同
SPI 发送完成 HAL_SPI_TxCpltCallback() ✅ 完全相同
SPI 收发完成 HAL_SPI_TxRxCpltCallback() ✅ 完全相同
SPI 接收半完成 --- ❌ DMA 独有
SPI 发送半完成 --- ❌ DMA 独有
SPI 错误 HAL_SPI_ErrorCallback() ✅ 完全相同
I2C 主机发送完成 HAL_I2C_MasterTxCpltCallback() ✅ 完全相同
I2C 主机接收完成 HAL_I2C_MasterRxCpltCallback() ✅ 完全相同
I2C 从机发送完成 HAL_I2C_SlaveTxCpltCallback() ✅ 完全相同
I2C 从机接收完成 HAL_I2C_SlaveRxCpltCallback() ✅ 完全相同
I2C Memory 发送完成 HAL_I2C_MemTxCpltCallback() ✅ 完全相同(EEPROM 等)
I2C Memory 接收完成 HAL_I2C_MemRxCpltCallback() ✅ 完全相同
I2C 错误 HAL_I2C_ErrorCallback() ✅ 完全相同
I2C 半完成 --- --- --- I2C 协议无半完成概念

这种统一机制对用户来讲,意义是什么?

  • 业务逻辑与底层传输解耦:你写的回调函数名,无论底层是走中断还是走 DMA,完全不用变;
  • 符合**"面向接口编程"**设计模式(降低学习与记忆成本);
  • 结果就是:隐藏复杂性,暴露统一抽象。

2.2 为什么要有句柄结构体,为什么会有 stm32f1xx_hal_msp.c 这样的软件层?

就一个问题,直接写寄存器不就好了,为什么搞这么复杂?

还要什么结构体句柄、保存属性,然后再通过结构体设置好的属性去设置寄存器?

答案:

  • 底层硬件差异很大:即便都是串口、都是 DMA,不同厂商、不同版本都可能不同;但是都会有同样的属性类型(比如控制寄存器、状态寄存器),只是设置方式可能不同;
  • HAL 库为了屏蔽这些底层差异,就使用结构体句柄把相关配置先在软件层面保存一份,然后再调用底层不同的外设初始化方式------这样即便底层硬件配置模式发生变化,只需要更改底层初始化方式,而不用改结构体句柄及其以上的应用层;
  • 本质是代码解耦和增加可扩展性的表现。

stm32 + HAL 软硬件是层状结构,一共 4 层:

层级 名称 代表文件 核心职能 形象比喻
第一层 应用层(Application) main.c, app.c 处理具体业务逻辑, 调用 HAL 接口实现功能 驾驶员:决定去哪
第二层 硬件抽象层(HAL) stm32f1xx_hal_uart.c 提供统一 API (如 HAL_UART_Init), 屏蔽不同型号芯片的寄存器差异 汽车控制系统:不管底盘什么样
第三层 底层接口层(MSP) stm32f1xx_hal_msp.c 处理与具体电路板相关的硬件配置 (时钟、引脚、中断优先级) 底盘接线:确定这辆车的发电机怎么供电
第四层 寄存器定义层(CMSIS) stm32f103xe.h 提供寄存器基地址和位偏移的宏定义,直接操作物理硬件 物理零件:发动机、电线

这就是为什么 DMA 和串口的初始化工作是在 XXX_msp 函数中进行的原因------HAL 层只做"通用逻辑",凡是跟具体板卡(哪个引脚、哪路时钟、哪个中断优先级)有关的,全部下沉到 MSP 层,换板子只需换 MSP,应用代码一行不动。


配套面试题

一、概念理解题

Q1. 为什么要引入状态机?(FSM Logic)

A:核心是用状态流代替线性逻辑,实现收发时序解耦,确保硬件资源(如缓冲区)在确定状态下被安全访问。具体依据两条准则:

  • 嵌套风险规避(Stack Safety):严禁在中断内调用耗时或依赖其他中断的函数(如 HAL_Delay),防止优先级冲突导致永久死锁、降低深层嵌套引发的栈溢出风险;
  • 逻辑归位准则(Context Separation):中断负责"事件通知",主循环负责"逻辑决策",保证 CPU 在干净上下文中处理复杂任务,提高鲁棒性和可维护性。

Q2. "标志位 + if 判断"和"状态机"两种开发方式有什么区别?

A:

维度 标志位 + if 状态机(switch(g_state))
状态表达 多个 bool 标志隐式组合,靠记忆推演当前处于什么阶段 一个枚举变量显式表达"现在处于哪个状态",无歧义
非法组合 可能出现"两个标志同时为真"的矛盾状态 任一时刻只有一个状态,结构上不可能矛盾
迁移逻辑 散落在各处 if 里,改一处容易漏一处 迁移条件集中在每个 case 内,可审计
扩展性 加一个阶段=加一个标志+加若干处 if 加一个状态=加一个枚举值+一个 case,互不影响
错误处理 没有统一的错误出口,易遗漏 统一的 ERROR 状态集中恢复

Q3. 状态机中"中断负责事件通知、主循环负责逻辑决策"具体怎么体现?

A:

三个回调里只修改 g_state(DATA_READY / IDLE / ERROR),不做任何业务处理;

真正的业务(大小写转换、翻转 LED、启动发送、错误恢复)全部放在主循环switch(g_state) 的各分支里。

中断上下文短小、不阻塞,复杂逻辑在干净的主循环上下文执行。


Q4. 为什么中断内严禁调用 HAL_Delay 这类函数?

A:两点:

  1. 优先级冲突死锁:HAL_Delay 依赖 SysTick 中断,若当前中断优先级 ≥ SysTick(或嵌套方式不恰当),SysTick 无法抢占,Delay 永远等不到 tick,系统卡死;
  2. 栈溢出风险:中断内再嵌套调用耗时函数会加深调用层级,栈空间有限,深层嵌套容易溢出。

二、状态机代码题

Q5. 状态机五个状态各自的含义和迁移条件是什么?

A:

状态 含义 进入条件 迁出条件
IDLE 空闲,唯一可发起接收 初始化 / TX 完成 / ERROR 恢复后 HAL_UART_Receive_DMA 成功 → RXING;失败 → ERROR
RXING USART+DMA 正在接收 IDLE 启动接收成功 RxCpltCallback 触发 → DATA_READY
DATA_READY 收到一包,数据就绪 接收完成回调 HAL_UART_Transmit_DMA 成功 → TXING;失败 → ERROR
TXING USART+DMA 正在发送 DATA_READY 启动发送成功 TxCpltCallback 触发 → IDLE(一次收发闭环)
ERROR 错误,统一恢复 启动失败 / ErrorCallback(ORE 等) 恢复动作完成后 → IDLE

Q6. 为什么 IDLE 启动 HAL_UART_Receive_DMA 失败时进入 ERROR 而不是原地重试**?**

A:

原地重试会形成紧循环:一旦失败原因不消除(如 ORE 标志未清、DMA 状态未归位),会一直空转重试,浪费 CPU 且可能加剧错误。

进 ERROR 先恢复现场(停 DMA、清标志、等 300ms 让硬件稳定),再回 IDLE 重来,保证每次启动前外设都处于已知干净状态。


Q7. RXING / TXING 状态"什么都不做"的意义是什么?

A:

这是状态机的价值所在------明确告诉系统"当前正忙,不该再发起新的操作"。

防止在接收/发送进行中重复调用 HAL_UART_Receive_DMA / HAL_UART_Transmit_DMA(会因 State=BUSY 失败或产生干扰)。

同时这两个分支留给 CPU 干其他事情(处理别的任务),体现"非阻塞 + 分时复用"。


Q8. 为什么回调函数里"只改状态标志"而不直接做业务或启动新 DMA?

A:

① 中断里做业务会阻塞其他中断、拉长中断响应;

② 回调触发时外设句柄 State 可能还是 BUSY(HAL 尚未完全归位),此时直接调用 HAL_UART_Receive_DMA 会启动失败,且存在寄存器未清理/死锁风险;

③ 把决策放回主循环,逻辑集中、可读、可调试。


Q9. 普通模式 DMA 收完即关闭 DMA,这个特性在状态机设计中会造成什么问题?怎么解决?

A:

普通模式下收满一包后 DMA 自动关闭(中断使能也被关),如果 CPU 迟迟不来重新启动接收(比如主循环被长延时堵住),USART 新来的数据没人搬运 → USART 接收过载(ORE 溢出)。

解决:用状态机统一管理,ERROR 分支中停 DMA + 清 ORE + 延时 + 回 IDLE 重新启动;业务处理尽量短,或换成循环模式(硬件自动续接)。


三、异常与恢复题

Q10. 如何人为构造一次 DMA 收发异常?现象和验证点是什么?

A:

  • 方法:在 USART_STATE_DATA_READY 分支加一个长延时(如 HAL_Delay(2000),只在第一次执行);
  • 操作:上位机以高频率(如每 1ms 一包)连续发数据;
  • 现象:CPU 被 Delay 堵住 + 普通模式 DMA 收完即关 → USART 数据来不及处理 → 硬件迅速产生 ORE 溢出;
  • 验证点:程序能否跳进 ERROR 状态、执行清除动作(灯闪 20 次指示)、延时结束后恢复正常收发。

Q11. ERROR 分支的处理顺序为什么是:DMAStop → 清 ORE → Delay(300) → IDLE?

A:每步有明确目的:

  1. HAL_UART_DMAStop(&huart1):停掉 DMA 并重置收发相关寄存器(关 DMAR/DMAT、Abort 通道、State 置 READY);
  2. __HAL_UART_CLEAR_OREFLAG(&huart1):必须手动清除 ORE 溢出标志,否则 USART 认为仍处于错误状态,无法重启接收;
  3. HAL_Delay(300):给硬件一点时间让"停止→重启"生效;
  4. g_state = IDLE:回到唯一能发起接收的状态,重新开始。

Q12. HAL_UART_DMAStop 函数内部做了哪三件事?

A:

① 禁用 USART 硬件 DMA 使能------往 CR3 写位关闭 DMAR(接收)和 DMAT(发送);

② 终止 DMA 通道------调用 HAL_DMA_Abort 强制停止收发 DMA 通道;

③ 恢复软件状态------将 huart->RxState 和 huart->gState 重置为 HAL_UART_STATE_READY。

一句话:

把 DMA 通道和 USART 相关状态寄存器恢复成初始状态,主要用在出错后的状态恢复。


Q13. 测试时为什么要在 DMA1_Channel5_IRQHandler 里加 HAL_Delay(500)?还要把 DMA1_Channel5 抢占优先级调到 15?

A:

  • 中断里加 HAL_Delay(500):人为拖慢 DMA 中断的处理,让"关闭 DMA"这步迟迟不执行,从而给 USART 制造出"溢出窗口",方便复现错误;
  • 调整优先级(抢占优先级 15,最低):HAL_Delay 依赖 SysTick(Time base),必须保证 SysTick 能抢占 DMA 中断(即 DMA 中断优先级低于 SysTick),否则 Delay 在 DMA 中断里永远等不到 tick,系统卡死------这正是"嵌套风险规避"准则的实战验证。

Q14. 去掉 __HAL_UART_CLEAR_OREFLAG 有时也能恢复,怎么理解?

A:

HAL 库其他模块(如错误处理路径、后续的收发起始代码)在特定流程下可能顺带清掉了 ORE 标志,所以没显式清除也能跑通。

但从代码健壮性角度,显式清除更妥当------不依赖"恰好被其他代码清掉"这种隐性行为,保证错误恢复在任何路径下都可靠。


四、对比与扩展题

Q15. 为什么 USART / SPI / I2C 的"中断模式 weak 回调"和"DMA 模式 weak 回调"函数名完全相同?这对用户意味着什么?

A:

HAL 统一了"传输完成/错误"的抽象------对用户来说,无论底层走中断还是 DMA,回调函数名都一致(如 HAL_UART_RxCpltCallback)。

意味着:

① 业务逻辑与底层传输解耦,换传输方式不用改上层代码;

② 符合**"面向接口编程"设计模式,降低学习与记忆成本;

③ 最终效果是隐藏复杂性、暴露统一抽象**。

注意两点:

半完成(HalfCplt)回调是 DMA 独有(中断模式没有"搬到一半"的概念);

I2C 协议没有半完成概念。


Q16. stm32 + HAL 的软件分为哪四层?为什么 DMA 和串口的初始化放在 XXX_msp 函数里?

A:

层级 名称 代表文件 职能 比喻
第一层 应用层 main.c / app.c 业务逻辑,调用 HAL 接口 驾驶员:决定去哪
第二层 硬件抽象层 HAL stm32f1xx_hal_uart.c 统一 API,屏蔽寄存器差异 汽车控制系统
第三层 底层接口层 MSP stm32f1xx_hal_msp.c 板级硬件配置:时钟、引脚、中断优先级 底盘接线
第四层 寄存器定义层 CMSIS stm32f103xe.h 寄存器基地址、位偏移宏定义 物理零件

原因:

HAL 层只做平台无关的通用逻辑;

凡是与具体电路板有关的(哪个引脚、哪路时钟、哪个中断优先级、DMA 通道挂接)全部下沉 MSP 层。

换板子只需改 MSP,应用层和 HAL 层一行不动------这就是代码解耦与可扩展性。


Q17. 为什么"半完成回调"是 DMA 独有,而 I2C 完全没有半完成概念?

A:

DMA 搬运是按"数据数量"由硬件逐步完成的,硬件能精确知道搬到一半的时机,所以提供 HT 中断(可做双缓冲接力等优化);

中断模式按字节/事件触发,没有"总量一半"这种硬件事件。

I2C 面向协议(起始/地址/ACK/停止),其传输以事务为单位,协议上没有"半完成"事件,因此不存在半完成回调。


五、改错与编程题

Q18. 指出下面代码的错误并说明原因(中断回调内直接做业务):

复制代码
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    TransformCase(tx_buffer, rx_buffer, BUFFER_SIZE);          // ①
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, BUFFER_SIZE);    // ②
    HAL_Delay(100);                                            // ③
}

A:

  • ①中断里做业务:阻塞中断、拉长响应,违背"中断只通知、主循环决策"准则;
  • ②回调里直接启动新 DMA:此时串口/DMA 句柄 State 可能仍为 BUSY(HAL 尚未把状态归位),HAL_UART_Transmit_DMA 会返回 HAL_BUSY 启动失败,且存在寄存器未清理/死锁风险;
  • ③中断里 HAL_Delay:依赖 SysTick,优先级配置不当会卡死;且耗时操作会错过后续数据导致 ORE 溢出。
  • 正确做法:回调只写 g_state = USART_STATE_DATA_READY;,业务、发送、延时全部移到主循环 DATA_READY 分支处理。

Q19. 口述题:请描述串口 DMA 收发状态机从上电到完成一次收发的完整运行过程。

A(答题要点):

  1. 上电初始化后 g_state = IDLE,主循环进入 IDLE 分支,调用 HAL_UART_Receive_DMA 成功 → 状态变 RXING;
  2. 上位机发来 8 字节,DMA 硬件自动搬运到 rx_buffer,搬完触发 DMA1_Channel5_IRQHandler → HAL_DMA_IRQHandler → UART_DMAReceiveCplt → HAL_UART_RxCpltCallback,回调只把状态改为 DATA_READY;
  3. 主循环轮询到 DATA_READY,做大小写转换、翻转 LED,调用 HAL_UART_Transmit_DMA 成功 → 状态变 TXING;
  4. DMA 发送完成 → DMA1_Channel4_IRQHandler → ... → UART_DMATransmitCplt → HAL_UART_TxCpltCallback,回调把状态改回 IDLE;
  5. 主循环回到 IDLE,再次启动接收,周而复始;期间任何启动失败或 ORE 错误 → ERROR 分支统一恢复(停 DMA、清标志、延时)后回 IDLE。

面试加分点:

① 能说出"普通模式收完即关 DMA → 不及时重启会 ORE"的隐患;

② 能说出 SysTick 优先级必须高于 DMA 中断(否则中断内 Delay 死锁);

③ 能说出"回调同名 = 面向接口编程、业务与传输解耦"的设计思想。


相关推荐
ao-weilai1 小时前
MySQL数据库:数据类型
android·数据库·mysql
天空之城--1 小时前
Android一周动态:Android 18首次官宣、Compose Material3 1.4转正(5趋势+5资讯)
android·人工智能·flutter·架构·android jetpack
恋猫de小郭1 小时前
Meta 分享怎么用 AI 迁移 Compose 项目不烧心
android·前端·flutter
Ai-_Man1 小时前
请问豆包的智能体聊天记录该怎么弄
开发语言·前端·javascript·人工智能·小程序·ecmascript·电脑
铅笔小新z1 小时前
【stm32】DMA 源码分析与实战
stm32·单片机·嵌入式硬件
言乐61 小时前
Python根据关联词搜索模型
开发语言·人工智能·python·机器学习·django
纪念 2291 小时前
C++ string(一)
android·开发语言·c++
一木 之林1 小时前
阿里云百炼与通义千问接入
开发语言·php
纪念 2291 小时前
C++算法(二)
开发语言·c++·算法