目录一、F103 vs F429 DMA 资源对比二、最大的四个差异1. 命名体系完全不同2. 并发能力3. FIFO4. 传输宽度配置三、对你学 DMA 的影响四、Stream/Channel 选择规则1M2M随意选2外设 DMA必须查表五、为什么 UART 一个句柄DMA 要两个六、F103 vs F429 并发能力七、实战踩坑DMA 和 IT 抢 RxState问题根源错误写法昨晚最初烧录的版本修复加一行 AbortReceive为什么不直接删掉 HAL_UART_Receive_IT八、__HAL_LINKDMA告诉串口谁帮你搬东西1huart1 内部的结构2不挂上去会怎样九、DMA 初始化字段逐个解释第①步1. Instance — 选哪台运输车2. Channel — 选哪条运输线3. Direction — 搬运方向4. PeriphInc — 外设地址要不要递增5. MemInc — 内存地址要不要递增6. PeriphDataAlignment — 外设端的搬运单位7. MemDataAlignment — 内存端的搬运单位8. Mode — 普通还是循环9. Priority — 多台车抢总线时谁先走10. FIFOMode — 要不要用 FIFO 缓冲11.TX vs RX 初始化差异就两处十、TX DMA 数据流第②步十一、RX DMA 数据流第③步第一段DMA 默默搬数据CPU 完全不知情第二段总线空闲 → USART1_IRQHandler第三段main 感知并处理涉及的中断和回调NDTR 计算原理对比 IT 接收 vs DMA 接收为什么句柄填的全是 uart十二、main.c demoDMA 模式 vs IT 模式第④步DMA 模式 main.cIT 模式 main.c切换四步十三、IT vs DMA 场景选择第⑤步判断标准具体场景一句话一、F103 vs F429 DMA 资源对比STM32F103STM32F429控制器数量2 个DMA1、DMA22 个DMA1、DMA2组织方式Channel通道Stream Channel两级DMA17 个 Channel8 个 Stream每 Stream 可选 8 个 ChannelDMA25 个 Channel仅大容量型号8 个 Stream每 Stream 可选 8 个 Channel总并发数12 个7516 个端口架构单端口同一时间只能一个 Channel 干活双端口多个 Stream 可以并发FIFO❌ 没有✅ 有可配置阈值1/4、1/2、3/4、全满DMAMUX❌ 硬绑定❌ 也是硬绑定但多了 Stream 层级可选Memory-to-Memory仅 DMA2DMA1 和 DMA2 都支持二、最大的四个差异1. 命名体系完全不同F103 DMA1_Channel1, DMA1_Channel2, ..., DMA2_Channel5 F429 DMA1_Stream0, DMA1_Stream1, ..., DMA2_Stream7 └── 每个 Stream 再选一个 Channel0~7F103 的 Channel 运输车 运输线合二为一。F429 是 Stream车 Channel选哪条线分开。2. 并发能力F103同一时刻 DMA1 只有一个 Channel 在传数据单端口 DMA1_Channel1 在传 → Channel 2~7 都得排队 F429多个 Stream 可以同时干活双端口带仲裁 DMA1_Stream0 传 USART1_TX DMA1_Stream3 传 SPI1_RX → 同时跑互不干扰3. FIFOF103 没 FIFO数据一来必须立刻搬到目的地。F429 有 FIFO 缓冲——比如串口波特率很高时DMA 可以先把数据暂存 FIFO然后一次性搬走减少总线占用。4. 传输宽度配置F103源宽度和目标宽度可以不相等但没有突发模式 → 只能说每次读 1 字节写 2 字节 F429支持 Burst 传输一次突发传 4/8/16 拍 → 比如一次 Burst 搬 16 个字节效率高很多三、对你学 DMA 的影响F103 的经验概念都能复用DMA 就是搬运工外设→内存、内存→外设、内存→内存但 F429 的编程模型变了操作F103F429选外设直接选 Channel 号先选 Stream再往 Stream 里选 Channel启停DMA_Cmd(Channel, ENABLE)HAL_DMA_Start_IT(hdma, ...)中断标志DMA_GetITStatus(Channel)__HAL_DMA_GET_FLAG(hdma, DMA_FLAG_TCIFx)HAL 帮你包了一层但底层概念你得知道——选错 Stream/Channel 组合是最常见的坑。四、Stream/Channel 选择规则1M2M随意选// 内存到内存不绑外设Stream 和 Channel 随便配 DMA_Handle.Instance DMA1_Stream3; // 或用 DMA2_Stream0都行 DMA_Handle.Init.Channel DMA_CHANNEL_5; // 或 0~7 随便没影响 DMA_Handle.Init.Direction DMA_MEMORY_TO_MEMORY;2外设 DMA必须查表一旦Direction选了MEMORY_TO_PERIPH或PERIPH_TO_MEMORYStream Channel 组合就是写死在芯片里的每个外设的 DMA 请求线连到了固定的 Stream 和 Channel → 软件改不了F4 没 DMAMUX → 选错了 DMA 就找不到这个外设传不了五、为什么 UART 一个句柄DMA 要两个UART 一个外设收发都在同一个硬件模块里 → huart1 一个句柄就够里面 Mode TX_RX 同时管两路 DMA 两台独立的运输车各干各的 → hdma_usart1_tx 车7号专门往 USART 搬发 → hdma_usart1_rx 车5号专门从 USART 搬收 → 两辆车同时跑互不影响这就是为什么 DMA 每个 Stream 只能干一件事——想同时收发就得占两个 Stream两个句柄。六、F103 vs F429 并发能力F103单端口 DMA1_Channel4 (USART1_TX) 在传 → Channel 5(USART1_RX) 只能排队 不能同时收发 F429双端口 DMA2_Stream7 (USART1_TX) ─┐ ├─ 同时跑互不干扰 DMA2_Stream5 (USART1_RX) ─┘F429 比 F103 强的不只是主频DMA 的并发能力也是质变。后面学 FreeRTOS 时优势更明显——多个任务各自用各自的 DMA Stream谁都不用等谁。七、实战踩坑DMA 和 IT 抢 RxState问题根源Usart1_Init结尾默认调了HAL_UART_Receive_IT// Usart.c 第 66 行 HAL_UART_Receive_IT(huart1, rx_byte, 1); // → huart1.RxState BUSY_RX之后在 main.c 调HAL_UART_Receive_DMA时HAL 内部检查状态不是READY直接返回HAL_BUSYDMA 静默失败编译零报错。错误写法昨晚最初烧录的版本int main(void) { Usart1_Init(115200); // 内部调了 HAL_UART_Receive_IT // → RxState BUSY_RX Usart1_DMA_Init(); HAL_UART_Receive_DMA(huart1, dma_rx_buf, 128); // ↑ 静默失败HAL 检查 RxState ! READY → 直接 return HAL_BUSY // DMA 根本没启动串口助手收不到任何数据 }修复加一行 AbortReceiveUsart1_Init(115200); Usart1_DMA_Init(); HAL_UART_AbortReceive(huart1); // ← 停掉 ITRxState 恢复 READY HAL_UART_Receive_DMA(huart1, dma_rx_buf, 128); // ← 现在成功了为什么不直接删掉 HAL_UART_Receive_IT保留它是为了随时切换 IT/DMA 模式Usart1_Init是公共入口IT 接收默认备着用 DMA 时多写一行AbortReceive停掉即可切回 IT 时删掉AbortReceive那行就行不用改Usart1_Init八、__HAL_LINKDMA告诉串口谁帮你搬东西__HAL_LINKDMA不是函数是个宏。内部就是简单的赋值__HAL_LINKDMA(huart1, hdmatx, hdma_usart1_tx); // 等价于 huart1.hdmatx hdma_usart1_tx; // 意思串口1你的 TX由 DMA2_Stream7 这辆车负责 __HAL_LINKDMA(huart1, hdmarx, hdma_usart1_rx); // 等价于 huart1.hdmarx hdma_usart1_rx; // 意思串口1你的 RX由 DMA2_Stream5 这辆车负责1huart1 内部的结构初始化时只填了 Instance 和 Init 部分其余字段是 HAL 内部在用huart1 ├── .Instance USART1 ← 你填哪个串口硬件 ├── .Init.BaudRate 115200 ← 你填波特率 ├── .Init.Mode TX_RX ← 你填收发模式 ├── .hdmatx hdma_usart1_tx ← LINK 挂上去的HAL 内部找 DMA ├── .hdmarx hdma_usart1_rx ← 同上 ├── .RxState READY/BUSY ← HAL 自管接收状态DMA 抢 RxState 的坑 ├── .TxState READY/BUSY ← HAL 自管发送状态 ├── .Lock UNLOCK/LOCK ← HAL 自管防重入锁 └── .ErrorCode 错误码 ← HAL 自管句柄 大管家结构体跟这个外设有关的所有信息全打包在一起。2不挂上去会怎样之后HAL_UART_Transmit_DMA内部会从huart-hdmatx找 DMA 句柄来启动传输。如果没挂 → 空指针 → 跑飞。九、DMA 初始化字段逐个解释第①步hdma_usart1_tx.Instance DMA2_Stream7; hdma_usart1_tx.Init.Channel DMA_CHANNEL_4; hdma_usart1_tx.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_usart1_tx.Init.PeriphInc DMA_PINC_DISABLE; hdma_usart1_tx.Init.MemInc DMA_MINC_ENABLE; hdma_usart1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_usart1_tx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_usart1_tx.Init.Mode DMA_NORMAL; hdma_usart1_tx.Init.Priority DMA_PRIORITY_LOW; hdma_usart1_tx.Init.FIFOMode DMA_FIFOMODE_DISABLE;1. Instance — 选哪台运输车.Instance DMA2_Stream7;F429 有 16 台车DMA1 8 台 DMA2 8 台这里选了 DMA2 的第 7 号车。必须跟芯片手册的映射表一致——USART1_TX 固定绑在 DMA2_Stream7。跟串口类比huart1.Instance USART1选哪个串口。2. Channel — 选哪条运输线.Init.Channel DMA_CHANNEL_4;一辆车可以跑 8 条线Channel 0~7每条线连着一个外设。Channel 4 连的是 USART1_TX。DMA2_Stream7 这台车 ├─ Channel 0 → 某外设 ├─ Channel 1 → 某外设 ├─ Channel 4 → USART1_TX ← 我们选这条 └─ ...3. Direction — 搬运方向.Direction DMA_MEMORY_TO_PERIPH; // 内存 → 外设发送值方向场景DMA_MEMORY_TO_PERIPH内存 → 外设TXbuf 里的数据搬到串口 DR 寄存器DMA_PERIPH_TO_MEMORY外设 → 内存RX串口 DR 的数据搬到 dma_rx_bufDMA_MEMORY_TO_MEMORY内存 → 内存野火那个 M2M 例程4. PeriphInc — 外设地址要不要递增.PeriphInc DMA_PINC_DISABLE; // 不递增TX 时外设就是 USART1 的数据寄存器 DR。这个寄存器的地址是固定的0x40011004你不会往 DR1、DR2 发数据。所以 DISABLE——每次搬都往同一个地址写。对比 M2M 模式源地址在内存里需要递增才能把整个数组搬完 → ENABLE。5. MemInc — 内存地址要不要递增.MemInc DMA_MINC_ENABLE; // 递增内存就是你的发送缓冲区。你要把buf[0], buf[1], buf[2]...依次发出去所以每搬一个字节地址要 1。第 1 次: 搬 buf[0] → USART_DR 第 2 次: 搬 buf[1] → USART_DR ← 地址自动 1 第 3 次: 搬 buf[2] → USART_DR ← 地址自动 1 ...6. PeriphDataAlignment — 外设端的搬运单位.PeriphDataAlignment DMA_PDATAALIGN_BYTE; // 1 字节串口一次只能收发 1 字节8 位所以设 BYTE。如果是 ADC12 位结果存在 16 位寄存器里就要用 HALFWORD。7. MemDataAlignment — 内存端的搬运单位.MemDataAlignment DMA_MDATAALIGN_BYTE; // 1 字节两边对齐——外设 1 字节内存也 1 字节。内存到内存且是 uint32_t 数组时野火那个两端都设 WORD。8. Mode — 普通还是循环.Mode DMA_NORMAL; // 普通模式模式行为DMA_NORMAL搬完指定数量就停。比如你叫它搬 15 字节搬完它就下班了DMA_CIRCULAR搬完自动从头再来。适合 ADC 连续采样这种场景永远不停9. Priority — 多台车抢总线时谁先走.Priority DMA_PRIORITY_LOW; // TX 优先级低当多个 DMA Stream 同时要干活时优先级高的先拿到总线。发送慢点没事接收偏不能丢——所以 TX 用 LOWRX 用 HIGH。10. FIFOMode — 要不要用 FIFO 缓冲.FIFOMode DMA_FIFOMODE_DISABLE; // 不用F429 有 FIFOF103 没有但串口逐字节传输用不到。关了就行数据直接从源到目的地。11.TX vs RX 初始化差异就两处字段TXRX原因DirectionMEMORY_TO_PERIPHPERIPH_TO_MEMORY方向反PriorityLOWHIGH收不能丢十、TX DMA 数据流第②步你调Usart_SendString_DMA((uint8_t*)Hello, 5)整条路径main 调用 Usart_SendString_DMA(str, 5) │ ▼ HAL_UART_Transmit_DMA(huart1, Hello, 5) │ ├─ ① 查 huart1.TxState 是否 READY → OK置 BUSY_TX ├─ ② 从 huart1.hdmatx 取出 DMA 句柄 → hdma_usart1_tx ├─ ③ 配 DMA源Hello 目标USART1→DR 次数5 ├─ ④ 使能 DMA2_Stream7挂上 USART1_TX 请求线 └─ ⑤ 立刻返回 ← main 不用等DMA 自己在后台搬第1次: str[0]H → DMA → USART_DR → PA9 发出计数器-1 第2次: str[1]e → DMA → USART_DR → PA9 发出计数器-1 第3次: str[2]l → DMA → USART_DR → PA9 发出计数器-1 第4次: str[3]l → DMA → USART_DR → PA9 发出计数器-1 第5次: str[4]o → DMA → USART_DR → PA9 发出计数器0 → 完成完成后DMA2_Stream7 硬件 → TCIF 置 1 → DMA2_Stream7_IRQHandler │ ▼ HAL_DMA_IRQHandler(hdma_usart1_tx) │ 判断是 TC 中断 → 清标志 → 回调 ▼ HAL_UART_TxCpltCallback(huart1) ← 你的回调 │ printf(DMA TX done\n); ▼ huart1.TxState READY ← 释放可以发下一包了对比阻塞发送 vs DMA 发送Usart_SendString(Hello) Usart_SendString_DMA(str, 5) main发出H→等 main说帮我发→立刻干别的 main发出e→等 │ main发出l→等 同时 DMA 在后台发 main发出l→等 H→e→l→l→o 发完了回调通知你 main发出o→恢复 ← 零等待 五连等全程卡住 CPU 时间省下来了TX 流程里只有一个中断DMA2_Stream7_IRQHandler一个回调TxCpltCallback。搬运过程 CPU 不参与只收一次完成通知。十一、RX DMA 数据流第③步以串口助手发AT\n3 字节为例分三段。第一段DMA 默默搬数据CPU 完全不知情字节 A 到达 PA10 USART_DR ← 0x41 → 硬件 RXNE1 DMA2_Stream5 看到 RXNE → 自动读 DR → 把 A 搬进 dma_rx_buf[0] DMA 内部地址 1 → 指向 buf[1]计数器 -1127 剩余 RXNE 自动清零CPU 全程旁观 字节 T 到达 DMA → dma_rx_buf[1]地址 1计数器 -1126 剩余 字节 \n 到达 DMA → dma_rx_buf[2]地址 1计数器 -1125 剩余此时dma_rx_buf [A][T][\n][?]...main 还在while(1)傻跑一无所知。第二段总线空闲 → USART1_IRQHandler发完 \n 后 TX 不再发数据 → RX 引脚回到高电平 持续 1 字节时间~87us→ 硬件 IDLE 标志置 1 USART1_IRQHandler 触发 ← USART1 的中断不是 DMA 中断 │ ├─ HAL_UART_IRQHandler(huart1) │ 扫 SR 寄存器所有标志 │ RXNE0DMA 已清→ 跳过 │ IDLE1 → HAL 故意不处理F4 无回调→ 跳过 │ 其他错误标志 → HAL 顺便处理 │ 返回 │ └─ 你手搓的 IDLE 处理 if (IDLE 标志 ! RESET) → YES { ① __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清硬件标志 ② uint16_t ndtr __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 读 DMA 剩余计数器ndtr 125128-3 ③ if (ndtr DMA_RX_BUF_SIZE) // 125 128 → TRUE → DMA 模式 { dma_rx_len 128 - 125 3; // 算出实际收到 3 字节 if (dma_rx_len 0) // 3 0 → 真的收到了 dma_rx_flag 1; // 按门铃 ④ HAL_UART_DMAStop(huart1); // 停 DMA ⑤ HAL_UART_Receive_DMA(..., 128); // 重启指向 buf[0] ⑥ __HAL_UART_CLEAR_IDLEFLAG(huart1); // 重启后可能立即再触发 IDLE清掉 } }第三段main 感知并处理if (dma_rx_flag 1) // 门铃响了 { LED_B(0); // 蓝灯亮处理中 printf(DMA rx %d bytes: , dma_rx_len); Usart_SendString_DMA(dma_rx_buf, dma_rx_len); // DMA 回显 printf(\n); dma_rx_flag 0; // 关掉门铃 LED_B(1); // 蓝灯灭 }涉及的中断和回调场景 中断 回调 ───────────────────── ───────────────────────── ────────────────── DMA 搬数据中 不触发任何中断 无 DMA 自己默默搬 帧结束空闲 USART1_IRQHandler 无回调 └─ IDLE 部分你手搓 直接置 flag 通知 main DMA 收满 64/128 字节 DMA2_Stream5_IRQHandler 你未实现预留 └─ HAL_DMA_IRQHandler小数据帧只走 USART1_IRQHandlerIDLE不经过 DMA2_Stream5_IRQHandler。NDTR 计算原理初始NDTR 128缓冲区总大小 收 ANDTR → 127 收 TNDTR → 126 收 \nNDTR → 125 实际收到 128 - 125 3 字节NDTR 在哪DMA 硬件寄存器里__HAL_DMA_GET_COUNTER每次实时读取不存在读到旧值的问题ndtr 变量在哪局部变量栈上。每次进 ISR 重新创建退出时销毁。跟全局缓冲区dma_rx_buf/flag/len不同它不需要清零对比 IT 接收 vs DMA 接收IT 接收DMA 接收每字节进几次中断1 次 RXNE → CPU 亲自存0 次→ DMA 自动搬帧结束谁通知USART1 IDLE同一套手搓USART1 IDLE同一套帧长怎么算usart1_rx_len一字节一字节数128 - NDTRDMA 计数器算缓冲区usart1_rx_buf[50]dma_rx_buf[128]CPU 每字节耗时~3us 中断处理0us适合场景短帧调试大批量高速省 CPU为什么句柄填的全是 uartHAL_UART_DMAStop(huart1); // 主体是串口内部从 huart1.hdmarx 找 DMA HAL_UART_Receive_DMA(huart1, ...); // 同上内部自动操作 DMA __HAL_UART_CLEAR_IDLEFLAG(huart1); // 这个跟 DMA 无关清 USART 自己的标志主体从来都是串口DMA 只是挂在上面的外挂。__HAL_LINKDMA把 DMA 句柄写进 huart1 之后UART 函数内部就能自动找到对应的 DMA。用 UART 层面的函数能保证 UART 状态机和 DMA 两边状态一起更新。十二、main.c demoDMA 模式 vs IT 模式第④步同一套收发逻辑换底层搬运工。DMA 模式 main.cUsart1_Init(115200); Usart1_DMA_Init(); HAL_UART_AbortReceive(huart1); // 停掉 IT HAL_UART_Receive_DMA(huart1, dma_rx_buf, 128); // 启动 DMA 收 __HAL_UART_CLEAR_IDLEFLAG(huart1); Usart_SendString_DMA(...); // DMA 发 while (1) { if (dma_rx_flag 1) { // 检测 DMA flag Usart_SendString_DMA(dma_rx_buf, dma_rx_len); // DMA 回显 dma_rx_flag 0; } }IT 模式 main.cUsart1_Init(115200); // 不用 DMA_Init // HAL_UART_Receive_IT 在 Usart1_Init 内部自动启动了 while (1) { if (usart1_rx_flag 1) { // 检测 IT flag HAL_UART_Transmit(huart1, usart1_rx_buf, usart1_rx_len, 1000); // 阻塞回显 usart1_rx_flag 0; usart1_rx_len 0; } }切换四步IT → DMA DMA → IT ─────── ─────── ① Usart1_DMA_Init() ① 不调 Usart1_DMA_Init ② HAL_UART_AbortReceive() ② HAL_UART_Receive_IT(huart1, rx_byte, 1) ③ HAL_UART_Receive_DMA(...,128) ③ 不调 AbortReceive ④ 轮询 dma_rx_flag/len ④ 轮询 usart1_rx_flag/lenISR 是同一个USART1_IRQHandler内部自动通过 NDTR 判断走 DMA 分支还是 IT 分支不用改。十三、IT vs DMA 场景选择第⑤步判断标准数据量大不大 → 大 几十字节→ DMA → 小几个字节→ IT 够了 CPU 有没有更要紧的事 → 有RTOS、PID、通信并发→ DMA → 没有裸机傻等→ IT 也行具体场景场景推荐原因调试打印、命令行交互IT每次就几个字中断存一下毫秒级AT 指令发AT\r\n回OK\r\nIT帧很短DMA 浪费 128 字节缓冲区GPS 模块每秒吐长串 NMEADMA每帧几百字节DMA 省心Modbus RTU 从机DMA帧长不定可能到 256 字节CPU 同时要干别的串口传文件、OTA 固件升级DMA几十 K 数据IT 中断会炸跑 FreeRTOS 时串口通信DMACPU 忙着调度不能老被串口中断打断一句话短帧 裸机 没别的事 → IT写起来简单 长帧 / 高波特率 / 跑 RTOS / CPU 忙 → DMA
F429-HAL-DMA(2026/7/24)
目录一、F103 vs F429 DMA 资源对比二、最大的四个差异1. 命名体系完全不同2. 并发能力3. FIFO4. 传输宽度配置三、对你学 DMA 的影响四、Stream/Channel 选择规则1M2M随意选2外设 DMA必须查表五、为什么 UART 一个句柄DMA 要两个六、F103 vs F429 并发能力七、实战踩坑DMA 和 IT 抢 RxState问题根源错误写法昨晚最初烧录的版本修复加一行 AbortReceive为什么不直接删掉 HAL_UART_Receive_IT八、__HAL_LINKDMA告诉串口谁帮你搬东西1huart1 内部的结构2不挂上去会怎样九、DMA 初始化字段逐个解释第①步1. Instance — 选哪台运输车2. Channel — 选哪条运输线3. Direction — 搬运方向4. PeriphInc — 外设地址要不要递增5. MemInc — 内存地址要不要递增6. PeriphDataAlignment — 外设端的搬运单位7. MemDataAlignment — 内存端的搬运单位8. Mode — 普通还是循环9. Priority — 多台车抢总线时谁先走10. FIFOMode — 要不要用 FIFO 缓冲11.TX vs RX 初始化差异就两处十、TX DMA 数据流第②步十一、RX DMA 数据流第③步第一段DMA 默默搬数据CPU 完全不知情第二段总线空闲 → USART1_IRQHandler第三段main 感知并处理涉及的中断和回调NDTR 计算原理对比 IT 接收 vs DMA 接收为什么句柄填的全是 uart十二、main.c demoDMA 模式 vs IT 模式第④步DMA 模式 main.cIT 模式 main.c切换四步十三、IT vs DMA 场景选择第⑤步判断标准具体场景一句话一、F103 vs F429 DMA 资源对比STM32F103STM32F429控制器数量2 个DMA1、DMA22 个DMA1、DMA2组织方式Channel通道Stream Channel两级DMA17 个 Channel8 个 Stream每 Stream 可选 8 个 ChannelDMA25 个 Channel仅大容量型号8 个 Stream每 Stream 可选 8 个 Channel总并发数12 个7516 个端口架构单端口同一时间只能一个 Channel 干活双端口多个 Stream 可以并发FIFO❌ 没有✅ 有可配置阈值1/4、1/2、3/4、全满DMAMUX❌ 硬绑定❌ 也是硬绑定但多了 Stream 层级可选Memory-to-Memory仅 DMA2DMA1 和 DMA2 都支持二、最大的四个差异1. 命名体系完全不同F103 DMA1_Channel1, DMA1_Channel2, ..., DMA2_Channel5 F429 DMA1_Stream0, DMA1_Stream1, ..., DMA2_Stream7 └── 每个 Stream 再选一个 Channel0~7F103 的 Channel 运输车 运输线合二为一。F429 是 Stream车 Channel选哪条线分开。2. 并发能力F103同一时刻 DMA1 只有一个 Channel 在传数据单端口 DMA1_Channel1 在传 → Channel 2~7 都得排队 F429多个 Stream 可以同时干活双端口带仲裁 DMA1_Stream0 传 USART1_TX DMA1_Stream3 传 SPI1_RX → 同时跑互不干扰3. FIFOF103 没 FIFO数据一来必须立刻搬到目的地。F429 有 FIFO 缓冲——比如串口波特率很高时DMA 可以先把数据暂存 FIFO然后一次性搬走减少总线占用。4. 传输宽度配置F103源宽度和目标宽度可以不相等但没有突发模式 → 只能说每次读 1 字节写 2 字节 F429支持 Burst 传输一次突发传 4/8/16 拍 → 比如一次 Burst 搬 16 个字节效率高很多三、对你学 DMA 的影响F103 的经验概念都能复用DMA 就是搬运工外设→内存、内存→外设、内存→内存但 F429 的编程模型变了操作F103F429选外设直接选 Channel 号先选 Stream再往 Stream 里选 Channel启停DMA_Cmd(Channel, ENABLE)HAL_DMA_Start_IT(hdma, ...)中断标志DMA_GetITStatus(Channel)__HAL_DMA_GET_FLAG(hdma, DMA_FLAG_TCIFx)HAL 帮你包了一层但底层概念你得知道——选错 Stream/Channel 组合是最常见的坑。四、Stream/Channel 选择规则1M2M随意选// 内存到内存不绑外设Stream 和 Channel 随便配 DMA_Handle.Instance DMA1_Stream3; // 或用 DMA2_Stream0都行 DMA_Handle.Init.Channel DMA_CHANNEL_5; // 或 0~7 随便没影响 DMA_Handle.Init.Direction DMA_MEMORY_TO_MEMORY;2外设 DMA必须查表一旦Direction选了MEMORY_TO_PERIPH或PERIPH_TO_MEMORYStream Channel 组合就是写死在芯片里的每个外设的 DMA 请求线连到了固定的 Stream 和 Channel → 软件改不了F4 没 DMAMUX → 选错了 DMA 就找不到这个外设传不了五、为什么 UART 一个句柄DMA 要两个UART 一个外设收发都在同一个硬件模块里 → huart1 一个句柄就够里面 Mode TX_RX 同时管两路 DMA 两台独立的运输车各干各的 → hdma_usart1_tx 车7号专门往 USART 搬发 → hdma_usart1_rx 车5号专门从 USART 搬收 → 两辆车同时跑互不影响这就是为什么 DMA 每个 Stream 只能干一件事——想同时收发就得占两个 Stream两个句柄。六、F103 vs F429 并发能力F103单端口 DMA1_Channel4 (USART1_TX) 在传 → Channel 5(USART1_RX) 只能排队 不能同时收发 F429双端口 DMA2_Stream7 (USART1_TX) ─┐ ├─ 同时跑互不干扰 DMA2_Stream5 (USART1_RX) ─┘F429 比 F103 强的不只是主频DMA 的并发能力也是质变。后面学 FreeRTOS 时优势更明显——多个任务各自用各自的 DMA Stream谁都不用等谁。七、实战踩坑DMA 和 IT 抢 RxState问题根源Usart1_Init结尾默认调了HAL_UART_Receive_IT// Usart.c 第 66 行 HAL_UART_Receive_IT(huart1, rx_byte, 1); // → huart1.RxState BUSY_RX之后在 main.c 调HAL_UART_Receive_DMA时HAL 内部检查状态不是READY直接返回HAL_BUSYDMA 静默失败编译零报错。错误写法昨晚最初烧录的版本int main(void) { Usart1_Init(115200); // 内部调了 HAL_UART_Receive_IT // → RxState BUSY_RX Usart1_DMA_Init(); HAL_UART_Receive_DMA(huart1, dma_rx_buf, 128); // ↑ 静默失败HAL 检查 RxState ! READY → 直接 return HAL_BUSY // DMA 根本没启动串口助手收不到任何数据 }修复加一行 AbortReceiveUsart1_Init(115200); Usart1_DMA_Init(); HAL_UART_AbortReceive(huart1); // ← 停掉 ITRxState 恢复 READY HAL_UART_Receive_DMA(huart1, dma_rx_buf, 128); // ← 现在成功了为什么不直接删掉 HAL_UART_Receive_IT保留它是为了随时切换 IT/DMA 模式Usart1_Init是公共入口IT 接收默认备着用 DMA 时多写一行AbortReceive停掉即可切回 IT 时删掉AbortReceive那行就行不用改Usart1_Init八、__HAL_LINKDMA告诉串口谁帮你搬东西__HAL_LINKDMA不是函数是个宏。内部就是简单的赋值__HAL_LINKDMA(huart1, hdmatx, hdma_usart1_tx); // 等价于 huart1.hdmatx hdma_usart1_tx; // 意思串口1你的 TX由 DMA2_Stream7 这辆车负责 __HAL_LINKDMA(huart1, hdmarx, hdma_usart1_rx); // 等价于 huart1.hdmarx hdma_usart1_rx; // 意思串口1你的 RX由 DMA2_Stream5 这辆车负责1huart1 内部的结构初始化时只填了 Instance 和 Init 部分其余字段是 HAL 内部在用huart1 ├── .Instance USART1 ← 你填哪个串口硬件 ├── .Init.BaudRate 115200 ← 你填波特率 ├── .Init.Mode TX_RX ← 你填收发模式 ├── .hdmatx hdma_usart1_tx ← LINK 挂上去的HAL 内部找 DMA ├── .hdmarx hdma_usart1_rx ← 同上 ├── .RxState READY/BUSY ← HAL 自管接收状态DMA 抢 RxState 的坑 ├── .TxState READY/BUSY ← HAL 自管发送状态 ├── .Lock UNLOCK/LOCK ← HAL 自管防重入锁 └── .ErrorCode 错误码 ← HAL 自管句柄 大管家结构体跟这个外设有关的所有信息全打包在一起。2不挂上去会怎样之后HAL_UART_Transmit_DMA内部会从huart-hdmatx找 DMA 句柄来启动传输。如果没挂 → 空指针 → 跑飞。九、DMA 初始化字段逐个解释第①步hdma_usart1_tx.Instance DMA2_Stream7; hdma_usart1_tx.Init.Channel DMA_CHANNEL_4; hdma_usart1_tx.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_usart1_tx.Init.PeriphInc DMA_PINC_DISABLE; hdma_usart1_tx.Init.MemInc DMA_MINC_ENABLE; hdma_usart1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_usart1_tx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_usart1_tx.Init.Mode DMA_NORMAL; hdma_usart1_tx.Init.Priority DMA_PRIORITY_LOW; hdma_usart1_tx.Init.FIFOMode DMA_FIFOMODE_DISABLE;1. Instance — 选哪台运输车.Instance DMA2_Stream7;F429 有 16 台车DMA1 8 台 DMA2 8 台这里选了 DMA2 的第 7 号车。必须跟芯片手册的映射表一致——USART1_TX 固定绑在 DMA2_Stream7。跟串口类比huart1.Instance USART1选哪个串口。2. Channel — 选哪条运输线.Init.Channel DMA_CHANNEL_4;一辆车可以跑 8 条线Channel 0~7每条线连着一个外设。Channel 4 连的是 USART1_TX。DMA2_Stream7 这台车 ├─ Channel 0 → 某外设 ├─ Channel 1 → 某外设 ├─ Channel 4 → USART1_TX ← 我们选这条 └─ ...3. Direction — 搬运方向.Direction DMA_MEMORY_TO_PERIPH; // 内存 → 外设发送值方向场景DMA_MEMORY_TO_PERIPH内存 → 外设TXbuf 里的数据搬到串口 DR 寄存器DMA_PERIPH_TO_MEMORY外设 → 内存RX串口 DR 的数据搬到 dma_rx_bufDMA_MEMORY_TO_MEMORY内存 → 内存野火那个 M2M 例程4. PeriphInc — 外设地址要不要递增.PeriphInc DMA_PINC_DISABLE; // 不递增TX 时外设就是 USART1 的数据寄存器 DR。这个寄存器的地址是固定的0x40011004你不会往 DR1、DR2 发数据。所以 DISABLE——每次搬都往同一个地址写。对比 M2M 模式源地址在内存里需要递增才能把整个数组搬完 → ENABLE。5. MemInc — 内存地址要不要递增.MemInc DMA_MINC_ENABLE; // 递增内存就是你的发送缓冲区。你要把buf[0], buf[1], buf[2]...依次发出去所以每搬一个字节地址要 1。第 1 次: 搬 buf[0] → USART_DR 第 2 次: 搬 buf[1] → USART_DR ← 地址自动 1 第 3 次: 搬 buf[2] → USART_DR ← 地址自动 1 ...6. PeriphDataAlignment — 外设端的搬运单位.PeriphDataAlignment DMA_PDATAALIGN_BYTE; // 1 字节串口一次只能收发 1 字节8 位所以设 BYTE。如果是 ADC12 位结果存在 16 位寄存器里就要用 HALFWORD。7. MemDataAlignment — 内存端的搬运单位.MemDataAlignment DMA_MDATAALIGN_BYTE; // 1 字节两边对齐——外设 1 字节内存也 1 字节。内存到内存且是 uint32_t 数组时野火那个两端都设 WORD。8. Mode — 普通还是循环.Mode DMA_NORMAL; // 普通模式模式行为DMA_NORMAL搬完指定数量就停。比如你叫它搬 15 字节搬完它就下班了DMA_CIRCULAR搬完自动从头再来。适合 ADC 连续采样这种场景永远不停9. Priority — 多台车抢总线时谁先走.Priority DMA_PRIORITY_LOW; // TX 优先级低当多个 DMA Stream 同时要干活时优先级高的先拿到总线。发送慢点没事接收偏不能丢——所以 TX 用 LOWRX 用 HIGH。10. FIFOMode — 要不要用 FIFO 缓冲.FIFOMode DMA_FIFOMODE_DISABLE; // 不用F429 有 FIFOF103 没有但串口逐字节传输用不到。关了就行数据直接从源到目的地。11.TX vs RX 初始化差异就两处字段TXRX原因DirectionMEMORY_TO_PERIPHPERIPH_TO_MEMORY方向反PriorityLOWHIGH收不能丢十、TX DMA 数据流第②步你调Usart_SendString_DMA((uint8_t*)Hello, 5)整条路径main 调用 Usart_SendString_DMA(str, 5) │ ▼ HAL_UART_Transmit_DMA(huart1, Hello, 5) │ ├─ ① 查 huart1.TxState 是否 READY → OK置 BUSY_TX ├─ ② 从 huart1.hdmatx 取出 DMA 句柄 → hdma_usart1_tx ├─ ③ 配 DMA源Hello 目标USART1→DR 次数5 ├─ ④ 使能 DMA2_Stream7挂上 USART1_TX 请求线 └─ ⑤ 立刻返回 ← main 不用等DMA 自己在后台搬第1次: str[0]H → DMA → USART_DR → PA9 发出计数器-1 第2次: str[1]e → DMA → USART_DR → PA9 发出计数器-1 第3次: str[2]l → DMA → USART_DR → PA9 发出计数器-1 第4次: str[3]l → DMA → USART_DR → PA9 发出计数器-1 第5次: str[4]o → DMA → USART_DR → PA9 发出计数器0 → 完成完成后DMA2_Stream7 硬件 → TCIF 置 1 → DMA2_Stream7_IRQHandler │ ▼ HAL_DMA_IRQHandler(hdma_usart1_tx) │ 判断是 TC 中断 → 清标志 → 回调 ▼ HAL_UART_TxCpltCallback(huart1) ← 你的回调 │ printf(DMA TX done\n); ▼ huart1.TxState READY ← 释放可以发下一包了对比阻塞发送 vs DMA 发送Usart_SendString(Hello) Usart_SendString_DMA(str, 5) main发出H→等 main说帮我发→立刻干别的 main发出e→等 │ main发出l→等 同时 DMA 在后台发 main发出l→等 H→e→l→l→o 发完了回调通知你 main发出o→恢复 ← 零等待 五连等全程卡住 CPU 时间省下来了TX 流程里只有一个中断DMA2_Stream7_IRQHandler一个回调TxCpltCallback。搬运过程 CPU 不参与只收一次完成通知。十一、RX DMA 数据流第③步以串口助手发AT\n3 字节为例分三段。第一段DMA 默默搬数据CPU 完全不知情字节 A 到达 PA10 USART_DR ← 0x41 → 硬件 RXNE1 DMA2_Stream5 看到 RXNE → 自动读 DR → 把 A 搬进 dma_rx_buf[0] DMA 内部地址 1 → 指向 buf[1]计数器 -1127 剩余 RXNE 自动清零CPU 全程旁观 字节 T 到达 DMA → dma_rx_buf[1]地址 1计数器 -1126 剩余 字节 \n 到达 DMA → dma_rx_buf[2]地址 1计数器 -1125 剩余此时dma_rx_buf [A][T][\n][?]...main 还在while(1)傻跑一无所知。第二段总线空闲 → USART1_IRQHandler发完 \n 后 TX 不再发数据 → RX 引脚回到高电平 持续 1 字节时间~87us→ 硬件 IDLE 标志置 1 USART1_IRQHandler 触发 ← USART1 的中断不是 DMA 中断 │ ├─ HAL_UART_IRQHandler(huart1) │ 扫 SR 寄存器所有标志 │ RXNE0DMA 已清→ 跳过 │ IDLE1 → HAL 故意不处理F4 无回调→ 跳过 │ 其他错误标志 → HAL 顺便处理 │ 返回 │ └─ 你手搓的 IDLE 处理 if (IDLE 标志 ! RESET) → YES { ① __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清硬件标志 ② uint16_t ndtr __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 读 DMA 剩余计数器ndtr 125128-3 ③ if (ndtr DMA_RX_BUF_SIZE) // 125 128 → TRUE → DMA 模式 { dma_rx_len 128 - 125 3; // 算出实际收到 3 字节 if (dma_rx_len 0) // 3 0 → 真的收到了 dma_rx_flag 1; // 按门铃 ④ HAL_UART_DMAStop(huart1); // 停 DMA ⑤ HAL_UART_Receive_DMA(..., 128); // 重启指向 buf[0] ⑥ __HAL_UART_CLEAR_IDLEFLAG(huart1); // 重启后可能立即再触发 IDLE清掉 } }第三段main 感知并处理if (dma_rx_flag 1) // 门铃响了 { LED_B(0); // 蓝灯亮处理中 printf(DMA rx %d bytes: , dma_rx_len); Usart_SendString_DMA(dma_rx_buf, dma_rx_len); // DMA 回显 printf(\n); dma_rx_flag 0; // 关掉门铃 LED_B(1); // 蓝灯灭 }涉及的中断和回调场景 中断 回调 ───────────────────── ───────────────────────── ────────────────── DMA 搬数据中 不触发任何中断 无 DMA 自己默默搬 帧结束空闲 USART1_IRQHandler 无回调 └─ IDLE 部分你手搓 直接置 flag 通知 main DMA 收满 64/128 字节 DMA2_Stream5_IRQHandler 你未实现预留 └─ HAL_DMA_IRQHandler小数据帧只走 USART1_IRQHandlerIDLE不经过 DMA2_Stream5_IRQHandler。NDTR 计算原理初始NDTR 128缓冲区总大小 收 ANDTR → 127 收 TNDTR → 126 收 \nNDTR → 125 实际收到 128 - 125 3 字节NDTR 在哪DMA 硬件寄存器里__HAL_DMA_GET_COUNTER每次实时读取不存在读到旧值的问题ndtr 变量在哪局部变量栈上。每次进 ISR 重新创建退出时销毁。跟全局缓冲区dma_rx_buf/flag/len不同它不需要清零对比 IT 接收 vs DMA 接收IT 接收DMA 接收每字节进几次中断1 次 RXNE → CPU 亲自存0 次→ DMA 自动搬帧结束谁通知USART1 IDLE同一套手搓USART1 IDLE同一套帧长怎么算usart1_rx_len一字节一字节数128 - NDTRDMA 计数器算缓冲区usart1_rx_buf[50]dma_rx_buf[128]CPU 每字节耗时~3us 中断处理0us适合场景短帧调试大批量高速省 CPU为什么句柄填的全是 uartHAL_UART_DMAStop(huart1); // 主体是串口内部从 huart1.hdmarx 找 DMA HAL_UART_Receive_DMA(huart1, ...); // 同上内部自动操作 DMA __HAL_UART_CLEAR_IDLEFLAG(huart1); // 这个跟 DMA 无关清 USART 自己的标志主体从来都是串口DMA 只是挂在上面的外挂。__HAL_LINKDMA把 DMA 句柄写进 huart1 之后UART 函数内部就能自动找到对应的 DMA。用 UART 层面的函数能保证 UART 状态机和 DMA 两边状态一起更新。十二、main.c demoDMA 模式 vs IT 模式第④步同一套收发逻辑换底层搬运工。DMA 模式 main.cUsart1_Init(115200); Usart1_DMA_Init(); HAL_UART_AbortReceive(huart1); // 停掉 IT HAL_UART_Receive_DMA(huart1, dma_rx_buf, 128); // 启动 DMA 收 __HAL_UART_CLEAR_IDLEFLAG(huart1); Usart_SendString_DMA(...); // DMA 发 while (1) { if (dma_rx_flag 1) { // 检测 DMA flag Usart_SendString_DMA(dma_rx_buf, dma_rx_len); // DMA 回显 dma_rx_flag 0; } }IT 模式 main.cUsart1_Init(115200); // 不用 DMA_Init // HAL_UART_Receive_IT 在 Usart1_Init 内部自动启动了 while (1) { if (usart1_rx_flag 1) { // 检测 IT flag HAL_UART_Transmit(huart1, usart1_rx_buf, usart1_rx_len, 1000); // 阻塞回显 usart1_rx_flag 0; usart1_rx_len 0; } }切换四步IT → DMA DMA → IT ─────── ─────── ① Usart1_DMA_Init() ① 不调 Usart1_DMA_Init ② HAL_UART_AbortReceive() ② HAL_UART_Receive_IT(huart1, rx_byte, 1) ③ HAL_UART_Receive_DMA(...,128) ③ 不调 AbortReceive ④ 轮询 dma_rx_flag/len ④ 轮询 usart1_rx_flag/lenISR 是同一个USART1_IRQHandler内部自动通过 NDTR 判断走 DMA 分支还是 IT 分支不用改。十三、IT vs DMA 场景选择第⑤步判断标准数据量大不大 → 大 几十字节→ DMA → 小几个字节→ IT 够了 CPU 有没有更要紧的事 → 有RTOS、PID、通信并发→ DMA → 没有裸机傻等→ IT 也行具体场景场景推荐原因调试打印、命令行交互IT每次就几个字中断存一下毫秒级AT 指令发AT\r\n回OK\r\nIT帧很短DMA 浪费 128 字节缓冲区GPS 模块每秒吐长串 NMEADMA每帧几百字节DMA 省心Modbus RTU 从机DMA帧长不定可能到 256 字节CPU 同时要干别的串口传文件、OTA 固件升级DMA几十 K 数据IT 中断会炸跑 FreeRTOS 时串口通信DMACPU 忙着调度不能老被串口中断打断一句话短帧 裸机 没别的事 → IT写起来简单 长帧 / 高波特率 / 跑 RTOS / CPU 忙 → DMA