资讯动态

嵌入式DMA驱动开发实战:串口、SPI、ADC与TIM DMA Burst详解

发布时间:2026/10/27 1:11:19 来源:尧图企业网站定制
1. 为什么DMA是嵌入式驱动开发的必修课搞嵌入式驱动开发绕不开的一个话题就是DMA。你可以不知道RTOS的调度细节也可以暂时不碰文件系统但只要你的项目涉及串口高速收发、ADC连续采样、SPI大批量数据传输DMA就是那道必须跨过去的坎。我见过太多项目MCU主频跑到几百兆结果串口接收大量数据时还是丢包一问就是中断里一个一个字节搬数据CPU全耗在搬运上了根本没时间干正事。DMA全称Direct Memory Access直接内存访问。它的核心价值就一句话让数据在外设和内存之间自己搬CPU只负责下命令和收结果。打个比方CPU是公司老板外设是仓库内存是办公桌。没有DMA的时候老板要亲自一趟一趟去仓库搬货到办公桌搬货的时候没法开会、没法签文件。有了DMA老板只需要告诉搬运工“把这批货从仓库搬到桌上搬完了叫我”然后老板继续开会搬运工自己干活。这个搬运工就是DMA控制器。这一期我们聚焦DMA在嵌入式驱动开发中的实际应用覆盖串口DMA收发、SPI DMA传输、ADC DMA采样、DMA测速与性能评估、以及STM32 TIM DMA burst这类进阶用法。不管你是刚接触DMA的新手还是用过但总觉得没吃透的老手下面这些内容都是我实际项目中踩过坑、调过参数、验证过的经验可以直接参考复现。2. DMA核心原理与关键概念拆解2.1 DMA控制器的基本工作模型DMA控制器本质上是一个独立于CPU的总线主设备。它有自己的寄存器组包括源地址寄存器、目标地址寄存器、传输长度寄存器、控制寄存器等。你配置好这些寄存器之后置位使能位DMA就开始按照你设定的方式搬运数据。整个传输过程涉及几个关键要素源地址、目标地址、传输方向、传输宽度、传输数量、传输模式。源地址和目标地址可以是外设数据寄存器也可以是内存地址。传输方向决定了是从外设读到内存还是从内存写到外设或者内存到内存。传输宽度决定了每次搬1个字节、2个字节还是4个字节。传输数量就是你告诉DMA这次要搬多少个数据单元。传输模式分为单次模式和循环模式单次模式搬完指定数量就停循环模式搬完自动回到起点继续搬。这里有一个容易被忽略的点DMA的传输宽度必须和外设的数据寄存器宽度匹配。比如STM32的串口数据寄存器是8位的你如果配成半字16位传输就会出问题。我早期调串口DMA的时候就犯过这个错数据全是乱的查了半天才发现是传输宽度配错了。2.2 总线仲裁与优先级机制DMA控制器和CPU共享总线当两者同时要访问同一块内存或同一个总线时就需要仲裁。STM32的DMA控制器通常有多个通道每个通道可以配置优先级最高、高、中、低。当多个DMA通道同时请求传输时优先级高的先获得总线使用权。但这里有个实际经验优先级只在DMA通道之间竞争时起作用DMA和CPU之间的总线仲裁是由硬件自动处理的。CPU访问总线和DMA访问总线是分时复用的DMA传输会占用总线周期从而在一定程度上降低CPU的可用带宽。这就是为什么DMA虽然不占CPU的计算资源但仍然会影响CPU性能的原因。在实际项目中如果你的系统对实时性要求很高比如电机控制那DMA传输对总线带宽的占用就需要仔细评估。我一般会建议把DMA传输安排在CPU不太忙的时间段或者选择合适的总线矩阵让DMA和CPU访问不同的总线从设备减少冲突。2.3 中断与DMA的配合方式DMA传输完成之后通常会产生一个中断通知CPU。但DMA的中断和普通外设中断有一个重要区别DMA中断是在传输完成或半完成、出错时触发的而不是每个数据单元都触发。这就大大降低了中断频率。以串口接收为例假设你要接收1000个字节。如果用中断方式每个字节产生一次中断总共1000次中断。如果用DMA方式你可以配置传输完成中断只在接收完1000个字节后产生一次中断。中断次数从1000降到1CPU的负担天差地别。STM32的DMA还支持半传输中断。比如你配置了1000个字节的DMA接收当接收到500个字节时触发半传输中断接收到1000个字节时触发传输完成中断。这个特性在串口不定长数据接收中特别有用可以配合空闲中断实现高效的数据帧解析。3. 串口DMA收发实战配置3.1 串口DMA接收的完整配置流程串口DMA接收是嵌入式项目中最常见的DMA应用场景。下面我以STM32F103C8T6为例把整个配置流程拆开讲清楚。第一步配置串口本身。设置波特率、数据位、停止位、校验位使能串口接收。这一步和普通串口配置没有区别。第二步配置DMA通道。STM32F103的USART1_RX对应DMA1通道5USART1_TX对应DMA1通道4。这个对应关系是固定的查参考手册的DMA请求映射表就能找到。配置内容包括外设地址设为串口数据寄存器地址内存地址设为你定义的接收缓冲区首地址传输方向设为外设到内存传输宽度设为字节传输数量设为你期望接收的最大字节数模式设为循环模式或单次模式。第三步使能串口的DMA接收请求。在串口控制寄存器中置位DMA接收使能位。这一步很多人会忘结果DMA配置好了但串口根本不给DMA发请求数据自然收不到。第四步使能DMA通道。最后一步才是使能DMA通道开始传输。// 以STM32标准外设库为例的串口DMA接收配置 #define RX_BUFFER_SIZE 256 uint8_t rxBuffer[RX_BUFFER_SIZE]; void USART1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; USART_InitTypeDef USART_InitStructure; // 使能DMA1和USART1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // DMA配置 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_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel5, 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_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); // 使能串口DMA接收请求 USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); // 使能DMA通道 DMA_Cmd(DMA1_Channel5, ENABLE); // 使能串口 USART_Cmd(USART1, ENABLE); }这段代码的关键点在于DMA的传输数量设成了缓冲区大小256模式设为Normal。这意味着DMA会一直往缓冲区里填数据填满256个字节后停止。但实际串口数据往往是不定长的你可能只收到10个字节就结束了。这时候就需要配合串口空闲中断来判断一帧数据是否接收完毕。3.2 串口空闲中断配合DMA实现不定长接收串口空闲中断IDLE Interrupt是STM32串口的一个实用特性当串口接收线在一个字节传输时间之后仍然没有新数据到来就触发空闲中断。这个特性配合DMA可以完美解决不定长数据接收的问题。思路是这样的DMA在后台默默往缓冲区搬数据你不需要管它搬了多少。当一帧数据发送完毕串口线空闲下来触发空闲中断。在空闲中断里你读取DMA当前剩余的传输数量用缓冲区总大小减去剩余数量就得到了这一帧实际接收到的字节数。然后处理数据重新配置DMA准备接收下一帧。volatile uint8_t rxFrameLength 0; volatile uint8_t rxFrameComplete 0; void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 清除空闲中断标志 USART_ReceiveData(USART1); // 计算接收到的数据长度 rxFrameLength RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); rxFrameComplete 1; // 重新配置DMA准备接收下一帧 DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUFFER_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }注意清除空闲中断标志的方法是先读一次状态寄存器再读一次数据寄存器。只读状态寄存器是清不掉标志的这个坑我踩过中断会一直反复进。这个方案的好处是CPU几乎不参与数据搬运只在每帧数据结束时进一次中断。实测在115200波特率下连续接收数据CPU占用率不到1%。如果用传统的中断接收方式同样的数据量CPU占用率至少在20%以上。3.3 串口DMA发送的配置与注意事项串口DMA发送比接收简单一些因为发送的时机和长度都是你自己控制的。配置步骤和接收类似只是方向反过来内存到外设内存地址递增外设地址固定。但发送有一个关键问题DMA发送完成之前不能修改发送缓冲区的内容。如果你在DMA还在发送的时候就去改缓冲区发出去的数据就会出错。所以发送完成中断是必须的在中断里置位发送完成标志应用层检测到这个标志后才能进行下一次发送。另一个实际问题是串口DMA发送完成中断和串口发送完成中断TC中断不是一回事。DMA传输完成中断表示DMA已经把数据全部写入了串口数据寄存器但串口可能还在往外移位发送最后一个字节。如果你在DMA传输完成中断里就直接切换RS485的方向引脚最后一个字节可能还没发完就被截断了。正确的做法是等串口的TC标志置位后再切换方向。void DMA1_Channel4_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC4) ! RESET) { DMA_ClearITPendingBit(DMA1_IT_TC4); // 等待串口发送完成 while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 这里可以安全地切换RS485方向或进行下一次发送 txComplete 1; } }4. SPI DMA与ADC DMA的典型应用4.1 SPI DMA高速数据传输配置SPI接口在驱动LCD屏幕、读写Flash、与高速ADC通信时数据量往往很大。比如一块320x240的LCD刷一屏数据就是153600字节。如果用SPI中断方式发送每个字节一次中断CPU基本干不了别的。用DMA就轻松多了。SPI DMA的配置要点SPI的发送和接收是同时进行的所以通常需要配置两个DMA通道一个负责发送内存到SPI数据寄存器一个负责接收SPI数据寄存器到内存。发送通道的传输数量决定了整个传输的长度接收通道的传输数量必须和发送通道一致。这里有一个SPI DMA特有的坑SPI的发送和接收是全双工的即使你只想发送数据不想接收SPI也会把接收到的数据放进接收寄存器。如果你只配置了发送DMA没有配置接收DMA接收寄存器溢出后会导致SPI出错。所以稳妥的做法是同时配置发送和接收DMA接收DMA的目标地址指向一个临时缓冲区收到的数据不用管它。void SPI1_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // SPI1_TX使用DMA1通道3 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); // SPI1_RX使用DMA1通道2 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_Init(DMA1_Channel2, DMA_InitStructure); // 使能SPI的DMA请求 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx | SPI_I2S_DMAReq_Rx, ENABLE); }实测下来SPI时钟配到18MHz用DMA传输153600字节的LCD数据耗时大约70msCPU占用率几乎为零。同样的数据量如果用中断方式CPU占用率会飙到50%以上而且很容易因为中断响应不及时导致SPI时序出错。4.2 ADC多通道DMA采样方案ADC多通道采样是DMA的另一个经典应用。STM32的ADC可以配置为扫描模式依次转换多个通道。每次转换完成后如果不及时把数据读走下一次转换的结果就会覆盖掉上一次的。用DMA可以把每次转换的结果自动搬到内存数组里CPU只需要在全部转换完成后处理数据。配置要点ADC设置为扫描模式、连续转换模式、DMA使能。DMA配置为循环模式这样ADC一直在转换DMA一直在搬内存数组里始终是最新的采样值。传输宽度要配成半字16位因为ADC数据寄存器是12位的用16位对齐。#define ADC_CHANNELS 4 uint16_t adcValues[ADC_CHANNELS]; void ADC_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校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }这个方案下adcValues数组里始终是最新的4个通道采样值你随时读随时用不需要等待。循环模式的DMA会自动从头开始搬永远不会停。唯一需要注意的是如果你在读取adcValues的时候DMA正好在更新它可能会读到新旧混合的数据。对大多数应用来说这无所谓如果对数据一致性要求极高可以在读取前先关闭DMA读完再打开或者使用双缓冲模式。4.3 安富莱AD7606的DMA数据采集思路AD7606是一款8通道16位并行ADC在电力监测、工业控制领域用得很多。安富莱的AD7606模块在STM32平台上通常使用FSMC或GPIO模拟并行总线来读取数据。AD7606的转换结果通过并行数据总线输出每次转换完成后BUSY信号拉低你需要在BUSY下降沿之后读取8个通道的数据。用DMA来搬AD7606的数据是一个很自然的想法但这里有一个限制AD7606的并行接口通常接在FSMC上而FSMC本身不支持DMA。所以实际做法是用GPIO模拟并行总线配合定时器触发DMA来读取。或者用FSMC读取但用DMA来搬运FSMC数据寄存器到内存这个需要确认FSMC是否支持DMA请求。更常见的做法是用定时器产生PWM信号作为AD7606的CONVST触发信号同时用这个定时器的更新事件触发DMADMA从GPIO数据寄存器读取数据到内存。但GPIO数据寄存器不是标准的外设DMA请求源所以这个方案需要验证。如果不行就只能在BUSY中断里用CPU读取但这样CPU占用率会比较高。我实际用过的方案是AD7606配置为并行模式BUSY信号接外部中断。在BUSY下降沿中断里用CPU连续读取8次数据寄存器存入数组。虽然没用DMA但因为每次只读8个16位数据速度很快CPU占用率可以接受。如果采样率要求很高比如每通道100kSPS那就必须用DMA或者FPGA来做了。5. DMA测速与性能评估方法5.1 DMA传输速率测试方案DMA测速是评估系统性能的重要手段。你想知道DMA到底能搬多快瓶颈在哪里就需要实际测一下。测速的基本方法配置一个内存到内存的DMA传输传输一大块数据用定时器或者系统滴答计时算出来传输耗时再算出传输速率。内存到内存的DMA不涉及外设能测出DMA控制器的理论最大吞吐量。#define TEST_SIZE 4096 uint8_t srcBuffer[TEST_SIZE]; uint8_t dstBuffer[TEST_SIZE]; void DMA_MemToMem_Test(void) { DMA_InitTypeDef DMA_InitStructure; uint32_t startTime, endTime; 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 TEST_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); startTime GetTickCount(); DMA_Cmd(DMA1_Channel6, ENABLE); while(DMA_GetFlagStatus(DMA1_FLAG_TC6) RESET); endTime GetTickCount(); DMA_ClearFlag(DMA1_FLAG_TC6); uint32_t elapsed endTime - startTime; // 计算传输速率单位KB/s uint32_t speed (TEST_SIZE * 1000) / elapsed; }在STM32F103C8T6上72MHz主频内存到内存DMA传输4096字节实测耗时大约60微秒传输速率约68MB/s。这个速度已经接近AHB总线带宽的极限了。但要注意这是内存到内存的测试实际外设DMA传输会受到外设本身速度的限制。比如串口115200波特率每秒最多传11520字节DMA再快也没用瓶颈在串口。5.2 影响DMA传输性能的关键因素DMA传输速率受多个因素影响我按影响程度从大到小排列影响因素影响程度说明总线时钟频率高AHB总线频率越高DMA吞吐量越大传输数据宽度高32位传输比8位传输快4倍源和目标是否在同一总线中跨总线传输需要总线桥速度会降低DMA通道优先级中高优先级通道在竞争时获得更多带宽传输模式低循环模式和单次模式对速率影响不大内存对齐低对齐访问比非对齐访问稍快实际项目中如果你发现DMA传输速度不够首先检查数据宽度是不是配到了最大。比如SPI传输如果SPI数据寄存器是16位的DMA传输宽度也配成16位速度直接翻倍。其次检查总线时钟有些项目为了省电把AHB时钟降得很低DMA速度自然上不去。5.3 DMA测速失败常见原因排查DMA测速失败是很多人遇到过的问题代码看着没问题但就是测不出结果或者结果明显不对。我整理了几个常见原因和排查方法传输完成标志一直不置位。最常见的原因是DMA通道没使能或者外设的DMA请求没使能。先确认DMA_Cmd调用了再确认外设的DMA请求位设置了。还有一个隐蔽的原因传输数量配成了0DMA根本不传输自然不会有完成标志。测出来的速度异常高。如果你用系统滴答计时而系统滴答的中断优先级低于DMA中断可能在DMA传输完成中断里读到的计时值还没更新。解决办法是用硬件定时器计时或者把系统滴答优先级设到最高。传输的数据不对。检查源地址和目标地址是否配反了传输方向是否正确。内存到内存的DMA源地址和目标地址都是内存地址但方向要配成外设到内存因为DMA控制器把第一个地址当外设地址处理。这个很容易搞混。DMA传输一半就停了。检查传输模式是不是配成了单次模式单次模式搬完指定数量就停。如果你想要连续传输要配成循环模式。另外检查有没有DMA传输错误标志比如总线错误、配置错误等。6. STM32 TIM DMA Burst进阶应用6.1 TIM DMA Burst的工作原理TIM DMA Burst是STM32高级定时器的一个高级特性它允许DMA在定时器的特定事件如更新事件、比较匹配事件触发下向定时器的多个寄存器批量写入数据。这个特性在需要动态改变PWM占空比、生成复杂波形、控制步进电机加减速等场景中非常有用。传统的做法是每次改变PWM占空比都要进中断在中断里写CCR寄存器。如果PWM频率很高中断频率也会很高CPU根本扛不住。用TIM DMA Burst你可以预先算好一系列占空比值放在内存数组里DMA在每次定时器更新事件时自动把下一个值写入CCR寄存器完全不需要CPU参与。配置要点定时器配置为PWM模式使能更新事件触发DMADMA配置为循环模式源地址为占空比数组目标地址为CCR寄存器。每次更新事件DMA搬一个数据到CCRPWM占空比就变了。#define WAVE_POINTS 100 uint16_t dutyCycleTable[WAVE_POINTS]; void TIM1_DMA_Burst_Init(void) { DMA_InitTypeDef DMA_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // 生成正弦波占空比表 for(int i 0; i WAVE_POINTS; i) { dutyCycleTable[i] (uint16_t)(500 400 * sin(2 * 3.14159 * i / WAVE_POINTS)); } DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)TIM1-CCR1; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)dutyCycleTable; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize WAVE_POINTS; 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_Channel5, DMA_InitStructure); TIM_TimeBaseStructure.TIM_Period 999; TIM_TimeBaseStructure.TIM_Prescaler 71; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); // 使能TIM1的DMA请求 TIM_DMACmd(TIM1, TIM_DMA_Update, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE); TIM_Cmd(TIM1, ENABLE); }这段代码的效果是TIM1的PWM输出引脚上会产生一个频率1kHz、占空比按正弦规律变化的PWM波。占空比的变化完全由DMA自动完成CPU只需要初始化一次之后什么都不用管。这个方案在LED调光、电机驱动、音频合成等场景中非常实用。6.2 TIM DMA Burst的注意事项TIM DMA Burst虽然强大但有几个坑需要注意第一DMA的传输宽度必须和定时器寄存器的宽度匹配。STM32的TIM1是16位定时器CCR寄存器是16位的所以DMA传输宽度要配成半字。如果你配成了字节只会写入低8位高8位不变占空比就不对了。第二DMA的循环模式和数据表长度要配合好。如果你想要一个周期性的波形数据表长度就是波形的点数DMA配成循环模式搬完一轮自动从头开始。如果你想要单次波形DMA配成单次模式搬完就停。第三定时器的更新频率决定了DMA的搬运频率。更新频率越高DMA搬运越频繁对总线带宽的占用越大。如果更新频率太高比如几MHzDMA可能来不及搬会导致定时器行为异常。实际使用中更新频率建议不要超过1MHz。第四不是所有STM32系列都支持TIM DMA Burst。F1系列的部分定时器支持F4和F7系列支持得更完整。用之前查一下参考手册的DMA请求映射表确认你的定时器支持DMA请求。7. 常见问题与排查技巧实录7.1 DMA配置了但数据不传输这是最常见的问题代码看起来没问题但DMA就是不动。排查顺序如下先确认时钟使能了。DMA控制器的时钟、外设的时钟一个都不能少。我遇到过好几次都是忘了使能DMA时钟查了半天。再确认DMA通道使能了。DMA_Cmd(DMA1_ChannelX, ENABLE)这一句不能少。有些人配置完DMA_Init之后以为就完事了其实DMA_Init只是写寄存器真正启动需要DMA_Cmd。然后确认外设的DMA请求使能了。串口有USART_DMACmdSPI有SPI_I2S_DMACmdADC有ADC_DMACmd定时器有TIM_DMACmd。每个外设的DMA请求使能函数名不一样但都必须调用。最后确认传输数量不为0。DMA_BufferSize设成0的话DMA不会传输任何数据完成标志也不会置位。7.2 DMA传输数据错位或丢失数据错位通常是因为地址配置错误。检查外设地址是不是指向了正确的数据寄存器内存地址是不是指向了正确的缓冲区首地址。内存地址递增使能了吗如果内存地址不递增所有数据都会堆在同一个地址上只有最后一个数据有效。数据丢失可能是因为DMA传输速度跟不上外设速度。比如串口波特率很高DMA优先级又低总线被其他高优先级DMA占用串口数据寄存器溢出数据就丢了。解决办法是提高DMA通道优先级或者降低外设速度。还有一种可能是传输完成中断里没有及时重新配置DMA。比如串口DMA接收一帧数据接收完成后如果你不在中断里重新设置传输数量并重新使能DMA下一帧数据就收不到了。7.3 DMA中断进不去DMA中断进不去先检查NVIC配置。DMA通道的中断优先级使能了吗NVIC_EnableIRQ调用了吗中断服务函数的名称写对了吗STM32的DMA1_Channel5_IRQHandler和DMA1_Channel4_IRQHandler是不同的函数名写错了就进不去。再检查中断标志清除了吗如果中断标志没清除中断会一直触发但如果你在中断里没有清除标志中断会反复进入看起来像是卡在中断里。清除标志用DMA_ClearITPendingBit注意参数是DMA1_IT_TC5这种格式不是DMA1_FLAG_TC5。7.4 DMA与Cache的一致性问题这个问题在STM32F7、H7等带Cache的MCU上特别突出。DMA直接访问内存不经过Cache。如果CPU把数据写到了Cache里但还没写回内存DMA从内存读到的就是旧数据。反过来DMA把新数据写到内存但CPU从Cache里读到的还是旧数据。解决办法是在DMA传输前后进行Cache维护操作。发送前把数据从Cache写回内存Clean操作。接收后把内存数据无效化CacheInvalidate操作让CPU重新从内存读取。STM32 HAL库提供了SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr函数来做这件事。注意Cache维护操作是按Cache行进行的地址和长度都要按Cache行大小对齐。STM32F7的Cache行是32字节H7是64字节。如果你只Clean了部分数据剩下的部分可能还在Cache里DMA读到的就是新旧混合的数据。7.5 常见问题速查表问题现象可能原因排查方法DMA完全不传输时钟未使能、通道未使能、外设DMA请求未使能逐一检查RCC、DMA_Cmd、外设DMACmd传输完成标志不置位传输数量为0、通道未使能检查DMA_BufferSize和DMA_Cmd数据错位地址配置错误、递增设置错误检查外设地址、内存地址、递增使能数据丢失DMA优先级低、外设速度过快提高优先级、降低外设速度、增大缓冲区中断进不去NVIC未配置、中断函数名错误检查NVIC_EnableIRQ和函数名中断反复进入中断标志未清除检查DMA_ClearITPendingBit调用Cache一致性问题DMA绕过Cache传输前后做Cache Clean/Invalidate传输速度慢数据宽度小、总线时钟低增大数据宽度、提高总线时钟8. 几个实战中总结的DMA使用心得DMA用得好不好很大程度上取决于你对系统整体数据流的理解。我做了这么多年嵌入式驱动关于DMA有几点体会特别深。第一DMA不是万能的不要为了用DMA而用DMA。有些场景数据量很小比如每次只传几个字节用DMA的配置开销比直接CPU搬运还大。一般来说单次传输超过16个字节用DMA才有明显优势。第二DMA的缓冲区设计很关键。缓冲区太小容易溢出缓冲区太大浪费内存。我一般会根据数据帧的最大长度和传输速率来估算。比如串口115200波特率一帧数据最长100字节那缓冲区至少设256字节留足余量。第三DMA和中断的配合要恰到好处。全用DMA不用中断你不知道传输什么时候完成全用中断不用DMACPU负担太重。正确的做法是DMA负责搬运中断负责通知各司其职。第四调试DMA问题时先看寄存器。DMA的CNDTR寄存器告诉你还剩多少数据没传DMA的ISR寄存器告诉你有没有出错。这两个寄存器能帮你快速定位大部分问题。我习惯在调试的时候把这两个寄存器的值打印出来一目了然。第五不同MCU的DMA实现差异很大。STM32的DMA和GD32的DMA寄存器不兼容ESP32的DMA又是另一套体系。移植代码的时候DMA部分往往需要重写。所以写DMA驱动的时候尽量把硬件相关的部分封装起来上层应用不直接操作DMA寄存器这样移植起来会轻松很多。关于ESP32的DMA接收有一点需要特别注意ESP32的串口DMA接收和STM32的思路不太一样。ESP32的UART DMA通常配合FreeRTOS的队列使用DMA接收到的数据会触发中断在中断里把数据放入队列任务从队列里取数据。发送端不需要改变发送方式正常发送即可接收端的DMA配置和任务调度才是关键。如果你从STM32转到ESP32这一点需要适应一下。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