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) 状态机:
cppwhile (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; } }三个回调:中断只改状态,不做业务(逻辑归位准则的直接体现):
cppvoid 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,执行清除动作,并在延时结束后恢复正常收发 测试代码(其他代码不变):
cppint 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 区加一句:
cppvoid 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:两点:
- 优先级冲突死锁:HAL_Delay 依赖 SysTick 中断,若当前中断优先级 ≥ SysTick(或嵌套方式不恰当),SysTick 无法抢占,Delay 永远等不到 tick,系统卡死;
- 栈溢出风险:中断内再嵌套调用耗时函数会加深调用层级,栈空间有限,深层嵌套容易溢出。
二、状态机代码题
Q5. 状态机五个状态各自的含义和迁移条件是什么?
A:
状态 含义 进入条件 迁出条件 IDLE 空闲,唯一可发起接收 初始化 / TX 完成 / ERROR 恢复后 HAL_UART_Receive_DMA成功 → RXING;失败 → ERRORRXING USART+DMA 正在接收 IDLE 启动接收成功 RxCpltCallback触发 → DATA_READYDATA_READY 收到一包,数据就绪 接收完成回调 HAL_UART_Transmit_DMA成功 → TXING;失败 → ERRORTXING 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:每步有明确目的:
HAL_UART_DMAStop(&huart1):停掉 DMA 并重置收发相关寄存器(关 DMAR/DMAT、Abort 通道、State 置 READY);__HAL_UART_CLEAR_OREFLAG(&huart1):必须手动清除 ORE 溢出标志,否则 USART 认为仍处于错误状态,无法重启接收;HAL_Delay(300):给硬件一点时间让"停止→重启"生效;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(答题要点):
- 上电初始化后
g_state = IDLE,主循环进入 IDLE 分支,调用HAL_UART_Receive_DMA成功 → 状态变 RXING;- 上位机发来 8 字节,DMA 硬件自动搬运到 rx_buffer,搬完触发
DMA1_Channel5_IRQHandler → HAL_DMA_IRQHandler → UART_DMAReceiveCplt → HAL_UART_RxCpltCallback,回调只把状态改为 DATA_READY;- 主循环轮询到 DATA_READY,做大小写转换、翻转 LED,调用
HAL_UART_Transmit_DMA成功 → 状态变 TXING;- DMA 发送完成 →
DMA1_Channel4_IRQHandler → ... → UART_DMATransmitCplt → HAL_UART_TxCpltCallback,回调把状态改回 IDLE;- 主循环回到 IDLE,再次启动接收,周而复始;期间任何启动失败或 ORE 错误 → ERROR 分支统一恢复(停 DMA、清标志、延时)后回 IDLE。
面试加分点:
① 能说出"普通模式收完即关 DMA → 不及时重启会 ORE"的隐患;
② 能说出 SysTick 优先级必须高于 DMA 中断(否则中断内 Delay 死锁);
③ 能说出"回调同名 = 面向接口编程、业务与传输解耦"的设计思想。


