51单片机原理与应用

51单片机原理与应用

Wreckloud_雲之残骸 Lv3

51单片机介绍

准备工作

首先需要安装几个软件

开始使用

安装 Keil 集成开发环境

Keil 是用来写 C 语言程序的, 它还可以将程序编译成单片机可以识别的指令集.

alt text

STC-ISP 安装

STC-ISP 可以将编译好的程序下载到单片机中, 以让程序能在单片机运行.

USB驱动

安装了串口驱动后 STC-ISP 才能识别单片机.

由于我已经安装的时候没有截图写教程, 所以先就不写详细的安装步骤了(

51 单片机简介

单片机(MicrocontrollerUnit, MCU)算一种袖珍版计算机, 一个芯片就能构成完整的计算机系统.
单片机成本低, 体积小, 结构简单. 在学习使用单片机的过程中, 可以快速了解计算机原理与其结构.

Intel 公司推出了 8051 单片机内核, 因此这种类似于 8051 内核的单片机, 我们都统称为 51 单片机.

51 单片机的
位数: 8位.
RAM: 512字节.
ROM: 8K (Flash).
工作频率: 12Mhz.

RAM: 随机存取存储器, 用于存储程序和数据(会丢失数据, 相当于工作台).
ROM: 只读存储器, 用于存储程序(长期存储, 相当于仓库).

单片机内部结构图

alt text

乱七八糟的我也不爱看

alt text

LED 灯控制

新建一个 Keil 工程

为了方便导入和管理代码:

首先, 在电脑中新建一个文件夹, 作为所有 Keil 工程的存放位置.
然后, 打开 Keil 软件, 新建第一个工程文件夹.

如果不这么做, 文件就会散成一团(

接着, 如图所示.

alt text

然后在跳出的对话框中选中 Atmel 下的 AT89C52.

alt text

会有一个对话框询问是否自动创建开始文件.

alt text

推荐选 ,选了是影响也不大.

接着, 我们要在项目中添加一个 .c 的文件, 作为程序的入口.
由于这个版本的 keil 不能直接新建, 所以我们得先在 系统的文件管理器 中新建文件.

在文件管理器中找到项目文件夹, 右键新建一个文件, 命名为 main.c.

然后在 keil 中将这个 main.c 文件添加到工程中.

alt text

这样, 工程就新建完成了!

alt text

接下来就可以开始编写代码了.

为了能在程序编译好后能直接得到 .hex 文件, 我们还得在 Keil 中设置一下编译选项.

alt text

Output 选项卡中, 勾选 Create HEX file.

LED 灯控制

alt text

LED 灯是指发光二极管, 它只允许电流从一个方向通过.

当我们把电池的正极接到P型材料这一端, 负极接到N型材料这一端时, 电流就可以顺利通过LED.
因为电子和空穴可以分别从N型和P型材料流向对方, 然后在中间的PN结区域相遇并复合, 发出光来.
反之, 电子和空穴都被推向相反的方向, 就无法发光了.

在开发板中, LED模块连接如图所示:

alt text

如图所示, LED 模块有八个 LED 灯. 右边接在 VCC ,也就是电源的正极, 左边与单片机的 P2(P2.0~P2.7) 引脚相连.

在 LED 与 电源正极 之间 有两个 1K 的电阻, 起到限流的作用, 防止 LED 灯发光过强而烧毁.

如何点亮 LED 灯?

为了让 LED 灯点亮, 需要在 LED 的阳极和阴极之间形成电流的通路.
已知 LED 灯的一侧接通的是电源正极, 我们只要能控制另一端引脚的电压即可.

1
2
3
void main() {
P2 = 0xFE; // 1111 1110 16进制为0xFE
}

但程序并不认识 P2 这个寄存器, 这时只需要导入头文件即可.

1
2
3
4
5
#include <regx52.h>

void main() {
P2 = 0xFE; // 1111 1110 16进制为0xFE
}

在这个头文件中就有 P2 这个寄存器的定义.

接着, 将写好的程序编译成 .hex 文件, 下载到单片机中.

alt text

LED 流水灯

让 8 个 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
#include <REG52.H>

// 定义一个延时函数
void Delay(unsigned int xms)
{
unsigned char i;

while(xms)
{
i = 500;
while (--i);
xms--;
}
}

void main(){
while(1){
P2 = 0xFE;
Delay(800);
P2 = 0xFD;
Delay(400);
P2 = 0xFB;
Delay(200);
P2 = 0xF7;
Delay(100);
P2 = 0xEF;
Delay(100);
P2 = 0xDF;
Delay(200);
P2 = 0xBF;
Delay(400);
P2 = 0x7F;
Delay(800);
}
}

单独操作位寄存器

在头文件 regx52.h 中, 我们可以看到寄存器的定义.

1
2
3
4
sbit P2_0 = 0xA0;
sbit P2_1 = 0xA1;
...
sbit P2_7 = 0xA7;

我们可以直接操作这些寄存器.

注意在 reg52.h 中并没有定义, 需要自己声明.

独立按键

按钮在单片机中的接线如图所示:

alt text

按钮一端接地(电源负极), 另一端接接在单片机的 IO 口上. 当单片机接电时, IO 口为高电平, 按钮按下, 电路导通, IO 口变为低电平.

即 按钮按下, IO 口为 ‘0’.
按钮松开, IO 口为 ‘1’.

按键的抖动

对于机械开关,当机械触点断开, 闭合时,由于机械触点的弹性作用, 一个开关在闭合时不会马上稳定地接通, 在断开时也不会立马完全断开, 所以在开关闭合及断开的瞬间会伴随一连串的抖动.

alt text

最方便的解决方法就是检测到按下时让程序延时一段时间即可.

用按钮控制 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
#include <REG52.H>

// 定义一个延时函数, 抹除抖动的影响.
void Delay(unsigned int xms)
{
unsigned char i;

while(xms)
{
i = 500;
while (--i);
xms--;
}
}

void main(){
while(1){
P2 = 0xFE; // 点亮全部 LED 灯
if(P2_0 == 0){ // 按下按钮
P2 = 0x00; // 熄灭全部 LED 灯
}
Delay(100);
}
}

定时器/计数器

在之前, 我们使用的是 Delay() 函数来延时, 但这种方式很不精确, 而且会占用主程序, 无法在 Delay() 函数执行时执行其他任务.

定时器就能用来实现精确的延时, 其电路的连接和运转均在单片机内部完成.

在 STC89C52 中, 有 T0, T1, T2 这三个定时器.
T0 和 T1 与传统的 51单片机 兼容, T2 则是此型号单片机增加的资源.

除了用作定时, 它也可用作计数器.

工作原理

定时器在单片机内部就像一个小闹钟. 根据时钟的输出信号, 每隔 一个时间段, 计数单元的数值就会增加一.
当计数单元数值增加到 设定的提醒时间 时, 计数单元就会向 中断系统 发出中断申请, 使程序跳转到中断服务函数中执行.

alt text

中断系统 类似于 定时器, 也是单片机内部的资源.

STC89C52 的 定时器/计数器 T0, T1 都有如下几种工作模式:

  • 模式0: 13 位定时器/计数器
  • 模式1: 16 位定时器/计数器 (常用)
  • 模式2: 8 位自动重装模式
  • 模式3: 两个 8 位计数器

工作模式 1 的框图:

alt text

计数器

如图, 在计数器里有

  • TL(Timer Low) : 低字节
  • TH(Timer High): 高字节

两个字节, 它们总共可以存储 65536 个不同的数值.

时钟模块 稳定地给 计数器 脉冲. 每来一次脉冲, 计数器就会增加 1.
直到 计数数到 65536 , 计数器溢出, 置一个标志位, 并向 中断系统 发出中断申请.

定时器时钟

系统时钟, 即晶振周期.

中断系统

中断系统是为使 CPU 具有对外界紧急事件的实时处理能力而设置的.

请示 CPU 中断的请求称为中断源, 中断源的轻重缓急称之为中断优先级.
高优先级的中断可以打断低优先级的中断.

alt text

不同型号的单片机拥有不同的中断资源.
在 STC89C52 中, 中断源共有 8 个.

alt text

不使用定时器依然可以实现延时, 可以通过查询 TF0 标志位.
就像你一直需要检查你的钟一样.

中断号:

中断源 中断服务函数 中断号
外部中断0 Int0_Routine(void) interrupt 0;
定时器0 Timer0_Routine(void) interrupt 1;
外部中断1 Int1_Routine(void) interrupt 2;
定时器1 Timer1_Routine(void) interrupt 3;
串口中断 UART_Routine(void) interrupt 4;
定时器2 Timer2_Routine(void) interrupt 5;
外部中断2 Int2_Routine(void) interrupt 6;
外部中断3 Int3_Routine(void) interrupt 7;

定时器相关的寄存器

寄存器是连接软硬件的桥梁, 相当于一个复杂机器的操作按钮.

在单片机中寄存器就是一段特殊的 RAM 存储器.
一方面, 寄存器可以存储和读取数据,
另一方面, 每一个寄存器背后都连接了一根导线, 控制着电路的连接方式.

TCON 定时器控制寄存器

TCON(Timer Control) 为定时器/计数器 T0, T1 的控制寄存器, 同时也锁存 T0, T1 溢出中断源和外部请求中断源等.
TF1, IE1, IT1等, 控制定时器 T1;IE0, TF0, IT0等 控制定时器 T0.

TCON 可位寻址, 因此能单独操作 TCON 寄存器的各个位.

TCON 格式如下:

SFR name Address bit B7 B6
TCON 88H name TF1 TR1
B5 B4 B3 B2 B1 B0
TF0 TR0 IE1 IT1 IE0 IT0
  • TF(Timer Flag): 定时器/计数器 溢出标志. 当 定时器 的最高位高位产生溢出时, 会由硬件将 TF 置为 1 ,然后向 CPU 请求中断. 直到 CPU 响应 此中断后, 才由硬件将 TF1 复位为 0.
  • TR(Timer Run): 定时器/计数器 运行控制位, 该位由软件置位和清零. 0 时停止计数, 1 时开始计数.
  • IE(Interrupt Enable): 定时器/计数器 中断使能位. IE1 = 1 时外部中断向 CPU 请求中断, 当 CPU 响应该中断时, 由硬件清 0.
  • IT(Interrupt Type): 定时器/计数器 中断类型位. 0 时为边沿触发, 1 时为电平触发.

TMOD 定时器工作模式寄存器

定时和计数功能由特殊功能寄存器 TMOD(Timer Mode) , 控制着 C/~T 进行选择.

TMOD 不可位寻址, 只能整体赋值.

7 6 5 4
GATE C/~T M1 M0
3 2 1 0
GATE C/~T M1 M0

其中, 03 是定时器0的工作模式, 47 是定时器1的工作模式.

可以看出, 两个定时器各有四种操作模式, 分别是:

符号 功能
TMOD.7/
TMOD.3/
GATE 控制定时器 T1, 当 GATE=1 , 需 INT1脚 为高 且 TR1=1 时, 才可打开 定时器T1.
控制定时器 T0, 当 GATE=1 , 需 INT0脚 为高 且 TR0=1 时, 才可打开 定时器T0.
TMOD.6/
TMOD.2/
C/~T 控制模式为 定时(= 0) 还是 计数(= 1).
TMOD.5 & TMOD.4 M1 & M0 控制定时器 T1 的工作模式.
TMOD.1 & TMOD.0 M1 & M0 控制定时器 T0 的工作模式.

M1, M0 共两位, 能用来选择定时器的工作模式.
具体的工作模式如下:

M1 M0 工作模式 说明
0 0 模式0:
13位定时器/计数器
兼容 8084 定时模式, TL只用低 5 位参与分频, TH1 整个 8 位全用.
0 1 模式1:
16位定时器/计数器
TL,TH 整个 16 位全用.
1 0 模式2:
8位自动重装模式
当溢出时将 TH1 存放的值自动装入 TL1.
1 1 模式3:
无效
停止计数.

使用定时器实现 LED 闪烁

接下来实践一下, 用定时器实现每隔 一秒 闪烁一次 LED 灯.

已知 计时器最大只能为 65535 ,我们先让计时器计数 1000 次时就溢出, 也就是先计 1微秒.
将计时器的初值设置为 64535 即可.

然后将这个 1微秒 的计时器重复 1000 次, 就是一个一秒的计时器了.

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

void Timer0_Init(){
// 设置 TMOD定时器工作模式(不能寻址, 只能整体赋值)
TMOD = 0x01; // 0000 0001 将模式设置为模式1, 使用整个TL0和TH0..

// 设置 TCON定时器控制寄存器(能寻址, 可以单独赋值)
TF0 = 0;
TR0 = 1;

// 将 64535 赋值给定时器0, 先把64535分成高8位和低8位, 然后分别赋值给TH0和TL0.
TH0 = 64535/256;
TL0 = 64535%256;

// 打开中断系统
ET0 = 1;
EA = 1;
PT0 = 0;
}

void main(){
Timer0_Init; // 启动配置好的定时器0
while(1){
}
}

unsigned int T0Counter = 0; // 定义一个计算定时器执行了多少次的变量, 以让1毫秒的计时器重复1000次.
// 定时器0中断服务程序
void Timer0_Routine() interrupt 1 {

// 重新校对计时器, 保证下一次计时也是 1毫秒
TH0 = 64535/256;
TL0 = 64535%256;

T0Counter++;
if (T0Counter >= 1000){
T0Counter = 0;
P2_0 = ~P2_0;
}
}

TMOD 缺陷

当需要使用两个定时器时, 又由于 TMOD 只能整体赋值, 所以当给其中一个定时器赋值时, 会对另一个定时器造成影响.

因此, 我们这样修改代码:

1
2
3
4
5
// 旧代码: TMOD = 0x01; 0000 0001, 会影响 T1 的工作模式.

// 新思路: 只操作 T0 的工作模式.
TMOD = TMOD & 0x0F; // 0000 1111 , 把 TMOD 低四位清零, 保留 T1 的工作模式.
TMOD = TMOD | 0x01; // 0000 0001 , 给 TMOD 低四位赋值, 使 T0 的工作模式为模式1.

在数字电路中, 任何数 & 1,都等于自身, 任何树 & 0, 都等于 0.
利用这个特征, 我们可以只操作需要赋值的定时器.

让其只操作需要赋值的定时器即可.

在 STC-ISP 软件, 可以很方便地配置定时器:

alt text

  • 89C52 是没有 16位自动重载的. 只有 16位 或 8位自动重载.
  • 定时器时钟选中 12T
  • 生成的代码没有配置中断系统, 记得加上:
  • 生成的代码针对新系列单片机, 会有 AUXR 寄存器, 但是我们在学习的单片机没有, 因此记得删除.
1
2
3
EA = 1; // 打开总中断
ET0 = 1; // 打开定时器0中断
PT0 = 0; // 设置优先级为0

数码管

LED 数码管是一种简单, 廉价的显示器, 是由多个发光二极管封装在一起组成 “8” 自型的器件.

LED 连接方式有两种

  • 共阴极
  • 共阳极

也就只是一种连接方式而已, 了解即可.

想要使单个 数码管 显示数字, 根据其接线口, 可以很轻易推出显示数字的值.

alt text

例如想要显示数字 1, 则将 b, c 赋值为1.
赋值时注意 高位是从 DP 开始. 例如 1 就该赋值为 0000 0110.

其他的数字同理:

显示数字 二进制 十六进制
0 0011 1111 0x3F
1 0000 0110 0x06
2 0101 1011 0x5B
3 0100 1111 0x4F
4 0110 0110 0x66
5 0110 1101 0x6D
6 0111 1101 0x7D
7 0000 0111 0x07
8 0111 1111 0x7F
9 0110 1111 0x6F
A 0111 1011 0x77
B 0111 1110 0x7C
C 0011 1001 0x39
D 0101 1110 0x5E
E 0111 1001 0x79
F 0111 1111 0x71
1111 1111 0x00

四位一体数码管在单片机上接线如图所示:

alt text

如图, 数码管的一端连接着 74HC254 , 能进行数据缓冲.

由于高电平的驱动能力有限, 因此信号传输都用低电平完成. 但是直接用低电平点亮LED, 效果并不理想.
因此, 这个数据缓冲就是用来提高驱动能力的.
我们用低电平发送信号, 然后由这个数据缓冲器把信号提高到高电平来驱动 LED.

数码管的另一边接的就是 138 译码器, 是一种3线-8线译码器:

alt text

138 译码器 只用三个 IO 端(A/P2_2, B/P2_3, C/P2_4) , 就可以控制八个输出端.

它的工作原理是:

A, B, C 输出
000 0 (LED 8)
001 1 (LED 7)
010 2 (LED 6)
011 3 (LED 5)
100 4 (LED 4)
101 5 (LED 3)
110 6 (LED 2)
111 7 (LED 1)

选中的输出端, 就会输出0.

点亮一个 LED 数码管

那么一个 LED 数码管, 究竟是怎么样被点亮的呢?

第一步就是通过 138 译码器, 通过这三个口, 使某一位输出为 0 ,选中要显示的LED.
接下来通过缓冲送来的信号, 驱动 LED 点亮.

例如, 点亮 LED6, 使其显示数字 6, :
代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <REGX52.H>

void main(){
// 选中 LED6 ->
P2_4=1;
P2_3=0;
P2_2=1;

// 设定显示的数字
P0=0x7D;
while(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
// 设计一个数组存储每个数字对应的二进制数
unsigned char numToHex[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00};
// 设定显示在哪一个数码管上, 以及要显示的数字
void displayNum(unsigned char Location, num){
switch(Location){
case 8: // 数码管8 -> 000
P2_4=0;P2_3=0;P2_2=0;break;
case 7:
P2_4=0;P2_3=0;P2_2=1;break;
case 6:
P2_4=0;P2_3=1;P2_2=0;break;
case 5:
P2_4=0;P2_3=1;P2_2=1;break;
case 4:
P2_4=1;P2_3=0;P2_2=0;break;
case 3:
P2_4=1;P2_3=0;P2_2=1;break;
case 2:
P2_4=1;P2_3=1;P2_2=0;break;
case 1:
P2_4=1;P2_3=1;P2_2=1;break;
}
P0=numToHex[num]; // 设定显示的数字
}

多位数码管动态显示

由于其设计, 数码管无法做到同时显示多个数字.
但是我们可以快速切换显示的数字, 只要切换的足够迅速, 利用人眼的视觉暂留效应, 就可以看到”同时”显示多个数字.

不过如果我们直接这样写

1
2
3
4
5
6
7
8
9
// 使用刚刚设计的函数 displayNum()

void main(){
while(1){
displayNum(1, 1); // 在第一个数码管显示数字 1
displayNum(2, 2);
displayNum(3, 3);
}
}

会出现 数码管的残影:

alt text

影响数字辨认.

这是由于数码管显示数字时, 会先选位选(选择要在哪个数码管上显示), 然后再段选(选择要显示的数字).

位选 –> 段选 –> 位选 –> 段选 –> …

由于这个过程十分迅速, 就会导致数码管串位显示, 数字显示在了错误的位置上.

为了解决这个问题, 可以这样做:

位选 –> 段选 –> 清零 –> 位选 –> 段选 –> 清零 –> …

代码层面, 我们可以优化一下 displayNum() 函数:

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

unsigned char numToHex[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00};

// 定义一个延时函数
void Delay(unsigned int xms)
{
unsigned char i;

while(xms)
{
i = 500;
while (--i);
xms--;
}
}

void displayNum(unsigned char Location, num){
switch(Location){
case 8:
P2_4=0;P2_3=0;P2_2=0;break;
case 7:
P2_4=0;P2_3=0;P2_2=1;break;
case 6:
P2_4=0;P2_3=1;P2_2=0;break;
case 5:
P2_4=0;P2_3=1;P2_2=1;break;
case 4:
P2_4=1;P2_3=0;P2_2=0;break;
case 3:
P2_4=1;P2_3=0;P2_2=1;break;
case 2:
P2_4=1;P2_3=1;P2_2=0;break;
case 1:
P2_4=1;P2_3=1;P2_2=1;break;
}
P0=numToHex[num];
Delay(5);
P0=numToHex[0]; // 清零
}

void main(){
while(1){
displayNum(1,1);
displayNum(2,2);
displayNum(3,3);
}
}

模块化编程

众所周知, 程序员最不喜欢一直做重复的事. 像之前提到的 Delay(), 我们可以把它封装成一个函数, 然后在其他地方调用, 就不用每次都要写一遍.

alt text

C预编译

C语言的预编译以 # 开头, 作用是在珍珠的编译开始之前, 对代码做一些预处理(预编译)

预编译 作用
#include 包含头文件
#define 定义常量
#ifndef 防止头文件重复包含
#endif 结束头文件包含

接下来, 尝试一下将之前写的 Delay() 函数模块化.

首先创建工程, 完成一些基本操作:

1
2
3
4
5
6
7
8
#include <REGX52.H> // <> 表示在安装目录寻找头文件
#include "delay.h" // "" 表示在当前工程目录寻找头文件

void main(){
while(1){
Dlay(500);
}
}

然后在工程目录下创建 delay.h 和 delay.c 文件:

delay.c 存放函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
// 定义一个延时函数
void Delay(unsigned int xms)
{
unsigned char i;

while(xms)
{
i = 500;
while (--i);
xms--;
}
}

delay.h 存放函数声明:

1
2
3
4
5
6
7
8
#ifndef __DELAY_H__ // 命名一般以这样的形式定义, 全大写, 下划线分隔.
#define __DELAY_H__ // 防止头文件重复包含,如果没有这个名字, 就创建.

// 声明函数

void Delay(unsigned int xms);

#endif

矩阵键盘

  • 标题: 51单片机原理与应用
  • 作者: Wreckloud_雲之残骸
  • 此记初现于 : 2024-09-30 12:41:49
  • 此记变迁于 : 2024-11-22 12:07:31
  • 链接: https://www.wreckloud.com/2024/09/30/猎识印记-领域/嵌入式/51单片机/
  • 版权声明: 本幽影记采用 CC BY-NC-SA 4.0 进行许可。
影踪语