免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

STM32C542 PWM实践:运行中修改频率与占空比的影子寄存器机制

STM32C542 PWM实践:运行中修改频率与占空比的影子寄存器机制 STM32C542是ST C0系列里的性价比担当Cortex-M0内核最高48 MHz一颗料就把LQFP48封装、CAN、多路UART/I2C/SPI和定时器资源堆齐了价格却压得很低。这个系列前几篇我们走完了建工程、点灯和串口打印这篇终于轮到嵌入式开发里跑不掉的功能——PWM输出以及很多人一上手就懵的“运行中修改频率和占空比”。为什么会把修改频率和占空比单独拎出来讲因为单看输出固定PWMCubeMX填几个参数就能跑。但实际项目里几乎没有固定PWM的场景调光要变占空比电机要变频率舵机要变脉宽甚至在运行中两者都要同时变。如果你只是把__HAL_TIM_SET_COMPARE()拿来改占空比改频率时发现波形乱了多半是没搞懂寄存器层面的“影子机制”。这篇文章就把这条链路完整走一遍从选型、计算公式、CubeMX配置到代码写法、运行中调参再到上板实测的坑全部展开。1. 先从这颗芯片说起C542做PWM输出的选型逻辑1.1 为什么是C542而不是F103或者G031很多初学者习惯性选F103资源多、教程多但放到今天的成本压力下F103已经谈不上性价比。STM32C0系列是ST面向大批量、低成本场景推出的Cortex-M0产品线C542在C0里属于中高阶配置最高48MHz主频带CAN、带多路定时器封装覆盖从QFN32到LQFP48。做LED调光、小型电机驱动、舵机控制这类PWM应用完全够用。我自己的选型习惯是能在低主频芯片上完成的功能绝不上高主频芯片。C542虽然只有48MHz但内核跑PWM输出这类逻辑根本不需要多少算力反而是定时器外设的丰富程度更关键。C0系列的定时器资源比一些人想象中厚道高级定时器TIM1支持带死区的互补PWMTIM2是32位计数器TIM3、TIM14、TIM16、TIM17是16位计数器。也就是说C542不仅能输出单路PWM做多路、做H桥驱动都有硬件基础。1.2 PWM输出能覆盖哪些典型场景C542的PWM应用场景可以分三个层次看轻量场景LED调光、呼吸灯、蜂鸣器驱动。这类场景对频率要求不高1kHz到5kHz都能跑重点是占空比调节要平滑。中等场景直流电机调速、加热器功率控制。电机驱动需要避开人耳可听范围工程上常用10kHz到20kHz既降低噪音又不至于让功率管开关损耗过大。进阶场景舵机控制、半桥/全桥驱动。舵机要求固定50Hz周期靠脉宽控制角度全桥驱动需要TIM1输出互补PWM并插入死区防止上下管直通。这篇文章还是以最常用的“单路PWM输出动态调频调占空比”为主线把底层机制讲透。后面实验部分会覆盖呼吸灯和舵机电机调速也会给一组可直接用的参数。2. PWM频率与占空比的计算三个寄存器值玩明白一半2.1 频率公式PSC和ARR到底怎么配PWM输出的核心是定时器计数。标准向上计数模式下计数器从0开始递增到ARR时溢出归零然后重新开始。溢出一次就是一个PWM周期。公式只有一条PWM频率 定时器时钟 / ((PSC 1) * (ARR 1))PSC是预分频器ARR是自动重载值两者都是整数寄存器值。为什么都要加1因为这两个寄存器的值是“计数值减1”。PSC47表示48分频ARR999表示计数1000次。举个例子C542的TIM2时钟是48MHz想输出1kHz PWM可以这么算先定PSC 47则计数器时钟 48MHz / (47 1) 1MHz再定ARR 999则PWM频率 1MHz / (999 1) 1kHz这两个值的分配没有唯一解。PSC0、ARR47999同样能得到1kHz48MHz / 48000但两者效果差别很大原因在下一个小节讲。2.2 占空比与分辨率CCR是唯一控制占空比的地方占空比由比较寄存器CCR决定。向上计数时CNT小于CCR输出一种电平CNT大于等于CCR切换另一种电平。占空比 CCR / (ARR 1) * 100%注意这个公式里也有ARR1。所以当ARR改变时即使CCR数值不变占空比也会变。这是很多人改频率时发现“占空比跟着变了”的根本原因。再说分辨率。占空比调节的最小步进是1 / (ARR 1)。如果ARR999占空比步进是0.1%千分之一如果ARR49步进就是2%五十分之一。这就是为什么刚才的问题里PSC和ARR的分配不是随便定的在能实现目标频率的前提下尽量用小的PSC把ARR做大分辨率才够细腻。举个实际体验同样输出20kHz方案A用PSC47计数器时钟1MHzARR49占空比只能按2%跳方案B用PSC3计数器时钟12MHzARR599占空比步进约0.17%。同样是20kHz后者调光顺滑程度天差地别。2.3 修改频率时占空比为什么会跟着变因为占空比是CCR和ARR的比值不是绝对值。改ARR相当于改了分母CCR不变的情况下比值自然变化。要保证改频率后占空比不变必须同步更新CCR新CCR (旧CCR / 旧ARR1) * (新ARR1)举个例子原先是1kHzPSC47ARR999占空比50%对应CCR500。现在要把频率提到2kHz如果仍保持PSC47新ARR499想让占空比依然50%新CCR就该等于 (500 / 1000) * 500 250。这个换算逻辑不仅在代码里要用到CubeMX图形化配置时也一样。先确定PSC和ARR再把要的占空比换算成Pulse值填进去。3. CubeMX初始化配置分频、重载、脉宽先填对3.1 时钟树内部HSI还是外部晶振C542内部HSI可以跑到48MHz精度对标称值约±1%左右普通PWM应用足够。如果对频率精度要求高比如需要精准的时间基准再接外部晶振HSE。我个人建议前期调试阶段直接用内部HSI把时钟树里RCC的HSE设为Disable少接两个引脚板子布线和物料成本都能省一点。如果后续发现频率偏差影响业务再切HSE不迟。CubeMX的Clock Configuration里把System Clock设为48MHz即可HSI会自动调整倍频。注意定时器时钟未必等于系统时钟取决于APB预分频。在C542上如果你在CubeMX里看到APB总线是48MHz定时器时钟一般就是48MHz。如果你把系统时钟设为更低频率一定要去Clock Configuration里的APB1 Timer Clocks确认实际值后面所有计算都以这个值为准。3.2 定时器实例与GPIO复用选择C542定时器资源有多路但做PWM实验建议先用TIM2或TIM3配置简单、通道多以后扩展方便。这里以TIM2为例。在CubeMX左侧引脚图中点击TIM2选择Channel1的PWM Generation CH1模式对应引脚会自动分配。TIM2_CH1具体映射到哪个引脚以你的封装和CubeMX自动分配结果为准。C0系列引脚复用通过AFR寄存器配置CubeMX会自己生成你不需要手动改。有一点要自己检查引脚被复用为TIM2_CH1之后同时也会失去普通GPIO功能。如果这个引脚还接了其他外设会冲突。CubeMX在引脚图上会用颜色区分复用状态养成编译前扫一眼Pinout视图的习惯。3.3 参数面板里PSC、ARR、Pulse到底怎么填进入TIM2的Parameter Settings有几个关键项Prescaler (PSC)填47对应48分频计数器时钟1MHzCounter Mode默认Up向上计数Counter Period (ARR)填9991kHz周期Auto-reload preload建议Enable这个会影响后续修改ARR是否立即生效后文详述Pulse (CCR)填500输出50%占空比此时CubeMX右侧会自动预览PWM Frequency显示1.0 kHz占空比约50%。这个预览功能很实用填完参数先看一眼如果和预期不符多半是时钟配错了。然后切到GPIO Settings确认TIM2_CH1引脚的模式是Alternate Function速度建议选High。PWM频率不高时Low也行但选High更稳妥避免高速开关时边沿变缓。4. 代码让PWM跑起来启动、改占空比、改频率的完整写法4.1 HAL库三行代码启动输出CubeMX生成工程后main函数里调用MX_TIM2_Init()完成初始化但此时定时器还没开始输出波形。要真正让PWM从引脚送出去需要在主循环前调用HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);就这么一行。如果你用的是高级定时器TIM1HAL_TIM_PWM_Start内部也会自动打开主输出使能MOE不需要额外操作。但如果你是手动操作寄存器模式TIM1要记得设置TIM1-BDTR | TIM_BDTR_MOE;否则引脚上永远没波形这是很多人踩过的坑。4.2 修改占空比首选HAL宏HAL库提供了标准宏__HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 300); // 占空比变为30%第二个参数是通道第三个参数是CCR值范围0到ARR。CCR0输出一直为无效电平CCRARR输出一直为有效电平。注意不要超过ARR超过以后PWM输出会变成恒高PWM mode 1或恒低PWM mode 2行为不可控。底层本质是修改TIM2-CCR1寄存器。这个值在PWM模式下受预装载机制控制写入后会等合适时机生效不会在一个周期中间突然跳变。4.3 修改频率改ARR还是改PSC修改频率有两个切入点ARR或PSC。两者效果不同适用场景也不同。改ARR适用于小范围精调。比如1kHz要微调到1.1kHz改PSC可能一档就跳太多改ARR可以精确到小数。代码__HAL_TIM_SET_AUTORELOAD(htim2, 899); // 频率变为1MHz/900 ≈ 1.111kHz // 同时重算CCR保持占空比 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 450); // 50%改PSC适用于大范围粗调。比如从1kHz跳到10kHz可以PSC从47改为4计数器时钟从1MHz变成9.6MHzARR不变频率直接提高9.6倍__HAL_TIM_SET_PRESCALER(htim2, 4); // 计数器时钟 48MHz/5 9.6MHzARR999时频率约9.6kHz // 同时需要重设CCR以调整占空比这里有个关键点PSC在STM32的定时器里是有预装载特性的写入的新PSC值不会立即反应到计数器时钟上而是要在**更新事件UEV**发生时才会真正加载。如果你的代码写完PSC立刻去读频率发现没变这是正常现象。同理ARR是否立即生效取决于前文说的ARPE位。CubeMX默认把Auto-reload preload设为Enable这时ARR也有影子缓冲新值要等下一个更新事件才生效。4.4 封装一个频率和占空比同时设置的函数实际项目中很少单独改频率或单独改占空比。更常见的需求是“给我一个函数输入频率和占空比PWM直接输出对应波形。”我建议封装一个通用函数void PWM_SetFreqDuty(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t freq_hz, float duty_percent) { uint32_t psc, arr, ccr; uint32_t timer_clock 48000000; // C542定时器时钟48MHz uint32_t counter_freq; if (freq_hz 0) return; if (duty_percent 0.0f) duty_percent 0.0f; if (duty_percent 100.0f) duty_percent 100.0f; // 先尝试PSC47计数器时钟1MHz分辨率最高 psc 47; counter_freq timer_clock / (psc 1); arr counter_freq / freq_hz - 1; // 如果算出来的ARR超出16位范围说明目标频率过低需要增大PSC if (arr 65535) { psc (timer_clock / freq_hz) / 65536; counter_freq timer_clock / (psc 1); arr counter_freq / freq_hz - 1; } ccr (uint32_t)((float)(arr 1) * duty_percent / 100.0f); __HAL_TIM_SET_PRESCALER(htim, psc); __HAL_TIM_SET_AUTORELOAD(htim, arr); __HAL_TIM_SET_COMPARE(htim, Channel, ccr); }调用方式PWM_SetFreqDuty(htim2, TIM_CHANNEL_1, 1000, 50.0f); // 1kHz, 50% PWM_SetFreqDuty(htim2, TIM_CHANNEL_1, 20000, 40.0f); // 20kHz, 40%函数里固定timer_clock是48MHz如果系统时钟改了记得同步修改。这个函数在绝大多数16位ARR场景下够用。连续调用时由于PSC和ARR都有预装载机制新参数会在下一个PWM周期整体切换波形不会产生半周期毛刺前提是你没有关闭ARPE。5. 运行时改参不生效预装载/影子寄存器才是关键5.1 影子寄存器到底是个什么机制很多初学者卡在这里代码明明写了__HAL_TIM_SET_AUTORELOAD()示波器一看频率纹丝不动。这不是宏写错了而是根本没理解“写寄存器”和“寄存器生效”是两回事。定时器内部ARR、CCR、PSC这些寄存器背后都有一层“影子寄存器”。你通过总线写进去的值先进入预装载寄存器影子寄存器里的值才是硬件实际使用的值。预装载寄存器到影子寄存器的搬运发生在更新事件发生时。用生活化类比预装载寄存器是“候选名单”影子寄存器是“实际值班表”。你在办公室里改了候选名单写预装载值班表影子要等下次排班周期更新事件才换。好处是如果更新事件是周期性的所有改动都在周期边界统一生效不会在两个周期中间出现一个异常短或异常长的脉冲。5.2 ARPE位决定ARR改动能多快生效ARR是否使用预装载机制由ARPE位Auto-reload preload enable控制ARPE写入ARR后的行为适用场景1Enable下一个更新事件才生效周期边界切换运行中改频率不容易出毛刺0Disable立即写入影子寄存器需要立即响应的场合但可能产生畸形周期CubeMX里默认Enable这是最推荐的。PSC则没有这个开关它一直具备预装载能力所以改PSC后必须等更新事件。CCR也有类似机制。PWM模式下修改CCR不一定等新的更新事件同一次比较匹配发生后也会加载新值。但在使用预装载机制后最安全的表述是新值不会造成当前周期的占空比异常跳变会在“合适的时刻”生效。5.3 想让新频率立即生效手动产生更新事件有些场景你不想等当前周期结束比如用户按键切换转速希望立刻生效。可以通过软件置位UG位产生更新事件__HAL_TIM_SET_AUTORELOAD(htim2, 499); __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 250); __HAL_TIM_GENERATE_UPDATE(htim2); // 强制产生更新事件但要提醒UG位会同时更新所有带预装载的寄存器包括PSC、ARR、CCR同时还会把计数器CNT清零并可能触发更新中断。如果你的定时器中断里做了周期相关的事情比如ADC采样时刻控制强制UG会让那个逻辑提前跑一次。用之前想清楚副作用。更推荐的做法是直接调用4.4节封装好的函数连续设置PSC、ARR、CCR后让硬件自动在下一个周期边界统一切换。对于电机、灯效这类应用晚一个PWM周期几十微秒根本感知不到没必要用UG强行立即生效。6. 上板实测的典型问题与排查思路6.1 引脚量不到波形先查时钟再查复用这是最典型的问题代码逻辑看起来没问题示波器探针一放上去就是一条平线。我的排查固定顺序是查时钟进入调试器看TIM2-PSC和TIM2-ARR是不是预期值再看RCC寄存器里TIM2时钟是否使能。如果PSC读到的是0说明CubeMX生成的初始化根本没执行先查MX_TIM2_Init()是否被调用。查GPIO复用看引脚对应的AFR寄存器是否设置正确GPIO模式是不是GPIO_MODE_AF_PP而不是GPIO_MODE_OUTPUT_PP。查PWM是否启动确认执行了HAL_TIM_PWM_Start()。很多人只调用了MX_TIM2_Init()就去看波形当然没有输出。查通道确认示波器探针接在TIM2_CH1对应的引脚上别接错引脚。用逻辑分析仪抓PWM比示波器更直观几十块的小工具就能看频率和占空比。调试阶段我基本是逻辑分析仪常挂波形一抓频率对不对、占空比有没有跳变一目了然。6.2 改了频率但波形没变十有八九是没等更新事件我在4.3节写过PSC写入后要等更新事件才生效。如果你在主循环里连续写PSC和ARR然后又立刻用示波器看波形看到的仍然是旧频率。更隐蔽的情况是你改了ARR但CubeMX生成代码把ARPE设为Disable此时写ARR立即生效但占空比同时变了。所以遇到“频率没变但占空比变了”这种怪现象先检查ARPE。此外如果程序在定时器更新中断里频繁写ARR要注意用户代码和中断之间的竞争。比如主循环定时写入频率中断里也修改ARR两个写操作可能相互覆盖。这种情况建议只保留一个地方负责写ARR或者用标志位传递转速变更请求由中断统一执行。6.3 高级定时器TIM1没波形MOE没使能TIM1是高级定时器输出互补PWM前必须使能主输出MOE位。用HAL库时HAL_TIM_PWM_Start()已经帮你做了但如果你的工程里手动控制寄存器或者调用的是HAL_TIM_PWM_Stop()之后再用寄存器方式重新启动就可能漏掉MOE。// 手动操作TIM1寄存器时必备 TIM1-BDTR | TIM_BDTR_MOE; // 主输出使能另外TIM1的引脚默认可能与调试端口复用或者与其他外设冲突。C542的TIM1_CH1通常可以映射到特定引脚具体看CubeMX分配不要照搬F103的老经验。6.4 停止再启动失败PWM输出状态没复位主循环里做这样的操作HAL_TIM_PWM_Stop()然后过一会儿再HAL_TIM_PWM_Start()第二次启动后波形不出来了。这在高级定时器上容易出现原因是MOE或通道使能位没有完全复位或者更新标志没有清除。稳妥做法是停止后加一段小延时再启动前调用__HAL_TIM_CLEAR_FLAG(htim1, TIM_FLAG_UPDATE); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);如果是手动寄存器方式检查TIM1-CCER、TIM1-BDTR的MOE位确实被清掉了再重新配置。7. 三种典型负载的实验记录呼吸灯、舵机与直流电机7.1 呼吸灯用占空比线性变化验证基本功能最简单也最有效的PWM验证实验就是驱动一个LED做呼吸灯。C542的GPIO驱动能力强一点但直接推LED还是要串一个220Ω到1kΩ限流电阻具体阻值按LED额定电流算。1kHz PWMARR999主循环里CCR从0递增到999再递减回0。占空比变化频率大约2秒一个来回代码while (1) { for (uint16_t i 0; i 999; i 5) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, i); HAL_Delay(5); } for (uint16_t i 999; i 5; i - 5) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, i - 5); HAL_Delay(5); } }跑起来你会发现一个现象线性递增占空比LED亮度变化并不线性暗处变化明显亮处变化很小。这是人眼对亮度的感知近似对数的特性不是芯片或者代码的问题。想做出更舒服的呼吸效果可以对CCR做gamma校正把线性值映射成接近指数变化的序列。数据量不大时可以直接在代码里放一张查表。7.2 舵机控制50Hz固定频率只变脉宽标准舵机比如SG90、MG995对PWM的要求和LED完全不同频率必须稳定在50Hz左右占空比本身不重要重要的是高电平持续的时间也就是脉宽。0.5ms对应0°1.5ms对应90°2.5ms对应180°。用C542的TIM2配置50HzPSC 95计数器时钟 48MHz / 96 500kHzARR 9999PWM频率 500kHz / 10000 50HzCCR 脉宽时间 * 500kHz各角度对应的CCR值角度脉宽CCR占空比0°0.5ms2502.5%90°1.5ms7507.5%180°2.5ms125012.5%写代码时直接用CCR控角度比用百分比直观得多// 舵机转到90度 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, 750);舵机对频率抖动不敏感但对周期稳定性有要求。C542的定时器由硬件产生PWM不占用CPU周期非常稳定比用GPIO模拟PWM的方案可靠得多。这里也顺带说明一个误区有人试图用主循环延时加翻转GPIO模拟50Hz结果发现舵机抖动。原因是中断、循环长短都会影响周期。硬件PWM的伟大之处就在于此配置好之后CPU哪怕去跑别的任务波形依然稳定。7.3 直流电机调速频率选择与死区问题直流有刷电机调速是PWM频率选择最有代表性的场景。频率太低能听到电机啸叫电流纹波也大频率太高MOS管开关损耗上升驱动芯片发热。工程上常用10kHz到20kHz。以20kHz为例PSC 3计数器时钟 48MHz / 4 12MHzARR 599PWM频率 12MHz / 600 20kHzCCR 占空比 * 600这种参数下占空比步进约0.17%调速足够细腻。做电机驱动还回避不了一个词死区。如果你用的是集成电机驱动芯片比如DRV8833、TB6612芯片内部已经处理了死区MCU不需要管。但如果你用分立MOS管搭H桥TIM1的互补PWM就派上用场了TIM1_CH1和TIM1_CH1N输出互补波形CubeMX的Break and Dead-Time设置里填死区时间死区时间必须大于MOS管关断时间否则上下管有一瞬间同时导通轻则发热重则炸管C542的TIM1能直接硬件插入死区不需要软件干预。死区时间的具体数值和高速时钟有关CubeMX参数面板会以纳秒为单位实时显示直观好用。这里提醒一句拿示波器量互补波形时先确认探头共地双通道同时抓否则看不到死区细节。7.4 中心对齐模式什么时候值得用C542的定时器除了边沿对齐向上计数还支持中心对齐模式向上/向下计数。中心对齐模式下计数器先从0数到ARR再从ARR数到0一个完整PWM周期包含两次溢出。这种模式有两个特点PWM频率 定时器时钟 / (2 * (PSC1) * (ARR1))注意多乘了一个2输出波形的上升沿和下降沿相对周期中心对称中心对齐模式在电机控制里很常见特别是配合互补PWM使用因为对称波形能减小输出谐波。同时计数器的三角波形状也让ADC采样时机更容易安排——你可以在计数器到达ARR或到达0时触发ADC采样获得固定的电气角度位置。这篇先不展开细节但如果你以后要做FOC或者无刷电机控制一定会回来用这个功能。做电机驱动如果只想要简单调速边沿对齐足够中心对齐不是必须。我的建议是先把边沿对齐玩熟理解频率、占空比、预装载这些核心概念再去碰中心对齐和死区。最后说点我自己的调试习惯。PWM这种外设逻辑不复杂出问题大多出在配置细节上。按“时钟→复用→参数→时序”四步排查基本能解决99%的问题先确认定时器时钟频率再确认引脚复用再看PSC/ARR/CCR数值是否吻合最后确认预装载和更新事件是不是按预期触发。千万别在波形不对的时候上来就怀疑芯片C542的定时器外设很稳定绝大多数问题出在拿着F1系列的经验来套C0系列。手边常备逻辑分析仪抓一次波形很多疑问就消失了。
返回列表