1.PWM概念介绍
PWM叫脉宽调制,它可以实现用软件来模拟模拟量的变化,比如正弦波。本质就是在一定的周期里调节脉宽,使输出类似模拟量。
概念一:什么是脉宽时间,就是高电平的时间
概念二:什么是周期,从低电平变到高电平,在从高电平变到低电平所用的总时间,算是一个完整周期。
概念三:什么是占空比?占空比就是高电平占总周期的比例
2.案例一:用PWM控制电机速度(这里用51单片机举例)
第一步:先配置定时器1,100us产生一次中断
void Timer1_Init(void) //100微秒@11.0592MHz
{
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xA4; //设置定时初始值
TH1 = 0xA4; //设置定时重载值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
ET1 = 1; //使能定时器1中断
EA = 1;
}
第二步:在定时器1的中断服务函数中加加计数值,当计数值小于比较值,电机驱动,反之电机停转。
unsigned char Counter,Compare; //计数值和比较值,用于输出PWM
//定时器1中断服务函数
void Timer1_Isr(void) interrupt 3
{
Counter++;
Counter%=100; //计数值变化范围限制在0~10
Compare = 100;
if(Counter<Compare)
{
ENA=1; //输出1
ENB=1; //输出1
}
else //计数值大于比较值
{
ENA=0; //输出0
ENB=0; //输出0
}
}
这样,我们就实现了电机控速的功能。
此时PWM的频率为100Hz,当然电机的频率低于1k可能会有噪声。
3.案例二:呼吸灯(这里用stm32f1+hal库举例)
for(int i=0;i<100;i++){
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,i);
HAL_Delay(20);
}
for(int i=99;i>0;i--){
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,i);
HAL_Delay(20);
}
上面是核心代码,本质就是维持一个比较值一段时间,如何在增加比较值,使得占空比呈现一个逐渐增大的趋势,LED灯越来越亮
像上图这样。
完成这样一个呼吸灯的周期是20*100+20*100= 4000ms=4s(这个不是PWM的周期)
图中的纵轴为占空比,横轴为时间
如图可以计算,PWM周期为10000Hz
计算流程如上
4.案例三:呼吸灯优化--正弦函数
#include <math.h> // 需要包含数学库以使用 sinf 函数
// ... (可能需要包含HAL库GPIO头文件,如 "gpio.h")
// ... (假设 ucLed 数组 和 led_disp 函数已在别处定义)
extern uint8_t ucLed[6];
extern void led_disp(uint8_t *ucLed);
/**
* @brief LED 显示处理函数 - 呼吸灯效果 (在主循环中周期性调用)
*/
void led_proc(void)
{
// 呼吸灯相关变量 (使用 static 确保它们在函数调用之间保持值)
static uint32_t breathCounter = 0; // 呼吸效果的内部计时器,模拟时间流逝
static uint8_t pwmCounter = 0; // 软件PWM的内部计数器,用于生成PWM波形
static uint8_t brightness = 0; // 当前计算出的LED亮度值 (0-pwmMax)
static const uint16_t breathPeriod = 2000; // 定义一个完整的呼吸周期时长 (单位:毫秒或调用次数,取决于调用频率)
static const uint8_t pwmMax = 10; // 软件PWM周期的最大计数值 (决定PWM精度和频率)
// 更新呼吸计时器:每次调用函数时加1,达到周期后归零
// 这个计数器相当于呼吸效果的时间轴
breathCounter = (breathCounter + 1) % breathPeriod;
// 核心:计算当前时刻的亮度值
// 使用正弦函数 (sinf) 来模拟平滑的亮度变化
// (2.0f * 3.14159f * breathCounter) / breathPeriod 将 breathCounter 映射到 0 到 2π 的弧度范围
// sinf(...) 的结果在 -1.0 到 1.0 之间
// (sinf(...) + 1.0f) 将范围变为 0.0 到 2.0
// * pwmMax / 2.0f 将范围缩放到 0 到 pwmMax,即我们期望的亮度范围
brightness = (uint8_t)((sinf((2.0f * 3.14159f * breathCounter) / breathPeriod) + 1.0f) * pwmMax / 2.0f);
// 更新软件PWM计数器:每次调用函数时加1,达到 pwmMax 后归零
// 这个计数器用于在 pwmMax 的周期内比较亮度,决定当前时刻LED是亮还是灭
pwmCounter = (pwmCounter + 1) % pwmMax;
// 软件PWM逻辑:
// 如果 pwmCounter 小于当前的亮度值 brightness,则LED应该亮 (ucLed[0] = 1)
// 否则,LED应该灭 (ucLed[0] = 0)
// 效果:brightness 越大,LED在一个PWM周期内亮的时间越长,看起来就越亮
// 当 brightness 为 0 时,pwmCounter 永远不小于 0,LED 始终灭
// 当 brightness 为 pwmMax 时,pwmCounter 始终小于 pwmMax,LED 始终亮
ucLed[0] = (pwmCounter < brightness) ? 1 : 0; // 控制第一个LED (ucLed[0])
// 调用之前定义的 led_disp 函数,将计算好的 ucLed 状态更新到实际的GPIO引脚
led_disp(ucLed); // 注意:led_disp内部最好也有优化,避免状态不变时重复写GPIO
}
易知:PWM的周期为10ms,呼吸灯的周期是2s
pmwMax是一个时间窗口,pwmCounter是pwm的计数值,也就是把一个周期的PWM分成几块,如pwmCounter为10,则把一个周期的PWM分成十个时间片
优点是波形平滑,和案例二线性的比较的话,pwmMax越大,越精细,波形更平滑,当然不能一直大下去。