前言
这篇文章仅为个人学习单片机所总结的笔记,可能更多的是单片机的应用部分,而单片机的原理部分大多都一笔带过。如果文章有错误的地方,多多担待。
注:本文所使用到单片机型号为普中的STC89C52开发板
准备
代码编写软件:Keil uVision5,可以到官网下载:https://www.keil.com/
烧录软件为:stc-isp,官网:https://www.stcmicro.com/rjxz.html
一、单片机基本原理
1.1 单片机核心
上面是单片机的核心,stc89c52一共有24个IO口,分别是P0.0-P0.7、P1.0-P1.7、P2.0-P2.7、P3.0-P3.7;
IO口也就是input(输入)/output(输出),通俗点理解就是可以对外输出高低电平,同样能检测外界的高低电平。对外输出高低电平控制外部电路,同时检测外部电路的电平变化,这是单片机最基本的控制原理。
1-2 单片机的基本控制
刚刚说了,stc89c52一共有24个IO口,分为4个部分:P0、P1、P2、P3;每个部分8个IO口,这正好是1个字节(byte);所以我们可以使用一个字节直接控制8个IO口。 每个IO口都可以对外输出高电平/低电平,分别对应1和0。
例如:
//我们需要P0.0口和P0.5对外呈现高电平,其它口呈现低电平:
P0=0x21;
//0x21对应的二进制为:0 0 1 0 0 0 0 1
//高位在前,低位在后:P0.7 P0.6 P0.5 P0.4 P0.3 P0.2 P0.1 P0.0
二、点亮LED
2.1 LED模块
LED模块的阳极接了高电平,阴极接了P2,所以想要点亮LED,需要对应的IO口呈现低电平。
注:IO口一开始默认都为高电平,并且IO口的驱动能力比较弱,一般都不会用高电平来驱动其它电子元件。
2.2 点亮第一个LED
#include <REGX52.H>
void main()
{
while(1)
{
P2 = 0xFE; //1111 1110 点亮第1个led
P2 = 0x7F; //0111 1111 点亮第8个led
P2 = 0x5F; //0101 1111 led 6,8亮
P2 = 0xAA; //1010 1010 led 1,3,5,7亮
P2 = 0x55; //0101 0101 led 2,4,6,8亮
}
}
2.3 LED闪烁
即LED亮灭两个状态的切换,如果切换太快就会有一些问题,一是切换太快单片机不一定能反应的过来,二是人眼不一定能看得清这两种状态。所以需要在切换时作停顿,如果不需要特别准确的时间,可以让单片机跑循环,因为执行命令的次数越多,所需时间就越多。大致时间可以参照普通的计算机,普通的计算机1s大约可以执行108-109条代码,单片机的话肯定会更慢。
那如果需要较为准确的时间的话,可以打开stc-isp,选择软件延时计算器,选择单片机上面晶振对应的频率,我的是11.0592MHz,然后选择延时的时间,以及指令集(单片机型号)
点击生成C代码,然后复制到工作区代码里。这里是1ms的延时:
void Delay1ms() //@11.0592MHz
{
unsigned char data i, j;
_nop_();//如果有这一句代码,需要加头文件<INTRINS.H>
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
为了调用方便,我们可以这样改一下
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)//执行xms次,也就是延时xms毫秒
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
现在我们让第一个LED亮500ms,灭500ms的方式闪烁
#include <REGX52.H>
#include <INTRINS.H>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void main()
{
while(1)
{
P2 = 0xFE;
Delay(500); //延迟500ms
P2 = 0xFF;
Delay(500);
}
}
三、独立按键
3.1 独立按键模块
P31、P30、P32、P33分别接了K1、K2、K3、K4,当我们按下对应的按键时,对应的IO口呈现低电平
3.2 独立按键控制LED状态
K1、K2、K3、K4按键分别控制LED0、LED1、LED2、LED3的亮灭。按下时,如果LED是亮的,则会熄灭;如果是熄灭的,则会亮。
#include <REGX52.H>
void main()
{
while(1)
{
if(P3_1==0)
{
P2_0=~P2_0;//对IO口进行取反,1变0,0变1
}
else if(P3_0==0)
{
P2_1=~P2_1;
}
else if(P3_2==0)
{
P2_2=~P2_2;
}
else if(P3_3==0)
{
P2_3=~P2_3;
}
}
}
3.3 按键消抖
上面的代码看似没问题,但是真正到了单片机上面,你会发现反应不太灵敏。这是因为开关按下或松开时存在按键抖动。
通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。所以在开关闭合及断开的瞬间会伴随一连串的抖动。
抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。按键稳定闭合时间的长短则是由操作人员的按键动作决定的。为确保CPU对键的一次闭合仅作一次处理,必须去除键抖动。在按键闭合稳定时读取键的状态,并且必须判别到键释放稳定后再作处理。
消抖的方式有很多种,这里只讲一种软件消抖,即利用软件延时消抖:
检测出按键闭合后执行一个延时程序,5ms~10ms(取决于机械特性)的延时,让按键闭合稳定后再一次检测按键的状态,如果仍保持闭合状态电平,则确认为真正有键按下。当检测到按键释放后,也要给5ms~10ms的延时,待后沿抖动消失后才能转入该键的处理程序。
#include <REGX52.H>
#include <INTRINS.h>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void main()
{
while(1)
{
if(P3_1==0)
{
Delay(20);//5ms~10ms只是一个大概范围,20ms会更精确一些,但消耗的CPU资源也更多,也可以取10ms,因人而定
if(P3_1==0)
{
P2_0=~P2_0;
while(P3_1==0);//等待按键释放
Delay(20); //延时消抖
}
}
else if(P3_0==0)
{
Delay(20);
if(P3_0==0)
{
P2_1=~P2_1;
while(P3_0==0);
Delay(20); //延时消抖
}
}
else if(P3_2==0)
{
Delay(20);
if(P3_2==0)
{
P2_2=~P2_2;
while(P3_2==0);
Delay(20); //延时消抖
}
}
else if(P3_3==0)
{
Delay(20);
if(P3_3==0)
{
P2_3=~P2_3;
while(P3_3==0);
Delay(20); //延时消抖
}
}
}
}
3.4 独立按键控制LED显示二进制
K1按下变量加1,K2按下变量减1,变量以二进制LED显示
#include <REGX52.H>
#include <INTRINS.h>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void main()
{
unsigned char LEDNum=0;
while(1)
{
if(P3_1==0) //如果K1按键按下
{
Delay(20); //延时消抖
if(P3_1==0)
{
LEDNum++; //变量自增
P2=~LEDNum; //变量取反输出给LED
while(P3_1==0); //松手检测
Delay(20); //延时消抖
}
}
if(P3_0==0) //如果K2按键按下
{
Delay(20); //延时消抖
if(P3_0==0)
{
LEDNum--; //变量自减
P2=~LEDNum; //变量取反输出给LED
while(P3_0==0); //松手检测
Delay(20); //延时消抖
}
}
}
}
四、数码管
4.1 数码管基本原理
数码管是由多个发光二极管封装在一起组成8字型的器件,引线已在内部连接完成,只需引出它们的各个笔划,公共电极;2、数码管常用段数一般为7段,有的另加一个小数点。如果公共极接的是阴极(右上图)则称为共阴;如果接的是阳极(右下图)则称为共阳。
显示时只需点亮对应的LED即可,比如我们要显示5,则只需要点亮a c d f g段即可。
4.2 数码管模块
开发板上有8个数码管,P0控制段选(a,b,c,d,e,f,g,dp),P22、P23、P24通过74HC138译码器控制位选(具体控制哪一个数码管)。74HC138的作用是通过P22、P23、P24三个IO口所表示的二进制数选择Y0—Y7中的一个端口输出低电平。例如:P22、P23、P24分别为1 0 1,二进制101是十进制的5,即Y5会输出低电平。
经过测试,8个数码管是共阴数码管,公共端低电平有效。
4.3 静态数码管显示
让每一位数码管分别显示数字5
#include <REGX52.H>
#include <INTRINS.h>
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void main()
{
unsigned char i=0;//位选
P0=0x6D;//显示数字5,即a c d f g高电平,其余为低电平,二进制就是0110 1101,十六进制为0x6D
while(1)
{
P2_2=i&1;//取出i的最低位
P2_3=i>>1&1;//右移一位后再取出最低位
P2_4=i>>2&1;//右移两位后再取出最低位
/*
例如i为5,二进制表示为101,第一次取出最低一位1赋值给P2_2
然后先右移一位,101-->010,取出最低位为0赋值给P2_3;
接着右移两位,101-->001,取出最低位为1赋值给P2_4;
这样P22、P23、P24就分别为1 0 1了
*/
if(++i==8)i=0;//i等于8后,再将其重置为0
Delay(1000);//延时1s
}
}
4.4 动态数码管显示
在上一个样例中,数码管每次只能选择一个数码管进行显示,如果要多个数码管同时进行显示,称为数码管的动态显示。
数码管的动态显示:数码管的动态显示又叫做数码管的动态扫描。动态显示的特点是:将所有位数码管段选线并联在一起,由位选控制是哪一位数码管有效。所谓动态扫描显示即轮流向各位数码管送出字型码和相应的位选,利用发光管的余晖和人眼视觉暂留作用,使人感觉好像各位数码管同时都在显示,而实际上多位数码管是一位一位轮流显示的,只是轮流的速度比较快,人眼已经无法分辨出来。
每个数码管分别显示0—7
#include <REGX52.H>
#include <INTRINS.h>
//先将数码管段码记录下来 0 1 2 3 4 5 6 7 8 9
unsigned char seg_dula[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void main()
{
unsigned char i=0;
while(1)
{
P0=0;//为了避免干扰,在显示之前先全部关闭,在作显示
P2_2=i&1;
P2_3=i>>1&1;
P2_4=i>>2&1;
P0=seg_dula[i];//选择要显示的数字
if(++i==8)i=0;
Delay(1);//短暂延时使显示更稳定
}
}
从0显示到1000
#include <REGX52.H>
#include <INTRINS.h>
//先将数码管段码记录下来 0 1 2 3 4 5 6 7 8 9 不显示
unsigned char seg_dula[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0};
unsigned char Buffer[]={10,10,10,10,10,10,10,10};//显示缓冲数组(8个数码管分别要显示的数字)
unsigned int num=0,x;
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void display()
{
unsigned char i=0;
for(i;i<8;i++)
{
P0=0;
P2_2=i&1;
P2_3=i>>1&1;
P2_4=i>>2&1;
P0=seg_dula[Buffer[i]];
Delay(1);//短暂延时使显示更稳定
}
}
void main()
{
unsigned char i=0,j=0;
while(1)
{
x=num;
for(i=0;i<8;i++)
{
Buffer[i]=x%10;
x/=10;
if(x==0)break;
}
for(j=0;j<25;j++)
{
display();;
}
if(++num>1000)//大于1000后清空缓冲数组
{
for(i=0;i<8;i++)
{
Buffer[i]=10;
}
num=0;
}
}
}
五、矩阵按键
5.1 矩阵按键模块
矩阵按键由4行4列按键组成,跟独立按键的原理一样,检测电平的高低来判断按键是否按下。唯一的区别就是独立按键的另一端接地,而矩阵按键全部接IO口。
5.2 按键检测方法
采取逐行或逐列扫描的方式检测:先让P1=0xFF,P1_7=0;判断P1_0——P1_3是否为0,如果为0,则由行与列就可以确定是哪个按键按下,比如说,P1_2=0,顺着P1_2走到P1_7就可以判断出是S2按键按下。以此类推。
unsigned char MatrixKey()//返回按键码 1—16
{
unsigned char KeyNumber=0;
P1=0xFF;
P1_3=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=13;}
P1=0xFF;
P1_2=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=10;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=14;}
P1=0xFF;
P1_1=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=7;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=11;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=15;}
P1=0xFF;
P1_0=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=4;}
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=8;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=12;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=16;}
return KeyNumber;
}
线翻转扫描方法:对P1赋值为0x0F(行全为0,列全为1),在对P1进行检测,如果P1不等于0x0F,则有按键按下,对此时的P1进行读取并记录,再令P1=0xF0,再对P1进行读取并记录,此时这两个数的就可以确定哪个按键按下。
unsigned char MatrixKey()
{
unsigned char key_value=0;
P1=0x0F;
if(P1!=0x0F)
{
Delay(20);
if(P1!=0x0F)
{
P1=0x0F;
switch(P1)//确定列
{
case 0x07:key_value=1;break;
case 0x0B:key_value=2;break;
case 0x0D:key_value=3;break;
case 0x0E:key_value=4;break;
}
P1=0xF0;
switch(P1)//确定行
{
case 0x70:key_value=key_value;break;
case 0xB0:key_value=key_value+4;break;
case 0xD0:key_value=key_value+8;break;
case 0xE0:key_value=key_value+12;break;
}
while(P1!=0xF0);//等待按键松开
}
}
return key_value;
}
优点:只需要进行两次扫描即可确定按键
5.3 数码管显示矩阵按键按键码
#include <REGX52.H>
#include <INTRINS.h>
//先将数码管段码记录下来 0 1 2 3 4 5 6 7 8 9 不显示
unsigned char seg_dula[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0};
unsigned char Buffer[]={10,10,10,10,10,10,10,10};//显示缓冲数组(8个数码管分别要显示的数字)
unsigned int num=0,x;
void Delay(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void display()
{
unsigned char i=0;
for(i;i<8;i++)
{
P0=0;
P2_2=i&1;
P2_3=i>>1&1;
P2_4=i>>2&1;
P0=seg_dula[Buffer[i]];
Delay(1);//短暂延时使显示更稳定
}
}
unsigned char MatrixKey()
{
unsigned char key_value=0;
P1=0x0F;
if(P1!=0x0F)
{
Delay(20);
if(P1!=0x0F)
{
P1=0x0F;
switch(P1)
{
case 0x07:key_value=1;break;
case 0x0B:key_value=2;break;
case 0x0D:key_value=3;break;
case 0x0E:key_value=4;break;
}
P1=0xF0;
switch(P1)
{
case 0x70:key_value=key_value;break;
case 0xB0:key_value=key_value+4;break;
case 0xD0:key_value=key_value+8;break;
case 0xE0:key_value=key_value+12;break;
}
while(P1!=0xF0)
{
Buffer[0]=key_value%10;
Buffer[1]=key_value/10%10;
display();
}
}
}
return key_value;
}
void main()
{
while(1)
{
MatrixKey();
}
}
六、定时器/计数器
51系列的单片机内部都有两个定时器/计数器T0、T1;每个定时器计数器都有定时方式和计数方式两种工作方式。可通过内部的特殊寄存器TMOD来选择T0或T1为定时器还是计数器。其本质是对脉冲进行计数,并存储计数值,当这个计数值溢出时就会进入中断系统执行中断程序。
6.1 中断系统
由于篇幅过长以及繁琐,所以该小节会单独作为一篇文章,详情请跳转:中断系统 – Aurora’s Blog (lolife.top)
6.2 定时器/计数器T0和T1的相关寄存器
由于TCON是可位寻址的,所以可以对TCON下的每一位单独配置。
小知识:
可位寻址:可对该寄存器下的每一位单独配置,例如TCON,我们可以直接对TF0等直接操作;
不可位寻址:不能单独配置,需要对整个寄存器进行配置,例如TOMD,只能对整个寄存器一起操作;
TMOD寄存器
定时器0有4种工作模式:13位定时器、16位定时器、8位自动装载定时器、双8位定时器
我们最常用的是16位定时器,所以这里只讲16位定时器。
6.3 定时器/计数器结构
SYSclk是系统时钟,即晶振周期,接着对晶振频率进行分频,默认是12T分频,C/T是选择用内部系统时钟(置0)还是外部时钟(置1);TR0是在TCON中配置,用于控制定时器/计数器开启(置1)或关闭(置0);GATE可以选择是由TR0单独控制开关还是INT0外部中断与TR0共同控制,GATE=0时,由TR0单独控制,GATE=1时,由INTE0与TR0共同控制(TR0可理解为总控制)。
SYSclk每来一个脉冲,计数就加1,这个计数是变量由TH0(高8位)和TL0(第8位)共同表示,当这个计数溢出时,就会产生中断标志(TF0),进而进入中断系统。
6.4 定时器的配置
内部时钟晶振为12MHz,T0计时器时间为1ms:
TCON:
只需要配置TR0、TF0;TR0=1,TF0=0;
TMOD:
高4为不需要配置,GATE=0,C/T=0,M1=0,M0=1
void Timer0Init(void)
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1; //使能定时器0中断
EA=1; //中断总开关开启
PT0=0; //中断优先级设置,如果不需要优先级也可以不写
}
我们也可以用STC-ISP来配置:
选择工具——>定时器计算器,选择系统频率(晶振频率)以及需要的定时的时间就可以了
需要注意的是,89C51没有16位自动重载以及24位自动重载的,选择16定时器模式时不要选到了16自动重载,以及定时器时钟默认是12T的,不要选成1T的。
自动重载是指,当计数器溢出时,会把定时的初值重新赋予TH0以及TL0,非自动重载则需要手动赋值。
AUXR是定义的辅助寄存器,在89C51中只有最低两位有具体功能,其他的没有功能
所以第3行我们可以删除,但其他系列的单片机则不一定,具体功能需要查阅手册。
6.5 定时器时间计算方法
我们知道时间与频率呈倒数关系,100Hz=1/100s,所以定时器的时间计算方法也是如此:
FOSC为晶振频率,例如12MHz、11.0592Mhz等
一个时间周期(1个频率周期的时间)为 1/FOSC 单位为us(1MHz=106Hz,1s=106us)
一个机器周期是12/FOSC(12T)或1/FOSC(1T),多少分频就是乘多少,89C51默认是12T,
所以如果晶振频率为12MHz时,一个机器周期是时间为12/12=1us。
一个机器周期就是单片机执行1条指令所用的时间,比如说i++、i–用时就是一个机器周期
例如:FOSC=12MHz,在16位定时器下我们需要定时1ms:
1ms=1000us
我们知道当计数值为65535(0xFFFF)时再来一位计数就会溢出进而产生中断标志,所以也就是计数为65536时会产生中断,所以我们从65536-1000=64536(0xFC18)开始计数,也就是高位(TH0)为FC,地位(TL0)为18。
6.6 实例
LED闪烁
#include<reg52.h>
unsigned char count;
void Timer0_Init(void) //50毫秒@12.000MHz
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0xB0; //设置定时初始值
TH0 = 0x3C; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1; //使能定时器0中断
EA=1;
}
void Timer0_Routine() interrupt 1 // 中断服务程序
{
TH0=0x3C; //重新初始化
TL0=0xB0;
count++;
if(count==20)//1s
{
count=0;
P2=~P2;
}
}
void main()
{
Timer0_Init();
P2=0;
while(1)
{
}
}
独立按键控制LED流水灯模式
#include<REG52.h>
#include<INTRINS.h>
unsigned char KeyNum,LEDMode;
sbit P3_1=P3^1;
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char data i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void Timer0_Init(void) //1毫秒@12.000MHz
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1;
EA = 1;
}
void main()
{
P2=0xFE;
Timer0_Init(); //定时器0初始化
while(1)
{
if(P3_1==0)
{
Delay(20);
if(P3_1==0)
{
while(P3_1==0);
Delay(20);
LEDMode++;
if(LEDMode>=2) LEDMode=0;
}
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //重新赋值1ms
TH0 = 0xFC;
T0Count++;
if (T0Count>=500) //0.5s
{
T0Count=0;
if(LEDMode==0)
P2=_crol_(P2,1); //向左移位,并且循环
if(LEDMode==1)
P2=_cror_(P2,1); //向右移位,并且循环
}
}
七、串口通信
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大的扩展了单片机的应用范围,增强了单片机系统的硬件实力。
7.1 硬件电路
TXD与RXD交叉连接;当电平标准不一致时,需要加电平转换芯片。
7.2 原理
当写入SBUF或者读取SBUF(发送数据或者接收到数据)时,RI或TI中断请求标志位置1,由此可进入到串口的中断系统。
STC89C52的UART有四种工作模式:
模式0:同步移位寄存器
模式1:8位UART,波特率可变(常用)
模式2:9位UART,波特率固定
模式3:9位UART,波特率可变
RB8/TB8一般用于奇偶校验,也可自定义;数据位8位即SBUF数据缓冲位。
波特率由计数器产生,所以在使用串口时,需要屏蔽计时器中断。
备注:串口通信只能用定时器1,并且定时器1只能使用8位自动重装工作模式
7.3 串行口控制寄存器
SCON
SM2:允许方式2或方式3多机通信控制位;
REN:允许/禁止穿行接收控制位。
由软件置位REN,REN=1为允许串行接收状态,可启动串行接收器RxD,开始接收信息;
软件复位REN,即REN=0,则禁止接收;
TB8:在方式2或方式3,它为要发送的第9位数据,按需要由软件置位或清0;
RB8:在方式2或方式3,是接收到的第9位数据;
TI: 发送中断请求标志位。在方式0,当串行发送数据第8位结束时,由内部硬件自动置位,即TI=1,向主机请求中断,响应中断后必须用软件复位,即TI=0。在其他方式中,则在停止位开始发送时由内部硬件置位,必须用软件复位;
RI:接收中断请求标志位。在方式0,当串行接收到第8位结束时由内部硬件自动置位RI=1,向主机请求中断,响应中断后必须用软件复位,即RI=0。在其他方式中,串行接收到停止位的中间时刻由内部硬件置位,即RI=1(例外情况见SM2说明),必须由软件复位,即RI=0。
PCON
7.4 波特率的计算方法
上两个图中已经有了详细的计算公式,下面只用其中的一个公式作为例子讲解:
溢出率:
定时器T1溢出一次的时间:
而溢出率为溢出一次时间的倒数,所以溢出率为:
以方式1为例,公式为:
SMOD即波特率是否加倍,加倍SMOD=1,不加倍SMOD=0
对于12MHz晶振来说,计算波特率是有一定误差的,这种情况就可以采用波特率加倍的方式降低误差
例如:12MHz晶振选择4800波特率时,计算出来的TH1为249.49,由于TH1只能是整数,所以只能选249或250,如果选择TH1=249,则波特率约为4464,误差率为7%;如果选择TH1=250,波特率为5208,误差率为8.5%;但如果选择波特率加倍(SMOD=1),TH1=242.979,取243误差更小,波特率为4807,误差率为0.145%
7.5 电脑通过串口控制 LED 点亮
#include<REG52.h>
void UartInit(void) //4800bps@12.000MHz
{
PCON |= 0x80; //波特率加倍
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF3; //设置定时初始值
TH1 = 0xF3; //设置定时重载值
ET1 = 0; //禁止定时器中断
TR1 = 1; //定时器1开始计时
EA=1; //中断打开
ES=1;
}
void UART_SendByte(unsigned char Date)//发送数据
{
SBUF=Date;
while(TI==0);
TI=0;
}
//如果要通过中断的方式接收数据,一定要记得打开中断(EA、ES)
//STC-ISP的串口波特率计时器默认是没有打开中断的
void UART_Routine() interrupt 4
{
if(RI==1) //如果接收标志位为1,接收到了数据
{
P2=~SBUF; //读取数据,取反后输出到LED
UART_SendByte(SBUF); //将受到的数据发回串口
RI=0; //接收标志位清0
}
}
void main()
{
unsigned char Sec=0;
UartInit();
while(1)
{
}
}
OK了,基础篇就讲到这吧,这篇也写了挺多的了,一些外设的使用(如点阵屏、蜂鸣器等)等在这篇文章就没有细讲了,我再重新写一篇外设篇吧
有一块8051,好像还只能用汇编,不会用~