这次我们直接看 PWM 输出。STM32 开发里最常用、出镜率最高的外设定时器绝对是其中之一而定时器的核心玩法就是 PWM。平时写呼吸灯、控制舵机角度、给直流电机调速、驱动蜂鸣器发声底层基本都是同一套 PWM 配置思路。这篇以 STM32C5A3R 为例通过 STM32CubeMX 加 HAL 库完整跑一遍 PWM 输出再重点解决两个高频需求运行中怎么改频率运行中怎么改占空比。文章不会绕太多概念直接给配置方法、计算公式、关键代码和示波器验证步骤。会覆盖几个最容易踩的坑高级定时器主输出没使能导致没波形、ARR 和 CCR 没有同步导致占空比突变、GPIO 输出速度不够导致波形边沿失真、修改 PSC 后没有立即生效导致新频率没变化。看完这篇你可以照着在 STM32C5A3R 上把 1kHz、20% 占空比的 PWM 调成 5kHz、60%并且完全清楚每个寄存器改的是什么。1. 核心能力速览能力项说明主控型号STM32C5A3R输出方式定时器 PWM 模式GPIO 复用推挽输出配置工具STM32CubeMX 生成初始化代码软件框架STM32Cube HAL 库主要功能单路/多路 PWM 输出运行中修改频率与占空比依赖外设TIMx、GPIO可选串口、DMA、ADC、按键适用场景LED 调光、蜂鸣器、舵机控制、直流电机调速、电源类控制验证工具示波器、逻辑分析仪、串口打印、LED 亮度观察关键公式PWM 频率 定时器时钟 / ((PSC 1) * (ARR 1))占空比 CCR / (ARR 1)常见坑高级定时器需要使能主输出ARR 与 CCR 需要同步修改这里先强调一点STM32C5A3R 的具体定时器时钟、引脚复用关系、定时器位宽都要以你手上的参考手册和 CubeMX 实际生成结果为准。文章里用到的时钟频率只是为了把计算过程演示清楚实际工程请替换成自己的时钟配置值。2. 适用场景与使用边界2.1 适合什么场景PWM 适合做所有“周期性输出高低电平、通过调节占空比控制平均功率”的任务。常见的有LED 亮度调节比如呼吸灯、跑马灯。蜂鸣器发声通过改变 PWM 频率改变音调。舵机控制50Hz 左右的周期下通过不同脉宽控制角度。直流电机调速通过占空比改变平均电压配合 H 桥实现正反转。加热器功率控制PWM 输出经过驱动电路控制加热丝。通过 RC 滤波把 PWM 近似成 DAC 输出用于简易音频或信号发生。STM32 的定时器不只是输出单一 PWM还可以输出多路 PWM、互补 PWM、带死区 PWM甚至通过定时器的更新事件触发 ADC 采样。在电机控制类项目里PWM 触发 ADC 再配合故障保护是非常典型的组合用法。2.2 不适合什么场景PWM 不是万能的。如果需要极高分辨率的占空比或者需要多通道严格相位同步普通定时器 PWM 可能就不够。此时要看芯片是否有高级定时器、HRTIM 这种高分辨率定时器或者使用定时器的主从同步模式。另外PWM 只是控制信号不能直接驱动大功率负载。直接拿 MCU 引脚去驱动直流电机、电磁阀、加热丝会导致引脚过流、MCU 发热甚至烧毁。PWM 输出后必须经过晶体管、MOSFET、驱动芯片、光耦隔离等功率放大电路。2.3 使用边界与安全提醒调试电机驱动、电源类电路时注意电气隔离不要带电插拔。用示波器测量驱动电路时确认探头接地可靠避免短路。不要用 MCU 引脚直接驱动感性负载必须加续流二极管或专用驱动芯片。涉及商业产品时注意 PWM 输出控制逻辑是否满足安全规范比如超温、过流时能否强制关断输出。如果项目中涉及人脸、声音、图像等素材必须获得合法授权这篇虽然是纯粹的 MCU 外设教程也一并提醒合规边界。3. 环境准备与前置条件3.1 硬件准备STM32C5A3R 核心板或最小系统板。ST-Link / J-Link 调试器用于下载和在线调试。示波器或逻辑分析仪用于验证频率和占空比。没有示波器也可以用 LED 亮度、万用表频率档做粗测。USB 转串口模块如果要用串口指令动态修改 PWM 参数。杜邦线、面包板、LED、限流电阻等基础测试物料。3.2 软件准备STM32CubeMX用来配置芯片引脚、时钟树和生成初始化代码。STM32CubeIDE 或 Keil MDK用于编译调试。STM32C5 系列对应的 HAL 固件包。具体版本打开 CubeMX 时选择即可不需要额外手动下载。3.3 需要了解的基础概念定时器预分频器 PSC。自动重载寄存器 ARR。比较捕获寄存器 CCR。PWM 频率与占空比计算公式。CubeMX 中如何查看引脚复用关系。如果你已经会新建普通 GPIO 工程这篇的流程基本可以直接迁移。4. STM32CubeMX 配置与工程生成4.1 新建工程并选择型号打开 STM32CubeMX新建工程在 MCU 选择页面搜索 STM32C5A3R选中后进入项目配置界面。根据实际开发板选择封装型号不确定就选和板子丝印一致的型号。4.2 配置时钟树在 Clock Configuration 页面配置系统时钟。不同开发板的外部晶振频率不一样常见的有 8MHz、16MHz、25MHz。如果不接外部晶振可以直接使用芯片内部 HSI 时钟先把基础工程跑通再根据项目要求切换到 PLL 倍频到更高主频。STM32C5 系列支持的最大主频、APB 分频关系需要打开 CubeMX 时钟树页面看实时提示。下面统一用一个假设值来做计算演示定时器时钟 80MHz目标 PWM 频率 1kHz目标占空比 20%这个 80MHz 只是方便演示不代表 STM32C5A3R 的默认定时器时钟。实际工程里请以 CubeMX 生成的时钟树数值为准。4.3 选择定时器并配置 PWM 通道以 TIM3 的通道 1 为例。在左侧 Categories 里找到 TIM3勾选 Internal Clock然后在 Channel1 的 Mode 下拉框里选择 PWM Generation CH1。设置 PWM 参数时主要关注三个值参数示例值说明Prescaler79预分频值寄存器实际值是 79对应分频倍数 80Counter ModeUp向上计数模式Counter Period999自动重载值 ARRPulse200初始比较值 CCR决定初始占空比Output PolarityHigh输出极性一般保持 High根据公式计算PWM 频率 定时器时钟 / ((PSC 1) * (ARR 1)) 80000000 / (80 * 1000) 1000 Hz 占空比 CCR / (ARR 1) 200 / 1000 20%在 CubeMX 的引脚配置页面TIM3_CH1 可选引脚通常有多个常见的可能是 PA6、PB4、PC6 等具体以 CubeMX 显示为准。选中一个没有和其他外设冲突的引脚即可。4.4 配置 GPIO 速度在 GPIO 配置页面找到 PWM 输出引脚建议把 GPIO Pull-up/Pull-down 设置为 No pull-up and no pull-downGPIO Speed 设置为 Very High。高速 PWM 对 GPIO 翻转速度有要求速度等级太低会导致波形边沿变缓。4.5 生成工程在 Project Manager 页面设置项目名称和存储路径。Toolchain 选择 STM32CubeIDE 或 MDK-ARM根据需要选择。路径尽量不要包含中文和空格避免编译工具链出现奇怪的问题。点击 Generate Code 生成工程。5. 代码解析与 PWM 启动5.1 初始化代码CubeMX 生成后定时器初始化代码一般在tim.c里。核心结构类似下面static void MX_TIM3_Init(void) { TIM_OC_InitTypeDef sConfigOC {0}; htim3.Instance TIM3; htim3.Init.Prescaler 79; htim3.Init.CounterMode TIM_COUNTERMODE_UP; htim3.Init.Period 999; htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim3); sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 200; sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(htim3, sConfigOC, TIM_CHANNEL_1); HAL_TIM_MspPostInit(htim3); }此时定时器还没有开始输出波形。必须在 main 函数里调用启动函数。5.2 启动 PWM在main.c中初始化外设后调用HAL_TIM_PWM_Startint main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); // 启动 TIM3 通道 1 的 PWM 输出 HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); while (1) { } }编译下载后用示波器测量 TIM3_CH1 引脚应该能看到 1kHz、20% 占空比的方波。5.3 注意高级定时器如果选择的是 TIM1、TIM8 这类高级定时器只调用HAL_TIM_PWM_Start可能依然看不到输出。原因在于高级定时器需要额外使能主输出 MOE。常见写法是HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); __HAL_TIM_MOE_ENABLE(htim1);顺序通常先启动 PWM再使能主输出。这条是高频问题建议记下来。6. 修改 PWM 频率与占空比运行中修改频率和占空比是工程里最常用的操作。做法有两种直接改寄存器或者封装一个统一的设置函数。6.1 修改对应关系的公式假设定时器时钟为TIM_CLK目标频率为FREQ_HZ目标占空比为DUTY_PERCENTPSC 1 分频倍数 ARR 1 定时器计数值 CCR 比较值 FREQ_HZ TIM_CLK / ((PSC 1) * (ARR 1)) DUTY_PERCENT CCR * 100 / (ARR 1)因此修改频率时既可以改 PSC也可以改 ARR。一般策略是固定 PSC修改 ARR。这样占空比步进可能变化修改 ARR 时必须同步重新计算 CCR。固定 ARR修改 PSC。调频更快但低频时分辨率会下降。更通用的做法是扫描 PSC使 ARR 落在合理范围内然后同时写入 PSC、ARR、CCR。6.2 封装一个动态设置函数下面的函数用遍历 PSC 的方式找到一个合适的 PSC 和 ARR使 ARR 不超出 16 位定时器范围然后同步更新 CCR。函数需要传入定时器句柄、通道、定时器时钟、目标频率和占空比。#include stdint.h void PWM_SetFreqDuty(TIM_HandleTypeDef *htim, uint32_t channel, uint32_t timer_clock, uint32_t freq_hz, uint16_t duty_percent) { uint32_t psc 0; uint32_t arr 0; uint32_t ccr 0; if (freq_hz 0 || duty_percent 100) { return; } // 遍历 PSC让 ARR 落在 16 位范围内 for (psc 0; psc 0xFFFF; psc) { arr timer_clock / ((psc 1) * freq_hz) - 1; if (arr 0xFFFF) { break; } } if (arr 0xFFFF) { // 频率太低16 位定时器无法覆盖 return; } // 计算比较值 ccr (uint32_t)(((uint64_t)(arr 1) * duty_percent) / 100); // 停止 PWM避免更新过程中输出不确定波形 HAL_TIM_PWM_Stop(htim, channel); // 清空计数器 __HAL_TIM_SET_COUNTER(htim, 0); // 写入 PSC、ARR、CCR __HAL_TIM_SET_PRESCALER(htim, psc); __HAL_TIM_SET_AUTORELOAD(htim, arr); __HAL_TIM_SET_COMPARE(htim, channel, ccr); // 重新启动 PWM HAL_TIM_PWM_Start(htim, channel); }调用方式PWM_SetFreqDuty(htim3, TIM_CHANNEL_1, 80000000UL, 1000, 20);这样就能把 TIM3_CH1 设置为 1kHz、20% 占空比。想改成 5kHz、60%直接调用PWM_SetFreqDuty(htim3, TIM_CHANNEL_1, 80000000UL, 5000, 60);6.3 运行时更新频率和占空比如果项目需要持续变更 PWM 参数可以把函数放在 while 循环里通过按键、串口或者传感器输入来触发。简单按键切换示例uint8_t counter 0; while (1) { if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { counter; if (counter % 2 0) { PWM_SetFreqDuty(htim3, TIM_CHANNEL_1, 80000000UL, 1000, 20); } else { PWM_SetFreqDuty(htim3, TIM_CHANNEL_1, 80000000UL, 5000, 60); } HAL_Delay(200); } }需要注意这里用 HAL_Delay 防止按键抖动如果放在实际产品里改用按键消抖状态机更可靠。6.4 串口指令控制如果想让 PWM 参数可以通过 PC 或上位机修改最直接的方式是加串口解析。这里给一个简化示例接收形如 “1000 20” 的字符串第一个数是频率第二个数是占空比#include stdio.h #include string.h #include usart.h uint8_t rx_buf[32]; int rx_len 0; // 假设串口中断或 DMA 收到了数据放到 rx_buf void Process_PWM_Command(void) { int freq 0; int duty 0; if (sscanf((char *)rx_buf, %d %d, freq, duty) 2) { if (freq 0 duty 0 duty 100) { PWM_SetFreqDuty(htim3, TIM_CHANNEL_1, 80000000UL, freq, duty); } } memset(rx_buf, 0, sizeof(rx_buf)); rx_len 0; }实际工程中建议使用串口空闲中断或 DMA 接收不要使用阻塞式HAL_UART_Receive长时间等待否则会拖慢主循环。6.5 大批量连续改频如果要做扫频测试比如从 100Hz 扫到 10kHz每次步进 100Hz可以直接用 for 循环驱动上面的设置函数for (uint32_t f 100; f 10000; f 100) { PWM_SetFreqDuty(htim3, TIM_CHANNEL_1, 80000000UL, f, 50); HAL_Delay(50); }这种方式适合验证某个频段是否存在共振、蜂鸣器音调变化是否正常、电机转速切换是否稳定。实际项目里要保证每次修改前的 Stop/Start 不会产生预期外的电机冲击必要时应提前关断输出控制引脚。7. 功能测试与效果验证7.1 使用示波器测量PWM 配好之后验证是最重要的。连接示波器探头到 PWM 输出引脚GND 接板子地线观察波形。测试步骤如下程序默认输出 1kHz、20% 占空比。示波器测量频率应接近 1kHz。测量高电平时间占空比应约为 20%。调用PWM_SetFreqDuty(htim3, TIM_CHANNEL_1, 80000000UL, 5000, 60);示波器频率应变为 5kHz占空比应变为 60%。如果没波形先检查三件事HAL_TIM_PWM_Start是否被调用。引脚是否真的复用到了 TIM3_CH1。高级定时器是否使能了主输出。7.2 使用逻辑分析仪没有示波器的情况下可以使用逻辑分析仪。逻辑分析仪适合观察频率和占空比但不适合观察模拟边沿质量。把逻辑分析仪采样率设到远高于 PWM 频率比如 25MHz否则测出的频率会明显偏低。7.3 使用 LED 做粗略验证如果手边没有示波器接一个 LED 和限流电阻到 PWM 引脚。占空比设为 10% 时LED 比较暗。占空比设为 90% 时LED 明显变亮。频率低于 100Hz 时肉眼能看到 LED 闪烁。频率高于 1kHz 时LED 看起来基本是常亮亮度随占空比变化。这种方法不能精确测频率和占空比但可以快速确认 PWM 是否在正常工作。7.4 配合 ADC 做闭环验证有些项目需要用 PWM 触发 ADC 采样比如在 PWM 高电平中间时刻采样电流或电压。这时候可以把定时器更新事件或 PWM 的触发输出接到 ADC 触发源让硬件自动完成“PWM 输出 - ADC 采样”而不是在中断里频繁读 ADC。这一部分需要查参考手册里 ADC 触发源和定时器的连接关系不同系列差异较大。STM32C5A3R 具体是否支持该触发源以数据手册为准。8. 资源占用与性能观察8.1 CPU 占用PWM 波形由定时器硬件产生CPU 不需要参与每个周期的输出。PWM 一旦启动后主循环几乎零占用这是定时器 PWM 比较大的优势。CPU 的负担主要在修改参数那一刻。使用HAL_TIM_PWM_Stop和HAL_TIM_PWM_Start这种同步方式会稍微增加延迟但对普通调光、调速场景没有影响。8.2 定时器资源占用每个 PWM 通道会占用对应的定时器通道。如果一个定时器只有一个通道有输出其他通道仍然可以复用作普通定时或输入捕获。但如果不同通道需要不同频率最好分别使用不同定时器因为同一个定时器的 ARR 是共用的频率只能保持一致。8.3 频率与占空比精度频率越低需要的分频倍数越大ARR 会变小占空比调节的步进就会变粗。举一个例子定时器时钟 80MHz目标频率 1kHzARR 可以是 999占空比最小步进为 0.1%。目标频率还是 80MHz但想输出 1Hz这时必须把 PSC 调得很大ARR 可能很小占空比步进可能跳到 10% 甚至更多。所以在低频高精度占空比场景需要选用支持更宽计数范围的定时器或者使用其他方案。8.4 GPIO 输出速度影响PWM 频率较高时如果 GPIO 输出速度等级不够波形边沿会变缓高电平时间和低电平时间都不准确。CubeMX 中把 PWM 引脚 GPIO Speed 设为 Very High能改善波形质量。8.5 DMA 批量更新如果项目需要连续更新 PWM 占空比比如做任意波形输出、音频播放、动态灯效可以使用 DMA 把一组 CCR 值搬运到定时器比较寄存器减少 CPU 中断次数。典型写法是uint32_t ccr_buffer[16] {200, 300, 400, 500, 400, 300, 200, 100}; // 伪代码需要先确认芯片支持 PWN DMA 请求 HAL_TIM_PWM_Start_DMA(htim3, TIM_CHANNEL_1, ccr_buffer, 16);这里只是扩展思路。STM32C5A3R 的具体 DMA 请求映射、缓冲区准备、传输完成回调需要以参考手册和 HAL 库驱动为准不能照搬所有型号的写法。9. 常见问题与排查方法问题现象可能原因排查方式解决方案PWM 引脚没有波形定时器未启动检查是否调用 HAL_TIM_PWM_Start调用 HALTIM_PWM_StartPWM 引脚没有波形引脚复用配置错误查看 CubeMX 引脚配置确认复用功能是 TIMx_CHy重新选择正确的复用引脚使用 TIM1/TIM8 没有波形高级定时器主输出未使能查看代码是否有 MOE 使能添加 __HAL_TIM_MOE_ENABLE(htim1)频率不对定时器时钟和计算值不一致检查 CubeMX 时钟树用实际定时器时钟重新计算 PSC/ARR修改频率后没变化PSC 写入后没有触发更新事件检查寄存器是否实际写入停止 PWM 后写 PSC/ARR/CCR再重新启动占空比一直是 50%CCR 没有更新成功检查是否调用了 __HAL_TIM_SET_COMPARE同步设置 ARR 和 CCR修改 ARR 后占空比突变ARR 和 CCR 没有同步检查修改顺序使用统一函数同时更新 ARR 和 CCR高频波形边沿不陡GPIO 输出速度太低查看 GPIO Speed 配置设置为 Very High上电瞬间引脚输出高电平PWM 极性/默认电平设置结合电路确认上电时负载是否允许使用下拉电阻或调整输出极性串口通过 HAL_UART_Receive 阻塞接收函数一直等待数据检查串口使用方式改用中断或 DMA 接收PWM 触发 ADC 没有反应触发源配置错误查参考手册的触发映射正确配置定时器触发输出和 ADC 触发源9.1 修改频率后看不到变化修改 PSC 或 ARR 之后定时器本身的影子寄存器不一定立即加载新值。如果只是直接写外部寄存器宏没有等到更新事件输出可能还是旧频率。因此在实际代码里推荐先HAL_TIM_PWM_Stop再写 PSC、ARR、CCR再重新启动。这样每一步都是确定的。9.2 占空比无法调到期望值先确认 CCR 是否不大于 ARR。如果 CCR 大于 ARR输出会一直为高电平看起来像占空比 100%。如果 CCR 为 0输出会一直为低电平看起来像完全关闭。这两者在调试时容易被误判为“PWM 坏了”。9.3 使用高级定时器互补输出时无波形高级定时器做互补 PWM、刹车输入、死区控制时除了启动 PWM还需要处理死区参数、刹车状态和主输出使能。建议先配置一个最简单的独立 PWM 输出等波形正常后再逐步加入互补和死区功能。10. 最佳实践与工程化建议10.1 先跑通最小工程不要一上来就配置多路 PWM、互补输出、死区、DMA、ADC 触发。先把单通道 PWM 输出跑通用示波器确认频率和占空比再逐步增加功能。每一步都有一个可确认的验证点排查难度会小很多。10.2 统一封装频率和占空比设置接口不要在主循环里直接散落写__HAL_TIM_SET_AUTORELOAD和__HAL_TIM_SET_COMPARE而是封装成类似PWM_SetFreqDuty的函数。这样可以让“修改频率”和“修改占空比”的逻辑集中在一起也方便在不同定时器通道之间复用。10.3 将关键参数宏定义建议将定时器时钟、默认频率、默认占空比、定时器型号、通道号集中放到头文件里#define PWM_TIM_CLK 80000000UL #define PWM_FREQ 1000UL #define PWM_DUTY 20 #define PWM_TIM htim3 #define PWM_CH TIM_CHANNEL_1调用时PWM_SetFreqDuty(PWM_TIM, PWM_CH, PWM_TIM_CLK, PWM_FREQ, PWM_DUTY);这样做的好处是以后更换定时器或调整频率时只需要改头文件不用到处翻代码。10.4 注意中断保护如果 PWM 参数需要在上层任务和中断中同时修改需要加临界区保护。最简单的方式是关中断后修改退出前恢复中断状态。否则中断里写 PSC主循环也在写 ARR两边可能互相覆盖导致频率出现偶发异常。10.5 涉及电机和电源时做好故障保护PWM 用于电机驱动或电源控制时不能只依赖 MCU 内部逻辑。建议增加过流检测、过温检测、硬件刹车输入。高级定时器的刹车功能和故障保护引脚可以配合硬件保护电路在异常时快速关闭 PWM 输出而不等待主循环处理。10.6 批量测试时添加日志如果要扫频测试电机振动点、蜂鸣器谐振点、LED 频闪区间建议在每次修改 PWM 参数后通过串口把实际设置的频率、占空比打印出来。示波器波形只能看到单次结果串口日志可以记录完整变化过程。11. 总结与下一步STM32C5A3R 的 PWM 输出本质是定时器的预分频、自动重载、比较匹配三个寄存器配合工作。用 CubeMX 可以快速生成初始化代码用 HAL 库可以迅速启动 PWM。真正需要在工程中理解的是频率由 PSC 和 ARR 共同决定。占空比由 CCR 和 ARR 共同决定。修改频率时必须同步重新计算 CCR否则输出占空比会跳变。高级定时器需要使能主输出GPIO 速度要设置为 Very High。验证 PWM 必须用示波器或逻辑分析仪不能只凭 LED 亮度判断精准度。建议你第一次实验时按这篇的流程配置一个 1kHz、20% 的 PWM然后调用PWM_SetFreqDuty改成 5kHz、60%用示波器观察两次波形差异。这个动作跑通之后RGB 灯调光、舵机控制、电机调速、蜂鸣器音调这些项目就都可以在这个基础上扩展。后续可以继续做的是把 PWM 和 DMA 结合起来做动态波形输出用高级定时器输出互补 PWM 驱动电机或者用定时器触发 ADC 采样实现电流环闭环。每一步都是建立在基础 PWM 配置上的先把这个基础打牢再往上层应用走会更顺利。