51单片机学习笔记

KEIL使用

KEIL安装包及相关资源,视频教程见b站江科大视频

https://pan.baidu.com/s/1Fao9VfrM67TIFeIsusnSIw?pwd=7rx1#list/path=%2F

烧录代码(生成hex文件)

魔术棒->output->勾选create hex file(每次新建工程都要配置)

wps1

->编译,在object文件夹找到生成的hex文件(每次新建工程后要换成当前工程的文件夹,不然默认烧的是上个工程的hex文件)

->选择单片机型号为STC89C52RC 若选成STC89C52无法下载(用的普中的这个单片机丝印上写的有RC)

->单片机关机,点击下载程序,显示正在检测目标单片机后开机即可下载

wps2

当勾选左下角“当目标文件变化……”,我们在keil中编译后STC-ISP软件会自动发送下载指令,我们只需要对单片机断电,再次开机即可。

字体设置

​ 我们在使用Keil的时候编译器默认是使用ANSI进行编码的,在ANSI中对于英文是使用一个字节来表示,但是对于中文,在GB2312的编码中是利用两个字节来表示的,所以如果我们按一次backspace键在ANSI下只会删掉一个字节,所以出现乱码,因为中文还有一个字节没有删掉。

按此设置后输入中文不会出现乱码

image-20250110163500423

创建多文件

https://www.bilibili.com/video/BV1RB4y1i71i?spm_id_from=333.788.videopod.episodes&vd_source=a9d487fcf1a579639c6348eb5a9321db&p=157

keil主题配置

非常nice的一个主题配色

https://blog.csdn.net/wsstony579/article/details/53128206

模块化编程

image-20250111180622650 image-20250111180601816 image-20250111181343717

1.新建文件夹

image-20250111185458919

2.在文件夹内新建.c .h文件

image-20250111185543920

3.添加.c文件

image-20250111185623718image-20250111185646516

4.将.h文件路径包含进去,否则编译器找不到

魔术棒->c51->Include Paths 添加.h存放位置

image-20250111181210122

5.在.c文件中包含对应.h,在.h里写如下代码:

1
2
3
4
5
6
#ifndef __DELAY_H__
#define __DELAY_H__

void Delay_ms(unsigned int ms) ;

#endif

模板

image-20250112202028899

可以把一些固定的代码当作模板,后面需要用的时候直接双击即可,不用重复自己敲代码,提升效率。

C51语言基础

数据类型

image-20250110203426972

sfr(特殊功能寄存器)

8051单片机的特殊功能寄存器分布在内部数据存储区的地址单元80H~FFH中。sfr数据类型占用一个内存单元(一个字节)。利用它可以访问单片机内部的所有特殊功能寄存器。eg”sfr P1=0x90”定义了P1口在内部的寄存器中,在程序后续的语句中可以用”P1=0xff”语句,使P1口的所有引脚输出为高电平,来操作特殊功能寄存器

image-20250111124834310

1
2
sfr P2 = 0xA0;
//将符号P2与地址0xA0关联起来,P2被定义为SFR,编译器会将其视为一个8位寄存器。然后当你在程序中写P2 = 0xfe;时,编译器会将这个操作翻译为:将值0xfe写入地址为0xA0的寄存器。

为什么P2 = 0xfe;能直接赋值

  • 在C语言中,P2被定义为SFR,编译器会将其视为一个8位寄存器
  • 当你对P2赋值时,编译器会生成对应的机器指令,将值写入0xA0地址的寄存器
  • 硬件会根据写入的值,直接控制P2端口的状态。

赋值过程

当你执行P2 = 0xfe;时,实际发生了以下过程:

  1. 编译器处理:
    • 编译器知道P2对应地址0xA0,因此将P2 = 0xfe;翻译为:**将值0xfe写入地址0xA0**。
  2. 硬件执行:
    • 单片机的硬件会将值0xfe(二进制1111 1110)写入P2端口的输出寄存器。
    • P2端口的每个引脚(P2.0到P2.7)会根据这个值设置电平状态:
      • 0表示低电平。
      • 1表示高电平。
    • 因此,P2.0输出低电平,P2.1到P2.7输出高电平。

sbit(特殊功能位)

image-20250110203141038

image-20250111124847597

在8051系列单片机(如STC89C52)中:

  • SFR的地址是8位的,范围是0x800xFF
  • 每个SFR占用一个字节(8位),例如P2的地址是0xA0
  • 位地址是对SFR的每一位单独寻址的地址。8051单片机支持位寻址,因此每个SFR的每一位都有一个独立的位地址
  • SFR地址(如0xA0)是对整个8位寄存器的操作地址。
  • 位地址(如0xA00xA7)是对SFR中某一位的操作地址。

eg:

