Chapter 08 · ARM 嵌入式 C 开发
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 字节对齐,否则触发总线错误。