ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32 DMA突发传输:高效生成动态PWM脉冲序列

STM32 DMA突发传输:高效生成动态PWM脉冲序列 STM32的DMA出现在PWM生成这种需求里很多人第一反应是“小题大做”——PWM不就是定时器比较输出吗ARR和CCR写一下不就行了但一旦你接触过需要连续输出几百上千个不同占空比脉冲的场景比如SPWM逆变、步进电机S曲线加减速、伺服控制里的动态负载波形模拟你就会发现纯靠CPU一个周期一个周期去改比较寄存器不但累而且时间不准、时序乱跳。这篇文章就围绕我最近做的一个小项目展开利用STM32的DMA突发传输机制把一张预先生成的脉冲宽度表按节拍“喂”给定时器的比较寄存器从而高效、稳定地产生动态PWM脉冲序列。我会把原理、CubeMX配置、代码实现、参数计算到调试中踩过的坑全部捋一遍适合正在做电机控制、逆变器、信号发生器或者任何需要动态波形输出的开发者参考。1. 先搞清楚需求什么样的PWM需要“DMA来扛”1.1 传统动态PWM写法的三大瓶颈常见的PWM输出无非是定频定占空比。设定好ARR和CCR定时器自己就能一直输出CPU完全不用管。但“动态PWM”就不一样了——每一个脉冲的宽度都可能和上一个不同也就是CCR的值需要周期性更新。如果脉冲序列比较短比如10个脉冲用定时器更新中断改CCR完全没问题。可当序列长度到达几百上千甚至需要持续、实时地输出问题就出来了。第一种写法是中断里改CCR。每个周期进一次更新中断CPU在中断里计算下一个CCR值并写入寄存器。这个方案最直观但致命问题是中断响应延迟。如果你的PWM频率是20kHz那每隔50微秒就要进一次中断如果CPU还在执行其他耗时任务中断响应稍微抖动输出的脉冲宽度就会跟着抖。对电机控制或逆变器这种对波形连续性要求极高的场景这种抖动就是噪声和振动。第二种写法是主循环查询更新标志然后改CCR。这种比中断更差——主循环的扫描周期不稳定占空比更新时刻完全不可控只能用在极低速、不要求精度的场合。第三种写法是用定时器本身的开漏输出或PWM模式切换。本质上还是要写寄存器依然逃不出“周期性写CCR”这个瓶颈。说到底传统写法的本质问题有三个CPU占用率高、更新时刻受软件执行影响、无法保证脉冲序列的连续一致性。尤其是做逆变器SPWM这种动辄几千个载波周期的场景CPU如果还要兼顾采样、保护逻辑和通信根本忙不过来。1.2 典型需要动态PWM脉冲序列的场景需要动态PWM脉冲序列的场景其实比想象中多我大致列几类SPWM正弦波调制逆变器、电机驱动器里最经典的应用。载波频率比如20kHz调制波50Hz一个正弦周期内有400个载波周期每个载波周期的占空比按正弦规律变化。占空比序列就是一张400个点的表。步进电机S曲线加减速步进电机启动时需要速度从0平滑上升到目标值停止时反向。这里的“速度”体现在脉冲频率上实际上就是脉宽周期不断变化的脉冲序列。DMADMA定时器可以有效生成这种变速脉冲流。舵机和伺服系统动态波形需要输出连续变化的位置指令比如一段阻尼振荡指令或者梯形轨迹。每个周期更新一次脉冲宽度输出就是控制波形本身。信号发生器和测试激励想用PWM输出任意波形比如三角波、噪声、自定义编码信号本质都是把波形离散成一张占空比表然后用PWM还原。这些场景的共同特点是脉冲数量大、更新频率高、序列有规律但需要逐个更新。这时候DMA就派上用场了——CPU只负责准备好数据表启动DMA传输之后表里的数值会在每个定时器更新事件被打进CCR寄存器周期准确、零CPU干预。2. 核心机制拆解定时器、DMA和突发传输是怎么配合的2.1 定时器PWM的工作原理与“更新事件”这个关键信号先复习一下STM32定时器产生PWM的基础。定时器内部有一个计数器CNT从0递增计数到达自动重装值ARR后回0并产生更新事件。另一边还有比较寄存器CCR计数器每走一步都和CCR比较相等时输出电平翻转。所以PWM的频率由ARR决定占空比由CCR决定。对于动态PWM来说ARR通常是固定的我们只需要周期性地修改CCR。那“什么时候改”最合适答案就是更新事件。更新事件表示一个完整PWM周期结束了、新周期刚开始这时候把下一个脉冲宽度写入CCR新周期立即生效脉冲序列就能无缝衔接。如果在一个周期的中间改写CCR就会导致当前周期被截断或拉长波形出现毛刺。STM32定时器的一大便利之处在于更新事件不止能触发中断还能触发DMA请求。这意味着定时器可以在硬件层面发出“我这边需要一个新数据”的信号DMA收到这个请求后自动从内存搬运数据到CCR寄存器。整个过程中CPU不需要参与也不再受中断响应延迟的影响。2.2 DMA在这里扮演什么角色DMADirect Memory Access说白了就是一个硬件搬运工。它可以在不需要CPU干预的情况下把数据从一处搬到另一处。在这个项目中DMA的搬运路径是固定的从内存里的波形表数组搬到定时器的CCR寄存器。配置方式也很直接数据方向内存到外设外设地址定时器的CCR寄存器地址内存地址波形表数组的首地址传输宽度半字16位因为CCR寄存器是16位传输模式循环模式Circular这样传输完最后一个数据后会自动回到表头继续传输形成循环的脉冲序列这套机制跑起来之后每个PWM周期DMA都会自动搬运一个数据到CCR波形表有多长就输出多少个不同占空比的脉冲。如果波形表是循环的输出就是周期性的动态波形。CPU完全可以去睡觉或者干别的活。2.3 突发传输到底“突发”在哪里关于“突发传输”我做项目过程中也反复琢磨过这个概念。很多新手会以为必须配置DMA的突发模式才能实现PWM序列生成其实这里要分两种理解。第一种理解是STM32高级DMA控制器的burst功能比如F4系列DMA2的MBURST/PBURST位可以配置一次外设请求触发后连续搬运1、4、8甚至16个数据。这种模式适用于需要一次更新多个寄存器的情况比如同时更新定时器的CCR1到CCR4保持四个通道的同步性或者更新ARRCCRCCER的组合。因为是一次硬件请求连续搬运多个数据之间的更新间隔几乎为零能保证多通道输出的一致性。第二种理解其实更通用就是我们常说的“DMA以硬件触发方式连续工作”。哪怕STM32F103这种不带burst寄存器的芯片通过“定时器更新事件触发DMA 循环模式”的组合就已经实现了硬件层面的连续突发搬运效果——每个事件触发一次数据源源不断。对单通道PWM序列来说这种逐数据触发的机制已经足够精准。我在F103上做单通道SPWM的时候用的就是逐数据触发后来在F407上做三相逆变器因为要同时控制三对互补PWMDMA的burst模式确实派上了用场。同样一个“突发传输”的名词落到不同芯片上配置和效果是有差异的这点一定要分清。2.4 为什么要用表驱动缓冲区设计的核心思想DMA本身不知道什么是正弦波它只是机械地把内存里的数据搬到外设寄存器。所以我们要在内存里预先准备好一张“波形表”这张表就是整个系统的心脏。以SPWM为例假设载波频率20kHz调制波频率50Hz那么一个调制周期内有400个PWM周期。我们把一个正弦周期等分为400份对每一份计算对应的占空比数值然后按顺序存进一个uint16_t数组。DMA每个周期从这个数组里取一个数写入CCR——于是定时器输出的PWM占空比就按正弦规律变化了。这就叫“表驱动”。表驱动的好处很直观CPU和波形生成完全解耦CPU只需要在系统初始化时把表算好运行中完全不用管。波形质量稳定更新时刻由硬件触发不依赖中断延迟每个脉冲周期都精确。改波形只需改表想从正弦波改成三角波、锯齿波、自定义任意波形只需要重新生成表驱动代码一行不用改。表的设计里有个容易被忽视的细节表长度和PWM周期频率的关系。假如PWM载波频率是20kHz表长400那么整个波形序列的重复频率是20kHz/40050Hz。如果你希望输出1kHz的正弦调制波形延用20kHz载波表长就得是20。这个换算关系设计表时必须算清楚。3. 实操从CubeMX配置到代码实现3.1 硬件准备和整体方案我这次项目手里的板子是STM32F103C8T6最小系统板虽然它没有F4那种带burst位的DMA控制器但用“更新事件触发DMA 循环模式”这套组合已经能跑得很稳。后面我也在F407上验证了burst配置代码逻辑会一并说明。硬件清单STM32F103C8T6核心板一块示波器或逻辑分析仪观察输出波形没有的话用LED看亮暗变化也能验证个大概ST-Link或者串口线用于下载调试杜邦线若干输出引脚我选择的是TIM1的CH1对应PA8。TIM1属于高级定时器支持PWM输出更重要的是它的DMA请求映射配置很典型放到F103和F407上都能跑。如果你用通用定时器TIM2/TIM3/TIM4原理完全一样只要DMA通道映射对得上就行。3.2 CubeMX配置过程定时器、DMA、GPIO一次搞定用STM32CubeMX配置这个项目非常快我一步步说。第一步是时钟树。我给定时器时钟选了最大的72MHzF103这样ARR的分辨率更高。F407的话定时器时钟可以跑到168MHz甚至更高看具体总线。第二步配置TIM1在Timers里选中TIM1Channel1选PWM Generation CH1。预分频器PSC如果我想让PWM频率为20kHz定时器时钟72MHz那么PSC1和ARR1的乘积应该是3600。为了灵活我把PSC设为0ARR设为3599这样CCR的调整分辨率是1/3600占空比精度大约0.028%足够用。如果觉得频率太高导致ARR值太小也可以加大PSC。设置完成后把“Trigger Output (TRGO)”先不选因为我们不需要定时器触发ADC这里要让更新事件触发DMA。第三步配置DMA。在TIM1的DMA Settings标签页里添加一个DMA请求选择TIM1_UP即更新事件触发。具体参数DirectionMemory To PeripheralPriorityHighData WidthHalf Word外设和内存都是Half WordModeCircularIncrement 地址内存地址递增打开外设地址递增关闭这里要特别提醒一下F103和F407的差异。F103在CubeMX里直接选DMA就行F407的话要确认选择的是DMA2的Stream因为F407的定时器DMA请求需要查重映射表并且DMA mode要选Circular。F407的DMA控制器支持burst配置在F407的DMA设置里可以看到Burst Mode和Increment相关选项这个在F103的设置界面里是没有的。如果你要用burst模式一次更新多个数据可以在F407的DMA Request Settings里把Burst Size设成INCR4或更大具体看你要搬运的数据量。第四步配置GPIO。PA8会被自动配置为复用推挽输出AF_PP。别忘了在GPIO设置里确认引脚速度PWM频率高的话选High或Very High避免信号边沿太缓。生成代码后加几行用户代码就可以出波形了。3.3 核心代码波形表生成与DMA启动初始化代码生成后需要自己写的东西不多。首先是波形表的生成。以50Hz正弦波调制、20kHz载波为例表长400#define TABLE_SIZE 400 #define PWM_ARR 3599 uint16_t pwm_table[TABLE_SIZE]; void generate_sine_table(uint16_t *table, uint32_t size, uint32_t arr_max) { for (uint32_t i 0; i size; i) { // 0 ~ 359.9 度的正弦值映射到 1%~99% 占空比 double angle 2.0 * PI * i / size; double duty 0.5 0.5 * sin(angle); table[i] (uint16_t)(duty * (arr_max - 2)) 1; } }这个公式里的关键点有两个。第一占空比范围故意控制在1%到99%而不是0到100%。实际逆变器或电机驱动里死区时间要靠这个余地来插如果占空比到0或100%上下桥臂直通的危险就大了。第二幅值系数是(arr_max - 2)加1是为了百分之一占空比下限不至于完全关断。接下来是启动DMA传输HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start_DMA(htim1, TIM_CHANNEL_1, (uint32_t *)pwm_table, TABLE_SIZE);这里有个非常容易踩的坑——如果你先调用了HAL_TIM_PWM_Start再调HAL_TIM_PWM_Start_DMAST的标准外设库和HAL库在F1系列上DMA和PWM不能同时都start。实际上HAL_TIM_PWM_Start_DMA已经包含了PWM输出的启动不需要单独再启动PWM。我在F103上第一次跑就是两个都调了结果DMA根本没启动。F1系列HAL库还有一个细节HAL_TIM_PWM_Start_DMA的参数里内存地址建议直接传(uint32_t *)而不是强制转换成uint32_t类型。我看过一些人写代码传(uint32_t)pwm_table编译没问题但运行时DMA搬运地址错误查了很久才发现是这里写错了。HAL库函数原型要求的是uint32_t *SrcAddr。之后不需要做任何事PWM就已经在输出了。如果这时候你用示波器看PA8的波形占空比会按正弦规律变化脉冲列的包络就是50Hz正弦波。3.4 参数计算一个650点SPWM案例步步推导很多读者会问这些数是怎么算出来的我拿一个实际案例走一遍全计算过程。需求系统时钟72MHz想用TIM1输出SPWM载波频率20kHz调制波频率50Hz整周期脉冲数量正好400。求PSC、ARR、表长。第一步算定时器计数频率。预分频PSC是分频系数定时器实际计数频率 72MHz / (PSC1)。取PSC0那么计数频率就是72MHz每个计数周期是1/72MHz≈13.9ns。第二步算ARR。PWM频率 计数频率 / (ARR1)。20kHz 72MHz / (ARR1)所以ARR1 3600ARR3599。第三步算表长。一个50Hz正弦调制周期的时间是1/50Hz20ms。一个20kHz PWM周期的时间是1/20kHz50微秒。表长 20ms / 50微秒 400。也就是说每个PWM周期对应正弦波上1/400个点。第四步算占空比和CCR。假如某一点的瞬时值为sin(θ)0.6中点偏移0.5后占空比约0.8CCR 0.8*3599 ≈ 2879。注意因为整数运算取整的关系实际占空比会有半个计数周期的量化误差这在脉冲宽度调制里是正常的PWM分辨率足够高时误差可忽略。如果我的需求换了比如载波频率提高到100kHz其他不变那ARR1720ARR719此时占空比分辨率只有1/720≈0.14%在电机控制里也算够用但谐波会稍微大一点。若想同时保持高频率和高分辨率要么系统主频更高要么改用F407这类支持高定时器时钟的芯片要么用多定时器级联的方式扩展分辨率这就属于进阶玩法了。实际操作中我会在Excel里把这套计算提前验算一遍把ARR、表长、基准占空比对应好再写进代码免得烧录后现象不对还得回头算。3.5 动态更新波形表什么时候改怎么改表驱动方案里CPU看似闲着但实际项目往往需要动态改变输出波形——频率变了、幅值变了、波形类型变了。这就要求我们在运行中修改波形表。最简单的做法修改表内容后重启DMA。但这里有个坑。如果DMA正处于循环传输中你直接改表本周期DMA已经搬运出去的数据不受影响但下一拍搬运的可能是修改后的值导致序列前后不一致波形出现一跳。规避方法有三种我按推荐程度排序统一更新时机利用DMA的半传输/传输完成中断在DMA搬运到表边界时修改数据。因为边界处意味着一个完整序列正好结束此刻更新不会破坏当前序列的连续性。双缓冲准备两张表一张正在被DMA使用一张待更新。需要切换时停止DMA、切换内存地址指针、重新启动DMA。F407上甚至可以硬件自动切换。停止-修改-重启简单粗暴但存在短时波形中断。我用的比较多的是在DMA传输完成中断里改表。F103循环模式下DMA没有严格意义上的“完成中断”因为不会完成但会产生半传输中断和传输完成中断每循环一圈触发一次。在传输完成中断里把新表的数据拷贝到当前表修改期间DMA恰好在表头位置重新开始改动就可以在下个周期干净地生效。volatile uint8_t table_ready 0; void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM1) { if (table_ready) { // 此时DMA刚刚完成一个周期的搬运处于表头位置适合更新 generate_sine_table(pwm_table, TABLE_SIZE, PWM_ARR); table_ready 0; } } }需要注意的是HAL库的PWM脉冲完成回调函数名是HAL_TIM_PWM_PulseFinishedCallback不是HAL_TIM_PeriodElapsedCallback。定时器更新事件中断回调这两个函数在启用DMA后都可能会进别混淆了。4. 调试实录与避坑指南4.1 现象一DMA只传一次就停了没有循环输出这个现象最常见。我在F103上第一次启动发现输出一个高电平脉冲后波形就没了示波器看PA8只有一次翻转。排查下来原因是DMA模式没有配成Circular。CubeMX里DMA模式默认可能是NormalNormal一整个数组传完就停止不会继续。改成Circular后问题解决。另外一个原因是DMA请求源不对。TIM1有多个DMA请求映射比如TIM1_CH1、TIM1_CH2、TIM1_CH3、TIM1_CH4和TIM1_UP。如果错误选择了TIM1_CH1请求它触发的是比较事件而不是更新事件行为就会非常奇怪——不是按周期搬家而是按比较匹配频繁触发。正确的应该是TIM1_UP。注意F407的定时器DMA映射还要在CubeMX里看一下具体选的是哪个DMA Stream如果选错Stream请求是触发不了的。4.2 现象二输出波形毛刺多、占空比抖动大毛刺问题一般三个方向排查。第一个是GPIO速度配置过低。PWM频率达到几十kHz时如果GPIO输出速度配成Low边沿会变缓波形看起就像毛刺。把引脚速度改成High或Very High即可。第二个是DMA数据宽度没配成Half Word。如果你配成了ByteDMA每次只搬运CCR寄存器的低8位高8位被清0或残留占空比就完全错乱。我见过有人把内存宽度配置成Byte、外设宽度配置成Half WordDMA会以16位去读内存而内存只写了8位数据错位相当隐蔽。第三个是系统时钟不稳定或PSC倍频设置问题导致PWM载波频率本身抖动。这种情况优先检查CubeMX时钟配置是否正确。4.3 现象三更新波形频率时波形“撕裂”或跳变修改波形表的一瞬间输出波形突然出现一个极宽或极窄的脉冲这就说明更新时机没控制好。如果你想改正弦波的频率比如从50Hz切到100Hz新表和旧表的第一个数据大概率差得很远。如果DMA正好在表中间搬运突然从旧值切到新值CCR突变脉冲宽度就会剧烈跳变。对电机驱动来说这种突变等于负载瞬间冲击。解决方法就是前面说的等DMA传输完成中断来了再改表并且新表生成时做一个过渡——从旧表的当前相位渐变到新表的目标相位避免突然跳变。比如我从50Hz切到100Hz不是直接把整个表换成100Hz的数据而是用10个周期的时间把频率从50Hz线性“拉”到100Hz脉冲宽度逐步变化。这个渐变过程其实就是在运行中重新生成一张带过渡包络的新表。4.4 现象四DMA搬运影响了其他外设比如ADC采样数据乱了DMA虽然不占CPU但它是系统总线的一个主设备。高负载时DMA频繁搬运会抢占总线带宽。如果项目里ADC、串口、SPI也用了DMA多个DMA通道抢占总线低优先级的通道就可能被饿死。排查方法很简单把TIM1所在DMA的优先级调低一点给ADC、串口这些对实时性要求更高的DMA分配更高优先级。STM32的DMA仲裁规则是固定优先级优先同优先级再按通道编号。我把TIM1的DMA优先级设为Low之后ADC的DMA就再没丢过数据。另一个办法是不要在突发传输较长的模式下把外设和内存都用最大宽度。例如F407的burst一次搬运16个数据如果内存不是连续16个半字对齐地址回绕会出现效率下降甚至错误。用之前确认一下内存缓冲区的对齐和突发长度的匹配。4.5 常见问题速查表现象可能原因解决措施DMA一次就停无循环输出DMA模式为Normal改成Circular输出完全没波形未启动PWM/DMA确认HAL_TIM_PWM_Start_DMA被调用占空比错乱DMA数据宽度配置错误内存和外设宽度都设成Half Word波形毛刺明显GPIO速度偏低引脚速度改High/Very High更新频率时波形跳变修改表时机不对在传输完成中断里改表并做渐变过渡其他DMA通道异常丢失总线带宽被抢占降低TIM DMA优先级调整通道编号burst配置后数据乱序突发长度与地址增量不匹配按数据量配置INCRn内存表连续5. 一点进阶话题F4/H7上的Burst模式到底怎么用前面提到F4/H7这类带MBURST/PBURST寄存器的芯片burst模式确实是个利器。我后来在做三相逆变器时用F407验证过一次DMA请求直接更新TIM1的CCR1到CCR3三个通道保证了三相PWM占空比在同一载波周期内完全同步。配置思路是当定时器更新事件到来DMA不传输一个数据而是连续搬运三个数据到CCR1、CCR2、CCR3寄存器。因为三个数据来自连续内存区一个结构体数组或二维数组的一行且外设地址递增地址是连续的CCR寄存器多通道之间没有相位差。关键配置点DMA外设突发长度PBURST设为INCR4如果你的数据段是一次更新3个寄存器INCR4也能满足因为多传的1拍会被忽略或不影响但严谨起见按实际数据量来配置内存突发长度MBURST和PBURST要匹配不匹配时DMA会报FIFO错误DMA工作在循环模式内存缓冲区大小必须是“每个更新数据量 × 波形表长度”的整数倍F4的DMA流还有FIFO配置如果突发数据比较多要保证FIFO阈值能容纳一个突发长度否则可能溢出F4上如果再配合DMA双缓冲甚至能做到一边输出一边把下一段的波形表预加载进来这就是很多高端数字电源、变频器常用的“乒乓缓冲”结构。CPU只在缓冲区切换时做一次指针搬运波形生成完全交由硬件完成。不过这套玩法初上手容易糊涂建议先把F103的简单方案跑通再往F4的burst和乒乓上靠。我个人的体会是DMA生成动态PWM这件事难点不在配置参数本身而在对整个“数据表 → 硬件触发 → 寄存器更新”链条的理解。只要想清楚每个PWM周期对应一个表元素、每个表元素对应一个CCR值很多问题就迎刃而解。后面做复杂项目时这套思路还能往ADC采样、串口收发、传感器数据采集上扩展——本质上都是“用DMA把外设和内存之间的数据流自动化让CPU专注于真正需要思考的事情”。最后分享一个小技巧调试时别急着用高级工具先用一个LED接在PWM引脚上把波形表改成前50个点全亮、后50个点全灭验证DMA循环是否正常。这一步过了再换成正弦表调波形问题定位会快很多。
返回列表