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;//打开定时器
}

移位

在c语言中,尤其是单片机的C51中,移位运算操作很常见。实现移位操作的方法有两种:一是利用移位运算符,二是利用移位函数

区别:

1.移位运算符,是系统内置的运算操作,编译编译不用包含相关头文件;而移位函数编译要包含intrins.h头文件。

2.<<和>>的移位规则数据从一端移动到另外一端,数据尾部移走后会补0,数据头部移到最前端后会溢出,溢出的数据就被抹掉了。
_crol_等函数是是循环移位,首位相接,数据前端移动到尾部后,会从尾部再次进入队列,数据不会溢出。运算符是线性队列,循环移位函数是环形队列

1
2
3
4
unsigned char _crol_ (
unsigned char c, /* 要被进行 位左移 的形式参数 */
unsigned char b); /* 要进行的 位移数 */

基本语句

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:

自动重装载(初值),用于串口波特率

将TL1,TH1赋为相同的初值,当TL1溢出后自动将TH1的值赋值给TL1

image-20250117162729154

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没有,我们用定时器中断来实现,同时也会方便后面学习其它单片机的理解。

实验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

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

首先配置定时器,每100us进一次定时器中断,每次进入中断后计数器(Counter)+1,同时与比较值(Compare)比较,若Counter<Compare,置高电平,反之置低电平,在这里设置计数器最大到100后清0,pwm周期:100us*100=10ms ,若pwm频率过小则电机会出现抖动(频繁启动停止),故我们要让pwm频率取到一个相对大的值,这时就可等效的获得所需要的模拟参量

定时器的作用就在于生成周期为T,每个周期内高电平持续时间为Compare的波形

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
//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;
}
}
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);
}
}

串口通信(重要)

简介

image-20250116193708797

image-20250116224704361

image-20250116194452836

image-20250116194922886

image-20250116200105907 image-20250116200845182 image-20250116201908921

串口参数及时序图

image-20250116204330274

通常用的串口传输格式为:1bit起始位+8bit数据位+1bit停止位(无奇偶校验位)

image-20250116204353526

波特率:每秒钟传输二进制位数 eg:波特率为115200即1s传输二进制的位数115200个

比特率:每秒钟传送二进制有效数据的位数,表示有效数据的传输速率。

计算波特率和比特率

例:在异步串行传输系统中,字符格式为:1个起始位,8个数据位、1个校验位、2个终止位。若要求每秒传送120个字符,试求传送的波特率和比特率。
解答:
根据题目给出的字符格式,有效数据为8位,一帧包含1+8+1+2=12位

故波特率为:120*(1+8+1+2)=1440 bps=1440波特

又因为有效数据位为8位,而传送一个字符需1+8+1+2=12位

故比特率为:1440*(8/12)=960 bps

(比特率还可以直接求:8*120=960 bps)

串口内部收发原理

讲解视频:https://www.bilibili.com/video/BV1344y1M7pH/?spm_id_from=333.337.search-card.all.click&vd_source=a9d487fcf1a579639c6348eb5a9321db

image-20250116232539779 image-20250116212523392

寄存器

image-20250116212614576

image-20250116231436451

image-20250116231600175

image-20250117103834727

image-20250117103847263

注意是发送/接收第8位后才置1

image-20250117124935563

工作方式,波特率

image-20250121120807323

image-20250116232458199

T1溢出率:T1每秒溢出的次数,即1/(T1的定时时间),T1定时时间=(12/fosc)*(2^N-初值),12/fosc是计一次数的时间

定时/计数器T1是串口通信的波特率发生器,此时通常将T1设置为定时器且工作于工作方式2(自动重装初值的8位定时器)(自动重装载,省去了每次进中断赋值的时间,定时更加准确),并屏蔽其中断。T1溢出率是T1每秒钟溢出的次数,该次数与T1的初值有关。若假设T1的初值为M,则T1相邻两次溢出之间的时间间隔为(256-M)/(12/fosc),因此

img

式中,fosc为单片机的晶振频率。

9600 = (2^SMOD / 32) * 11059200 / [12 *(256 - TH1)]

9600 = (1 / 32) * 921600 / (256 - TH1)

9600 = 28800 / (256 - TH1)

TH1=253

将十进制数 253 转换为十六进制

  1. 253 ÷ 16 = 15,余数为 13(十六进制中 13 对应 ‘D’)。
  2. 15 ÷ 16 = 0,余数为 15(十六进制中 15 对应 ‘F’)。
  3. 将余数倒序排列,得到 FD

所以,十进制数 253 的十六进制表示为 0xFD

image-20250117162856863

image-20250117112621096

最高波特率就是要让定时计数器溢出的尽可能快,技术次数N=1,初值为255

最低波特率就是要让定时计数器溢出的尽可能慢,计数次数N=256,初值为0