字节操作(字节地址0xA0

1
P2 = 0xfe;  // 将P2端口的8位设置为1111 1110
  • 操作的是字节地址0xA0,影响整个P2端口。

位操作(位地址0xA0

1
2
sbit P2_0 = P2^0;  // 定义P2.0的位地址
P2_0 = 0; // 将P2.0设置为0
  • 操作的是位地址0xA0,只影响P2.0这一位。
  • **字节地址0xA0**:用于操作整个P2端口的8位。
  • **位地址0xA0**:用于操作P2端口的第0位(P2.0)。
  • 虽然它们的地址值相同,但它们的用途和操作对象完全不同。

位寻址

可位寻址的寄存器可以对它的每一位单独赋值,不可位寻址的寄存器只能整体赋值

image-20250115193711698

image-20250115193733687

数据运算

image-20250110205733185

123÷10=12

123%10=3 %取余可以用来判断一个数是否可被另一个数整除。

0011 1100<<1 -> 0111 1000

0011 1100>>2 -> 0000 1111

0001 1000&0010 1010 -> 0000 1000

0001 1000|0010 1010 -> 0011 1010

0001 1000^0010 1010 -> 0011 0010 相同取0 不同取1

~0001 1000 -> 1110 0111

1
2
3
4
5
6
7
8
9
10
11
12
13
void TIM0_Init(void)
{
TMOD&=0xF0;//1111 0000 高4位为T1,保持不变,低4位为T0,清0
TMOD|=0x01;//0000 0001 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

TH0=0xFC;//给定时器赋初值,定时1ms
TL0=0x18;

ET0=1;//打开定时器0中断允许
EA=1;//打开总中断

TR0=1;//打开定时器
}

基本语句

image-20250110213906743

数组

image-20250111110451884

子函数

image-20250111110533229

预编译

image-20250111181442588
1
2
#include <REG52.H>//在安装目录搜索头文件
#include "delay.h"//在程序目录搜索头文件

代码

LED

image-20250110195012155

8个led对应P2寄存器的八个I/O口,置低电平则可使LED导通发光。

2-1 点亮第一个LED

image-20250110201354031

在REGX52.H中已经定义了P2寄存器,可以直接对其赋值

1
2
3
4
5
6
7
8
9
#include <REGX52.H>

void main()
{
P2=0xfe; //1111 1110 让P2.0为低电平 从而使LED1亮; 将值0xfe写入地址为0xA0的寄存器
while(1)
{
}
}

P2各位在头文件已经被定义,故也可单独对P2的某一位进行赋值操作:

image-20250110201939191

1
2
3
4
5
6
7
8
9
#include <REGX52.H>

void main()
{
P2_0=0;
while(1)
{
}
}

2-2 LED闪烁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <REGX52.H>
#include <INTRINS.H>
void Delay500ms() //@12.000MHz 延时500ms,该函数由STC-ISP软件生成
{
unsigned char i, j, k;

_nop_();//空语句,定义在INTRINS.H中,使用该函数时需包含INTRINS.H
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);

}

void main()
{
while(1)
{
P2=0xfe; //1111 1110
Delay500ms();
P2=0xff; //1111 1110
Delay500ms();
}
}

延时函数由STC-ISP软件生成

image-20250110163702305

2-3 LED流水灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <REGX52.H>
#include <INTRINS.H>
void Delay500ms() //@12.000MHz 延时500ms,该函数由STC-ISP软件生成
{
unsigned char i, j, k;

_nop_();//空语句,定义在INTRINS.H中,使用该函数时需包含INTRINS.H
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);

}

void main()
{
while(1)
{
P2=0xfe; //1111 1110
Delay500ms();
P2=0xfd; //1111 1101
Delay500ms();
P2=0xfb; //1111 1011
Delay500ms();
P2=0xf7; //1111 0111
Delay500ms();
P2=0xef; //1110 1111
Delay500ms();
P2=0xdf; //1101 1111
Delay500ms();
P2=0xbf; //1011 1111
Delay500ms();
P2=0x7f; //0111 1111
Delay500ms();
}
}

延迟函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Delay_ms(unsigned int ms)		//@12.000MHz 参数ms为需要延迟几毫秒
{
unsigned char i, j;
while(ms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
ms--;
}
}

独立按键

image-20250110200504735

单片机上电后默认为高电平,按下按键后,I/O口接地,变为低电平。检测I/O口高低电平状态即可知道按键是否被按下。

3-1独立按键控制LED亮灭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//实验现象:按下按键1,LED亮,松开后LED灭
#include <REGX52.H>

void main()
{
while(1)
{
if(P3_1==0)
{
P2_0=0;
}
else
{
P2_0=1;
}
}
}

3-2独立按键控制LED状态

软件消抖

image-20250110215436695
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//实验现象:每按一次LED状态变一次
#include <REGX52.H>
#include <INTRINS.H>

void Delay_ms(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
ms--;
}
}

void main()
{
while(1)
{
if(P3_1==0)//按键被按下
{
Delay_ms(20);//软件消抖 以免按键还没松开 但由于抖动 单片机误以为按键松开进行相应的操作
while(P3_1==0);//手松开前一直停在这里
Delay_ms(20);

P2_0=~P2_0;
}
}
}

3-2独立按键控制LED显示二进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <REGX52.H>
#include <INTRINS.H>

void Delay_ms(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
ms--;
}
}

void main()
{
unsigned char LEDNum=0;//char刚好8位 与寄存器位数相同
while(1)
{
if(P3_1==0)//按键被按下
{
Delay_ms(20);//软件消抖 以免按键还没松开 但由于抖动 单片机误以为按键松开进行相应的操作
while(P3_1==0);//手松开前一直停在这里
Delay_ms(20);

LEDNum++;
P2=~LEDNum;//LED是低电平亮,取反
}
}
}

