ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

变频PWM参数同步更新:DMA burst模式原理与实战配置

变频PWM参数同步更新:DMA burst模式原理与实战配置 最近在调一台变频驱动的小板子需要输出频率连续变化、占空比同步调整的PWM波形。最开始图省事直接在定时器更新中断里改ARR和CCR代码不到一百行但示波器一测就露馅了频率切换有明显的毛刺CPU占用率也高得离谱。后来翻LAT1202的参考手册看到TIM的DMA burst功能研究了两天踩了几个坑总算是把波形参数切换这件事彻底交给了硬件。这套方案适合正在用LAT1202或者同类型MCU做变频电源、电机驱动、LED调光、超声波驱动需要在运行中高频切换PWM参数的同学。核心思路一句话把每个PWM周期需要更新的寄存器值预先排成一张数据表用DMA的burst模式一次性灌进定时器寄存器CPU只负责在后台计算和准备数据完全不参与波形切换。下面我把这套方案的原理、寄存器配置、代码实现和调试中踩过的坑完整拆开讲内容以LAT1202为主但思路和寄存器模型在STM32、GD32等大多数Cortex-M内核MCU上都是通用的。1. 为什么变频PWM会让CPU如此狼狈1.1 变频信号到底从哪来很多场景需要频率可变的PWM而且往往频率和占空比要同时变。我举几个常见的例子。变频压缩机驱动启动时不能直接上高频否则转子跟不上轻则过流报警重则烧功率管。所以要从几十Hz慢慢往上爬同时占空比也要按一定曲线加整个过程类似电机软启动。超声波清洗设备需要在某个频带内来回扫描找到换能器的谐振点谐振时电流最小、清洗效果最好。这个扫描频率通常几十kHz变化速率还挺快。还有LLC电源的PFM调压、电子负载的功率曲线模拟、LED的深度调光为了避免频闪要把PWM频率搬到人眼感知不到的高频段——本质上都是“运行中不断修改PWM频率和占空比”的需求。落到MCU代码上就是定时器的ARR寄存器决定周期CCR寄存器决定占空比这两个值要在运行中被周期性改写。低频时可能几毫秒改一次高频应用比如20kHz PWM一个周期只有50us在这50us内你要完成“算好新参数写入寄存器”而且写入时机还必须在特定相位不然波形就乱套。1.2 中断方案的三宗罪一开始我用TIM更新中断代码长这样void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); TIM_SetAutoreload(TIM1, new_arr); TIM_SetCompare1(TIM1, new_ccr); } }功能完全没问题但示波器和逻辑分析仪一测问题就出来了。第一宗罪是抖动。中断响应的延迟是不确定的。如果此刻系统正在处理串口中断或者ADC DMA正在占用总线ARR或CCR实际被写入的时刻会有很大的不确定性。变频PWM最怕的就是周期不稳每一个周期的误差都会在频谱上形成杂散分量电机噪音变大电源传导骚扰变差这是做产品很难接受的。第二宗罪是CPU占用率。假设20kHz PWM每个周期50us进一次中断中断里还要计算下一组ARR和CCR。如果只是简单的线性扫描倒还好但如果要按S曲线加减速或者做电流解算50us内往往算不完。CPU会迅速饱和实时性就无从谈起。第三宗罪是边界条件复杂。更新中断里修改ARR如果新ARR比计数器当前的CNT还小计数器会立即产生更新事件计数器清零重来。如果此刻你又正在修改CCR那这几个寄存器的更新顺序就全乱了可能出来一个周期长度异常的长脉冲或者短脉冲。我在做压缩机驱动时就被这种毛刺折磨过一个异常周期足以让电流采样计算出错误的结果。1.3 DMA burst怎么改变局面DMA burst的思路完全不同MCU事先把接下来几百甚至几千个PWM周期的参数全部算好放在一块连续的内存里。定时器每次产生更新事件自动触发DMA搬运把下一组数据直接写到定时器寄存器。整个过程CPU零参与。但这里最关键的还不是“用DMA代替中断”而是“一次DMA请求硬件连续写入多个寄存器”。变频PWM往往既要改ARR又要改CCR如果分开两个DMA通道搬运二者到达定时器的时间会出现微妙差异导致某几个PWM周期出现“频率变了、占空比没跟上”的错误输出。burst模式把多个寄存器的更新打包进一个DMA周期内这个窗口被彻底消除。2. 理解DMA burst在定时器里的真实工作逻辑2.1 DMAR所有数据都走这一个入口先说一个容易混淆的概念。LAT1202定时器里有一个寄存器叫DMARDMA Request RegisterDMA总线只跟它打交道。也就是说无论目标寄存器是ARR、CCR、RCR还是PSCDMA每次传输都只往DMAR这个固定地址写数据至于这些数据最终落到哪个寄存器由定时器硬件根据DCR寄存器的“分拣规则”自动分发。形象一点说DMA是个送货员每次只把包裹送到前台DMAR前台拿到包裹后按照订单DCR把包裹分到各个部门的邮箱里。这就是burst模式最核心的逻辑。这个设计的好处是DMA那边配置特别简单外设地址固定永远只写一个地址不需要关心定时器内部的寄存器映射关系。所有复杂度都收敛在定时器自身的DCR配置上。2.2 DBA和DBL分拣规则的三个关键参数DCR寄存器里有两个字段需要用对一个是DBA一个是DBL。DBADMA Base Address是burst传输的起始寄存器地址按32位字偏移计算基准是TIM_CR1寄存器也就是定时器地址空间0x00处。注意是“字偏移”不是字节偏移。AR寄存器偏移0x2C按4字节一除字偏移是11CCR1偏移0x34字偏移是13。很多人在这里出错把0x2C直接写进DBA结果burst起点跑到了完全无关的寄存器上。DBLDMA Burst Length表示一次burst要传输几个数据。这个字段很有意思实际传输的个数是DBL1。比如你要一次写3个寄存器DBL就要填2。常见的寄存器字偏移可以参考这张表以LAT1202的TIM1为例寄存器字节偏移32位字偏移CR10x000CR20x041SMCR0x082DIER0x0C3SR0x104EGR0x145CCMR10x186CCMR20x1C7CCER0x208CNT0x249PSC0x2810ARR0x2C11RCR0x3012CCR10x3413CCR20x3814CCR30x3C15CCR40x4016BDTR0x4417DMAR0x4C19注意这张表是按寄存器地址排列的burst传输时硬件会从DBA所指的寄存器开始沿着地址递增方向连续写DBL1个寄存器。2.3 为什么“多寄存器同步更新”才是burst的价值所在普通DMA也可以写定时器寄存器但一次DMA请求只能写一个地址。如果需要同时更新ARR和CCR1常规思路有两个开两个DMA通道各管一个寄存器或者在更新中断里连续写两次。开两个DMA通道的隐患在于同步性。两个通道的优先级、仲裁延迟不一样在总线矩阵上谁先谁后是动态的没法保证每次都是同一个顺序到达。我实测过三相PWM同步切换的场景某一次切换中A相已经用了新占空比B相还在用上一帧的旧值整个电流波形出现一个明显的不对称尖峰。这个尖峰在电机运行中可能不会立刻造成故障但在电流采样里就是一个巨大的误差源。而在更新中断里连续写多次同样有中断延迟不稳定的问题。burst模式把多个寄存器的更新打包在一次DMA传输里数据依次进入DMAR并由定时器硬件自动分发给连续寄存器这个过程不经过任何软件介入时序完全由硬件保证。3. LAT1202的代码实现从CubeMX到裸机寄存器3.1 先明确需求再定数据帧格式我们的目标是TIM1_CH1输出PWM频率从2kHz到10kHz线性扫频同时占空比从30%线性变化到60%扫描周期100ms每1ms更新一次参数。这个需求里2kHz对应500us10kHz对应100us。每1ms切换一次参数意味着2kHz时每隔两个周期切一次10kHz时每隔10个周期切一次。DMA的更新事件频率等于PWM频率而我们的数据表只需要在PWM更新事件里被读取因此一帧数据会持续多个PWM周期这正是“按需更新”的典型用法。为了算参数我选定PSC71定时器时钟72MHz则计数时钟1MHz2kHz时ARR 1000000 / 2000 - 1 49930%占空比 CCR1 15010kHz时ARR 1000000 / 10000 - 1 9960%占空比 CCR1 60当然从499到99不能一步跳否则会产生大量的截断毛刺。我在实际工程里把100ms扫描周期分成100段每段1msARR每次减4CCR1每次加0.9左右。这样频率和占空比都按近似线性变化。数据帧格式怎么定因为我们计划用burst一次更新ARR和CCR1而这两个寄存器在地址上不连续中间隔了一个RCR。所以帧结构必须包含RCR的占位值typedef struct { uint32_t arr; // 要写入ARR的值 uint32_t rcr; // RCR占位填0即可不影响PWM uint32_t ccr1; // 要写入CCR1的值 } pwm_burst_frame_t;为什么RCR填0就可以因为RCR是重复计数寄存器默认0表示每次计数器溢出都产生更新事件如果我们不需要改变更新事件的频率这个位置填0完全正确。3.2 初始化DMA通道内存递增外设地址锁定DMA配置上有几个关键点。外设地址要指向TIM1的DMAR寄存器使能内存地址递增外设地址不变数据宽度32位循环模式。DMA_InitTypeDef dma; dma.PeriphBaseAddr (uint32_t)TIM1-DMAR; dma.MemoryBaseAddr (uint32_t)pwm_table[0]; dma.Direction DMA_DIR_PERIPH_DST; dma.BufferSize FRAME_COUNT * 3; // 总数据量 帧数 * 每帧3个32位数据 dma.PeriphInc DMA_PINC_DISABLE; dma.MemoryInc DMA_MINC_ENABLE; dma.PeriphDataWidth DMA_PDATAW_32B; dma.MemoryDataWidth DMA_MDATAW_32B; dma.Mode DMA_CIRCULAR; dma.Priority DMA_PRIORITY_HIGH; DMA_Init(DMA1_Channel1, dma);这里BufferSize的值是整个表的数据个数不是帧数。比如100帧每帧3个32位数BufferSize就是300。循环模式下DMA走完整个表后回卷到起始地址这样变频扫频就可以一直循环跑。我在第一次配置时犯过一个错把BufferSize填成了帧数100。结果DMA只搬运了三分之一的表就回卷了后面几十帧数据全被跳过扫频波形在前三分之一段反复跳。示波器上看起来像一只乱跳的青蛙排查了好一会儿才反应过来是BufferSize的问题。3.3 配置定时器burst把DBA指向ARRDCR寄存器配置就一行TIM1-DCR (2 8) | 11; // DBL2, DBA11(ARR)意思是burst从ARR开始连续写ARR、RCR、CCR1三个寄存器。因为数据帧的存放顺序就是{arr, rcr, ccr1}DMA把三个值依次写入DMAR后硬件自动把它们分发到ARR、RCR、CCR1一次搞定。接着使能定时器的更新DMA请求TIM1-DIER | TIM_DIER_UDE; // 使能Update DMA Request同时别忘了配置PWM模式、预装载、输出通道TIM1-PSC 71; TIM1-ARR 499; // 初值随便填DMA很快会覆盖 TIM1-CCR1 150; TIM1-CCMR1 TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // PWM模式1 TIM1-CCMR1 | TIM_CCMR1_OC1PE; // 使能CCR1预装载 TIM1-CR1 | TIM_CR1_ARPE; // 使能ARR预装载 TIM1-CCER | TIM_CCER_CC1E; // 使能CH1输出使能ARR和CCR的预装载是很重要的。预装载的作用是你写入的ARR和CCR的值不会立刻生效而是等下一个更新事件到来时才一次装载到影子寄存器。这和我们“DMA在更新事件时写入新参数”的节奏是匹配的先写预装载寄存器更新事件到来时同时生效。3.4 启动顺序先DMA再UG再计数器启动顺序有一个比较容易踩的坑。正确顺序是先配置好定时器但不使能计数器CR1的CEN0使能DMA通道软件置位UG位产生一次更新事件让DMA搬运第一帧数据最后使能计数器PWM开始输出。为什么必须这样因为更新事件是DMA burst的触发源。如果计数器已经跑了第一次更新事件可能发生在DMA还没有准备好的窗口里第一帧数据就丢了。后续所有帧都会错位输出波形和预期完全对不上。用软件UG触发一次的好处是DMA会把pwm_table[0]搬运到寄存器等你再使能计数器时寄存器里已经是合法的一组参数。DMA_Cmd(DMA1_Channel1, ENABLE); TIM1-EGR | TIM_EGR_UG; // 软件触发更新事件DMA开始搬运第一帧 TIM1-CR1 | TIM_CR1_CEN; // 使能计数器另一个容易被忽略的点如果UG触发的更新事件同时把CNT清零了PWM输出会从配置好的第一帧参数开始起始相位是确定的这有利于多通道或多机同步。3.5 如果ARR不在起始位置怎么处理前面那种DBA11的方式写的是ARR、RCR、CCR1中间夹了一个RCR。虽然RCR占位不碍事但总感觉别扭。另一种更简洁的方式是把DBA指向CCR1字偏移13DBL设为1这样只更新CCR1和CCR2适合四路占空比同步更新的场景不需要碰ARR。如果你一定要同时更新ARR和CCR又不想被RCR夹在中间可以用两个DMA通道一个通道响应更新事件专职写CCR1另一个通道用一个不那么频繁的触发源比如某个从定时器事件写ARR。但这种方式会引入两个通道的同步问题复杂度更高。我个人的经验是如果burst控制范围里只是多一个RCR占位完全没必要折腾多写一个0而已。4. 调频过程中的同步问题与实测数据4.1 影子寄存器写入不等于生效这个坑几乎所有初用者都会踩。通过DMA写进ARR和CCR的其实是预装载寄存器真正参与计数比较的是影子寄存器。影子寄存器的更新发生在更新事件计数器溢出或UG置位时刻。所以你在运行中通过DMA写入新ARR并不会立刻改变频率而是要等下一个更新事件。这本身不是问题因为我们的DMA就是为了卡在更新事件时搬运。但如果你的代码里还有其他软件路径在修改CCR那就必须搞清楚预装载是否使能。预装载未使能时写入CCR会立即生效这在突发模式下可能产生一个与预期不符的脉宽。4.2 ARR变小会不会截断周期会不会造成帧错乱这里是调频过程中最重要的一个现象。比如当前ARR499CNT正计数到300此时DMA把ARR改成了99。硬件会立刻比较CNT(300) ARR(99)于是立即产生更新事件CNT清零当前半周期被截断。这个截断会体现在波形上一个本应500us的周期变成了300us左右就结束了。在变频扫频中这种截断很常见未必是坏事因为变频本身就是要改变下一周期的长度。但要注意后续影响立即更新事件会再次触发DMA请求如果上一次burst还没有结束新的请求会被pending等本次burst结束后紧接着再来一次。这样会导致什么如果数据表里已经是下一帧的数据这个“额外”的请求会把下一帧提前搬入寄存器造成帧序错乱。实际现象是扫频曲线在某一段跳变了然后突然又跳回来整体节奏全乱。规避方式有三个一是帧间平滑让相邻帧的ARR差值不要太大确保写入时CNT大概率小于新ARR减少截断触发。二是在DMA传输完成中断里做保护每次DMA中断后重新对齐帧指针。但这样CPU又会参与进来破坏了全DMA的初衷。三是利用RCR做重复计数让更新事件每N个PWM周期才产生一次这样burst的更新频率降低留给DMA完成上一帧的时间更充足。这个方法在实际工程中很有效我做的LLC电源就是让RCR9每10个周期才更新一次参数频率切换依然平滑但DMA的负载瞬间降到十分之一。4.3 DMA优先级与总线竞争一次不稳定的实测记录我在一套系统里同时跑了TIM1的DMA burst、三路ADC DMA和两个串口DMA。最开始把TIM1的DMA优先级配成了中优先级。示波器上PWM频率偶尔出现“卡顿”用逻辑分析仪长时间抓取后确认是DMA仲裁延迟导致更新事件响应不够及时。实验现象很有意思不是每次切换都卡而是偶尔卡。具体表现是某个PWM周期略微变长大约多了几个时钟周期正好是总线矩阵仲裁等待的时间。定位到问题后把TIM1的DMA通道优先级提到最高串口和ADC降为中优先级卡顿现象彻底消失。原因是定时器DMA每次burst只有3个32位数据即使优先级最高占用总线的时间也只有几十纳秒对ADC采样和串口收发几乎无影响。所以如果系统里DMA通道比较紧张、竞争激烈定时器DMA的优先级一定要给高这个问题在普通DMA场景不明显但在burst场景下会被放大因为burst要求连续几个总线周期不被打断。4.4 初次触发丢失的隐蔽表现前面提到用UG位手动触发一次来拉齐帧指针。但还有一种隐蔽情况如果你用的是HAL库初始化函数内部可能会在DCR配置完成之前就使能了DMA请求导致第一次更新事件发生时DMA还没有完全准备好事件就丢了。我调试时用的土办法在DMA中断里打断点观察DMA计数器CNDTR的值。循环模式下正常情况下每次DMA传输后CNDTR的减少是规律的。如果出现某次CNDTR减得比预期多就说明触发丢失后又补了一次tick帧指针已经错位。另一个调试技巧在数据帧里预留一个magic字段虽然硬件不会读它但可以通过DMA把它写进某个不用的寄存器比如TIM1的保留位然后周期性检查这个寄存器的值从而判断当前DMA读到了数据表的哪个位置。这个方法帮我定位过好几次帧错乱的问题。5. 从变频到更复杂的波形多通道与闭环5.1 三相PWM为什么更需要burst电机控制里三路PWM半桥互补输出每一路都需要更新CCR而且三相之间必须严格同步。如果中断里更新三个CCR虽然代码很简单但中断延迟会导致三相切换时刻不一致产生所谓的“相移抖动”。尤其在高转速下这种抖动会变成电机噪音和扭矩纹波。用DMA burst把DBA指向CCR1DBL设为3一次burst就能把CCR1、CCR2、CCR3全部更新。三相之间的切换时刻相差不超过一个总线周期几乎可以忽略。我做过一个测试同样的PWM频率和占空比中断更新和burst更新对比电流波形的高频纹波幅度下降了大约20%电机的啸叫声也明显减弱。5.2 与ADC联动做电流闭环做FOC或者变频压缩机控制时除了输出PWM还需要同步采样电流。常见做法是PWM中心对齐模式下计数器上溢或下溢时触发ADCADC采样完成后用另一个DMA通道把结果搬运到内存控制算法根据电流误差计算新的占空比更新到DMA数据表里。这时的DMA数据表实际上是一个环形缓冲。控制算法在后台往缓冲里写新帧定时器DMA循环读取。需要小心的是如果控制算法更新帧的速度和DMA读表的速度不一致可能出现“读到一半被改写”的问题。解决办法是使用双缓冲算法写完当前帧后切换DMA的内存起始地址到新的缓冲区。这个过程在DMA传输完成中断里做不会造成数据竞争。5.3 什么时候不值得用这套方案不是所有变频PWM都要上DMA burst这个话我得说在前面。如果只是风扇调速几百Hz更新一次频率中断完全够用没必要引入DMA和数据表徒增调试成本。如果PWM频率只有1kHzCPU负担也小中断里直接改改寄存器更直观。另外要注意DMA通道资源。有些芯片DMA通道数量有限一个定时器burst会占掉一个通道如果没有合适的DMA通道可用方案就得重新评估。我遇到过因为DMA通道不够把串口DMA挪走给定时器用结果串口在高波特率下频繁丢数据的尴尬局面。6. 一些实测收尾的经验这套方案我在LAT1202上跑了一个多月后来换到同系列的芯片上代码基本没怎么改。有几个经验值得分享。第一拿到芯片先翻参考手册确认DBA的起始寄存器偏移和DMAR的地址每个芯片虽然总体模型一致但具体的寄存器偏移可能有差异。照着手册把DBA和DBL算好再上电能省很多时间。我吃过一次亏DBA填成了字节偏移0x2C结果数据被分发到了CCR3和BDTRPWM输出直接变成一堆奇怪的窄脉冲我还以为是DMA配置错了排查了整整三天。第二数据表的初始值不要随便填零。如果DMA在定时器启动前就把第一帧数据搬运完帧里的ARR会覆盖你初始化时写的初始值。建议把pwm_table[0]的值直接作为启动参数这样UG触发后第一帧、以及后续每一帧都是你预期内的值。第三逻辑分析仪采样要尽量长时间尤其是扫频场景。变频波形的问题往往是间歇性的短时间采样很难捕捉到那一两个异常周期。我习惯用DMA中断计数来统计总触发次数再和理论上应该发生的更新事件次数做对比偏差就是异常帧或者丢失帧的数量。最后再分享一个小技巧数据表里建议在帧结构末尾加一个shadow字段平时填任意值调试时把它改成一个自增的计数。这样在读取某个寄存器时可以通过这个值判断当前MCU已经跑到了数据表的哪个位置。虽然硬件不会读它但对软件调试对齐非常有用。这套方案比预想中简单硬件把最麻烦的“多寄存器同步更新”都已经解决好了剩下的就是按部就班配置寄存器、合理设计数据帧希望对正在做同类项目的人有帮助。
返回列表