DMA详解:为什么USART、SPI、ADC都喜欢用DMA 一、本文适用场景DMA 是单片机和嵌入式开发中非常重要的一个外设。无论是 STM32、GD32还是其他 Cortex-M 系列 MCU只要涉及大量数据搬运经常都会用到 DMA。很多初学者在学习 USART、SPI、ADC 时经常会遇到这些问题DMA 到底是什么DMA 和中断有什么区别为什么 USART 接收喜欢用 DMA为什么 SPI 刷屏、读写 Flash 喜欢用 DMA为什么 ADC 连续采样经常配合 DMADMA 是不是可以完全替代 CPUDMA 的源地址、目标地址、传输长度分别是什么普通模式和循环模式有什么区别半传输中断和传输完成中断有什么作用DMA 配置了但不工作应该怎么排查本文将从 DMA 的基本概念开始详细介绍 DMA 的工作原理、核心参数、常见传输方向以及 USART、SPI、ADC 为什么都适合配合 DMA 使用。二、什么是DMADMA 的英文全称是Direct Memory Access中文通常称为直接存储器访问可以简单理解为DMA 是单片机内部的一个硬件数据搬运工。在没有 DMA 的情况下外设和内存之间的数据搬运通常需要 CPU 参与。例如串口收到一个字节USART收到数据 ↓ CPU进入中断或轮询 ↓ CPU读取USART数据寄存器 ↓ CPU把数据存入内存数组如果数据量很小这样做没有问题。但如果数据量很大或者数据连续不断到来CPU 就会被频繁打断系统效率会下降。有了 DMA 后流程变成CPU提前配置DMA ↓ 外设产生DMA请求 ↓ DMA自动把数据从外设搬到内存 ↓ 搬完后通知CPU也就是说CPU负责配置 DMA负责搬运CPU 不需要每个字节都亲自处理。三、为什么需要DMADMA 的核心作用是减少CPU参与数据搬运的次数它特别适合下面几类场景数据量大数据连续传输频繁对实时性要求较高CPU不希望被频繁中断外设和内存之间需要高速搬运数据。例如USART高速接收数据 SPI连续刷屏 ADC连续采样 音频数据流 传感器连续数据 内存块复制这些场景如果完全靠 CPU 逐字节搬运会造成较大负担。DMA 可以让 CPU 从“搬运工”变成“调度员”。图1 DMA 基础概念与工作流程DMA 可以在外设与内存之间或者内存与内存之间自动搬运数据。CPU 主要负责配置源地址、目标地址、传输长度和触发源。四、没有DMA时CPU怎么搬数据以 USART 接收为例。如果不用 DMA串口每收到一个字节CPU 通常需要做这些事情1. USART收到1个字节 2. RXNE标志置位 3. CPU进入串口中断 4. CPU读取数据寄存器 5. CPU把数据存入数组或缓冲区 6. 退出中断 7. 下一个字节到来后重复上述流程如果波特率较低、数据量较小这种方式是可以接受的。但如果数据持续不断到来例如115200 460800 921600 甚至更高波特率CPU 就可能被频繁打断。问题包括中断次数多CPU占用升高业务任务执行变慢容易丢数据缓冲区处理压力变大实时性变差。这就是 USART 接收经常配合 DMA 的原因。五、DMA的基本工作流程DMA 的基本工作流程可以简化为1. CPU配置DMA参数 2. 外设产生DMA请求 3. DMA读取源地址数据 4. DMA写入目标地址 5. DMA传输计数递减 6. 传输完成后产生中断或标志 7. CPU处理完成事件其中CPU主要负责前期配置例如源地址 目标地址 传输方向 传输长度 数据宽度 地址是否递增 传输模式 触发源 中断使能配置完成后真正的数据搬运由 DMA 自动完成。六、DMA的核心参数学习 DMA最重要的是理解下面几个参数。1. 源地址源地址表示数据从哪里来例如USART数据寄存器 SPI数据寄存器 ADC数据寄存器 内存数组2. 目标地址目标地址表示数据要搬到哪里去例如RAM接收缓冲区 SPI数据寄存器 USART发送寄存器 另一个内存数组3. 传输方向DMA 常见传输方向包括外设到内存 内存到外设 内存到内存例如USART接收外设到内存 USART发送内存到外设 ADC采样外设到内存 SPI发送内存到外设 SPI接收外设到内存4. 传输长度传输长度表示这次要搬多少个数据单位例如搬100个字节 搬256个半字 搬1024个采样点DMA 每搬一次计数器通常会减 1。当计数器减到 0 时表示本次传输完成。5. 数据宽度数据宽度表示每次搬运的数据单位大小。常见包括8位 16位 32位例如USART通常是8位数据 ADC结果可能是16位 内存拷贝可能是32位数据宽度配置错误可能导致数据错位或结果异常。6. 地址是否递增DMA 搬运时源地址和目标地址是否递增非常关键。例如 USART 接收源地址USART数据寄存器不递增 目标地址RAM数组递增因为每次都从同一个 USART 数据寄存器读数据但要依次存到数组不同位置。ADC 采样也是类似源地址ADC数据寄存器不递增 目标地址RAM采样数组递增而 USART 发送源地址RAM发送数组递增 目标地址USART发送寄存器不递增七、DMA常见传输方向DMA 常见方向可以分为三类。1. 外设到内存典型应用USART接收 ADC采样 SPI接收 I2S音频接收 传感器数据接收数据流向外设数据寄存器 → DMA → RAM缓冲区2. 内存到外设典型应用USART发送 SPI发送 屏幕刷图 DAC输出 PWM波形表输出数据流向RAM数组 → DMA → 外设数据寄存器3. 内存到内存典型应用内存块复制 数组搬运 数据初始化 大块数据转移数据流向内存A → DMA → 内存B八、DMA普通模式和循环模式DMA 常见工作模式包括普通模式 Normal 循环模式 Circular1. 普通模式普通模式下DMA 完成指定长度传输后停止。适合发送一段串口数据 SPI发送一帧图像数据 接收固定长度数据包 ADC采样固定数量点例如 USART 发送一段字符串RAM中的字符串 ↓ DMA搬到USART发送寄存器 ↓ 发送完成 ↓ DMA停止2. 循环模式循环模式下DMA 传输完成后不会停止而是自动回到缓冲区开头继续传输。适合ADC连续采样 音频数据流 循环接收缓冲区 持续传感器采集例如 ADC 连续采样ADC结果不断产生 ↓ DMA不断写入数组 ↓ 写到数组末尾后回到开头继续写循环模式很适合持续数据流但要注意缓冲区覆盖问题。九、半传输中断和传输完成中断DMA 常见两个重要中断半传输中断 传输完成中断1. 半传输中断半传输中断表示DMA已经搬完缓冲区前一半例如缓冲区长度为 100搬到第50个数据时产生半传输中断2. 传输完成中断传输完成中断表示DMA已经搬完整个缓冲区例如缓冲区长度为 100搬完100个数据后产生完成中断3. 为什么需要半传输中断半传输中断经常用于连续数据流处理。例如 ADC 采样数组长度为 100DMA写前50个数据 ↓ 半传输中断 ↓ CPU处理前50个数据 ↓ DMA继续写后50个数据 ↓ 传输完成中断 ↓ CPU处理后50个数据这样可以一边采集一边处理提高效率。十、USART为什么喜欢用DMAUSART 是连续字节流通信。如果不用 DMA每收到一个字节就可能打断 CPU 一次。USART 配合 DMA 后可以让 DMA 自动把接收到的数据搬到内存缓冲区。接收流程为外部数据进入USART ↓ USART接收数据寄存器 ↓ DMA自动搬到RAM缓冲区 ↓ CPU在合适时机处理整段数据这样可以减少 CPU 频繁响应单字节中断的压力。十一、USART接收DMAUSART 接收 DMA 的方向是外设到内存也就是USART_DR → DMA → RAM缓冲区典型配置源地址USART数据寄存器 目标地址接收缓冲区 源地址不递增 目标地址递增 传输方向外设到内存 数据宽度通常8位 模式普通模式或循环模式USART 接收 DMA 常见用法包括固定长度接收不定长接收循环缓冲区接收DMA IDLE 空闲中断接收。十二、USART DMA IDLE空闲中断USART 接收很多时候是不定长数据。例如AT指令 串口屏数据 传感器数据帧 上位机命令数据长度不一定固定。这时常用DMA IDLE空闲中断基本思路DMA持续接收数据 ↓ 一段时间没有新字节到来 ↓ USART产生IDLE空闲中断 ↓ CPU计算DMA已经接收了多少字节 ↓ 处理这一帧数据这种方式非常适合串口变长帧。十三、USART发送DMAUSART 发送 DMA 的方向是内存到外设也就是RAM发送缓冲区 → DMA → USART发送数据寄存器适合发送长字符串日志数据协议帧文件数据大量连续数据。不用 DMA 时CPU 需要不断等待 TXE 标志再写下一个字节。使用 DMA 后CPU 只需要启动一次 DMA 发送后续搬运由 DMA 完成。图2 USART 为什么喜欢用 DMAUSART 是连续字节流。高波特率、批量发送和变长接收场景中DMA 能明显减少 CPU 逐字节处理的压力。十四、SPI为什么喜欢用DMASPI 的特点是速度快 全双工 连续传输字节流SPI 经常用于屏幕刷图Flash 读写ADC 数据读取传感器高速采样外部存储器访问。这些场景通常数据量比较大。如果不用 DMACPU 需要不断等待发送缓冲区为空 写入下一个字节 等待接收缓冲区非空 读取接收数据数据量一大CPU 就会很忙。所以 SPI 非常适合配合 DMA。十五、SPI发送DMASPI 发送 DMA 方向是内存到外设典型场景LCD刷屏 OLED刷新 SPI Flash写入 发送传感器命令序列例如刷屏时一帧图像数据可能有几 KB、几十 KB甚至更多。如果逐字节由 CPU 写 SPI 数据寄存器效率很低。使用 DMA 后图像数据在RAM中 ↓ DMA自动搬到SPI发送寄存器 ↓ SPI连续发送 ↓ 传输完成后通知CPUCPU 可以去做其他事情。十六、SPI接收DMASPI 接收 DMA 方向是外设到内存典型场景SPI Flash读取 外部ADC采样 高速传感器数据读取 IMU数据读取但要注意SPI 是全双工通信。接收数据时主机通常仍然需要发送 dummy 字节来产生时钟。所以 SPI 接收 DMA 经常需要TX DMA发送dummy数据 RX DMA接收真实数据或者CPU或DMA负责发送dummy DMA负责接收数据十七、SPI使用DMA的注意点SPI 使用 DMA 时要注意1. TX和RX长度要匹配SPI 全双工通信中发送多少个字节通常就会接收多少个字节。如果 TX 和 RX 长度不匹配可能导致接收错位或溢出。2. 片选CS时序要正确很多 SPI 设备要求CS拉低 ↓ 完成整段SPI传输 ↓ CS拉高如果 DMA 传输还没真正结束就拉高 CS设备可能认为通信提前结束。3. 区分DMA完成和SPI真正发送完成DMA 完成表示DMA已经把数据写入SPI数据寄存器或缓冲区但不一定表示最后一个 bit 已经从 SPI 引脚发出。有些场景还需要等待 SPI 的 BUSY 标志清除后再拉高 CS。图3 SPI 为什么喜欢用 DMASPI 适合高速连续传输。屏幕刷图、Flash 读写、外部 ADC 和高速传感器数据搬运配合 DMA 可以明显减少 CPU 等待和逐字节操作。十八、ADC为什么喜欢用DMAADC 的作用是把模拟量转换成数字量。例如电压 电流 温度 传感器模拟信号 音频波形ADC 每完成一次转换结果通常存放在 ADC 数据寄存器中。如果采样频率较低CPU 可以通过中断或轮询读取结果。但如果采样频率较高例如连续采样波形CPU 每次都读取 ADC 结果就会很忙。这时使用 DMA 非常合适。十九、ADC DMA工作流程ADC DMA 的典型流程为定时器触发ADC采样 ↓ ADC转换完成 ↓ ADC结果写入ADC数据寄存器 ↓ DMA自动把结果搬到RAM数组 ↓ 达到半缓冲或满缓冲后中断通知CPU ↓ CPU处理一批采样数据这样 ADC 专心采样DMA 专心搬运CPU 专心处理数据。二十、ADC DMA常见模式ADC 配合 DMA 常见模式包括1. 单次采样DMA采样一次DMA 搬运一次。适合简单场景。2. 连续转换DMAADC 连续转换DMA 连续搬运。适合持续采样。3. 循环DMADMA 缓冲区写满后自动回到开头继续写。适合持续数据流例如波形采集 音频采样 传感器连续数据4. 多通道扫描 DMAADC 依次采样多个通道DMA 按顺序把结果放入数组。例如CH0, CH1, CH2 CH0, CH1, CH2 CH0, CH1, CH2对应数组可能是adc_buffer[0] CH0第1次结果 adc_buffer[1] CH1第1次结果 adc_buffer[2] CH2第1次结果 adc_buffer[3] CH0第2次结果 ...多通道 DMA 时数组长度必须和通道数、采样次数对应好。二十一、ADC为什么特别适合DMAADC 特别适合 DMA 的原因采样结果连续产生 数据格式固定 每次都从同一个ADC数据寄存器读取 结果要批量存入RAM这正好符合 DMA 的优势。如果用 CPU 每次读取 ADCADC转换完成一次 CPU响应一次 CPU读一次ADC_DR CPU存一次数组而用 DMA 后ADC负责采样 DMA负责搬运 CPU处理整批数据效率更高数据也更稳定。图4 ADC 为什么喜欢用 DMAADC 连续采样时DMA 可以自动把 ADC 数据寄存器中的结果搬到 RAM 数组特别适合电压采样、电流采样、波形采集和传感器数据流。二十二、DMA和中断的区别很多初学者容易把 DMA 和中断混在一起。其实它们不是一类东西。1. 中断是什么中断是事件通知机制它告诉 CPU某件事发生了需要处理例如串口收到数据 定时器溢出 ADC转换完成 DMA传输完成2. DMA是什么DMA 是数据搬运机制它负责把数据从一个地址搬到另一个地址。例如USART_DR → RAM RAM → SPI_DR ADC_DR → RAM3. DMA也可以产生中断DMA 完成搬运后可以产生中断通知 CPU我搬完了所以 DMA 和中断经常配合使用。可以这样记DMA负责搬数据 中断负责通知CPU二十三、DMA常见调试问题1. DMA完全不工作可能原因DMA时钟没有开启外设DMA请求没有使能DMA通道或请求映射错误源地址配置错误目标地址配置错误传输方向配置错误传输长度为0DMA没有启动外设没有真正产生请求。2. USART DMA收不到数据可能原因USART接收没有使能USART DMA接收请求没开DMA目标缓冲区地址错误接收长度配置不对IDLE中断没有处理变长帧逻辑错误缓冲区被覆盖没有重新启动DMA。3. SPI DMA发送异常可能原因CS时序不对TX DMA和RX DMA长度不匹配只开了TX没有处理RX溢出DMA完成后过早拉高CSSPI BUSY标志未清DMA通道映射错误。4. ADC DMA结果异常可能原因ADC通道数和DMA数组长度不匹配DMA数据宽度设置错误ADC数据对齐方式理解错Circular模式没有开启采样时间太短采样频率超过处理能力半传输和传输完成回调逻辑写反。二十四、DMA调试推荐顺序遇到 DMA 不工作建议按照下面顺序排查第1步确认外设本身能否正常工作 第2步确认DMA时钟是否开启 第3步确认外设DMA请求是否使能 第4步确认DMA通道或请求映射是否正确 第5步确认源地址和目标地址是否正确 第6步确认传输方向是否正确 第7步确认数据宽度是否正确 第8步确认地址递增配置是否正确 第9步确认传输长度是否正确 第10步确认DMA是否真正启动 第11步确认半传输或完成中断是否触发 第12步最后再看业务逻辑排查 DMA 时建议同时查看外设状态寄存器 DMA状态寄存器 中断标志 内存缓冲区内容 实际波形图5 DMA 调试与常见问题排查DMA 不工作时通常不是“DMA搬不动”而是源地址、目标地址、长度、触发源、通道映射或完成处理配置不正确。二十五、STM32 HAL中USART DMA接收示例下面代码只作为理解思路不同芯片和工程配置可能不同。1. 定义接收缓冲区uint8_tuart_rx_buffer[128];2. 启动DMA接收HAL_UART_Receive_DMA(huart1,uart_rx_buffer,sizeof(uart_rx_buffer));3. 使用IDLE中断处理变长数据常见思路是启动DMA接收 ↓ 收到一段数据 ↓ 出现IDLE空闲中断 ↓ 停止DMA ↓ 计算已接收长度 ↓ 处理数据 ↓ 重新启动DMA示例思路voidUSART1_IRQHandler(void){if(__HAL_UART_GET_FLAG(huart1,UART_FLAG_IDLE)!RESET){__HAL_UART_CLEAR_IDLEFLAG(huart1);HAL_UART_DMAStop(huart1);uint16_treceive_lensizeof(uart_rx_buffer)-__HAL_DMA_GET_COUNTER(huart1.hdmarx);/* 处理 uart_rx_buffer 中 receive_len 长度的数据 */HAL_UART_Receive_DMA(huart1,uart_rx_buffer,sizeof(uart_rx_buffer));}HAL_UART_IRQHandler(huart1);}实际工程中还要注意缓冲区保护、数据解析和多任务同步。二十六、STM32 HAL ADC DMA示例1. 定义ADC采样数组uint16_tadc_buffer[100];2. 启动ADC DMAHAL_ADC_Start_DMA(hadc1,(uint32_t*)adc_buffer,100);3. 半传输和传输完成回调voidHAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef*hadc){if(hadc-InstanceADC1){/* 处理 adc_buffer 前半部分数据 */}}voidHAL_ADC_ConvCpltCallback(ADC_HandleTypeDef*hadc){if(hadc-InstanceADC1){/* 处理 adc_buffer 后半部分数据 */}}这种方式适合连续采样。二十七、STM32 HAL SPI DMA示例1. SPI DMA发送uint8_tspi_tx_buffer[256];HAL_SPI_Transmit_DMA(hspi1,spi_tx_buffer,sizeof(spi_tx_buffer));2. SPI DMA接收uint8_tspi_rx_buffer[256];HAL_SPI_Receive_DMA(hspi1,spi_rx_buffer,sizeof(spi_rx_buffer));3. SPI全双工收发uint8_tspi_tx_buffer[256];uint8_tspi_rx_buffer[256];HAL_SPI_TransmitReceive_DMA(hspi1,spi_tx_buffer,spi_rx_buffer,sizeof(spi_tx_buffer));对于 SPI 设备还要重点控制 CS 时序。有些场景需要CS拉低 启动DMA传输 等待DMA完成 等待SPI BUSY清除 CS拉高二十八、学习DMA要记住的核心结论学习 DMA可以先记住以下结论DMA 是直接存储器访问。DMA 可以理解为硬件数据搬运工。CPU 负责配置 DMA。DMA 负责自动搬运数据。DMA 常用于外设和内存之间搬运数据。USART 接收适合 DMA因为串口数据可能连续到来。USART 发送适合 DMA因为可以一次发送整段数据。SPI 适合 DMA因为 SPI 常用于高速连续传输。ADC 适合 DMA因为 ADC 连续采样结果需要自动入内存。DMA 的关键参数是源地址、目标地址、长度和方向。外设寄存器地址通常不递增。RAM数组地址通常需要递增。普通模式传输完成后停止。循环模式传输完成后自动重新开始。半传输中断适合处理前半段缓冲区。传输完成中断适合处理后半段或整段数据。DMA 和中断不是一回事。DMA 负责搬数据中断负责通知 CPU。DMA 不工作时要先看外设是否真的产生请求。数据量越大、越连续DMA 优势越明显。二十九、结语DMA 是嵌入式开发中非常重要的效率工具。它不能替代 CPU 的逻辑判断和控制决策但可以帮助 CPU 从大量重复的数据搬运工作中解放出来。可以用一句话总结 DMACPU擅长计算和控制 DMA擅长搬运数据 数据量越大、越连续 DMA价值越明显。USART、SPI、ADC 都喜欢用 DMA本质原因都是它们经常产生连续数据流 而这些数据需要在外设和内存之间高效搬运。真正掌握 DMA需要把下面这些内容串起来源地址 目标地址 传输方向 传输长度 数据宽度 地址递增 触发源 普通/循环模式 半传输/完成中断当这些配置理解清楚后再去看 USART DMA、SPI DMA、ADC DMA就会顺很多。