51单片机基础

学习时间:2022年11月6日

学习来源:江科大自化协

1 开发工具

Keil5Keil C51是美国Keil Software公司出品的51系列兼容单片机C语言软件开发系统。Keil提供了包括C编译器、宏汇编、链接器、库管理和一个功能强大的仿真调试器等在内的完整开发方案,通过一个集成开发环境(μVision)将这些部分组合在一起。

官网:https://www.keil.com/

下载地址:https://www.keil.com/download/product/

image-20221106083912547

STC-ISP:STC-ISP 是一款单片机下载编程烧录软件,是针对STC系列单片机而设计的,可下载STC89系列、12C2052系列和12C5410等系列的STC单片机,使用简便。

官网:http://www.stcmcudata.com/

image-20221106085143818

2 单片机和开发板介绍

2.1 单片机

单片机(Single-Chip Microcomputer)是一种集成电路芯片,是采用超大规模集成电路技术把具有数据处理能力的中央处理器CPU、随机存储器RAM、只读存储器ROM、多种I/O口和中断系统、定时器/计数器等功能(可能还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统,在工业控制领域广泛应用。

从上世纪80年代,由当时的4位、8位单片机,发展到现在的300M的高速单片机。

单片机又称单片微控制器,它不是完成某一个逻辑功能的芯片,而是把一个计算机系统集成到一个芯片上。相当于一个微型的计算机,和计算机相比,单片机只缺少了I/O设备。概括的讲:一块芯片就成了一台计算机

2.2 51单片机

51单片机是对兼容英特尔8051指令系统的单片机的统称。51单片机广泛应用于家用电器、汽车、工业测控、通信设备中。

  • 主要产品:
    • Intel(英特尔):80C31、80C51、87C51,80C32、80C52、87C52等;
    • Atmel(艾特梅尔):89C51、89C52、89C2051,89S51(RC),89S52(RC)等;
    • Philips(飞利浦)、华邦、Dallas(达拉斯)、Siemens(西门子)等公司的许多产品;
    • STC(宏晶):STC89C51、STC90C51、STC11系列、STC15系列、STC8系列等。

STC89C52单片机

image-20221106091048956

  • 所属系列:51单片机系列
  • 公司:STC公司(宏晶),中国公司
  • 位数:8位
  • RAM:512字节
  • ROM:8K(Flash)
  • 工作频率:12MHz(本开发板使用)

命名规则

image-20221106091344914

内部结构

STC89C52系列单片机的内部结构框图如下图所示。STC89C52单片机中包含中央处理器(CPU)、程序存储器(Flash)、数据存储器(SRAM)、定时/计数器、UJART串口、I/O接口、EE-PROM、看门狗等模块。STC89C52系列单片机几平包含了数据采集和控制中所需的所有单元模块,可称得上一个片上系统。

image-20221106092106234

2.3 开发板

image-20221106092617470

原理图

image-20221106093607492

注:我的自用开发板(STC89C52 RC):

image-20221111095510049

image-20221111095532817

3 LED

3.1 点亮一个LED

3.1.1 新建工程

  • 在Keil5中新建一个工程:搜索AT89C52(采用的是Atmel),然后选择,不生成启动文件。

image-20221110151651414

  • 新建C源代码文件

image-20221110151857736

image-20221110151917508

  • 编写初始代码
1
2
3
void main() {

}
  • 构建build项目

image-20221110152240453

image-20221110152253228

3.1.2 LED介绍

发光二极管(Light Emitting Diode

简称:LED

用途:照明、广告灯、指引灯、屏幕

image-20221110152757233

上图:左正右负。

原理图

简单的原理:单片机的CPU通过向IO口写入值(1高电平5v,0低电平0v),来控制硬件电路的高低电平

  • LED模块

image-20221110154407963

从上图可知,LED模块引出的管脚编号从P20~P27,共8个管脚,控制8个LED

  • 单片机核心

image-20221110154544972

从上图可知,LED的P20~P27管脚连接至单片机CPU的P2.0~P2.7管脚。因此通过控制51单片机核心中的P2特殊功能寄存器的每一位的值,来控制输出到LED的P20~P27管脚的电平高低。

例如,如果P2.0的值设置为1,代表高电平,此时LED灭(两端没有电压差);相反设置为0,代表低电平,此时LED亮。

3.1.3 代码实现

这里让P20(芯片寄存器为P2,位号为P2.0)对应的LED亮,其他LED灭。

1
2
3
4
#include <REGX52.H>
void main() {
P2 = 0xFE; // P2.7~P2.0 1111 0000
}

头文件REGX52.H包含了AT89X52芯片的寄存器变量(仅列出部分):

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
/*--------------------------------------------------------------------------
AT89X52.H

Header file for the low voltage Flash Atmel AT89C52 and AT89LV52.
Copyright (c) 1988-2002 Keil Elektronik GmbH and Keil Software, Inc.
All rights reserved.
--------------------------------------------------------------------------*/

#ifndef __AT89X52_H__
#define __AT89X52_H__

/*------------------------------------------------
Byte Registers
------------------------------------------------*/
sfr P0 = 0x80;
sfr SP = 0x81;
sfr DPL = 0x82;
sfr DPH = 0x83;
sfr PCON = 0x87;
sfr TCON = 0x88;
sfr TMOD = 0x89;
sfr TL0 = 0x8A;
sfr TL1 = 0x8B;
sfr TH0 = 0x8C;
sfr TH1 = 0x8D;
sfr P1 = 0x90;
sfr SCON = 0x98;
sfr SBUF = 0x99;
sfr P2 = 0xA0; // P2特殊功能寄存器(8位),0xA0是IO口P2在RAM中的字节地址
sfr IE = 0xA8;
sfr P3 = 0xB0;
// ...

然后构建,生成相应的hex十六进制文件

3.1.4 烧录

  • 相应设置:

image-20221110163657714

打开程序文件:选择项目下构建好的hex十六进制文件,然后下载。

关闭单片机,再进行冷启动(单片机在冷启动时加载程序),之后即可发现LED亮。

程序改进

单片机上电后,会执行main函数:

1
2
3
4
#include <REGX52.H>
void main() {
P2 = 0xFE; // P2.7~P2.0 1111 0000
}

执行到第三行后,LED亮,之后从main函数退出,cpu会再次执行main函数,到第三行,LED亮,如此反复,此时cpu会不断操作P2寄存器。

改进:

1
2
3
4
5
6
7
#include <REGX52.H>
void main() {
P2 = 0xFE; // P2.7~P2.0 1111 0000
while (true) {

}
}

cpu执行至第4行进入死循环,此时P2 = 0xFE;,LED一直亮,且cpu不会再操作P2寄存器。

3.2 LED闪烁

效果:让LED以1s为周期闪烁。新建一个工程。

代码实现

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

void Delay1000ms(){
unsigned char i, j, k;

_nop_();
i = 8;
j = 154;
k = 122;
do{
do{
while (--k);
} while (--j);
} while (--i);
}

void main() {
while (1){
P2 = 0xFE;
Delay1000ms();
P2 = 0xFF;
Delay1000ms();
}
}

3.3 LED流水灯

效果: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
#include <REGX52.H>
#include <INTRINS.H>

void Delay500ms(){
unsigned char i, j, k;

_nop_();
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();
}
}

代码优化:修改延时代码,接受一个参数n来控制熄灭的时间。

1
2
3
4
5
6
7
8
9
10
11
12
// n为延时的时间,单位ms
void Delay(unsigned int n) {
unsigned char i, j;
while(n) {
i = 2;
j = 239;
do{
while (--j);
} while (--i);
n--;
}
}

4 独立按键

4.1 介绍

轻触按键:相当于是一种电子开关,按下时开关接通,松开时开关断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开。

image-20221111095750241

电路原理图

  • 独立按键模块:例如,按下K1时,P31接地为低电平(0),使得cpu的P3寄存器的该位为0;反之为1。

image-20221111103411354

  • cpu:接至P3寄存器

image-20221111101404759

4.2 控制LED亮灭

效果:按下按键K1(注意编号为P31),LED D1(编号P20)亮,松开按键K1,LED D1灭。


REGX52.H头文件中,不仅定义了8位寄存器的变量(以sfr修饰),还定义了针对8位寄存器每1位的单独变量(以sbit修饰),例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*------------------------------------------------
Byte Registers
------------------------------------------------*/
sfr P2 = 0xA0; // 8位寄存器变量P2
/*------------------------------------------------
P2 Bit Registers
------------------------------------------------*/
sbit P2_0 = 0xA0; // P20 低位,0xA0是位地址
sbit P2_1 = 0xA1;
sbit P2_2 = 0xA2;
sbit P2_3 = 0xA3;
sbit P2_4 = 0xA4;
sbit P2_5 = 0xA5;
sbit P2_6 = 0xA6;
sbit P2_7 = 0xA7; // P27 高位

代码实现

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

void main() {
while(1) {
if(P3_1 == 0) { // 如果P31口为低电平(即按下按键K1,使得P31接地了)
P2_0 = 0; // 让P20口输出低电平,使LED亮
} else { // 松开K1
P2_0 = 1; // 让P20口输出高电平,使LED灭
}
}
}

4.3 控制LED状态

效果:按一下按键K1,LED D1亮,再按一下,LED灭

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

image-20221112131658891

消抖有两种策略:

  • 硬件消抖:在电路中加入电容
  • 软件消抖:加入延时函数

代码实现

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

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

void main() {
while(1) {
if(P3_1 == 0) {
// 按下时的消抖
Delay(20);
while(P3_1 == 0); // 如果按住不松手的话,一直循环
// 松开时的消抖
Delay(20);
P2_0 = ~P2_0; // 亮灭状态取反
}
}
}

4.4 控制LED显示二进制

效果:每按一次K1,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
#include <REGX52.H>

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

void main() {
// unsigned char为8位正值,常用来代表一个8位寄存器
unsigned char LEDNum = 0;
while(1) {
if(P3_1 == 0) {
Delay(20);
while(P3_1 == 0);
Delay(20);
// 单片机上电后,端口为高电平
// 因此P2的初始值为1111 1111
LEDNum++;
P2 = ~LEDNum;
}
}
}

上述程序:

  • 第一次按下时,LEDNum=0000 0001,取反为LEDNum=1111 1110,并赋值给P2,使得最后一位(P2_0)的LED亮。
  • 第二次按下时,LEDNum=0000 0010,取反为LEDNum=1111 1101,并赋值给P2,使得P2_1的LED亮。
  • 以此类推

4.5 控制LED移位

效果:每按一次K1,LED从低位到高位,一次移动一位亮。每按一次K2,LED从高位到低位,一次移动一位亮。

  • P2寄存器需要的变化:
    • 1111 1110–>1111 1101–>1111 1011
  • 取反后相应的变化:
    • 0000 0001–>0000 0010–>0000 0100
    • 0000 0001向左进行移位,按多少次,就移动多少位

代码实现

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 Delay(unsigned int n) {
unsigned char i, j;
while(n) {
i = 2;
j = 239;
do{
while (--j);
} while (--i);
n--;
}
}

void main() {
unsigned char LEDNum = 0;
// 先让最低位的LED亮,初始状态
P2 = ~0x01;
while(1) {
if(P3_1 == 0) {
Delay(20);
while(P3_1 == 0);
Delay(20);
LEDNum++;
if(LEDNum >= 8)
LEDNum = 0;
P2 = ~(0x01<<LEDNum);
}
if(P3_0 == 0) {
Delay(20);
while(P3_0 == 0);
Delay(20);
if(LEDNum == 0)
LEDNum = 7;
else
LEDNum--;
P2 = ~(0x01<<LEDNum);
}
}
}

5 数码管

5.1 静态数码管显示

5.1.1 概述

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

一位数码管

image-20221113091929288

分为共阴极COM连接和共阳极GRN连接两种连接方式:

image-20221113091956859

image-20221113091959469

  • 引脚连接

以共阳极(上面的下图)为例,3和8号引脚接至正极,A号LED负极引脚编号为7,以此类推。

多位数码管

image-20221113092547876

也分为共阴极连接和共阳极连接两种连接方式:

image-20221113092603250

5.1.2 电路原理图

  • 动态数码管模块

image-20221113093616534

其中74HC245是一个双向数据缓冲器,端口是一一对应的关系,作用是提供驱动能力(如果去掉,则P传过来的信号直接当做驱动,给LED的阳极供电,反之,加上缓冲器后,P传过来的信号只是当做控制信号,即使很微弱也能区分相对的正负,之后再由74HC245的VCC直接给LED提供电源驱动,使得LED更亮)。DIR控制数据流向,高电平为从左到右(右边读左边),低电平从右到左(左边读右边),图中DIR接电源,则上电后为右边读左边,有的单片机该端口接的是跳线帽Jxx

  • 74H138译码器

输入端:P22~P24,高位到低位:CBA

输出端Y0~Y7

使能端G1~G2:单片机上电就使能。

例如,输入CBA为101(二进制),换算成十进制为5,则选中Y5使能,其余失效。

image-20221113100059191

5.1.3 代码实现

效果:点亮LED6,并显示6

代码实现

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

void main() {
P2_4 = 1;
P2_3 = 0;
P2_2 = 1;
P0 = 0x7D;
while(1);
}

程序解析:

  • 在译码器中,LED6对应Y5,因此输入端二进制应该为101
  • abcdefg(dp)亮的为acdefg,因此LED段码为:0111 1101,对应十六进制0x7D

代码优化

构造一个函数Nixie,接收参数控制哪个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>

unsigned char NixieTable[] = {
0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F
};

// 显示的位置(1-8)和数字(0-9)
void Nixie(unsigned char loc, num) {
switch(loc) {
case 1:
P2_4 = 1; P2_3 = 1; P2_2 = 1;break;
case 2:
P2_4 = 1; P2_3 = 1; P2_2 = 0;break;
case 3:
P2_4 = 1; P2_3 = 0; P2_2 = 1;break;
case 4:
P2_4 = 1; P2_3 = 0; P2_2 = 0;break;
case 5:
P2_4 = 0; P2_3 = 1; P2_2 = 1;break;
case 6:
P2_4 = 0; P2_3 = 1; P2_2 = 0;break;
case 7:
P2_4 = 0; P2_3 = 0; P2_2 = 1;break;
case 8:
P2_4 = 0; P2_3 = 0; P2_2 = 0;break;
}
P0 = NixieTable[num];
}

void main() {
Nixie(1, 9);
while(1);
}

5.2 动态数码管显示

所谓动态显示,是指多个数码管同时亮起,实际上从电路原理图来看,在确定的某一时刻,只能有一个数码管亮(译码器只能送一个确定的值来决定位选);但是单片机执行指令速度很快,人眼具有视觉暂留,因此看起来像是多个数码管同时亮了起来。

数码管显示通常以位选 段选 位选 段选...的顺序来执行,其中,位选是选择哪一个数码管亮,段选是数码管中的LED哪些亮,以形成一个数字或某种形状。

1
2
3
4
5
6
7
void main() {
while(1) {
Nixie(1, 1); // 位选1,段选的形状为1
Nixie(2, 2); // 位选2,段选的形状为2
Nixie(3, 3);
}
}

消隐:单片机执行速度很快,以位选 段选 位选 段选...的顺序执行时,段选码可能会窜到位选码的位置上,导致错误。在数码动态显示过程中,若进行位选切换时没有对上一位显示的内容进行清空,则会导致当前数码管中出现上一片内容的余影,从而使显示模糊,影响了整个显示效果。

解决方法:在数码管位选信号切换前,先向段传送“不亮”字型码,然后在进行切换和正常传递新段码。

image-20221115100921700

实现代码

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>

unsigned char NixieTable[] = {
0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F
};

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

void Nixie(unsigned char loc, num) {
switch(loc) {
case 1:
P2_4 = 1; P2_3 = 1; P2_2 = 1;break;
case 2:
P2_4 = 1; P2_3 = 1; P2_2 = 0;break;
case 3:
P2_4 = 1; P2_3 = 0; P2_2 = 1;break;
case 4:
P2_4 = 1; P2_3 = 0; P2_2 = 0;break;
case 5:
P2_4 = 0; P2_3 = 1; P2_2 = 1;break;
case 6:
P2_4 = 0; P2_3 = 1; P2_2 = 0;break;
case 7:
P2_4 = 0; P2_3 = 0; P2_2 = 1;break;
case 8:
P2_4 = 0; P2_3 = 0; P2_2 = 0;break;
}
P0 = NixieTable[num]; // 段选
Delay(1); // 延时,让LED更亮
P0 = 0x00; // 段选清零,让其不亮
}

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

数码管驱动方式:

  • 单片机直接扫描:硬件设备简单,但会耗费大量的单片机CPU时间
  • 专用驱动芯片:内部自带显存、扫描电路,单片机只需告诉它显示什么即可

6 模块化编程和LCD

6.1 简介

传统方式编程:所有的函数均放在main.c里,若使用的模块比较多,则一个文件内会有很多的代码,不利于代码的组织和管理,而且很影响编程者的思路。

模块化编程:把各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用函数的声明,其它.c文件想使用其中的代码时,只需要#include "XXX.h"文件即可。使用模块化编程可极大的提高代码的可阅读性、可维护性、可移植性等。

image-20221115104229885

注意事项:

  • .c文件:函数、变量的定义
  • .h文件:可被外部调用的函数、变量的声明
  • 任何自定义的变量、函数在调用前必须有定义或声明(同一个.c)
  • 使用到的自定义函数的.c文件必须添加到工程参与编译
  • 使用到的.h文件必须要放在编译器可寻找到的地方(工程文件夹根目录、安装目录、自定义)

6.2 预编译

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

预编译 意义
#include <REGX52.H> REGX52.H文件的内容搬到此处
#define PI 3.14 定义PI,将PI替换为3.14
#define ABC 定义ABC
#ifndef __XX_H__ 如果没有定义__XX_H__
#endif #ifndef,#if匹配,组成“括号”

此外还有#ifdef,#if,#else,#elif,#undef

6.3 模块化实现

需求:将延时函数和数码管显示的函数模块化。

  • 项目结构:

image-20221115111034058

代码实现

  • Delay.cDelay.h
1
2
3
4
5
6
7
8
9
10
11
void Delay(unsigned int n) {
unsigned char i, j;
while(n) {
i = 2;
j = 239;
do{
while (--j);
} while (--i);
n--;
}
}
1
2
3
4
5
6
#ifndef __Delay_H_
#define __Delay_H_
// 函数声明
void Delay(unsigned int n);

#endif
  • Nixie.cNixie.h
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 "Delay.h"

unsigned char NixieTable[] = {
0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F
};

void Nixie(unsigned char loc, num) {
switch(loc) {
case 1:
P2_4 = 1; P2_3 = 1; P2_2 = 1;break;
case 2:
P2_4 = 1; P2_3 = 1; P2_2 = 0;break;
case 3:
P2_4 = 1; P2_3 = 0; P2_2 = 1;break;
case 4:
P2_4 = 1; P2_3 = 0; P2_2 = 0;break;
case 5:
P2_4 = 0; P2_3 = 1; P2_2 = 1;break;
case 6:
P2_4 = 0; P2_3 = 1; P2_2 = 0;break;
case 7:
P2_4 = 0; P2_3 = 0; P2_2 = 1;break;
case 8:
P2_4 = 0; P2_3 = 0; P2_2 = 0;break;
}
P0 = NixieTable[num]; // 段选
Delay(1); // 延时,让LED更亮
P0 = 0x00; // 段选清零,让其不亮
}
1
2
3
4
5
6
#ifndef __Nixie_H__
#define __Nixie_H__

void Nixie(unsigned char loc, num);

#endif
  • main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <REGX52.H>
#include "Nixie.h"

void main() {
while(1) {
Nixie(1, 1);
Nixie(2, 2);
Nixie(3, 3);
Nixie(4, 4);
Nixie(5, 5);
Nixie(6, 6);
Nixie(7, 7);
Nixie(8, 8);
}
}

6.4 LCD1602调试工具

6.4.1 原理图

使用LCD1602液晶屏作为调试窗口,提供类似printf函数的功能,可实时观察单片机内部数据的变换情况,便于调试和演示。

本视频提供的LCD1602代码属于模块化的代码,使用者只需要知道所提供函数的作用和使用方法就可以很容易的使用LCD1602。

函数 作用
LCD_Init(); 初始化
LCD_ShowChar(1,1,'A'); 显示一个字符
LCD_ShowString(1,3,"Hello"); 显示字符串
LCD_ShowNum(1,9,123,3); 显示十进制数字
LCD_ShowSignedNum(1,13,-66,2); 显示有符号十进制数字
LCD_ShowHexNum(2,1,0xA8,2); 显示十六进制数字
LCD_ShowBinNum(2,4,0xAA,8); 显示二进制数字
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#include <REGX52.H>

//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0

//函数定义:
/**
* @brief LCD1602延时函数,12MHz调用可延时1ms
* @param 无
* @retval 无
*/
void LCD_Delay()
{
unsigned char i, j;

i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}

/**
* @brief LCD1602写命令
* @param Command 要写入的命令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}

/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}

/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}

/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}

/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}

/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}

/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}

/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}

/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}

/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}

/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef __LCD1602_H__
#define __LCD1602_H__

//用户调用函数:
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);

#endif

电路原理图

  • LCD1602接口

image-20221115113003628

  • cpu

image-20221115113042232

由图可知,LCD的八个引脚LCD D0 ~ LCD D7与cpu的P0寄存器的P0 ~ P7相连

6.4.2 代码示例

1
2
3
4
5
6
7
8
9
10
11
12
#include <REGX52.H>
#include "LCD1602.h"

void main() {
LCD_Init();
LCD_ShowChar(1, 1, 'A');
LCD_ShowString(1, 3, "Hello World!");
LCD_ShowNum(2, 1, 123, 3);
while(1) {

}
}

7 矩阵键盘

7.1 简介

image-20221117155801715

在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式。

采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态。

扫描

  • 数码管扫描(输出扫描) 原理:显示第1位→显示第2位→显示第3位→……,然后快速循环这个过程,最终实现所有数码管同时显示的效果
  • 矩阵键盘扫描(输入扫描) 原理:读取第1行(列)→读取第2行(列) →读取第3行(列) → ……,然后快速循环这个过程,最终实现所有按键同时检测的效果

以上两种扫描方式的共性:节省I/O口

7.2 原理图

矩阵按键

image-20221117160609682

通过原理图可知,P17~P14连接按键的左端,P13~P10连接按键的右端

  • 按行扫描:
    • 扫描第一行:给定P17~P140111,当P13~P10的哪一位为0,则S1~S4对应的被按下。例如P13为0,则S1被按下。
    • 扫描第二行:给定P17~P141011,当P13~P10的哪一位为0,则S5~S8对应的被按下。例如P13为0,则S5被按下。
    • 以此类推
  • 按列扫描:
    • 扫描第一列:给定P13~P100111,当P17~P14的哪一位为0,则S1,S5,S9,S13对应的被按下。
    • 以此类推

单片机核心

image-20221117162805282

7.3 矩阵键盘

效果:采用列扫描;按下一个按键,在LCD显示屏上显示按键的序号。


可以为头文件新建一个模板(代码片段):

image-20221117163406228


  • MatrixKey.c和头文件
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
#include <REGX52.H>
#include "Delay.h"

/**
* @brief 矩阵键盘按键检测
* @param 无
* @retval keyNumber 按下按键的序号
*/
unsigned char MatrixKey() {

unsigned char keyNumber = 0; // 按下的按键的序号

// 按列扫描
P1 = 0xFF; // 先全部置1
P1_3 = 0; // 扫描第1列
if (P1_7 == 0) {Delay(20);while(P1_7 == 0);Delay(20);keyNumber = 1;}
if (P1_6 == 0) {Delay(20);while(P1_6 == 0);Delay(20);keyNumber = 5;}
if (P1_5 == 0) {Delay(20);while(P1_5 == 0);Delay(20);keyNumber = 9;}
if (P1_4 == 0) {Delay(20);while(P1_4 == 0);Delay(20);keyNumber = 13;}

P1 = 0xFF; // 先全部置1
P1_2 = 0; // 扫描第2列
if (P1_7 == 0) {Delay(20);while(P1_7 == 0);Delay(20);keyNumber = 2;}
if (P1_6 == 0) {Delay(20);while(P1_6 == 0);Delay(20);keyNumber = 6;}
if (P1_5 == 0) {Delay(20);while(P1_5 == 0);Delay(20);keyNumber = 10;}
if (P1_4 == 0) {Delay(20);while(P1_4 == 0);Delay(20);keyNumber = 14;}

P1 = 0xFF; // 先全部置1
P1_1 = 0; // 扫描第3列
if (P1_7 == 0) {Delay(20);while(P1_7 == 0);Delay(20);keyNumber = 3;}
if (P1_6 == 0) {Delay(20);while(P1_6 == 0);Delay(20);keyNumber = 7;}
if (P1_5 == 0) {Delay(20);while(P1_5 == 0);Delay(20);keyNumber = 11;}
if (P1_4 == 0) {Delay(20);while(P1_4 == 0);Delay(20);keyNumber = 15;}

P1 = 0xFF; // 先全部置1
P1_0 = 0; // 扫描第4列
if (P1_7 == 0) {Delay(20);while(P1_7 == 0);Delay(20);keyNumber = 4;}
if (P1_6 == 0) {Delay(20);while(P1_6 == 0);Delay(20);keyNumber = 8;}
if (P1_5 == 0) {Delay(20);while(P1_5 == 0);Delay(20);keyNumber = 12;}
if (P1_4 == 0) {Delay(20);while(P1_4 == 0);Delay(20);keyNumber = 16;}

return keyNumber;
}
1
2
3
4
#ifndef __MATRIXKEY_H__
#define __MATRIXKEY_H__
unsigned char MatrixKey();
#endif
  • main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"

unsigned char keyNumber;

void main() {
LCD_Init();
LCD_ShowString(1, 1, "HelloWorld");
while(1) {
keyNumber = MatrixKey();
if(keyNumber) {
LCD_ShowNum(2, 1, keyNumber, 2);
}
}
}

7.4 密码锁

效果:S1~S9定义为输入的数字,S10为0,S11为确认,S12为取消。

  • 代码实现
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
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"

unsigned char keyNumber;
unsigned int password, count;

void main() {
LCD_Init();
LCD_ShowString(1, 1, "Password:");
while(1) {
keyNumber = MatrixKey();
if(keyNumber) {
if(keyNumber <= 10) {
if(count < 4) { // 假设只输入4位密码
password *= 10; // 相当于密码左移
password += (keyNumber % 10); // 获取一位密码
count++;
}
LCD_ShowNum(2, 1, password, 4);
}
if(keyNumber == 11) { // 确认键
if (password == 2345) { // 密码正确
LCD_ShowString(1, 14, "OK ");
password = 0; // 输入清零
count = 0; // 计数清零
LCD_ShowNum(2, 1, password, 4); // 更新显示
} else { // 密码错误
LCD_ShowString(1, 14, "ERR");
password = 0; // 输入清零
count = 0; // 计数清零
LCD_ShowNum(2, 1, password, 4);
}
}
if(keyNumber == 12) {
password = 0; // 输入清零
count = 0; // 计数清零
LCD_ShowNum(2, 1, password, 4);
}
}
}
}

8 定时器

8.1 简介

8.1.1 定时器资源

定时器介绍:51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。

定时器作用:

  • 用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作
  • 替代长时间的Delay,提高CPU的运行效率和处理速度(不让CPU空转)

定时器个数:3个(T0T1T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源。

注意:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机所共有的。

8.1.2 框图

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

image-20221120143519757

8.1.3 工作模式

STC89C52的T0T1均有四种工作模式:

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

工作模式1的框图

image-20221120144730109

SYSclk:系统时钟,即晶振周期,本开发板上的晶振为12MHz。然后可进行12分频和6分频两种,12分频使得时钟脉冲的频率变为1MHz(每1μs,计数加1),6分频使其变为2MHz。

T0 PIN:计数器功能。C/T非是计数器(Counter)和定时器(Timer)的功能选择开关。若为低电平,则定时器功能选通,否则是计数器功能。

计数单元有两个字节寄存器组成,TL0表示低8位,TH0表示高8位,最大可表示的数为65535;当左侧时钟脉冲每过来一次,计数单元加1,直到最大值,使得标志位TF0为true,触发中断系统。

下方的TR0GATEINT0是控制位,控制定时器或计数器开启。

8.1.4 中断系统

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

当中央处理机CPU正在处理某件事的时候外界发生了紧急事件请求,要求CPU暂停当前的工作,转而去处理这个紧急事件,处理完以后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断。实现这种功能的部件称为中断系统,请示CPU中断的请求源称为中断源

微型机的中断系统一般允许多个中断源,当几个中断源同时向CPU请求中断,要求为它服务的时候,这就存在CPU优先响应哪一个中断源请求的问题。通常根据中断源的轻重缓急排队,优先处理最紧急事件的中断请求源,即规定每一个中断源有一个优先级别。CPU总是先响应优先级别最高的中断请求。

image-20221120150129912

STC89C51RC/RD+系列单片机提供了8个中断请求源,它们分别是∶外部中断0(INT0)、定时器0中断、外部中断1(INT1)、定时器1中断、串口(UART)中断、定时器2中断、外部中断2(INT2)、外部中断3(INT3)。所有的中断都具有4个中断优先级。

image-20221120150315099

注意:中断的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的中断资源,例如中断源个数不同、中断优先级个数不同等等。

如果使用C语言编程,中断查询次序号就是中断号,例如:

image-20221120150330100

8.1.5 相关寄存器

寄存器是连接软硬件的媒介。在单片机中寄存器就是一段特殊的RAM存储器特殊功能寄存器sfr详见补充知识1.4节存储),一方面,寄存器可以存储和读取数据,另一方面,每一个寄存器背后都连接了一根导线,控制着电路的连接方式。寄存器相当于一个复杂机器的“操作按钮”。

定时器/计数器相关寄存器

image-20221120151001559

中断寄存器

image-20221120150937221

① TCON控制

对定时器T0或T1的控制寄存器:

image-20221120152554392

  • TF∶定时器/计数器溢出标志。T1被允许计数以后,从初值开始加1计数。当最高位产生溢出时由硬件置1,向CPU请求中断,一直保持到CPU响应中断时,才由硬件清0
  • TR:定时器的运行控制位。该位由软件置位和清零。当GATE=0,TR1=1时就允许T1开始计数,TR1=0时禁止T1计数。
② TMOD模式

定时和计数功能由特殊功能寄存器TMOD的控制位C/T进行选择,TMOD寄存器的各位信息如下表所列。

image-20221120153229129

可以看出,2个定时/计数器有4种操作模式,通过TMOD的M1和M0选择。

image-20221120153747321

③ IE中断允许

image-20221120153846419

8.2 LED间隔闪烁

效果:通过LED隔一秒闪烁一次。

初始化定时器

假设要让定时器0以模式1(16位定时)工作:

image-20221120161955762

  • TMOD
    • GATE=0,C/T=0,M1=0,M0=1
  • TCON
    • 初值计算:0~65535,每隔1μs计数加1,总共计时65535μs;64535距离计数器差值为1000,所以计时为1ms;
    • TH=64535/256,TL=64535%256;也可以使用工具生成定时器代码
    • TR0=1:允许T0开始计数

初始化中断系统

image-20221120162447931

初始化值:ET0=1(T0溢出中断控制),EA=1(总中断控制),PT0=0(优先级设为最低级,也可以不设置,默认为0)


代码实现

注:这里使用了STC-ISP的定时器计算器生成的代码

image-20221120172000328

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>

void Timer0Init(void) { //1毫秒@12.000MHz
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; // 打开定时器中断
EA = 1; // 打开cpu总中断
PT0 = 0; // 设置中断优先级
}


void main() {
Timer0Init();
while(1) {

}
}

unsigned int T0Count = 0;

// 中断子程序
void Timer0_Routine() interrupt 1 {
TL0 = 0x18;
TH0 = 0xFC;
T0Count++;
if(T0Count >= 1000) {
T0Count = 0;
P2_0 =~ P2_0;
}
}

说明:

  • 第17行定时器初始化后开始计时,main程序进入while循环。
  • 到达1ms时,定时器产生中断,cpu进入中断子程序,重新给定时器赋初值(让定时器以1ms定时),T0Count++(1000时就是1s);
  • 中断子程序执行完毕,又进入main程序继续执行(仍处于在while循环中);
  • 到达1ms时,定时器产生中断,cpu进入中断子程序,重新给定时器赋初值,T0Count++;
  • 以此类推…(当T0Count=1000时,进入if语句,刷新T0Count,让LED灯状态取反)

注意:在main函数的while循环里并未显式调用中断子程序,这一步是由定时器引起的中断让cpu自动做出的调用。

代码改造——模块化

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

void Timer0Init(void) { //1毫秒@12.000MHz
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; // 打开定时器中断
EA = 1; // 打开cpu总中断
PT0 = 0; // 设置中断优先级
}
1
2
3
4
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0Init(void);
#endif
  • main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <REGX52.H>
#include "Timer0.h"

void main() {
Timer0Init();
while(1) {

}
}

// 中断子程序
void Timer0_Routine() interrupt 1 {
static unsigned int T0Count = 0;
TL0 = 0x18;
TH0 = 0xFC;
T0Count++;
if(T0Count >= 1000) {
T0Count = 0;
P2_0 =~ P2_0;
}
}

注:T0Count需要有static修饰,详见C语言学习笔记的12.1.6 块作用域的静态变量(静态无链接)

8.3 按键控制LED流水灯

效果:通过按键控制LED流水灯的方向。

代码实现

  • Key.c和头文件
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <REGX52.H>
#include "Delay.h"

unsigned char Key() {
unsigned char keyNum = 0;
if(P3_1==0) {Delay(20); while(P3_1==0); Delay(20); keyNum=1;}
if(P3_1==0) {Delay(20); while(P3_1==0); Delay(20); keyNum=1;}
if(P3_0==0) {Delay(20); while(P3_0==0); Delay(20); keyNum=2;}
if(P3_2==0) {Delay(20); while(P3_2==0); Delay(20); keyNum=3;}
if(P3_3==0) {Delay(20); while(P3_3==0); Delay(20); keyNum=4;}

return keyNum;
}
1
2
3
4
#ifndef __KEY_H__
#define __KEY_H__
unsigned char Key();
#endif

作用:返回按下按键的序号

  • main.c
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
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.h>

// LEDMode:流水灯模式;0左移,1右移
unsigned char keyNum, LEDMode;

void main() {
P2 = 0xFE; // 点亮最低位
Timer0Init();
while(1) {
keyNum = Key();
if(keyNum) {
if(keyNum==1) {
LEDMode++;
if(LEDMode>=2) LEDMode=0;
}
}
}
}

// 中断子程序
void Timer0_Routine() interrupt 1 {
static unsigned int T0Count = 0;
TL0 = 0x18;
TH0 = 0xFC;
T0Count++;
if(T0Count >= 500) {
T0Count = 0;
if(LEDMode == 0) {
P2 = _crol_(P2, 1); // 向左循环移位
}
if(LEDMode == 1) {
P2 = _cror_(P2, 1); // 向右循环移位
}
}
}

8.4 定时器时钟

效果:LCD显示屏1s递增

代码实现

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>
#include "Timer0.h"
#include "LCD1602.h"

unsigned char Sec, Min, Hour;

void main() {
LCD_Init();
Timer0Init();
LCD_ShowString(1, 1, "CLOCK:");
while(1) {
LCD_ShowNum(2, 1, Hour, 2);
LCD_ShowString(2, 3, ":");
LCD_ShowNum(2, 4, Min, 2);
LCD_ShowString(2, 6, ":");
LCD_ShowNum(2, 7, Sec, 2);
}
}

void Timer0_Routine() interrupt 1 {
static unsigned int T0Count = 0;
TL0 = 0x18;
TH0 = 0xFC;
T0Count++;
if(T0Count >= 1000) {
T0Count = 0;
Sec++;
if(Sec == 60) {
Sec = 0;
Min++;
if(Min == 60) {
Min = 0;
Hour++;
if(Hour==24) {
Hour = 0;
}
}
}
}
}

9 串口通信

9.1 简介

串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。

单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大的扩展了单片机的应用范围,增强了单片机系统的硬件实力。

51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。

硬件电路

  • 简单双向串口通信有两根通信线(发送端TXD和接收端RXD)
  • TXD与RXD要交叉连接
  • 当只需单向的数据传输时,可以直接一根通信线
  • 当电平标准不一致时,需要加电平转换芯片

image-20221121053910748

电平标准

电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:

  • TTL电平:+5V表示1,0V表示0
  • RS232电平:-3~-15V表示1,+3~+15V表示0
  • RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)

