2. DMA工作原理:DMA请求、DMA响应、DMA传输流程(读-写-地址递增)
DMA(Direct Memory Access,直接存储器访问)是一种允许外设与存储器之间直接交换数据而无需CPU干预的硬件机制。理解DMA的请求、响应和传输流程,是掌握MCU高效数据传输的基础。
2.1 DMA请求与响应机制
DMA传输的发起方通常是外设(如UART、ADC、SPI、定时器),当外设准备好数据或需要数据时,会向DMA控制器发送请求信号。DMA控制器在获得总线控制权后,执行传输操作。
| 阶段 | 发起方 | 动作描述 | CPU状态 |
|---|---|---|---|
| DMA请求 | 外设 | 外设产生事件(如接收缓冲区非空、发送缓冲区空),向DMA控制器发出硬件请求信号 | 继续执行当前指令(无感知) |
| DMA响应 | DMA控制器 | DMA控制器仲裁优先级,向总线仲裁器申请总线控制权;获得授权后,接管地址/数据总线 | 暂停总线访问(插入等待周期) |
| DMA完成 | DMA控制器 | 传输计数减1,地址递增/递减;若计数归零,发送完成中断信号 | 恢复总线控制,响应中断(若使能) |
关键概念: DMA请求分为硬件请求(由外设硬件信号触发)和软件请求(由CPU写寄存器触发)。在MCU中,绝大多数外设驱动使用硬件请求实现实时数据传输。
2.2 DMA传输流程:读-写-地址递增
一次完整的DMA传输由三个基本操作构成:读取源数据 → 写入目标地址 → 更新地址指针。这三个步骤在DMA控制器的硬件状态机中顺序执行,每个总线周期完成一次数据搬运。
传输粒度: DMA支持字节(8位)、半字(16位)、字(32位)三种传输宽度。源和目标宽度可以不同,DMA控制器会自动进行对齐或填充。
- 读阶段: DMA控制器将源地址(外设数据寄存器或存储器地址)加载到地址总线,发出读控制信号,从源位置读取数据到内部临时寄存器(FIFO或缓冲器)。
- 写阶段: DMA控制器将目标地址加载到地址总线,发出写控制信号,将临时寄存器中的数据写入目标位置(存储器或外设数据寄存器)。
- 地址递增: 传输完成后,DMA控制器根据配置自动更新源地址和/或目标地址。递增模式支持:固定(地址不变)、递增(地址+传输宽度)、递减(地址-传输宽度)。
常见陷阱: 外设数据寄存器通常配置为固定地址模式(不递增),而存储器缓冲区配置为递增地址模式。若两者配置颠倒,会导致数据覆盖或读取错误。
2.3 DMA传输流程SVG图
下图展示了DMA控制器从外设到存储器的单次传输流程,包含请求、仲裁、读、写、地址递增五个核心步骤。
2.4 地址递增模式对比
DMA控制器支持灵活的地址更新策略,下表总结了三种常见模式及其适用场景。
| 地址模式 | 源地址行为 | 目标地址行为 | 典型应用 |
|---|---|---|---|
| 固定模式 | 地址不变 | 地址不变 | 外设寄存器→外设寄存器(如SPI回环测试) |
| 递增模式 | 地址 + 传输宽度 | 地址 + 传输宽度 | 外设寄存器→存储器缓冲区(ADC采集、UART接收) |
| 递减模式 | 地址 - 传输宽度 | 地址 - 传输宽度 | 存储器→外设(如从尾部向前发送数据) |
配置建议: 在MCU外设驱动中,外设地址固定 + 存储器地址递增是最常用的组合。例如UART接收DMA:源地址固定为USART->DR寄存器,目标地址为数组buffer,每次传输后buffer地址自动后移。
2.5 DMA传输配置代码示例(以STM32 HAL库为例)
以下代码展示了如何配置UART接收DMA,实现外设到存储器的循环传输,包含地址递增和传输完成中断。
// UART接收DMA配置(STM32 HAL库)
UART_HandleTypeDef huart2;
DMA_HandleTypeDef hdma_usart2_rx;
void MX_DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
// DMA传输配置:外设→存储器,地址递增
hdma_usart2_rx.Instance = DMA1_Channel5;
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; // 8位传输
hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_rx.Init.Mode = DMA_NORMAL; // 普通模式(非循环)
hdma_usart2_rx.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_usart2_rx);
// 关联DMA句柄到UART句柄
__HAL_LINKDMA(&huart2, hdmarx, hdma_usart2_rx);
// 使能传输完成中断
HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
}
// 启动DMA传输:接收100字节到buffer
uint8_t rx_buffer[100];
HAL_UART_Receive_DMA(&huart2, rx_buffer, 100);
// DMA传输完成中断回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2) {
// 数据处理:rx_buffer已包含100字节数据
// 注意:地址递增后,下次传输从rx_buffer[0]重新开始
ProcessReceivedData(rx_buffer, 100);
}
}
调试要点: 在DMA传输过程中,CPU可以执行其他任务。但需注意数据一致性问题:CPU读取DMA缓冲区前,必须确保DMA传输已完成(通过查询传输完成标志或等待中断),否则可能读到不完整数据。
2.6 DMA传输与CPU协同的时序关系
DMA传输并非完全独立于CPU,两者共享系统总线。下图展示了DMA与CPU在总线访问上的交替关系。
性能权衡: DMA传输虽然减少了CPU的数据搬运开销,但每次传输会占用1~2个总线周期。对于高频小数据量传输(如SPI每字节触发一次DMA),总线竞争可能导致CPU性能下降。此时可考虑FIFO缓冲或突发传输模式来减少总线占用次数。