1. 从“嘀嗒”声到PWM一个工程师的直觉理解如果你玩过单片机或者拆过任何带电机、呼吸灯的小玩意儿大概率听说过PWM。全称是“脉冲宽度调制”听起来挺唬人但它的核心思想其实和我们小时候用手电筒模拟摩斯电码或者用开关水龙头控制水流大小本质上是一回事。在STM32这类微控制器里PWM是驱动电机、调节LED亮度、控制舵机角度的基石。但很多朋友在配置寄存器看着一堆ARR、PSC、CCR数值时会有点发懵我填的这些数到底是怎么决定最终输出波形那个“嘀嗒”声的快慢频率和长短占空比的网上的教程往往直接给公式频率 时钟 / ((ARR1)*(PSC1))。背下来也能用但一旦遇到波形不对、频率不准、电机啸叫这些问题如果只知其然不知其所以然排查起来就非常痛苦。我自己在早期做智能车和机械臂项目时就因为没彻底搞懂这里的计算逻辑调一个舵机频率调了一下午最后发现是时钟源配置错了。所以今天我们不只讲公式更要把STM32产生PWM的整个“流水线”拆开看看时钟是怎么一步步被“裁剪”成我们需要的方波的。理解了这条流水线任何相关的问题你都能自己推导出答案。2. 拆解PWM发生器一条精密的“脉冲裁剪流水线”你可以把STM32内部产生PWM的定时器模块想象成一条高度自动化的脉冲裁剪生产线。这条生产线的目标是生产出一种标准产品周期和占空比都可精确设定的方波脉冲。它的原材料是稳定的“时钟脉冲”而生产线的各个环节由几个关键寄存器控制。2.1 原材料时钟源的选择与分配一切始于时钟。STM32的定时器不能无中生有它需要一个节拍器。这个节拍器就是时钟源。常见的有内部时钟CK_INT通常来自系统时钟SYSCLK比如72MHzSTM32F1系列常见。这是最常用的源稳定且方便。外部时钟从特定引脚输入时钟脉冲用于特殊同步场合。这里第一个容易踩的坑就来了你以为系统时钟是72MHz定时器就用上了72MHz吗不一定。以STM32F1为例APB1总线上的定时器如TIM2, TIM3, TIM4的时钟在APB1预分频器不为1时会自动倍频。如果APB1预分频器设为2APB1时钟是36MHz但挂载在它上面的定时器时钟会是72MHz。这个细节在CubeMX配置时钟树时至关重要很多频率对不上的问题根源就在这里。实操心得使用CubeMX或直接读代码配置时钟后务必通过读取RCC-CFGR寄存器或使用SystemCoreClock变量再次确认最终到达定时器的实际时钟频率是多少。这是所有计算的绝对基准基准错了后面全错。2.2 第一道工序预分频器PSC的粗加工原材料时钟频率通常很高几十MHz直接用来计数的话计数器会累加得飞快导致周期分辨率极低频率很高。因此我们需要第一道工序来降低节奏这就是预分频器Prescaler, PSC。PSC是一个分频器。如果PSC寄存器设置为N那么每N1个输入时钟脉冲才会产生1个脉冲给下一道工序计数器。例如时钟源72MHzPSC71则分频后的时钟频率 72MHz / (711) 1MHz。这里“1”是因为计数器从0开始计数这是一个关键点。为什么是N1可以这样理解PSC本身也是一个计数器。它从0开始对输入时钟计数计到N时产生一个溢出信号给计数器CNT然后自己归零重新开始。所以它计数的周期是N1个时钟周期。这个过程就是把高频率的“原材料”进行“降速粗加工”得到我们想要的“基础节奏”。2.3 第二道工序自动重装载寄存器ARR定长度经过PSC降速后的脉冲驱动着核心的计数器CNT向上或向下计数。计数器不能无限累加它的计数范围由自动重装载寄存器Auto-Reload Register, ARR决定。计数器CNT会从0开始每个脉冲加1向上计数模式一直累加到ARR的值然后产生一个更新事件Update Event并且CNT清零重新开始下一轮计数。这一个从0到ARR再归零的完整过程所花费的时间就是输出PWM波形的整个周期。所以ARR决定了PWM波形周期的“长度单位”有多少个。结合PSC提供的“单位时间”就能算出整个周期的时间。2.4 最终成型捕获/比较寄存器CCR定占空比有了周期我们还需要在周期内定义一个“高点平”的时间这就是占空比。这个工作由捕获/比较寄存器Capture/Compare Register, CCR来完成。在PWM模式下我们会设定一个比较模式。当计数器CNT的值小于CCR时输出一种电平通常为高电平当CNT的值大于等于CCR且小于等于ARR时输出另一种电平低电平。如此循环往复。因此CCR的值直接决定了高电平的持续时间占空比 CCR / (ARR 1)。注意这里又是ARR1因为计数范围是0到ARR总共是ARR1个计数点。3. 核心公式推导从寄存器值到频率与占空比现在我们把整条流水线串起来推导出那个经典的公式。定时器时钟频率F_TIM单位Hz预分频器值PSC寄存器写入值分频后驱动计数器的时钟频率F_CNT F_TIM / (PSC 1)单位Hz计数器计数一次的时间T_CNT 1 / F_CNT (PSC 1) / F_TIM单位秒自动重装载值ARR寄存器写入值一个完整的PWM周期需要计数的次数ARR 1次从0数到ARRPWM波形的周期T_pwmT_pwm T_CNT * (ARR 1) (PSC 1) * (ARR 1) / F_TIM单位秒PWM波形的频率F_pwmF_pwm 1 / T_pwm F_TIM / ((PSC 1) * (ARR 1))单位Hz占空比Duty Cycle计算高电平时间T_high T_CNT * CCR (PSC 1) * CCR / F_TIM占空比Duty T_high / T_pwm CCR / (ARR 1)这就是所有计算的根源。请务必理解每一步的物理意义而不是死记硬背F_pwm F_TIM / ((PSC1)*(ARR1))。4. 实战配置以STM32F103输出1kHz、50%占空比PWM为例理论说完了我们来个实际例子。目标用STM32F103C8T6的TIM3通道1PA6引脚输出一个1kHz频率、占空比50%的PWM方波。假设系统时钟为72MHz且TIM3挂在APB1总线上APB1预分频器为2此时TIM3时钟为72MHz。步骤1确定已知量F_TIM 72,000,000 HzF_pwm 1,000 HzDuty 50%步骤2公式变形与参数选择我们有公式F_pwm F_TIM / ((PSC1) * (ARR1))所以(PSC1) * (ARR1) F_TIM / F_pwm 72,000,000 / 1,000 72,000这里PSC和ARR都是16位寄存器最大值65535它们的乘积需要等于72000。我们需要找一个合理的组合。ARR决定了PWM的分辨率。ARR越大CCR可以设置的值就越多占空比调节就越精细。例如如果ARR999那么占空比分辨率是1/10000.1%。如果ARR71分辨率就只有约1.4%。对于1kHz我们通常希望有较高的分辨率。我们让ARR1 1000这样占空比分辨率就是0.1%。那么ARR 999PSC1 72,000 / 1000 72PSC 71步骤3计算CCR占空比50%所以CCR Duty * (ARR 1) 0.5 * 1000 500。步骤4代码配置以标准库为例// 1. 开启时钟和GPIO RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 2. 初始化GPIO为复用推挽输出 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 初始化定时器时基 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 999; // ARR TIM_TimeBaseStructure.TIM_Prescaler 71; // PSC TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; // 向上计数模式 TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 4. 初始化PWM输出通道 TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; // PWM模式1 TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; // 使能输出 TIM_OCInitStructure.TIM_Pulse 500; // CCR决定初始占空比 TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; // 输出极性高电平有效 TIM_OC1Init(TIM3, TIM_OCInitStructure); // 初始化通道1 TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); // 使能预装载寄存器 // 5. 使能定时器的预装载寄存器ARR和计数器 TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE);使用HAL库或CubeMX配置思路完全一致填入对应的PSC、ARR、CCR值即可。5. 那些年我踩过的坑频率不准、无输出与电机异响理解了原理和计算大部分配置都能一次成功。但实际项目环境复杂下面分享几个典型的坑和排查思路。5.1 坑一算出来的频率和示波器测的不一样这是最常见的问题。可能的原因有时钟源实际频率不对这是头号嫌疑犯。务必用代码或调试器确认F_TIM的真实值。比如你以为是72M可能因为PLL没锁住、HSE晶振没起振实际只有8MHSI。PSC或ARR的值超出了有效范围比如16位定时器你算出的PSC或ARR大于65535实际赋值时被截断导致计算错误。计数模式的影响我们上述计算基于向上计数模式。如果使用中央对齐模式向上向下计数计数器会从0到ARR再从ARR到0这样一个完整的PWM周期需要2 * ARR个计数。此时频率公式变为F_pwm F_TIM / ((PSC1) * (ARR * 2))。模式选错频率直接差一倍。重复计数器RCR的干扰高级定时器如TIM1, TIM8有重复计数器它可以使更新事件重置PWM周期在多次溢出后才发生。如果设置了RCRN则实际PWM周期会变为原来的(N1)倍。排查流程第一步用示波器测量实际频率F_actual。第二步根据公式反推(PSC1)*(ARR1) F_TIM / F_actual。第三步对比你计算出的(PSC1)*(ARR1)和反推出的值。如果比值是2的倍数检查计数模式如果差得很多检查时钟源和寄存器赋值。5.2 坑二完全没有PWM波形输出引脚上一片寂静。除了检查代码是否使能了定时器和输出通道还要关注GPIO模式错误必须配置为复用推挽输出AF_PP而不是普通的推挽输出。普通输出模式受GPIO输出数据寄存器控制无法输出定时器产生的波形。引脚复用功能未映射有些引脚默认功能不是定时器输出。需要调用GPIO_PinAFConfig函数对于支持重映射的型号来将定时器功能映射到特定引脚。在CubeMX中这个步骤是图形化完成的。主输出使能MOE未开启对于高级定时器TIM1/TIM8的刹车和死区功能需要设置BDTR寄存器的MOE位为1否则所有输出被禁止。这是新手用高级定时器时极易忽略的一点。CCR值设置不合理如果CCR 0在PWM模式1下可能输出恒低电平如果CCR ARR则可能输出恒高电平取决于极性设置。用示波器看会像没有波形其实是直流。5.3 坑三驱动电机时发出“滋滋”啸叫声用PWM驱动直流电机或步进电机驱动器时有时会听到高频啸叫。这通常不是代码逻辑错误而是PWM频率选在了人耳可听范围20Hz - 20kHz内。电机线圈和驱动器内部的电感、电容构成了一个谐振系统。当PWM频率落在可听频段特别是某个谐振点附近时线圈或磁芯的振动就会被人耳听到。解决方案将PWM频率提高到20kHz以上例如25kHz 30kHz。这样既避免了噪音又因为频率远高于电机的机械响应速度不会影响调速性能。但要注意提高频率意味着(PSC1)*(ARR1)的值要变小可能会牺牲PWM的分辨率。需要在噪音和分辨率之间做权衡。6. 进阶话题高精度频率生成与动态调整有时候我们需要非常精确的频率或者需要运行时动态改变频率和占空比。这里有一些技巧。6.1 追求极致精度时钟源与分频策略标准72MHz晶振的精度可能只有100ppm万分之一对于要求极高的场合如音频采样时钟这可能不够。可以使用更高精度的外部晶振如TCXO精度可达1ppm。利用STM32的时钟锁相环PLL进行小数分频部分型号支持可以产生非整数的分频比从而得到更精确的频率。在分频策略上为了获得最大的分辨率即ARR尽可能大我们通常先确定ARR再反算PSC。但也要注意PSC和ARR都必须是整数。有时无法得到精确的目标频率这时可以计算实际误差误差 |(F_实际 - F_目标) / F_目标|选择一个误差可接受的组合。6.2 运行时动态调整如何平滑改变在电机加速、LED呼吸灯等场景需要动态改变PWM的占空比甚至频率。改变占空比直接修改CCR寄存器的值即可。为了避免在周期中间修改导致毛刺可以使用预装载功能。在标准库中就是调用TIM_OCxPreloadConfig(TIMx, ENABLE)并在修改后设置TIMx-CCRx寄存器。真正的更新会发生在下一个更新事件计数器溢出时这样切换是同步的、平滑的。改变频率即改变周期这需要同时改变ARR和PSC。但直接修改ARR如果新的ARR值小于当前计数器值CNT会立即产生更新事件导致当前周期被突然截断产生一个很窄的脉冲可能损坏设备如电机驱动器。安全的做法是在修改前先停止计数器TIMx-CR1的CEN位置0。修改PSC和ARR寄存器。如果需要同时按比例修改CCR以保持占空比不变CCR_new (CCR_old / ARR_old) * ARR_new。产生一个软件更新事件TIMx-EGR的UG位置1让所有影子寄存器立即生效。重新使能计数器。 这个过程会丢失一两个PWM周期但对于大多数应用是可以接受的。更高级的定时器支持“重复计数器”和“影子寄存器”的复杂同步机制可以实现更平滑的频率切换。7. 举一反三从定时器到其他波形生成理解了PWM的定时器流水线模型再去理解其他相关功能就很容易了。输入捕获过程相反。外部信号通过引脚输入在信号的边沿触发将当前计数器CNT的值锁存到CCR寄存器中。通过计算两次捕获的差值就能算出输入信号的周期或脉冲宽度。这就是测频率、测占空比的原理。输出比较不产生连续的PWM而是在CNT计数到与CCR匹配时产生一个单一的动作如翻转引脚电平、产生中断。可以用来生成精确的延时或驱动步进电机的单步脉冲。单脉冲模式OPM可以理解为只输出一个指定宽度脉冲的PWM。在触发信号到来后开始一个完整的PWM周期从0到ARR周期结束后自动停止。非常适合需要精确单次触发的场合。PWM是嵌入式控制中最基础、最强大的工具之一。从调节一盏LED灯的亮度到驱动一个四旋翼飞行器的电调背后都是这套原理。把“时钟源 - PSC - CNT ARR - CCR 比较器”这条流水线刻在脑子里再结合寄存器的具体操作你就能从“配置寄存器”的层面真正驾驭定时器而不是被它牵着鼻子走。下次再遇到PWM相关的问题别急着搜答案拿起笔和纸画一画这条流水线算一算时钟路径答案往往自己就浮现出来了。