单片机原理与应用(基础篇)
本文最后更新于168 天前,其中的信息可能已经过时,如有错误请发送邮件到1739584917@qq.com

前言

这篇文章仅为个人学习单片机所总结的笔记,可能更多的是单片机的应用部分,而单片机的原理部分大多都一笔带过。如果文章有错误的地方,多多担待。

注:本文所使用到单片机型号为普中的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了,基础篇就讲到这吧,这篇也写了挺多的了,一些外设的使用(如点阵屏、蜂鸣器等)等在这篇文章就没有细讲了,我再重新写一篇外设篇吧

如果觉得本文对您有所帮助,可以支持下博主,一分也是缘

评论

  1. Windows Chrome
    5月前
    2023-12-30 12:12:42

    有一块8051,好像还只能用汇编,不会用~

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
/* */