一、引言
DMA(Direct Memory Access,直接存储器访问)是嵌入式系统中一项至关重要的核心技术,它允许外设与内存之间直接进行数据传输,而无需 CPU 的全程干预。在传统的程序控制或中断驱动的 I/O 方式中,CPU 需要频繁地参与每一个数据单元的搬运,这不仅消耗了宝贵的 CPU 周期,也限制了系统的整体吞吐量。DMA 技术的出现,正是为了解决这一瓶颈。
其核心思想是:CPU 仅需在传输开始前,对 DMA 控制器进行初始化配置(如设置源地址、目标地址、传输数据量等),随后便可“抽身”去执行其他计算任务。数据传输过程则由 DMA 控制器接管系统总线,在外设与内存之间直接完成。待整个数据块传输完毕,DMA 控制器再通过中断通知 CPU。这种方式将 CPU 从繁琐的、重复性的数据搬运工作中解放出来,显著提高了系统效率,降低了 CPU 占用率,尤其适用于音频/视频流处理、高速数据采集、网络通信等对实时性和带宽要求高的场景。
理解 DMA 的工作原理、工作模式及其在嵌入式系统中的应用,是嵌入式软件工程师面试中的高频考点,也是进行高性能系统设计的基础。本文将从 DMA 的基本概念出发,系统梳理其工作原理、传输模式、控制器结构、典型应用,并解答常见的面试问题。
二、DMA 工作原理
DMA(直接存储器访问)的核心在于将 CPU 从繁重的数据搬运任务中解放出来,实现外设与内存之间的高效、直接数据传输。其工作原理可以概括为“CPU 配置,DMA 执行,中断通知”。
一次完整的 DMA 传输通常包含以下五个关键步骤:
- CPU 初始化配置:CPU 作为“导演”,首先对 DMA 控制器进行编程。这包括设置源地址(数据从哪里来,如外设数据寄存器)、目标地址(数据到哪里去,如内存缓冲区)、传输数据量(字节数或字数)以及传输模式(单次、块传输等)。配置完成后,CPU 使能 DMA 通道,准备开始传输。
- 外设发起传输请求:当外设(例如 UART 收到一个字节、ADC 完成一次转换)准备好数据或需要发送数据时,它会向 DMA 控制器发出一个硬件信号——DMA 请求(DREQ)。这相当于外设“举手”示意自己已就绪。
- DMA 控制器接管系统总线:收到 DREQ 后,DMA 控制器作为“总线主设备”,会向 CPU 发出总线保持请求(HOLD 或类似信号)。CPU 在当前总线周期结束后,会响应此请求,释放对地址总线、数据总线和控制总线的控制权,并通过总线保持应答(HLDA)信号通知 DMA 控制器:“总线现在归你使用”。这个过程称为总线仲裁。
- DMA 执行数据传输:获得总线控制权后,DMA 控制器开始独立工作。它直接操纵总线,从源地址读取数据,然后写入目标地址。每完成一次传输(如一个字节),它会自动递增(或递减)地址寄存器,并递减字节计数器。这个过程完全由硬件完成,CPU 在此期间可以执行其他任务(前提是不需要访问被 DMA 占用的总线)。
- 传输完成与中断通知:当字节计数器减为零,表示预设的数据量已全部传输完毕。DMA 控制器会撤销总线请求(HOLD),将总线控制权交还给 CPU。同时,它通常会向 CPU 发出一个传输完成中断(TCI),通知 CPU 本次 DMA 传输已经结束。CPU 响应该中断后,便可对传输完成的数据进行处理(例如,处理内存缓冲区中已接收到的完整数据包)。
核心优势:在整个数据传输的“核心阶段”(步骤4),CPU 无需像中断方式那样,为每个数据单元执行保存现场、搬运数据、恢复现场等一系列指令。DMA 通过硬件接管总线,实现了数据的“直接”搬移,从而大幅降低了 CPU 开销,显著提升了系统整体吞吐量和实时性。
三、DMA 传输模式
DMA 控制器支持多种传输模式,以适应不同的外设特性和应用场景。理解这些模式对于合理配置 DMA、优化系统性能至关重要。
1. 单次传输模式
每次外设请求只传输一个数据单元(如一个字节或一个字),传输完成后 DMA 控制器释放总线,CPU 重新获得控制权。适用于低速、非连续的外设。
实战代码示例(STM32 HAL 库配置单次传输):
// 以 STM32 UART 接收 DMA 单次传输为例 #include "stm32f4xx_hal.h" UART_HandleTypeDef huart2; DMA_HandleTypeDef hdma_usart2_rx; uint8_t rx_buffer[1]; // 单字节缓冲区 void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn); } void MX_USART2_UART_Init(void) { huart2.Instance = USART2; huart2.Init.BaudRate = 115200; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart2.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart2); } void Start_DMA_Single_Transfer(void) { // 配置 DMA 为单次模式 hdma_usart2_rx.Instance = DMA1_Stream5; hdma_usart2_rx.Init.Channel = DMA_CHANNEL_4; hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart2_rx.Init.Mode = DMA_NORMAL; // 单次模式 hdma_usart2_rx.Init.Priority = DMA_PRIORITY_LOW; hdma_usart2_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; HAL_DMA_Init(&hdma_usart2_rx); // 关联 DMA 到 UART 接收 __HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx); // 启动 DMA 接收(单次传输一个字节) HAL_UART_Receive_DMA(&huart2, rx_buffer, 1); } // DMA 传输完成中断回调 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { // 处理接收到的单字节数据 rx_buffer[0] // 如需继续接收,需重新调用 HAL_UART_Receive_DMA } }2. 块传输模式
DMA 控制器在一次总线占用期间,连续传输整个数据块(多个数据单元),直到计数器减为零。传输效率高,但会长时间占用总线。
实战代码示例(STM32 ADC 多通道扫描 DMA 块传输):
// STM32 ADC 多通道扫描,使用 DMA 块传输搬运数据 #include "stm32f4xx_hal.h" ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; uint16_t adc_buffer[3]; // 存储 3 个通道的转换结果 void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ENABLE; // 扫描模式 hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 3; hadc1.Init.DMAContinuousRequests = ENABLE; // DMA 连续请求 hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV; HAL_ADC_Init(&hadc1); // 配置通道 0、1、2 sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_2; sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig); } void Start_ADC_DMA_Block_Transfer(void) { // 配置 DMA 为循环模式(块传输的连续形式) hdma_adc1.Instance = DMA2_Stream0; hdma_adc1.Init.Channel = DMA_CHANNEL_0; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式,持续传输 hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE; HAL_DMA_Init(&hdma_adc1); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); // 启动 ADC 并开始 DMA 传输(块传输 3 个通道数据) HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 3); } // DMA 传输完成中断(半传输和全传输) void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { // adc_buffer 中已更新了 3 个通道的最新数据 // 可在此进行数据处理 } }3. 请求传输模式
传输由外设的 DREQ 信号控制。只要 DREQ 有效,DMA 就持续传输;DREQ 无效则暂停,总线交还 CPU。适用于数据产生速率不固定的场景。
实战场景说明:
在请求传输模式下,DMA 控制器会持续监测外设的 DREQ(DMA 请求)信号。例如,一个外部传感器通过 GPIO 引脚产生不规则的数据就绪信号。当 DREQ 为高时,DMA 启动一次传输;传输完成后,若 DREQ 仍为高,则继续下一次传输;若 DREQ 变低,则暂停并释放总线。这种模式常见于需要根据外部事件节奏进行数据传输的场合,如从 FIFO 读取数据。
4. 级联模式
多个 DMA 控制器级联,扩展通道数。一个主 DMA 控制器可以管理多个从 DMA 控制器。
配置要点:
- 主 DMA 控制器的通道配置为级联模式,其“外设”实际上是另一个(从)DMA 控制器。
- 从 DMA 控制器负责实际的数据传输,其传输完成信号会触发主 DMA 控制器。
- 这种方式可以扩展出更多的 DMA 通道,用于非常复杂的多外设、多缓冲区场景。
模式选择建议:
| 传输模式 | 适用场景 | CPU 占用 | 总线占用 |
|---|---|---|---|
| 单次传输 | 低速外设、非连续数据(如按键扫描、单字节 UART) | 低(每次传输后释放总线) | 短时占用 |
| 块传输 | 高速、连续数据流(如 ADC 多通道采样、LCD 刷新) | 极低(传输期间 CPU 可执行缓存内代码) | 长时间连续占用 |
| 请求传输 | 数据产生速率不定(如外部传感器、FIFO 读取) | 低(仅在 DREQ 有效时占用) | 间歇性占用 |
| 级联模式 | 需要大量 DMA 通道的复杂系统 | 低(由主从 DMA 协同) | 取决于从 DMA 配置 |
四、DMA 控制器结构
DMA 控制器是执行 DMA 传输的核心硬件模块,其内部结构决定了 DMA 的功能和性能。一个典型的 DMA 控制器主要由寄存器组、地址生成单元、计数器单元、控制逻辑以及总线接口单元构成。
1. 核心寄存器组
寄存器是 CPU 与 DMA 控制器交互的接口,也是 DMA 控制器工作的依据。典型的 DMA 控制器包含以下关键寄存器:
- 基地址寄存器(Base Address Register):存放传输的源起始地址或目标起始地址。在传输开始前由 CPU 写入。
- 当前地址寄存器(Current Address Register):存放当前正在传输的地址。每次传输完成后,该寄存器的值会根据配置自动递增或递减,指向下一个要访问的单元。
- 基字节计数寄存器(Base Byte Count Register):存放本次 DMA 传输需要搬运的总数据量(字节数或字数)。
- 当前字节计数寄存器(Current Byte Count Register):存放剩余待传输的数据量。每完成一次传输,该计数器自动递减。当减为零时,表示传输完成,通常会触发中断。
- 控制寄存器(Control Register):用于配置 DMA 传输的各种参数,例如:
- 传输方向:内存到外设(Memory-to-Peripheral, M2P)、外设到内存(P2M)或内存到内存(M2M)。
- 传输模式:单次、块传输、请求传输或循环模式。
- 地址递增/递减:设置每次传输后源地址和目标地址是递增、递减还是保持不变。
- 数据宽度:字节(8位)、半字(16位)或字(32位)。
- 优先级:在多通道 DMA 中设置通道的优先级。
- 中断使能:使能传输完成中断、半传输中断或错误中断。
- 状态寄存器(Status Register):反映 DMA 控制器的当前工作状态,例如传输完成标志、错误标志、通道使能状态等。CPU 可通过轮询此寄存器或结合中断来获知传输进度。
2. 地址生成与计数器单元
这两个单元是 DMA 控制器自动执行传输的“引擎”。
- 地址生成单元:根据控制寄存器的配置,在每次传输后自动更新当前地址寄存器。例如,若配置为“地址递增”,则在每次传输后,地址寄存器会加上数据宽度对应的字节数(如+1、+2、+4)。
- 计数器单元:负责管理当前字节计数寄存器。每完成一次数据传输,计数器减1(或根据数据宽度减相应的值)。当计数器减为零时,产生“传输完成”信号,并可能触发中断。
3. 控制逻辑与仲裁器
- 控制逻辑:解析控制寄存器的配置,协调寄存器组、地址生成单元、计数器单元和总线接口单元的工作时序,生成内部控制信号。
- 仲裁器(Arbiter):当多个 DMA 通道同时请求总线时,仲裁器根据预设的优先级(固定优先级或轮询)决定哪个通道获得总线使用权。这是实现多通道 DMA 并发或顺序工作的关键。
4. 总线接口单元
这是 DMA 控制器与系统总线(地址总线、数据总线、控制总线)交互的物理接口。它负责:
- 在获得总线控制权后,驱动地址总线和控制总线,发出读写信号。
- 在数据总线上读取源地址的数据或写入数据到目标地址。
- 实现与 CPU 之间的总线请求(HOLD)和总线应答(HLDA)握手协议。
5. 现代 DMA 控制器的增强特性
随着技术的发展,现代 MCU/SoC 中的 DMA 控制器往往集成了更高级的功能:
- 多通道独立配置:一个 DMA 控制器通常包含多个独立的通道,每个通道拥有自己的一套寄存器,可以同时或分时处理不同外设的传输请求。
- 链接模式(Linked List)或描述符模式:支持将多个传输任务(描述符)链接成一个链表。DMA 控制器可以自动从一个描述符跳转到下一个,实现复杂的、非连续的数据块传输(即 Scatter/Gather DMA),无需 CPU 反复介入配置。
- 双缓冲区(Double Buffer)支持:允许配置两个目标缓冲区。当 DMA 向其中一个缓冲区写入数据时,CPU 可以安全地读取另一个缓冲区已满的数据,实现“乒乓”操作,避免数据竞争。
- 内存保护(MPU)集成:部分高端控制器集成了内存保护单元,可以限制 DMA 访问的地址范围,防止错误配置导致的内存覆盖,提升系统安全性。
理解 DMA 控制器的内部结构,有助于我们更深入地掌握其工作原理,并在实际编程中正确、高效地配置相关寄存器,充分发挥 DMA 的性能优势。
五、DMA 在嵌入式系统中的典型应用
DMA 技术极大地提升了嵌入式系统的 I/O 效率,其应用场景遍布各类外设与内存之间的数据搬运。以下是几个典型的应用实例,并附上关键配置思路或代码片段。
1. UART 串口通信
在串口通信中,使用 DMA 可以避免 CPU 为每个字节的收发产生中断,尤其适合高速或大数据量传输。
- 应用场景:Modbus 通信、GPS 模块数据接收、调试日志输出。
- 配置要点:
- 接收:配置 DMA 为外设到内存(Peripheral-to-Memory)模式,源地址为 UART 数据寄存器(如 USARTx→DR),目标地址为内存缓冲区。
- 发送:配置 DMA 为内存到外设(Memory-to-Peripheral)模式,源地址为待发送数据缓冲区,目标地址为 UART 数据寄存器。
- 通常使用循环模式(Circular Mode)实现持续接收,或单次/普通模式(Normal Mode)发送固定长度数据。
// STM32 HAL 库示例:UART1 使用 DMA 接收不定长数据(空闲中断 + DMA) // 关键步骤简述: // 1. 初始化 UART 和 DMA(方向 PERIPH_TO_MEMORY,循环模式)。 // 2. 启动 DMA 接收:HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE); // 3. 使能 UART 空闲中断:__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 4. 在空闲中断回调中,计算已接收数据长度并处理。 void HAL_UART_IdleCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 计算本次接收到的数据长度 uint32_t remaining = __HAL_DMA_GET_COUNTER(huart->hdmarx); uint32_t received_len = BUFFER_SIZE - remaining; if (received_len > 0) { process_rx_data(rx_buffer, received_len); // 处理数据 } // 重新启动 DMA 接收(循环模式下缓冲区自动回绕) } }2. ADC 模数转换数据搬运
ADC 转换速率往往很高,使用 DMA 可以将转换结果自动搬运到指定数组,供 CPU 后续处理,避免丢失采样点。
- 应用场景:多通道传感器采样、音频采集、电源监控。
- 配置要点:
- 配置 ADC 为扫描模式(Scan Mode)和连续转换模式(Continuous Conversion Mode)。
- 配置 DMA 为外设到内存模式,源地址为 ADC 数据寄存器(如 ADCx→DR),目标地址为数组。
- 使能 DMA 连续请求(DMA Continuous Requests),确保 ADC 转换完成立即触发 DMA 传输。
- 通常使用循环模式实现持续采样,结合双缓冲区(Double Buffer)或半传输/全传输中断实现“乒乓”操作,确保数据处理时 DMA 正在写入另一个缓冲区。
// STM32 HAL 库示例:ADC1 三通道扫描,DMA 循环传输 // 关键配置: // hadc1.Init.ScanConvMode = ENABLE; // hadc1.Init.ContinuousConvMode = ENABLE; // hadc1.Init.DMAContinuousRequests = ENABLE; // hadc1.Init.NbrOfConversion = 3; // hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式 // 启动:HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 3); // 在 DMA 传输完成中断(HAL_ADC_ConvCpltCallback)中处理 adc_buffer 数据。3. SPI/I2C 总线通信
SPI 和 I2C 常用于连接传感器、存储器等外设。在大块数据读写时,DMA 能显著提升吞吐量。
- 应用场景:读写 SD 卡、Flash 存储器、高分辨率传感器(如摄像头)、OLED 屏数据更新。
- 配置要点:
- SPI:全双工通信时,需同时使能 TX 和 RX 的 DMA。注意 SPI 时钟相位和极性的匹配。
- I2C:部分 MCU 的 I2C 外设支持 DMA(如 STM32 的 I2C 与 DMA 联动)。需注意 I2C 的时钟拉伸(Clock Stretching)特性。
- 通常使用内存到外设(发送)和外设到内存(接收)的组合。
// STM32 示例:SPI1 使用 DMA 发送数据到外部 Flash // 初始化 SPI 和 DMA(方向 MEMORY_TO_PERIPHERAL) // 启动传输:HAL_SPI_Transmit_DMA(&hspi1, tx_buffer, size); // 在传输完成中断(HAL_SPI_TxCpltCallback)中处理后续逻辑(如切换片选)。4. 内存到内存(Memory-to-Memory)拷贝
某些 DMA 控制器支持在两个内存区域之间直接搬运数据,速度远高于 CPU 的逐字节拷贝。
- 应用场景:图像处理中的缓冲区交换、数据块搬移、加密/解密前的数据准备。
- 配置要点:
- 配置 DMA 通道为内存到内存模式。
- 设置源地址和目标地址均为内存地址(如 SRAM 中的两个数组)。
- 数据宽度通常设置为字(32位)以获得最佳性能。
- 注意源和目标地址的对齐,不对齐可能降低性能或需要特殊处理。
// STM32 内存到内存 DMA 拷贝示例(伪代码) // hdma_memtomem.Init.Direction = DMA_MEMORY_TO_MEMORY; // hdma_memtomem.Init.PeriphInc = DMA_PINC_ENABLE; // 源地址递增 // hdma_memtomem.Init.MemInc = DMA_MINC_ENABLE; // 目标地址递增 // HAL_DMA_Start(&hdma_memtomem, (uint32_t)src, (uint32_t)dst, size); // HAL_DMA_PollForTransfer(&hdma_memtomem, HAL_DMA_FULL_TRANSFER, 1000); // 或使用中断5. LCD 显示刷新
图形界面需要持续向 LCD 控制器发送帧缓冲区数据,使用 DMA 可以解放 CPU,实现流畅的动画效果。
- 应用场景:TFT-LCD 屏驱动、OLED 屏刷新、图形用户界面(GUI)。
- 配置要点:
- 通常通过 FSMC(Flexible Static Memory Controller)或专用 LCD 控制器接口(如 LTDC)连接 LCD。
- 配置 DMA 为内存到外设模式,源地址为帧缓冲区(Frame Buffer),目标地址为 LCD 数据寄存器。
- 使用循环模式或配合垂直/水平同步信号触发 DMA 传输,实现自动刷新。
- 结合双缓冲区技术,避免屏幕撕裂(Tearing Effect)。
6. 音频流处理
音频数据的实时采集和播放对时序要求严格,DMA 能确保数据流的连续性。
- 应用场景:麦克风录音、扬声器播放、音频编解码器(Codec)数据搬运。
- 配置要点:
- 使用 I2S 或 SAIAI 接口连接音频 Codec。
- 配置两个 DMA 流:一个用于接收(录音),一个用于发送(播放)。
- 使用循环模式和双缓冲区,结合半传输中断(Half Transfer Complete)和全传输中断(Transfer Complete)实现“乒乓”缓冲,确保音频流无中断。
// 音频 DMA 双缓冲区“乒乓”操作示意 // 定义两个缓冲区:buffer[0] 和 buffer[1] // 启动 DMA 循环传输,目标为 I2S 数据寄存器,长度为单个缓冲区大小。 // 在 DMA 半传输中断中,处理 buffer[0] 的数据(已填满前半部分)。 // 在 DMA 全传输中断中,处理 buffer[1] 的数据(已填满后半部分)。 // 如此循环,实现连续音频流处理。总结与选型建议:在选择 DMA 应用模式时,需综合考虑外设特性、数据量、实时性要求以及系统总线负载。对于低速、零星数据,单次传输模式即可;对于高速连续流(如音频、视频),块传输或循环模式是首选;对于数据产生不规律的场景(如外部传感器),请求传输模式更为合适。合理运用 DMA 可以大幅降低 CPU 负载,提升系统整体性能和响应能力。
六、面试常见问题
1. DMA 相比中断方式传输数据有何优势?
答:中断方式每次传输一个数据单元都需要 CPU 介入,进行保存现场、处理数据、恢复现场等操作,开销大。DMA 将 CPU 从繁琐的 I/O 操作中解放出来,仅在传输开始和结束时参与,大大降低了 CPU 占用率,提高了数据传输效率,尤其适合大批量、高速的数据传输。
2. DMA 传输过程会占用系统总线,这会影响 CPU 访问内存吗?
答:会。在 DMA 传输期间,DMA 控制器作为总线主设备,CPU 对内存的访问会被暂时挂起(等待或插入等待周期)。这称为“周期窃取”。现代系统通常采用总线仲裁和内存控制器来优化,例如支持并发访问的存储器、设置 DMA 传输的优先级、利用 CPU 缓存减少总线争用等,以最小化对 CPU 性能的影响。
3. 如何防止 DMA 传输覆盖关键内存区域?
答:
- 硬件层面:一些高级 MCU/MPU 的 DMA 控制器支持内存保护单元(MPU),可以配置 DMA 可访问的内存地址范围。
- 软件层面:驱动程序在配置 DMA 源/目标地址时,必须进行严格的边界检查,确保地址位于合法的缓冲区范围内。同时,操作系统(如 Linux)会通过 I/O 内存管理单元(IOMMU)或类似机制,为 DMA 分配专用的、受保护的物理内存区域。
4. 解释一下“分散/聚集”(Scatter/Gather)DMA。
答:这是一种高级 DMA 模式。传统 DMA 要求数据在物理内存中是连续的。“分散/聚集” DMA 允许 DMA 控制器通过一个描述符链表来执行一次传输。每个描述符包含一个内存块的起始地址和长度。DMA 控制器可以自动按顺序从一个不连续的物理内存区域(聚集)读取数据,并传输到外设;或者从外设读取数据,并写入到多个不连续的内存块(分散)。这极大地提高了处理碎片化缓冲区的效率,常见于网络数据包处理、文件 I/O 等场景。
七、总结
DMA(直接存储器访问)作为嵌入式系统性能优化的核心技术,其核心价值在于将 CPU 从繁重的数据搬运任务中解放出来,实现外设与内存之间的高效、直接数据传输。通过“CPU 配置,DMA 执行,中断通知”的工作流程,DMA 大幅降低了 CPU 开销,提升了系统整体吞吐量和实时性。
本文系统梳理了 DMA 的关键知识点:
- 工作原理:基于总线仲裁与周期窃取,实现 CPU 与 DMA 控制器的协同工作。
- 传输模式:包括适用于低速外设的单次传输、适合高速连续流的块传输、适应不规则数据节奏的请求传输以及用于扩展通道的级联模式。
- 控制器结构:由寄存器组、地址生成与计数器单元、控制逻辑与仲裁器、总线接口单元构成,现代控制器还集成了多通道、描述符链表、双缓冲区、内存保护等增强特性。
- 典型应用:涵盖 UART、ADC、SPI/I2C、内存拷贝、LCD 刷新、音频流处理等场景,并提供了关键配置思路与代码片段。
- 面试要点:深入探讨了 DMA 相比中断的优势、总线占用影响、内存保护机制以及高级的分散/聚集(Scatter/Gather)DMA 模式。
掌握 DMA 技术,不仅要求理解其底层硬件机制,更需要结合具体外设和实际场景进行灵活配置与优化。在嵌入式系统设计中,合理运用 DMA 能够显著提升 I/O 效率,降低 CPU 负载,是构建高性能、低功耗、高实时性系统的关键技能。建议读者在理解本文概念的基础上,动手实践文中的代码示例,并在实际项目中根据数据特性、时序要求和系统资源,选择最合适的 DMA 传输模式与配置策略。