常用通信接口

名称 引脚定义 通信方式 特点
UART TXDRXD 全双工、异步 点对点通信
I²C SCL、SDA 半双工、同步 可挂载多个设备
SPI SCLK、MOSI、MISO、CS 全双工、同步 可挂载多个设备
1-Wire DQ 半双工、异步 可挂载多个设备

此外还有:CAN、USB等。

9.2 单片机的UART

9.2.1 工作模式

STC89C52有1个UART;

STC89C52的UART有四种工作模式:

  • 模式0:同步移位寄存器
  • 模式1:8位UART,波特率可变(常用)
  • 模式2:9位UART,波特率固定
  • 模式3:9位UART,波特率可变

image-20221121054656286

9.2.2 串口参数

  • 波特率:串口通信的速率(发送和接收各数据位的间隔时间)
  • 检验位:用于数据验证,对于9位数据格式,RB8/TB8为检验位
  • 停止位:用于数据帧间隔

先发低位,再发高位:

image-20221121055114036

9.2.3 串口模式图

image-20221121055403613

  • SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器,读操作时,读出的是接收寄存器。
  • T1溢出率(T1定时器)用来控制波特率。
  • 最左侧的箭头为内部的总线。
  • SMOD用于控制波特率是否加倍

9.2.4 串口和中断

