嵌入式DMA驱动开发实战:从原理到串口/SPI/ADC配置与性能优化

发布时间:2026/10/27 1:04:31
嵌入式DMA驱动开发实战:从原理到串口/SPI/ADC配置与性能优化 1. 为什么DMA是嵌入式驱动开发的分水岭搞嵌入式的人都有一个共同的感受GPIO点灯、串口轮询收发、定时器中断这些外设入门操作学起来很快一两天就能跑通。但到了DMA这一关很多人会卡住。不是因为它有多难而是因为它涉及的知识面突然变宽了——总线矩阵、AHB/APB协议、Cache一致性、描述符链表、中断与轮询的取舍这些东西搅在一起如果没有一个清晰的认知框架很容易变成“照着例程改改能跑就行”的状态。我自己带过不少新人发现一个规律能不能把DMA用明白基本可以作为区分“会写代码”和“懂驱动”的一条线。轮询和中断谁都会写但DMA用得好不好直接决定了你的系统在高吞吐场景下是稳定运行还是频繁丢包。这一期就围绕DMA这个主题把我在实际项目中积累的经验完整梳理一遍。从DMA的底层工作机制到不同外设串口、SPI、ADC的DMA配置要点再到测速验证、常见问题排查都会涉及。不管你是刚接触DMA的新手还是用过但总觉得不够透彻的老手应该都能从中找到有用的东西。注意本文涉及的代码示例以STM32系列为主但DMA的核心原理和设计思路是通用的换到其他MCU平台同样适用。2. DMA核心原理拆解它到底在替CPU做什么2.1 没有DMA的世界是什么样的先想象一个场景。你的串口配置为115200波特率每收到一个字节就触发一次中断CPU在中断服务函数里把这个字节读出来存到缓冲区。115200波特率下每秒最多接收11520个字节也就是大约每87微秒触发一次中断。每次中断的进出开销加上数据处理保守估计也要几微秒。这意味着CPU有将近10%的时间花在搬运数据上而且这还只是一个串口。如果你同时开了串口、SPI、ADC三个外设都在收数据CPU的负担就更重了。更关键的是这些中断的优先级如果配置不当还会互相抢占导致某些数据来不及处理就丢了。DMADirect Memory Access直接存储器访问要解决的就是这个问题让数据搬运这件事不再经过CPU。DMA控制器本质上是一个独立的小型协处理器它有自己的地址总线和数据总线接口可以在外设寄存器和内存之间直接搬数据CPU只需要在开始的时候告诉它“从哪搬到哪、搬多少”然后就可以去干别的事了。2.2 DMA控制器的内部结构以STM32的DMA控制器为例一个DMA控制器通常包含以下几个关键部分通道Channel或流Stream每个通道对应一组外设请求线比如STM32F103的DMA1有7个通道每个通道固定映射到特定的外设。而STM32F4的DMA有8个流每个流可以灵活选择通道灵活性更高。源地址寄存器和目标地址寄存器分别存放数据的来源地址和目的地址。这两个地址在每次传输后可以自动递增也可以保持不变。传输计数器NDTR记录还剩多少个数据单元需要传输每传一个就减一减到零时产生传输完成事件。控制寄存器配置传输方向、数据宽度、优先级、循环模式、中断使能等参数。数据流向大致是这样的外设产生DMA请求信号DMA控制器收到请求后根据配置好的源地址和目的地址通过总线矩阵直接完成一次数据读写。整个过程CPU不参与也不需要CPU的指令周期。2.3 总线矩阵与仲裁机制这里有一个容易被忽略但非常重要的细节DMA和CPU共享总线资源。当DMA在搬运数据时它需要占用总线带宽。如果CPU同时也要访问内存两者就会产生竞争。STM32的解决方案是通过总线矩阵Bus Matrix和仲裁器来协调。仲裁器通常采用轮询或固定优先级策略。在STM32中DMA通道可以配置为最高优先级、高优先级、中等优先级、低优先级四档。当多个DMA通道同时有请求时优先级高的先获得总线使用权。如果优先级相同则通道编号小的优先。这个机制的实际意义在于如果你的系统里有多个DMA通道同时工作一定要根据数据的重要性和实时性要求合理分配优先级。比如串口接收的优先级通常要高于SPI发送因为串口数据来得快、缓冲区小不及时搬走就会溢出。2.4 数据宽度与对齐问题DMA传输时源端和目的端的数据宽度可以不同。比如源端是8位串口数据寄存器目的端是32位内存数组DMA控制器会自动进行数据打包。但这里有一个坑如果地址没有对齐可能会出现数据错位。举个例子假设你配置源端8位、目的端16位目的地址是0x20000001奇数地址那么DMA在写入时可能会把两个8位数据拼成一个16位数据写到0x20000000和0x20000002导致你读到的数据完全乱了。所以目的地址一定要按数据宽度对齐16位数据要2字节对齐32位数据要4字节对齐。实操心得在定义DMA缓冲区时直接用__attribute__((aligned(4)))或者__align(4)来强制对齐省得后面调试时抓瞎。3. 串口DMA收发配置从寄存器到代码的完整实现3.1 串口DMA发送的配置流程串口DMA发送是最常用的场景之一。配置流程可以概括为以下几步使能DMA时钟和串口时钟在RCC寄存器中打开对应的时钟位。配置DMA通道设置外设地址为串口数据寄存器地址内存地址为发送缓冲区地址传输方向为内存到外设。配置串口使能串口的DMA发送请求位USART_CR3中的DMAT位。启动传输使能DMA通道数据就会自动从内存搬到串口发送寄存器。用标准库的代码大致长这样#define TX_BUFFER_SIZE 128 uint8_t txBuffer[TX_BUFFER_SIZE] __attribute__((aligned(4))); void USART1_DMA_TX_Init(void) { DMA_InitTypeDef DMA_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); DMA_DeInit(DMA1_Channel4); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)txBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize TX_BUFFER_SIZE; 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); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); USART_Cmd(USART1, ENABLE); } void USART1_DMA_Send(uint8_t *data, uint16_t len) { while (DMA_GetCmdStatus(DMA1_Channel4) ! DISABLE); DMA_Cmd(DMA1_Channel4, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, len); DMA1_Channel4-CMAR (uint32_t)data; DMA_Cmd(DMA1_Channel4, ENABLE); }这段代码里有一个关键点每次发送前要检查DMA通道是否还在忙。如果上一次传输还没完成就重新配置会导致数据错乱。上面的while循环就是在等上一次传输结束实际项目中更推荐用中断或者标志位来判断避免死等。3.2 串口DMA接收的两种方案串口DMA接收比发送要复杂一些因为接收的数据长度是不确定的。常见的有两种方案方案一DMA 空闲中断这是最经典的方案。DMA负责把串口收到的数据搬到缓冲区空闲中断负责检测一帧数据是否接收完毕。串口在空闲状态没有数据接收时会触发空闲中断此时读取DMA的剩余传输计数就能算出实际接收了多少字节。#define RX_BUFFER_SIZE 256 uint8_t rxBuffer[RX_BUFFER_SIZE] __attribute__((aligned(4))); volatile uint16_t rxLen 0; volatile uint8_t rxComplete 0; void USART1_DMA_RX_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rxBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_BUFFER_SIZE; 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_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, DMA_InitStructure); DMA_Cmd(DMA1_Channel5, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); NVIC_EnableIRQ(USART1_IRQn); } void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { volatile uint32_t tmp USART1-SR; tmp USART1-DR; (void)tmp; rxLen RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); rxComplete 1; DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUFFER_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }这里用了循环模式DMA缓冲区满了之后会自动从头开始覆盖。配合空闲中断每次收到一帧数据就读取当前写到了哪里然后重置计数器。这种方案的好处是不需要预先知道数据长度适合变长协议。方案二DMA 定时器超时有些场景下空闲中断不够可靠比如数据流非常密集帧与帧之间没有明显的空闲间隔。这时候可以用一个定时器来检测超时每收到一个字节就重置定时器如果定时器超时了说明一帧结束。这种方案更灵活但需要额外占用一个定时器资源而且超时时间需要根据实际数据速率来调整。3.3 串口DMA配置中的关键参数选择参数推荐值选择理由DMA模式发送用Normal接收用Circular发送通常是一次性的接收需要持续监听优先级接收High发送Medium接收不及时会丢数据发送慢一点没关系数据宽度8位串口数据寄存器就是8位的内存递增Enable缓冲区需要逐个字节写入外设递增Disable串口数据寄存器地址固定注意事项串口DMA接收的缓冲区大小要留足余量。我见过一个项目缓冲区只开了64字节结果上位机一次发了200字节直接溢出丢数据。建议至少开到256字节内存紧张的话也要保证能容纳最大帧长的两倍。4. SPI DMA与ADC DMA不同外设的配置差异4.1 SPI DMA的全双工配置SPI和串口最大的区别在于它是全双工的发送和接收同时进行。用DMA的时候需要同时配置发送DMA和接收DMA两个通道。以STM32F103的SPI1为例TX用DMA1_Channel3RX用DMA1_Channel2。配置时要注意发送DMA的方向是内存到外设接收DMA的方向是外设到内存。两个通道的传输计数器要设置成相同的值保证收发同步。SPI的DMA请求使能位在SPI_CR2寄存器中分别是TXDMAEN和RXDMAEN。void SPI1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)spiTxBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize SPI_BUFFER_SIZE; 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_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, DMA_InitStructure); DMA_DeInit(DMA1_Channel2); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)spiRxBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize SPI_BUFFER_SIZE; DMA_InitStructure.DMA_Priority DMA_Priority_VeryHigh; DMA_Init(DMA1_Channel2, DMA_InitStructure); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx | SPI_I2S_DMAReq_Rx, ENABLE); }SPI DMA最常见的应用场景是驱动LCD屏幕和读取外部Flash。驱动LCD时DMA把显存数据搬到SPI数据寄存器CPU可以同时处理其他任务。读取Flash时DMA把Flash返回的数据搬到内存速度比轮询快得多。4.2 ADC多通道DMA扫描ADC的DMA用法又不太一样。ADC通常需要扫描多个通道把转换结果依次存入一个数组。这时候DMA的工作模式要配置为循环模式让ADC持续转换、DMA持续搬运。#define ADC_CHANNELS 4 uint16_t adcValues[ADC_CHANNELS] __attribute__((aligned(4))); void ADC1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adcValues; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_CHANNELS; 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_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_CHANNELS; 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); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }这里有一个细节ADC的数据寄存器是16位的所以DMA的外设数据宽度和内存数据宽度都要配置为HalfWord16位。如果配成Byte读出来的数据会错位。实操心得ADC DMA循环模式下adcValues数组会被DMA不断覆盖。如果你在主循环里读这个数组可能会读到“半新半旧”的数据。解决办法是再开一个缓冲区在DMA传输完成中断里把数据拷贝过去或者用双缓冲模式。4.3 安富莱AD7606的DMA应用安富莱的AD7606模块是一个8通道16位ADC支持并行和串行两种接口。用SPI接口时DMA的配置和普通SPI DMA类似但有几个特殊点AD7606的转换结果是通过SPI连续读出的一次要读8个通道共16字节。转换启动信号CONVST和忙信号BUSY需要配合使用通常用定时器产生CONVST脉冲用外部中断检测BUSY下降沿。DMA传输完成后需要在中断里处理数据比如做校准和滤波。这个模块在电力监测和振动分析中用得比较多对采样率的稳定性要求很高。用DMA可以保证数据搬运不占用CPU时间让CPU专注于数据处理。5. DMA测速与性能验证怎么知道DMA真的在干活5.1 用GPIO翻转法测DMA吞吐量DMA配置好了怎么验证它真的在工作最直接的方法是用GPIO翻转来测时间。具体做法是在DMA传输开始前把一个GPIO拉高在DMA传输完成中断里把同一个GPIO拉低。然后用示波器或者逻辑分析仪测量高电平的持续时间就能算出DMA搬运指定数据量需要多长时间。void DMA1_Channel4_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC4) ! RESET) { GPIO_ResetBits(GPIOA, GPIO_Pin_0); DMA_ClearITPendingBit(DMA1_IT_TC4); } } void Start_DMA_Transfer(void) { GPIO_SetBits(GPIOA, GPIO_Pin_0); DMA_Cmd(DMA1_Channel4, ENABLE); }假设你传输1024字节测到高电平持续了100微秒那么DMA的吞吐量就是1024/100e-6 10.24 MB/s。这个数据可以用来评估DMA是否达到了预期性能也可以用来对比不同配置下的差异。5.2 用内存到内存模式测DMA极限速度DMA支持内存到内存的传输模式M2M这个模式不依赖外设可以用来测DMA控制器的极限吞吐量。配置M2M模式时源地址和目的地址都是内存地址传输方向设为内存到内存。传输完成后比较源缓冲区和目的缓冲区的内容是否一致就能验证DMA传输的正确性。#define M2M_SIZE 4096 uint8_t srcBuffer[M2M_SIZE] __attribute__((aligned(4))); uint8_t dstBuffer[M2M_SIZE] __attribute__((aligned(4))); void DMA_M2M_Test(void) { for (int i 0; i M2M_SIZE; i) { srcBuffer[i] i 0xFF; } memset(dstBuffer, 0, M2M_SIZE); DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel6); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)srcBuffer; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)dstBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize M2M_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Enable; 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_VeryHigh; DMA_InitStructure.DMA_M2M DMA_M2M_Enable; DMA_Init(DMA1_Channel6, DMA_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_0); DMA_Cmd(DMA1_Channel6, ENABLE); while (DMA_GetCmdStatus(DMA1_Channel6) ! DISABLE); GPIO_ResetBits(GPIOA, GPIO_Pin_0); for (int i 0; i M2M_SIZE; i) { if (srcBuffer[i] ! dstBuffer[i]) { printf(Mismatch at index %d\r\n, i); return; } } printf(M2M transfer OK\r\n); }在STM32F103上72MHz主频下M2M模式的DMA吞吐量大约在10-15 MB/s左右。这个数据可以作为基准用来判断外设DMA的配置是否合理。5.3 串口DMA测速的注意事项串口DMA测速时实际吞吐量受限于串口波特率而不是DMA本身。比如115200波特率下理论最大吞吐量是11520字节/秒DMA再快也没用。所以测串口DMA的重点不是速度而是稳定性——连续发送大量数据看会不会丢包、会不会出现数据错位。我通常的做法是让串口DMA连续发送1MB的数据接收端用校验和验证数据完整性。如果校验通过说明DMA配置没问题。如果出现丢包优先检查DMA优先级和缓冲区大小。6. DMA实战中的常见问题与排查技巧6.1 DMA传输不启动或只传一次这是新手最常遇到的问题。DMA配置看起来没问题但数据就是不动或者只传了一次就停了。排查思路检查时钟使能DMA控制器的时钟有没有打开外设的时钟有没有打开这两个缺一不可。检查DMA请求使能外设的DMA请求位有没有置位比如串口的DMAT位、SPI的TXDMAEN位。检查传输计数器NDTR寄存器的值是不是0如果是0DMA不会启动。检查通道使能DMA_Cmd有没有调用通道使能位有没有置1检查模式配置如果是Normal模式传完一次就停了需要重新配置NDTR并重新使能。如果是Circular模式会自动重载。踩过的坑有一次调试SPI DMA折腾了半天发现是SPI的DMA请求位没使能。标准库的SPI_I2S_DMACmd函数很容易被遗漏因为SPI本身能正常工作只是DMA不触发。6.2 DMA传输数据错位或丢失数据错位通常和地址对齐、数据宽度有关。数据丢失则多半是缓冲区溢出或优先级配置不当。现象可能原因解决方法数据整体偏移一个字节地址未对齐检查源/目的地址是否按数据宽度对齐数据高低字节颠倒数据宽度配置错误确认外设和内存的数据宽度一致每隔几个字节丢一个DMA优先级太低提高DMA通道优先级大量数据丢失缓冲区太小或模式错误增大缓冲区接收用Circular模式数据时好时坏总线竞争调整DMA和CPU的访问时序降低其他DMA通道优先级6.3 串口DMA接收丢包串口DMA接收丢包是一个高频问题。常见原因有空闲中断处理太慢空闲中断里做了太多事情导致下一次接收开始时DMA还没重新配置好。解决办法是把数据处理放到主循环中断里只做标记和DMA重配置。DMA缓冲区太小一帧数据超过缓冲区大小DMA循环覆盖导致数据丢失。解决办法是增大缓冲区或者在协议层限制帧长。中断优先级冲突空闲中断被其他高优先级中断打断导致响应延迟。解决办法是合理分配NVIC优先级空闲中断的优先级要高于普通任务中断。6.4 Cache一致性问题Cortex-M7及以上如果你用的是STM32F7、H7这类带Cache的MCUDMA会遇到一个特殊问题Cache和内存数据不一致。CPU写数据到内存时数据可能还在Cache里没写到实际内存。DMA从内存读数据时读到的是旧数据。反过来DMA把数据写到内存后CPU读的时候可能读到的是Cache里的旧数据。解决办法有两种使用非Cache区域把DMA缓冲区放在MPU配置的非Cache区域CPU和DMA都直接访问内存。手动维护Cache在DMA传输前用SCB_CleanDCache_by_Addr清理Cache传输后用SCB_InvalidateDCache_by_Addr无效化Cache。SCB_CleanDCache_by_Addr((uint32_t *)txBuffer, TX_BUFFER_SIZE); DMA_Cmd(DMA1_Channel4, ENABLE); while (DMA_GetCmdStatus(DMA1_Channel4) ! DISABLE); SCB_InvalidateDCache_by_Addr((uint32_t *)rxBuffer, RX_BUFFER_SIZE);注意Cache维护操作本身有开销如果DMA传输的数据量很小频繁维护Cache反而会降低性能。这种情况下建议直接用非Cache区域。6.5 DMA中断优先级配置建议DMA中断的优先级配置直接影响系统的实时性。我的经验是串口接收DMA的传输完成中断优先级设为较高保证数据及时处理。串口发送DMA的传输完成中断优先级可以设为中等发送慢一点不影响功能。ADC DMA的传输完成中断优先级设为中等因为ADC数据通常有周期性晚一点处理没关系。内存到内存的DMA中断优先级设为低这种传输通常不紧急。在NVIC中优先级数值越小优先级越高。STM32F103的NVIC有4位优先级可以配置16个优先级等级。建议把DMA中断的抢占优先级设为1-3子优先级设为0-1。7. DMA进阶技巧与项目实战经验7.1 双缓冲模式实现无缝数据流双缓冲Double Buffer是DMA的一个高级功能STM32F4/F7/H7的DMA支持。它的原理是DMA有两个内存缓冲区当一个缓冲区在传输时CPU可以处理另一个缓冲区的数据。传输完成后自动切换并产生中断。这个模式特别适合音频流、高速ADC采样等场景。配置时使用DMA_DoubleBufferModeConfig函数指定两个缓冲区的地址然后用DMA_DoubleBufferModeCmd使能。DMA_DoubleBufferModeConfig(DMA1_Stream0, (uint32_t)buffer1, DMA_Memory_0); DMA_DoubleBufferModeConfig(DMA1_Stream0, (uint32_t)buffer2, DMA_Memory_1); DMA_DoubleBufferModeCmd(DMA1_Stream0, ENABLE);在传输完成中断里通过DMA_GetCurrentMemoryTarget函数判断当前DMA在用哪个缓冲区然后处理另一个缓冲区的数据。7.2 DMA与RTOS的配合在RTOS环境下使用DMA需要注意几点DMA中断中不要调用阻塞API中断服务函数里只能用FromISR结尾的API比如xSemaphoreGiveFromISR。用信号量或任务通知同步DMA传输完成后通过信号量通知任务处理数据而不是在中断里直接处理。注意栈空间DMA缓冲区不要放在任务栈上因为任务栈可能被回收或移动。用全局数组或动态内存分配。SemaphoreHandle_t dmaSemaphore; void DMA1_Channel4_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; if (DMA_GetITStatus(DMA1_IT_TC4) ! RESET) { DMA_ClearITPendingBit(DMA1_IT_TC4); xSemaphoreGiveFromISR(dmaSemaphore, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } } void DMATask(void *pvParameters) { while (1) { if (xSemaphoreTake(dmaSemaphore, portMAX_DELAY) pdTRUE) { ProcessDMAData(); } } }7.3 DMA在嵌入式AI测试中的应用嵌入式AI推理通常需要大量数据搬运比如把摄像头采集的图像数据搬到神经网络输入缓冲区把推理结果搬到输出缓冲区。DMA在这些场景中能显著降低CPU占用率。以STM32H7为例配合DCMI接口和DMA可以实现摄像头数据的零拷贝采集。DCMI的DMA直接把图像数据写入帧缓冲区CPU只需要在帧中断里做推理调度。7.4 嵌入式面试中的DMA高频考点DMA是嵌入式面试的必考内容常见问题包括DMA和中断的区别是什么各自适用什么场景DMA传输过程中CPU能访问同一块内存吗会有什么问题如何保证DMA传输的数据一致性DMA的循环模式和普通模式有什么区别多个DMA通道同时请求时仲裁器如何决定优先级回答这些问题时不要只背概念要结合实际项目经验。比如问到数据一致性可以讲Cache维护的具体操作问到优先级可以讲串口接收和SPI发送同时工作时如何分配优先级。8. 从DMA看嵌入式驱动开发的思维方式DMA这个外设表面上看是一个数据搬运工但它背后体现的是嵌入式系统设计的一个核心思想让每个硬件模块做它最擅长的事CPU只做决策和调度。轮询是CPU亲自搬数据中断是CPU被叫去搬数据DMA是CPU安排别人搬数据。这三种方式的演进本质上是在不断释放CPU的算力让它去做更有价值的事情。我在实际项目中总结出一个判断标准如果一个数据搬运任务有明确的源、目的和长度而且不需要CPU做实时处理那就应该用DMA。串口收发、SPI读写、ADC采样、DAC输出、内存拷贝这些场景都符合这个标准。反过来如果数据需要在搬运过程中做变换或者搬运的时机需要精确控制那DMA就不一定合适。比如协议解析、数据加密、动态长度调整这些还是得CPU来干。还有一个容易被忽略的点DMA不是银弹它也有开销。配置DMA寄存器需要时间DMA中断也有响应延迟。如果一次只搬几个字节用DMA反而比直接读寄存器慢。一般来说单次传输超过16字节时DMA的优势才开始显现。最后分享一个我在调试DMA时常用的技巧用逻辑分析仪同时抓DMA请求信号和GPIO翻转信号。DMA请求信号能告诉你DMA什么时候在工作GPIO翻转能告诉你传输什么时候完成。两个信号一对比就能看出DMA的响应延迟和传输效率比单纯看代码调试高效得多。这个内容后续还可以这样扩展把DMA和BDMABasic DMA、MDMAMaster DMA的区别讲清楚以及在不同STM32系列上DMA控制器的架构差异。如果大家感兴趣下一期可以专门聊聊这些。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询