3-3独立按键控制LED移位(有思维)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
//实验现象:按键1LED左移,按键2LED右移
#include <REGX52.H>
#include <INTRINS.H>

unsigned char LEDNum=0;//char刚好8位 与寄存器位数相同

void Delay_ms(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
ms--;
}
}

void main()
{
P2=~0x01;
while(1)
{
if(P3_0==0)//按键被按下
{
Delay_ms(20);//软件消抖 以免按键还没松开 但由于抖动 单片机误以为按键松开进行相应的操作
while(P3_0==0);//手松开前一直停在这里
Delay_ms(20);

LEDNum++;
if(LEDNum>=8)
LEDNum=0;
P2=~(0x01<<LEDNum);
}

if(P3_1==0)//按键被按下
{
Delay_ms(20);//软件消抖 以免按键还没松开 但由于抖动 单片机误以为按键松开进行相应的操作
while(P3_1==0);//手松开前一直停在这里
Delay_ms(20);

if(LEDNum==0)
LEDNum=7;
else
LEDNum--;

P2=~(0x01<<LEDNum);

}
}
}

数码管

数码管是由多个发光二极管封装在一起组成的“8”字型的器件。

image-20250111101223027 image-20250111101711029

对于四位一体数码管,eg:共阴,让第三个数码管亮其余灭,则位选1101,第三个数码管显示数字1,让7,4端口高电平,即给整个数码管01100000,若位选时为0000,则四个数码管都显示1,共阴极这种设计是四个数码管的A,B,C….分别在同一条线上,可以节省单片机I/O资源

image-20250111100427053

该单片机数码管为公阴极。74HC245是一个信号缓冲器,由于单片机引脚的驱动能力较弱,通过该缓冲器后,输出的电流更大(利用它自己接的VCC输出)

image-20250111103017979

LED1-8接到138译码器的输出端

image-20250111103147929

138译码器输入端为A,B,C(P22,P23,P24),输出端为Y0-Y7(LED1-8),由三个输入端控制8个输出端。 G1,G2A,G2B为使能端(此电路设计时已经接好,单片机上电就可以用该译码器)。

给CBA(C为高位)写二进制,转换成的十进制即要让输出端哪一位亮。eg:给CBA 101,101转换成十进制即5,即让Y5为0

总结:驱动方式:用138译码器选中哪个数码管亮,再用245缓冲器给段码数据使数码管显示对应数字

image-20250111161002712

4-1静态数码管显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <REGX52.H>

unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0X6D,0X7D,0X07,0x7F,0X6F};//数码管段码表,对应数字0-9
void NixieTube(unsigned char Location,Number)
{
//从右往左
switch(Location)
{
case 1:P2_4=0;P2_3=0;P2_2=0;break;
case 2:P2_4=0;P2_3=0;P2_2=1;break;
case 3:P2_4=0;P2_3=1;P2_2=0;break;
case 4:P2_4=0;P2_3=1;P2_2=1;break;
case 5:P2_4=1;P2_3=0;P2_2=0;break;
case 6:P2_4=1;P2_3=0;P2_2=1;break;
case 7:P2_4=1;P2_3=1;P2_2=0;break;
case 8:P2_4=1;P2_3=1;P2_2=1;break;
}
P0=NixieTable[Number];
}

void main()
{
NixieTube(7,3);//从右往左数第七个数码管显示数字3
while(1)
{
}
}

4-2动态数码管显示

利用人眼视觉暂留和数码管显示的余晖(先让第一个数码管显示1,第二关显示2,第三个显示3,不断地扫描,由于视觉暂留可以看到123)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <REGX52.H>
#include <INTRINS.H>

unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0X6D,0X7D,0X07,0x7F,0X6F};//数码管段码表,对应数字0-9

void Delay_ms(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
ms--;
}
}

void NixieTube(unsigned char Location,Number)
{
//从右往左
switch(Location)
{
case 1:P2_4=0;P2_3=0;P2_2=0;break;
case 2:P2_4=0;P2_3=0;P2_2=1;break;
case 3:P2_4=0;P2_3=1;P2_2=0;break;
case 4:P2_4=0;P2_3=1;P2_2=1;break;
case 5:P2_4=1;P2_3=0;P2_2=0;break;
case 6:P2_4=1;P2_3=0;P2_2=1;break;
case 7:P2_4=1;P2_3=1;P2_2=0;break;
case 8:P2_4=1;P2_3=1;P2_2=1;break;
}
P0=NixieTable[Number];
}

void main()
{
while(1)
{
NixieTube(1,5);
Delay_ms(1);
NixieTube(2,5);
Delay_ms(1);
NixieTube(3,3);
Delay_ms(1);
}
}

image-20250111165931402

LCD1602显示屏

普中、51单片机LCD引脚与数码管和D3,D4,D5冲突,与其它引脚不冲突

image-20250111202800133 image-20250111203012292
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);

