
1. 从一个让人困惑的现象说起刚接触嵌入式的朋友十有八九都卡在同一个地方明明代码里只是让一个引脚输出高低电平怎么就能让 LED 从刺眼的满亮变成柔和的呼吸灯又怎么让一个直流电机从静止变成每分钟几千转更让人摸不着头脑的是这中间既没有改变电压也没有换电源只是换了一种“开关方式”。我第一次碰到这个问题是在一块 STM32 最小系统板上。当时用 GPIO 直接驱动一颗 LED代码写的是HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET)灯亮了改成RESET灯灭了。这很直观。但当我试着用同一个 PA8 引脚输出 PWM 去调光时示波器上看到的是一串方波灯却呈现出介于亮和灭之间的状态。那一刻我才真正意识到PWM 的本质不是“输出一个中间电压”而是“用时间换效果”。这篇文章就围绕这个核心问题展开PWM 为什么能调速、调光它背后的原理是什么在 STM32 这类单片机上怎么配置LED 和直流电机这两种负载用 PWM 驱动时有什么本质区别我会从原理讲到实操从 GPIO 讲到定时器从 LED 讲到电机驱动把这条链路完整地串起来。不管你是刚学单片机的新手还是已经用过 PWM 但没深究过原理的开发者都能从中找到自己需要的那部分。2. PWM 到底是什么用时间维度理解“模拟量”2.1 方波里的秘密占空比与等效值PWM 全称 Pulse Width Modulation中文叫脉冲宽度调制。名字听起来很学术但拆开看就三件事频率、占空比、幅值。假设有一个 3.3V 的电源我用一个开关不停地接通、断开。接通 1 毫秒断开 1 毫秒循环往复。这个波形的频率是 500Hz占空比是 50%。如果接通 0.5 毫秒断开 1.5 毫秒频率不变占空比变成 25%。关键来了如果这个开关速度足够快快到人眼无法分辨每一次通断快到电机的机械惯性来不及响应每一次断开那么负载“感受到”的就是一个平均效果。50% 占空比下LED 感受到的平均功率大约是满功率的一半电机感受到的平均电压大约是电源电压的一半。这就是 PWM 调速调光的核心逻辑不改变电源电压只改变通电时间占总时间的比例从而改变负载获得的平均能量。用公式表达就是等效平均电压 电源电压 × 占空比比如 12V 电源、40% 占空比电机端等效电压就是 4.8V。LED 同理3.3V 电源、20% 占空比等效亮度大约相当于 0.66V 持续供电的效果实际还要考虑 LED 的非线性特性后面会细说。2.2 为什么不用直接调电压效率与发热的博弈有人会问既然要调光调速为什么不直接用一个可调电阻或者可调电源把电压降下来这个问题问到了要害。直接调电压有两个致命问题效率低、发热大。用可调电阻分压多余的电压全部变成热量消耗在电阻上。假设 12V 电源驱动一个等效内阻 10Ω 的电机想要它只得到 6V串联一个 10Ω 电阻电流 0.6A电阻上消耗的功率是 0.6² × 10 3.6W。这 3.6W 纯粹是浪费而且电阻会烫得没法摸。对于电池供电的设备来说这简直是灾难。PWM 则完全不同。开关管要么完全导通压降接近 0要么完全截止电流为 0。理想情况下开关管本身不消耗功率能量几乎全部传递给负载。实际中 MOSFET 有导通电阻开关瞬间有损耗但整体效率通常能做到 90% 以上。这就是为什么现在几乎所有的 LED 调光、电机调速、电源管理都用 PWM而不是线性调节。效率差距不是一点半点而是数量级的差别。2.3 频率选多少不同负载的“时间常数”决定一切PWM 频率的选择不是随便定的它取决于负载的响应速度。对于 LED人眼的视觉暂留时间大约是 1/24 秒也就是 40 毫秒左右。只要 PWM 周期远小于这个时间人眼就看不到闪烁。所以 LED 调光频率通常在 100Hz 以上常见的是 1kHz 到 20kHz。频率太低会看到闪烁频率太高则开关损耗增加而且某些 LED 驱动芯片可能响应不过来。对于直流电机情况更复杂。电机的机械惯性比人眼慢得多但电机的电感特性会导致电流纹波。频率太低电流断续电机运转不平稳有噪音频率太高开关损耗增加驱动电路要求更高。一般有刷直流电机的 PWM 频率在 1kHz 到 20kHz 之间具体要看电机参数和驱动方式。这里有个经验值可以参考负载类型推荐 PWM 频率原因普通 LED 指示灯1kHz - 5kHz避免可见闪烁开关损耗小LED 背光20kHz 以上避开音频范围防止电感啸叫有刷直流电机5kHz - 20kHz平衡平稳性与开关损耗无刷电机20kHz - 50kHz需要更平滑的电流波形加热丝1Hz - 100Hz热惯性大不需要高频注意如果 PWM 频率落在音频范围内20Hz - 20kHz电机或电感可能会发出啸叫声。遇到这种情况要么把频率提到 20kHz 以上要么降到 20Hz 以下。3. STM32 上怎么产生 PWM从 GPIO 到定时器3.1 GPIO 直接翻转最原始但不可取的方式在讲定时器之前先说一个很多人试过的方法用 GPIO 加延时函数手动翻转电平。while (1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET); delay_us(500); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET); delay_us(500); }这段代码确实能产生 1kHz、50% 占空比的方波。但它有几个致命问题第一CPU 被完全占用。延时函数通常是空循环CPU 什么也干不了就在那里数数。对于 STM32 这种资源有限的单片机来说这是极大的浪费。第二精度极差。延时函数的精度受中断、编译器优化、时钟配置影响实际频率可能偏差百分之几甚至更多。而且占空比只能取离散的几个值没法精细调节。第三无法同时输出多路。如果要控制 RGB 三色 LED需要三路 PWM手动翻转根本忙不过来。所以GPIO 直接翻转只适合验证概念实际项目必须用定时器硬件产生 PWM。3.2 定时器产生 PWM 的硬件原理STM32 的通用定时器如 TIM2、TIM3、TIM4和高级定时器如 TIM1、TIM8都内置了 PWM 生成模式。它的工作原理可以用一个比喻来理解想象一个计数器从 0 开始往上数数到某个值ARR自动重装载寄存器就归零重新开始。同时有一个比较寄存器CCR捕获/比较寄存器。计数器每数一个数就和 CCR 比较一次如果计数值小于 CCR输出高电平如果计数值大于等于 CCR输出低电平。这样占空比就等于 CCR / (ARR 1)。改变 CCR 的值就改变了占空比改变 ARR 的值就改变了频率。频率计算公式PWM 频率 定时器时钟 / ((ARR 1) × (PSC 1))其中 PSC 是预分频器。举个例子STM32F103 的 TIM2 挂在 APB1 总线上时钟 72MHz。想要 1kHz 的 PWM 频率可以设 PSC 71ARR 999。计算72,000,000 / (72 × 1000) 1000Hz。占空比 50% 时CCR 500。这种硬件方式的好处是CPU 完全不参与定时器自己就能持续输出波形精度由晶振决定非常稳定。3.3 STM32CubeMX 配置 PWM 的完整流程以 PA8 输出 PWM 为例PA8 对应 TIM1_CH1。用 CubeMX 配置的步骤如下第一步在 Pinout 视图里找到 PA8点击选择 TIM1_CH1。第二步进入 Timers 配置页选择 TIM1Clock Source 选 Internal ClockChannel1 选 PWM Generation CH1。第三步配置参数。Prescaler 设为 71Counter Period 设为 999这样频率是 1kHz。Pulse 设为 500初始占空比 50%。第四步生成代码。CubeMX 会自动生成 MX_TIM1_Init 函数并在 main 函数里调用 HAL_TIM_PWM_Init。第五步在 main 函数里启动 PWMHAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);第六步运行时改变占空比__HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 300); // 占空比变成 30%这套流程适用于绝大多数 STM32 型号。GD32、HC32F460 等国产单片机的配置逻辑类似只是寄存器名称和库函数略有差异。3.4 PWM 模式 1 和模式 2 的区别STM32 的 PWM 输出有两种模式PWM 模式 1 和 PWM 模式 2。模式 1计数值小于 CCR 时输出有效电平大于等于时输出无效电平。 模式 2计数值小于 CCR 时输出无效电平大于等于时输出有效电平。“有效电平”是高还是低由极性配置决定。如果极性设为 High有效电平就是高电平如果设为 Low有效电平就是低电平。实际使用中模式 1 加高极性是最常见的组合输出的是“先高后低”的波形。模式 2 则相反。两者没有本质优劣只是相位不同。有些驱动芯片要求输入特定极性的 PWM这时候就需要根据芯片手册来选。实操心得如果发现 PWM 输出和预期相反先检查极性配置再检查模式设置。这两个地方最容易搞混。4. LED 调光实操从刺眼到柔和4.1 LED 的电气特性为什么不能直接接 GPIO很多人第一次驱动 LED 时会把 LED 直接接到 GPIO 和地之间然后发现灯要么不亮要么烧了。这是因为 LED 是非线性器件它的正向压降基本固定红光约 1.8V蓝光约 3.0V但电流随电压变化极其剧烈。以一颗蓝光 LED 为例正向压降 3.0V。如果 GPIO 输出 3.3V直接接 LED那么 LED 两端电压是 3.0V剩下的 0.3V 加在 GPIO 内阻上。GPIO 内阻通常几十欧姆假设 30Ω电流就是 10mA。看起来还行但温度升高后 LED 压降会降低电流会急剧增大最终可能烧毁 LED 或 GPIO。所以驱动 LED 必须串联限流电阻。电阻值计算R (电源电压 - LED 正向压降) / 目标电流比如 3.3V 电源蓝光 LED 压降 3.0V目标电流 5mA则 R (3.3 - 3.0) / 0.005 60Ω。实际选 68Ω 标准电阻。4.2 PWM 调光的两种方式改变占空比 vs 改变频率用 PWM 调光通常固定频率改变占空比。占空比越大LED 平均电流越大亮度越高。但这里有个细节LED 的亮度与电流的关系不是线性的。在小电流区域亮度随电流增长很快在大电流区域亮度增长变缓。所以占空比从 0% 到 50% 的亮度变化可能比从 50% 到 100% 更明显。如果要做平滑的呼吸灯效果直接线性改变 CCR 值会显得“前快后慢”。更好的做法是使用伽马校正让占空比按指数曲线变化。简单实现可以用查表法const uint16_t gamma_table[32] { 0, 1, 2, 4, 8, 16, 24, 32, 48, 64, 80, 96, 128, 160, 192, 224, 256, 320, 384, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2048, 2560, 3072, 4095 };这个表把 0-31 的输入映射到 0-4095 的 CCR 值低端变化慢高端变化快符合人眼感知。4.3 多路 LED 混色RGB 灯珠的控制逻辑RGB LED 有三颗灯珠分别对应红、绿、蓝。用三路 PWM 分别控制三颗灯珠的亮度就能混合出各种颜色。STM32 上可以用同一个定时器的三个通道比如 TIM3 的 CH1、CH2、CH3分别对应 PA6、PA7、PB0。这样三路 PWM 频率完全同步不会出现颜色闪烁。混色时要注意不同颜色的 LED 正向压降不同同样的占空比下亮度可能差异很大。红光通常最亮蓝光最暗。所以实际调色时需要对各通道做归一化处理。比如红色通道最大占空比限制在 60%绿色 80%蓝色 100%这样三者满亮度时才能混出白色。注意RGB LED 分共阳和共阴两种。共阳时PWM 有效电平为低共阴时有效电平为高。接错的话颜色会完全相反。4.4 LED 驱动芯片的 FB 和 CS 脚什么时候需要外置驱动当 LED 功率较大比如 1W 以上的灯珠GPIO 或普通三极管已经无法提供足够电流这时候就需要专用的 LED 驱动芯片。这类芯片通常有 FB反馈脚和 CS电流采样脚。FB 脚用于电压反馈CS 脚用于电流采样。芯片通过检测 CS 脚上的电压来判断电流大小然后调整开关管的占空比实现恒流输出。用 PWM 调光时有两种方式一种是在 FB 脚注入 PWM 信号改变反馈基准另一种是在 CS 脚做文章用 PWM 控制采样阈值。具体用哪种要看芯片手册。但核心思想是一样的用 PWM 改变电流设定值而不是直接切断电流。直接切断电流的方式叫“斩波调光”简单粗暴但可能导致 LED 在低亮度时闪烁或色偏。改变电流设定值的方式更平滑适合对色彩要求高的场合。5. 直流电机调速从静止到转起来5.1 直流电机的等效模型电阻、电感与反电动势直流电机不是纯电阻负载。它可以用一个等效电路来描述串联的电阻 R、电感 L以及一个与转速成正比的反电动势 E。当电机转动时反电动势 E Ke × ω其中 Ke 是反电动势常数ω 是角速度。电源电压 U 加在电机上电流 I (U - E) / R。启动瞬间转速为 0反电动势为 0电流 I U / R。这个电流叫堵转电流通常是额定电流的 5 到 10 倍。如果电源和驱动电路扛不住这个冲击电机根本转不起来。随着转速升高反电动势增大电流减小。最终达到平衡电磁转矩等于负载转矩转速稳定。用 PWM 调速时改变占空比就改变了等效电压 U_avg U × D。等效电压降低平衡转速也降低。这就是 PWM 调速的物理本质。5.2 为什么电机不能直接接 GPIO电机的堵转电流可能达到几安培而 GPIO 的最大输出电流通常只有 20mA 左右。直接接 GPIO轻则电机不转重则 GPIO 烧毁。所以驱动电机必须用专门的驱动电路。常见方案有三种第一种三极管或 MOSFET 搭建 H 桥。适合小功率电机成本低但需要自己设计电路调试麻烦。第二种集成 H 桥芯片如 L298N、DRV8833、DRV8323。这些芯片内置了逻辑控制和功率开关只需要输入 PWM 和方向信号即可。L298N 适合 1A 以下的电机DRV8833 效率更高DRV8323 则适合三相无刷电机。第三种成品电机驱动模块。比如基于 DRV8323 的模块直接通过 SPI 或 PWM 接口控制适合快速原型开发。不管用哪种方案核心接口都是一样的PWM 控制速度GPIO 控制方向。5.3 用 STM32 控制直流电机的完整接线与代码以 L298N 为例接线如下STM32 引脚L298N 引脚功能PA8 (TIM1_CH1)ENA使能 A 通道PWM 调速PB0IN1方向控制 1PB1IN2方向控制 2GNDGND共地12V 电源12V电机电源代码逻辑// 初始化 PWM HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 正转 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 600); // 60% 速度 // 反转 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, 600); // 停止 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);这里有个细节停止时最好把 PWM 占空比也设为 0否则电机可能处于“刹车”状态而不是自由停止。刹车状态是 IN1 和 IN2 同时为高或同时为低电机绕组短路产生制动效果。自由停止则是 IN1 和 IN2 都为低电机惯性滑行。5.4 电机调速的平滑性频率与死区时间电机调速时如果 PWM 频率太低比如 100Hz电机会有明显的抖动和噪音。因为每个 PWM 周期内电机经历一次“通电-断电”转速波动大。提高频率可以改善平滑性但也不能无限提高。频率越高MOSFET 的开关损耗越大驱动芯片发热越严重。一般 5kHz 到 20kHz 是比较好的平衡点。另外H 桥驱动需要设置死区时间。死区时间是上下桥臂同时关断的一小段时间防止直通短路。STM32 的高级定时器TIM1、TIM8硬件支持死区插入配置起来很方便。通用定时器则需要软件处理或者外接死区控制芯片。实操心得调试电机时先用低占空比比如 10%测试确认方向正确、没有异常发热再逐步提高。直接上 100% 占空比如果接线有误可能瞬间烧毁驱动芯片。6. 常见问题与排查技巧实录6.1 PWM 没有输出从引脚到时钟逐级排查PWM 配置好了但引脚没波形这是最常见的问题。排查顺序如下第一检查 GPIO 是否配置为复用推挽输出。如果配成了普通输出定时器信号出不来。第二检查定时器时钟是否使能。在 CubeMX 里配置了定时器但生成的代码可能没有调用__HAL_RCC_TIM1_CLK_ENABLE()。第三检查 PWM 启动函数是否调用。HAL_TIM_PWM_Start必须调用否则定时器不工作。第四检查 CCR 值是否大于 ARR。如果 CCR 大于 ARR占空比 100%输出恒高看起来像没有 PWM。第五检查引脚是否被其他外设占用。比如 PA8 可能被配置成了其他功能。6.2 LED 低亮度闪烁频率与视觉暂留的博弈LED 在低占空比时闪烁通常是因为 PWM 频率太低。人眼在低亮度时对闪烁更敏感因为瞳孔放大进光量增加闪烁更容易被察觉。解决办法很简单提高 PWM 频率到 1kHz 以上。如果已经很高了还闪检查电源是否稳定或者 LED 驱动电路是否有问题。另一个可能的原因是占空比分辨率不够。比如 ARR 设为 99占空比只能以 1% 为步进。在低亮度区域1% 的变化可能对应很大的亮度跳变。解决办法是增大 ARR 值比如设为 999提高分辨率。6.3 电机启动困难堵转电流与电源容量电机启动时电流很大如果电源容量不足电压会被拉低导致单片机复位或驱动芯片欠压保护。解决办法第一电源要留足余量至少能提供电机堵转电流的 1.5 倍。第二在电源端并联大电容比如 1000μF提供瞬时电流。第三启动时用软启动占空比从 0 逐渐增加到目标值避免电流冲击。6.4 PWM 干扰显示背光调光与 LCD 的兼容问题用 PWM 调 LCD 背光时有时会发现屏幕上有横纹或闪烁。这是因为 PWM 信号通过电源或地线耦合到了 LCD 的模拟电路。解决办法第一提高 PWM 频率到 20kHz 以上超出音频范围。第二在背光 LED 两端并联电容滤除高频纹波。第三PWM 走线远离 LCD 的模拟信号线。第四如果条件允许用独立的 LDO 给 LCD 模拟部分供电。6.5 常见问题速查表现象可能原因排查方法PWM 无输出GPIO 模式错误检查是否配置为复用推挽PWM 无输出定时器时钟未使能检查 RCC 配置PWM 无输出启动函数未调用检查 HAL_TIM_PWM_Start占空比不对CCR 值设置错误检查 CCR 与 ARR 的关系LED 闪烁PWM 频率太低提高到 1kHz 以上LED 闪烁占空比分辨率不够增大 ARR 值电机不转驱动电流不足检查驱动芯片和电源电机抖动PWM 频率太低提高到 5kHz 以上电机发热频率太高或死区不当降低频率或调整死区屏幕横纹PWM 干扰提高频率或加滤波电容7. 从原理到实践我的几点体会PWM 这个东西原理说起来简单就是开关加时间平均。但真正用好需要考虑的东西很多频率选多少、占空比怎么变化、驱动电路怎么设计、干扰怎么抑制。我刚开始做电机调速时犯过一个典型错误PWM 频率设了 100Hz结果电机噪音大得像个蜂鸣器。后来提高到 15kHz世界瞬间安静了。还有一次调 LED 背光频率设了 200Hz屏幕上有明显的横纹折腾了半天才发现是 PWM 耦合到了 LCD 的电源上加了个电容就好了。这些经验书本上不会写只有自己踩过坑才记得住。所以我的建议是先理解原理再动手实验遇到问题从最基础的地方查起。GPIO 模式、时钟使能、启动函数这三个地方检查一遍能解决 80% 的 PWM 问题。最后分享一个小技巧调试 PWM 时如果手头没有示波器可以用一个 LED 加限流电阻接到输出引脚上。如果 LED 亮度可调说明 PWM 基本正常。虽然看不到波形细节但至少能判断有没有输出。这个方法在紧急排查时特别管用。