image-20221121055943122

两个中断经过了或门,如果其中有一个中断了,就可通过或门输出1中断。

9.2.5 相关寄存器

image-20221121060152417

其中:SCONSM0SM1用于确定串口的工作方式:

image-20221121060937682

9.3 串口向电脑发送数据

效果:串口每秒向电脑发送按秒递增的数据。

image-20221125113836091

代码实现

  • 可以通过STC-ISP的波特率计算器来获取串口初始化代码

image-20221125115137533

  • 串口初始化和发送模块
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 <REGX52.H>

void UART_Init() {
// 串口初始化
SCON = 0x40; // 串口发送数据 0100 0000 工作在方式1
PCON |= 0x80; // 1000 0000 波特率加倍
// 时钟1初始化
TMOD &= 0x0F;
TMOD |= 0x20;
TL1 = 0xF4;
TH1 = 0xF4;
TR1 = 1;
// 禁止定时器中断
ET1 = 0;
}

void UART_SendByte(unsigned char Byte){
// 将发送数据写入SBUF中
SBUF = Byte;
// 当缓冲满时,硬件将TI置为1并触发中断
while(TI == 0);
// 软件置为0
TI = 0;
}

TI:发送中断请求标志位。在方式0,当串行发送数据第8位结束时,由内部硬件自动置位,即TI=1,向主机请求中断,响应中断后必须用软件复位,即TI=0

