DMA 直接内存访问
理解 DMA 通道仲裁与三种传输方向,掌握双缓冲技术,实现 UART/ADC/SPI 的零拷贝高性能数据传输。
理解 DMA 通道仲裁与三种传输方向,掌握双缓冲技术,实现 UART/ADC/SPI 的零拷贝高性能数据传输。
/** * DMA2 M→M 快速内存复制(比 memcpy 更高效,CPU 可同时运行) * 注意:仅 DMA2 支持 M2M,且两端地址必须是 4 字节对齐(word传输时) */ DMA_HandleTypeDef hdma_m2m; HAL_StatusTypeDef DMA_MemCopy(uint32_t *dst, uint32_t *src, uint32_t words) { hdma_m2m.Instance = DMA2_Stream0; hdma_m2m.Init.Channel = DMA_CHANNEL_0; hdma_m2m.Init.Direction = DMA_MEMORY_TO_MEMORY; hdma_m2m.Init.PeriphInc = DMA_PINC_ENABLE; /* 源地址自增 */ hdma_m2m.Init.MemInc = DMA_MINC_ENABLE; /* 目标地址自增 */ hdma_m2m.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma_m2m.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma_m2m.Init.Mode = DMA_NORMAL; hdma_m2m.Init.Priority = DMA_PRIORITY_HIGH; hdma_m2m.Init.FIFOMode = DMA_FIFOMODE_ENABLE; hdma_m2m.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL; hdma_m2m.Init.MemBurst = DMA_MBURST_INC4; hdma_m2m.Init.PeriphBurst = DMA_PBURST_INC4; HAL_DMA_Init(&hdma_m2m); return HAL_DMA_Start_IT(&hdma_m2m, (uint32_t)src, /* 源地址 */ (uint32_t)dst, /* 目标地址 */ words); /* 传输 N 个 word */ }
/* UART1 DMA 发送:CPU 只提交请求,DMA 搬运数据,TX 完成后回调 */ volatile uint8_t uart_tx_busy = 0; void UART_DMA_Send(uint8_t *data, uint16_t len) { while (uart_tx_busy); /* 等待上次发送完成(可改为超时) */ uart_tx_busy = 1; HAL_UART_Transmit_DMA(&huart1, data, len); } /* 发送完成回调,清除忙标志 */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { uart_tx_busy = 0; } } /* 使用示例:发送 JSON 字符串 */ static uint8_t tx_buf[128]; void Report_Sensor(float temp, float humi) { int len = snprintf((char*)tx_buf, sizeof(tx_buf), "{\"t\":%.1f,\"h\":%.1f}\r\n", temp, humi); UART_DMA_Send(tx_buf, len); /* 函数立即返回,DMA 在后台发送,CPU 可以干别的 */ }
/** * SPI1 DMA 全双工收发(同时 TX 和 RX) * 适合高速读取 Flash/ADC,提高总线利用率 */ uint8_t spi_tx_buf[256]; uint8_t spi_rx_buf[256]; void SPI_DMA_Transfer(uint8_t *tx, uint8_t *rx, uint16_t len) { W25Q_CS_LOW(); /* 同时启动 TX DMA 和 RX DMA */ HAL_SPI_TransmitReceive_DMA(&hspi1, tx, rx, len); /* DMA 完成后触发 HAL_SPI_TxRxCpltCallback */ } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { W25Q_CS_HIGH(); /* 处理 spi_rx_buf 中的数据 */ } }
#define BUF_SIZE 512 uint16_t adc_buf0[BUF_SIZE]; /* 双缓冲 */ uint16_t adc_buf1[BUF_SIZE]; volatile uint8_t buf_ready = 0; /* 0=buf0就绪,1=buf1就绪 */ void ADC_DoubleBuffer_Init(void) { /* 启动 ADC DMA,使用 HAL 双缓冲(注:需要直接操作 DMA 寄存器) */ HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf0, BUF_SIZE * 2); /* 使能半传输中断 */ __HAL_DMA_ENABLE_IT(hadc1.DMA_Handle, DMA_IT_HT); } /* 半传输中断:DMA 已填充 buf0(前半),CPU 开始处理 buf0 */ void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { buf_ready = 0; /* buf0 就绪 */ } /* 传输完成中断:DMA 已填充 buf1(后半),CPU 开始处理 buf1 */ void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { buf_ready = 1; /* buf1 就绪 */ } void main_process(void) { while (1) { uint16_t *process_buf = (buf_ready == 0) ? adc_buf0 : adc_buf1; /* 在 DMA 填充另一半期间,处理当前这半 */ Process_ADC_Data(process_buf, BUF_SIZE); } }
DMA FIFO 是 DMA 内部的缓冲,可以将多个小传输合并成一次 Burst 传输到 AHB 总线,减少总线仲裁次数,提高带宽利用率。FIFO 阈值有 1/4、1/2、3/4、Full 四种,阈值越大,延迟越高但效率越好。Burst 大小(MBURST/PBURST)必须与 FIFO 阈值匹配,否则会产生 FIFO 错误(FEIF)。若 FIFO 模式关闭(直接模式),每次外设请求触发一次单次传输。
① 使用局部变量作为 DMA 缓冲:局部变量在栈上,函数返回后被回收,DMA 继续写入会破坏栈数据,导致极难定位的 HardFault。DMA 缓冲必须声明为全局或 static 变量。
② Stream/Channel 选错:必须查 Reference Manual 的 DMA Request Mapping 表,用错了 DMA 请求会导致外设触发时 DMA 无响应。
③ 缓冲区地址未对齐:字传输(Word)要求 4 字节对齐,半字(Half-Word)要求 2 字节对齐,否则触发总线错误。
DMA 传输可能因总线错误、FIFO 溢出等原因失败,需要注册错误回调处理:
/* DMA 传输错误回调 */ void HAL_DMA_ErrorCallback(DMA_HandleTypeDef *hdma) { if (hdma->Instance == DMA2_Stream0) { uint32_t err = hdma->ErrorCode; if (err & HAL_DMA_ERROR_FE) HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14); /* FIFO 错误 */ if (err & HAL_DMA_ERROR_DME) HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_15); /* 直接模式错误 */ if (err & HAL_DMA_ERROR_TE) HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13); /* 传输错误 */ /* 重置 DMA 并重启 */ HAL_DMA_DeInit(hdma); HAL_DMA_Init(hdma); /* 根据场景重新启动传输 */ } } /* FIFO 错误(FEIF)最常见原因和解决方案 */ /* 原因:Burst 大小与 FIFO 阈值不匹配 * 规则:FIFO 阈值 × 数据宽度 必须能被 Burst 大小整除 * * 例:FIFO_THRESHOLD = FULL(4字), MemDataAlignment = WORD(4字节) * PeriphBurst = INC4 → FIFO 需要 4×4=16 字节 = 4个 Word * FIFO_THRESHOLD_FULL = 4 Word → 恰好匹配 ✓ * * 简单方案:出现 FEIF 时,禁用 Burst(设为 SINGLE)和 FIFO(直接模式)*/ hdma.Init.FIFOMode = DMA_FIFOMODE_DISABLE; /* 关闭 FIFO,使用直接模式 */ hdma.Init.MemBurst = DMA_MBURST_SINGLE; /* 单次传输 */ hdma.Init.PeriphBurst = DMA_PBURST_SINGLE;
在 STM32H7(Cortex-M7,有 D-Cache)上使用 DMA 时,必须处理 Cache 与 DMA 之间的数据一致性:
/* STM32H7:DMA 写入缓冲区后,CPU 读前必须使 Cache 失效 */ static uint8_t dma_rx_buf[256] __attribute__((aligned(32))); /* 32字节 Cache 行对齐 */ void DMA_ReceiveComplete(void) { /* DMA 传输完成后,使 CPU D-Cache 中对应区域失效 * 确保 CPU 读到的是 DMA 写入的最新数据,而非 Cache 中的旧值 */ SCB_InvalidateDCache_by_Addr((uint32_t*)dma_rx_buf, sizeof(dma_rx_buf)); /* 现在可以安全读取 dma_rx_buf */ Process_Data(dma_rx_buf, sizeof(dma_rx_buf)); } void DMA_PrepareTransmit(void) { /* CPU 写数据到 tx 缓冲区后,DMA 读前必须刷 Cache */ static uint8_t dma_tx_buf[256] __attribute__((aligned(32))); memcpy(dma_tx_buf, source_data, sizeof(dma_tx_buf)); /* 刷 Cache:将 CPU Cache 中的数据写回 RAM,让 DMA 读到最新值 */ SCB_CleanDCache_by_Addr((uint32_t*)dma_tx_buf, sizeof(dma_tx_buf)); /* 现在可以安全启动 DMA TX */ }
DMA 让外设与内存直接交换数据,CPU 无需介入搬运,可以并行做其他工作。STM32F4 有 2 个 DMA 控制器,各 8 个 Stream,每个 Stream 有独立的优先级和 FIFO。三种传输方向:P→M(ADC/UART 接收)、M→P(UART 发送/DAC 输出)、M→M(内存复制,仅 DMA2)。循环模式 + 半传输中断实现双缓冲流水线处理。DMA 缓冲必须声明为全局/static 变量(不能是局部变量)。FEIF 错误通常由 Burst/FIFO 参数不匹配引起,解决办法是禁用 Burst 和 FIFO。STM32H7 有 D-Cache,DMA 传输前后需要 SCB_CleanDCache/SCB_InvalidateDCache 维护一致性。