LCD_Init();
LCD_ShowChar(1,1,'A');
LCD_ShowString(1,3,"Hello word");
LCD_ShowNum(1,9,123,3);
LCD_ShowSignedNum(1,13,-66,2);
LCD_ShowHexNum(2,1,0xA8,2);
LCD_ShowBinNum(2,4,0xAA,8);

5-1 LCD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <REGX52.H>
#include "delay.h"
#include "LCD1602.h"

int result=0;
void main()
{
LCD_Init();

LCD_ShowBinNum(2,4,0xAA,8);

while(1)
{
result++;
Delay_ms(1000);
LCD_ShowNum(1,1,result,3);
}
}

矩阵键盘

按键以矩阵的形式连接在I/O上。

image-20250111222000568

image-20250111225558421

检测方法

1.行列式扫描法(将矩阵按键拆分为独立按键) 2.线翻转法

行列式扫描法一行一行扫描,检测的次数不定,线翻转法只用检测两次,一次定行一次定列。

行列式扫描法(编程最简单最无脑,但相比线翻转法效率低)

原理:P17,P16,P15,P14为矩阵的4行,P13,P12,P11,P10为矩阵的4列,给行赋1011,则是单独看第二行,此时它们一端接地,只用检测P13,P12,P11,P10的电平状态即可知道该行有没有被按下的……以此类推,可以逐行/列扫描,由于此开发板引脚冲突,蜂鸣器会一直响,所以在此用逐列扫描。

普通按键是直接给它一端接地,矩阵键盘两端连的是两个I/O,行列式扫描法是通过令一个I/O为低电平达到和普通键盘一样的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
unsigned char MatrixKey()
{
unsigned char KeyNumber=0;

P1=0xFF;
P1_3=0;
if(P1_7==0){Delay_ms(20);while(P1_7==0);Delay_ms(20);KeyNumber=1;}
if(P1_6==0){Delay_ms(20);while(P1_6==0);Delay_ms(20);KeyNumber=5;}
if(P1_5==0){Delay_ms(20);while(P1_5==0);Delay_ms(20);KeyNumber=9;}
if(P1_4==0){Delay_ms(20);while(P1_4==0);Delay_ms(20);KeyNumber=13;}

P1=0xFF;
P1_2=0;
if(P1_7==0){Delay_ms(20);while(P1_7==0);Delay_ms(20);KeyNumber=2;}
if(P1_6==0){Delay_ms(20);while(P1_6==0);Delay_ms(20);KeyNumber=6;}
if(P1_5==0){Delay_ms(20);while(P1_5==0);Delay_ms(20);KeyNumber=10;}
if(P1_4==0){Delay_ms(20);while(P1_4==0);Delay_ms(20);KeyNumber=14;}

P1=0xFF;
P1_1=0;
if(P1_7==0){Delay_ms(20);while(P1_7==0);Delay_ms(20);KeyNumber=3;}
if(P1_6==0){Delay_ms(20);while(P1_6==0);Delay_ms(20);KeyNumber=7;}
if(P1_5==0){Delay_ms(20);while(P1_5==0);Delay_ms(20);KeyNumber=11;}
if(P1_4==0){Delay_ms(20);while(P1_4==0);Delay_ms(20);KeyNumber=15;}

P1=0xFF;
P1_0=0;
if(P1_7==0){Delay_ms(20);while(P1_7==0);Delay_ms(20);KeyNumber=4;}
if(P1_6==0){Delay_ms(20);while(P1_6==0);Delay_ms(20);KeyNumber=8;}
if(P1_5==0){Delay_ms(20);while(P1_5==0);Delay_ms(20);KeyNumber=12;}
if(P1_4==0){Delay_ms(20);while(P1_4==0);Delay_ms(20);KeyNumber=16;}

return KeyNumber;
}

线翻转法

先让四行为0,检测哪一列被按下(该列上任何一个按键被按下都会导致该列代表的I/O为低电平)

再让四列为0,检测哪一行被按下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
unsigned char MatrixKey_flip_scan(void)
{
unsigned char KeyNumber=0;
P1=0x0f;//00001111 先让四行为0
if(P1!=0x0f)//读取按键是否按下,若不为0x0f说明检测到某列上有按键被按下
{
Delay_ms(20);
if(P1!=0x0f)//再次检测键盘是否按下
{
//测试列
P1=0x0f;
switch(P1)
{
case(0X07): KeyNumber=1;break;//0x07 0000 0111
case(0X0b): KeyNumber=2;break;//0x0b 0000 1011
case(0X0d): KeyNumber=3;break;//0x0d 0000 1101
case(0X0e): KeyNumber=4;break;//0x0e 0000 1110
}
//测试行
P1=0Xf0;
switch(P1)
{
case(0X70): KeyNumber=KeyNumber;break;//0x70 0111 0000
case(0Xb0): KeyNumber=KeyNumber+4;break;//0xb0 1011 0000
case(0Xd0): KeyNumber=KeyNumber+8;break;//0xd0 1101 0000
case(0Xe0): KeyNumber=KeyNumber+12;break;//0xe0 1110 0000
}
while(P1!=0xf0);//有按键按下
}
}
else
KeyNumber=0;

return KeyNumber;
}

6-1 矩阵键盘读取并显示在LCD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <REGX52.H>
#include "delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"