1
2
3
4
5
#ifndef __UART_H__
#define __UART_H__
void UART_Init();
void UART_SendByte(unsigned char Byte);
#endif
  • 主函数
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"

unsigned char Sec;
void main() {
UART_Init();
while(1) {
UART_SendByte(Sec);
Sec++;
Delay(1000);
}
}

9.4 电脑向串口发送数据

  • SCON寄存器的第4位REN位:允许/禁止串行接收控制位。由软件置位REN,即REN=1为允许串行接收状态,可启动串行接收器RxD,开始接收信息。软件复位REN,即REN=0,则禁止接收。因此需要置为1;
  • 启动串口中断ESES=1
  • 接收中断请求标志位:在方式0,当串行接收到第8位结束时由内部硬件自动置位RI=1,向主机请求中断,响应中断后必须用软件复位,即RI=0。在其他方式中,串行接收到停止位的中间时刻由内部硬件置位,即RI=1,必须由软件复位,即RI=0。

代码实现

  • UART.c
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>

void UART_Init() {
// 串口初始化
SCON = 0x50; // 接收使能
PCON |= 0x80;
// 时钟1初始化
TMOD &= 0x0F;
TMOD |= 0x20;
TL1 = 0xF4;
TH1 = 0xF4;
TR1 = 1;
ET1 = 0;
// 打开总中断
EA = 1;
// 打开串口中断
ES = 1;
}

