|
9.9 PWM知识与实例 PWM在单片机中的应用是非常广泛的,它的基本原理很简单,但往往应用于不同场合上意义也不完全一样。 PWM是Pulse Width Modulation的缩写,中文名字是脉冲宽度调制,一种说法是它利用微处理器的数字输出对模拟电路进行控制的一种有效的技术,其实就是使用数字信号达到一个模拟信号的效果。这是个什么概念呢? 首先从名字来看,脉冲宽度调制,就是改变脉冲宽度来实现不同的效果。先来看三组不同的脉冲信号,如图9-5所示。 图9-5 PWM波形 这是一个周期是10ms,即频率是100Hz的波形,但是每个周期内,高低电平脉冲宽度各不相同,这就是PWM的本质。引入一个概念--占空比。占空比是指高电平的时间占整个周期的比例。比如第一部分波形的占空比是40%,第二部分波形占空比是60%,第三部分波形占空比是80%,这就是PWM的解释。 为何它能对模拟电路进行控制呢?数字电路里,只有0和1两种状态,比如第2章学会的点亮LED小灯那个程序,写一个LED = 0;小灯就会长亮,写一个LED = 1;小灯就会灭掉。当让小灯亮和灭间隔运行的时候,小灯是闪烁。如果把这个间隔不断的减小,减小到肉眼分辨不出来,也就是100Hz以上的频率,这个时候小灯表现出来的现象就是既保持亮的状态,但亮度又没有LED = 0;时的亮度高。不断改变时间参数,让LED = 0;的时间大于或者小于LED = 1;的时间,会发现亮度都不一样,这就是模拟电路的感觉了,不再是纯粹的0和1,还有亮度不断变化。如果用100Hz的信号,如图9-5所示,假如高电平熄灭小灯,低电平点亮小灯的话,第一部分波形熄灭4ms,点亮6ms,亮度最高,第二部分熄灭6ms,点亮4ms,亮度次之,第三部分熄灭8ms,点亮2ms,亮度最低。那么用程序验证一下理论,用定时器T0定时改变P0.0的输出来实现PWM,与纯定时不同的是,这里每周期内都要重载两次定时器初值,即用两个不同的初值来控制高低电平的不同持续时间。为了使亮度的变化更加明显,程序中使用的占空比差距更大。 #include <reg52.h> sbit PWMOUT = P0^0; sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; unsigned char HighRH = 0; //高电平重载值的高字节 unsigned char HighRL = 0; //高电平重载值的低字节 unsigned char LowRH = 0; //低电平重载值的高字节 unsigned char LowRL = 0; //低电平重载值的低字节 void ConfigPWM(unsigned int fr, unsigned char dc); void ClosePWM(); void main() { unsigned int i; EA = 1; //开总中断 ENLED = 0; //使能独立LED ADDR3 = 1; ADDR2 = 1; ADDR1 = 1; ADDR0 = 0; while (1) { ConfigPWM(100, 10); //频率100Hz,占空比10% for (i=0; i<40000; i++); ClosePWM(); ConfigPWM(100, 40); //频率100Hz,占空比40% for (i=0; i<40000; i++); ClosePWM(); ConfigPWM(100, 90); //频率100Hz,占空比90% for (i=0; i<40000; i++); ClosePWM(); //关闭PWM,相当于占空比100% for (i=0; i<40000; i++); } } /* 配置并启动PWM,fr-频率,dc-占空比 */ void ConfigPWM(unsigned int fr, unsigned char dc) { unsigned int high, low; unsigned long tmp; tmp = (11059200/12) / fr; //计算一个周期所需的计数值 high = (tmp*dc) / 100; //计算高电平所需的计数值 low = tmp - high; //计算低电平所需的计数值 high = 65536 - high + 12; //计算高电平的重载值并补偿中断延时 low = 65536 - low + 12; //计算低电平的重载值并补偿中断延时 HighRH = (unsigned char)(high>>8); //高电平重载值拆分为高低字节 HighRL = (unsigned char)high; LowRH = (unsigned char)(low>>8); //低电平重载值拆分为高低字节 LowRL = (unsigned char)low; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = HighRH; //加载T0重载值 TL0 = HighRL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 PWMOUT = 1; //输出高电平 } /* 关闭PWM */ void ClosePWM() { TR0 = 0; //停止定时器 ET0 = 0; //禁止中断 PWMOUT = 1; //输出高电平 } /* T0中断服务函数,产生PWM输出 */ void InterruptTimer0() interrupt 1 { if (PWMOUT == 1) //当前输出为高电平时,装载低电平值并输出低电平 { TH0 = LowRH; TL0 = LowRL; PWMOUT = 0; } else //当前输出为低电平时,装载高电平值并输出高电平 { TH0 = HighRH; TL0 = HighRL; PWMOUT = 1; } } 需要提醒的是,由于标准51单片机中没有专门的PWM模块,所以用定时器加中断的方式来产生PWM,而现在有很多的单片机都会集成硬件的PWM模块,这种情况下需要做的仅仅是计算一下周期计数值和占空比计数值然后配置到相关的SFR中即可,既使程序得到了简化又确保了PWM的输出品质(因为消除了中断延时的影响)。 如果想让亮度等级更多,并且让亮度等级连续起来,会产生一个小灯渐变的效果,与呼吸有点类似,习惯上称之为呼吸灯,程序代码如下,这个程序用了2个定时器2个中断。 #include <reg52.h> sbit PWMOUT = P0^0; sbit ADDR0 = P1^0; sbit ADDR1 = P1^1; sbit ADDR2 = P1^2; sbit ADDR3 = P1^3; sbit ENLED = P1^4; unsigned long PeriodCnt = 0; //PWM周期计数值 unsigned char HighRH = 0; //高电平重载值的高字节 unsigned char HighRL = 0; //高电平重载值的低字节 unsigned char LowRH = 0; //低电平重载值的高字节 unsigned char LowRL = 0; //低电平重载值的低字节 unsigned char T1RH = 0; //T1重载值的高字节 unsigned char T1RL = 0; //T1重载值的低字节 void ConfigTimer1(unsigned int ms); void ConfigPWM(unsigned int fr, unsigned char dc); void main() { EA = 1; //开总中断 ENLED = 0; //使能独立LED ADDR3 = 1; ADDR2 = 1; ADDR1 = 1; ADDR0 = 0; ConfigPWM(100, 10); //配置并启动PWM ConfigTimer1(50); //用T1定时调整占空比 while (1); } /* 配置并启动T1,ms-定时时间 */ void ConfigTimer1(unsigned int ms) { unsigned long tmp; //临时变量 tmp = 11059200 / 12; //定时器计数频率 tmp = (tmp * ms) / 1000; //计算所需的计数值 tmp = 65536 - tmp; //计算定时器重载值 tmp = tmp + 12; //补偿中断响应延时造成的误差 T1RH = (unsigned char)(tmp>>8); //定时器重载值拆分为高低字节 T1RL = (unsigned char)tmp; TMOD &= 0x0F; //清零T1的控制位 TMOD |= 0x10; //配置T1为模式1 TH1 = T1RH; //加载T1重载值 TL1 = T1RL; ET1 = 1; //使能T1中断 TR1 = 1; //启动T1 } /* 配置并启动PWM,fr-频率,dc-占空比 */ void ConfigPWM(unsigned int fr, unsigned char dc) { unsigned int high, low; PeriodCnt = (11059200/12) / fr; //计算一个周期所需的计数值 high = (PeriodCnt*dc) / 100; //计算高电平所需的计数值 low = PeriodCnt - high; //计算低电平所需的计数值 high = 65536 - high + 12; //计算高电平的定时器重载值并补偿中断延时 low = 65536 - low + 12; //计算低电平的定时器重载值并补偿中断延时 HighRH = (unsigned char)(high>>8); //高电平重载值拆分为高低字节 HighRL = (unsigned char)high; LowRH = (unsigned char)(low>>8); //低电平重载值拆分为高低字节 LowRL = (unsigned char)low; TMOD &= 0xF0; //清零T0的控制位 TMOD |= 0x01; //配置T0为模式1 TH0 = HighRH; //加载T0重载值 TL0 = HighRL; ET0 = 1; //使能T0中断 TR0 = 1; //启动T0 PWMOUT = 1; //输出高电平 } /* 占空比调整函数,频率不变只调整占空比 */ void AdjustDutyCycle(unsigned char dc) { unsigned int high, low; high = (PeriodCnt*dc) / 100; //计算高电平所需的计数值 low = PeriodCnt - high; //计算低电平所需的计数值 high = 65536 - high + 12; //计算高电平的定时器重载值并补偿中断延时 low = 65536 - low + 12; //计算低电平的定时器重载值并补偿中断延时 HighRH = (unsigned char)(high>>8); //高电平重载值拆分为高低字节 HighRL = (unsigned char)high; LowRH = (unsigned char)(low>>8); //低电平重载值拆分为高低字节 LowRL = (unsigned char)low; } /* T0中断服务函数,产生PWM输出 */ void InterruptTimer0() interrupt 1 { if (PWMOUT == 1) //当前输出为高电平时,装载低电平值并输出低电平 { TH0 = LowRH; TL0 = LowRL; PWMOUT = 0; } else //当前输出为低电平时,装载高电平值并输出高电平 { TH0 = HighRH; TL0 = HighRL; PWMOUT = 1; } } /* T1中断服务函数,定时动态调整占空比 */ void InterruptTimer1() interrupt 3 { static bit dir = 0; static unsigned char index = 0; unsigned char code table[13] = { //占空比调整表 5, 18, 30, 41, 51, 60, 68, 75, 81, 86, 90, 93, 95 }; TH1 = T1RH; //重新加载T1重载值 TL1 = T1RL; AdjustDutyCycle(table[index]); //调整PWM的占空比 if (dir == 0) //逐步增大占空比 { index++; if (index >= 12) { dir = 1; } } else //逐步减小占空比 { index--; if (index == 0) { dir = 0; } } }
|