一、基础
1.常用头文件
- reg51.h
- intrins.h:包含移位函数、_nop_()函数。_crol_(P1,1)把P1中的数据循环左移1位;_nop_()执行一个空指令。
2.端口选择
- P1=0xfe:0xfe=1111 1110b,向P1.0输出低电平,其他为高电平
- sfr x=0x80:通过特殊功能寄存器选择P0端口,P0口在存储器中的地址是0x80,对地址x的操作也就是对P0的操作,以后可直接给x赋值,如x=0xfe
- sbit s=P3^2:特殊功能位,^后面只能是0~7,可直接操作P3.2引脚
3.时钟频率与机器周期
所有的频率、周期均由外接晶振计算而出。时钟周期是晶振频率的倒数,一个机器周期包括12个时钟周期。常用的晶振频率对应的时钟周期以及机器周期如下:
晶振频率fosc | 时钟周期 Tosc | 机器周期Tcy |
---|---|---|
11.0592MHZ =11.0592×106HZ |
≈9.042245370370...×10-8s =9.042245370370...×10-5ms =9.042245370370...×10-2us =90.42245370370...ns |
1.085069444...×10-6s =1.085069444...×10-3ms =1.085069444...us =1085.069444...ns |
12MHZ =12×106HZ |
8.333...×10-8s =8.333...×10-5ms =8.333...×10-2us =83.33...ns |
10-6s =10-3ms =1us =1000ns |
然而实际上常用的晶振仍是11.0592MHZ的,因为这样算出的9600的波特率对应的定时器初值是个整数,在下面的UART中会详细说明。
4.延时函数的计算
延时函数与机器周期有关,完成一个基本操作(如取指令、读数据或写数据)所用的时间是一个机器周期,一条指令常被分为几个机器周期。
代码中常有这样的延时函数:
void delay(unsigned char i)
{
unsigned j;
for(;i>0;i--)
for(j=0;j<125;j++)
;
}
其中j的取值范围是与机器周期或者说晶振频率有密切关系的,这里是拿12MHZ的晶振为例。12MHZ晶振产生的一个机器周期为1us,即执行一个基本操作需要1us,但是125us×1=125us,i=1的时候延时125us?
不对,C语言转换为汇编指令时语句增多,这里一个循环大概变成了8个CPU基本操作(个人猜测,未实际验证),所以8×125us=1000us=1ms。
因此这里产生了一个问题,传进去的i在循环的时候也操作了8次,但是这个8是在外循环,假如CPU需要4个基本操作先执行外循环(比如给i赋初值等),然后进入内循环,然后再需要4个基本操作执行外循环(判断i是否大于0,i自减等)。那么假如i=1的话,CPU的基本操作应该有4+8+4=16,但是当i=2的时候,它执行第二次时就不需要再执行外循环的第一组指令了(给i赋初值等),所以这时的CPU基本操作应该有(4+8+4)+(8+4),那么当i很大的时候,延时就变得很不准了。所以以上关于一个循环是8个CPU基本操作的猜想可能是错的,具体是多少需要查看汇编指令。
但实际上延时函数并不需要很精确,因此11.0592MHZ的频率也可以使用此函数,只需记住循环125次大概是1ms就可以了。如果需要十分精确的延时,可以使用_nop_()函数,此函数执行一个空指令,消耗一个机器周期。
二、中断
1.基础
- EA=1:开放总中断
- EX0=1:允许(Enable)使用外中断0。EX1——外中断1,ET0——定时器T0,ET1——定时器T1,ES——串口中断
- IT0=1:选择外中断0为负跳变触发(跳沿触发);=0电平触发
- 优先级控制。1为高优先级,0为低优先级。PX0控制外部中断0;PX1控制外部中断1;PT0控制定时器中断T0;PT1控制定时器中断T1;PS控制串口中断。
2.中断服务函数
//函数功能:外中断T0的中断服务程序
void int0(void) interrupt 0 using 0 //外中断0的中断编号为0
{
P1 =~ P1; //每产生一次中断请求,P1取反一次。
}
interrupt 0 指明是外部中断0;
interrupt 1 指明是定时器中断0;
interrupt 2 指明是外部中断1;
interrupt 3 指明是定时器中断1;
interrupt 4 指明是串行口中断;
using 0 是第0组寄存器;
using 1 是第1组寄存器;
using 2 是第2组寄存器;
using 3 是第3组寄存器;
51单片机内的寄存器是R0--R7(不是R0-R3)。通用寄存器区由四个寄存器组构成,分别是0组、1组、2组、3组。每个寄存器组含有8个通用寄存器:R0-R7;四个组共有32个通用寄存器。
R0-R7在数据存储器里的实际地址是由特殊功能寄存器PSW里的RS1、RS0位决定的。
using 0时设置 RS1=0,RS0 =0,用第0组寄存器,R0--R7的在数据存储区里的实际地址是00H-07H。R0(00H)....R7(07H);
using 1时设置 RS1=0,RS0 =1,用第1组寄存器,R0--R7的在数据存储区里的实际地址是08H-0FH。R0(08H)....R7(0FH);
using 2时设置 RS1=1,RS0 =0,用第2组寄存器,R0--R7的在数据存储区里的实际地址是10H-17H。R0(10H)....R7(17H);
using 3时设置 RS1=1,RS0 =1,用第3组寄存器,R0--R7的在数据存储区里的实际地址是18H-1FH。R0(18H)....R7(1FH)。
3.定时器/计数器中断
- 工作方式选择:通过工作方式寄存器TMOD(8位)里面的两位M1(D5、D1,D5控制T1,D1控制T0)、M0(D4、D0,D4控制T1,D0控制T0)选择,共有四种工作方式。
方式0为13位计数器,方式1为16位计数器。
方式2实际上相当于8位计数器,可自动恢复初值。 - 使用方式2,即TMOD = 0x10(使用T0定时器时)或0x20(使用T1定时器)
- 启动定时器:通过控制寄存器TCON(8位)里面的TR0/TR1=1
定时器时间计算
定时器在溢出的时候才会产生中断,什么时候溢出?
寄存器的值存满了就溢出了,寄存器什么时候存满?
- 如果采用方式1,也就是16位计数器,并且不能恢复初值。那么到216=65536(1后面跟16个0,共17位)就存满溢出了。
所以如果需要指定一段时间产生中断,就要给定时器设置初值,然后定时器到达65536产生中断,进入中断函数,之后从0开始重新计数。
所以假如定时器的初值为X,定时的时间为t,那么:
t = Tcy × ( 65536 - X ) |
---|
所以得到:
X = 65536 - ( t / Tcy ) |
---|
或者直接代入晶振频率,得:
X = 65536 - ( 12 × t × fosc ) |
---|
需要注意的是单位要统一,如果 fosc的单位是HZ,那么t的单位就是s,如果 fosc的单位是MHZ,那么t的单位就是us。
这样算出来的X值可能是16位的,但是一个寄存器只能存八位,所以要把它放入两个特殊功能寄存器,将高8位放入TH0或TH1(通常用X/256),低八位放入TL0或TL1(通常用X%256)。
如果需要的时长远大于65536个时钟周期,那么就在中断函数中设置个count记录中断次数,中断次数达到一定值再进行其他操作。
由于每次中断后会从0开始计数,因此需要在中断函数中重新为其赋初值,但是在中断函数里面赋初值这条语句也会花费一段时间,由此会导致计时不够精确。
- 设为方式2可以自动恢复初值,解决计时不够精确问题。而代价是X只能是8位。溢出检测的算法是每过一个机器周期,X加1,当X加到最大值时便溢出到0,这样就丢失了初值数据。
而方式2需要把TH0/TH1与TL0/TL1设为相同的值,TLX在每个机器周期后会加1,加溢出后变为0,检测到TLX溢出就把备份的初值THX自动重新赋给TLX。所以初值就只能是8位了。
与方式1的计算方法相同,方式2只要把65536改为256即可:
X = 256 - ( 12 × t × fosc ) |
---|
示例程序
#include<reg51.h>
sbit led1=P0^0; //将D1位定义为P2.0引脚
sbit led2=P0^1;
unsigned char Countor; //设置全局变量,储存定时器T1中断次数
unsigned char countor1;
/**************************************************************
函数功能:主函数,定时器方式1
**************************************************************/
void main(void)
{
EA=1; //开总中断
ET1=1; //定时器T1中断允许
TMOD=0x10; //使用定时器T1的模式1
TH1=15536/256; //定时器T1的高8位赋初值
TL1=15536%256; //定时器T1的低8位赋初值
TR1=1; //启动定时器T1
Countor=0; //从0开始累计中断次数
countor1++;
while(1) //无限循环等待中断
;
}
/**************************************************************
函数功能:定时器T1的中断服务程序
**************************************************************/
void Time1(void) interrupt 3 using 0
{
Countor++; //中断次数自加1
if(Countor==4) //若累计满20次,即计时满1s
{
led1=~led1; //按位取反操作,将P2.0引脚输出电平取反
Countor=0; //将Countor清0,重新从0开始计数
}
if(countor1==16)
{
led2=!led2;
countor1=0;
}
TH1=15536/256; //定时器T1的高8位重新赋初值
TL1=15536%256; //定时器T1的低8位重新赋初值
}
三、串行口(UART)
1.基础
- 先设置定时器,然后设置串口工作方式以及波特率
- 串行口控制寄存器SCON(8位)的高两位SM0(D7)和SM1(D6)控制串口的四种工作方式,通常用方式1(SM0=0,SM1=1)8位异步收发(一次可发一个字节,也就是可发一个字符char),波特率可变(由定时器控制)
- 将要发送的数据放入SBUF缓存中(unsigned char )
- SCON中的TI为发送中断标志位,TI=1表示1帧数据发送结束,在程序中每次发送数据前都需要进行判断
- SCON = 0x40,方式1,发送
- SCON = 0x50,方式1,接收(SM2(D5) 用于多机通信)
- 特殊功能寄存器PCON中的SMOD(D7),为波特率倍增位,SMOD=1时比SMOD=0时波特率加倍
2.波特率的计算
波特率就是发送的数据的速率,单位是bit/s,最常用的波特率为9600bit/s,也就是一秒钟发送9600个二进制位,9600bit = 1200Byte = 1.2KByte。在proteus仿真器中的终端默认波特率就是9600,当然这个是可以改的。
不同的波特率对数据的收发肯定会造成影响,比如现在有一个周期为一秒的方波,如果我跟你说,周期是一秒,那么你解析出来的就是数据0,1。如果我跟你说,周期是0.1秒,那么你解析出来的就是1,1,1,1,1,0,0,0,0,0。所以数据收发双方必须要使用相同的波特率才能保证数据传输的一致性。
如果使用方式1或方式3,通过定时器T1(通常使用T1作为波特率发生器)和波特率倍增位SMOD就可以确定波特率。
波特率 = 2 SMOD / 32 × 定时器T1的溢出率 |
---|
可以这么理解,本来应该定时器溢出一次产生一个中断就向串口发送一个二进制,但是这个速度太快了,所以单片机内部有个分频器把它速度降低了,这个除的32是内置的,无法修改。所以现在变成了定时器每溢出32次才向串口发送一个二进制位。
那么假如SMOD=0,我想让他1s发送9600个二进制位,定时器1s要溢出几次?
9600 × 32 = 307200次 |
---|
也就是T1的溢出率为307200次/s。那溢出率怎么用晶振的频率表示,怎么求T1的初值?
每秒溢出307200次,那么溢出一次需要多少秒?倒数,溢出一次需要1/307200s,这个就是在定时器中断中的t值,也就是要定时的时间,把定时器中断中的公式拿来,但是需要注意,串口通信需要的定时精确度较高,因此要使用定时器方式2(8位计数,可恢复初值):
t = Tcy × ( 256 - X ) |
---|
套进去:
1 / 溢出率 = Tcy × ( 256 - X ) |
---|
假如晶振是11.0592MHZ,算算波特率为9600时X是多少:
Tcy = 12 / 11.05926 |
---|
1 / 307200 = 12 / 11.05926 × ( 256 - X ) |
X = 253 = 0xfd |
OMG!X恰好是个整数!那么晶振是12MHZ时呢?除不尽!所以这就是常用晶振为11.0592MHZ的原因。
3.示例代码
- 发送端代码:
#include<reg51.h>
unsigned char code Tab[ ]={0xFE,0xFD,0xFB,0xF7,0xEF,0xDF,0xBF,0x7F};
//流水灯控制码,该数组被定义为全局变量
/*****************************************************
函数功能:发送一个字节数据
***************************************************/
void Send(unsigned char dat)
{
SBUF=dat; //缓存放满,TI为1
while(TI==0)
;
TI=0;
}
/**************************************************************
函数功能:延时
**************************************************************/
void delay(void)
{
unsigned char m,n;
for(m=0;m<200;m++)
for(n=0;n<125;n++)
;
}
/*****************************************************
函数功能:主函数
***************************************************/
void main(void)
{
unsigned char i;
TMOD=0x20; //TMOD=0010 0000B,定时器T1工作于方式2
SCON=0x40; //SCON=0100 0000B,串口工作方式1
PCON=0x00; //PCON=0000 0000B,SMOD=0,波特率9600
TH1=0xfd; //备份值
TL1=0xfd; //波特率9600
TR1=1; //启动定时器T1
while(1)
{
for(i=0;i<8;i++)
{
Send(Tab[i]); //发送数据i
delay();
}
}
}
- 接收端代码:
#include<reg51.h>
#define uchar unsigned char
#define unit unsigned int
void main(){
uchar temp=0;
TMOD=0x20;
TH1=0xfd;
TL1=0xfd;
SCON=0x50;
PCON=0x00;
TR1=1;
while(1){
while(RI==0);
RI=0;
temp=SBUF;
P1=temp;
}
}