void UART_SendByte(unsigned char Byte){
// 将发送数据写入SBUF中
SBUF = Byte;
// 当缓冲满时,硬件将TI置为1并触发中断
while(TI == 0);
// 软件置为0
TI = 0;
}
  • main.c
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 <REGX52.H>
#include "Delay.h"
#include "UART.h"


void main() {
UART_Init();
while(1) {

}
}

// 中断子程序
void UART_Routine() interrupt 4 {
// 当接收中断时
if(RI == 1) {
// 将SBUF的数据写入P2
P2 = ~SBUF;
// 将电脑的数据又发回电脑
UART_SendByte(SBUF);
// 软件复位
RI = 0;
}
}

9.5 波特率计算

定时器1的初始值为:

1
2
3
4
5
6
TL1 = 0xF4;
TH1 = 0xF4;

// 换算为十进制:
TL1 = 244;
TH1 = 244;

则计数256 - 244 = 12个数就溢出一次(配置的是定时器1的8位自动重装,因此到达8位最大值256时就溢出);

对于11.0592MHz的晶振,设定在12T的模式下(12分频)时,实际的晶振频率为0.9216MHz,所以计数1次需要1/0.9216μs(MHz对应μs),大约为1.085069444444444μs,则计数12次为13.02083333333333μs,对应的溢出频率为0.0768MHz