unsigned char KeyNum=0;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"zzxnb666");
while(1)
{
// KeyNum=MatrixKey();
KeyNum=MatrixKey_flip_scan();
if(KeyNum)
{
LCD_ShowNum(2,1,KeyNum,2);
}
}
}

6-2 矩阵键盘密码锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <REGX52.H>
#include "delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"

unsigned char KeyNum,Count=0;
unsigned int PassWord=0;//int最大为65535 五位数 若给它赋值六位数及以上就会出错
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Password:");
while(1)
{
// KeyNum=MatrixKey();
KeyNum=MatrixKey_flip_scan();//获取键值
if(KeyNum)
{
if(KeyNum<=10)//如果S1-S10按键按下,输入密码
{
if(Count<4)//最多输入4位数
{
PassWord*=10;//密码左移一位
PassWord+=KeyNum%10;//1-9对10取余为1-9,10对10取余为0
}
Count++;
LCD_ShowNum(2,1,PassWord,4);
}
if(KeyNum==11)//若S11按下,确认
{
if(PassWord==2345)//正确密码
{
LCD_ShowString(1,14,"OK ");
PassWord=0;//密码清0
Count=0;//计次清0,可再次输入
LCD_ShowNum(2,1,PassWord,4);
}
else
{
LCD_ShowString(1,14,"ERR");
PassWord=0;
Count=0;
LCD_ShowNum(2,1,PassWord,4);
}
}
if(KeyNum==12)//取消
{
PassWord=0;
Count=0;
LCD_ShowNum(2,1,PassWord,4);
}
}
}
}

中断系统(重要)

中断概念

image-20250114214017637

image-20250113121039615

中断源:引起中断的源头

中断优先级:中断允许多个中断源(外部中断,串口中断,定时器中断……)存在,当多个中断源同时出现时,谁的中断优先级高就先相应谁,先执行高的再执行低的,然后再返回主程序。若两个中断优先级相同,通过查询次序来决定谁先(有一个固定的顺序)。

中断嵌套:当执行一个中断时,若此时出现了一个比它优先级更高的中断,则要转向执行高优先级的,然后再返回优先级低的那个继续执行,然后再返回主程序。对于51来说比较少,对于STM32,DSP等中断更复杂,则更容易出现中断嵌套。

中断的开启关闭,使用哪一个中断等等 都是有特殊功能寄存器来设置的

中断结构

8个中断请求源:INT0,INT1,INT2,INT3,TIM0,TIM1,TIM2,UART 加粗部分对于所有51内核的单片机都有

所有中断都有4个中断优先级

image-20250113135129899

INT0的IT0决定的是下降沿触发还是低电平触发,IE0是中断标志位(当中断源到来时由单片机自动置1),EA为全局总中断,IP是用来设置中断优先级

TCON(中断请求标志),IE(中断允许控制),IP都是寄存器

中断寄存器

TCON(中断请求标志)

image-20250113171843543

IE(中断允许控制)

image-20250113171807611

中断响应条件

image-20250113172317209

中断优先级

image-20250113171949116

中断号

image-20250113172031193

外部中断

51内核的单片机都有INT0,INT1;STC89C5X提供了4个外部中断,INT0,INT1,INT2,INT3

image-20250113174721897

INT0的IT0决定的是下降沿触发还是低电平触发,IE0是中断标志位(当中断源到来时由单片机自动置1),EA为全局总中断,IP是用来设置中断优先级

1
2
3
4
5
6
7
EA=1;//打开总中断开关
EX0=1;//开外部中断0
IT0=0/1;//设置外部中断触发方式
void int0() interrupt 0//0为中断号 interrupt为中断关键字 int0为中断函数名,可自己定义
{
//编写用户所需的功能代码,尽量不要写一些特别复杂,占用大量时间的代码,保证中断快进快出
}

对于STC89C52单片机,INT0,INT1对应P3.2,P3.3 这里我们使用按键模拟外部中断触发

image-20250113180023559image-20250113180037658image-20250113195037070

外部中断实验

通过独立按键K3,K4控制LED1,LED2亮灭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <REGX52.H>
#include "delay.h"

#define KEY3 P3_2 //与INT0相连
#define KEY4 P3_3 //与INT1相连
#define LED1 P2_0
#define LED2 P2_1

void exti0_init()
{
EA=1;
EX0=1;
IT0=1;//下降沿触发
}

void exti1_init()
{
EA=1;
EX1=1;
IT1=1;//下降沿触发
}

void main()
{
exti0_init();
exti1_init();

while(1)
{
}
}

void exti0() interrupt 0
{
Delay_ms(20);//先消抖
if(KEY3==0)
{
LED1=!LED1;
}
}

void exti1() interrupt 2
{
Delay_ms(20);//先消抖
if(KEY4==0)
{
LED2=!LED2;
}
}

对其模块化封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// Header: exti.c
// File Name: STC89C51外部中断模块
// Author: 张志雄
// Date: 2025/1/14
#include <REGX52.H>
#include "exti.h"

/**
* @brief exti0初始化 下降沿触发
* @param 无
* @retval 无
*/
void exti0_init()
{
EA=1;
EX0=1;
IT0=1;//下降沿触发
}


