第一次接触PWM调光是在大二电子设计课上,当时看着LED灯从暗到亮平滑变化,感觉特别神奇。后来参加蓝桥杯比赛才发现,这竟然是单片机竞赛的必考题型!PWM(脉冲宽度调制)本质上是通过快速开关电路来控制平均功率的技术。举个例子,就像用自来水龙头接一桶水——如果全程开最大水流,1秒钟就能接满;但如果快速开关龙头,让水流累计时间只有0.3秒,得到的水量就是30%。PWM正是用数字信号模拟这种"断续供电"的效果。
在蓝桥杯单片机开发板上,LED的工作电压通常是3.3V。当我们给LED施加50%占空比的PWM信号时(比如高电平1ms+低电平1ms循环),LED实际获得的平均电压就是1.65V,表现为半亮度状态。这里有两个关键参数需要特别注意:
去年省赛中有道题要求用PWM实现8级亮度调节,很多选手栽在周期设置上——有人用了10ms周期,结果评委一眼就看出灯光在闪烁。后来实测发现,当PWM频率超过400Hz(周期2.5ms以下)时,人眼就基本感知不到闪烁了。这也是为什么在后续的代码示例中,我都采用1kHz(1ms周期)作为基准值。
STC15系列单片机自带增强型定时器,配置成1T模式后,12MHz晶振下最小能实现1μs的定时精度。这为我们构建精准PWM提供了硬件基础。下面这段初始化代码我用了三年都没翻车:
c复制void Timer0Init(void) //1微秒@12.000MHz
{
AUXR |= 0x80; //定时器时钟1T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0xF4; //设置定时初值
TH0 = 0xFF; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //开启定时器中断
}
实现PWM的关键在于中断服务程序的设计。我的独门秘笈是用一个结构体管理所有PWM参数:
c复制typedef struct {
uint cnt; //当前计数值
uint max; //周期总步数
uint highCnt; //高电平步数
} pwm_t;
pwm_t pwm1 = {0, 1000, 300}; //1ms周期,30%占空比
中断服务程序要处理三个关键节点:
c复制void Timer0Handle() interrupt 1
{
if(pwm1.cnt == 0) {
LED = 0; //周期开始点亮LED
}
else if(pwm1.cnt == pwm1.highCnt) {
LED = 1; //达到占空比关闭LED
}
if(++pwm1.cnt > pwm1.max) {
pwm1.cnt = 0; //周期重置
}
}
这种实现方式的优势在于:
呼吸灯的本质是让占空比随时间呈正弦变化。但在实际比赛中,更常见的做法是用线性变化+查表法。这里分享一个经过实战检验的方案:
c复制uint breathTable[] = {10,30,60,100,150,210,280,360,450,550,660,780,900,1000,
900,780,660,550,450,360,280,210,150,100,60,30,10}; //27级亮度表
void main()
{
uint index = 0;
while(1) {
if(++index >= 27) index = 0;
pwm1.highCnt = breathTable[index];
Delayms(30); //控制呼吸速度
}
}
这个方案有三大亮点:
在2023年省赛中,有个陷阱题要求呼吸周期控制在3秒±0.5秒。很多选手直接用线性递增递减算法,结果因为函数调用耗时导致周期超标。而查表法由于执行时间恒定,很容易通过调整表格大小和延时参数来精确控制周期。
当遇到需要同时控制多个LED不同亮度的需求时,可以采用分时复用的方法。下面这个案例实现了8个LED从暗到亮的梯度变化,同时整体做流水移动:
c复制uint pwmValue[8] = {50,100,150,200,250,300,350,400}; //各灯亮度基准
uchar ledMask = 0xFE; //流水灯位置掩码
void Timer0Handle() interrupt 1
{
static uint cnt;
uchar i;
if(++cnt > 1000) cnt = 0; //1ms周期
for(i=0; i<8; i++) {
if((ledMask & (1<<i)) && (cnt < pwmValue[i])) {
LED = 0; //在占空比时间内点亮
} else {
LED = 1;
}
}
}
void liushui()
{
static uint delay;
if(++delay > 500) { //每500ms移动一次
delay = 0;
ledMask = (ledMask << 1) | (ledMask >> 7); //循环左移
}
}
这段代码的精妙之处在于:
在调试这类复合效果时,建议先用示波器确认各通道PWM信号是否正常,再观察整体视觉效果。有个常见问题是中断服务程序执行时间过长,导致PWM波形畸变。这时可以优化代码结构,或者降低PWM分辨率(比如从1000步降到500步)。
在实验室能跑通的代码,到了赛场可能就出问题。去年国赛时就遇到PWM输出不稳定的情况,后来发现是中断优先级配置问题。这里分享几个血泪教训:
中断冲突处理
当系统中有多个中断源时,一定要合理设置优先级。建议将PWM定时器中断设为最高优先级,避免被其他中断打断导致波形畸变。STC15的设置方法:
c复制IP |= 0x02; //PT0=1,定时器0高优先级
IPH |= 0x02; //PT0H=1,更高优先级
功耗控制技巧
在电池供电场景下,可以通过动态调整PWM频率来节能。当需要精细调光时用1kHz频率,在固定亮度模式下可以降到200Hz:
c复制void setPWMFreq(uint freq)
{
pwm1.max = 1000000UL / freq; //根据频率计算周期步数
}
抗干扰设计
在资源紧张的情况下,还可以用PCA模块硬件生成PWM。以STC15的PCA0为例:
c复制CCON = 0x00;
CL = 0x00;
CH = 0x00;
CMOD = 0x02; //PCA时钟=系统时钟/2
CCAPM0 = 0x42; //PWM模式
CCAP0L = 0x80; //50%占空比
CCAP0H = 0x80;
CR = 1; //启动PCA
硬件PWM的优点是不占用CPU资源,但灵活性较差。在蓝桥杯比赛中,建议还是以定时器中断方案为主,除非题目明确要求使用硬件PWM。