如果设置了波特率加倍(SMOD=1,即没有除以2),则T1溢出率(0.0768MHz)到达发送/接收控制器的频率为0.0768MHz/16=0.0048MHz,即4800Hz,即为波特率。

10 LED点阵屏

10.1 简介

LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯珠亮灭来显示文字、图片、视频等。LED点阵屏广泛应用于各种公共场合,如汽车报站器、广告屏以及公告牌等。

LED点阵屏分类:

  • 按颜色:单色、双色、全彩
  • 按像素:8*816*16等(大规模的LED点阵通常由很多个小点阵拼接而成)

显示原理

  • 单色LED点阵

image-20221125134347451

image-20221125134341203

  • 双色LED点阵

image-20221125134403629

LED点阵屏的结构类似于数码管,只不过是数码管把每一列的像素以“8”字型排列而已;

LED点阵屏与数码管一样,有共阴和共阳两种接法,不同的接法对应的电路结构不同;

LED点阵屏需要进行逐行或逐列扫描,才能使所有LED同时显示;

电路图

image-20221125135400916

  • DPa~DPh选择的是从下到上的行
  • P00~P07选择的是从右到左的列

74HC595

74HC595是串行输入并行输出的移位寄存器(串转并),可用3根线输入串行数据,8根线输出并行数据,多片级联后,可输出16位、24位、32位等,常用于IO口扩展。

image-20221125141328039

  • OE非:74HC595芯片使能端,非表示低电平使能,因此需要将J24跳线帽接在2和3号位,让OE端接地使能;
  • RCLK:上升沿锁存,接P35口;P35口由0转1(上升沿)时,将串行输入缓存送至并行输出端(QA至QH口);
  • SRCLK:上升沿移位,接P36口;P36口由0转1(上升沿)时,将输入进行移位;
  • SER:串行输入口,接P34口
  • QA~QH:并行输出口
  • QH':用于多片级联,接在下一片74HC595的串行输入口SER

芯片内部图如下:

image-20221125141806205

10.2 点阵显示图形

效果:在LED点阵屏上显示对角线

代码实现

  • MatrixLED
1
2
3
4
5
6
7
8
9
10
#ifndef __MATRIXLED_H__
#define __MATRIXLED_H__

sbit RCK = 0xB5; // 上升沿锁存
sbit SCK = 0xB6; // 上升沿移位
sbit SER = 0xB4; // 串行输入
void _74HC595_WriteByte(unsigned char byte);
void MatrixLED_ShowColumn(unsigned char Col, Data);

#endif
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 "MatrixLED.h"
#include "Delay.h"

void _74HC595_WriteByte(unsigned char byte) {
// 这里左侧SER是1位寄存器,右侧是一个8位数据
// 8位赋值给1位的规则是:非零即1
// 假设取出byte的第8位
// 如果第8位是1,则与后的结果为1000 0000,非零,则SER=1
// 如果第8位是0,则与后的结果为0000 0000,则SER=0
// 与操作起到了掩码的作用
unsigned char i =0;
for(i = 0; i < 8; i++) {
SER = byte&(0x80 >> i);
// 给一个上升沿进行移位
SCK = 1;
// 复位
SCK = 0;
}
// 给一个上升沿进行锁存输出
RCK = 1;
// 复位
RCK = 0;
}

// Col: 74HC595选中的列
// Data:控制LED亮灭的数据
void MatrixLED_ShowColumn(unsigned char Col, Data) {
_74HC595_WriteByte(Data);
P0 = ~(0x80>>Col);
// 延时和消影
Delay(1);
P0 = 0xFF;
}
  • main
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 "MatrixLED.h"

void main() {
// 复位
SCK = 0;
RCK = 0;
// 死循环不断扫描
while(1) {
MatrixLED_ShowColumn(0, 0x80);
MatrixLED_ShowColumn(1, 0x40);
MatrixLED_ShowColumn(2, 0x20);
MatrixLED_ShowColumn(3, 0x10);
MatrixLED_ShowColumn(4, 0x08);
MatrixLED_ShowColumn(5, 0x04);
MatrixLED_ShowColumn(6, 0x02);
MatrixLED_ShowColumn(7, 0x01);
}
}

11 实时时钟

11.1 简介

12 蜂鸣器

12.1 简介

蜂鸣器是一种将电信号转换为声音信号的器件,常用来产生设备的按键音、报警音等提示信号。

蜂鸣器按驱动方式可分为有源蜂鸣器和无源蜂鸣器

  • 有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定
  • 无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音

一般的,蜂鸣器电路图表示:

image-20221231195653736

12.2 驱动电路

  • 三极管驱动

image-20221231200608160

例如上图中,通过NPN型三极管Q1进行驱动。当基极接高电平时,三极管导通,此时蜂鸣器工作,反之不工作。

  • 集成电路驱动

image-20221231201026822

上图,蜂鸣器负极引出引脚为BEEP,该引脚接至ULN2003芯片的12号引脚,如下图所示:

image-20221231201137354

其中ULN2003是一个单片高电压、高电流的达林顿晶体管阵列集成电路,主要用于继电器驱动器、字锤驱动器、灯驱动器、显示驱动器、线路驱动器和逻辑缓冲器。

由上图可知,通过P25IO口取反控制OUT5的输出,进而控制蜂鸣器的工作状态,即P25口输入1,取反为0输出到OUT5,使得蜂鸣器工作,输入0则不工作。

12.3 音符和频率

image-20230101101735904

12.4 蜂鸣器播放提示音

效果:按下独立按键,蜂鸣器播放提示音。

代码实现

  • Buzzer.h
1
2
3
4
#ifndef __BUZZER_H__
#define __BUZZER_H__
void Buzzer_Time(unsigned int ms);
#endif
  • Buzzer.c
1
2
3
4
5
6
7
8
9
10
11
12
#include <REGX52.H>
#include "Delay.h"

sbit Buzzer = P2^5;

void Buzzer_Time(unsigned int ms) {
unsigned int i;
for(i = 0; i < ms; i++) {
Buzzer = !Buzzer;
Delay(1);
}
}
  • main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <REGX52.H>
#include "Delay.h"
#include "Key.h"
#include "Nixie.h"
#include "Buzzer.h"

unsigned char KeyNum;
unsigned int i;

void main() {
Nixie(1, 0);
while(1) {
KeyNum = Key();
if(KeyNum) {
Buzzer_Time(1000);
Nixie(1, KeyNum);
}
}
}

12.5 蜂鸣器播放音乐

13 AT24C02(I2C)

补充知识

1 单片机原理(基础)

主要参考资料:电光耗子皮卡皮

1.1 单片机的三大发展阶段

  • SCM:(Single Chip Micro computer),单片微型计算机阶段。寻求最佳单片状态的嵌入式体系结构。最具代表性的产品是Intel的8位MCS-51系列单片机。

image-20221117195828104

  • MCU:(Micro Controller Unit),微控制器阶段。集成各种外围电路与接口电路的能力。

image-20221117195835031

  • SOC:(System on Chip),SoC是一个有专用目标的集成电路,包含完整系统并嵌入软件的全部内容。

image-20221117195842252

1.2 基础框架结构

MCS-51为例。

SCM:将通用微计算机基本功能部件集成在一块芯片上构成的一种专用微机系统。

image-20221117200548494

SCM = CPU+OSC+ROM+RAM+I/O+INTERRUPT+TIME/COUNTER+UART+BUS EXTENDER

  • 1个8位中央处理器CPU
  • 4KB片内程序存储器ROM,相当于计算机的硬盘
  • 256字节数据存储器RAM,相当于计算机的内存
  • 4个8位并行I/O
  • 5个中断源INT
  • 2个16位定时器/计数器T/C
  • 1个全双工串行口UART
  • 可扩展64KB ROM和RAM 的总线

1.3 CPU

CPU是单片机的核心。CPU(中央处理器)分为控制器和运算器两部分。

1.3.1 控制器
  • 控制器的用途:统一指挥和控制各单元协调工作
  • 控制器的任务: 从ROM中取出指令—>译码—>执行指令
  • 控制器的组成:
    • 程序计数器(Program Counter - PC) : 指向下一条指令的首地址,ROM存储单元的地址指针(引导程序运行),可修改,让程序跳转运行复位时:PC初值=0 --> 复位后程序从0开始运行
    • 指令寄存器(nstruction Register - IR) : 8位寄存器,暂存指令,等待译码
    • 指令译码器(Instruction Decoder - ID) : 将指令寄存器的指令进行译码,转为可执行的电信号再通过定时器电路将其执行
    • 数据指针寄存器 (Data Pointer -DPTR) : 指向ROM或RAM存储单元的地址指针(引导数据传送),DPTR是专门为16位(或者小于16位)的外部RAM或者外部ROM准备用于读取和写入的。
