@Mahdi
2018-01-03T23:25:31.000000Z
字数 8637
阅读 1808
单片机
知识点
STC98C52是C51的拓展版
通常所说的51单片机是指兼容Intel MCS-51体系架构的一系列单片机。
eg: Atmel的AT89C52
NXP(Philips)的P89V51
宏晶科技的STC89C52
闪存
空间(空间是8KB)内存
空间(空间是512B)单片机有很多很多功能,每个功能都会对应一个或多个SFR,通过对SFR的读写来实现单片机的多种多样的功能的
原理图
用最少的原件组成单片机可以工作的系统
5V的供电系统
40 脚+5V,通常称VCC或VDD
20 脚GND,代表电源负极
+5V与GND之间有个电容,是用来去耦高频电容直接用0.1uF就可以了,不需要再去计算和考量太多。
为单片机系统提供基准时钟信号
18 脚和 19 脚是晶振引脚,接了一个 12MHz 的晶振(它每秒钟振荡 12000000次)外加两个20pF的电容,帮助晶振起振,并维持振荡信号的稳定。
8051系列单片机一个机器周期由6个
S周期
(状态周期)组成。
1S周期
=2时钟周期
,所以1机器周期
=6S周期
=12时钟周期
。
指令周期:执行一条汇编指令所需要的时间,一般由若干个机器周期组成。
时钟周期=1/12um。 标准架构(8051)下一个机器周期是 12 个时钟周期
大概是1um执行一条汇编语句
一个nop();是一个时钟周期
接到了单 9 脚RST(Reset)复位引脚上。
单片机复位一般是 3 种情况:
上电复位、手动复位、程序自动复位
IO口对应地址与模块
IO有4组分别是P0,P1,P2,P3每组有八个共32个引脚,供我们定义使用。
P4组是C52在C51的基础上扩展的。
地址
P0 --->LED点阵
P2 --->LED模块
LED
普通的贴片发光二极管正向导通电压是1.8V~2.2V之间
工作电流一般在 1mA~20mA 之间
当电流在 1mA~5mA 之间变化时,随着通过 LED 的电流越来越大,肉眼会明显感觉到这个小灯越来越亮,而当电流从5mA~20mA 之间变化时,发光二极管的亮度变化不太明显
在regc52.h这个头文件里已经定义好了各个io口
sfr是C51特有的关键字作
用是定义一个单片机特殊功能寄存器
/* reg52.h中对寄存器的定义情况*/
sfr P0 = 0x80;
sfr P1 = 0x90;
sfr P2 = 0xA0;
sfr P3 = 0xB0;
sfr PSW = 0xD0;
...
可以看出PO只是一个标识符,真正起作用的是地址,这点与汇编语言吻合。地址(即是指针)指向了各个寄存器。
所以sfr就是为各个寄存器
定义标识符
的关键字。
sbit定义的是具体的引脚,其变量值只能是
0
或1
eg:
sbit led = P0^3;
是定义的PO口的第四个4引脚,led就对应了P^3。
sbit,就是对刚才所说的SFR里边的8个开关其中的一个进行定义
/*reg中P3口各个引脚的定义情况*/
sbit RD = ^7;
sbit WR = P3^6;P3
sbit T1 = P3^5;
sbit T0 = P3^4;
sbit INT1 = P3^3;
sbit INT0 = P3^2;
sbit TXD = P3^1;
sbit RXD = P3^0;
/*定义某个引脚的方法*/
sbit P0_2 = 0x82;//定义P0口的P0.2引脚
sbit P1_2 = 0x90^2;//定义P1口的P1.2引脚
sbit P2_2 = P2^2//定义P2口的P2.2引脚,但前提是将0xa0指向了P2。
/*以上三种方法均可定义其他组的任意某个引脚*/
对定义过的srf变量和sbit变量赋值即是对各个引脚的控制,通过0或1控制各个引脚输出低或高电平。
eg:
sbit P2_2 = 0;//对P2.2引脚赋值0使其输出低电平
sbit P0_2 = 1;//对P0.2引脚赋值1使其输出高电平
P0 = 0xf7; //对从0.7到0.0依次对这八个引脚赋值 1111 0111
P0 = 0; // 同上依次赋值为 0000 0000
P0 = 1; //同上依次赋值 1111 1111
P0 = 0x5 //同上依次赋值 0000 0101
Demo
#include<reg52.h>
#define uint unsigned int
void delay(void)
{
uint x, y;
for(x = 200; x > 0; x--)
for(y = 100; y > 0; y--);
}
void main()
{
while(1)
{
//P0 = 0xfe;
delay();
P2 = 0x5;//自动给P2 = 0000 0101
delay();
P2 = 0;//自动给P2 = 0000 0000
P2 = 1;//自动给P2 = 1111 1111
}
}
sbit led = 0xa7;//定义p2.7引脚
void main()
{
led = 0;//sbit 变量的值只能是 0 或 1
}
#include<reg52.h>
#define uint unsigned int
void main()
{
uint i, j;
bit flag;
flag = 1;
P2 = 0xff;
do
{
if(flag == 1)
{
P2 = 0xfe;
flag = 0;
}
else
{
P2 = 0xff;
flag = 1;
}
for(i = 0; i < 200; i++)
for(j = 200; j > 0; j--);
}while(1);
}
定时器
和计数器
是单片机内部的同一个模块,通过配置SFR(特殊功能寄存器)可以实现两种不同的功能,大多数情况下是使用定时器功能.定时器内部有一个寄存器,它开始计数后,这个寄存器的值每经过一个机器周期(也就是 12/11059200 秒)就会自动加1,机器周期理解为定时器的计数周期。还有一个特别注意的地方称之为溢出。
定时器有多种工作模式,分别使用不同的位宽,假如是 16 位的定时器,最大值就是 65535,那么加到 65535 后,再加 1 就算溢出,他位数的,道理是一样的,对于 51 单片机来说,溢出后,这个值会直接变成 0。从某一个初始值开始,经过确定的时间后溢出,这个过程就是定时的含义。标准的 51 单片机内部有 T0 和 T1 这两个定时器。单片机的每一个功能模块,都是由它的 SFR来控制。与定时器有关的特殊功能寄存器,有以下几个:
TH0 / TL0用于T0;
TH1 / TH1用于T1。
名称 | 描述 | SFR地址 | 复位值 |
---|---|---|---|
TH0 | 定时器0 高字节 | 0x8C | 0x00 |
TL0 | 定时器0 低字节 | 0x8A | 0x00 |
TH1 | 定时器1 高字节 | 0x8D | 0x00 |
TL1 | 定时器1 高字节 | 0x8B | 0x00 |
TCON--定时器控制寄存器的位分配(地址 0x88、可位寻址)
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | TF1 | TR1 | TF0 | TR0 | IE1 | IT1 | IE0 | IT0 |
复位值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
TCON——定时器控制寄存器的位描述
位 | 符号 | 描述 |
---|---|---|
7 | TF1 | 定时器1 溢出标志,溢出时硬件置1 。清零有两种方式:软件清0 ,或者进入定时器中断时硬件清0 。 |
6 | TR1 | 定时器1 运行控制位。软件置1 /清0 来进行启动 /停止 定时器。 |
5 | TF0 | 定时器0 溢出标志。溢出时硬件置 1。清零有两种方式:软件清0 ,或者进入定时器中断时硬件零0 。 |
4 | TR0 | 定时器0 运行控制位。软件置1 /清0 来进行启动 /停止 定时器。 |
3 | IE1 | 外部中断部分 |
2 | IT1 | 外部中断部分 |
1 | IE0 | 外部中断部分 |
0 | IT0 | 外部中断部分 |
- 硬件
置1
或清0
是指一旦符合条件,单片机将自动完成的动作,软件置1
或清0
的,是指必须自己用编写程序去完成这个动作定时器有多种工作模式,工作模式的选择就由 TMOD 来控制
TMOD——定时器模式寄存器的位分配(地址 0x89、不可位寻址)
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | GATE(T1) | C/T(T1) | M1(T1) | M0(T1) | GATE(T1) | C/T(T0) | M1(T0) | M0(T0) |
复位值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
TMOD——定时器模式寄存器的位描述
符号 | 描述 |
---|---|
T1/T0 | 标 T1 的表示控制定时器1 的位,标 T0 的表示控制定时器0 的位。 |
GATE | 该位被置1 时为门控位。仅当INTn 脚为高电平"1"并且"TRn“控制位也被置 1 时才使能定时器n。当该位被清0 时,只要TRn 位被置 1,定时器n 就使能开始计时,并不会受到单片机引脚INTn 外部信号的干扰,常用来测量外部信号脉冲宽度。这是定时器一个额外功能 |
C/T | 定时器或计数器选择位。该位被清0 时用作定时器功能,被置1 用作计数器功能 |
TMOD——定时器模式寄存器 M1/M0 工作模式
M1 | M0 | 工作模式 | 描述 |
---|---|---|---|
0 | 0 | 0 | 兼容 8048 单片机的 13 位定时器,THn 的 8 位和 TLn 的 5 位组成一个 13 位定时器。 |
0 | 1 | 1 | THn 和 TLn 组成一个 16 位的定时器 |
1 | 0 | 2 | 8 位自动重装模式,定时器溢出后 THn 重装到 TLn 中 |
1 | 1 | 3 | 禁用定时器 1,定时器 0 变成 2 个 8 位定时器 |
- TCON 最后标注了“可位寻址”, TMOD 标注的是“不可位寻址”。意思就是说:比如 TCON 有一个位叫 TR1,我们可以在程序中直接进行 TR1 = 1 这样的操作。但对 TMOD 里的位比如(T1)M1 = 1 这样的操作就是错误的。我们要操作就必须一次操作这整个字节,也就是必须一次性对 TMOD 所有位操作,不能对其中某一位单独进行操作,那么我们能不能只修改其中的一位而不影响其它位的值呢?当然可以
- 0 是为了兼容老的 8048 系列单片机而设计的,现在的 51 几乎不会用到这种模式,而模式 3 根据我的应用经验,它的功能用模式 2 完全可以取代,所以基本上也是不用的,那么我们就重点来学习模式 1 和模式 2。
定时器/计数器模式示意图
以定时器 0 为例。
1) TR0 和下边或门电路的结果要进行与运算,TR0 如果是 0 的话,与运算完了肯定是 0,所以如果要让定时器工作,那么 TR0 就必须置 1。
2) 这里的与门结果要想得到 1,那么前面的或门出来的结果必须也得是 1 才行。在 GATE位为 1 的情况下,经过一个非门变成 0,或门电路结果要想是 1 的话,那 INT0 即 P3.2 引脚必须是 1 的情况下,这个时候定时器才会工作,而 INT0 引脚是 0 的情况下,定时器不工作,这就是 GATE 位的作用。
3) 当 GATE 位为 0 的时候,经过一个非门会变成 1,那么不管 INT0 引脚是什么电平,经过或门电路后都肯定是 1,定时器就会工作。
4) 要想让定时器工作,就是自动加 1,从图上看有两种方式,第一种方式是那个开关打到上边的箭头,就是 C/T = 0 的时候,一个机器周期 TL 就会加 1 一次,当开关打到下边的箭头,即 C/T =1 的时候,T0 引脚即 P3.4 引脚来一个脉冲,TL 就加 1 一次,这也就是计数器功能。
使用定时器的时候,需要以下几个步骤:
第一步:设置特殊功能寄存器 TMOD,配置好工作模式。
第二步:设置计数寄存器 TH0 和 TL0 的初值。
第三步:设置 TCON,通过 TR0 置 1 来让定时器开始计数。
第四步:判断 TCON 寄存器的 TF0 位,监测定时器溢出情况。晶振是 11.0592M,时钟周期就是 1/11059200,机器周期是 12/11059200,假如要定时 20ms,就是 0.02 秒,要经过x 个机器周期得到 0.02 秒,我们来算一下 x*12/11059200=0.02,得到 x= 18432。16 位定时器的溢出值是 65536(因 65535 再加 1 才是溢出),于是我们就可以这样操作,先给 TH0 和 TL0一个初始值,让它们经过 18432 个机器周期后刚好达到 65536,也就是溢出,溢出后可以通过检测 TF0 的值得知,就刚好是 0.02 秒。那么初值 y = 65536 - 18432 = 47104,转成 16 进制就是 0xB800,也就是 TH0 = 0xB8,TL0 = 0x00。
/*定时器使LED亮一秒,熄灭一秒,以 0.5Hz 的频率进行闪烁*/
/**************************************/
#include <reg52.h>
sbit LED = P2^0;
void main(){
unsigned char cnt = 0; //定义一个计数变量,记录 T0 溢出次数
TMOD = 0x01; //设置 T0 为模式 1 init 初始化
TH0 = 0xB8; //为 T0 赋初值 0xB800
TL0 = 0x00;
TR0 = 1; //软件置1 启动 T0 定时器
while (1){
if (TF0 == 1){ //判断 T0 是否溢出
TF0 = 0; //T0 溢出后,软件清0溢出标志
TH0 = 0xB8; //并重新赋初值
TL0 = 0x00;
cnt++; //计数值自加 1
if (cnt >= 50){ //判断 T0 溢出是否达到 50 次
cnt = 0; //达到 50 次后计数值清零
LED = ~LED; //LED 取反:0-->1、1-->0
}
}
}
}
标准 51 单片机中控制中断的寄存器有两个,一个是
中断使能寄存器
,另一个是中断优先级寄存器
中断使能寄存器的位分配(地址 0xA8、可位寻址)
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号位 | EA | - | ET2 | ES | ET1 | EX1 | ET0 | EX0 |
复位值 | 0 | - | 0 | 0 | 0 | 0 | 0 | 0 |
中断使能寄存器的位描述
位 | 符号 | 描述 |
---|---|---|
7 | EA(Enable All Interrupt) | 中断总使能位,相当于总开关 |
6 | -- | -- |
5 | ET2 | 定时器2中断使能 |
4 | ES(Enable Serial Port) | 串口中断使能 |
3 | ET1 | 定时器1中断使能 |
2 | EX1(Enable External) | 外部中断1使能 |
1 | ET0 | 定时器0中断使能 |
0 | EX0 | 外部中断0使能 |
- 中断使能寄存器 IE 的位 0~5 控制了 6 个中断使能,而第 6 位没有用到,第 7 位是总开关。总开关就相当于我们家里或者学生宿舍里的那个电源总闸门,而 0~5 位这 6 个位相当于每个分开关。那么也就是说,我们只要用到中断,就要写
EA=1
,打开中断总开关,然后用到哪个分中断,再打开相对应的控制位就可以了- 中断服务函数,它的书写格式是固定的,首先中断函数前边 void表示函数返回空,即中断函数不返回任何值,函数名是 InterruptTimer0(),这个函数名在符合函数命名规则的前提下可以随便取,我们取这个名字是为了方便区分和记忆,而后是 interrupt这个关键字,一定不能错,这是中断特有的关键字,另外后边还有个数字 1
/*中断去‘鬼影’数码管动态显示*/
#include <reg52.h>
sbit A1 = P2^2;
sbit A2 = P2^3;
sbit A3 = P2^4;
unsigned char code LedChar[] = { //数码管显示字符转换表
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
};
unsigned char LedBuff[8] = { //数码管显示缓冲区,共阴极数码管 初值 0x00 确保启动时都不亮
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,0x00,0x00
};
unsigned char i = 0; //动态扫描的索引
unsigned int cnt = 0; //记录 T0 中断次数
void main(){
unsigned long sec = 0; //记录经过的秒数
EA = 1; //使能总中断
ET0 = 1; //使能 T0 中断
TMOD = 0x01; //设置 T0 为模式 1
TH0 = 0xFC; //为 T0 赋初值 0xFC67,定时 1ms
TL0 = 0x67;
TR0 = 1; //启动 T0
while (1){
if (cnt >= 1000){ //判断 T0 溢出是否达到 1000 次
cnt = 0; //达到 1000 次后计数值清零
sec++; //秒计数自加 1
//以下代码将 sec 按十进制位从低到高依次提取并转为数码管显示字符
LedBuff[0] = ~LedChar[sec%10]; //第1位数码管
LedBuff[1] = ~LedChar[sec/10%10];
LedBuff[2] = ~LedChar[sec/100%10];
LedBuff[3] = ~LedChar[sec/1000%10];
LedBuff[4] = ~LedChar[sec/10000%10];
LedBuff[5] = ~LedChar[sec/100000%10];
LedBuff[6] = ~LedChar[sec/1000000%10];
LedBuff[7] = ~LedChar[sec/10000000%10]; //第8位数码管
}
}
}
/* 定时器 0 中断服务函数 */
void InterruptTimer0() interrupt 1
{
TH0 = 0xFC; //重新加载初值 无需 TF0 = 0;软件清0 进入定时器中断时硬件零0。
TL0 = 0x67;
cnt++; //中断次数计数值加 1
//以下代码完成数码管动态扫描刷新
P0 = 0x00; //显示消隐
switch (i)
{
case 0: A3=0; A2=0; A1=0; i++; P0=LedBuff[0]; break;
case 1: A3=0; A2=0; A1=1; i++; P0=LedBuff[1]; break;
case 2: A3=0; A2=1; A1=0; i++; P0=LedBuff[2]; break;
case 3: A3=0; A2=1; A3=1; i++; P0=LedBuff[3]; break;
case 4: A3=1; A2=0; A1=0; i++; P0=LedBuff[4]; break;
case 5: A3=1; A2=0; A1=1; i=0; P0=LedBuff[5]; break;
case 6: A3=1; A2=1; A1=0; i=0; P0=LedBuff[6]; break;
case 7: A3=1; A2=1; A1=1; i=0; P0=LedBuff[7]; break;
default: break;
}
}
中断函数编号 | 中断名称 | 中断标志位 | 中断使能位 | 中断向量地址 | 默认优先级 |
---|---|---|---|---|---|
0 | 外部中断 | IE0 | EX0 | 0x0003 | 1(最高) |
1 | T0中断 | TF0 | ET0 | 0x000B | 2 |
2 | 外部中断1 | IE1 | EX1 | 0x0013 | 3 |
3 | T1中断 | TF1 | ET1 | 0x001B | 4 |
4 | UART中断 | TI/RI | ES | 0x0023 | 5 |
5 | T2中断 | TF2/EXF2 | ET2 | 0x002B | 6 |
- 第二行的 T0中断,如果要
使能
这个中断就要把它的中断使能位 ET0置1
,当它的中断标志位TF0
变为1时,就会触发T0中断了,来执行相应的中断函数了,单片机是靠中断向量地址找到中断函数的,interrupt 后面中断函数编号的数字x
就是根据中断向量得出的,它的计算方法是x*8+3=中断向量地址
。- 中断函数写好后,每当满足中断条件而触发中断后,系统就会自动来调用中断函数。比如我们上面这个程序,平时一直在主程序 while(1)的循环中执行,假如程序有 100 行,当执行到 50 行时,定时器溢出了,那么单片机就会立刻跑到中断函数中执行中断程序,中断程序执行完毕后再自动返回到刚才的第 50 行处继续执行下面的程序,这样就保证了动态显示间隔是固定的 1ms,不会因为程序执行时间不一致的原因导致数码管显示的抖动了。
中断优先级有两种,一种是抢占优先级,一种是固有优先级
中断优先级寄存器的位分配(地址 0xB8、可位寻址)
下面是抢占优先级
位 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
符号 | - | - | PT2 | PS | PT1 | PT0 | PX0 | |
复位值 | - | - | 0 | 0 | 0 | 0 | 0 | 0 |
中断优先级寄存器的位描述
位 | 符号 | 描述 |
---|---|---|
7 | - | 保留 |
6 | - | 保留 |
5 | PT2 | 定时器2中断优先级控制位 |
4 | PS(Priorit Serial) | 串口中断优先级控制权 |
3 | PT1(Priority Timer) | 定时器1中断优先级控制权 |
2 | PX1(Priority External) | 外部中断12中段优先级控制权 |
1 | PT0 | 定时器0中断优先级控制权 |
0 | PX0 | 外部中断0中断优先级控制权 |
IP 这个寄存器的每一位,表示对应中断的抢占优先级,每一位的复位值都是 0,当我们把某一位设置为 1 的时候,这一位的优先级就比其它位的优先级高了。比如我们设置了 PT0位为 1 后,当单片机在主循环或者任何其它中断程序中执行时,一旦定时器 T0 发生中断,作为更高的优先级,程序马上就会跑到 T0 的中断程序中来执行。反过来,当单片机正在 T0中断程序中执行时,如果有其它中断发生了,还是会继续执行 T0 中断程序,直到把 T0 中的中断程序执行完毕以后,才会去执行其它中断程序。
当进入低优先级中断中执行时,如又发生了高优先级的中断,则立刻进入高优先级中断执行,处理完高优先级级中断后,再返回处理低优先级中断,这个过程就叫做中断嵌套,也称为抢占。所以抢占优先级的概念就是,优先级高的中断可以打断优先级低的中断的执行,从而形成嵌套。当然反过来,优先级低的中断是不能打断优先级高的中断的。
那么既然有抢占优先级,自然就也有非抢占优先级了,也称为固有优先级。在表 6-3 中的最后一列给出的就是固有优先级,请注意,在中断优先级的编号中,一般都是数字越小优先级越高。从表中可以看到一共有 1~6 共 6 级的优先级,这里的优先级与抢占优先级的一个不同点就是,它不具有抢占的特性,也就是说即使在低优先级中断执行过程中又发生了高优先级的中断,那么这个高优先级的中断也只能等到低优先级中断执行完后才能得到响应。既然不能抢占,那么这个优先级有什么用呢?
答案是多个中断同时存在时的仲裁。比如说有多个中断同时发生了,当然实际上发生这种情况的概率很低,但另外一种情况就常见的多了,那就是出于某种原因我们暂时关闭了总中断,即 EA=0,执行完一段代码后又重新使能了总中断,即 EA=1,那么在这段时间里就很可能有多个中断都发生了,但因为总中断是关闭的,所以它们当时都得不到响应,而当总中断再次使能后,它们就会在同时请求响应了,很明显,这时也必需有个先后顺序才行,这就是非抢占优先级的作用 ,谁优先级最高先响应谁,然后按编号排队,依次得到响应。