image-20250117130909760

有误差是因为晶振是12MHZ,当晶振是11.0592MHZ时误差才为0,此时波特率越大误差越大

image-20250116233150040 image-20250116233336581

RI=1丢弃数据是因为此时接收SBUF已经满了但是还没有传递到总线上,如果此时继续写入数据则会破坏原来收好的一包。

image-20250116233444506

编程

1.初始化:

image-20250117102346667

image-20250117102159872

image-20250117104622207

N即定时器计数次数,最大计数-N即T1初值

image-20250117110400970

乙机可用始终查询或者中断的方式接收

image-20250117111013792

image-20250117111424043

实验:串口助手发给单片机,单片机再传回串口助手

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
#include "uart.h"
#include <REGX52.H>

/**
* @brief 晶振11.0592MHZ
* @param ival 定时器初值
* @retval 无
*/
void UART_Init(unsigned char ival)
{
//配置串口寄存器
SCON=0x50;
PCON=0x70;//波特率不加倍

//配置波特率(配置定时器1)
TMOD&=0x0F;
TMOD|=0x20;//0010 0000 方式2 8位自动重装定时/计数器 自动重装载无需软件赋初值(会占用时间),所以定时更加准确 用于串口波特率配置
TH1=ival;
TL1=ival;

ET1=0;//禁止定时器中断
ES=1;//打开串口接收中断
EA=1;//打开总中断

TR1=1;//打开定时器 TCON寄存器中的一位
}

//写一个字节的数据到SBUF中,写入后会由硬件自动完成发送,发送完成后硬件会把TI置1,需要软件清0
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte; //向缓存器中写入数据
while(!TI); //等待是否完成
TI=0; //复位
}

void UART_SendString(unsigned char *str) //参数是指针
{
while(*str !='\0' ) //字符串是以\0结尾的
{
UART_SendByte(*str);
str++; //地址加1.依次发送
}
}

void uart() interrupt 4
{
unsigned rec_data=0;
if(RI==1)
{
RI=0;//清零RI
//一个一个字节接收一个一个字节发送
rec_data=SBUF;//读取SBUF数据存到rec_data
UART_SendByte(rec_data);
}
}

单片机系统的并行扩展

51单片机最小系统

image-20250118111210550

image-20250118112145181

image-20250118113116918 image-20250118113156726

存储器扩展

image-20250118114104519 image-20250118114937463

image-20250118120734977

image-20250118120944559 image-20250118121225752 image-20250118121405652

程序存储器扩展

image-20250118121605765

简化画法:

image-20250118122226965

image-20250118122545598

image-20250118122635779 image-20250118122838242

数据存储器扩展

image-20250118123057700

2024期末题

1.外部中断

​ 如下图所示,在单片机P1口上接有8只LED,全灭。在外部中断1输入引脚(P3.3)接一只按钮开关K1,每按一次按钮开关K1,使外部中断1引脚接地,产生一个低电平触发的外部中断请求。在中断服务程序中,使低4位的LED保持灭,高4位的LED亮,如此交替闪烁10次。然后从中断程序返回,控制8只LED全亮。请补充完整下列程序的主函数main()和终端服务程序int1()。

image-20250121100953889

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
#include <reg51.h>
#define uchar unsigned char
#define K1 P3_3

void Delay(unsigned int i)
{
unsigned int j;
for(;i>0;i--)
for(j=0;j<333;j++)
{;}
}
void main()
{
EA=1;//开总中断
EX1=1;//开外部中断1
IT1=0;//低电平触发
while(1)
{
P1=0x00;
}
}

void int1() interrupt 2
{
uchar m;
EX1=0;//关闭外部中断1
for(m=0;m<10;m++)
{
P1=0x0f;
Delay(500);
P1=0xf0;
Delay(500);
}
EX1=1;//开启外部中断1
}

注意点:

1.LED是低电平点亮

2.外部中断1:中断号为2 低电平触发:IT1=0;

3.进入中断程序后EX1=0;是为了关闭外部中断1,此时再按按键没用,直到中断函数中执行完毕后,再令EX1=1;

4.若头文件为REGX52.H,可以用P3_3,在头文件已经定义了P3_3,若为reg52.h,头文件只定义了P3,操控第三位的话是P3^3, 可令sbit key=P3^3;

image-20250121102557365

image-20250121102614909

INT1的IT1决定外部中断1是下降沿触发还是低电平触发(外部中断1输入引脚接的是按键,当按键按下,引脚接地,低电平触发外部中断,进入中断服务程序),IE1是中断标志位(当中断源到来时由单片机自动置1),EX1,为外部中断1控制开关,EA为全局总中断控制开关,IP是用来设置中断优先级,默认是0,不用管。

2.串口双机通信

image-20250121114919014

题目分析:

