目录
1.PWM简介
脉宽调制(PWM,Pulse Width Modulation)是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。(百度百科)
PWM是通过脉宽调制来实现控制电压的大小,PWM中有一个重要的参量叫做占空比,占空比的意思就是在一个周期中高电平占整个周期的比例(例如周期为2s,高电平为1s则占空比为50%),实际输出的电压等于输出口电压乘于占空比(例如IO口在高电平输出5V,PWM占空比为50%输出则输出实际电压为5V*50%=2.5V)。PWM在各各领域中都占据非常重要的地位,尤其是电机控制。
2.PWM的软硬件配置
2.1PWM在STC12C5A60S2中的硬件配置
1、配置寄存器
首先我们打开我们STC12C5A60S2的官方中文数据手册,找到第十章PAC/PWM应用。
我们以呼吸灯为例,查看中文手册IO口功能,P1.3口是可以使用PWM输出的,我们以这个IO口为例输出呼吸灯。
那我们要控制多少个寄存器呢,我们可以先打开手册后面的历程
我们所用到的寄存器有CCON、CMOD、CCAPM0、PCA_PWM0、CCAP0L、CCAP0H、CL、CH和CR总共八个寄存器,由于CR寄存器是包含在CCON里面的,实际只需要操作七个寄存器。
下图是所用到的寄存器。
1.1配置CCON寄存器
我们翻回去看到CCON寄存器的格式图
往下翻看到PCA计数器架构图。
要打开计数器,必须开机CR,CR包含寄存器CCON,所以CCON=0X40。
1.2配置CMOD
打开中文手册第十章第一节可以看到寄存器格式图如下
同样的,我们看到PCA计数器架构图。
若要开启计数器必须CIDL置0,配置2分频时钟CPS2、CPS1、CPS0分别为0、0、1,所以CMOD=0X02。
1.3配置CCAPM0
查看中文手册10.2可以看到
我们选择P1.3IO口输出是属于PCA模块0,所以我们配置的寄存器是CCAPM0。在翻过来看到CCAPM0寄存器格式图。
我们往下翻,看到PWM配置的架构图。
若要产生PWM必须开启9位比较器,要开启9位比较器就必须开启ECOMn和PWMn,所以CCAPM0=0x42。
1.4配置PCA_PWM0寄存器
我们看到相应的寄存器
这里的话只有两个位要操控,在下面没有明确说明0是关闭1是打开的情况下,0或1都是可以开启的,这里我们使用0开启,则PCA_PWM0=0x00。
1.5配置CL、CH
我们看到数据手册
这个寄存器是用来保存PCA的装载值的,同样的我们看到PWM架构图。
架构图中,这个寄存器是通过比较器计数存于CL,最后用来与CCPnL进行比较输出的,进行比较之前我们必须置零,CH没有要求我们可写可不写,写的话CH置零,即CH=0、CL=0。
1.6配置寄存器CCAP0H和CCAP0L
我们翻上去看到手册。
我们再看到PWM结构框图。
CCAPnH指向CCAPnL意思就是CCAPnH会自动重装到CCAPnL。后面通过计数器CL计数并与CCPnL比较最后决定输出0或1。到这一步我们很明确的就知道这两个寄存器是控制PWM占空比的,这个寄存器很关键,我们做呼吸灯就是无时无刻的控制占空比来实现。
我们看下手册后面的历程代码怎么配置。
通过注释我们很快了解到,这个数值就是输出占空比为50%的PWM的信号。
这里解释一下为什么CCAP0H = CCAP0L = 0x80 输出的占空比为50%,CCAP计数器CL计满是256,如果 CCAP0H=0X80=128,即计到128时PWM输出管脚翻转,所以 占空比 是128/256=50%。
2、编写相应代码
配置寄存器如下。
void Init_PWM() // PWM功能的初始化
{
CMOD = 0X02; // 设置脉冲源
CCAPM0 = 0X42; // 开启比较器,允许输出脉宽调制信号
PCA_PWM0 = 0X00; // 组成9位比较器,可以设置成1,也可以设置成0
CCAP0L = CCAP0H = 0X80; // 比较器中的初值,比较器初值重装
CH=0;
CL=0; // 装载值为0
CCON = 0X40; //开启PAC计数器
}
前面我们说到,如果我们要实现呼吸灯,那么最重要的控制PWM的占空比,并且是时时刻刻都要改变占空比,那么我们就可以通过编写代码时时刻刻改变控制占空比的寄存器(CCAP0H)。
unsigned char dy[15]={16,32,48,64,80,96,112,128,144,160,176,192,208,224,240}; // CCAP0H值的分配,为了实现不同的占空比
我们可以定义一个数组,把256拆成16等份。
void modify_duty()
{
u8 i;
for(i=0;i<15;i++)
{
CCAP0H = dy[i];
delay_ms(100);
}
for(i=13;i>=1;i--)
{
CCAP0H = dy[i];
delay_ms(100);
}
}
每间隔一个delay()时间给CCAP0H装值,上面这个for循环是由低到高,下面的for循环是由高到低,这样就形成了占空比由少变多,再由多变少的过程,实际输出电压就由小到大,再由大到小,这样呼吸灯就完成了。这里的delay是毫秒级的延时,用于控制呼吸周期,延时越大越不丝滑,越小越丝滑。
2.2PWM在STC12C5A60S2中的软件配置
1、输出可调频率、固定占空比PWM
软件输出PWM和硬件输出PWM区别在于软件要靠代码去模拟,所以软件输出PWM算法方面会比较麻烦。我们设定输出可调频率、固定占空比PWM必须要用到定时器,下面教大家怎么利用定时器来输出PWM。
首先我们先配置好定时器,规定其溢出时间(100us)。
void Timer0Init()
{
TMOD|=0X01;//选择为定时器0模式,工作方式1,仅用TR0打开启动。
TH0=0XFF; //给定时器赋初值,定时100us
TL0=0X9C;
ET0=1;//打开定时器0中断允许
EA=1;//打开总中断
TR0=1;//打开定时器
}
然后我们就可以在定时器的服务函数里面编写我们的算法。
void Timer0() interrupt 1
{
static u16 i;
TH0=0XFF; //给定时器赋初值,定时100us
TL0=0X9C;
i++;
if(i<PWM_DUTY) P10=1;
if((i>PWM_DUTY) && (i<=period)) P10=0;
if(i>period) i=0;
}
有些变量是我另外用了一个函数封装。
void PWM(u16 frequency,u16 duty)
{
period = 10000/frequency;
PWM_DUTY = (duty*period)/100;
}
我们先看到PWM()这个函数,不难理解,变量period就是周期,其计算公式就是1/f,由于计算出来是一个小数,而定时器是一个100us为一个单位的时间变量,所以我们这里要乘上10000使其对等,另外一个变量PWM_DUTY就是占空比变量了,也很简单,公式为(duty*period)/100,这里占空比范围为1~100(精度为1)。
我们再看回定时器中断服务函数,i变量每隔100us加一次,当i小于占空比时间时P10输出1,当i大于占空比时间小于PWM周期时间时P10输出0,每完成一个周期i刷新一次。
很简单,这样我们就完成了使用定时器输出PWM,这样输出PWM是不会干扰主函数上执行的程序的,使得单片机运作更加高效。
2、输出呼吸灯PWM
这个相对于上面的就难多了,难在算法方面,上面固定占空比输出是占空比不变的形式输出,比较好计算出来,这个是时刻变动占空比输出PWM难度大涨。
同样的,我们也需要配置定时器。
void Timer1Init()
{
TMOD=0X01;//选择为定时器1模式,工作方式1,仅用TR1打开启动。
TH1 = 0xFF;
TL1 = 0xff; //1us
ET1=1;//打开定时器1中断允许
EA=1;//打开总中断
TR1=1;//打开定时器
}
在定时器中断服务函数里面我们设置两个变量,用于计算。
void Time1() interrupt 3
{
TH1 = 0xFF;
TL1 = 0xff; //1us
timer1++;
count++;
}
呼吸灯算法。
void huxideng()
{
if(count>50)
{
count=0;
if(DIR==1) //DIR控制增加或减小
{
value++;
}
if(DIR==0)
{
value--;
}
}
if(value==1000)
{
DIR=0;
}
if(value==0)
{
DIR=1;
}
if(timer1>1000) //PWM周期为1000*1us
{
timer1=0;
}
if(timer1 <value)
{
P10=1;
}
else
{
P10=0;
}
}
算法的思路是这样的,让timer1和count两个变量同时在定时器中计数,在函数中再设置一个变量value,count这个变量每计算到50时,判断DIR(0或1)就让value变量加或减一次,然后用timer1变量与value变量进行比较,最后P10输出0或1,DIR变量是决定占空比是增加还是减少的标志量。
我们可以根据程序手画一下占空比图。
假设当占空比变量value自增的情况下我们可以画出
value在自减的情况下我们又可以画出
这样无限循环下去有效电压就可以实现由低到高再有高到低,呼吸灯就实现了。另外提一下,后面的980、960、940us只是个大概数,实际值会比这个大一点,有兴趣的小伙伴可以自己手算一下,这里方便举例就不细算了。
硬件配置.C文件
#include <stc12c5a60s2.h>
#define u8 unsigned char
#define u16 unsigned int
unsigned char dy[15]={16,32,48,64,80,96,112,128,144,160,176,192,208,224,240}; // CCAP0H值的分配,为了实现不同的占空比
void delay_ms(u16 xms)
{
u8 j;
for(;xms>0;xms--)
for(j=110;j>0;j--);
}
void Init_PWM() // PWM功能的初始化
{
CMOD = 0X02; // 设置脉冲源
CCAPM0 = 0X42; // 开启比较器,允许输出脉宽调制信号
PCA_PWM0 = 0X00; // 组成9位比较器,可以设置成1,也可以设置成0
CCAP0L = CCAP0H = 0X80; // 比较器中的初值,比较器初值重装
CH=0;
CL=0; // 装载值为0
CCON = 0X40; //开启PAC计数器
}
void modify_duty()
{
u8 i;
for(i=0;i<15;i++)
{
CCAP0H = dy[i];
delay_ms(100);
}
for(i=13;i>=1;i--)
{
CCAP0H = dy[i];
delay_ms(100);
}
}
void main()
{
Init_PWM();
while(1)
{
modify_duty();
}
}
软件配置.C文件
#include <STC12C5A60S2.H>
#define u16 unsigned int
#define u8 unsigned char
u16 count,value,timer1;
u16 period;
u16 PWM_DUTY;
u8 DIR;
/*******************************************************************************
可调节频率、占空比固定输出PWM
********************************************************************************/
void Timer0Init()
{
TMOD|=0X01;//选择为定时器0模式,工作方式1,仅用TR0打开启动。
TH0=0XFF; //给定时器赋初值,定时100us
TL0=0X9C;
ET0=1;//打开定时器0中断允许
EA=1;//打开总中断
TR0=1;//打开定时器
}
void Timer0() interrupt 1
{
static u16 i;
TH0=0XFF; //给定时器赋初值,定时100us
TL0=0X9C;
i++;
if(i<PWM_DUTY) P10=1;
if((i>PWM_DUTY) && (i<=period)) P10=0;
if(i>period) i=0;
}
//第一个入口产量控制频率(范围1~9999HZ),建议不要太大或太小。
//第二个入口产量控制占空比(范围1~100,精度为1)
void PWM(u16 frequency,u16 duty)
{
period = 10000/frequency;
PWM_DUTY = (duty*period)/100;
}
/*******************************************************************************
呼吸灯时刻改变占空比输出PWM
********************************************************************************/
void Timer1Init()
{
TMOD=0X01;//选择为定时器1模式,工作方式1,仅用TR1打开启动。
TH1 = 0xFF;
TL1 = 0xff; //1us
ET1=1;//打开定时器1中断允许
EA=1;//打开总中断
TR1=1;//打开定时器
}
void Time1() interrupt 3
{
TH1 = 0xFF;
TL1 = 0xff; //1us
timer1++;
count++;
}
void huxideng()
{
if(count>50)
{
count=0;
if(DIR==1) //DIR控制增加或减小
{
value++;
}
if(DIR==0)
{
value--;
}
}
if(value==1000)
{
DIR=0;
}
if(value==0)
{
DIR=1;
}
if(timer1>1000) //PWM周期为1000*1us
{
timer1=0;
}
if(timer1 <value)
{
P10=1;
}
else
{
P10=0;
}
}
void main()
{
PWM(50,80);
Timer0Init();
while(1);
}