1. 项目概述:PWM在嵌入式竞赛中的核心地位
在蓝桥杯嵌入式设计与开发竞赛中,PWM(脉冲宽度调制)技术是一个绕不开的核心考点,也是连接软件逻辑与硬件执行的关键桥梁。很多新手选手初次接触时,往往觉得它只是一个简单的“输出不同占空比方波”的功能,但在实际的项目开发中,尤其是涉及到电机控制、LED调光、蜂鸣器发声等场景时,对PWM的深入理解和灵活运用,直接决定了作品的控制精度、响应速度和整体稳定性。
我参加过多次竞赛的评审和指导工作,发现很多队伍在基础功能实现上没问题,但一旦要求精细控制或动态调整,就容易出现电机抖动、灯光闪烁、功耗异常等问题,其根源大多在于对PWM的工作机制、定时器配置细节以及负载特性匹配理解不深。本章我们将彻底拆解PWM,不仅告诉你STM32G431(蓝桥杯嵌入式竞赛指定平台)的定时器如何配置出PWM,更会深入探讨在不同应用场景下,参数该如何计算、配置时有哪些隐藏的“坑”,以及如何通过PWM实现一些看似简单却极易出错的复杂效果。无论你是刚入门的新手,还是希望优化作品的老手,相信这些从一线实战中总结出的经验,都能让你对PWM有一个全新的、立体的认识。
2. 核心原理与硬件架构解析
2.1 PWM的本质:不是模拟,而是数字的“欺骗艺术”
PWM,中文叫脉冲宽度调制。它的核心思想非常巧妙:用数字信号来模拟模拟量输出。单片机是数字世界的产物,它的GPIO引脚通常只能输出高电平(如3.3V)或低电平(0V)。但我们控制的很多设备,如电机的转速、LED的亮度,需要的是连续变化的电压或电流(模拟量)。PWM通过快速开关GPIO,并调整一个周期内高电平所占的时间比例(即占空比),来让负载“感受”到一个平均电压。
举个例子,假设系统电压是3.3V,一个占空比为50%的PWM波,其平均输出电压就是3.3V * 50% = 1.65V。虽然电压实际上是在0V和3.3V之间剧烈跳变,但由于电机线圈、LED等负载本身具有惯性(电感、视觉暂留),它们无法响应如此高频的变化,最终表现出的效果就是平滑的1.65V驱动效果。这就是PWM的“欺骗艺术”,它用数字的方法,经济高效地解决了模拟控制的问题。
在STM32中,PWM功能通常由高级/通用定时器(TIM1, TIM2, TIM3, TIM4等)的输出比较通道产生。定时器就像一个精准的时钟,不断地从0计数到我们设定的重装载值(ARR),然后清零重新开始,如此循环。PWM通道则在这个循环中,设置了一个比较值(CCR)。当定时器的计数值(CNT)小于CCR时,输出一种电平(通常为高);当CNT大于等于CCR但小于ARR时,输出另一种电平(通常为低)。通过改变CCR的值,我们就改变了高电平的时间,即改变了占空比。
2.2 STM32G431的定时器与PWM资源盘点
蓝桥杯嵌入式竞赛使用的STM32G431RB单片机,其定时器资源非常丰富,我们需要根据需求合理分配。
- 高级控制定时器(TIM1):功能最强大,可以产生带死区互补的PWM,常用于驱动三相电机等复杂场景。它有4个通道,每个通道都可以独立输出PWM。
- 通用定时器(TIM2, TIM3, TIM4, TIM15, TIM16, TIM17):这些是我们最常用来产生普通PWM的定时器。其中TIM2/3/4是完整的16位定时器,有4个通道。TIM15/16/17通道数较少,但功能也足够。
- 基本定时器(TIM6, TIM7):没有输出通道,不能直接产生PWM,一般用作时基或触发源。
注意:在竞赛提供的CT117E-M4开发板上,部分定时器引脚已经连接到了固定外设。例如,TIM3的通道3(PB0)和通道4(PB1)连接了板载的LED(LD1和LD2)。这意味着如果你要用这两个LED做PWM调光,就必须使用TIM3的这两个特定通道,不能随意选用其他定时器。规划资源时,务必结合原理图进行。
2.3 关键参数计算:频率、周期与占空比的关系
这是配置PWM时最容易出错的地方。三个核心参数:定时器时钟源频率(Fclk)、自动重装载值(ARR)、预分频系数(PSC),共同决定了PWM的周期(或频率);而捕获/比较寄存器值(CCR)则决定了占空比。
PWM频率(Fpwm)计算公式:Fpwm = Fclk / [(PSC + 1) * (ARR + 1)]
PWM周期(Tpwm)计算公式:Tpwm = 1 / Fpwm = [(PSC + 1) * (ARR + 1)] / Fclk
占空比(Duty)计算公式:Duty = CCR / (ARR + 1) * 100%
举个例子:假设我们使用STM32G431的系统时钟为80MHz,TIM2挂载在APB1总线上(时钟也是80MHz)。我们希望产生一个1kHz的PWM波。
- 目标频率 Fpwm = 1kHz = 1000 Hz。
- 定时器时钟 Fclk = 80MHz = 80,000,000 Hz。
- 我们可以先设定预分频器 PSC = 79。这样,定时器的实际计数时钟 = Fclk / (79+1) = 1MHz。
- 此时,为了得到1kHz的频率,我们需要 ARR = (1MHz / 1kHz) - 1 = 1000 - 1 = 999。
- 验证:Fpwm = 80,000,000 / [(79+1)(999+1)] = 80,000,000 / (801000) = 1000 Hz。
- 若想要50%占空比,则设置 CCR = (ARR + 1) * 50% = 1000 * 0.5 = 500。
实操心得:ARR的值决定了PWM的分辨率。ARR=999时,占空比最小调节步进是0.1%。如果你需要非常精细的调光(如10000级灰度),就需要设置更大的ARR值,但这会降低PWM频率。所以频率和分辨率是一对矛盾,需要根据负载特性权衡。对于LED调光,人眼对100Hz以上的闪烁就不敏感了,所以可以优先保证分辨率,频率设在200Hz-1kHz即可。对于电机控制,频率太低会导致噪音大、运行不平稳,通常需要几千Hz到几十kHz,此时分辨率就会相应下降。
3. 基于HAL库的PWM输出配置实战
3.1 工程创建与定时器基础配置
我们以使用TIM2的通道1(PA0引脚)输出PWM为例,进行完整配置讲解。首先使用STM32CubeMX创建工程。
引脚配置:在
Pinout & Configuration视图下,找到TIM2,展开其通道,选择CH1为PWM Generation CH1。此时,CubeMX会自动将PA0引脚功能映射为TIM2_CH1。定时器参数配置:切换到
Configuration标签页,点击TIM2进行参数设置。- Prescaler (PSC - 预分频系数):设置为79。如上计算,从80MHz分频到1MHz计数时钟。
- Counter Mode (计数模式):选择
Up(向上计数),这是最常用的PWM模式。 - Counter Period (ARR - 自动重装载值):设置为999。得到1kHz的PWM频率。
- Internal Clock Division (时钟分频):保持默认
No Division。 - auto-reload preload (自动重载预装载):建议使能(Enable)。这样只有在更新事件发生时,新的ARR值才会生效,可以防止在修改参数时产生破碎的PWM脉冲。
- PWM Generation Channel 1子菜单:
- Mode:
PWM mode 1。在此模式下,当CNT<CCR时输出有效电平(Active),CNT≥CCR时输出无效电平(Inactive)。 - Pulse (CCR - 脉冲值):先设置为500,即初始占空比50%。
- Output compare preload (输出比较预装载):务必使能(Enable)。这是关键!使能后,CCR值的修改会在下次更新事件生效,避免在任意时刻修改CCR导致当前周期波形异常。
- Fast Mode (快速模式):禁用。用于紧急清除输出,普通应用不需要。
- CH Polarity (通道极性):
High。表示有效电平为高电平。如果你希望默认输出低电平,PWM有效时拉高,则选择Low。
- Mode:
生成代码:配置时钟树(确保系统时钟正确)后,生成工程代码。
3.2 代码编写与动态调参技巧
CubeMX生成的代码已经完成了定时器底层和GPIO的初始化。我们需要在用户代码中启动PWM并动态改变占空比。
// 在main.c的合适位置(如用户代码区2) /* USER CODE BEGIN 2 */ HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 启动TIM2的通道1 PWM输出 /* USER CODE END 2 */ // 在循环中或某个函数中修改占空比 /* USER CODE BEGIN WHILE */ while (1) { // 示例:让占空比从0%线性增加到100%,再减少,形成呼吸灯效果 for(uint16_t i=0; i<=1000; i++) // 注意:我们的ARR是999,分辨率是1000级 { __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i); // 修改CCR值 HAL_Delay(1); // 延时1ms,控制变化速度 } for(uint16_t i=1000; i>0; i--) { __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i); HAL_Delay(1); } } /* USER CODE END WHILE */关键函数解析:
HAL_TIM_PWM_Start(&htimx, TIM_CHANNEL_y):启动指定定时器通道的PWM输出。__HAL_TIM_SET_COMPARE(&htimx, TIM_CHANNEL_y, value):这是一个宏,用于安全地修改捕获/比较寄存器(CCRy)的值。由于我们使能了“输出比较预装载”,这个新值会在当前PWM周期结束后的下一个周期生效,从而保证波形完整。
注意事项:
HAL_Delay()在延时期间会阻塞CPU。在真正的呼吸灯或电机调速应用中,如果系统还有其他任务(如按键扫描、显示刷新),使用阻塞延时会导致其他任务卡顿。更好的做法是使用定时器中断,在中断服务函数里非阻塞地更新CCR值。例如,设置一个1ms的定时器中断,在中断里用一个变量累加或递减,然后用这个变量去更新CCR,这样主循环就完全自由了。
3.3 多通道与互补PWM配置
有时我们需要同时控制多个设备,或者控制一个直流电机正反转(H桥驱动),这就需要用到多通道甚至互补输出。
多通道独立PWM:配置非常简单,在CubeMX中使能同一个定时器的多个通道(如TIM2的CH1, CH2, CH3, CH4)即可。它们共享同一个ARR(即频率相同),但各有独立的CCR寄存器,可以设置不同的占空比。在代码中分别启动和设置即可。
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty1); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, duty2);互补PWM(带死区):这是驱动H桥的关键,可以防止上下桥臂直通短路。通常使用高级定时器(如TIM1)的互补通道功能。
- 在CubeMX中配置TIM1的一个通道(如CH1)为
PWM Generation CH1,其对应的互补通道(CH1N)会自动关联。 - 在参数配置中,会多出一个
Break and Dead-Time(断路和死区时间)设置。 - 死区时间(Dead Time)必须设置。它是一个非常短的时间(通常几十到几百纳秒),确保在切换上下管时,一个管子完全关闭后,另一个管子才打开。死区时间由
Dead Time参数设置,其值需要根据你使用的MOSFET或驱动芯片的开关速度来计算。CubeMX会自动计算并写入相应的寄存器。 - 代码中需要使用
HAL_TIMEx_PWMN_Start来启动互补通道。
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 启动主通道 HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); // 启动互补通道4. PWM典型应用场景与实战代码
4.1 LED呼吸灯(板载LED调光)
利用板载LED(连接在TIM3_CH3/CH4)实现呼吸灯,是检验PWM掌握程度的经典实验。
硬件连接:查看原理图,LD1 (PB0) -> TIM3_CH3, LD2 (PB1) -> TIM3_CH4。CubeMX配置:配置TIM3,CH3和CH4为PWM Generation。PSC和ARR根据呼吸灯平滑度需求设置。例如,设PSC=799,ARR=99,则PWM频率=80MHz/(800*100)=1kHz,分辨率100级,足够平滑。代码实现:
// 启动PWM HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4); // 呼吸灯效果(非阻塞式,利用系统滴答定时器) uint32_t last_tick = 0; uint16_t pwm_val = 0; int8_t dir = 1; // 方向,1为增加,-1为减少 while (1) { if(HAL_GetTick() - last_tick >= 10) { // 每10ms更新一次 last_tick = HAL_GetTick(); pwm_val += dir; if(pwm_val >= 100) dir = -1; if(pwm_val <= 0) dir = 1; __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, pwm_val); __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, 100 - pwm_val); // 两个灯反向变化 } // 此处可以执行其他任务,如按键扫描 }技巧:两个LED反向变化(一个变亮一个变暗)比同向变化更具视觉美感。通过计算
100 - pwm_val轻松实现。
4.2 舵机(Servo)控制
舵机通过PWM信号的脉冲宽度来定位角度,是一种非常常见的执行器。标准舵机的控制信号是周期为20ms(50Hz),脉冲宽度在0.5ms到2.5ms之间的PWM波,对应角度-90°到+90°(或0°到180°)。
参数计算:控制舵机,频率固定为50Hz,重点是精确控制高电平时间。
- 周期 T = 20ms = 0.02s。
- 定时器时钟 Fclk = 80MHz。
- 设定 ARR = 19999。因为 0.02s * 80MHz = 1,600,000 个时钟周期。但注意,定时器计数从0到ARR,所以ARR = 周期计数 - 1 = 1,600,000 - 1。这个值超过了16位定时器的最大值(65535),因此必须使用32位的定时器(如TIM2)或者对时钟进行预分频。
- 更实用的配置:对80MHz进行80分频,得到1MHz的计数时钟。此时,20ms需要 20,000 个计数周期。所以设置 PSC=79, ARR=19999。这样,1个计数就代表1us。
- 0.5ms高电平 -> CCR = 500
- 1.5ms高电平(中位)-> CCR = 1500
- 2.5ms高电平 -> CCR = 2500
代码实现:
// 初始化,使用TIM2,配置为PSC=79, ARR=19999 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 控制舵机转到中位(90度) __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1500); HAL_Delay(500); // 给舵机转动时间 // 控制舵机转到0度 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 500); HAL_Delay(500);重要提醒:舵机有堵转电流,不能长时间卡在极限位置,驱动电源要充足。竞赛中若用到,最好单独供电。
4.3 直流电机调速(通过MOSFET或电机驱动模块)
直流电机调速本质是调整平均电压。我们可以直接用PWM驱动一个MOSFET的栅极,或者使用集成的电机驱动芯片(如L298N、TB6612)。
硬件连接:PWM引脚连接驱动芯片的使能或速度控制引脚。频率选择:电机是感性负载,PWM频率不能太低,否则会有明显的噪音和振动;也不能太高,否则MOSFET开关损耗大。一般选择5kHz到20kHz之间。我们以10kHz为例。
- Fpwm = 10kHz。
- Fclk = 80MHz。
- 设置 PSC = 7,则计数时钟 = 80MHz / 8 = 10MHz。
- 则 ARR = (10MHz / 10kHz) - 1 = 999。
- 此时,占空比分辨率是1/1000。
代码实现:与LED控制类似,但频率更高。可以通过电位器(ADC读取)来实时调整CCR值,实现闭环调速的模拟。
// 假设ADC读取的电位器值(0-4095)存放在变量adc_val中 uint16_t motor_duty = adc_val / 4; // 将0-4095映射到0-1023,防止超限 if(motor_duty > 1000) motor_duty = 1000; // 限制在ARR范围内 __HAL_TIM_SET_COMPARE(&htimx, TIM_CHANNEL_y, motor_duty);5. 高级话题与性能优化
5.1 使用DMA自动更新PWM占空比
在需要产生复杂、精确的PWM波形序列时(如步进电机细分驱动、特定波形合成),频繁在中断中修改CCR会消耗大量CPU资源,且时序可能受其他中断影响。此时,DMA(直接存储器访问)是完美解决方案。
思路:将预先计算好的一个完整波形周期的所有CCR值,存放在一个数组里。然后配置DMA,让定时器的CCR寄存器与这个数组建立联系。DMA会在每次定时器更新事件(或比较匹配事件)发生时,自动将数组中的下一个值搬运到CCR寄存器,完全无需CPU干预。
CubeMX配置步骤:
- 在
DMA Settings标签页,为对应的定时器(如TIM2)的CH1(或CHx_UP更新事件)添加DMA请求。 - 设置方向为
Memory To Peripheral,数据宽度为Word(CCR是32位寄存器,但通常用16位数据)。 - 在代码中,定义一个数组
uint16_t pwm_data_buffer[BUFFER_SIZE];并填充波形数据。 - 使用HAL库函数启动DMA传输:
HAL_TIM_PWM_Start_DMA(&htim2, TIM_CHANNEL_1, (uint32_t*)pwm_data_buffer, BUFFER_SIZE); - 可以开启DMA的循环模式,让波形自动重复播放;也可以在DMA传输完成中断中,更换数据缓冲区,实现动态波形切换。
5.2 定时器主从模式与PWM同步
在需要多个PWM信号严格同步,或者一个PWM作为另一个定时器的时钟基准时,就需要用到定时器的主从模式。
典型应用:两个电机需要完全同步转动。我们可以配置TIM2为主模式(Master),输出一个触发信号(如更新事件);配置TIM3为从模式(Slave),将其时钟源设置为ITR1(内部触发,连接到TIM2)。这样,TIM3的计数就完全由TIM2的更新事件来驱动,两个定时器产生的PWM波在相位和频率上就完全锁定了。
CubeMX配置:
- 主定时器(TIM2):在
Parameter Settings->Trigger Output (TRGO) Parameters->Master Mode Selection中选择Update Event。 - 从定时器(TIM3):在
Slave Mode区域,Slave Mode Selection选择External Clock Mode 1,Trigger Source选择ITR1(根据芯片手册,ITR1对应TIM2)。
5.3 测量PWM频率与占空比(输入捕获模式)
PWM不仅可以输出,还可以测量。这是实现编码器测速、遥控器信号解码等功能的基础。STM32的定时器输入捕获功能可以精准测量外部PWM的高电平时间(脉宽)和周期。
原理:配置定时器通道为输入捕获模式。当引脚上出现上升沿时,硬件会自动将当前定时器计数值(CNT)锁存到捕获/比较寄存器(CCR)中,并产生中断。在中断中读取CCR值,并记录这次是上升沿。当下降沿到来时,再次捕获CNT值。两次捕获值之差,乘以计数周期,就是高电平时间。通过计算连续两个上升沿之间的时间差,就可以得到PWM周期。
代码逻辑(以测量周期和占空比为例):
- 开启上升沿和下降沿捕获。
- 在捕获中断中:
- 如果是上升沿,记录当前捕获值
rise1,并切换为下降沿捕获。 - 如果是下降沿,记录当前捕获值
fall,高电平时间 =fall - rise1。切换为上升沿捕获。 - 在下一个上升沿,记录
rise2,周期 =rise2 - rise1。 - 占空比 = 高电平时间 / 周期。
- 如果是上升沿,记录当前捕获值
避坑指南:输入捕获对高频信号测量时,要注意定时器溢出问题。如果PWM周期可能超过定时器从0计数到ARR的时间,必须开启定时器更新中断,在中断中对一个溢出计数器进行加减,并在计算时间时将此溢出次数考虑进去。例如,定时器是16位(ARR=65535),测量一个更长的脉冲,可能中间经历了N次定时器溢出,那么实际时间 =
(溢出次数 * 65536 + 本次捕获值) * 计数周期。
6. 常见问题排查与调试技巧
6.1 PWM无输出或波形异常
这是最常遇到的问题,可以按照以下清单逐一排查:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无输出 | 1. 定时器未使能时钟。 2. 未启动PWM输出( HAL_TIM_PWM_Start)。3. GPIO引脚模式配置错误(应为Alternate Function Push-Pull)。 4. 引脚复用功能未映射到正确定时器。 | 1. 检查CubeMX的时钟配置和生成的MX_TIMx_Init函数。2. 确认代码中调用了Start函数。 3. 查看生成的 gpio.c文件,确认引脚初始化代码。4. 核对数据手册的引脚复用表。 |
| 输出恒定高/低电平 | 1. CCR值设置异常(为0或等于ARR+1)。 2. PWM极性配置反了。 3. 高级定时器的刹车(Break)功能被使能。 | 1. 调试时单步执行,查看CCR寄存器值。 2. 检查CubeMX中 CH Polarity设置。3. 检查高级定时器的 Break and Dead-Time配置。 |
| 频率不对 | 1. 定时器时钟源频率计算错误。 2. PSC或ARR值计算或设置错误。 3. 定时器时钟分频( Internal Clock Division)被误改。 | 1. 使用示波器测量实际频率。 2. 根据公式重新计算,并检查代码中 htimx.Init.Prescaler和htimx.Init.Period的值。3. 确认CubeMX中该参数为 No Division。 |
| 占空比变化不线性或跳变 | 1. 未使能“自动重载预装载”(ARR preload)和“输出比较预装载”(CCR preload)。 2. 在中断或主循环中修改CCR的代码有逻辑错误。 3. 修改CCR的时机不当,打断了当前周期。 | 1.务必在CubeMX中使能这两个预装载功能! 2. 检查修改CCR的变量计算过程。 3. 确保使用 __HAL_TIM_SET_COMPARE宏,它考虑了预装载机制。 |
6.2 电机控制中的异常噪音与抖动
用PWM驱动电机时,如果听到刺耳的啸叫声或感觉电机抖动:
- 频率过低:电机线圈在PWM频率处于人耳可听范围(20Hz-20kHz)内时,会因振动发出声音。尝试将频率提高到16kHz以上,通常能有效消除可闻噪音。
- 电源问题:电机启动和制动时电流很大,可能导致电源电压被拉低,影响单片机稳定工作。务必为电机驱动部分提供独立、功率充足的电源,并在电机电源端并联大容量(如100uF以上)的电解电容和多个小容量(0.1uF)陶瓷电容进行退耦。
- 死区时间不足(H桥驱动):如果使用互补PWM驱动H桥,死区时间设置太短会导致上下桥臂有瞬间同时导通的风险,产生很大的短路电流,不仅发热严重,也可能引起抖动和噪音。需要根据MOSFET的开关速度(查看数据手册中的
Turn-on/off delay和Rise/fall time)适当增加死区时间。
6.3 使用逻辑分析仪或示波器进行调试
“眼见为实”在硬件调试中至关重要。没有仪器,调试PWM就像盲人摸象。
- 逻辑分析仪:价格亲民,非常适合数字信号时序分析。可以同时抓取多路PWM信号,清晰显示频率、占空比、相位关系。是分析多路PWM同步、死区时间是否足够的利器。
- 示波器:可以观察PWM波形的实际形状,查看上升/下降沿是否陡峭,有没有过冲或振铃(这可能是布线不良导致的信号完整性问题)。测量电机驱动端的波形时,务必使用差分探头或确保示波器接地良好,避免短路。
调试时,首先测量单片机引脚输出的PWM信号,确认软件配置正确。然后将探头移到电机驱动芯片的输出端或MOSFET的漏极,观察经过驱动后的信号质量。最后测量电机两端的电压波形,这才是真正加载在负载上的PWM。
我个人在项目中最深刻的体会是,PWM的配置公式并不难,难的是将理论参数与真实的物理世界匹配起来。一个在仿真里完美的1kHz PWM,驱动真实的电机时可能因为电源内阻、导线电感、负载反电动势等因素而变得不理想。因此,永远不要停留在软件层面,多用仪器测量,多观察实际现象,根据现象反推问题根源,这才是嵌入式工程师从入门到精通的必经之路。当你能够熟练运用PWM,并理解其背后每一个参数对硬件产生的实际影响时,你对嵌入式系统的控制能力就上了一个全新的台阶。