1.3.2 运算器

1.4 存储

1.4.1 存储空间
  • 普林斯顿结构:RAM和ROM统一编址
  • 哈佛结构:RAM和ROM独立编址,51单片机采用哈佛结构。

因此单片机共有4个物理存储空间:片内RAM,片内ROM,片外RAM和片外ROM,3个逻辑存储空间:片内片外ROM统一编址。

image-20221117201852382

1.4.2 程序存储器ROM

作用:存放程序,表格或常数,掉电不丢失。

  • EA = 1时,4 KB以内的地址在片内ROM,大于4KB的地址在片外ROM中 (图中折线),两者共同构成64KB空间。
  • EA = 0时,片内ROM被禁用,全部64KB地址都在片外ROM中 (图中直线)

image-20221117202214977

1.4.3 数据存储器RAM

地址

在单片机、计算机系统里面,内存为了定位(为了能够找到每一个内存空间),把每一个空间都编上一个序号,那么内存编号的那个号码我们就叫地址,地址编号通常以十六进制的数字表示。

存储器系统中,给字节编号就叫字节地址,给二进制位编号就叫位地址。


image-20221117202937409

  • ①区:共有32个字节地址:00H ~ 1FH,每个字节地址都有一个寄存器名称(R0 ~ R7),32个字节地址被分为4组(第0~第3组);CPU在一个时刻,只能选一组工作寄存器组,通过程序状态字寄存器PSWRS1RS0来控制选组,不设置的话默认选择第0组,剩下的三组则用作数据存储器

  • ②区:共有16个字节地址:20H ~ 2FH,合计16 * 8 = 128个位地址,可以用字节地址或位地址进行寻址。

image-20221117203628428

  • ③区:共有80个字节地址,没有位地址,也没有寄存器名称,此区可作为堆栈区和中间数据存储区使用。

  • 特殊功能寄存器区SFRSpecial Function Register,承担着单片机内部资源的管理工作。

image-20221117204222068

示例

直接寻址:通过把地址定义一个名称后,修改在该地址的内存空间的内容。

1
2
3
4
5
Sbit P0_0 = 80H; // 把80H编号的位空间取一个名称P0_0,定义一个名称P0_0
P0_0 = 1; // 把1赋值给P0_0,即赋值给80H位空间
//---------------
Sfr P0 = 80H; // 把80H编号的字节空间去一个名称P0
P0 = 10010101b; // 赋值

Sfr:特殊寄存器字节定义指令

Sbit:位地址定义指令

1.5 单片机引脚

image-20221117204915019

  • P1.0 ~ P1.7:1-8脚,准双向IO口,P1端口
  • P3.0 ~ P3.7:10-17脚,带复用功能,P3端口
  • P2.0 ~ P2.7:21-28脚,高八位数据总线,P2端口
  • P0.0 ~ P0.7:39-32脚,低八位数据总线,P0端口

引脚分类

  • 电源及晶振引脚(4)
  • 控制引脚(4)
  • 端口引脚(32)

image-20221117205038651

1.6 IO口

1.6.1 基本元器件
① 二极管

具有单向导电性能, 即给二极管阳极加上正向电压时,二极管导通。 当给阳极和阴极加上反向电压时,二极管截止。 因此,二极管的导通和截止,则相当于开关的接通与断开。二极管就是由一个PN结加上相应的电极引线及管壳封装而成的。

  • N型半导体:硅原子外层带4个负电子,多个硅原子连接,外层形成稳定的8电子结构,因此纯净的硅晶体不导电。为了能使其导电,在硅晶体中掺杂了磷原子,磷原子多余的一个电子成为自由电子,使其能够导电。因为自由电子带负电,因此称为N型半导体,即Negative

image-20221118144029673

  • P型半导体:在硅晶体中掺杂硼原子,硼原子周围缺少一个电子,形成空穴,吸引负电子,对外显正电,因此称为P型半导体,即Postive

image-20221118144248890

  • PN结:采用不同的掺杂工艺,通过扩散作用,将P型半导体与N型半导体制作在同一块半导体(通常是硅或锗)基片上,在它们的交界面就形成空间电荷区称为PN结。
  • 由P区引出的电极称为阳极,N区引出的电极称为阴极。因为PN结的单向导电性,二极管导通时电流方向是由阳极通过管子内部流向阴极。

image-20221117212032346

电场方向和电子的受力方向相反:

image-20221118144451523

当正极接P端时,自由电子可以和空穴复合,形成电流。反之则不行。正(电源正极)接正(P端Positive),负接负

image-20221118144728861
② 三极管

用电控制的开关分为三极管和mos管,实际上后两者的功能不仅限于电路的开闭,还有放大等作用。

三极管:通过电流控制电路开闭

  • NPN:NPN型三极管是指由两块N型半导体中间夹着一块P型半导体所组成的三极管。可以看成两个二极管的P端连接在一起。

image-20221118144922412

image-20221117212251657

三极管由两个PN结构成,共用的一个电极成为三极管的基极(用字母B表示——B取自英文Base),其他的两个电极分别称为集电极(用字母C表示——C取自英文Collector,收集)和有箭头的发射极(用字母E表示—— E取自英文Emitter,发射)。基区和发射区之间的结成为发射结,基区和集电区之间的结成为集电结。

集电区掺杂浓度(自由电子数量多)最高,发射区次之,基区(空穴数量较少)最低且最薄。

不论将电源接至集电极或发射极,三极管均不能导通。

image-20221118145237849

如果给BE极也加上电源(正接正,负接负):

image-20221118145804811

发射区的电子向基区的空穴复合,但是只有少部分能够复合成功,形成基极电流B->E

image-20221118150238629

而大部分负电子被吸引到了集电区,形成集电极电流C->E,也就是三极管的输出电流。

image-20221118150312775

基区做得很薄,是为了让发射区的电子更容易进入集电区。

功能:用 B-E 的电流 (IB) 控制 C-E 的电流 (IC),如果B-E没有电流,则C-E断;如果B接高,则B-E有电流,C-E通。

  • PNP:用 E-B 的电流 (IB) 控制 E-C 的电流(IC),E极电位最高,且正常放大时通常C极电位最低,即VC<VB<VE;如果B接低,则通。

image-20221117212904578

③ mos管

mos管:通过电压控制电路开闭。全称金属氧化物半导体场效应晶体管(Metal Oxide Semiconductor Field Effect Transistor),简写为MOSFET,再次简写为MOS。

  • N沟道mos管:由NPN构成,给两块N型半导体引出两个金属电极,分别作为mos管的漏极和源极。如果像下面这样直接连接并不会导通,因为NPN实际上构成了两个方向相反的二极管。

image-20221118151507156

电路符号:图中箭头的方向是N沟道电子的流动方向。

image-20221118151611529

为了让其导通,在P区添加了一层很薄的二氧化硅绝缘层,上覆盖一个金属板,形成栅极。给栅极也接上电(正极),栅极就会排斥空穴,吸引电子。电压越大,吸引的电子就越多,直到累计到一定数量时,在两个N型半导体之间形成了N沟道。此时可以将其看成一整块N型半导体。

image-20221118152434502

mos管特性:

  1. 栅极的输入阻抗非常高,因为有红色的绝缘层的存在,造成其输入电阻可达上亿欧姆。所以它的输入几乎不取电流。
  2. 栅极容易被静电击穿

总结:漏极D接正极,源极S接负极,栅极G正电压时,导电沟道建立

  • P沟道mos管:漏极D接负极,源极S接正极,栅极G负电压时,导电沟道建立

image-20221117213559641

口诀:P下N上:NPN和Nmos,通过高电位导通;PNP和Pmos通过低电位导通

image-20221117211502628

④ 三态门

image-20221117214204825

E接高电平导通,相当于NPN型的三极管。N上

⑤ 触发器

触发器具有记忆存储功能,由门电路组成。这里简单介绍D触发器。

  • 正边沿D触发器
    • D:输入;Q输出;CLK:时钟脉冲
    • 只有在时钟脉冲CLK上升沿到来的时刻,才采样D端的输入信号,来改变Q的输出状态,而在其他时刻,D和Q是隔离的。

