蓝桥笔记(二)
1.中断系统
中断系统可以实现:在设备请求服务或程序需要处理某些事件时,立即切换到相应的中断服务程序,并且当中断服务程序执行完后,返回主函数,恢复原工作状态继续执行。有了中断系统就可以使程序执行更加高效。
形象的来说,中断系统就像在办公室上班一样,本来在做手里的事情,突然电话响了,你就停下手中的事情,接电话,接完电话,再回到手中的事情。手中的事情就是主函数,电话响了就是中断,打电话的时候就是执行中断服务函数,打完电话再回到手中的事情就是恢复主函数运行状态。
使用中断系统需要两个函数,一个是初始化函数,用来允许中断的产生与作用,另一个是中断服务函数,用来在中断后执行一系列操作。有了中断,那么执行时会以【主函数】----->中断产生---->【中断服务函数】---->【主函数】的路线运行。
1.1中断初始化函数
要初始化,首先我们需要了解与中断相关的寄存器。与中断相关的一共有四个寄存器(均可以位寻址),IE寄存器、IP寄存器、TCON寄存器和SCON寄存器。其中IE相当于中断的开关,IP用来规定中断优先级,TCON和SCON则与中断标志和启动有关。
IE寄存器:
EA | — | — | ES | ET1 | EX1 | ET0 | EX0 |
---|---|---|---|---|---|---|---|
总开关 | — | — | 串口 | 定时器1 | 外部中断1 | 定时器0 | 外部中断0 |
0:禁止中断 | 1:允许中断 |
---|
IP寄存器:
— | — | PT2 | PS | PT1 | PX1 | PT0 | PX0 |
---|---|---|---|---|---|---|---|
— | — | 定时器2 | 串口 | 定时器1 | 外部中断1 | 定时器0 | 外部中断0 |
0:低优先级 | 1:高优先级 |
---|
TCON寄存器:
TF1 | TR1 | TF0 | TR0 | IE1 | IT1 | IE0 | IT0 |
---|
TF1:定时/计数器T1中断请求标志位(0:无中断请求,1:有中断请求)。
TR1:定时/计数器T1启动位(0:停止定时/计数器,1:启动定时/计数器)。
TF0:定时/计数器T0中断请求标志位(0:无中断请求,1:有中断请求)。
TR0:定时/计数器T0启动位(0:停止定时/计数器,1:启动定时/计数器)。
IE1:外部中断1请求标志位。
IT1:外部中断1触发方式控制位(0:低电平触发,1:下降沿触发)。
IE0:外部中断0请求标志位。
IT0:外部中断0触发方式控制位(0:低电平触发,1:下降沿触发)。
SCON寄存器:
— | — | — | — | — | — | T1 | R1 |
---|
T1:串行接口发送信息完成标志位,发送完成后为1(需要软件手动清除为0)。
R1:串行接口接收信息完成标志位,接收完成后为1(需要软件手动清除为0)。
了解了这四个寄存器之后,我们就可以开始初始化了,初始化内容根据所需要的中断源来决定,如果需要的是外部中断0,就要把总开关EA打开,再把外部中断0的开关EX0打开,把外部中断标志IE清零,选择外部中断触发方式。具体操作如下:
void init_timer0()
{
IE0=0;
IT0=0;
EA=1;
EX0=1;
}
这就是外部中断0的初始化函数了。
1.2中断服务函数
当中断源产生以后,会执行中断服务函数,编写一个中断服务函数,需要注意函数格式:
void 函数名() interrupt 中断号
中断服务函数没有返回值,也不能带参数,而且需要在中断服务函数后面加上interrupt
作为中断服务函数的标志,而中断号则是标志和区分此服务函数为哪一个中断源服务的。
中断号 | 对应的中断源 |
---|---|
0 | 外部中断0 (INT0) |
1 | 定时/计数器T0 |
2 | 外部中断1 (INT1) |
3 | 定时/计数器T1 |
4 | 串口中断 |
下面是一个例子:
void service() interrupt 0
{
tt=1;
}
当然,在工作的时候接到电话,谁都不希望被打扰太久,所以中断服务函数里面做的事情越少越好,这样才能尽快回到主函数里。中断一次之后,需要再初始化一次后才可以让中断再次起作用。
2.定时/计数器
定时/计数器一般会与中断系统组合起来,定时器可以用来定时,形象地说,定时器是一个闹钟,时间到了就会响,即发出中断请求,进入中断服务函数。计数器可以用来记录非周期性外部输入信号的次数,当记录数达到要求的时候,也会发出中断请求,进入中断服务函数。
本质上来讲,定时器和计数器都是计数器,不同点在于,定时器是在初始值的基础上,每过单位时间就会加一,当加到某个设定值时就会触发产生中断请求。而计数器则是每接收一次信号就加一,也是到当加到某个设定值时就会触发产生中断请求。
你可以把这个定时器想象成是一个水壶,里面的水会随着时间而均匀地增加,等啥时候水要溢出来了,就会产生一个脉冲,相当于闹钟。 想要设定不同的时间,可以设定初始水位的高低,如果想要定时久一点,就让水位低一些,反过来像定时短一点,就让初始水位高一些,至于如果想要定一个比较长的时间,大致操作就是当水满了之后把水倒掉,重新计时,这样反复操作就可以延长的定时了,而计数器原理类似。
2.1与定时/计数器相关的寄存器
与定时/计数器相关的寄存器有两个16位寄存器,TCON寄存器,TMOD寄存器。
16位寄存器:
两个16位寄存器分别是 定时/计数器T0和定时/计数器T1,每个十六位寄存器是由两个八位寄存器组成,低的八位是TLx,高的八位是THx(x为编号),最大记录数是65535,也就是说当记录数达到最大值时,再加一就产生溢出。而加一的频率是由单片机的晶振频率来决定的,假设计数脉冲为10Hz,那么相当于每一秒寄存器就会加10,如果我们要定时3秒,就要让寄存器初始值为65505,这样三秒过后,就会溢出产生脉冲。
也就是说,我们需要在定时/计数前给TLx和THx设定初始值以达到定时的目的。
TCON寄存器(中断中有提到):
TF1 | TR1 | TF0 | TR0 | ---- | ---- | ---- | ---- |
---|
TF1:定时/计数器T1中断请求标志位(0:无中断请求,1:有中断请求)。
TR1:定时/计数器T1启动位(0:停止定时/计数器,1:启动定时/计数器)。
TF0:定时/计数器T0中断请求标志位(0:无中断请求,1:有中断请求)。
TR0:定时/计数器T0启动位(0:停止定时/计数器,1:启动定时/计数器)。
注意TCON可以位寻址,但TMOD不能。
TMOD(模式控制):
GATE | C/T | M1 | M0 | GATE | C/T | M1 | M0 |
---|
(左侧为高位,右侧为低位)
高四位用于定义 定时/计数器T1的模式 ,低四位用于定义 定时/计数器T0 的模式。
GATE(门系统):
当GATE=0,由TR0/TR1启动定时器。
当GATE=1,由外部中断INT0/INT1启动定时器。
C/T(计数/定时):
高电平计数,低电平定时。
M1,M0用于设定寄存器工作模式:
M1M0 | 工作模式 |
---|---|
00 | 13位定时/计数模式 |
01 | 16位定时/计数模式 |
10 | 自动重装八位定时/计数模式 |
11 | T0分为独立的两个八位定时器,T1停用 |
若无自动重装,每次都需要手动重新赋初始值。
2.2定时/计数器初始化与服务函数
要想启动定时/计数器,需要一个初始化函数,设置定时器的参数然后再写一个中断服务函数,用来执行定时结束后的事情:
void init()
{
EA=1;
ET0=1;
TMOD=0x01;
TH0=0x00;
TL0=0x00;
TF0=0;
TR0=1;
}
然后写一个主函数和中断服务函数:
unsigned char f=10,i=0;
void main()
{
init();
while(1)
{
if(i==1)
{
nixie(1,1);
}
nixie(2,4);
}
}
void ieee() interrupt 1
{
//TH0=0x00;
//TL0=0x00; 若无自动重装需要手动重装
f--;
if(f==0)
{
i=1;
}
}
这里我们让它计时完就让参数f
减一,如此就实现了延长定时的功能。
3.PWM(脉宽调制)
脉宽调制是利用微处理器的数字输出,来对模拟电路进行控制的一种非常有效的技术,通过对一系列脉冲的宽度进行调制,来等效的获得所需要的波形,即通过改变导通时间占总时间的比例,也就是占空比,达到调整电压和频率的目的。
在51单片机中需要利用定时器来实现调制脉宽的功能。调制脉宽有什么作用呢?它可以实现对LED灯光亮度的控制,实现电机功率的控制等等。拿LED的亮度控制举例,我们能控制LED灯的亮度,是因为我们控制了LED的工作时间,比如我希望它亮度为50%,只需要在每秒钟里有一半的时间都是熄灭状态,一半的时间是亮起的状态就可以实现。也就是说,我们需要利用定时器控制LED亮起和熄灭的频率,虽然LED不停在闪,但由于亮灭频率比较高,我们肉眼是无法观察到灯在闪的,只能观察到亮度暗了一些。
现在举个例子,我们用定时器设置灯的信号频率为100Hz,相当于把一秒拆分成了100份,每一份我们都只让其中的一半时间让灯亮起,另一半熄灭,这样就达成了亮度为50%的目的。让我们来实际操作一下:
void Timer0Init(void)
{
EA=1;
ET0=1;
TMOD=0x01;
TL0 = 0x66;
TH0 = 0xFC;
TF0 = 0;
TR0 = 1;
}
unsigned char con
void main()
{
if(con<50)
{
light();
}
else
{
unlight();
}
}
void service() interrupt 1
{
con++;
if(con==100)
{
con=0;
}
}
总的来说,PWM是一种技术,而且很常用,虽然一些新的单片机其实已经将这个给融合进定时器里了,但我们还是需要加以理解和练习。
4.串行通信
串行通信就是将一串数据一位一位地顺序发送或者接收。串口通信有SPI,IIC,UART等种类,UART是最常用的。串口通信有单工,半双工,全双工三种制式,串口通信的方式主要有两种:同步和异步。
单工:要么只能发送,要么只能接收。
半双工:既可以发送信息,也可以接收,但两者不能同时进行。
全双工:同时可以进行的接收和发送信息。
同步:两个设备连接着一条线用来确定时间,或者使用同一个时钟,以达到收发信息同步的目的。
异步:两个设备约定一个相同的速率来收发信息,会有误差,但也可以正常收发信息。
这里需要提到一个概念叫做波特率,就是每秒接收或者发送信息的位数。
为了保证收发信息准确,发送信息会有起始位,停止位,有的还会有校验位。
在51单片机里,串口通信一共有四个模式,其中模式1和模式3的波特率是可以改变的,这取决于定时器,一般会使用定时器1的工作模式2(8位自动重装模式)来产生波特率,在这个模式下,TH1作为自动重装寄存器,而TL1作为脉冲计数寄存器,当计数器达到最大计数值溢出时,TH1的值会自动装到TL1中。
在12M或者11.0592M的晶振的情况下,要产生9600的波特率时,若SMOD=0时,参数位0xfd;SMOD=1时,参数为0xfa。
4.1与串口有关的寄存器
两个缓冲寄存器SBUF:
这两个SBUF,一个是发送寄存器,一个是接收寄存器,用来发送信息或者接受信息。它们在物理结构上是完全独立的两个寄存器,字节寻址,但两者拥有完全相同的地址。
当往SBUF写入值的时候,是往发送寄存器里写入东西,而当读取SBUF的值时,是读出接收寄存器的东西。所以是靠做左值运算还是右值运算来区分SBUF的。
SCON串口控制寄存器:
SM0 | SM1 | SM2 | REN | TB8 | RB8 | TI | RI |
---|
TI和RI:
数据发送完成,TI标志位值会被置1,而接收数据完成,RI标志位值会置1。这两个标志位都需要手动软件清零。
TB8和RB8:
分别是发送数据和接收数据的第八位。用于奇偶校验。
REN:
当REN=1时允许接受数据。REN=0时,禁止接收数据。
SM0,SM1:
控制工作模式:
SM0,SM1 | 工作模式 |
---|---|
00 | 同步移位寄存器(波特率:fosc/12) |
01 | 8位可变波特率的UART |
10 | 9位UART(波特率:fosc/32或者fosc/64) |
11 | 9位可变波特率的UART |
SM2置0即可。
串行通信一般使用异步八位UART并且允许接收数据模式,所以令SCON=0X50即可。
辅助寄存器AUXR(0x8e)
这个寄存器没法直接使用,需要自己定义一下。其实不管这个寄存器也没有关系,如果要用,直接赋值为0x00即可。
B7 | B6 | B5 | B4 | B3 | B2 | B1 |
---|
B7:定时器0的速度控制位。
为0:12分频。为1:不分频。
B6:定时器1的速度控制位。
为0:12分频。为1:不分频。
B5:串口1模式0的通信速度设置。
为0:12分频。为1:2分频。
B4:定时器2运行控制位。
为0:不可运行。为1:允许运行。
B3:定时器2作定时/计数器控制位。
为0:作定时器。为1:作计数器。
B2:定时器2的速度控制位。
为0:12分频。为1:不分频。
B1:内部/外部RAM存取控制位。
为0:允许使用扩展RAM。为1:不允许。
B0:串口1选则定时器1/2控制位。
为0:选择定时器1为串口1的波特率发生器。
为1:选择定时器2为串口1的波特率发生器。
4.2串口中断的初始化函数和中断服务函数
首先写一个初始化函数:
sfr AUXR=0x8e;
void init()
{
AUXR =0x00;
TMOD=0x20; //八位自动重装模式
TL1=0xfd; //波特率9600
TH1=0xfd;
TR1=1;
SM0=1;SM1=0;REN=1;//SCON=0x50
EA=1;//总开关
ES=1;//串口中断开关
TI=0;//清除标志
RI=0;
}
初始化不仅要打开串口的中断,还需要打开定时器1,用定时器1来产生波特率。
然后我们可以单独写一个用于发送信息的函数:
void send(unsigned char mes)
{
SBUF=mes;
while(TI==0);
TI=0;//手动清除标志
}
再写一个用于接收信息的中断服务函数:
void service() interrupt 4
{
if(RI==1)
{
dat=SBUF;
RI=0;//手动清除标志
}
}
这样就可以在主函数里随意调用来接收和发送信息的函数了,但在调试的时候要注意一下在STC-ISP上打开串口,设置相同的波特率后才能正常的收发信息。