ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32 DMA突发传输实现动态PWM脉冲序列

2026/9/28 2:44:50 拓冰建站 浏览量
STM32 DMA突发传输实现动态PWM脉冲序列 以前我在做轮速协议仿真和舵机控制时最烦的一件事就是PWM脉冲序列稍微复杂一点CPU就得频繁进中断去修改比较寄存器。后来把STM32的DMA突发传输机制用起来整个思路就变了——先把要输出的脉冲宽度全部算好放进内存表由DMA在定时器更新事件触发下自动搬运到比较寄存器CPU从“逐周期改寄存器”里彻底解放出来。这篇文章会从原理讲到CubeMX配置再到能够直接跑起来的代码覆盖基础的单通道DMA PWM方案以及真正发挥“突发”优势的多通道DMAR进阶方案。适合正在用STM32做舵机控制、电机调速、轮速协议仿真、逆变器波形生成的朋友也适合想把DMA和定时器协作关系一次搞懂的人。1. 为什么是DMA为什么还要扯上突发传输1.1 传统中断方案的痛点用PWM输出模拟信号本质是“在合适的时间把合适的占空比交给定时器”。最简单的做法是开一个定时器中断在中断里根据波形表查下一个周期的CCR值然后直接写TIMx-CCR1。这个方法在单通道、低更新率时完全没问题但项目一复杂就暴露两个硬伤。第一是CPU被打断的频率和PWM频率成正比。20kHz载波就意味着每秒2万次中断每次中断里要保存现场、查表、计算、写寄存器、恢复现场主循环的实时性基本报废。第二是中断响应本身有延迟且不稳定。查表判断写寄存器这段代码耗时不定一旦有其他高优先级中断插入PWM边沿的抖动会被放大用示波器看脉宽忽大忽小在电机控制上尤其明显。我做过一个三相SPWM闭环中断方案跑到8000Hz更新率的时候CPU占用已经到了没法接受的程度而且三相之间的同步性也靠软件硬凑非常痛苦。1.2 DMA替换中断后的核心链路用DMA之后思路完全反过来。事先把一整段“脉冲宽度序列”算好放到SRAM或者Flash的数组里然后告诉定时器每来一次更新事件DMA就自动搬一个数组元素到CCR寄存器。定时器的更新事件由硬件产生DMA搬数据也是硬件行为CPU只负责在波形表播完之前把下一段表准备好。就这样原来每秒两万次的中断被彻底消灭。核心链路是这样的定时器计数溢出 → 产生更新事件 → DMA控制器收到请求 → 从内存表读取当前点数值 → 写入比较寄存器 → 下一个PWM周期按新占空比输出。整条链路所有动作都是硬件DMA请求驱动的没有软件参与所以延迟和抖动基本由定时器计数本身决定。更关键的是CPU可以在DMA搬运波形表的这段时间里去算下一段波形或者直接进休眠模式这在低功耗场景和资源紧张的中大型项目里价值非常大。1.3 什么时候才真正需要突发这里需要对标题里的“突发传输”做个澄清。如果每次更新事件只搬运一个CCR值突发传输并不是必需品此时DMA的burst和FIFO大部分时间用不上普通单拍传输就够了。真正能用上突发的地方是“一次更新事件需要刷新多个参数”的场景。比如三相逆变器要同时更新CCR1、CCR2、CCR3或者一个PWM周期内要同时改ARR和CCR又或者DMA源数据位于Flash且希望按块读取减少总线冲突。这时候DMA会先把一帧数据放进内部FIFO再以多拍突发的形式一次性写入定时器的DMAR端口由定时器自己把数据分发到目标寄存器。所以这篇文章先讲单通道的基础方案因为它容易理解、上线快再把突发进阶方案单独拆开讲那才是标题里“突发生成动态PWM脉冲序列”的正解。2. 硬件资源与关键参数选型2.1 先确认芯片的DMA能力STM32家族的DMA控制器差距非常大。F1系列的DMA控制器比较朴素没有内部FIFO也没有MBURST/PBURST突发长度位严格意义上的“DMA突发传输”在F103上是操作不了的。如果你手上的项目就是以突发机制为核心卖点建议直接选STM32F4系列比如F407、F411或者具备类似DMA控制器的其他系列。F4的DMA有8个Stream每个Stream对应一组外设请求映射配置时最稳妥的方式是打开CubeMX让工具自动分配通道再去数据手册的“DMA request mapping”表核对一遍。F103用户也别急着走本文的基础版DMA-PWM方案可以照搬只是把FIFO和burst相关配置去掉即可。但如果你非要体验完整的突发传输我的建议很直接换F4别在F1上浪费时间折腾寄存器因为硬件根本不支持。2.2 DMA流、通道和定时器请求的对应关系以TIM1为例定时器的更新事件对应DMA请求TIM1_UP在CubeMX中通常映射到DMA2的Channel6具体落在哪个Stream要看CubeMX自动选择或者自己指定。我习惯把PWM通道和DMA流分开规划波形表用DMA2串口收发用DMA1两个DMA控制器并行工作总线压力自然分散。配置DMA时有几个字段必须两个一起看漏一个都跑不对Direction选MemoryToPeripheral方向是从内存到外设。Peripheral Address Increment选Disable外设地址固定不变。Memory Address Increment选Enable源地址每次传输后递增。Data Width外设和内存都选Half Word因为定时器CCR寄存器是16位。Mode选Circular循环模式播完最后一个点自动回到表头。这里有个容易理解错的地方外设地址固定指的是每次DMA传输时外设地址不递增而不是说DMA只能写给一个外设。基础版写给CCR1地址就是CCR1突发进阶版写给DMAR地址就是DMAR靠定时器内部逻辑再分发到不同寄存器。2.3 FIFO与突发长度的容量约束F4每个DMA Stream内部有一个4个字的FIFO也就是16字节。这意味着当你要使用突发传输时一次突发搬运的数据总量不能超过16字节否则FIFO会溢出。这个约束直接决定了数据宽度和Burst长度的搭配方式。我整理了一张速查表数据宽度单次最多的突发拍数原因8 bit16拍16×1B16B刚好占满FIFO16 bit8拍8×2B16B32 bit4拍4×4B16B在16位CCR的场景下如果配置PeriphBurst为INCR8单次突发就能把半满或整满FIFO的数据一次性搬出配INCR16就有问题因为32字节已经超过FIFO容量不少人在这里踩坑现象是DMA忙个不停但数据全乱了。很多初学者以为古发越长越好实际上突发长度必须受FIFO深度约束超出FIFO容量的配置是无效配置。3. 从CubeMX到可运行代码两种实现方案3.1 基础版更新事件触发DMA逐周期写CCR1先跑通最基本的一条链路。我用STM32F407Timer1来做目标PWM频率2kHz波形表长度500点每个点对应一个PWM周期的占空比。CubeMX配置步骤时钟配置。外部晶振加锁相环系统时钟168MHzAPB2定时器时钟168MHz。TIM1配置。选择PWM Generation Channel1Prescaler设83Counter Period设999这样PWM频率是168MHz/(84×1000)2kHz。打开AutoReload Preload避免在写入新CCR值时影响当前周期。DMA Settings。在TIM1的DMA Settings里添加TIM1_UP请求方向MemoryToPeripheral模式Circular数据宽度都选Half Word地址增量和优先级按前面说的配置。这一层用的是普通单拍传输FIFO可以先不使能。NVIC设置。打开DMA Stream中断方便在波形表播完一轮时做换表操作。波形表定义#define TABLE_SIZE 500 uint16_t pwm_table[TABLE_SIZE]; void fill_wave_table(void) { for (uint16_t i 0; i TABLE_SIZE; i) { // 示例正弦占空比幅值范围 50~950 float v 500.0f 450.0f * sinf(2.0f * 3.14159f * i / TABLE_SIZE); pwm_table[i] (uint16_t)v; } }启动输出HAL_TIM_PWM_Start_DMA(htim1, TIM_CHANNEL_1, (uint32_t *)pwm_table, TABLE_SIZE);这里HAL库会做两件事把DMA外设地址指向TIM1的CCR1然后启动DMA传输。之后每当TIM1产生更新事件DMA就把pwm_table里的下一个值搬到CCR1。第一轮跑完会进入HAL_TIM_PWM_PulseFinishedCallback回调。我在实际项目里常用这个回调来重新填充下一段波形表整个过程CPU负载可以忽略不计。需要注意的是DMA搬运发生在更新事件到来时而这个更新事件正好是PWM周期结束的时刻CCR的新值会在下一个计数周期才参与比较所以时间上完全来得及不需要担心“更新事件来了DMA还没搬完”。3.2 突发进阶版DMAR加DMA一轮写入整帧参数接下来是这篇文章的核心。假设你要做一个三相逆变器每个PWM周期都要同时更新CCR1、CCR2、CCR3。常规做法是开三个DMA流分别对应三个通道但更好的做法是利用定时器的DMAR寄存器把DMA突发传输和定时器内部总线结合起来。思路是这样的在内存里定义一个“帧”连续存放CCR1、CCR2、CCR3的值。每次定时器更新事件触发DMA后DMA发起一次多拍突发把这一帧数据全部写入定时器DMAR端口。定时器通过TIM_DCR寄存器里的DBA和DBL字段自动把数据分发到CCR1、CCR2、CCR3。这样一次更新事件就完成了多通道刷新这就是标题里“DMA突发生成动态PWM序列”的真正含义。配置关键点DMA外设地址填htim1.Instance-DMAR而不是CCR1。PeriphInc保持Disable因为始终写同一个DMAR端口。PeriphDataAlignment和MemDataAlignment都选Half Word。PeriphBurst和MemBurst设置成INCR4或INCR8注意2.3节的容量约束。TIM1-DCR寄存器设置目标寄存器组合DBA5表示从CCR1开始DBL2表示连续写3个寄存器。示例代码// 一帧数据对应CCR1、CCR2、CCR3三个比较寄存器 uint16_t frame[3]; void set_frame(uint16_t ccr1, uint16_t ccr2, uint16_t ccr3) { frame[0] ccr1; frame[1] ccr2; frame[2] ccr3; } // DMA配置核心部分 DMA_HandleTypeDef hdma_pwm; hdma_pwm.Instance DMA2_Stream3; // 实际Stream按CubeMX分配为准 hdma_pwm.Init.Channel DMA_CHANNEL_6; // TIM1_UP对应的DMA通道 hdma_pwm.Init.Direction DMA_MEMORY_TO_PERIPH; hdma_pwm.Init.PeriphInc DMA_PINC_DISABLE; hdma_pwm.Init.MemInc DMA_MINC_ENABLE; hdma_pwm.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_pwm.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_pwm.Init.Mode DMA_CIRCULAR; hdma_pwm.Init.Priority DMA_PRIORITY_HIGH; hdma_pwm.Init.FIFOMode DMA_FIFOMODE_ENABLE; hdma_pwm.Init.FIFOThreshold DMA_FIFO_THRESHOLD_FULL; hdma_pwm.Init.MemBurst DMA_MBURST_INC4; hdma_pwm.Init.PeriphBurst DMA_PBURST_INC4; HAL_DMA_Init(hdma_pwm); // 把DMA与TIM1更新事件关联 __HAL_LINKDMA(htim1, hdma[TIM_DMA_ID_UPDATE], hdma_pwm); // 定时器DMA突发目标从CCR1开始连续3个寄存器 TIM1-DCR ((5U 8) | (2U)); // 启动DMA源数据是frame数组目标地址是TIM1-DMAR HAL_DMA_Start_IT(hdma_pwm, (uint32_t)frame, (uint32_t)htim1.Instance-DMAR, 1); __HAL_TIM_ENABLE_DMA(htim1); // 启动三个通道的PWM输出 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_3);这里HAL_DMA_Start_IT的Length传1是因为我们把“一帧”当作一个整体帧内3个16位元素的搬运由DMA突发完成。实际项目里如果表里有N帧Length就传N。这个方案的收益很直接一次更新事件只产生一次DMA总线请求而这一帧数据通过定时器内部总线分发到三个CCR寄存器总线占用更集中多路DMA同时工作时冲突也更少。这也是为什么我强烈建议在做多通道PWM时直接用DMAR而不是堆多个DMA流。3.3 双缓冲让波形表边播放边刷新DMA循环模式会一遍遍播放同一张波形表但很多场景需要动态修改序列比如电机在不同转速下的加减速曲线、舵机连续动作轨迹。如果波形表很长又不舍得占内存双缓冲是最好的解法。双缓冲的思想很简单两块缓冲区A和BDMA当前从A读CPU往B写A读完了触发DMA传输完成中断DMA源地址切到BCPU接着往A写下一段。这样PWM输出是连续的波形内容却可以随时更新。F4的DMA控制器提供了DBM位直接支持双缓冲模式CubeMX里也可以勾选Double Buffer。实现时有几个细节必须注意DMA传输完成中断里只做指针切换和标志位清理不要在中断里做耗时的波形计算切换缓冲区前确保DMA已经停止否则可能访问到正在被DMA读取的缓冲导致波形撕裂。我见过太多人在这块栽跟头问题都是“边播边改”引起的。4. 典型应用轮速协议、舵机曲线与SPWM4.1 舵机和轮速协议的PWM脉宽序列舵机的标准控制信号是50Hz周期脉冲宽度0.5ms到2.5ms。以定时器时钟2MHz为例PSC设83ARR设39999周期就是20ms高电平计数范围是1000到5000。传统的做法是在定时器中断里更新CCR但舵机动作轨迹往往需要连续的脉宽变化比如从1ms平滑过渡到1.5ms用DMA表就非常舒服。先把整段轨迹计算成脉宽数组例如200个点的平滑曲线然后DMA循环播放。CPU只需要在播放过程中每隔一段时间更新一下表内容或者干脆在DMA完成中断里切换下一张表。轮速协议仿真本质也是这个套路汽车仪表或者ECU要的就是特定频率、特定脉宽的PWM方波序列DMA一旦跑起来主循环还可以同时处理CAN报文互不干扰。4.2 SPWM把正弦表变成逆变器驱动波形SPWM是DMA-PWM最典型的应用。载波20kHzARR设4200PSC设1再准备512点的正弦表每个点对应一个载波周期内的占空比。DMA Circular模式会一遍遍从正弦表头开始播放输出就是一个标准的SPWM波形。如果用突发进阶版把正弦表按“帧”组织每帧放三列的数值一次更新事件就能同步刷新三相占空比这就直接够到三相逆变器的门槛了。配合高级定时器的互补输出和死区功能DMA只需要负责CCR值更新死区时间由TIM_BDTR寄存器独立控制两边完全不冲突。我在开环验证逆变器驱动板时就是用这套方案跑出了标准正弦滤波波形示波器上看频率稳定度非常理想。4.3 实测对比抖动和CPU占用我手头的F407实测数据是这样的单通道PWM载波20kHz中断方案每周期进一次中断CPU占用在20%到30%之间输出边沿抖动约2到3微秒。改用DMA方案后CPU在波形生成这条路径上的占用直接归零抖动基本降到定时器计数时钟的单个周期量级84MHz时钟下约12纳秒普通示波器已经很难测出差别。在突发进阶版下三相同时刷新三相之间的相位一致性由硬件保证不再依赖软件同步这对电机控制是质的提升。当然实测数据受编译优化、中断优先级、DMA总线优先级影响不同项目会有差异但趋势是明确的凡是能交给DMA的周期性数据搬运就别用CPU去扛。5. 常见问题与排查技巧5.1 现象与排查速查表我把做这个方案时踩过的坑整理成了一张速查表基本都是实际操作中会遇到的现象可能原因解决方法DMA搬完一遍后PWM不再更新Mode选成了Normal传输结束没有重触发改成Circular或在传输完成回调里重新启动波形一直卡在某个固定值源表指针越界、MemInc没有开启、缓冲长度不匹配核对DMA的MemInc和Length确保源地址随传输递增波形表长度越长输出越乱Burst长度超过FIFO容量FIFO溢出按2.3节规则重新配置MSIZE和Burst长度开启FIFO后数据出现跳变FIFO阈值和Burst长度不匹配产生了不足一拍的空读FIFO阈值选FULL且Burst长度等于FIFO总容量换表时程序HardFaultDMA还在搬旧表时就修改了内存地址先停DMA改完源地址后再启动F103上找不到FIFO和burst寄存器F1的DMA控制器不支持FIFO和突发模式用基础版单拍DMA或换F4芯片多路PWM同时工作时某一路丢步多个DMA流优先级配置不当低优先级被饿死给实时性要求高的通道设更高优先级必要时分散到DMA1/DMA2DMA完成中断不触发NVIC没使能对应DMA流中断或回调函数名写错开启DMA Stream中断使用HAL_TIM_PWM_PulseFinishedCallback并在中断里调用HAL_DMA_IRQHandler5.2 几个容易忽略的细节第一个是预装载。无论ARR还是CCR尽量都把预装载打开。预装载的意思是寄存器的值先写在影子寄存器等到更新事件或比较捕获事件时才真正生效。不开预装载的情况下DMA写入CCR的那一拍就可能立即改变比较结果导致PWM波形在一个周期内出现异常毛刺。第二个是DMA源地址的对齐。开启FIFO后Memory端的源地址建议按Half Word对齐。如果数组定义成uint16_t编译器一般会自然对齐但如果你用强制类型转换把一个uint8_t数组转给DMA就可能踩到对齐问题。第三个是HAL库版本差异。部分版本的CubeF4固件里DMA_InitTypeDef并没有Burst和FIFO字段代码编译会直接报错。老版本固件的处理方式是手动写寄存器DMA2_Stream1-CR配置MBURST和PBURST位DMA2_Stream1-FCR配置FIFO使能和阈值。如果你遇到编译报错先升级固件包再考虑手动寄存器方案。第四个是不要把DMA完成中断和定时器更新中断混在一起用。DMA完成中断表示“这张表播完了”定时器更新中断表示“这个PWM周期结束了”两者的触发时机不同职责也不同。换表逻辑放DMA完成中断改占空比的硬件动作交给DMA本身别在中断里重复写CCR否则DMA搬运和软件写入会打架。第五个是我想额外提醒的DMA毕竟也在占用总线带宽。当系统里同时有ADC、串口、SPI都在用DMA时多通道PWM的DMA优先级一定要配置合理。我通常把实时性要求最高的PWM DMA流优先级设为High串口这类放到Medium避免关键时刻PWM被低优先级DMA插队。最后再分享一点我的实际体会这套DMAPWM组合我前后用了两年多最大的感受是PWM波形生成这类任务本质是“数据搬运”而不是“逻辑计算”。把它丢给DMA等于把一个高频重复动作交到了专用硬件手里。如果你现在还在为中断里反复写CCR发愁建议先照着基础版把链路跑通波形表能看到稳定输出之后再回头研究突发和双缓冲。真正动手之后你会发现硬件的边界问题大多出在配置参数不匹配上只要把FIFO容量、burst长度、数据宽度这三个东西对齐整个系统就非常可靠。