image-20221117214641356

  • 负边沿D触发器:

image-20221117214659602

D触发器广泛用于数字信号的锁存输出

1.6.2 拉电流和灌电流

拉电流和灌电流是衡量电路输出驱动能力(注意:拉、灌都是对芯片输出端而言的,所以是驱动能力)的参数。

由于数字电路的输出只有高、低(0,1)两种电平值,高电平输出时,一般是输出端对负载提供电流,其提供电流的数值叫“拉电流”;

image-20221118163354353

低电平输出时,一般是输出端要吸收负载的电流,其吸收电流的数值叫“灌(入)电流”。

image-20221118163447989

对于大多数的逻辑电路(数字电路和单片机),输出拉电流的能力较弱,一般只有几毫安(<5mA),而输出灌电流的能力较强,一般为5~10mA

一般情况下,在需要一定电流驱动的情况下,通常将驱动负载使用“灌电流”比较合适(例如LED),对于只是提供“开关信号”或者基本不需要电流驱动的情况下,使用“拉电流”比较合适。

1.6.3 上拉和下拉

上拉电阻和下拉电阻:上拉电阻和下拉电阻都是电阻元器件,所谓上拉电阻就是接电源正极下拉的就是接负极或地

上拉就是将不确定的信号通过一个电阻钳位在高电平,上拉电阻其实和上拉电压并没有太大关系,它的主要作用是补全电路,起限流和负载的作用。

下拉同理,也是将不确定的信号通过一个电阻钳位在低电平。

上拉电阻解析

image-20221119110256333

如上图所示,当上拉电阻位于单片机内部时。若B没有电流,则三极管断开,IO输出位高电平(该端口没有接负载,为开路电压;或者没有电流,压降为0);若B有电流,则三极管导通接地,相当于IO口直接接地,输出低电平。

将上拉电阻移动到单片机外部,此时单片机会将IO端口设计成开漏输出或开集电极状态。所以芯片的内部已经集成了上拉电阻,则外部电路不需要添加上拉电阻。上拉电阻通常用在IO的输出口上。

image-20221119111510232

总结上拉电阻起到的作用:

  • 在三极管导通的时候(单片机内部控制电路输出高电平),它起到一个虚假的负载,防止因为正负极直接短路造成的线材发热或烧毁三极管,因为此时电源直接通过三极管接地。
  • 在三极管不通的时候,为后面的用电器提供限流的保险服务,在实际应用中,会防止后面的用电器功率或负载过大,烧毁前面的供电部分电路。这个作用跟保险丝几乎一样。
1.6.4 输出模式
① 推挽输出

image-20221118231911963

上面的三极管是NPN型三极管,下面的三极管是PNP型三极管,请留意控制端、输入端和输出端。

当Vin电压为V+时,上面的N型三极管控制端有电流输入,Q3导通,于是电流从上往下通过,提供电流给负载。

image-20221118232011071

经过上面的N型三极管提供电流给负载(Rload),这就叫「」,此时的电流就是拉电流。

当Vin电压为V-时,下面的三极管有电流流出,Q4导通,有电流从上往下流过。

image-20221118232047658

经过下面的P型三极管提供电流给负载(Rload),这就叫「」,此时的电流为灌电流。

② 开漏输出

首先介绍开集输出:开集的意思,就是集电极C一端什么都不接,直接作为输出端口。

image-20221118232236171

如果要用这种电路带一个负载,比如一个LED,必须接一个上拉电阻:当Vin没有电流,Q5断开时,LED亮。当Vin流入电流,Q5导通时,LED灭。

image-20221118232259208

开漏电路,就是把上图中的三极管换成场效应管(MOSFET)。

1.6.5 IO口结构

STC89C51RC/RD系列单片机所有I/O口均有3种工作类型∶

  • 准双向口/弱上拉(标准8051输出模式)
  • 仅为输入(高阻)
  • 开漏输出功能。

STC89C51RC/RD+系列单片机的P1/P2/P3上电复位后为准双向口/弱上拉模式P0口上电复位后是开漏输出。P0口作为总线扩展用时,不用加上拉电阻,作为I/O口用时,需加10K-4.7K上拉电阻。

① 准双向P1口

准双向P1口:8个相同结构电路,组成P1特殊功能寄存器(90H

P1_n = 1个锁存器(D触发器) + 1个场效应管驱动器(N沟道mos管) + 2个三态门缓冲器

image-20221118233342247

  • 单片机执行向P1口写数据指令时,P1口工作于输出方式,例如P1 = 0x12。如下图,内部总线写入1,经过锁存器的Q非口出来为0,此时mos管截止,电源vcc经过R上拉电阻向P1口出,为高电平,注意读锁存器为0,使三态门2不放行。

image-20221118235500246

  • 当单片机执行从P1口读取数据并存到变量val指令时,P1口工作于读引脚方式,例如val = P1。但是mos管如果处于导通的情况下,会出现P1口读取不准确,可能走mosV接地那部分电路,因此要在读引脚前先执行一条写1指令强迫V截止,引脚P1电平便不会读错,所以P1口作为输入口是有条件的,要先让MOS管截止。而作为输出口是无条件的,因此,P1口被称为准双向口。

image-20221118234911073

  • 当单片机执行读-改-写类指令时,P1口工作于读锁存器方式。如P1++;。Q端电平经过三态门2读入内部总线(得到P1的原始值),然后在运算器中进行+1运算,然后结果重新写到Q端(下一个上升沿,同时也输出到P1.n)。
  • 读锁存器与读引脚的效果是不同的,读锁存器是为了获得前次的锁存值,而读引脚则是为了获得引脚上的当前值。
② 复用口P3口

P3口:8个相同结构电路,组成P3特殊功能寄存器(B0H

P3.n = 1个锁存器 + 1个mos + 2个三态门 + 1个与非门

P3口比P1口多了一个与非门,且与非门接在Q输出端上。

image-20221118235844906

  • 通用IO口:当第二输出功能端置为1时,那么与非门相当于非门,即Q变为Q非,然后接在mos管上,此时P3功能与P1功能相同,作为通用IO口

image-20221119000231733

  • 第二输出功能:当锁存器置为1时,那么与非门相当于非门,此时通过第二输出功能来输出。例如下图,第二输出功能为1,则输出为1;

image-20221119000746072

  • 第二输入功能:当锁存器置为1,第二输出功能置为1,那么与非门输出0,P3引脚输入则走第二输入功能引脚。

image-20221119001046618

③ 高八位拓展地址总线P2口

P2口:8个相同结构电路,组成P2特殊功能寄存器(A0H

P2 = 1个锁存器 + 2个三态门 + 1个mos + 两路开关 + 非门

image-20221119001248983

  • 通用IO:系统将控制器置为0,两路开关打到锁存器端,Q经过非门为Q非,此时与P1口相同。

  • 控制端置为1,用作外部地址总线(16位地址总线的高8位)

④ 低八位拓展地址总线P0口

P0 = 1个锁存器 + 2个三态门 + 2个mos + 两路开关 + 与门 + 非门

image-20221119001614464

  • 通用IO:系统会将控制端置0,两路开关打到锁存器端,而V2连接的与门一端由控制端输入 (0与任何数都等于0),所以一直高阻态,因此当作输出时需要自行接上拉电阻。数据输入时,CPU自动使地址/数据端一1,控制端一0,故分时复用方式为无条件的真双向口

总结

P0-P3 口都可作为准双向通用IO口,其中只有 P0 口需要外接上拉电阻;在需要扩展片外设备时,P2 口可作为其地址线接口,P0 口可作为其地址线/数据线复用接口,此时它是真正的双向口。

1.7 时钟电路

单片机需要时钟信号才能正常运行,时钟信号是脉冲信号的一种,周期固定,占宽比1:1的矩形脉冲波。时钟电路就是通过其他元器件综合来形成时钟信号(单片机的心跳)来提供给单片机,那么据上述可知,提供给单片机时钟信号即可让单片机运行起来。

下面是两种实现方式:

  • 晶振时钟:通过外部晶振电路来获取时钟信号,电容用于起振
  • 脉冲时钟:外部从XTAL2引脚输入时钟信号

image-20221120134209509

1.8 复位

在微机系统中,为了保证电路的稳定可靠运行,复位电路是必不可少的一部分,复位电路的首要功能是通电复位。使单片机恢复原始默认状态的操作,通俗来讲,复位就是把你写的程序从第一步开始运行,类似一键重播功能。

image-20221120134621675

P0~P3复位默认是FFH,也就是默认全高电平