目录
一.时钟电路与复位电路
单片机需要有两个外围电路来进行工作,一个是时钟电路,一个是复位电路。
1.时钟电路
单片机常见的时钟电路有两种,一个时11.0592MHz,一个是12MHz,而前者是单片机进行通信时使用
2.复位电路
二.LED原理与流水灯的实现
1. LED的原理
在实现流水灯时,存在共阳极与共阴极。
共阳极即P极以一根导线连接后与电源相连,N极与单片机的I/O口相连,当输出为低电平时,LED灯亮。输出为高电平时,LED灯灭。
共阴极即N极以一根导线连接后与地相连,P极与单片机的I/O口相连,当输出为高电平时,LED灯亮。输出为低电平时,LED灯灭。
由于单片机引脚能够通过的最大电流为20mA,电源电压为5V,所以LED灯直接与I/O口相连会导致单片机损坏,故添加一个最大电阻250Ω电阻即可。
2. 绘制原理图(快速标号)
当需要连接的部分过多时,可以使用电气连接的方式将两根导线进行连接,这样可以省去大量空间。可以在相应导线部分进行右键,点击”添加网络标号“进行对导线编号,也可以点击左边工具栏中的"LBL”快速对导线进行编辑。
若标号过多且有顺序,可以进行批量标号选择“LBL"后,按下”A“键进入标号设置界面。
绘制完成的原理图
3. 在keil中编写相关流水灯的代码
1) 点亮一个灯
通过sbit对引脚进行定义。
#include "reg51.h"
sbit Led0 = P2^0;
void delay() //@12.000MHz
{
unsigned char i, j;
i = 12;
j = 169;
do
{
while (--j);
} while (--i);
}
void main()
{
while(1)
{
Led0 = 0;
delay();
Led0 = 1;
delay();
}
}
生成HEX文件后,在Proteus中双击芯片将HEX文件导入,运行Proteus即可看到单个LED闪烁的现象。
2) 流水灯
想要实现流水灯的效果,需要对灯进行移位,使其每一次只有一个灯点亮。以共阳极为例:
第一个灯:1111 1110
第二个灯:1111 1101
以此类推。那么在对引脚定义时,可以直接对P2进行定义
#include "reg51.h"
#include "intrins.h"
sbit Led0 = P2^0;
void delay() //@12.000MHz
{
unsigned char i, j, k;
_nop_();
_nop_();
i = 5;
j = 144;
k = 71;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void Led()
{
int i =1;
for(i = 1; i <=8; i++)
{
P2 = _crol_(P2,1);
delay();
}
}
void main()
{
P2 = 0xfe;
while(1)
{
Led();
delay();
}
}
生成HEX文件后在Proteus中仿真即可。
3)流水灯通用实现方式(数组)
在第二条中通过移位的方式进行实现流水灯可以满足少量情况,而涉及到复杂情况时,使用数组方式会更容易实现。仍使用共阳极LED。
#include "reg51.h"
#include "intrins.h"
unsigned char Led_Buf[]={
0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,
0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01
};
void Delay50ms() //@12.000MHz
{
unsigned char i, j, k;
_nop_();
_nop_();
i = 3;
j = 72;
k = 161;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main()
{
while(1)
{
int i = 0;
for(i=0;i<16;i++)
{
P2 = ~Led_Buf[i];
Delay50ms();
}
}
}
通过数组的形式,给P2口分配不同的0和1来实现复杂流水灯的形式。
三.数码管
1. 数码管原理
1)共阴/阳
数码管的原理与LED灯相同,同样分为共阳极与共阴极。
共阳极简称为CA,共阴极简称为CC
在共阴极数码管中,com口接地,a-dp端若接高电平,则指定部分亮。
a到g按顺时针方向绕一圈,dp代表小数点,在g的后面。在画完a-dp的框架后,根据想要亮的形状将其标1,其余标0。然后写出a-dp的16进制数,a为最小位,即从dp-a。填入后转为16进制即可。
共阳极与共阴极数码管编码
共阴极0-9:unsigned char Seg_Buf[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
共阳极0-9:
2)静态/动态显示
数码管分为动态显示与静态显示。
静态显示:
在Proteus中,数码管的IO口只有7个,是因为在Protues中小数点的位没有,但并不影响使用。Proteus中共阳极与共阴极的区分方式:com口在上为共阳极,com口在下为共阴极。
在Keil中编写代码,对P2 IO口进行定义,根据上面的数码管编码以数组的形式编写。
#include "reg51.h"
unsigned char Seg_Buf[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void delay(unsigned int n)
{
unsigned int i=0,j=0;
for(i=0;i<n;i++)
{
for(j=0;j<120;j++);
}
}
void Seg()
{
int i=0;
for(i=0;i<10;i++)
{
P2 = Seg_Buf[i];
delay(1000);
}
}
void main()
{
while(1)
{
Seg();
}
}
动态显示:
动态显示先选择位码,再配置段码。根据视觉暂留效应,观察到所有的数码管同时工作。数码管在进行动态显示时,需要使用两串IO口进行驱动,显然对IO口的数量有很大的需求。为了减少对IO口的需求,可以使用74ls138译码器(3-8译码器)来减少IO口。将电路图按此连接。
根据74ls138的工作原理。E1-E3为选通端。只有当E1为高电平,E2、E3为低电平时,译码器可用。根据ABC的二进制控制Y0-Y7工作。如果ABC为000,那么输出Y0为低电平,其余为高电平。可浏览3-8译码器的真值表。
在编写代码时,P3口只需对数组0x00-0x07循环访问,即可以三个IO口驱动位端。
若P3值为0x00,那么转换为二进制为000,ABC为000,输出Y0为低电平,其余为高电平。
若P3值为0x01,那么转换为二进制位001,ABC为001,输出Y1为低电平,其余为高电平。
代码部分:
#include "reg51.h"
unsigned char Seg_dula[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07};
unsigned char Seg_wela[]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07};
void delay(unsigned int n)
{
unsigned int i=0,j=0;
for(i=0;i<n;i++)
{
for(j=0;j<120;j++);
}
}
void Seg()
{
unsigned char i=0;
for(i=0;i<8;i++)
{
P3 = Seg_wela[i];
P2 = Seg_dula[i];
delay(5);
}
}
void main()
{
while(1)
{
Seg();
}
}
导入到Proteus中观察现象。
3)关于在驱动LED灯和数码管时不使用P0口进行驱动的原因
在学习中,我发现视频中从来不使用P0口进行驱动,进行查询发现P0口为8位漏极开路型双向并行I/O口。漏极开路是指漏极没有接上拉电阻也没与电源正级相连
具体8位漏极开路型双向并行I/O口挖个坑以后再填。先对今天学习的东西进行总结。浏览了一些资料后得知:
P0作为共阳极LED数码管的驱动端口时,P0是作为吸收电源的电流工作,所以不需要使用上拉电阻,所以此时P0与其他端口相同,可以正常工作。
但输出为高电平时,不正常输出高电平,而是进入了一个高阻态的情况(可以理解为0),那么无法正常输出高电平,导致共阴极LED灯无法正常进行工作。若想让其进行工作,需要手动添加一个上拉电阻。
关于上拉电阻阻值的选取问题。在查阅了相关资料后,发现若驱动LED、数码管等,470欧的上拉电阻即可,若要驱动三极管,则设置为1k,若要驱动信号,则设置为4.7k。方便以后更深入了解,放上两个链接。
1.关于51单片机的P0口上拉电阻取值问题 - 21ic电子网
2.51单片机P0口什么时候使用上拉电阻_51扩展总线为什么p0不需要上拉-CSDN博客
四.按键
1.按键的原理
以P1.1端口驱动一个按键为例,当按键未落下时,P1.1为高电平。当按键落下时,P1.1为低电平。
由于人按下按键时会存在抖动,所以消除抖动的方法有两种,一种为硬件消抖,一种为软件消抖。软件消抖是通过延时,将抖动部分略过。但延时时间不易确定,可以引入标志位的方式进行消抖。
2.独立按键
独立按键即一个IO口控制一个按键,这样容易操作但是会占用大量的IO口。
在Keil中编写程序,为了消除抖动,采取标志位方式进行消抖。
#include "reg51.h"
unsigned char Seg_Buf[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
unsigned char num = 0,flag = 0;
sbit key0 = P1^0;
void delay(unsigned int n)
{
unsigned int i=0,j=0;
for(i=0;i<n;i++)
{
for(j=0;j<120;j++);
}
}
void Key()
{
if(key0 == 0 && flag == 0)
{
flag = 1;
}
if(flag == 1 && key0 == 1)
{
flag = 0;
num++;
}
}
void Seg()
{
P2 = Seg_Buf[num];
if(num == 10)
num = 0;
}
void main()
{
while(1)
{
Key();
Seg();
}
}
3.矩阵按键
矩阵按键可以明显减少使用IO口的数量,判别的方式是先判断是否有按键按下,再判断是哪个按键按下。
判断是否有按键按下的方法:全扫描法
判断哪个按键按下的方法:逐行逐列扫描法
矩阵按键设置8个IO口,4个位行4个位列,在本次实验中,将他们设置在P1口。每次都对四行中的其中一行赋0,其余全为1,列全部都为1。以第一行举例。H0为低电平,H1-H3为高电平。如果第一行中有按键被按下,那么对应的列的标号从高电平变为低电平。可以先对LO-L3进行位定义.
sbit L0 = P1^0;
sbit L1 = P1^1;
sbit L2 = P1^2;
sbit L3 = P1^3;
那么如果P1 = 0xef时, 1110 1111 说明H0为低电平,H1-H3为高电平。若第一行第二列按键被按下,那么L1拉低变为0,可以根据if语句来判断。
Proteus仿真图:
Keil5代码:
#include <reg51.h>
sbit L0 = P1^0;
sbit L1 = P1^1;
sbit L2 = P1^2;
sbit L3 = P1^3;
unsigned char Seg_Buf[]={
0x3f,0x06,0x5b,0x4f,
0x66,0x6d,0x7d,0x07,
0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71};
unsigned char num = 99;
void delay(unsigned int n)
{
unsigned int i=0,j=0;
for(i=0;i<n;i++)
{
for(j=0;j<120;j++);
}
}
void Key_Read()
{
P1 = 0xef; //1110 1111 扫描第一行
if(L0 == 0) num = 0;
if(L1 == 0) num = 1;
if(L2 == 0) num = 2;
if(L3 == 0) num = 3;
delay(50);
P1 = 0xdf; //1101 1111 扫描第二行
if(L0 == 0) num = 4;
if(L1 == 0) num = 5;
if(L2 == 0) num = 6;
if(L3 == 0) num = 7;
delay(50);
P1 = 0xbf; //1011 1111 扫描第三行
if(L0 == 0) num = 8;
if(L1 == 0) num = 9;
if(L2 == 0) num = 10;
if(L3 == 0) num = 11;
delay(50);
P1 = 0x7f; //0111 1111 扫描第四行
if(L0 == 0) num = 12;
if(L1 == 0) num = 13;
if(L2 == 0) num = 14;
if(L3 == 0) num = 15;
delay(50);
}
void Seg_Disp()
{
P2 = Seg_Buf[num];
}
void main()
{
while(1)
{
Key_Read();
Seg_Disp();
}
}
仿真可以观察到数码管的效果。
矩阵按键的补充
在学习过程中,发现部分学习资料使用矩阵按键是先对P1赋值为 0000 1111,然后判断P1是否发生改变,若发生改变,则将P1值赋给temp0,再次修改P1为 1111 0000,再次判断P1是否发生改变,若发生改变,再次将P1赋给temp1,使用temp将temp0和temp1相加,再根据temp的值对应一个num值。
比如按下第一个按键,temp值变为0xee,num=0.按理来说数码管将要显示0,但是实际情况并不是这样。经过调试后发现keil中并没有代码编写错误,Proteus硬件部分也没有搭建错误。但是若将0xee换为255,那么数码管将正常显示0。再经过与他人讨论过后,发现这是Proteus的版本问题,更换一个较老的版本就可以解决这个问题。(我使用的是Proteus8.9)
五.定时器/计数器
定时器顾名思义就是具有定时的功能,51单片机有两个定时器/计数器。但是同一时刻只能使用其中的一种功能。51单片机提供了两个最大长度为16位的定时器,分别为T0和T1。
下列都以T0定时器为例。定时器的工作原理简单可以以一句话概括:启动定时器后,每个机器周期到来,初值寄存器自动加1,直到计数溢出。
1.定时器工作原理
1) 启动
使用定时器需要给一个启动信号,来告诉CPU现在要开始执行定时操作。
2) 机器周期
一个机器周期等于12个振荡周期,即计数频率为晶振频率的1/12。常见的晶振频率有11.0592MHz和12MHz。那么以11.0592MHz为例,一个机器周期即为:1/ ( 11.0592/12) us
3) 初值寄存器
定时器的寄存器就是专用的寄存器,T0的初值寄存器为TH0和TL0,分别为高字节访问和低字节访问。两者都是八位寄存器,合起来为T0的初值寄存器。单片机复位后,初值都为00H,合起来也就是16个0。
4) 自动加1
自动加1也就是定时器有一个计数的功能,只有经过了一个机器周期后,才会自动加1。每次从低位+1,直到加到255,变成高位。
5) 溢出
当经过了65535个机器周期,也就是16个1后,再经过一个机器周期,则变成1个1与16个0。此时发生溢出现象,代表一次定时结束。一次定时时间经过了65536个机器周期,也就是默认定时65.536ms。
2.初值的计算
1) 低于65.535ms的定时
已知默认的定时器定时65.536ms,不符合实际需求,所以如果想要定时50ms,则需要对初值进行修改。所以只需要从中间某刻开始进行机器周期,经过50000次机器周期(以12MHz为例)之后,定时结束,完成一次定时,那么用时50ms。即:
X + 50000 = 65536
X = 15536
那么只需要将15536转为16进制后把值给到TH0和TH1即可。有两种方式
第一种方式是以16进制分别给TH0和TH1。
TH0 = 0x3c
TL0 = 0xb0
第二种方式是使用公式,已知低八位一共为255,那么256就是高八位的最后一位。也就是高八位存入的是15536对256取整数,低八位取的是小于256的值。
TH0 = ( 65536 - 50000)/ 256
TL0 = ( 65536 - 50000)% 256
2) 高于65.536ms的定时
如果想要定时1s,那么可以先定时50ms,然后再进行循环语句重复进行20次循环即可。
3.编程实现
1) 报备
报备即告诉CPU信息,需要使用定时器功能还是使用计数器功能。
定时器的报备使用一个专用的寄存器TMOD(定时模式)进行设置,TMOD是8为寄存器,低4位设置定时器0,高4位设置定时器1。低两位是用来设置定时器模式,高两位是指示操作。TMOD的字节地址位89H,不能进行位寻址,所以在进行设置的时候,只能使用16进制给值。单片机复位时,TMOD全部清0
(1) Gate:门控位
Gate = 1时,定时器/计数器的启动与停止由TCON寄存器中的TRx(TR0或TR1)和外部中断引脚INTx(INT0或INT1)的电平状态共同控制。
Gate = 0时,定时器/计数器的启动与停止仅由TCON寄存器中的TRx(TR0或TR1)控制。
TR0 = 1启动定时器, TR0 = 0,关闭定时器
(2) C/T:定时器/计数器选择位
C/T=0,定时功能
C/T=1,计数功能
(3)M1/M0:工作方式的选取
在了解完TMOD所有位的功能后,因为使用定时器0,所以定时器1的四位都置0,定时器0使用方式1,则位0001,那么TMOD8位为 0000 0001,TMOD=0x01
2) 置初值
初值根据需要定时的时间进行设置。
TH0 = ( 65536 - 50000)/ 256
TL0 = ( 65536 - 50000)% 256
3) 启动
启动定时器需要用到寄存器TCON,字节地址为88H,同样需要特别注意的是,TCON可以进行位寻址,也就是可以单独对寄存器TCON中的某一位进行设置。TCON寄存器用来控制定时器的启动与停止。
(1) TFx( TF0与TF1)
这个位分别对应定时器0与定时器1的定时器溢出标志位。当定时器1计满溢出时,由硬件自动将TFx置1。如果存在定时器中断服务程序,那么硬件会自动清0。但是如果没有涉及到中断,那么需要软件查询法手动将TFx置0。
(2) TRx( TR0与TR1)
定时器0/1的的运行控制位。
当Gate=1且INTx为高电平时,TRx = 1 ,启动定时器1
当Gate=0时, TRx=1,启动定时器1
TRx=0,关闭定时器
4) 等待
同上述TFx所述,在本节只使用了定时功能,所以当计数器计满溢出时,硬件会将TFx置1,这是需要手动将TFx置0来重新计数。那么判断是否溢出可以使用while语句
while(TF0 == 0)
如果没有计满,那么一直执行while语句,知道计满TF0=1,然后需要手动将TF0置0使其重新计数
5) 重置初值
如果需要计时1s,需要重复进行计数,那么这时就需要进行重置初值,来让定时器重复进行工作,重置初值的方法与上面相同。
6) 清溢出
手动将TF0 = 0,进行清0。
4.代码编写与Proteus仿真
通过定时器设置1s的时间间隔,每1s LED交替闪烁实现流水灯功能。
代码:
#include "reg51.h"
unsigned char num=0;
unsigned char Led_Buf[]={
0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,
0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01
};
void InitTimer0()
{
TMOD = 0x01; //报备,选择定时器并且使用定时器0
TH0 = (65536-50000)/256; //设初值 50ms
TL0 = (65536-50000)%256; //设初值
TR0 = 1; //启动定时器0
}
void main()
{
unsigned char i=0;
InitTimer0();
while(1)
{
while(i<20) //循环定时,设置为1s
while(TF0 == 0); //判断定时是否完成
TH0 = (65536-50000)/256; //重新设初值
TL0 = (65536-50000)%256; //重新设初值
TF0 = 0; //手动将溢出标志位置0
i++;
}
i=0;
P2 = ~Led_Buf[num];
num++;
if(num == 17)
num = 0;
}
}
Proteus仿真效果
5.其他定时方式
除了常用的模式1以外,还有模式0和模式2,分别位13位定时与8位重装载定时。
(1) 13位定时
13位定时的使用方法与16位定时的使用方法相同,区别在于16位定时的THx和TLx的8位都参与计数,而13位计数器的THx的8位参与计数,而TLx仅有低5位参与计数,高三位不参与计数。所以在设置初值的时候需要特别注意。在使用第二种计数方式时,需要对2^5=32取整和取余。
(2) 8位重装载定时
8位重装载定时的好处在于不需要像16位那样每次定时结束都需要重新对THx和TLx设置初值。模式2定时器只有8位参与计数,当TLx计数溢出时,单片机会自动把THx中的值装给TLx,继续进行定时计数,这就完成了8位重装载。与其他模式相比,不需要在中断程序在对THx和TLx赋值,只需在初始化时,对TLx和THx赋相同的值即可。但是8位重装载的定时时间很短,所以一般只在单片机串行通信时使用。
6.计数器
计数器与定时器的原理和使用方法相同,唯一不同的是TMOD不同与THx和TLx中代表的意义不同。在计数器中,计数的次数与定时器中定时的时间相同,只需要对初值进行修改即可。
(1) 代码编写与Proteus仿真
假设每按3次按键,数码管依次显示
代码部分:
#include "reg51.h"
// 数码管段码数组
unsigned char Seg_Buf[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
// 初始化定时器0为计数器模式
void InitCount0() {
TMOD = 0x05; // 设置定时器0为模式1,C/T=1,选择计数器
TH0 = (65536 - 3) / 256; // 设置高位初值
TL0 = (65536 - 3) % 256; // 设置低位初值
TR0 = 1; // 启动计数器0
}
void main() {
unsigned char num = 0; // 当前显示的数字
InitCount0(); // 初始化计数器
P2 = Seg_Buf[num]; // 显示初始值
while (1) {
if (TF0) { // 检测计数器是否溢出
TH0 = (65536 - 3) / 256; // 重新设置高位初值
TL0 = (65536 - 3) % 256; // 重新设置低位初值
TF0 = 0; // 清除溢出标志
num++; // 显示值加1
if (num >= 10) { // 防止显示溢出
num = 0;
}
P2 = Seg_Buf[num]; // 更新数码管显示
}
}
}
在代码部分,因为还没有介绍中断,所以没有使用中断服务函数,而是对计数器如何进行工作进行了详细了了解以及了解与定时器的区别。在这段代码中,在一次计数结束后仍然需要重新设置初值,计数完成后仍有标志位。可能有读者会好奇明明使用了按键,可是为什么在代码里面并没有对按键进行定义呢?
通过浏览89C51单片机的引脚可以发现,P3^4引脚可以作为定时器0的外部脉冲输入。而定时器的一大功能就是收集脉冲信号,收集一次计数一次。所以可以把按键作为脉冲来进行计数。
再有需要注意的是,在定时器初始函数下面第一次对数码管进行显示,这是因为计数器一旦结束,num就会变成1,无法显示出0这种情况。
(2) Proteus仿真
搭建电路:
仿真结果:
六.中断
对单片机而言,中断就是CPU在处理事件A时,发生了另一间事件B,请求CPU去立即处理(中断发生),CPU暂时停下当前的工作(中断响应),转而去做事件B(中断服务);待CPU将事件B处理完后,再回到原来的事件A被中断的地方继续处理事件A(中断返回)。
1.工作原理
(1) 中断源
在定义中提到了突发事件,那么突发事件就是中断源。中断源打断的是正常运行的程序。一旦中断发生那么就会立即跳入到中断服务程序中执行另一事件,直到事件结束才会重新返回程序被打断前的地方。
单片机提供了5个中断源
在此处使用定时器0中断举例。
(2) 中断允许
同定时器中的报备一样,使用中断同样要告诉单片机需要使用中断,进行打开中断开关。由中断允许标志位决定。中断允许的设置使用专用的寄存器IE(Interrupt Enable Register)。同TCON寄存器一样,IE寄存器也支持位寻址,可以直接针对某一位进行设置
EA:全局中断允许位。
EA=1,打开全局中断控制,由各中断控制位确定相应中断的打开或关闭
EA = 0,关闭全部中断
ES:串行口中断允许位
ES=1,打开串行口中断;
ES=0,关闭串行口中断
ETx:定时器/计数器(0/1)中断允许位。
ETx=1,打开Tx中断
ETx=0,关闭Tx中断。
EXx:外部中断0/1中断允许位
EXx=1,打开外部中断x中断
EXx=1,关闭外部中断x中断
如果想要使用中断,需要先打开全局中断,将EA置1,然后再打开想要使用的中断,将其中断允许为置1。
(3) 中断请求
既然要发生中断,那么肯定有请求,发生请求之后然后开始中断。各中断源的中断请求形式不同,分为外部中断和定时器中断。定时器0的中断请求为发生溢出。
中断请求标志位:以定时器0为例,发生溢出,那么TF0会自动变为1,这就是中断请求标志位
(4) 中断响应
响应中断就是执行中断服务程序,但是在执行中断服务程序前需要保存当前的断点。
中断服务程序,中断服务程序也就是另一突发事件,也存在一个函数,在这个函数里需要写发生中断后需要执行什么事情。但是在中断服务程序的函数后还需要添加 interrupt x,x(中断号)根据具体使用到的中断源进行填写。
(5) 中断返回
完成中断后,程序会自动返回到中断发生前的位置继续执行之前的事件。
可以看出,中断无非就是多了一个启动的过程,告诉CPU我需要使用中断以及多了有一个函数,需要将发生中断去执行语句写入其中。下面会以一个示例来详细讲解。
2. 代码编写与Proteus仿真
想要实现 用定时器0的方式1实现第一个LED以200ms间隔闪烁,同定时器1的方式1实现数码管前两位59s循环计时。
这里面使用到了两个中断,那么就需要启动两个中断。
#include "reg51.h"
unsigned char num1 = 0, num2 = 0, num = 0;
unsigned char shi, ge;
unsigned char Seg_Buf[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
sbit Led = P2^2;
sbit LS0 = P2^0;
sbit LS1 = P2^1;
// 初始化定时器0为计数器模式
void InitTimer0() {
TMOD = 0x01; // 设置定时器0为模式1
TH0 = (65536 - 50000) / 256; // 设置高位初值
TL0 = (65536 - 50000) % 256; // 设置低位初值
TR0 = 1; // 启动定时器0
ET0 = 1; // 使能定时器0中断
}
void InitTimer1() {
TMOD |= 0x10; // 设置定时器1为模式1
TH1 = (65536 - 50000) / 256; // 设置高位初值
TL1 = (65536 - 50000) % 256; // 设置低位初值
TR1 = 1; // 启动定时器1
ET1 = 1; // 使能定时器1中断
}
void Timer0IsrServer() interrupt 1 {
TH0 = (65536 - 50000) / 256; // 设置高位初值
TL0 = (65536 - 50000) % 256; // 设置低位初值
TF0 = 0; // 清除中断标志
num1++;
if(num1 == 5) {
num1 = 0;
Led = ~Led;
}
}
void Timer1IsrServer() interrupt 3 {
TH1 = (65536 - 50000) / 256; // 设置高位初值
TL1 = (65536 - 50000) % 256; // 设置低位初值
TF1 = 0; // 清除中断标志
num2++;
if(num2 == 21) {
num2 = 0;
num++;
if(num == 60)
num = 0;
shi = num / 10;
ge = num % 10;
}
}
// 延时函数
void delay(unsigned int time) {
unsigned int j = 0;
for(; time > 0; time--)
for(j = 0; j < 125; j++);
}
// 显示函数
void display() {
LS0 = 0; LS1 = 1; // 选择数码管第1位
P1 = Seg_Buf[shi]; // 显示十位数
delay(5); // 延时一段时间
P1 = 0x00; // 消隐
LS0 = 1; LS1 = 0; // 选择数码管第2位
P1 = Seg_Buf[ge]; // 显示个位数
delay(5); // 延时一段时间
P1 = 0x00; // 消隐
}
void main() {
EA = 1; // 使能全局中断
InitTimer0();
InitTimer1();
while(1) {
display(); // 调用显示函数
}
}
Proteus:
这里就不放动图了,点击运行即可。
七. 串口通信
本节考虑到由于此博客用于51单片机在Proteus上进行仿真,知道使用方法以及懂得如何在Proteus上进行仿真即可。所以在本节只介绍如何使用UART串口通信以及如何在Proteus上正确进行仿真。具体的原理内容会放到8051单片机中进行介绍。
1. 概况
51单片机的串口是一个异步全双工的UART串口。
1). TXD/RXD
这两个引脚分别是发送引脚和接收引脚。
2). TI/RI
TI是发送中断请求标志位。
RI是接收中断请求标志位。
回顾中断里提到的中断源,串口使用的中断源,需要使用串口时需要先把串口的中断允许标志位打开,也就是将ES置1。然后由于串口通信存在接收结束和发送结束,所以一共有两个中断请求标志位,一个是TI=1,一个是RI=1。并且中断号为1。
另外需要注意的是,中断发生之后,也就是中断请求标志位置1,需要手动将其清0。
3) SBUF:串口数据缓冲寄存器
SBUF是用来告诉CPU发送的数据以及接收时存放数据的地方。
接收时UART直接将数据存放在SBUF中。
发送时直接将要发送的内容放入SBUF中即可。
2. SCON寄存器
上述TXD/RXD与TI/RI都属于SCON寄存器中的一位,同TCON相同,SCON也支持位寻址,可以单独对其中的某一位进行修改。
1) SM0/SM1:工作方式选择
SM0和SM1用来确定串口的工作方式
与定时器相同,在使用之前需要告诉CPU想要使用哪一种工作方式。波特率一般使用常用的9600波特率。
2) SM2: 多机通信控制位
SM2主要用于方式2和方式3。如果只有两个设备,只需要将其置0即可。
3) REN:允许串行接收位
REN=1,允许串行口接收数据。
REN=0,禁止串行口接收数据。
4) TB8/RB8:数据校验位
使用方式1不涉及到校验,所以将TB8和RB8直接置0即可。
5) TI/RI:中断请求标志位
同上。
这就是SCON寄存器的全部内容,需要在使用串口通信之前,告诉CPU相应的内容,所以需要在主程序初始化位置就把SCON寄存器配置好。
3. 波特率
对于串口通信,最重要的就是波特率,在使用单片机的串口通信时,需要将上位机与单片机的波特率对齐,最常用的就是9600波特率。单片机提供了一个专门产生波特率的寄存器:T1。在定时器中,T1用于定时器1,但是在串口中,T1还有产生波特率的功能。如何告诉T1需要使用9600的波特率呢?
已知9600波特率就是每1/9600s接收或发送一位数据。可以根据这个时间间隔来设置定时器1的初值然后启动进行使用。每次发生数据溢出时,也就是实现了一位数据的传输,从而实现波特率的功能。
因为使用串口通信使用的是方式1,对应的是8位数据,那么定时器1就要使用方式2:8位自动重装载定时器。那么就无需在溢出时手动对溢出标志位清0。编写时不可以开启T1的中断,也不需要编写T1的中断服务程序。
4. T1的初值
T1初值的计算公式:
为了更深入理解单片机内部结构,可以将上述公式再次进行转换:
SMOD:
SMOD是电源管理寄存器PCON中的最高位,PCON寄存器不能进行位寻址。SMOD与串口通信波特率有关 SMOD=0时,串口波特率正常。 SMOD=1时,串口波特率翻倍。
2和16:
在上面概况介绍内部结构时SMOD内部存在2分频和16分频,所以第二个计算公式更直观。
5. 代码编写与程序仿真
1) 代码编写
(1) 设置串口SCON
首先要设置串口的工作模式,我们使用工作模式1。
SCON = 0x50
(2) 设置TMOD
TMOD使用定时器1,同时使用定时器1的8位重装载功能,也就是方式2。
TMOD = 0x20
(3) 设置波特率
确定波特率后要对T1进行设置初值,理解原理之后直接查询下表即可。
因为不对波特率进行翻倍,所以
TH1 = TL1 = 0xfd
(4) 开中断
在使用串口通信时,除了开总中断和定时器1以外,还需要额外打开串口中断。
串口中断:ES = 1
总中断: EA = 1
打开定时器1: TR1 = 1
(5) 代码部分
在本次实验中,仿真下列情况:输入1,单片机点亮第一个灯;输入2,单片机点亮第二个灯;以此类推知道点亮第八个灯。并且每次只能存在一个灯点亮,若输入不属于1-8的情况,则接收ERROR信息。
#include "reg51.h"
unsigned char rec = 0; // 定义全局变量rec,用于存储接收到的数据
void Init_Serial()
{
SCON = 0x50; // 设置串口模式1,8位数据,允许接收
TMOD = 0x20; // 设置定时器1为模式2,8位自动重装
TH1 = TL1 = 0xFD; // 设置定时器重装值,以产生9600的波特率
ES = 1; // 使能串口中断
EA = 1; // 使能全局中断
TR1 = 1; // 启动定时器1
}
void Send_Error()
{
char error_msg[] = "ERROR";
unsigned char i = 0;
while (error_msg[i] != '\0') // 逐字节发送 "ERROR" 字符串
{
SBUF = error_msg[i]; // 发送当前字符
while (!TI); // 等待发送完成
TI = 0; // 清除发送中断标志
i++; // 发送下一个字符
}
}
void Serial_Server() interrupt 4
{
if (TI) // 检查发送中断标志
TI = 0; // 清除发送中断标志
if (RI) // 检查接收中断标志
{
rec = SBUF; // 读取接收到的数据
RI = 0; // 清除接收中断标志
// 根据接收到的数据进行相应处理
switch (rec)
{
case '1': P2 = ~0x01; break; // 输入'1'时,点亮第一个LED
case '2': P2 = ~0x02; break; // 输入'2'时,点亮第二个LED
case '3': P2 = ~0x04; break; // 输入'3'时,点亮第三个LED
case '4': P2 = ~0x08; break; // 输入'4'时,点亮第四个LED
case '5': P2 = ~0x10; break; // 输入'5'时,点亮第五个LED
case '6': P2 = ~0x20; break; // 输入'6'时,点亮第六个LED
case '7': P2 = ~0x40; break; // 输入'7'时,点亮第七个LED
case '8': P2 = ~0x80; break; // 输入'8'时,点亮第八个LED
default:
Send_Error(); // 发送错误消息 "ERROR"
P0 = 0xFF; // 设置P0口输出,全亮
break;
}
}
}
void main()
{
Init_Serial(); // 调用初始化串口函数
P2 = 0xFF; // 初始化 P2 口,关闭所有 LED
while (1); // 主循环,等待中断
}
这里想要提出的一点是,因为单片机与上位机之间传输的数据是HEX,也就是16进制数。所以在传输和接收的时候,数字要使用''符号进行强调,但是我注意到在实物单片机操作中似乎并不需要这一操作。但是在Proteus仿真时需要该操作。然后发送错误消息接收ERROR时也是逐步接收ERROR的16进制数。
2) Proteus仿真
首先搭建硬件电路。
这里对使用到的器件进行说明,首先共阳极LED要使用到的排阻已经使用很多次了,在库里面可以搜索"RX8"进行使用。
串口调试工具在左栏
选择后即可进行是使用。有时候进行调试时串口框会消失,找到调试,最底部有Virtual terminal,打开即可继续调试。
仿真结果:
点击仿真即可,这里由于没有设置输入1-8返回1-8,所以效果不是特别好,懒得搞了,不是很难,自己可以尝试一下。