1.编写甲机在定时/计数器T1工作在方式2下的串口方式1的发送程序,晶振位11.0592MHZ,波特率为9600bit/s

2.甲机只能发送,不能接收

image-20250114101345916

定时器工作方式2:自动重装载(初值),用于串口波特率

将TL1,TH1赋为相同的初值,当TL1溢出后自动将TH1的值赋值给TL1

image-20250121120821775

image-20250121115638729

串口工作方式1:8位的异步通信方式,通常用于双机通信

image-20250121121758633

波特率计算

image-20250121190558887

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
#include <REGX52.H>
unsigned char temp=0;
void main()
{
/*配置串口寄存器*/
//Serial Control 串行控制寄存器 可位寻址 SM0 SM1 SM2 REN TB8 RB8 TI RI
SCON=0x40;//工作方式1(SM0 0 SM1 1) 禁止串口接收(REN 0) 0100 0000 = 0X40
//Power Control 波特率选择寄存器 不可位寻址 SMOD SMOD0 - POF GF1 GF0 PD IDL
PCON=0x00;//波特率不加倍 (SMOD 0) 其余位也全为0 其实可以不用配置

/*配置波特率(配置定时器1)*/
//Timer Mode Register 定时器模式寄存器 GATE C/T M1 M0 GATE C/T M1 M0
TMOD&=0x0F;//清楚定时器1模式位 定时器0模式位不变
TMOD|=0x20;//0010 0000 方式2
TH1=0xfd;
TL1=0xfd;

//中断
ET1=0;//禁止定时器中断
ES=1;//打开串口接收中断
EA=1;//打开总中断

TR1=1;//打开定时器 TCON寄存器中的一位
while(1)
{
temp=P3;
SBUF=temp;
while(!TI);
TI=0;
Delay_ms(500);
}
}

3.写数据到片内片外RAM

​ 编写C51程序,记录函数(x+1)^2的值(x=0到19),将函数值存入片内RAM的30H为首地址的连续单元中,再将个位数不为6的函数值读入到片外RAM以1000H为首地址的连续单元中,并在屏幕上显示个位数不为6的函数值的个数。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "reg51.h"
xdata unsigned int buffer[20]_at_0x1000H;
data unsigned int buf[20]_at_0x30H;
void main()
{
unsigned char i,j;
for(i=0;i<20;i++)
{
buf=(i+1)*(i+1);
if(buf[i]%10!=6)
{
buffer[j++]=buf[i];
}
}
printf("j",%d);
}

PS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
xdata unsigned char databuf[256] _at_0x5000;
/*xdata
xdata 是 8051 单片机 中的一个存储类型修饰符,表示变量存储在 外部数据存储器(External Data Memory) 中。
8051 单片机的内存分为多个区域:
data:内部 RAM(128 字节)。
idata:间接寻址的内部 RAM(256 字节)。
xdata:外部扩展的 RAM(最大 64KB)。
使用 xdata 表示这个数组将存储在外部 RAM 中。*/
/*
_at_ 0x5000
_at_ 是一个编译器特定的关键字(例如 Keil C51 编译器),用于将变量或数组放置在 指定的内存地址。
0x5000 是内存地址的十六进制表示,表示数组 databuf 将从外部 RAM 的 0x5000 地址开始存储。
由于数组大小为 256 字节,因此 databuf 将占用从 0x5000 到 0x50FF 的内存空间。
*/

image-20250122112113101

4.动态数码管

image-20250122085339251

1
2
3
4
5
答案:
0x01
while(1);
i<3
(_crol_(j,1))&0x07 //0x07将j的值限制在0x01 0x02 0x04之间 因为题目只有三个数码管

在c语言中,尤其是单片机的C51中,移位运算操作很常见。实现移位操作的方法有两种:一是利用移位运算符,二是利用移位函数

区别:

1.移位运算符,是系统内置的运算操作,编译编译不用包含相关头文件;而移位函数编译要包含intrins.h头文件。

2.<<和>>的移位规则数据从一端移动到另外一端,数据尾部移走后会补0,数据头部移到最前端后会溢出,溢出的数据就被抹掉了。
_crol_等函数是是循环移位,首位相接,数据前端移动到尾部后,会从尾部再次进入队列,数据不会溢出。运算符是线性队列,循环移位函数是环形队列

1
2
3
4
unsigned char _crol_ (
unsigned char c, /* 要被进行 位左移 的形式参数 */
unsigned char b); /* 要进行的 位移数 */

此题我觉得有点问题,一方面是它段码表{0xf9,0xa4,0xb0}一般共阳极来说确实表示的是1,2,3,但是带到图中却表示的并不是1,2,3,不过这个不影响做题

第二方面是他说的左边第一个,左边第二个,左边第三个,是不是从左起的意思,这样的话文档里的答案是有问题的它是0x01,也就是从最右边第一个开始,依次向左。