--- tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — DMA章节 + 配套代码28~31" author: "尚硅谷研究院" date: 2026-07-15 created: 2026-07-15 --- # DMA数据传输 > **用生活理解**:DMA 就像公司里的快递员——原来你要自己去取文件(CPU 轮询/中断读外设),DMA 就是专门配了个快递员自动帮你搬数据,搬完了喊你一声(完成中断),你就可以专心做核心工作。CPU 只负责"下单"(配置 DMA 参数),不负责"搬货"。 --- ## DMA 概述 DMA = Direct Memory Access(直接存储器访问),允许外设和存储器之间直接传输数据,**传输过程不需要 CPU 参与**。 ### 传统方式 vs DMA 方式 **轮询方式**(无 DMA): ```c for (i = 0; i < 100; i++) { while (!(USART1->SR & USART_SR_RXNE)); // CPU 一直等 buffer[i] = USART1->DR; // CPU 亲自搬 } // CPU 在此过程中无法做其他任何事 ``` **中断方式**: ```c // CPU 每次只搬 1 字节(进一次中断),搬 100 次要进 100 次中断 ``` **DMA 方式**: ```c // CPU 配置好 DMA(源地址+目的地址+长度)后,去干别的事 // DMA 控制器自动搬完 100 字节后产生中断通知 CPU // 100 次传输只进 1 次中断 ``` ### STM32F103 DMA 控制器 | 控制器 | 通道数 | 可访问的存储器和外设 | |--------|--------|-------------------| | **DMA1** | 7 通道 | SRAM、APB1/APB2 外设 | | **DMA2** | 5 通道 | SRAM、APB1/APB2 外设、FSMC(仅 F103ZET6 等大容量型号有) | ### DMA 通道映射表(DMA1) | 通道 | 外设请求 | |------|---------| | CH1 | **ADC1**、TIM2_CH3、TIM4_CH1 | | CH2 | **USART1_TX**、SPI1_RX、TIM1_CH1 | | CH3 | **USART1_RX**、SPI1_TX、TIM1_CH2 | | CH4 | SPI2_RX、SPI1_RX、**USART1_TX** | | CH5 | SPI2_TX、**USART2_TX**、TIM1_CH4 | | CH6 | **USART2_RX**、ADC3、TIM1_CH1 | | CH7 | **USART3_TX**、ADC3、TIM1_CH2 | > **注意**:同一时刻一个 DMA 通道只能服务一个外设。需要查参考手册 §10 确认当前通道哪个外设请求有效。 ### DMA 传输模式 | 模式 | 说明 | 适用场景 | |------|------|---------| | **内存→内存** | 源地址和目的地址都是内存 | Flash→SRAM 数据搬移 | | **内存→外设** | 源是内存,目标是外设数据寄存器 | 向串口发送一批数据 | | **外设→内存** | 源是外设数据寄存器,目标是内存 | 从 ADC/串口接收一批数据 | --- ## DMA 关键寄存器 | 寄存器 | 功能 | 重要位域 | |--------|------|---------| | **CCR** | 通道配置寄存器 | EN(使能)、DIR(方向)、PL[1:0](优先级)、MSIZE[1:0](存储单元宽度)、PSIZE[1:0](外设宽度)、MINC(存储地址增量)、PINC(外设地址增量)、CIRC(循环模式)、MEM2MEM(内存到内存) | | **CNDTR** | 传输数量寄存器 | 16 位(最大 65535),每次传输后自减 | | **CPAR** | 外设地址寄存器 | 外设数据寄存器的地址(如 USART1->DR = 0x40013804) | | **CMAR** | 存储器地址寄存器 | 内存缓冲区的地址 | | **ISR** | 中断状态寄存器 | TCIFx(通道x传输完成)、HTIFx(通道x半传输)、TEIFx(通道x传输错误) | | **IFCR** | 中断标志清除寄存器 | CTCIFx(写1清完成)、CHTIFx(写1清半传) | > **参考**:参考手册 §10(DMA 寄存器描述)完整位域表 ### CCR 关键位详解 | 位 | 名称 | 值 | 说明 | |----|------|-----|------| | 0 | EN | 1 | 通道使能(配置完所有参数后再置 1) | | 4 | DIR | 0=外设→内存, 1=内存→外设 | M2M=1 时此位忽略 | | 6:5 | MSIZE | 00=8位, 01=16位, 10=32位 | 存储器数据宽度 | | 7:6 | PSIZE | 00=8位, 01=16位, 10=32位 | 外设数据宽度(必须与外设寄存器宽度匹配) | | 9 | MINC | 1 | 存储器地址自动递增(增量=MSIZE 字节) | | 8 | PINC | 1 | 外设地址自动递增 | | 10 | CIRC | 1 | 循环模式(CNDTR 到 0 后自动重装,持续传输) | | 14 | MEM2MEM | 1 | 内存到内存模式(不需要外设请求,配置完即启动) | --- ## DMA 传输流程 ``` 1. 开启 DMA 时钟(AHBENR 寄存器) 2. 设置 CPAR(外设地址)和 CMAR(内存地址) 3. 设置 CNDTR(传输次数) 4. 配置 CCR(优先级、数据宽度、增量模式、传输方向) 5. 使能通道(CCR.EN = 1) 6. DMA 自动开始传输(外设请求触发或 MEM2MEM 立即启动) 7. 传输完成 → CNDTR=0 → 产生 TC 中断(如果使能)或 CIRC 重装 ``` --- ## 实验:DMA 内存到内存传输 ### 软件设计(寄存器版) **项目路径**:`stm32/28_dma_mem2mem_register` **需求**:将 SRAM 中源数组的数据通过 DMA 复制到目的数组,完成后翻转 LED。 **文件:`stm32/28_dma_mem2mem_register/Hardware/DMA/dma.h`** ```c #ifndef __DMA_H #define __DMA_H #include "stm32f10x.h" // 初始化 void DMA1_Init(void); // DMA1通道1传输数据,需要指定源地址、目的地址、数据长度 void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen); #endif ``` **文件:`stm32/28_dma_mem2mem_register/Hardware/DMA/dma.c`** ```c #include "dma.h" // 初始化 void DMA1_Init(void) { // 1. 开启时钟 RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 2. DMA参数配置 // 2.1 传输方向DIR = 0,从外设到存储器(实际是ROM到RAM,存储器到存储器) DMA1_Channel1->CCR &= ~DMA_CCR1_DIR; DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM; // 2.2 设置数据宽度,均为00 - 8位 DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE; DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE; // 2.3 外设和存储器地址增量 DMA1_Channel1->CCR |= DMA_CCR1_PINC; DMA1_Channel1->CCR |= DMA_CCR1_MINC; // 2.4 使能传输完成中断 DMA1_Channel1->CCR |= DMA_CCR1_TCIE; // 3. NVIC配置 NVIC_SetPriorityGrouping(3); NVIC_SetPriority(DMA1_Channel1_IRQn, 3); NVIC_EnableIRQ(DMA1_Channel1_IRQn); } // DMA1通道1传输数据,需要指定源地址、目的地址、数据长度 void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen) { // 1. 将目的地址写入外设地址寄存器(MEM2MEM 模式下 CPAR=目的) DMA1_Channel1->CPAR = destAddr; // 2. 将源地址写入存储器地址寄存器(MEM2MEM 模式下 CMAR=源) DMA1_Channel1->CMAR = srcAddr; // 3. 将数据长度写入 CNDTR DMA1_Channel1->CNDTR = dataLen; // 4. 使能通道,开始数据传输 DMA1_Channel1->CCR |= DMA_CCR1_EN; } extern uint8_t isOver; // 中断服务函数 void DMA1_Channel1_IRQHandler(void) { // 判断是否是传输完成中断(TCIF) if ( DMA1->ISR & DMA_ISR_TCIF1 ) { // 清除中断标志位 DMA1->IFCR |= DMA_IFCR_CTCIF1; // 关闭通道 DMA1_Channel1->CCR &= ~DMA_CCR1_EN; isOver = 1; } } ``` **文件:`stm32/28_dma_mem2mem_register/User/main.c`** ```c #include "usart.h" #include "dma.h" // 常量全局数组,存储在 ROM 区 const uint8_t src[] = {10, 20, 30, 40}; // 变量全局数组,存储在 RAM 区 uint8_t dest[4]; // 全局变量,表示数据传输完毕 uint8_t isOver; int main(void) { // 初始化 USART_Init(); DMA1_Init(); printf("Hello world!\n"); // 打印源和目的地址 printf("src = %p, dest = %p\n", src, dest); // 调用函数启动传输 DMA1_Transmit((uint32_t)src, (uint32_t)dest, 4); // 等待传输完毕,打印结果 while (!isOver) { } for (uint8_t i = 0; i < 4; i++) { printf("%d\t", dest[i]); } while (1) { } } ``` ### 软件设计(HAL 库版) **项目路径**:`stm32/29_dma_mem2mem_hal` **文件:`stm32/29_dma_mem2mem_hal/Core/Src/dma.c`**(MX_DMA_Init) ```c #include "dma.h" DMA_HandleTypeDef hdma_memtomem_dma1_channel1; void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_memtomem_dma1_channel1.Instance = DMA1_Channel1; hdma_memtomem_dma1_channel1.Init.Direction = DMA_MEMORY_TO_MEMORY; hdma_memtomem_dma1_channel1.Init.PeriphInc = DMA_PINC_ENABLE; hdma_memtomem_dma1_channel1.Init.MemInc = DMA_MINC_ENABLE; hdma_memtomem_dma1_channel1.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_memtomem_dma1_channel1.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_memtomem_dma1_channel1.Init.Mode = DMA_NORMAL; hdma_memtomem_dma1_channel1.Init.Priority = DMA_PRIORITY_LOW; if (HAL_DMA_Init(&hdma_memtomem_dma1_channel1) != HAL_OK) { Error_Handler(); } HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn); } ``` **文件:`stm32/29_dma_mem2mem_hal/Core/Src/stm32f1xx_it.c`**(DMA 中断) ```c void DMA1_Channel1_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_memtomem_dma1_channel1); } ``` **文件:`stm32/29_dma_mem2mem_hal/Core/Src/main.c`** ```c #include "main.h" #include "dma.h" #include "usart.h" #include "gpio.h" const uint8_t src[] = {10,20,30,40}; uint8_t dest[4] = {0}; uint8_t isFinished = 0; void finishedCallback(DMA_HandleTypeDef *_hdma) { printf("call back...\n"); HAL_DMA_Abort_IT(&hdma_memtomem_dma1_channel1); isFinished = 1; } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); printf("Hello, world!\n"); printf("src = %p, dest = %p\n", src, dest); HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, finishedCallback); HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src, (uint32_t)dest, 4); while (1) { if (isFinished) { for (uint8_t i = 0; i < 4; i++) { printf("%d\t", dest[i]); } isFinished = 0; } } } ``` --- ## 实验:DMA + USART 自动发送 ### 软件设计(寄存器版 — 内存到外设) **项目路径**:`stm32/30_dma_mem2usart_register` DMA + USART 使用 **DMA1 通道4**(USART1_TX 映射到此通道),配置为内存→外设方向: **文件:`stm32/30_dma_mem2usart_register/Hardware/DMA/dma.h`** ```c #ifndef __DMA_H #define __DMA_H #include "stm32f10x.h" // 初始化 void DMA1_Init(void); // DMA1通道4传输数据,需要指定源地址、目的地址、数据长度 void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen); #endif ``` **文件:`stm32/30_dma_mem2usart_register/Hardware/DMA/dma.c`** ```c #include "dma.h" // 初始化 void DMA1_Init(void) { // 1. 开启时钟 RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 2. DMA参数配置 // 2.1 传输方向DIR = 1,从存储器到外设 DMA1_Channel4->CCR |= DMA_CCR4_DIR; // 2.2 设置数据宽度,均为00 - 8位 DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE; DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE; // 2.3 存储器地址递增,外设地址不递增 DMA1_Channel4->CCR &= ~DMA_CCR4_PINC; DMA1_Channel4->CCR |= DMA_CCR4_MINC; // 2.4 使能传输完成中断 DMA1_Channel4->CCR |= DMA_CCR4_TCIE; // 2.5 使能USART发送DMA请求 USART1->CR3 |= USART_CR3_DMAT; // 循环模式 DMA1_Channel4->CCR |= DMA_CCR1_CIRC; // 3. NVIC配置 NVIC_SetPriorityGrouping(3); NVIC_SetPriority(DMA1_Channel4_IRQn, 3); NVIC_EnableIRQ(DMA1_Channel4_IRQn); } // DMA1通道4传输数据,需要指定源地址、目的地址、数据长度 void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen) { // 1. 将源地址写入存储器地址寄存器 DMA1_Channel4->CMAR = srcAddr; // 2. 将目的地址写入外设地址寄存器 DMA1_Channel4->CPAR = destAddr; // 3. 将数据长度写入 CNDTR DMA1_Channel4->CNDTR = dataLen; // 4. 使能通道,开始数据传输 DMA1_Channel4->CCR |= DMA_CCR4_EN; } // 中断服务函数 void DMA1_Channel4_IRQHandler(void) { // 判断是否是传输完成中断(TCIF) if ( DMA1->ISR & DMA_ISR_TCIF4 ) { // 清除中断标志位 DMA1->IFCR |= DMA_IFCR_CTCIF4; // 关闭通道(此处注释掉,循环模式下保持使能) // DMA1_Channel4->CCR &= ~DMA_CCR4_EN; } } ``` **文件:`stm32/30_dma_mem2usart_register/User/main.c`** ```c #include "usart.h" #include "dma.h" #include "delay.h" // 全局变量,需要发送的数据 uint8_t src[] = {'a', 'b', 'c', 'd', 'e'}; int main(void) { // 初始化 USART_Init(); DMA1_Init(); printf("Hello world!\n"); Delay_ms(1); // 调用函数启动传输 DMA1_Transmit((uint32_t)src, (uint32_t)&(USART1->DR), 5); while (1) { } } ``` ### 软件设计(HAL 库版) **项目路径**:`stm32/31_dma_mem2uart_hal` **文件:`stm32/31_dma_mem2uart_hal/Core/Src/dma.c`**(MX_DMA_Init) ```c #include "dma.h" void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn); } ``` **文件:`stm32/31_dma_mem2uart_hal/Core/Src/usart.c`**(HAL_UART_MspInit 中包含 DMA 初始化) ```c void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle) { if(uartHandle->Instance==USART1) { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_9; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_10; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // USART1_TX DMA Init DMA_HandleTypeDef hdma_usart1_tx; hdma_usart1_tx.Instance = DMA1_Channel4; 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_CIRCULAR; hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_usart1_tx); __HAL_LINKDMA(uartHandle, hdmatx, hdma_usart1_tx); } } ``` **文件:`stm32/31_dma_mem2uart_hal/Core/Src/main.c`** ```c #include "main.h" #include "dma.h" #include "usart.h" #include "gpio.h" int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); uint8_t src[] = {'a','b','c','d','e'}; HAL_UART_Transmit_DMA(&huart1, src, 5); while (1) { } } ``` **文件:`stm32/31_dma_mem2uart_hal/Core/Src/stm32f1xx_it.c`**(DMA 中断) ```c void DMA1_Channel4_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart1_tx); } ``` --- ## HAL 库版 DMA 传输 HAL 库提供了简化的 DMA API,使用 `HAL_DMA_Start_IT()` 和 `HAL_UART_Transmit_DMA()` 等函数封装寄存器操作: ```c // HAL 库 DMA 内存到内存传输(通过注册完成回调) DMA_HandleTypeDef hdma_memtomem_dma1_channel1; void MX_DMA_Init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_memtomem_dma1_channel1.Instance = DMA1_Channel1; hdma_memtomem_dma1_channel1.Init.Direction = DMA_MEMORY_TO_MEMORY; hdma_memtomem_dma1_channel1.Init.PeriphInc = DMA_PINC_ENABLE; hdma_memtomem_dma1_channel1.Init.MemInc = DMA_MINC_ENABLE; hdma_memtomem_dma1_channel1.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_memtomem_dma1_channel1.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_memtomem_dma1_channel1.Init.Mode = DMA_NORMAL; hdma_memtomem_dma1_channel1.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_memtomem_dma1_channel1); } // 注册传输完成回调 + 启动传输 HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, MyCallback); HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src, (uint32_t)dest, 4); // HAL 库 DMA + USART 发送(一步到位) HAL_UART_Transmit_DMA(&huart1, tx_buffer, size); // 参数: uart句柄, 数据指针, 数据长度 // 异步: 函数立即返回,传输在后台进行 // HAL 库 DMA 接收 HAL_UART_Receive_DMA(&huart1, rx_buffer, size); // 完成时回调: HAL_UART_RxCpltCallback(&huart1) // DMA + USART 需在 HAL_UART_MspInit 中配置 DMA 通道 // 关键步骤: __HAL_LINKDMA(uartHandle, hdmatx, hdma_usart1_tx); ``` --- ## DMA 中断优先级注意事项 DMA 中断默认优先级较低,但具有硬件优先级: | DMA 优先级设置(CCR.PL) | 说明 | |------------------------|------| | 00 (最低) | 次于所有外设中断 | | 01 (低) | 与低优先级外设中断同级 | | 10 (高) | 与高优先级外设中断同级 | | 11 (最高) | 优于外设中断 | 当多个 DMA 通道同时请求时,优先级高的先执行;同级时通道号小的优先。 --- ## 核心函数速查表 | 寄存器操作 | 代码 | 说明 | |-----------|------|------| | 时钟使能 | `RCC->AHBENR |= RCC_AHBENR_DMA1EN` | 开启 DMA1 时钟 | | 清除标志 | `DMA1->IFCR |= DMA_IFCR_CGIF1` | 清除通道1所有标志 | | 设外设地址 | `DMA1_Channel1->CPAR = (uint32_t)&USART1->DR` | 外设数据寄存器地址 | | 设内存地址 | `DMA1_Channel1->CMAR = (uint32_t)buffer` | 内存缓冲区地址 | | 设传输次数 | `DMA1_Channel1->CNDTR = size` | 16 位,传输次数 | | 使能通道 | `DMA1_Channel1->CCR |= DMA_CCR1_EN` | 最后一步才置 EN | | 查询完成 | `DMA1->ISR & DMA_ISR_TCIF1` | TCIF=1 表示完成 | | 清完成标志 | `DMA1->IFCR |= DMA_IFCR_CTCIF1` | 写 1 清除 | ## 常见问题与避坑 1. **DMA 传输没开始** → 检查 CCR.EN 是否使能、CNDTR≠0、外设是否已发出 DMA 请求(如 USART 的 CR3.DMAT=1) 2. **传输数据错了** → 检查 MSIZE/PSIZE 是否与外设匹配(8/16/32 位)、地址增量方向是否正确 3. **中断只进一次** → 检查 ISR.GIF 是否清除、NVIC 是否使能、CCR.TCIE 是否置 1 4. **DMA 和 Cache 一致性** → STM32F103 无 Cache,不必担心(Cortex-M7 需注意) 5. **CNDTR 初始值 = 最大 65535** → 如果传输超过 65535 次,需分段传输或使用循环模式 6. **MEM2MEM 传输方向** → "外设地址寄存器"放源地址,"存储器地址寄存器"放目的地址(命名反直觉但实际如此) 7. **DMA 通道冲突** → 同一通道同一时间只能服务一个外设请求