--- title: DMA tags: [STM32, DMA, 数据传输] created: 2026-07-06 --- # DMA — 直接存储器访问 ## 原理 > **PPT 参考图 -- DMA简介与存储器映射** ![PPT: DMA简介与存储器映射](11-DMA/assets/slide-100.jpg) ![PPT: DMA简介与存储器映射](11-DMA/assets/slide-101.jpg) ### DMA 是什么 Direct Memory Access(直接存储器访问),在不占用 CPU 的情况下在外设和存储器之间搬运数据: ``` 传统方式 (CPU搬运): CPU: 读取 外设DR → CPU寄存器 → 写入 内存数组 CPU忙 → 无法处理其他任务 DMA方式: CPU: 配置DMA → 继续运行其他代码 DMA: 自动 外设DR → 内存数组 (不占用CPU) CPU: DMA传输完成 → 中断通知CPU ``` ### 详细工作原理 **DMA 传输完整流程**:外设产生 DMA 请求 → DMA 控制器仲裁 → 获得 AHB 总线控制权 → 从源地址读取数据 → 写入目标地址 → 传输计数器减 1 → 重复直到计数器为 0。 1. **外设请求**:外设(如 ADC、USART、TIM)在特定事件发生时(ADC 转换完成、USART 接收数据、TIM 更新事件)向 DMA 控制器发送硬件请求信号 2. **仲裁**:DMA1 有 7 个通道,每个通道有可编程优先级(`DMA_Priority_VeryHigh/High/Medium/Low`)。多个通道同时请求时,仲裁器先响应高优先级通道;同优先级时,通道号小的优先 3. **总线访问**:获得仲裁后,DMA 通道成为 AHB 总线主控,执行一次数据传输。DMA 传输以 AHB 总线时钟(72MHz)为节奏,每次传输占用若干 AHB 周期 4. **源/目标地址操作**: - 外设地址:通常固定(如 `&ADC1->DR`),配置 `DMA_PeripheralInc_Disable` - 内存地址:通常递增(如数组),配置 `DMA_MemoryInc_Enable` - 每次传输后地址按数据宽度递增(Byte+1、HalfWord+2、Word+4) **传输方向详解**: - **外设→内存(P2M)**:ADC 多通道扫描时,每次转换完成 DMA 将 ADC_DR 的值搬运到数组。方向位 `DMA_DIR_PeripheralSRC`,外设是源 - **内存→外设(M2P)**:USART 发送时,DMA 将发送缓冲区逐字节写入 USART_DR。方向位 `DMA_DIR_PeripheralDST`,外设是目标 - **内存→内存(M2M)**:`DMA_M2M_Enable` 时,DMA 通过软件触发启动,自动从源数组搬运到目标数组,无需外设请求。此时 DMA 通道不受通道映射限制,任何通道均可用于 M2M **循环模式**:`DMA_Mode_Circular` 下,传输计数器减到 0 后自动重载为初始值(`DMA_BufferSize`),地址指针回到起点,持续传输。适用于 ADC 连续扫描 + DMA 的后台无 CPU 采集。 **M2M 模式**:`DMA_M2M_Enable` 时,DMA 通过软件写 `DMA_CCR.EN` 位触发启动(无需外设请求),数据从 `DMA_CPAR`(外设地址寄存器)指向的源地址传送到 `DMA_CMAR`(内存地址寄存器)指向的目标地址。M2M 模式不受通道与硬件请求的映射限制,任意通道均可使用。 ### 基本结构 > **PPT 参考图 -- DMA框图、请求与数据宽度** ![PPT: DMA框图、请求与数据宽度](11-DMA/assets/slide-102.jpg) ![PPT: DMA框图、请求与数据宽度](11-DMA/assets/slide-103.jpg) ![PPT: DMA框图、请求与数据宽度](11-DMA/assets/slide-104.jpg) ![PPT: DMA框图、请求与数据宽度](11-DMA/assets/slide-105.jpg) ![PPT: DMA框图、请求与数据宽度](11-DMA/assets/slide-106.jpg) ![PPT: DMA框图、请求与数据宽度](11-DMA/assets/slide-107.jpg) ``` DMA基本传输单元: 外设地址寄存器 DMA_CPARx ↓ 外设 ←→ DMA 通道 ←→ 存储器 ↑ 传输方向 DMA1 (STM32F103C8T6): ├─ 通道1: ADC1 / TIM2_CH3 / TIM4_CH1 ├─ 通道2: USART3_TX / SPI1_RX / TIM1_CH1 ├─ 通道3: USART3_RX / SPI1_TX / TIM1_CH2 ├─ 通道4: USART1_TX / SPI2_RX / TIM2_CH4 ├─ 通道5: USART1_RX / SPI2_TX / TIM3_CH2 ├─ 通道6: USART2_TX / SPI3_RX / TIM3_CH3 └─ 通道7: USART2_RX / SPI3_TX / TIM3_CH4 DMA2: 仅大容量芯片有 (F103RC以上) ``` ### 传输方向 ``` 内存到内存 (M2M): 源: 数组A 目标: 数组B DMA直接复制,不经过CPU 外设到内存 (P2M): 源: ADC_DR/USART_DR 目标: 用户数组 典型:ADC多通道扫描 内存到外设 (M2P): 源: 发送缓冲区 目标: USART_DR/SPI_DR 典型:串口发送大批量数据 ``` ### 循环模式 vs 正常模式 ``` 正常模式 (Normal): 传输 BufferSize 次后停止 需要重新配置 BufferSize 和 Cmd 循环模式 (Circular): 传输完 BufferSize 次后自动重置 地址回到起点,持续传输 适合 ADC 连续扫描 + DMA ``` --- ## API 表格 ### DMA_InitTypeDef ```c typedef struct { uint32_t DMA_PeripheralBaseAddr; // 外设基地址 (&USART1->DR, &ADC1->DR) uint32_t DMA_MemoryBaseAddr; // 内存基地址 (uint32_t)&数组 uint32_t DMA_DIR; // DMA_DIR_PeripheralSRC / PeripheralDST uint32_t DMA_BufferSize; // 传输次数 uint32_t DMA_PeripheralInc; // DMA_PeripheralInc_Enable / Disable uint32_t DMA_MemoryInc; // DMA_MemoryInc_Enable / Disable uint32_t DMA_PeripheralDataSize; // Byte / HalfWord / Word uint32_t DMA_MemoryDataSize; // Byte / HalfWord / Word uint32_t DMA_Mode; // DMA_Mode_Normal / Circular uint32_t DMA_Priority; // VeryHigh / High / Medium / Low uint32_t DMA_M2M; // DMA_M2M_Enable / Disable } DMA_InitTypeDef; ``` ### 核心函数 | 函数 | 描述 | |------|------| | `DMA_Init(DMA_Channelx, &DMA_InitStructure)` | 初始化 DMA 通道 | | `DMA_Cmd(DMA_Channelx, ENABLE)` | 使能 DMA 通道 | | `DMA_ITConfig(DMA_Channelx, DMA_IT_TC, ENABLE)` | 使能传输完成中断 | | `DMA_GetFlagStatus(DMA_Channelx, DMA_FLAG_TCx)` | 获取传输完成标志 | | `DMA_ClearFlag(DMA_Channelx, DMA_FLAG_TCx)` | 清除传输完成标志 | | `DMA_GetITStatus(DMA_Channelx, DMA_IT_TC)` | 获取传输完成中断状态 | | `DMA_ClearITPendingBit(DMA_Channelx, DMA_IT_TC)` | 清除传输完成中断标志 | | `DMA_SetCurrDataCounter(DMA_Channelx, uint16_t)` | 设置剩余传输次数 | ### DMA 通道与硬件请求映射 | 通道 | 请求1 | 请求2 | 请求3 | 请求4 | 请求5 | |------|-------|-------|-------|-------|-------| | DMA1_CH1 | ADC1 | TIM2_CH3 | TIM4_CH1 | — | — | | DMA1_CH2 | USART3_TX | SPI1_RX | TIM1_CH1 | TIM2_UP | TIM3_CH3 | | DMA1_CH3 | USART3_RX | SPI1_TX | TIM1_CH2 | TIM2_CH1 | TIM3_CH4 | | DMA1_CH4 | USART1_TX | SPI2_RX | TIM1_CH3 | TIM2_CH2 | TIM4_CH3 | | DMA1_CH5 | USART1_RX | SPI2_TX | TIM1_CH4 | TIM2_CH4 | — | | DMA1_CH6 | USART2_TX | SPI3_RX | TIM1_UP | TIM3_CH1 | — | | DMA1_CH7 | USART2_RX | SPI3_TX | TIM1_TRIG | TIM3_CH2 | — | --- ## 代码示例 ### 1. 内存到内存搬运 (数组A → 数组B) ```c #include "stm32f10x.h" #define BUFFER_SIZE 32 const uint32_t src_buf[BUFFER_SIZE] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 }; uint32_t dst_buf[BUFFER_SIZE] = {0}; void DMA_M2M_Init(void) { RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)src_buf; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)dst_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; // 内存到内存 DMA_Init(DMA1_Channel6, &DMA_InitStructure); // CH6 可用于 M2M DMA_Cmd(DMA1_Channel6, ENABLE); // 等待传输完成 while (DMA_GetFlagStatus(DMA1_Channel6, DMA1_FLAG_TC6) == RESET); // 此时 dst_buf 内容应与 src_buf 相同 } int main(void) { DMA_M2M_Init(); while (1); } ``` ### 2. ADC + DMA 多通道连续采集 ```c #include "stm32f10x.h" #define ADC_CH_COUNT 4 uint16_t adc_values[ADC_CH_COUNT]; // PA0–PA3 void ADC1_DMA_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DMA1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); // ADC 配置:扫描+连续 ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = ADC_CH_COUNT; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); // DMA 配置 DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_values; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = ADC_CH_COUNT; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); ADC_DMACmd(ADC1, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // DMA 自动将 4 通道结果循环写入 adc_values[] } // 读取最新值 // adc_values[0]=PA0, [1]=PA1, [2]=PA2, [3]=PA3 // DMA 在 Circular 模式下自动更新 ``` ### 3. DMA + USART 发送 ```c #include "stm32f10x.h" const uint8_t tx_buffer[] = "Hello DMA!\r\n"; void USART1_DMA_Init(void) { // ... USART1 初始化同上节课内容 ... RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = sizeof(tx_buffer) - 1; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel4, &DMA_InitStructure); // CH4: USART1_TX DMA_Cmd(DMA1_Channel4, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); // DMA 自动将 tx_buffer 数据发送出去 } ``` --- ## 常见坑点 1. **DMA_BufferSize 是传输次数**:不是字节数。HalfWord 模式下传输 10 次 = 20 字节,Word 模式下传输 10 次 = 40 字节 2. **外设地址不要配错**:外设地址必须使用 `(uint32_t)&外设->DR` 形式。例:ADC 是 `&ADC1->DR`,USART 是 `&USART1->DR` 3. **DMA 通道与硬件请求对应**:ADC1 只能使用 DMA1_Channel1。USART1_TX 只能用 DMA1_Channel4。通道选错不会报错但也不会传输 4. **内存到内存(M2M)模式限制**: - M2M 模式使用 DMA1,且仅**通道 2/6/7** 可用于 M2M(不同芯片有差异) - M2M 模式下 `DMA_PeripheralBaseAddr` 指向源地址,`DMA_MemoryBaseAddr` 指向目标地址 - M2M 模式下 `DMA_DIR` 必须是 `DMA_DIR_PeripheralSRC` 5. **循环模式(Circular)注意**:使能循环后传输永不停,不触发正常传输完成中断(只有半传输和错误中断)。想要周期性处理数据时,用半传输中断(`DMA_IT_HT`)或定时器中断 6. **传输完成中断**:正常模式下每传输 `BufferSize` 次触发一次 TC 中断;使用 `DMA_ITConfig(DMA_Channelx, DMA_IT_TC, ENABLE)` 使能;中断服务函数中先清除标志再处理数据 7. **DMA 使能顺序**:先配置 DMA,再使能外设的 DMA 请求。如果先使能外设再使能 DMA,外设可能在 DMA 未就绪时产生第一次请求 8. **DMA 半传输中断**:`DMA_IT_HT` 在传输到一半时触发,适合双缓冲(乒乓缓冲)场景 9. **ADC 连续模式 + DMA 循环**:ADC 设置 `ContinuousConvMode=ENABLE`、DMA 设 `Mode=Circular`,可做到全程无 CPU 参与采集。但注意 ADC_CLK 越快,DMA 带宽占用越高 10. **DMA 不会自动停止**:正常模式传输完成后自动停止(`DMA_Cmd` 自动变 DISABLE),再次传输前需重新使能 `DMA_Cmd(DMA_Channelx, ENABLE)`;循环模式不会停止,即使重新配置也需要先 Disable 再 Enable