和我一起学51单片机(1)——PWM输出(频率,占空比,相位差可调)
文章目录
前言
笔者是一名大一新生,但专业并不是真正的电子信息领域的,只是因为机缘巧合打开了这扇大门,但是其实笔者还挺喜欢硬件的。主攻信号调理,刚刚学习不久,于是本着记笔记的目的来到CSDN发表笔记,希望能够为大家提供思路。
以下是本篇文章正文内容
一、PWM是什么?
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。
有一个博主写的非常的好,笔者自知不如,故不再班门弄斧,直接上链接!
二、如何出一路占空比可调,频率可调的方波?方法(一)——计算重装值
先声明,这种方法的优点是精度高
众所周知,定时器拥有计数器和定时器模式。在输出PWM波时,咱们肯定要选择定时器模式。并且在使用不熟练的情况下一般都会选择使用辅助软件帮助配置相关寄存器
因为一直都是软件在帮助咱们进行配置,但其实咱们自己也可以通过任意修改重装值来实现计时器在记了咱们想要它记多少次数之后而溢出的数据。并且咱们也知道PWM实际上就是方波,不过是高低电平,那么说实话我只需要控制方波的高低电平的时间就可以了,既能控制频率,也能控制占空比。
那么想要实现就不需要用软件帮咱们设置重装值了,只要自己计算就可以了。
接下来对要用到的定时器进行初始化。
void Timer0_Init(void)
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
}
很明显,咱们没有进行重装,那么在哪里重装呢?
这是咱们就要自己写一个函数啦,直接上代码!
void PWM_init(unsigned int f,D)
{
unsigned int highload;
unsigned int lowload;
unsigned long T;
unsigned int low;
unsigned int high;
T=921600/f;
high=(T*D)/100;
low=T-high;
highload=65535-high;
lowload=65535-low;
highH=(unsigned char)(highload/256);
highL=(unsigned char)(highload%256);
lowH=(unsigned char)(lowload/256);
lowL=(unsigned char)(lowload%256);
TH0=highH;
TL0=highL;
}
再来看看中断里的函数
void Timer0_Routine() interrupt 1
{
if(P2_5==1)
{
TH0=lowH;
TL0=lowL;
P2_5=0;
}
else
{
TH0=highH;
TL0=highL;
P2_5=1;
}
}
注意,这里P2_5是取决于你想要的输出方波的口的,可以随便改,但是一般推荐用P2
你可能还不知道上边的函数 PWM_init(unsigned int f,D) 有啥用,先不管它。
你单看中断里边的代码,我们可以知道,你给定时器重装low的重装值,并且翻转电平,那么在他下一次翻转电平的这段时间(从设置重装值到记满溢出)都维持的是低电平的状态,那下次翻转电平(进入中断)不就是你装high重装值的时候吗?于是,就形成了一个完美闭环。
现在咱们就可以说说 **PWM_init(unsigned int f,D)**有啥用了,是的,它的作用只是计算low和high的重装值。
咱们来逐行分析代码(定义变量就不说了)
T=921600/f;
这个就是在计算周期,是用921600/f,那么921600是怎么来的呢?是用11059200/12得来的,那么看到这两个数字你就应该敏感起来,11.0592是晶振的频率,而12,则是时钟周期和机械周期的关系。
tips:
在单片机的运行过程中,机械周期和时钟周期是两个重要的时间单位。理解它们之间的关系对于编写和优化单片机程序非常重要。
时钟周期是单片机最基本的时间单位,它由晶振提供的时钟信号决定。时钟周期的长度等于晶振频率的倒数。例如,对于一个12MHz的晶振,时钟周期为1/12微秒
机械周期是单片机执行一个基本操作所需的时间。对于MCS-51单片机,一个机械周期包含12个时钟周期。这意味着,如果晶振频率为12MHz,那么一个机械周期为1微秒
咱们的晶振频率为11.0592MHz,那么一个机械周期为12/11.0592微秒,那么一个定时器加一的的时间为12/11.0592微秒。
假如周期为T,T是一个数,你先不必纠结它啥意思,你就知道他是一个数,然后这个数到时候是要去计算重装值的,
那么T的实际运行时间就是T*12/11.0592微秒,那由T=1/F,可知下式。
T*12/11.0592=1000000/f
这是一个时间等式,本质是周期的实际时间相等。至于为什么右边乘以1000000,是因为这时候的周期是微秒级别的,不是秒了,要换算一下。
那就上边的问题就迎刃而解了。
high=(T*D)/100;
low=T-high;
很明显,这是在操作占空比,给high和low分配数值。
highload=65535-high;
lowload=65535-low;
highH=(unsigned char)(highload/256);
highL=(unsigned char)(highload%256);
lowH=(unsigned char)(lowload/256);
lowL=(unsigned char)(lowload%256);
这是分为高八位和低八位,为了更好的重装。
TH0=highH;
TL0=highL;
设置初始重装值
(可根据你的初始I/O口的状态修改重装high还是low)
重点!
1.其实65535可以适当增加和减少(考虑到单片机自身性能和代码运算),比如65547,会很准很准,但这时候也要舍弃占空比为0和100的情况,因为65547比65535大,直接溢出了(悲)
highload=65535-high;
lowload=65535-low;
2.在这种方法在低频率和极端占空比时候不太适用,需要修改代码,因为highload和lowload不能为负的。所以你可以给921600除某个数。比如,除以10(根据你自己的需求),那么这时候频率乘以10了,所以你要记满10次再改变一次电平。并且,在记满十次之前,你的重装值就该是你当前的电平状态,等需要修改电平时再修切换重装值。
修改如下
T=92160/f;
if(P2_7 == 1)
{
if(count1 == 10)
{
count1 = 0;
TH0 = LowH;
TL0 = LowL;
PWMOUT = 0;
}
else
{
TH0 = HighH;
TL0 = HighL;
count1++;
}
}
else
{
if(count1 == 10)
{
count1 = 0;
TH0 = HighH;
TL0 = HighL;
PWMOUT = 1;
}
else
{
TH0 = LowH;
TL0 = LowL;
count1++;
}
}
三、如何用定时器出两路占空比可调,频率可调,相位可调的方波?
第二路波的出波方式不过是上边的复制,至于如何出相位差,就可以通过玩count来实现,在初定义count1和count2时候,可以给count1置0,给count2置某个数,比如2,那么相位差就是360*2/10,相位差的步进为360/10。
但是上述方法都太占定时器资源了!
四、如何只用一个定时器出两路占空比可调,频率可调,相位差可调的方波?方法(二)——翻转吧,电平!
现在这里有一条线
其实,你也可以认为是占空比为满或是为零的一个PWM波,那么调整占空比不过是在连续的每一个定长分块里面选取定长拉高或摁下去。
如下
那么其实只要我控制好每个分块的长度和每个长度里面多长被拉高或者多长被摁下就可以了,也就可以控制一个波的周期和占空比,从而控制频率和占空比。
那我该如何实现呢?
从图上来看只需要控制红点处的位置就可以了
咱们在这些地方拉高后就可以控制波的频率和占空比了。
放到单片机上去实现的话,其实只需要在恰当时间将某个I/O口输出0和1就可以了.
那么我要是想要实现精准拉高或者拉低的话,就不得不使用定时器了。
那么我们便可以考虑将一个定长分为等长小份,那么我只要在适当的小份里边改变电平就可以实现较为精准的调整了。
那么这些小份说白了就是一些较短定长的时间片段。
那就通过定时器去实现这些时间片段
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x02; //设置定时器模式
TL0 = 0xA4; //设置定时初始值
TH0 = 0xA4; //设置定时重载值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //使能定时器0中断
这里笔者没有选择50us的重装周期是因为要和后面的一个定时器实现测量两路方波结合起来,不然无法同时使用。当然,若是只有出波需求,那么50us就是最好的选择,再小受到51单片机的性能和计算机的愚笨限制必定无法实现准确的数据。
那么咱们就以100us为基本来实现。
设出变量F和D,n
那么就可以知道,其实一共有10^4/F个小份
咱们为了方便,默认在T记满之后,翻转一次电平,那么其实若要控制占空比,就只需要在T*D/100的时候翻转另一次电平就好
为了便于移植和调试,不妨将n设为T_OP。
设频率为F_OP,占空比为D_OP,相位差为phase_OP,定时器周期为T_OP,N_OP为总计时次数,ND_OP是为了实现占空比时候的计时次数
则有
N_OP=100*100/T_OP*100/F_OP;
ND_OP=100*100/T_OP*D_OP/F_OP;
注意到100*100/T_OP频繁使用,故将其赋给一个变量
Tcontainer=100*100/T_OP;
N_OP=Tcontainer*100/F_OP; ND_OP=Tcontainer*D_OP/F_OP;
相位差的实现更是容易,其实只要在第一个的波的翻转的地方的前一段时间翻转另一个电平,就可以了
分别设第二路波的翻转电平的时间点为NP_OP和NDP_OP。
设变量Pcontainer去储存要N_OP,ND_OP的基础上减去的值。
明显, Pcontainer=phase_OP*N_OP/360;
但考虑到51单片机为八位处理器,变量如果运算过程中超过了65535就会重置,所以变为
Pcontainer=phase_OP*(1.0*N_OP/360);
NP_OP=N_OP-Pcontainer;
NDP_OP=ND_OP-Pcontainer;
又考虑到
ND_OP和Pcontainer的大小问题,那么需要分类讨论。
if(ND_OP>Pcontainer)
{
NP_OP=N_OP-Pcontainer;
NDP_OP=ND_OP-Pcontainer;
}
else
{
NP_OP=N_OP-Pcontainer;
NDP_OP=N_OP+ND_OP-Pcontainer;
}
else 中的代码是为了因为Pcontainer过大,变为负值。
那么封装为一个函数
void compute_OP (unsigned int F_OP,D_OP,phase_OP,T_OP)
{
unsigned int Tcontainer;
unsigned int Pcontainer;
Pcontainer=phase_OP*(1.0*N_OP/360);
Tcontainer=100*100/T_OP;
N_OP=Tcontainer*100/F_OP;
ND_OP=Tcontainer*D_OP/F_OP;
if(ND_OP>Pcontainer)
{
NP_OP=N_OP-Pcontainer;
NDP_OP=ND_OP-Pcontainer;
}
else
{
NP_OP=N_OP-Pcontainer;
NDP_OP=N_OP+ND_OP-Pcontainer;
}
}
接下来就在定时器中断里边翻转电平就可以了
void ce1() interrupt 1 //定时器中断
{
unsigned int a;
a++;
if(a==NDP_OP)
P2_6=0;
if(a==ND_OP)
P2_7=0;
if(a==NP_OP)
P2_6=1;
if(a==N_OP)
{ P2_7=1; a=0;}
}
笔者是一名新人,如有错误,请谅解并指出,我一定好好修改。
下期预告——测量频率,相位差,占空比