/**
* @brief exti1初始化 下降沿触发
* @param 无
* @retval 无
*/
void exti1_init()
{
EA=1;
EX1=1;
IT1=1;//下降沿触发
}

/**
* @brief exti0中断服务函数
* @param 无
* @retval 无
*/
void exti0() interrupt 0
{
//用户代码
}

/**
* @brief exti1中断服务函数
* @param 无
* @retval 无
*/
void exti1() interrupt 2
{
//用户代码
}
1
2
3
4
5
6
7
#ifndef __EXTI_H__
#define __EXTI_H__

void exti0_init();
void exti1_init();

#endif

定时器(重要)

image-20250114214141840

定时器作用:

1.可用于计时系统,实现软件计时,或者使程序每隔一固定时间完成一项操作

2.替代长时间delay,提高CPU的运行效率和处理速度

51内核的定时器都有T0,T1,对于STCC9852单片机,还有T3

51单片机有两组定时计数器,既可以定时又可以计数;

定时器计数器与单片机CPU相互独立,工作过程自动完成,不需要CPU参与;

定时计数器是根据机器内部的时钟(使用定时功能)或外部的脉冲信号(使用计数功能)来对寄存器进行加1;

CPU时序周期相关知识

image-20250113223443880

时钟周期(振荡周期):单片机控制信号的基本时间单位。若时钟晶体震荡频率为fosc,则时钟周期Tosc=1/fosc.

机器周期:CPU完成一个基本操作所需要的时间为机器周期。单片机通常把执行一条指令的过程分为几个机器周期,AT89S51单片机每12个时钟周期为一个机器周期。Tcy=12Tosc=12/fosc。eg:fosc=12MHZ,Tcy=12/12=1us.

指令周期:执行一条指令所需要的时间。

image-20250113224916285

image-20250113224941611

寄存器

详细的每一位介绍可以看参考手册。

image-20250115192122419

image-20250114093636787

image-20250115193933757

不可位寻址 只能对寄存器整体赋值

一般用方式1,方式2(串口波特率生成)。TMOD高四位控制T1,低四位控制T0

GATE:门控位 1:(只需TR0/TR1为1来决定定时计数器工作)0:(除了TR0/TR1还需INT0/INT1为1来决定定时计数器工作)

C/T: 1(计数模式)0(定时模式)

M1,M0:工作方式

image-20250114094733511

image-20250115193910592

可位寻址 可对寄存器中的每一位单独赋值

TF1:T1溢出标志位,溢出时自动置1,向CPU发出中断请求

TR1: T1定时计数器运行控制位 1:开始工作 0:停止工作

工作方式(原理)

方式0:

image-20250114095146635 image-20250114211051491

C/T 若为1 计数器模式 将开关打到1 ,若为0 定时器模式 将开关打到0

方式1(常用):

不会自动装载初值 每次溢出进入中断后需要我们手动装载

每来一个脉冲,16位(最大为65535)的计数器(TH,TL)里面的值就会自动加1,当计数达到最大值65535后,再+1就会溢出,TF0置1,向中断系统申请中断

image-20250114101303771 image-20250114215237257

方式2:

自动重装载(初值)

image-20250114101345916

方式3:

image-20250114101647745

定时器配置(重要)

其实就是根据工作方式的图把相应的寄存器配一下

image-20250114101849411

外部晶振12MHZ,则机器周期=1us,若想让定时器定时1ms

1ms/1us=1000次 初值=65536-1000=64536 将其转换为16进制为0xFC18,高八位写入TH,低八位写入TL

image-20250114113551152

当要计时的时间比较大,次数超过65536的话,如500ms,我们可以设置定时器1ms,然后在定时器中断里设置一个变量cnt,每次进入中断时加一,当cnt=500时即为500ms.

也可以这样算:

2^16=65536 2^8=256

高八位=65535/(2^8),低八位=65535%(2^8)) 类比十进制:1880 取高2位和低2位,高二位=1880/(10^2)=18,低二位=1880%(10^2)=80)

1
2
TH=65536/256;
TL=65536%256;

此外,在掌握了计算方法后,也可以使用定时器计算工具提高效率:

image-20250114114324053

STC-ISP

image-20250115205758206

1
2
3
4
5
6
7
8
9
10
11
12
13
void TIM0_Init(void)
{
TMOD&=0xF0;//1111 0000 高4位为T1,保持不变,低4位为T0,清0
TMOD|=0x01;//0000 0001 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

TH0=0xFC;//给定时器赋初值,定时1ms
TL0=0x18;

ET0=1;//打开定时器0中断允许
EA=1;//打开总中断

TR0=1;//打开定时器
}
image-20250114215302207

可位寻址的寄存器可以对它的每一位单独赋值,不可位寻址的寄存器只能整体赋值

image-20250115193711698

image-20250115193733687

定时器实验

1.通过定时器0中断控制D1指示灯间隔1s闪烁,定时器1中断控制D2指示灯间隔0.5s闪烁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <REGX52.H>
#include "delay.h"

#define LED1 P2_0
#define LED2 P2_1
typedef unsigned char u8;
typedef unsigned int u16;

