
前面几篇已经能把 STM32C542R 的工程建起来、GPIO 点灯、串口打印跑通这一篇进入实际控制里最常用的功能输出 PWM以及运行时修改频率和占空比。PWM 用在哪基本不用多解释电机调速、舵机控制、LED 亮度、蜂鸣器音调、DC-DC 占空比调整都靠它。很多新手第一次能配置出固定 PWM但要做到“频率可变、占空比可调”时往往不知道到底改哪个寄存器或者改了之后不生效甚至波形直接消失。这篇就以 STM32C542R 为对象用 STM32CubeMX 生成 HAL 工程把 CubeMX 配置、HAL 库启动、动态修改、实测验证、问题排查完整拆一遍。我个人的建议是不要先急着把参数调到“看起来对”而是先把 PWM 产生的原理和定时器里几个关键寄存器的关系搞清楚。否则后面调频率、调占空比时很多报错和异常都无从下手。1. 先把PWM这件事拆成三个环节1.1 频率、占空比和定时器计数的对应关系PWM 的本质是让定时器不断从 0 往上计数同时把计数值和一个设定值做比较。常见的是向上计数模式计数器 CNT 从 0 累加到自动重装值 ARR然后清零重新计数。比较寄存器 CCR 是“阈值”当 CNT 小于 CCR 时引脚输出一种电平当 CNT 大于等于 CCR 时引脚输出另一种电平。这样输出引脚上就形成了周而复始的高电平和低电平波形。这里有两个关键角色ARR 决定计数周期也就是 PWM 周期。CCR 决定高电平持续多长时间也就是占空比。用公式表达就是PWM 频率 定时器时钟频率 / ((PSC 1) * (ARR 1))占空比 CCR / (ARR 1)注意两边都有“1”因为计数器是从 0 开始计数的。例如定时器时钟是 80MHz要得到 1kHz 的 PWM可以让 (PSC 1) * (ARR 1) 80000。常见分法PSC 79ARR 999那么频率正好是 80MHz / (80 * 1000) 1kHz。如果 CCR 500占空比就是 500 / 1000 50%。很多人会忽略这个“1”导致计算出来的频率差一点。虽然肉眼可能看不出来但用示波器测频率或者做需要精确周期的控制时误差就会体现出来。1.2 该选哪个定时器和哪个通道STM32 系列里的定时器资源通常分几类高级定时器、通用定时器、基本定时器以及可能存在的低功耗定时器 LPTIM。STM32C542R 具体有哪些定时器以 CubeMX 里实际能看到的为准。大多数情况下输出简单 PWM 用通用定时器就够了例如 TIM3、TIM4。需要互补输出、死区插入、刹车输入等功能时才需要选用高级定时器。选择定时器的基本原则是先看引脚。CubeMX 里选中某个定时器通道后会自动分配到对应的 GPIO但要注意这个引脚是否被调试口、外部晶振或者其他外设占用。再看频率范围。如果目标 PWM 频率很低比如 50Hz 舵机控制需要 ARR 和 PSC 的分频比很大。如果定时器是 16 位ARR 最大只能到 65535那么频率下限就要靠 PSC 来压低。最后看是否有多通道需求。如果一路 PWM 只控制一个负载任意定时器通道都可以如果要多路控制电机最好提前规划好定时器通道和引脚映射。1.3 常规场景下的参数范围不同应用对 PWM 频率要求完全不一样LED 呼吸灯一般 200Hz 到 1kHz人眼就看不到闪烁了。舵机控制典型 50Hz周期 20ms占空比 5% 到 10% 对应不同角度。电机驱动常见 10kHz 到 30kHz要避开人耳可听范围同时兼顾开关损耗。蜂鸣器发声频率跟随音符变化需要动态修改频率。开关电源频率更高但一般不会直接拿通用定时器去做更多是专用控制器。我建议第一次测试时选 1kHz、50% 占空比因为频率不高不低用示波器、逻辑分析仪甚至简单用 LED 都能直观验证。先把固定 PWM 跑出来再研究动态修改频率和占空比。2. CubeMX配置引脚和定时器参数2.1 新建工程并配置时钟打开 STM32CubeMX新建 STM32C542R 工程时第一步是先确认系统时钟和定时器时钟来源。在“System Core RCC”里根据板子晶振情况选择外部高速晶振或内部时钟。如果你用的是内部时钟时钟树里也要确认 APB1、APB2 总线时钟是多少因为定时器时钟通常来自 APB1 或 APB2。这里最容易忽略的是定时器时钟不一定等于总线时钟。在某些时钟配置下APB 预分频器不为 1 时定时器时钟会翻倍。所以实际计算 PSC 和 ARR 时应该以 CubeMX 时钟树里显示的具体定时器时钟频率为准不要直接拿主频去算。我一般会先在 Clock Configuration 页面看清楚定时器挂在哪条总线上并把数值记下来。后面调试频率不准时这个值就是排查的起点。2.2 定时器PWM参数配置明细在 CubeMX 的 Pinout Configuration 页面找到要使用的定时器比如 TIM3。先勾选 Channel1 为 PWM Generation CH1然后在 Parameter Settings 里填写参数。以一个目标为 1kHz、占空比 50% 的示例来演示假设定时器时钟是 80MHz可以这样配置参数名称配置值说明Prescaler (PSC)79分频系数实际分频为 PSC 1Counter ModeUp向上计数Counter Period (ARR)999自动重装值Auto-reload preloadEnableARR 预装载使能Pulse (CCR)500初始比较值决定初始占空比PWM ModeMode 1计数小于 CCR 时输出有效电平CH PolarityHigh有效电平为高或按需要选择 Low如果你的定时器时钟不是 80MHz一定要按公式重新计算。比如定时器时钟是 64MHz想得到 1kHz可以取 PSC 63ARR 999这样 64MHz / (64 * 1000) 1kHz。关键是 (PSC 1) 和 (ARR 1) 的乘积要满足频率要求。这里的 Counter Mode 通常选 Up也就是向上计数。实际工程里如果用到中心对齐模式也可以配置但初学者建议先用向上计数模式逻辑更清晰。PWM Mode 选 Mode 1 还是 Mode 2只影响输出有效电平的区间。如果极性 HighMode 1 通常是 CNT 小于 CCR 时高电平Mode 2 则相反。对于大多数场景Mode 1 High 已经足够。Pulse 也就是 CCR 的初始值建议不要设成 0否则 PWM 一启动就是全低电平很难判断到底是没启动还是占空比太小。先设一个中间值比如周期的 50%这样启动后立刻能看到方波。2.3 生成代码后首先要确认的三个位置CubeMX 生成代码后不要直接编译下载就完事先打开代码确认三个位置。第一在 tim.c 文件里找到 MX_TIM3_Init()确认结构体里的参数和 CubeMX 配置一致。PWM 输出不正常的很多情况其实是 CubeMX 配置界面和实际生成代码不一致。第二在 main.c 里检查定时器的 FSP/HAL 初始化顺序。HAL 库里定时器外设时钟使能由 CubeMX 自动生成但如果你手动关过时钟或其他外设占用了引脚PWM 仍可能出现问题。第三在 HAL_TIM_MspPostInit 或 GPIO 初始化部分确认引脚复用配置是否正确。PWM 信号要输出到引脚GPIO 必须被初始化为“复用推挽输出”而不是普通推挽输出。如果这里被莫名改掉引脚上就不会有波形。做完这三处检查基本可以排除大部分初始化遗漏。3. HAL库输出PWM的启动与停止3.1 启动单通道PWMCubeMX 生成的初始化函数会在 main 里被调用但定时器 PWM 输出还需要在应用代码里手动启动。HAL 库对应函数是HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);调用时机一般放在 while(1) 之前PWM 一旦启动就会持续输出不需要每次都去调用。如果只需要输出一段时间然后关闭调用HAL_TIM_PWM_Stop(htim3, TIM_CHANNEL_1);停止后引脚会恢复到默认状态。要注意停止 PWM 并不代表引脚变成低电平不同配置下可能是保持当前电平不能简单认为停止就是输出 0。如果需要安全电平应该在停止前先把 CCR 设为 0或者手动把引脚拉低。高级定时器和通用定时器在 HAL 库里的启动方式基本一致但高级定时器多了一个主输出使能问题。如果用的是 TIM1、TIM8 这类高级定时器启动后没有波形优先检查主输出使能位 MOE 是否打开。CubeMX 生成的代码里通常有对应处理但如果手工精简过工程容易把这一步删掉导致无输出。3.2 主循环里动态修改占空比PWM 启动之后最常用的操作就是改占空比。占空比由 CCR 决定HAL 库提供直接写入 CCR 的方式__HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 750);把 CCR 改成 750 后对于 ARR 999 的配置占空比就是 750 / 1000 75%。这个操作非常轻量如果使能了预装载新的 CCR 会在下一个计数周期开始生效不会突然改变当前周期的波形所以适合在控制循环里频繁调用。下面是一个呼吸灯效果的最小示例while (1) { for (uint32_t duty 0; duty 999; duty 5) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, duty); HAL_Delay(5); } for (uint32_t duty 999; duty 0; duty - 5) { __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, duty); HAL_Delay(5); } }注意这里的循环上界是 ARR 999所以 CCR 的最大值不要超过 999。实际工程里建议做一次限幅避免把 CCR 设置到 ARR 之上否则占空比会一直为 100%波形看起来像直流电。3.3 动态修改频率为什么比改占空比更容易出问题修改占空比只改 CCR不影响周期所以相对安全。修改频率则要动 ARR 或 PSC而这个操作带来的影响会明显更多。先说改 ARR。ARR 是自动重装值直接决定 PWM 周期。如果在运行中把 ARR 从一个较大值突然改小而当前 CCR 还大于新 ARR那么占空比会变成 100%直到你同时修改 CCR。反过来把 ARR 突然改大又可能导致一次很长的高电平或低电平波形中间出现异常。再说改 PSC。PSC 是预分频改它也会改变频率但 PSC 往往带缓冲寄存器真正生效可能在更新事件之后。如果你在运行中直接写 PSC实际波形变化时间点和代码执行时间点不一定一致容易造成频率跳变或者瞬间不连续。所以我的经验是如果应用只是要求“固定频率、动态占空比”运行时只修改 CCR。如果确实要动态改频率比如蜂鸣器播放旋律或可调信号发生器建议把频率切换设计成“先停止再改 ARR 和 PSC再重新启动”的流程。虽然会有短暂中断但波形更可控排查也更简单。如果要求无间隙变频还要考虑 ARR 和 CCR 的同步更新、预装载使能、定时器更新事件的处理复杂度会高很多。普通项目能用停/启方式解决就尽量用停/启方式。4. 实测验证从示波器看波形再到LED和电机4.1 验证工具和接线PWM 是否输出正确不能光靠“感觉”。最直接的验证工具是示波器或逻辑分析仪。没有示波器的话也可以先用 LED 看亮度变化、用蜂鸣器听音调变化但频率和占空比的精确值还是需要仪器确认。接线方面把 PWM 输出引脚用杜邦线连到示波器探头GND 要共地。最好不要直接带电机、加热丝这类大功率负载测试先把空载波形测出来确认频率和占空比正确后再接负载。否则一旦 PWM 初始状态是意外的高电平负载可能会直接动作存在安全风险。4.2 验证步骤与期望波形我一般按下面这个顺序验证先使用 1kHz、50% 占空比的配置启动 PWM。示波器上应该看到周期性方波周期约 1ms高电平时间约 0.5ms。用示波器的测量功能读取频率如果读数接近 1kHz说明 PSC 和 ARR 的乘积正确。读取占空比读数如果接近 50%说明 CCR 初始值正确。用 __HAL_TIM_SET_COMPARE 修改 CCR 为 250占空比应变为约 25%。再次修改 CCR 为 750占空比应变为约 75%。如果发现占空比读数和预期差很多先检查 CCR 是否超过了 ARR再检查计数模式和极性配置。波形输出正常之后才建议接负载。4.3 怎样在不停机的情况下调节实际项目里用户或主控经常需要在运行过程中调整 PWM。最简单的做法是利用按键或串口。比如用两个按键一个加占空比一个减占空比或者通过串口接收数字直接设置 CCR 和 ARR。下面是一个按键调整占空比的伪代码思路if (KEY_UP_PRESSED) { current_duty 50; if (current_duty 999) { current_duty 999; } __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, current_duty); } if (KEY_DOWN_PRESSED) { if (current_duty 50) { current_duty - 50; } else { current_duty 0; } __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, current_duty); }注意按键要处理抖动否则一次按下可能连续触发多次调整。这里只是演示结构真正项目里建议用带防抖的按键扫描或使用串口指令避免输入不稳定。修改频率时同样建议先做范围检查防止 ARR 和 PSC 被设置成无效组合。5. 常见问题和排查顺序5.1 现象排查表PWM 相关的问题现象不同排查路径也不同。下面是我实际调试中经常遇到的清单现象可能原因排查顺序完全没有波形定时器未启动、通道错误、GPIO复用不对、引脚被占用先确认 HAL_TIM_PWM_Start 是否调用再确认通道号再查 GPIO 复用最后看引脚是否有其他外设占用引脚一直是高或低CCR 为 0 或超过 ARR、极性反了、PWM 模式选错先把 CCR 设为中间值检查极性确认 PWM Mode 1/2 是否符合预期频率和计算值不一致定时器时钟算错、PSC/ARR 公式忘加 1、时钟树倍频不对重新确认定时器时钟把 PSC 和 ARR 分别代入公式计算修改占空比不生效没使能预装载、CCR 写入失败、在错误的地方调用确认 Auto-reload preload 为 Enable检查是否操作了正确的定时器和通道修改频率后波形异常ARR 改太小、CCR 没同步更新、PSC 生效时机不对先把 CCR 限制在新 ARR 范围内必要时先停止 PWM 再改参数运行一段时间后无输出定时器更新中断冲突、引脚被复用、HAL_TIM_PWM_Stop 被意外调用查代码里是否有人调用了停止函数查引脚是否被其它外设切换查中断优先级是否互相影响5.2 多个PWM通道同时输出一个定时器可以输出多路 PWM这些通道共享同一个 ARR 和 PSC也就是说频率必须相同但每个通道可以有不同的 CCR也就是不同占空比。使用多通道时每个通道都要分别调用 HAL_TIM_PWM_Start修改时也要分别指定通道HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 200); __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, 500);如果多路 PWM 需要不同的频率那就不能共用一个定时器要换到不同的定时器或者在一个定时器里把计数模式改为高级应用方式。对于大多数入门和中等复杂度的项目多路 PWM 共用定时器、频率相同、占空比不同已经足够满足需求。5.3 这类项目最容易忽略的两个坑第一个坑是预装载没有使能。CubeMX 参数里默认可能有 Auto-reload preload 和 PWM preload如果没使能运行中修改 ARR 或 CCR可能不会在预期时间生效某些芯片行为会显得“改了没反应”。调试动态修改时先确认预装载配置。第二个坑是低频场景下 16 位计数器的限制。如果定时器是 16 位ARR 最大是 65535。要产生 1Hz 甚至更低的 PWM如果 PSC 也到了 65535 上限可能组合不出目标频率。这时就需要更换定时器或者接受步进误差。解决方法是先算一下最低能达到的频率再决定参数组合。另外实际计算频率时不少人会直接把 PSC 和 ARR 设置为目标值但忘了 PSC 和 ARR 是“减一”写入所以频率会偏大。养成习惯每个参数都按“写入值 分频比 - 1”的思路去写。6. 从“能出波形”到“稳定输出”的几点建议6.1 学习场景和生产场景的差别学习时点亮一个 LED、让蜂鸣器响PWM 怎么简单怎么来。但一旦进入实际控制就要开始考虑更多东西。驱动电机时启动占空比不要直接从 100% 起步建议从 0 开始慢慢增加否则冲击电流很大。停止时也要先让占空比降到 0再关闭 PWM防止负载突然失去控制。使用半桥或全桥驱动电路时还要考虑死区时间避免上下管直通这通常需要高级定时器的互补 PWM 功能。如果 PWM 引脚要连到外部设备建议看一下引脚的驱动能力和连接器是否可能短路。做样机时可以先串一个限流电阻避免因为代码错误把引脚拉高后烧坏外部模块。6.2 把PWM配置封装成可维护接口当项目里 PWM 通道变多到处直接写 __HAL_TIM_SET_COMPARE 会让代码不好维护。我习惯用一组结构体来管理各路 PWMtypedef struct { TIM_HandleTypeDef *htim; uint32_t channel; uint32_t max_arr; uint32_t current_duty; } pwm_handle_t;然后封装两个接口void pwm_set_duty(pwm_handle_t *pwm, uint32_t duty_percent) { uint32_t ccr pwm-max_arr * duty_percent / 100; if (ccr pwm-max_arr) { ccr pwm-max_arr; } pwm-current_duty duty_percent; __HAL_TIM_SET_COMPARE(pwm-htim, pwm-channel, ccr); } void pwm_set_freq(pwm_handle_t *pwm, uint32_t freq_hz, uint32_t timer_clock) { // 根据 timer_clock 和 freq_hz 重新计算 PSC/ARR // 同时需要限制 CCR 不超过新 ARR }这样调用方不需要关心底层定时器细节只需要传入占空比或频率。这里给出的只是接口思路实际计算 PSC 和 ARR 时还要结合定时器位数、时钟频率、目标频率范围做校验。6.3 如果你接下来要做PID调速或PWM触发ADCPWM 最常见的进阶应用是电机调速闭环。此时改动占空比的频率通常很快主循环里加 HAL_Delay 就太粗糙了一般会在定时器更新中断里更新 CCR或者由控制频率更快的任务来执行。修改占空比时仍然要注意 CCR 的限幅和生效时机。还有一个常见场景是 PWM 触发 ADC 采样。比如在 PWM 周期的固定时刻采样电流或电压以避免开关噪声干扰信号。这种玩法需要关注定时器触发输出和 ADC 触发源的配置不能简单地让 ADC 一直连续采样。具体做法可以在之后的文章里展开但前提是先把 PWM 本身输出稳定并且理解 ARR、CCR 和更新事件之间的关系。如果你只是想做一个简单的信号发生器PWM 加 RC 滤波也能得到近似模拟波形但精度和纹波都有限别把它当成高精度 DAC 用。真正做闭环控制时还是要关注输入信号的带宽和 PWM 更新速率是否匹配。最后留几个我一定会检查的点第一定时器时钟到底是多少第二ARR 和 PSC 计算时有没有漏掉“1”第三运行时改参数有没有做边界限制第四预装载有没有使能第五引脚有没有被其它外设冲突占用。把这几项养成习惯PWM 相关的问题基本能解决一大半。