直接存储器访问-DMA

📅 发布时间:2026/7/11 8:11:05
直接存储器访问-DMA DMADirect Memory Access直接存储器访问是一种允许外设与内存、或内存与内存之间直接进行数据传输的硬件机制整个过程无需CPU逐字节参与主要解决了CPU被大量数据搬运工作占用的问题。在没有DMA时CPU需要全程参与数据的转移效率低下。有了DMACPU只需在传输开始前进行配置传输过程中可以处理其他任务传输完成后通过中断获知结果。QDMA传输有哪几种方向外设到内存 (P2M)如UART接收、ADC采样内存到外设 (M2P)如UART发送、DAC输出内存到内存 (M2M)内存拷贝DMA vs. 中断 vs. 轮询对比数据传输方式工作机制CPU参与度适用场景轮询 (Polling)CPU不断检查外设状态标志等待数据就绪全程占用CPU空转等待数据量小、实时性要求不高的简单场景中断 (Interrupt)外设每准备好一个数据或一组数据就发送中断通知CPU来读取频繁打断每次中断都有现场保存与恢复的开销数据量中等需要CPU及时响应的场景DMADMA控制器硬件自动搬运数据搬运完成后才通过一次中断通知CPU几乎不参与仅在开始和结束时介入数据量大、高速、连续的传输如串口大批量收发、ADC连续采样、屏幕刷新 场景实战一个UART发送的例子下面是一个将内存中data数组通过串口1发送出去的完整配置示例#includestm32f4xx_hal.h// 1. 定义DMA句柄DMA_HandleTypeDef hdma_uart_tx;voidMX_DMA_Init(void){// 2. 使能DMA时钟__HAL_RCC_DMA1_CLK_ENABLE();// 3. 配置DMA通道参数hdma_uart_tx.InstanceDMA1_Channel4;// USART1_TX对应DMA1通道4hdma_uart_tx.Init.DirectionDMA_MEMORY_TO_PERIPH;// 传输方向内存 - 外设hdma_uart_tx.Init.PeriphIncDMA_PINC_DISABLE;// 外设地址不递增固定为DR寄存器hdma_uart_tx.Init.MemIncDMA_MINC_ENABLE;// 内存地址递增数组连续hdma_uart_tx.Init.PeriphDataAlignmentDMA_PDATAALIGN_BYTE;// 外设数据宽度8位hdma_uart_tx.Init.MemDataAlignmentDMA_MDATAALIGN_BYTE;// 内存数据宽度8位hdma_uart_tx.Init.ModeDMA_NORMAL;// 传输模式单次hdma_uart_tx.Init.PriorityDMA_PRIORITY_MEDIUM;// 优先级中等// 4. 调用初始化函数HAL_DMA_Init(hdma_uart_tx);// 5. 将DMA句柄与UART句柄绑定 (假设已有UART句柄 huart1)__HAL_LINKDMA(huart1,hdmatx,hdma_uart_tx);}之后在需要发送数据时只需调用uint8_t data[] Hello DMA!; HAL_UART_Transmit_DMA(huart1, data, sizeof(data));DMA驱动LCD显示屏SPI/QSPI 接口 LCD最常见如ILI9341、ST7789:外设 (Peripheral)SPI外设或QSPI外设的发送数据寄存器TXDR。DMA将内存中的帧缓冲区Frame Buffer数据源源不断地搬运到SPI的TXDR中。SPI外设再通过MOSI线将数据串行发送给屏幕。传输模式 (Mode)单次模式Normal用于显示一张静态图片。DMA搬运完一帧数据后自动停止触发完成中断。循环模式Circular/ 双缓冲Double Buffer用于播放视频或动态GUI。DMA不断循环搬运缓冲区数据实现连续帧刷新。触发源 (Trigger Source)SPI硬件请求。当SPI的发送FIFO先入先出队列有空位时会自动向DMA控制器发出传输请求。注意通常不需要额外定时器触发SPI的时钟速率决定了像素传输速度。CPU收益 (CPU Gain)无DMA时刷新一张320x240 RGB565图片CPU需循环发送320*240*2 153,600个字节。在此期间CPU被完全占用无法响应按键或传感器中断。有DMA时CPU只需配置好源地址、目标地址和数据长度然后调用一次启动函数耗时不到1微秒。之后CPU可以全速运行GUI逻辑如LVGL绘图、解码JPEG或处理Wi-Fi数据包数据传输完全由硬件后台完成。SPI DMA驱动 LCD 实现双缓冲实例下面提供一个基于STM32 HAL 库、使用SPI DMA驱动 LCD 并实现双缓冲的完整 C 语言代码框架。核心机制是乒乓操作DMA 传输前台缓冲区时CPU 绘制后台缓冲区通过传输完成中断实现无缝切换。⚙️ 1. 核心数据结构与缓冲区定义.h#includestm32f4xx_hal.h// 以STM32F4系列为例// 假设屏幕分辨率为 240x320RGB565 格式每帧字节数 240 * 320 * 2#defineLCD_WIDTH240#defineLCD_HEIGHT320#defineLCD_FRAME_SIZE(LCD_WIDTH*LCD_HEIGHT*2)// 153600 字节// 定义两个独立的显示缓冲区 (注意内存对齐)__attribute__((aligned(32)))uint8_tbuffer_a[LCD_FRAME_SIZE];__attribute__((aligned(32)))uint8_tbuffer_b[LCD_FRAME_SIZE];// 定义DMA传输句柄DMA_HandleTypeDef hdma_spi_tx;// 双缓冲控制变量uint8_t*dma_active_bufferbuffer_a;// DMA当前正在发送的缓冲区 (前台)uint8_t*cpu_draw_bufferbuffer_b;// CPU当前可以绘制的缓冲区 (后台)volatileuint8_tdma_transfer_complete0;// DMA传输完成标志__attribute__((aligned(32)))确保缓冲区32 字节对齐这对于带有 Cache 的 Cortex-M7 等内核至关重要可以避免数据一致性问题。volatile修饰dma_transfer_complete防止编译器优化确保主循环能及时感知中断中对它的修改。 2. SPI DMA 初始化 (CubeMX生成)强烈建议使用 STM32CubeMX 进行图形化配置它会自动生成大部分初始化代码。关键配置如下SPI配置为主模式、仅发送、硬件 NSS 管理、数据宽度建议 16 位若 LCD 支持。DMA为 SPI 的 TX 通道添加 DMA设置方向为Memory to Peripheral模式为Normal由软件循环启动数据宽度与 SPI 一致优先级设为High。NVIC使能 DMA 的传输完成中断。✍️ 3. 核心代码实现3.1 DMA 传输完成中断服务函数 (ISR)这是实现“乒乓”切换的核心。注意 ISR 必须极短只做标志位操作和缓冲区切换。voidDMA2_Stream3_IRQHandler(void)// 中断向量名称请查阅启动文件{// 检查并清除传输完成中断标志if(__HAL_DMA_GET_FLAG(hdma_spi_tx,DMA_FLAG_TCIF3_4)){__HAL_DMA_CLEAR_FLAG(hdma_spi_tx,DMA_FLAG_TCIF3_4);HAL_DMA_IRQHandler(hdma_spi_tx);// 调用HAL库公共处理函数// --- 双缓冲切换核心逻辑 (乒乓操作) ---// 1. 交换指针刚发完的变成后台另一个变成前台uint8_t*tempdma_active_buffer;dma_active_buffercpu_draw_buffer;cpu_draw_buffertemp;// 2. 置位完成标志通知主循环一帧已发完dma_transfer_complete1;}}3.2 启动一帧 DMA 传输的函数这个函数负责配置并启动下一次 DMA 传输。voidLCD_Start_DMA_Transfer(uint8_t*buffer,uint32_tsize){// 1. 停止当前可能正在进行的DMA传输 (安全操作)HAL_DMA_Abort(hdma_spi_tx);// 2. 配置DMA (关键步骤设置源地址、目标地址、数据长度)// 注意此操作应在中断外、且DMA被禁用时进行__HAL_UNLOCK(hdma_spi_tx);// 解锁DMA句柄hdma_spi_tx.Instance-CR~DMA_SxCR_EN;// 禁用DMA流// 设置源地址 (内存缓冲区)hdma_spi_tx.Instance-M0AR(uint32_t)buffer;// 设置目标地址 (SPI的发送数据寄存器)hdma_spi_tx.Instance-PAR(uint32_t)(SPI1-DR);// 设置传输数据量 (字节数)hdma_spi_tx.Instance-NDTRsize;// 3. 重新使能DMA流启动传输hdma_spi_tx.Instance-CR|DMA_SxCR_EN;}3.3 主循环逻辑主循环负责响应中断启动下一次传输。intmain(void){HAL_Init();// ... 系统时钟、GPIO、SPI、DMA等初始化 (由CubeMX生成) ...// 初始化将 buffer_a 设为前台发送buffer_b 设为后台绘制dma_active_bufferbuffer_a;cpu_draw_bufferbuffer_b;dma_transfer_complete0;// 启动第一次DMA传输发送 buffer_aLCD_Start_DMA_Transfer(dma_active_buffer,LCD_FRAME_SIZE);while(1){// --- CPU在后台绘制下一帧到 cpu_draw_buffer ---// 注意绘制期间cpu_draw_buffer 不会被DMA访问是安全的Fill_Random_Color(cpu_draw_buffer);// 示例绘制函数// --- 等待当前帧DMA传输完成 ---while(!dma_transfer_complete){// 可在此处执行低优先级任务}// 消费掉完成标志准备下一次等待dma_transfer_complete0;// --- 启动下一帧的DMA传输 ---// 此时 dma_active_buffer 已在中断中被交换为刚绘制好的缓冲区LCD_Start_DMA_Transfer(dma_active_buffer,LCD_FRAME_SIZE);}}⚠️ 4. 注意事项与增强建议DMA 单次传输长度限制部分 DMA 控制器的NDTR寄存器为 16 位最大值为65535。若一帧数据超过此值如153600字节需将一帧拆分为多次 DMA 传输。可增加一个transfer_chunk_index变量来管理。Cache 一致性维护若使用带 Cache 的 MCU如 Cortex-M7在启动 DMA 前需调用SCB_CleanDCache_by_Addr()将cpu_draw_buffer数据从 Cache 刷到物理内存在 DMA 完成后CPU 读取dma_active_buffer前需调用SCB_InvalidateDCache_by_Addr()使 Cache 失效。中断中的原子操作在中断服务函数ISR中交换指针是原子操作是安全的。但在主循环中若需要操作这些指针必须关闭全局中断或使用互斥量保护防止被中断打断导致指针错乱。MCU 特定 DMA 特性对于 STM32F7/H7 等更高性能的 MCU可以使用DMA2D (Chrom-ART Accelerator)这个专用的图形 DMA它支持硬件像素格式转换和图层混合能进一步解放 CPU。LVGL 等 GUI 库集成若使用 LVGL可将此双缓冲机制集成到lv_disp_drv_t的flush_cb回调中。LVGL 也原生支持双缓冲配置。 5. 总结上述代码提供了一个完整的、可直接移植的 DMA 双缓冲 LCD 驱动框架。其核心逻辑可总结为下表组件职责关键代码位置前台缓冲区 (dma_active_buffer)DMA 正在读取并发送给 LCD 的数据源LCD_Start_DMA_Transfer后台缓冲区 (cpu_draw_buffer)CPU 正在绘制下一帧的目标区域main函数中的绘制逻辑传输完成中断 (ISR)交换前后台缓冲区指针通知主循环DMA2_Stream3_IRQHandler主循环等待中断信号启动下一轮 DMA 传输main函数的while(1)循环