void TIM0_Init(void)
{
TMOD&=0xF0;//1111 0000 高4位为T1,保持不变,低4位为T0,清0
TMOD|=0x01;//0000 0001 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

TH0=0xFC;//给定时器赋初值,定时1ms
TL0=0x18;

ET0=1;//打开定时器0中断允许
EA=1;//打开总中断

TR0=1;//打开定时器
}

void TIM1_Init(void)
{
TMOD&=0x0F;//0000 1111 高4位为T1,清0,低4位为T0,保持不变
TMOD|=0x10;//0001 0000 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

TH1=0xFC;//给定时器赋初值,定时1ms
TL1=0x18;

ET1=1;//打开定时器0中断允许
EA=1;//打开总中断

TR1=1;//打开定时器
}

void main()
{
TIM0_Init();
TIM1_Init();
while(1)
{
}
}

void TIM0() interrupt 1
{
static u16 cnt=0;//若不定义为static 下次重新调用后cnt又被初始化变成0了

TH0=0xFC;//TIM0不会自动重装载 当溢出进入中断后需要我们手动装载
TL0=0x18;

cnt++;
if(cnt==1000)//1s
{
LED1=!LED1;
cnt=0;
}

}

void TIM1() interrupt 3
{
static u16 cnt=0;//若不定义为static 下次重新调用后cnt又被初始化变成0了

TH1=0xFC;//TIM0不会自动重装载 当溢出进入中断后需要我们手动装载
TL1=0x18;

cnt++;
if(cnt==500)//0.5s
{
LED2=!LED2;
cnt=0;
}

}

对定时器文件进行模块化封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// Header: tim.c
// File Name: STC89C51单片机定时器模块
// Author: 张志雄
// Date: 2025/1/14
#include <REGX52.H>
#include "tim.h"

#define LED1 P2_0
#define LED2 P2_1
/**
* @brief TIM0初始化 工作方式1
* @param 无
* @retval 无
*/
void TIM0_Init(void)
{
TMOD&=0xF0;//1111 0000 高4位为T1,保持不变,低4位为T0,清0
TMOD|=0x01;//0000 0001 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

TH0=0xFC;//给定时器赋初值,定时1ms
TL0=0x18;

ET0=1;//打开定时器0中断允许
EA=1;//打开总中断

TR0=1;//打开定时器
}

/**
* @brief TIM1初始化 工作方式1
* @param 无
* @retval 无
*/
void TIM1_Init(void)
{
TMOD&=0x0F;//0000 1111 高4位为T1,清0,低4位为T0,保持不变
TMOD|=0x10;//0001 0000 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

TH1=0xFC;//给定时器赋初值,定时1ms
TL1=0x18;

ET1=1;//打开定时器0中断允许
EA=1;//打开总中断

TR1=1;//打开定时器
}

/**
* @brief TIM0中断服务函数
* @param 无
* @retval 无
*/
void TIM0() interrupt 1
{
//用户代码
//若为工作方式1 每次进入中断需要重装载初值
static unsigned int cnt=0;//若不定义为static 下次重新调用后cnt又被初始化变成0了

TH0=0xFC;//TIM0不会自动重装载 当溢出进入中断后需要我们手动装载
TL0=0x18;

cnt++;
if(cnt==1000)//1s
{
LED1=!LED1;
cnt=0;
}

}

/**
* @brief TIM1中断服务函数
* @param 无
* @retval 无
*/
void TIM1() interrupt 3
{
//用户代码
//若为工作方式1 每次进入中断需要重装载初值
static unsigned int cnt=0;//若不定义为static 下次重新调用后cnt又被初始化变成0了

TH1=0xFC;//方式1不会自动重装载 当溢出进入中断后需要我们手动装载
TL1=0x18;

cnt++;
if(cnt==500)//0.5s
{
LED2=!LED2;
cnt=0;
}

}
1
2
3
4
5
6
7
8
//tim.h
#ifndef __TIM_H__
#define __TIM_H__

void TIM0_Init(void);
void TIM1_Init(void);

#endif

2.定时器时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//main.c
#include <REGX52.H>
#include "delay.h"
#include "tim.h"
#include "key.h"
#include "LCD1602.h"

extern unsigned char Sec;
extern unsigned char Min;
extern unsigned char Hour;

void main()
{
TIM0_Init();
LCD_Init();

LCD_ShowString(1,1,"Clock:");
LCD_ShowString(2,1," : :");

while(1)
{
//LCD会占用相对较长时间 不适合放在中断函数中
LCD_ShowNum(2,1,Hour,2);
LCD_ShowNum(2,4,Min,2);
LCD_ShowNum(2,7,Sec,2);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//tim.c
#include <REGX52.H>
#include "tim.h"
#include "INTRINS.H"

#define LED1 P2_0
#define LED2 P2_1
unsigned char Sec=55;
unsigned char Min=59;
unsigned char Hour=23;

/**
* @brief TIM0初始化 工作方式1
* @param 无
* @retval 无
*/
void TIM0_Init(void)
{
TMOD&=0xF0;//1111 0000 高4位为T1,保持不变,低4位为T0,清0
TMOD|=0x01;//0000 0001 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

//给定时器赋初值,定时1ms
TH0=0xFC;//65536/256
TL0=0x18;//65536%256

ET0=1;//打开定时器0中断允许
EA=1;//打开总中断

TR0=1;//打开定时器
}

/**
* @brief TIM1初始化 工作方式1
* @param 无
* @retval 无
*/
void TIM1_Init(void)
{
TMOD&=0x0F;//0000 1111 高4位为T1,清0,低4位为T0,保持不变
TMOD|=0x10;//0001 0000 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

TH1=0xFC;//给定时器赋初值,定时1ms
TL1=0x18;

ET1=1;//打开定时器0中断允许
EA=1;//打开总中断

TR1=1;//打开定时器
}

/**
* @brief TIM0中断服务函数
* @param 无
* @retval 无
*/
void TIM0() interrupt 1
{
//用户代码
//若为工作方式1 每次进入中断需要重装载初值
static unsigned int cnt=0;//若不定义为static 下次重新调用后cnt又被初始化变成0了

TH0=0xFC;//TIM0不会自动重装载 当溢出进入中断后需要我们手动装载
TL0=0x18;

cnt++;
if(cnt==1000)//500ms
{
cnt=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
Hour++;
if(Hour>=24)
{
Hour=0;
}
}
}

}

}

/**
* @brief TIM1中断服务函数
* @param 无
* @retval 无
*/
void TIM1() interrupt 3
{
//用户代码
//若为工作方式1 每次进入中断需要重装载初值
static unsigned int cnt=0;//若不定义为static 下次重新调用后cnt又被初始化变成0了

TH1=0xFC;//方式1不会自动重装载 当溢出进入中断后需要我们手动装载
TL1=0x18;

cnt++;
if(cnt==500)//0.5s
{
LED2=!LED2;
cnt=0;
}

}

PWM

直流电机

image-20250115232126933image-20250115232431896

直流有刷电机主要由永磁体(定子),线圈(转子),换向器组成;直流无刷电机主要由永磁体(转子),绕组线圈(定子),少了碳刷和换向器的摩擦。

image-20250115234253196

PWM介绍

​ 电机调速不能和LED呼吸灯一样接一个滑动变阻器就完事,因为在驱动电机的过程中会产生很大电流,对于电机来说会转化为机械能没事,但对于滑动变阻器,电流会转化为热能使其损坏。

image-20250116095501121

最新的单片机TIM定时器都有输出PWM的功能,但STC89C52没有,我们用定时器中断来实现,同时也会方便后面的理解。

image-20250116104337285

该结构与最新单片机TIM定时器PWM硬件结构相似,在这里我们用软件来模拟这一结构

实验1:LED呼吸灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <REGX52.H>
#include <INTRINS.H>
#include "delay.h"

#define LED P2_0

void main()
{
unsigned char Time,i;
while(1)
{
for(Time=0;Time<100;Time++)
{
for(i=0;i<20;i++)
{
LED=0;
Delay_us(Time);
LED=1;
Delay_us(100-Time);
}
}
for(Time=100;Time>0;Time--)
{
for(i=0;i<20;i++)
{
LED=0;
Delay_us(Time);
LED=1;
Delay_us(100-Time);
}
}
}
}

实验2:直流电机调速

image-20250116113208512

image-20250116120658461

pwm周期:100us*100=10ms 每100us进一次定时器中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//main.c
#include <REGX52.H>
#include "delay.h"
#include "tim.h"
#include "key.h"
#include "nixietube.h"

unsigned char KeyNum=0,Speed=0;
extern unsigned char Compare;

void main()
{
P2=0xfe;
TIM0_Init();

while(1)
{
KeyNum=Key();
if(KeyNum==1)
{
Speed++;
if(Speed>=4)Speed=0;
switch (Speed)
{
case 0:
Compare=0;
break;
case 1:
Compare=60;
break;
case 2:
Compare=80;
break;
case 3:
Compare=100;
break;
default:
break;
}
}
NixieTube(1,Speed);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//tim.c
#include <REGX52.H>
#include "tim.h"
#include "INTRINS.H"

#define LED1 P2_0
#define LED2 P2_1
#define MOTOR P1_0
unsigned char LedMode=0;
unsigned char Counter,Compare=0;
/**
* @brief TIM0初始化 100us@12.000HZ
* @param 无
* @retval 无
*/
void TIM0_Init(void)
{
TMOD&=0xF0;//1111 0000 高4位为T1,保持不变,低4位为T0,清0
TMOD|=0x01;//0000 0001 选择为定时器0模式,工作方式1,使用或运算可以不干扰高四位

//给定时器赋初值,定时100us
TH0=0xFF;
TL0=0x9C;

ET0=1;//打开定时器0中断允许
EA=1;//打开总中断

TR0=1;//打开定时器
}

/**
* @brief TIM0中断服务函数
* @param 无
* @retval 无
*/
void TIM0() interrupt 1
{
//用户代码
//若为工作方式1 每次进入中断需要重装载初值
TH0=0xFF;
TL0=0x9C;
//Compare=50;

Counter++;//计数器
if(Counter>=100)Counter=0;//pwm周期:100us*100=10ms
if(Counter<Compare)
{
LED1=0;
MOTOR=1;
}
else
{
LED1=1;
MOTOR=0;
}

}

串口通信