# 单片机原理

# 8051 单片机

8051 单片机是一种经典的 8 位单片机,由英特尔(Intel)公司在 1980 年推出。它是一种基于哈佛架构的单片机,具有内部 ROM、RAM、IO 等外设,同时还支持多种外部接口,如串口、定时器、中断等。 8051 单片机是一种通用型微控制器,广泛应用于各种嵌入式系统和电子设备中。

8051 单片机的特点和优势主要包括以下几个方面:

  • 成本低廉:由于 8051 单片机集成度高、制造工艺成熟,因此价格相对较低,非常适合于大批量生产和应用。
  • 易于学习和使用:由于 8051 单片机使用的指令集比较简单,因此上手和学习比较容易,即使是没有编程经验的人也可以很快掌握。
  • 可扩展性强:8051 单片机的设计具有很强的可扩展性,支持多种外部接口,可以实现与其他硬件设备的连接和通信。

生产 51 单片机的厂商有很多,所以 51 单片机的型号也很多,市面上有许多单片机都是从 51 内核扩展出来的单片机,这些单片机的使用都是相通的,只要学会一款 8051 单片机的应用,其他单片机也就可以上手使用。所以之后的内容都以 STC 公司生产的STC89C52RC这款单片机为例

这是 STC89C52 单片机的各个引脚:

8051单片机

STC89C52 单片机有 40 个引脚,具体各个引脚的作用如下:

  • P0 端口:有 8 个引脚,P0.0-P0.7,是通用的输入输出口
  • P1 端口:有 8 个引脚,P1.0-P1.7,是通用的输入输出口
  • P2 端口:有 8 个引脚,P2.0-P2.7,是通用的输入输出口
  • P3 端口:有 8 个引脚,P3.0-P3.7,是通用的输入输出口,同时具有第二功能。
    • P3.0 和 P3.1(RXD 和 TXD)可用于串行通信
    • P3.2 和 P3.3(INT0 和 INT1)可用于外部中断 0 和 1 的触发
    • P3.4 和 P3.5(T0 和 T1)可用于定时器 0 和 1 的计数和控制
    • P3.6 和 P3.7(WR 和 RD)可用于外部存储器的控制

以上四个端口默认为输出状态

  • RST:复位引脚,低电平有效
  • RXD:为接收数据的引脚
  • TXD:为发送数据的引脚

RXD 口和 TXD 口是单片机的串口通信端口,可以连接外部串口设备。

  • INT0:外部中断 0 输入
  • INT1:外部中断 1 输入
  • WR:外部存储器写使能控制引脚
  • RD:外部存储器读使能控制引脚

RD 和 WR 是单片机与外部存储器进行数据读取和写入时使用的控制端口

  • XTAL1:晶体振荡器输入引脚
  • XTAL2:晶体振荡器输出引脚

XTAL1 和 XTAL2 用于连接外部晶振时使用的端口

  • ALE:地址锁存器使能控制引脚
  • EA:EA 引脚为片内程序存储器 ROM 的启动控制端。
    • EA=0 时,CPU 将从片内 ROM 地址 0000H 开始执行程序。
    • EA=1 时,CPU 将从外部存储器或 EPROM 中的 0000H 开始执行程序
  • PSEN:PSEN 引脚为程序存储器片选端。
    • 当 PSEN=0 时,程序存储器被选中,CPU 从程序存储器中读取指令
    • 当 PSEN=1 时,CPU 从数据存储器中读取数据
  • T0 口和 T1 口是单片机的定时器输入/输出端口,可以连接外部定时器/计数器。
  • GND:电源接地/负极
  • VCC:电源正极

# 单片机有什么用?

单片机是一种微型计算机,我们可以通过单片机来控制电路从而制造出各种各样的电子产品。例如,控制和监测各种设备和系统,用于各种应用,如智能家居、自动化控制、汽车电子、医疗仪器、航空航天、机器人技术等。

# 如何使用单片机去控制电路?

我们需要给单片机编写程序从而实现对电路的控制,一般我们要先使用软件Keil (opens new window)编写程序然后把它打包成.hex格式的文件,这是一个 16 进制的文件,这里面包含了地址和数据,再通过软件stc-isp (opens new window)把程序(.hex 格式的文件)烧录到单片机中

好了,现在我们已经对单片机有了一个大概的了解,就让我们上手使用单片机吧,先从点亮一个 LED 灯开始! (在学习单片机前你需要有C 语言的基础,至少能读懂它)

# 51 单片机程序烧录

8051 单片机程序的烧录可以通过编程器来实现。编程器可以将编写好的程序代码烧录到单片机的 Flash 或 EEPROM 中,以便单片机在运行时能够执行程序代码。

8051 单片机程序烧录的一般步骤

  1. 准备编程器:选购合适的编程器,并连接到计算机和单片机。这里我选择使用 USB-TTL 转换器,它可以将计算机的 USB 接口转换为串口接口
  2. 验证程序代码:对编写好的程序代码进行校验,确保程序代码正确无误。
  3. 准备程序代码:将编写好的程序代码编译成单片机可执行文件,一般为 HEX 或 BIN 格式。
  4. 连接单片机:将编程器与单片机连接,以 USB-TTL 为例
  • USB-TTL 转换器的 TXD 引脚连接到单片机的RXD 引脚上,用于将数据从计算机发送到单片机。
  • USB-TTL 转换器的 RXD 引脚连接到单片机的TXD 引脚上,用于将数据从单片机发送回计算机。
  • USB-TTL 转换器的 GND 引脚连接到单片机的GND 引脚上,用于单片机供电和信号共地。
  • USB-TTL 转换器的 5V 引脚连接到单片机的VCC 引脚上,用于单片机供电提供电源和信号共地。
  1. 烧录程序代码:使用烧录软件将程序代码写入单片机的 Flash 或 EEPROM 中。由于我使用的是 STC 的单片机,所以这里我使用软件 stc-isp ,不同单片机有不同的烧录软件或烧录方法,根据实际情况操作

注意

以上只是单片机程序烧录的大致操作,先连接好单片机和烧录工具,在之后的实践中我们会进行具体的操作

# 什么是 Flash?什么是 EEPROM?

Flash 和 EEPROM 是常见的非易失性存储器件,都用于储存程序代码和数据。

Flash 全称为"快闪存储器"(Flash Memory),与 ROM(Read Only Memory)类似,但它可以在应用程序运行时被重新编程,而 ROM 烧录后就只能读取,不能修改。它的容量通常比 EEPROM 大,但编程和擦除速度比 EEPROM 慢。Flash 存储器适用于需要在运行时对程序代码进行更新的应用程序,例如固件更新、嵌入式系统和移动设备等。

EEPROM 全称为"电可擦可编程只读存储器"(Electrically Erasable Programmable Read-Only Memory),容量较小,编程和擦除速度比 Flash 存储器快。它适用于需要存储少量数据的应用程序,例如存储校准参数、配置文件、密码和用户数据等。

它们之间的区别

  • 容量:Flash 存储器的容量通常比 EEPROM 大,因为 Flash 存储器通常用于存储程序代码和大量数据,而 EEPROM 存储器通常用于存储少量数据。
  • 速度:EEPROM 存储器的编程和擦除速度比 Flash 存储器快,但 Flash 存储器通常具有更快的读取速度。
  • 使用方式:Flash 存储器和 EEPROM 存储器的使用方式和接口有所不同。Flash 存储器通常使用块擦除和扇区擦除等方式进行擦除操作,而 EEPROM 存储器通常使用字节级擦除和编程操作。
    • 块擦除:一次性擦除整个存储器的数据
    • 扇擦除:只擦除存储器的一部分数据,以扇区为单位
    • 字节级擦除:一个字节一个字节的擦除全部数据
  • 寿命:Flash 存储器和 EEPROM 存储器的寿命都有限,但它们的寿命与使用方式和擦写次数有关。通常来说,EEPROM 存储器的寿命要比 Flash 存储器长。

# 实践:用单片机点亮 LED 灯

  1. 先在 keil 中创建一个项目,创建项目时选对单片机的型号(如果你和我一样使用的是 STC89C52 这款单片机,你可以选择 AT89C52,这两款单片机只是生产厂商不同,使用时都是一样的)在项目中新建 main.c 文件,并把以下代码写入文件。文件应添加在 Source Group 1
#include "reg52.h"//引入单片机头文件

sbit LED = P1^0;//给P1.0引脚命名为LED
void main()
{
	LED =0;//把引脚P1.0设为低电平
}
  • 0表示低电平1表示高电平
  • LED 正极接VCC,负极接引脚P1.0
  • VCC是高电平,电流从高电平流向低电平,只要把引脚P1.0设为低电平时,LED 灯亮
  1. 打开 Options for Target —> Output —> 勾选 Create HEX File
  2. 点击 Translate 查看代码是否有误,如果正常就点击 Build 构建
  3. 构建完成后你的项目文件夹 Objects 中会出现一个 .hex 格式的文件
  4. 打开软件 stc-isp ,通过烧录器连接单片机和电脑,选中 .hex 格式的文件并烧录/下载到单片机上

链接

如果初学者觉得文章难懂推荐看该视频进行学习

十分钟一集 51 单片机,手把手教你敲代码 _ 阿熊话太多 (opens new window)

# 什么是单片机头文件?为什么要引入这个文件?

  • reg51.hreg52.h 是一种用于 8051 系列单片机编程的头文件(header file)
  • 头文件中包含了 8051 系列单片机的寄存器定义和位定义,以便程序员在编写程序时可以更方便地访问这些寄存器和位,以实现对单片机的各种控制和操作。
  • 通过使用这些头文件中定义的宏,程序员可以轻松地访问单片机的串口、定时器、中断等功能,从而实现更加复杂的控制和操作。
  • 头文件 reg51.h 用于型号为 80C51 和 80C31 的微控制器,头文件 reg52.h 用与型号为 80C52 和 80C32 的微控制器
reg51.h
/*--------------------------------------------------------------------------
REG51.H
Header file for generic 80C51 and 80C31 microcontroller.
Copyright (c) 1988-2002 Keil Elektronik GmbH and Keil Software, Inc.
All rights reserved.
--------------------------------------------------------------------------*/
#ifndef __REG51_H__
#define __REG51_H__
/* BYTE Register */
sfr P0 = 0x80;
sfr P1 = 0x90;
sfr P2 = 0xA0;
sfr P3 = 0xB0;
sfr PSW = 0xD0;
sfr ACC = 0xE0;
sfr B = 0xF0;
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 IE = 0xA8;
sfr IP = 0xB8;
sfr SCON = 0x98;
sfr SBUF = 0x99;
/* BIT Register */
/* PSW */
sbit CY = 0xD7;
sbit AC = 0xD6;
sbit F0 = 0xD5;
sbit RS1 = 0xD4;
sbit RS0 = 0xD3;
sbit OV = 0xD2;
sbit P = 0xD0;
/* TCON */
sbit TF1 = 0x8F;
sbit TR1 = 0x8E;
sbit TF0 = 0x8D;
sbit TR0 = 0x8C;
sbit IE1 = 0x8B;
sbit IT1 = 0x8A;
sbit IE0 = 0x89;
sbit IT0 = 0x88;
/* IE */
sbit EA = 0xAF;
sbit ES = 0xAC;
sbit ET1 = 0xAB;
sbit EX1 = 0xAA;
sbit ET0 = 0xA9;
sbit EX0 = 0xA8;
/* IP */
sbit PS = 0xBC;
sbit PT1 = 0xBB;
sbit PX1 = 0xBA;
sbit PT0 = 0xB9;
sbit PX0 = 0xB8;
/* P3 */
sbit RD = 0xB7;
sbit WR = 0xB6;
sbit T1 = 0xB5;
sbit T0 = 0xB4;
sbit INT1 = 0xB3;
sbit INT0 = 0xB2;
sbit TXD = 0xB1;
sbit RXD = 0xB0;
/* SCON */
sbit SM0 = 0x9F;
sbit SM1 = 0x9E;
sbit SM2 = 0x9D;
sbit REN = 0x9C;
sbit TB8 = 0x9B;
sbit RB8 = 0x9A;
sbit TI = 0x99;
sbit RI = 0x98;
#endif
reg52.h
/*--------------------------------------------------------------------------
REG52.H
Header file for generic 80C52 and 80C32 microcontroller.
Copyright (c) 1988-2002 Keil Elektronik GmbH and Keil Software, Inc.
All rights reserved.
--------------------------------------------------------------------------*/
#ifndef __REG52_H__
#define __REG52_H__ //定义了一个宏,用于避免重复包含该头文件
/* BYTE Registers */
sfr P0 = 0x80; // 端口0的特殊功能寄存器地址,用于控制I/O口的输入输出
sfr P1 = 0x90; // 端口1的特殊功能寄存器地址,用于控制I/O口的输入输出
sfr P2 = 0xA0; // 端口2的特殊功能寄存器地址,用于控制I/O口的输入输出
sfr P3 = 0xB0; // 端口3的特殊功能寄存器地址,用于控制I/O口的输入输出
sfr PSW = 0xD0; // CPU状态寄存器地址,包含中断控制和CPU运行状态等信息
sfr ACC = 0xE0; // 累加器的特殊功能寄存器地址,用于运算时的暂存和结果输出
sfr B = 0xF0; // B寄存器的特殊功能寄存器地址,用于某些指令操作的时候临时存储数据
sfr SP = 0x81; // 堆栈指针的特殊功能寄存器地址,用于指向当前的堆栈位置
sfr DPL = 0x82; // 数据指针的低8位的特殊功能寄存器地址,用于访问外部数据存储器
sfr DPH = 0x83; // 数据指针的高8位的特殊功能寄存器地址,用于访问外部数据存储器
sfr PCON = 0x87; // 电源控制寄存器地址,用于控制电源管理和系统的低功耗状态
sfr TCON = 0x88; // 定时器控制寄存器地址,用于控制定时器的计数和相关的中断
sfr TMOD = 0x89; // 定时器模式寄存器地址,用于配置定时器的计数方式和工作模式
sfr TL0 = 0x8A; // 定时器0低8位的特殊功能寄存器地址,用于定时器计数和计数值的读写
sfr TL1 = 0x8B; // 定时器1低8位的特殊功能寄存器地址,用于定时器计数和计数值的读写
sfr TH0 = 0x8C; // 定时器0高8位的特殊功能寄存器地址,用于定时器计数和计数值的读写
sfr TH1 = 0x8D; // 定时器1高8位的特殊功能寄存器地址,用于定时器计数和计数值的读写
sfr IE = 0xA8; // 中断控制寄存器地址,用于控制中断的使能和优先级
sfr IP = 0xB8; // 中断优先级寄存器地址,用于设置中断的优先级
sfr SCON = 0x98; // 串行通信控制寄存器地址,用于控制串行通信的参数和状态
sfr SBUF = 0x99; // 串行通信数据缓存寄存器地址,用于存储串行通信的数据
/* 8052 Extensions */
sfr T2CON = 0xC8; // 定时器2控制寄存器地址,用于控制定时器2的工作模式和计数器的值
sfr RCAP2L = 0xCA; // 定时器2重载值低8位的特殊功能寄存器地址
sfr RCAP2H = 0xCB; // 定时器2重载值高8位的特殊功能寄存器地址
sfr TL2 = 0xCC; // 定时器2低8位计数器的特殊功能寄存器地址
sfr TH2 = 0xCD; // 定时器2高8位计数器的特殊功能寄存器地址
/* BIT Registers */
/* PSW */
sbit CY = PSW^7; // PSW(程序状态字)中进位标志位
sbit AC = PSW^6; // PSW中辅助进位标志位
sbit F0 = PSW^5; // PSW中用户可编程标志位0
sbit RS1 = PSW^4; // PSW中寄存器组选择位1
sbit RS0 = PSW^3; // PSW中寄存器组选择位0
sbit OV = PSW^2; // PSW中溢出标志位
sbit P = PSW^0; // PSW中奇偶校验位,只有8052芯片才有此标志位
/* TCON 位控制器的定义*/
sbit TF1 = TCON^7;  // 定义 TCON SFR 的位 7 为 TF1,用于控制定时器 1 溢出标志
sbit TR1 = TCON^6;  // 定义 TCON SFR 的位 6 为 TR1,用于控制定时器 1 运行控制
sbit TF0 = TCON^5;  // 定义 TCON SFR 的位 5 为 TF0,用于控制定时器 0 溢出标志
sbit TR0 = TCON^4;  // 定义 TCON SFR 的位 4 为 TR0,用于控制定时器 0 运行控制
sbit IE1 = TCON^3;  // 定义 TCON SFR 的位 3 为 IE1,用于控制外部中断 1 使能
sbit IT1 = TCON^2;  // 定义 TCON SFR 的位 2 为 IT1,用于控制外部中断 1 触发方式
sbit IE0 = TCON^1;  // 定义 TCON SFR 的位 1 为 IE0,用于控制外部中断 0 使能
sbit IT0 = TCON^0;  // 定义 TCON SFR 的位 0 为 IT0,用于控制外部中断 0 触发方式
/* IE */
sbit EA = IE^7;     // 定义 IE SFR 的位 7 为 EA,用于控制全局中断使能
sbit ET2 = IE^5;    // 定义 IE SFR 的位 5 为 ET2,仅适用于 8052,用于控制定时器 2 中断使能
sbit ES = IE^4;     // 定义 IE SFR 的位 4 为 ES,用于控制串行口中断使能
sbit ET1 = IE^3;    // 定义 IE SFR 的位 3 为 ET1,用于控制定时器 1 中断使能
sbit EX1 = IE^2;    // 定义 IE SFR 的位 2 为 EX1,用于控制外部中断 1 中断使能
sbit ET0 = IE^1;    // 定义 IE SFR 的位 1 为 ET0,用于控制定时器 0 中断使能
sbit EX0 = IE^0;    // 定义 IE SFR 的位 0 为 EX0,用于控制外部中断 0 中断使能
/* IP */
sbit PT2 = IP^5;    // 定义 IP SFR 的位 5 为 PT2,仅适用于 8052,用于控制定时器 2 中断优先级
sbit PS = IP^4;     // 定义 IP SFR 的位 4 为 PS,用于控制串行口中断优先级
sbit PT1 = IP^3;  // 定时器 1 中断优先级
sbit PX1 = IP^2;  // 外部中断 1 优先级
sbit PT0 = IP^1;  // 定时器 0 中断优先级
sbit PX0 = IP^0;  // 外部中断 0 优先级
/* P3 - 端口3 */
sbit RD = P3^7;   // 外部存储器读取
sbit WR = P3^6;   // 外部存储器写入
sbit T1 = P3^5;   // 定时器1计数器输入
sbit T0 = P3^4;   // 定时器0计数器输入
sbit INT1 = P3^3; // 外部中断1输入
sbit INT0 = P3^2; // 外部中断0输入
sbit TXD = P3^1;  // 串口发送端口
sbit RXD = P3^0;  // 串口接收端口

/* SCON - 串口控制寄存器 */
sbit SM0 = SCON^7; // 串口工作方式选择位0
sbit SM1 = SCON^6; // 串口工作方式选择位1
sbit SM2 = SCON^5; // 9位数据模式选择 (仅适用于特殊硬件)
sbit REN = SCON^4; // 串口接收使能
sbit TB8 = SCON^3; // 发送的第9位数据 (仅适用于特殊硬件)
sbit RB8 = SCON^2; // 接收的第9位数据 (仅适用于特殊硬件)
sbit TI = SCON^1;  // 串口发送中断标志
sbit RI = SCON^0;  // 串口接收中断标志

/* P1 - 端口1 */
sbit T2EX = P1^1; // 定时器2计数器的外部时钟输入 (仅适用于8052)
sbit T2 = P1^0;   // 定时器2计数器输入 (仅适用于8052)

/* T2CON - 定时器2控制寄存器 */
sbit TF2 = T2CON^7;   // 定时器2溢出标志
sbit EXF2 = T2CON^6;  // 定时器2外部脉冲标志
sbit RCLK = T2CON^5;  // 定时器2的外部时钟选择
sbit TCLK = T2CON^4;  // 定时器2的计数时钟选择
sbit EXEN2 = T2CON^3; // 定时器2的外部计数使能
sbit TR2 = T2CON^2;   // 定时器2的计数控制位
sbit C_T2 = T2CON^1;  // 定时器2的工作方式选择位
sbit CP_RL2 = T2CON^0; // 定时器2的模式选择位
#endif

# 什么是串口,端口,I/O 口?

串口是一种通过串行传输方式实现数据通信的接口,它通常包括两个信号线:一个是数据线,用于传输数据;另一个是时钟线,用于同步数据传输。串口可以连接各种外设,例如打印机、鼠标、调制解调器等等。

端口是一种物理接口,用于连接外部设备到计算机系统,通常由多个引脚组成。计算机通过端口与外设进行数据交换。在计算机中,通常将端口分为输入端口和输出端口,输入端口用于从外部设备读取数据,输出端口用于向外部设备发送数据。

I/O 口(Input/Output Port)则是一种用于输入输出的通用接口,它通常由多个引脚组成,可以用于输入输出各种类型的信号。I/O 口可以通过编程控制来实现与外部设备的数据交换,例如读取传感器数据、控制 LED 灯等等。

# 什么是串行传输?

串行传输方式是一种将数据比特一个接一个地传输的数据传输方式。在串行传输中,每个数据比特按照一定的时间间隔一个接一个地通过单根传输线传输。因为数据是按照顺序传输的,所以在接收端可以比较容易地重新组装成完整的数据。

相比较于并行传输方式(将多个数据比特同时传输),串行传输方式在使用线缆、连接器和外部设备等方面具有更低的成本,也可以更容易地扩展到更高的速度和更远的距离。

串行传输方式在计算机和通信领域广泛应用。例如,串口通信、以太网和 USB 等都是串行传输方式。在数字信号处理中,一些数字信号接口(如 SPI 和 I2C)也使用串行传输方式。

# 实践:点亮多个 LED 灯

点亮一个 LED 灯,我们可以通过让其连接的 I/O 口 引脚为低电平来实现,但如果要一次性点亮多个 LED 灯时怎么办?你可能会选择挨个把引脚设置为低电平。但这样的方法比较麻烦,用下面的方法,只用一行代码就可以点亮多个 LED 灯

#include "reg52.h"//引入引脚命名的头文件

void main()
{
	P2 = 0;//将P2端口设置为低电平
}

P2 端口共有 8 个引脚,如果直接将 P2 端口设置为低电平,就相当于把该端口的全部引脚都设为低电平。为了能更加清除的解释背后的原理我们一般使用 十六进制 的方式,如下

#include "reg52.h"//引入引脚命名的头文件

void main()
{
	P2 = 0x0;//将P2端口设置为低电平(十六进制)
}
  • 在十六进制中 0x0 表示 0000 0000(二进制形式)这表示八个引脚都为低电平
  • 使用十六进制可以更加灵活的控制各个引脚。例如,让第二个引脚和第五个引脚的灯不亮,其他灯都亮。这时只需要让第二个引脚和第五个引脚都为高电平,其他引脚都为低电平即可,用二进制表示为 0001 0010,用十六进制表示为 0x12
#include "reg52.h"//引入引脚命名的头文件

void main()
{
	P2 = 0x12;//P2端口输出 0001 0010
}

这样除了第二个引脚 P2^1 和第五个引脚 P2^4 的灯不亮以外,其他 P2 端口引脚的灯都会亮

# 实践:LED 闪烁

要实现一个 LED 闪烁的效果需要四个步骤:

  1. 点亮 LED
  2. 等待一秒
  3. 熄灭 LED
  4. 再等待一秒

然后循环以上过程,代码如下

#include "reg52.h"//引入引脚命名的头文件

sbit LED = P2^0;//给P2.0引脚命名为LED
void main()
{
	while(1)
  {
    LED = 0;//将引脚P2^0设置为低电平
    delay();//延时等待
    LED = 1;//将引脚P2^0设置为高电平
    delay();//延时等待
  }
}

但现在这些代码现在并不能运行,因为代码中还没有写入实现等待一秒的函数 delay(),如何实现延时功能呢?

其实单片机是按照一定的频率运行的,这种频率叫做 时钟频率 这是单片机中的晶振结合内部电路所提供的,晶振的提供的时钟频率越高,那单片机的运行速度也就越快。 既然如此,单片机运行每一段代码都需要花费一定的时间,那我们就可以制造一个程序来占用单片机运行的时间,从而实现延时的效果。通常会使用循环来实现这种效果

举一个完整的例子:

#include "reg52.h"//引入引脚命名的头文件

sbit LED = P2^0;//给P2.0引脚命名为LED

//延时函数
void delay(int count)
{
	int i;
	for(i=1;i<=count;i++);
}

void main()
{
	while(1)//一直循环
  {
    LED = 0;//将引脚P2^0设置为低电平
    delay(10000);//延时等待
    LED = 1;//将引脚P2^0设置为高电平
    delay(10000);//延时等待
  }
}
  • while() 中的 1 表示真,0 表示假,while(1) 则表示循环总是成立,所以循环会一直进行

delay()是一个延时函数,编写一个延时函数要考虑的因素很多,例如晶振频率,定时长度,指令集类型等,所以编写一个延时函数非常麻烦,但在软件 stc-isp 中有一个功能叫 软件延时计算器 ,可以帮助你生成延时代码 ,或者在网络上搜索单片机延时相关的代码

以下是一段的间隔一秒闪烁 LED 灯的代码,其中延时函数Delay1000ms是就是用stc-isp生成的,你可以把程序直接烧录在单片机中验证。注意:LED 灯正极接 VCC,负极接引脚 P20

#include <reg52.h>// 引入引脚命名的头文件
#include <intrins.h>// 引入文件声明void _nop_(void);

sbit LED = P2^0;//给P2.0引脚命名为LED

//延时函数
void Delay1000ms()		//@11.0592MHz
{
	unsigned char i, j, k;
	_nop_();
	i = 8;
	j = 1;
	k = 243;
	do
	{
		do
		{
			while (--k);
		} while (--j);
	} while (--i);
}

void main()
{
	while(1)
  {
		LED = 0;//将引脚P2^0设置为低电平
		Delay1000ms();//等待一秒
		LED = 1;//将引脚P2^0设置为高电平
		Delay1000ms();//等待一秒
	}
}

# 实践:流水灯

在 LED 闪烁的基础上,使用 for 循环,让闪烁动起来,形成流水灯。具体实现看代码



















 
 
 
 
 
 
 
 
 
 


#include <reg52.h>// 引入引脚命名的头文件
#include <intrins.h>// 声明了void _nop_(void);

sbit LED = P2^0;//给P2.0引脚命名为LED

//延时函数,延时200ms
void Delay200ms() //@11.0592MHz
{
  unsigned char i, j, k;
	_nop_();
	i = 2; j = 103; k = 147;
	do{do{while (--k);}
    while (--j);
  } while (--i);
}

void main()
{
	int i;
	P2=~0x01; // 0000 0001 取反为 1111 1110
	while(1)//总是循环
	{
		for(i=0;i<8;i++) //循环8次,每次循环i递增1知道i=8时停止循环
		{
			P2=~(0x01 <<i);// 移位运算 0000 0001 —> 0000 0010
			Delay200ms();//延时200ms
		}
	}
}

# 实践:按键的使用

让我们通过按键来控制 LED 灯的亮灭,以此为例来学习按键的使用

按键的使用电路示意图

以上是电路的连接示意图

  • 给 LED 灯串联一个电阻是为了保护 LED 灯不被烧毁,有些 LED 灯无法承受 5V 的电压,这对它们来说有点大,所以需要电阻来分走一部分电压
  • 按键的一端要接地线,因为51 单片机的引脚默认具有高电平,当按键按下时电路接地,电压降为低电平,现在只需要判断 P10 引脚是否为低电平就可以知道按键是否被按下
#include <reg52.h>// 引入引脚命名的头文件

sbit LED = P2^0; //LED灯接引脚 P2.0
sbit KEY = P1^0; //按键接引脚P 1.0

void main()
{
	while(1) //一直判断按键是否按下
	{
		if(KEY == 0) //判断按键是否按下
		{
			LED = ~LED; //取反操作,如果灯泡亮则熄灭灯泡;如果灯泡灭则点亮灯泡
			while(!KEY); //如果按键松开则继续进行循环
		}
	}
}

# 按键抖动问题

当按键按下时,活动触点击打固定触点会产生机械振动,因而造成输出波形抖动,这会影响开关使用的稳定性,可能造成按键灵时不灵的情况,波形对比如下

按键闭合波形

我们可以使用延时函数来解决按键抖动问题通过延时函数可以让程序等按键抖动结束后再对按键进行是否按下的判断,这时按键闭合稳定,判断不受物理因素干扰。

因按键形态和触点材料的不同,抖动的过程一般会持续数毫秒,金属触点的按键可能达到 10ms,而软性触点(如导电橡胶或薄膜)则可能在 1ms 以内,所以延时函数要高于这个时间。但也不能太高,不然会造成反应过慢的情况。

除了从程序上消除按键抖动的办法外,我们还可以通过物理方法实现按键的消抖。例如,在电路中增加滤波电容

# 通过按键控制 LED 灯亮灭





















 
 
 
 
 
 
 
 
 
 
 
 
 


#include <reg52.h>// 引入引脚命名的头文件

sbit LED = P2^0; //LED灯接引脚 P20
sbit KEY = P1^0; //按键接引脚 P10

//延时函数
void Delay50ms() //@11.0592MHz
{
	unsigned char i, j;

	i = 90;
	j = 163;
	do
	{
		while (--j);
	} while (--i);
}

void main()
{
	LED = 1; //设置灯泡初始状态为熄灭
	while(1) //一直判断按键是否按下
	{
		if(KEY == 0) //判断按键是否按下
		{
			Delay50ms(); //延时,解决按键抖动问题
			if (KEY == 0) //判断按键是否按下
			{
				LED = ~LED; //取反操作,如果灯泡亮则熄灭灯泡;如果灯泡灭则点亮灯泡
			}
			while(!KEY); //判断按键是否松开,如果松开则继续进行循环
		}
	}
}

# 如何实现按下按钮时 LED 灯熄灭,松开按钮后 LED 灯点亮?

最简单的方法就是让 LED 的状态等于按键的状态

#include <reg52.h>// 引入引脚命名的头文件

sbit LED = P2^0; //LED灯接引脚 P20
sbit KEY = P1^0; //按键接引脚 P10

void main()
{
	LED = KEY; //LED状态等于按键状态
}

# 实践:用矩阵键盘控制 LCD1602 屏幕的显示

要实现用矩阵键盘控制 LCD1602 屏幕的显示,需要先学习矩阵按键和 LCD1602 的使用,所以先从这两个外设开始讲起

# 矩阵键盘

矩阵键盘是一种常见的电子键盘,由多个按键排列组成。它的原理是将按键排列成一个行列矩阵,通过扫描矩阵的行和列,检测按键的按下和释放状态。

具体来说,矩阵键盘的每个按键都被分配了一个唯一的行列坐标,这些坐标按照行列排列成一个矩阵。当按键未被按下时,该按键所在的行列处于高电平状态;当按键被按下时,该按键所在的行列会产生短暂的低电平信号为了检测按键状态,矩阵键盘需要不断扫描矩阵的行和列,以检测低电平信号

矩阵键盘的优点是可以减少输入设备的引脚数量,从而降低成本,同时也可以实现较高的响应速度。缺点会受到按键反应速度和扫描速度的限制,因此在某些高速应用场合可能不够理想。

矩阵键盘原理图如下

矩阵键盘原理图

# 行列扫描法

矩阵键盘上有许多按键,通过判断按下按键的行和列来确定按下的是哪个按键,这种方法就是行列扫描法,具体代码如下

int Key; //设置变量Key,用于储存键值

P1 = 0x0f; //把P1端口的8位引脚设置为 0000 1111 (从右向左数),准备进行列扫描
if (P1 != 0x0f) {
    Delay10ms(); //延时10ms
    if (P1 != 0x0f) {
        //扫描列
        P1 = 0x0f;
        switch (P1) {
            case (0x0e): //0000 1110 第1列的按键被按下
                Key = 1;
                break;
            case (0x0d): //0000 1101 第2列的按键被按下
                Key = 2;
                break;
            case (0x0b): //0000 1011 第3列的按键被按下
                Key = 3;
                break;
            case (0x07): //0000 0111 第4列的按键被按下
                Key = 4;
                break;
        }
        //扫描行
        P1 = 0xf0; //把P1端口的8位引脚设置为 1111 0000 准备开始行扫描
        switch (P1) {
            case (0x70): //0111 0000 第1行的按键被按下
                Key = Key + 12;
                break;
            case (0xb0): //1011 0000 第1行的按键被按下
                Key = Key + 8;
                break;
            case (0xd0): //1101 0000 第2行的按键被按下
                Key = Key + 4;
                break;
            case (0xe0): //1110 0000 第1行的按键被按下
                Key = Key;
                break;
        }
    }
}
while (!P1 == 0xf0);
  • 高四位表示,低四位表示
  • 有了以上代码后,后续只需要根据变量 Key 的值来判断按下的是哪个按键,例如,按下按键 S4 后变量Key = 4

# LCD1602 工作原理

LCD1602 屏幕最下面是一层 LED 背光板,给他接上电它就可以发光,在它的上面整齐排列这一堆小块液晶,这些液晶就是屏幕显示的像素,按照 2×16 排列 所以叫 LCD"1602"(16×02),LCD 表示(Liquid Crystal Display)液晶显示屏。这些像素共有 32 个部分,每个部分都由 35 个小块液晶(5×7)组成当液晶没有施加电压时它几乎透明,但当给某一块液晶施加电压后它就不透光了,因此形成了一个正方形的小黑点,我们只需要控制每个像素的电压就可以在屏幕上用这些像素形成各种图案。

名称

但这块小小的屏幕却有 1120 个像素,如果每个像素都让单片机控制则需要 1120 个引脚,由于需要的引脚太多,所以人类开发了一个名为 HD44780U 的显示芯片,这个芯片有 80 个引脚,通过这个芯片我们就可以控制 LCD1602 的每一个像素。一般这些芯片会封装在 LCD1602 模块的电路板背部,不需要单独购买,由于像素数量的限制这块屏幕只能显示数字,字母和一些简单的符号,一共能显示 240 种字符,这些符号由ASCII 码进行编码,由八位二进制的数据表示,所以它需要八根数据引脚(D0~D7)传输数据,一般情况下会先发送一个显存地址,来告诉 LCD1602 数据显示的区域,然后再发送要在这块区域显示的字符的 ASCII 码,这样字符就会显示在那个区域

视频推荐

再简单的屏幕也需要显卡!让你彻底看懂 LCD 屏的控制原理!1602A LCD 的工作原理! (opens new window)

# LCD1602 的使用

为了更方便的使用 LCD1602 屏幕,可以在程序中引入函数库 LCD1602.c

#include "LCD1602.h" //引入LCD1602函数库头文件

引入函数库前要先在项目文件中添加以下两个文件(添加在根目录即可)

LCD1602.h
#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.c
#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');
	}
}
LCD1602.c 函数库常用函数说明
函数 参数 说明
LCD_Init() 初始化函数
LCD_ShowChar(Line,Column,Char) Line 行位置
Column 列位置
Char 字符
在指定位置上显示一个字符
LCD_ShowString(Line,Column,String) Line 起始行位置
Column 起始行位置
String 要显示的字符串
在指定位置开始显示所给字符串
LCD_ShowNum(Line,Column,Number,Length) Line 起始行位置
Column 起始行位置
String 要显示的数字
Length 数字的长度
在指定位置开始显示所给数字
LCD_ShowSignedNum(Line,Column,Number,Length) Line 起始行位置
Column 起始行位置
String 要显示的数字
Length 数字的长度
在指定位置以有符号的十进制形式显示所给数字
LCD_ShowHexNum(Line,Column,Number,Length) Line 起始行位置
Column 起始行位置
String 要显示的数字
Length 数字的长度
在指定位置开始以十六进制的形式显示所给数字
LCD_ShowBinNum(Line,Column,Number,Length) Line 起始行位置
Column 起始行位置
String 要显示的数字
Length 数字的长度
在指定位置开始以二进制的形式显示所给数字
LCD_SetCursor(Line,Column) Line 行位置
Column 列位置
设置光标的位置

然后根据以下原理图连接电路和单片机

LCD1602接线图

LCD1602 共有 16 个引脚,其各个引脚的作用如下:

  • VSS:地线
  • VDD:电源引脚,通常接 5V 电源。
  • V0:液晶对比度调整引脚,通过调节电压大小来调整液晶的对比度。一般接一个可变电阻来调节,电阻两端接 VDD 和 GND,中间接 V0 引脚。
  • RS:寄存器选择引脚,用于选择数据寄存器(RS=1)或指令寄存器(RS=0)。
  • RW:读写选择引脚,用于选择读操作(RW=1)或写操作(RW=0)。
  • E:使能引脚,用于启动数据传输。
  • D0-D7:数据引脚,用于传输 8 位并行数据。
  • A:LED 背光板正极引脚。
  • K:LED 背光板负极引脚。

# 程序:根据按键在 LCD1602 屏幕显示内容

这就是该程序的全部代码,现在阅读每一行代码后的注释,并把它烧录在单片机中试试吧

#include <reg52.h>// 引入引脚命名的头文件
#include "LCD1602.h" //引入操作LCD1602的函数库

void Delay10ms()		//@11.0592MHz
{
	unsigned char i, j;

	i = 18;
	j = 235;
	do
	{
		while (--j);
	} while (--i);
}

int Key;
void Key_onclick() //判断矩阵键盘按键
{
	P1 = 0x0f; //0000 1111
	if(P1 != 0x0f )
		{
			Delay10ms();//延时10ms
			if(P1 != 0x0f)
			{
				//扫描列
				P1 = 0x0f;
				switch(P1)
					{
					case(0x0e)://0000 1110 第1列的按键被按下
						Key = 1;break;
					case(0x0d)://0000 1101 第2列的按键被按下
						Key = 2;break;
					case(0x0b)://0000 1011 第3列的按键被按下
						Key = 3;break;
					case(0x07)://0000 0111 第4列的按键被按下
						Key = 4;break;
					}
					//扫描行
					P1 = 0xf0;
					switch(P1)
						{
						case(0x70)://0111 0000 第1行的按键被按下
							Key = Key +12;break;
						case(0xb0)://1011 0000 第1行的按键被按下
							Key = Key + 8;break;
						case(0xd0)://1101 0000 第2行的按键被按下
							Key = Key + 4;break;
						case(0xe0)://1110 0000 第1行的按键被按下
							Key = Key;break;
						}
			}
		}
		while(!P1 == 0xf0);
}

void main()
{
	LCD_Init(); //LCD初始化
	while(1)
	{
		Key_onclick(); //调用判断矩阵键盘按键的函数
		LCD_ShowNum(1,1,Key,3);//从屏幕第一行第一列开始显示数字(Key的值),数字位数设置为3位
		switch(Key)//根据变量Key判断按下的是哪个按键
		{
			case(1): //当按下S1时
				LCD_ShowString(2,1,"Hello!Yoseya    ");break;
			case(2): //当按下S2时
				LCD_ShowString(2,1,"Hello!Peter     ");break;
			case(3): //当按下S3时
				LCD_ShowString(2,1,"Hello!Tom       ");break;
			case(4): //当按下S4时
				LCD_ShowString(2,1,"Hello!World     ");break;
		}
	}
}

# 定时器与中断

如何用单片机制作一个定时器?如何让单片机获得定时的能力?这些都需要晶体振荡器的参与,俗称晶振。晶振按照固定频率振动,通过它的这种性质就可以实现计时,具体实现方法如下

# 定时器原理

在存储器中指定一个固定的存储空间,用于计数,并连接外部的晶体振荡器电路(OSC),晶振每震荡一次就会产生一个周期的方波,这时存储器中计数加一,由于晶振震荡周期是固定的,所以正好可以用于计时。

在 51 单片机中有一个分频器默认为 12 分频,分频后的信号传给“计数器”进行计数,这里的计数器就是用于计数的存储器单元,为了读取更加便捷,所以它被单独设计为特殊功能寄存器,他有两个字节的空间,这两字节分别为 TLiTHi ,这里的 i 可以为 0 也可以为 1,因为在 51 单片机中设有两个功能相同的定时器 0 和 1,所以这里的 i 是通配符,当使用定时器 0 时 i 为 0,使用定时器 1 时 i 为 1

如果使用的晶振频率为 12MHZ ,经过 12 分频后输入的频率为 1MHZ,由f=1T得,振动一个周期的时间为 1μs ,所以每经过 1μs 计数器中的值 +1想要知道过去多长时间只需要判断计数器中的数即可,如果计数器中的值为1×106时则刚好为 1 秒,但这个计时器大小只有两个字节,能存储的最大范围为216也就是 0~65535 ,计数器所能计到的最大数就是 65535 ,无法计数到1×106这个远超于 65536 的数这时就需要使用到标志位 TF ,TF0 是计数器 0 的标志位,TF1 是计数器 1 的标志位,每当计数器的值到 65535 时CNT 就会溢出从而触发 TF 标志位,使 TF 标志位从 0 变为 1 ,这时单片机会在一个新寄存器中 +1 ,这样就可以扩展计数范围。除此之外 TF 标志位为 1 后还会触发中断(这部分会在之后的中断系统中讲解)

定时器原理

为了更灵活的改变时间基准又不影响系统主频,所以设计了开关 C/T ,C 表示 Counter,T 表示 Timer,“T”上的横杠表示低电平有效,当C/T=0时 CNT 的时钟由 OSC 提供,当C/T=1时 CNT 的时钟由单片机的外部引脚提供,如果该引脚是 T0 则 T0 就是定时计数器 0 的外部输入端,如果该引脚是 T1 则 T1 就是定时计数器 1 的外部输入端,通过定时器外部输入端口,我们可以输入任意频率方波,为了方便的控制 CNT 的计数,前面还要设置一个使能开关,开关闭合时 CNT 就可以根据时钟源进行计数,开关断开后 CNT 停止工作,这个开关由下面的逻辑电路进行控制,其中 TRi 是定时器的控制位TR0控制定时器 0,TR1控制定时器 1 ,如果想要定时器运行就必须打开 TRTR=1

假设现在定时器控制位为 1 即 TR=1GATE=0或门输出端输出高电平(1)这时定时器使能开关闭合GATE=1 时引脚 INT0 就可以控制定时器使能开关INT0 为高电平时,即 INT0=1或门输出端输出高电平(1)定时器使能开关闭合或门输出端输出低电平(0)定时器使能开关断开

CNT 的工作模式由 M0M1 控制,具体如下表

M0 M1 工作模式
0 0 13 位定时器,TL 只用了 5 位,TH8 位全用
1 0 16 为定时器
0 1 8 位自动装载,TL 用于计数,TH 用于保存装载值,TL 溢出后 TH 的值自动装入 TL 内
1 1 定时器 0 为双 8 位定时器,定时器 1 无功能

参考视频

入坑单片机 -- [12_1]定时器工作原理 (opens new window)

# 中断系统

之前提到,当 CNT 溢出时 TF 标志位会置为 1 ,从而触发中断。以定时器 0 为例,当 TF0 置 1 后,ET0 闭合,中断总开关 EA 闭合,那么定时器溢出中断请求就会被登记到中断表上,此时单片机就会立即响应中断。

响应中断过程:

  • 执行中断服务程序
  • 硬件自动清除 TF 标志位为 0 ,即 TF=0,防止重复触发中断

中断系统

如图,在 51 单片机中共五条中断线路,每个线路都有对应的矢量地址,当有中断触发时程序计数器 PC 会自动跳转到对应的矢量地址处开始执行中断服务程序,那什么是中断服务程序呢?如下表

中断服务程序

中断源 矢量地址 C 语言中断服务程序接口
INT0 0x03 void INT0_ISR(void) interrupt 0
TIM0 0x0B void TIM0_ISR(void) interrupt 1
INT1 0x13 void INT1_ISR(void) interrupt 2
TIM1 0x1B void TIM1_ISR(void) interrupt 3
UART 0x23 void UART_ISR(void) interrupt 4

表中的中断服务程序接口其实就是函数头,当触发中断后就会执行该中断对应的函数

void INT0_ISR(void) interrupt 0
{
//触发中断后执行的程序
}

注意

  • 中断服务程序接口的函数名称允许用户自定义
  • 中断服务函数数据类型为 viod,不能带有参数,不能返回值
  • 如果打开多个中断服务,中断服务程序会根据优先级表从上到下依次执行
  • 高优先级表中的中断服务不仅可以在低优先级表前执行,还可以打断低优先级中断程序。通过这种性质可以实现中断函数程序的嵌套

中断程序执行过程

中断程序执行过程

# 控制位

  • TMOD(Timer Control Mode Register)定时器模式控制寄存器
  • TCON(Timer Control Register)定时器控制寄存器
  • IE(Interrupt Enable Register)中断允许寄存器
  • IP(interrupt priority)中断优先级控制寄存器
寄存器 7 6 5 4 3 2 1 0 地址
TMOD GATE C/T M1 M0 GATE C/T M1 M0 0x89
TCON TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0 0x88
IE EA - - ES ET1 EX1 ET0 EX0 0xA8
IP - - - PS PT1 PX1 PT0 PX0 0xB8

注意

  • 在定时器模式控制寄存器 TMOD 中,高四位控制定时器1,低四位控制定时器0
  • 在中断允许寄存器 IE 中,标志位为1时表示允许,标志位为0时表示禁止

参考视频

入坑单片机 -- [12_2]中断系统 (opens new window)

# 实践:制作简单的计时器

  1. 要制作计时器首先需要一个显示屏幕,LCD1602 就是一个不错的选择。引脚的连接与之前的一样,具体可以参考:LCD1602 的使用
  2. 在项目中添加文件 Timer0.cTimer0.h,该文件代码用于配置定时器
//Timer0.h
#ifndef __TIMER0_H__
#define __TIMER0_H__
void Timer0Init(void);
#endif

//Timer0.c
#include <REGX52.H>
void Timer0Init(void)
{
	TMOD &= 0xF0; //设置定时器模式
	TMOD |= 0x01; //设置定时器模式
	TL0 = 0x18; //设置定时初值
	TH0 = 0xFC; //设置定时初值
	TF0 = 0; //清除TF0标志
	TR0 = 1; //定时器0开始计时
	ET0=1; //定时器0中断允许
	PT0=0; //中断服务低优先级
}
  1. 编写主程序 main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "Timer0.h"

//设置变量时分秒,初始为0
int Sec = 0; //秒
int Min = 0; //分
int Hour = 0; //时

void main()
{
	LCD_Init(); //LCD初始化
	Timer0Init(); //配置定时器
	LCD_ShowString(1,1,"  :  :");
	while(1)
	{
		//显示时分秒
		LCD_ShowNum(1,1,Hour,2);
		LCD_ShowNum(1,4,Min,2);
		LCD_ShowNum(1,7,Sec,2);
	}
}

void Timer0_Routine() interrupt 1 //中断服务程序
{
	static unsigned int T0Count;
	TL0 = 0x18;	//设置定时初值
	TH0 = 0xFC; //设置定时初值
	T0Count++;
	if(T0Count>=1000) //定时器分频,1s
	{
		T0Count=0;
		Sec++; //经过1秒,Sec+1
		if(Sec>=60)
		{
			//经过60秒Sec清0,Min+1
			Sec=0;
			Min++;
			if(Min>=60)
			{
				//经过60分钟Min清0,Hour+1
				Min=0;
				Hour++;
				if(Hour>=24)
				{
					Hour=0;	//经过24小时Hour清0
				}
			}
		}
	}
}

# 串口通信

# 什么是串口通信?

串口通信(Serial Communication)是指通过串行接口(Serial Interface)进行数据传输的通信方式。在计算机和电子设备中,串口通信是一种常见的数据交换方式,用于在设备之间传输数据。

# 什么是串行通信?什么是并行通信?

串行通信(Serial Communication)是指在数据传输中,逐位地按照顺序传输数据的通信方式。在串行通信中,数据位按照顺序一个接一个地传输,每个数据位都紧跟着前一个数据位。串行通信使用单条通信线路进行数据传输,每次只传输一个数据位。

串行通信相对于并行通信来说,使用的通信线路更少,可以减少连接线路的数量,减小系统的复杂度和成本。它常用于远距离通信和资源受限的系统中,如串口通信、以太网通信中的串行传输等。

并行通信(Parallel Communication)是指在数据传输中,同时传输多个数据位的通信方式。在并行通信中,每个数据位都有自己的通信线路,数据位之间并行传输,同时进行。

并行通信相对于串行通信来说,可以实现更高的数据传输速率,因为同时传输多个数据位。它常用于短距离通信和高速数据传输的场景,如计算机内部总线、内存与处理器之间的通信等。

# 异步通信和同步通信的区别是什么?

异步通信是一种基于字符为单位的通信方式,每个字符之间独立地传输,没有固定的时钟信号来同步发送端和接收端。每个字符都包含起始位、数据位、可选的校验位和停止位,起始位和停止位的特殊组合用于标识字符的开始和结束。接收端根据起始位和停止位来识别和解析每个字符,而不依赖于固定的时钟信号。异步通信常用于串口通信等场景。

同步通信是一种基于数据块为单位的通信方式。在同步通信中,发送端和接收端通过共享的时钟信号来同步数据传输。数据被分成固定大小的块,每个块通过时钟信号的边沿进行同步传输。发送端和接收端的时钟频率必须相同或相近,以确保数据的正确传输。同步通信常用于高速数据传输和并行通信等场景。

主要区别如下:

  1. 时钟同步方式:异步通信没有固定的时钟信号,而同步通信使用共享的时钟信号来同步数据传输。
  2. 数据单位:异步通信以字符为单位进行传输,而同步通信以数据块为单位进行传输。
  3. 数据传输机制:异步通信通过起始位和停止位来标识字符的开始和结束,接收端根据这些位识别和解析数据。同步通信通过共享的时钟信号来同步发送端和接收端之间的数据传输。
  4. 应用场景:异步通信常用于串口通信等低速数据传输场景,而同步通信常用于高速数据传输和并行通信等场景。

# 串行控制寄存器 SCON

用于控制串口通信的方式和状态,以及接收和发送

7 6 5 4 3 2 1 0
符号 SM0 SM1 SM2 REN TB8 RB8 T1 R1
复位值 0 0 0 0 0 0 0 0

串行控制寄存器 SCON 各标志位的用途

  • SM0SM1 两个标志位用于控制串口通信的模式,两个标志位可以有 00,01,10,11 四种组合,对应着四种不同的通信模式
SM0 SM1 工作方式
0 0 同步移位寄存器方式
0 1 8 位异步收发,波特率由定时器控制
1 0 9 位异步收发,波特率为时钟频率的 1/64 或者 1/32
1 1 9 位异步收发,波特率由定时器控制
  • SM2 多机位控制
  • REN 用于串口接收,当该标志位为 0 时禁止串口接收,为 1 时允许接收
  • TB8 用于发送第 9 位数据
  • RB8 用于接收第 9 位数据
  • TI 发送中断标志位,当数据发送完成后该标志位会自动置为 1,继续发送下一条数据是要把标志位置为 0
  • RI 接收中断标志位,当数据接收完成后该标志位会自动置为 1,继续接收下一条数据是要把标志位置为 0

注意

  • 通常情况下,只会用到SM0SM1RENTIRI 这 5 个标志位,其余的都不常用,将其置为 0 即可
  • 8 位异步收发时最常使用的串口通信模式,也就是SM0=0SM1=1

在电源控制寄存器 PCON 中只有最后一个标志位 SMOD 与串口通信有关,当SMOD = 1时,波特率提高一倍,复位后SMOD = 0

# 什么是波特率?

波特率(Baud Rate)是指串口通信中单位时间内传输的符号或比特数。它表示了串口通信的数据传输速率,即每秒钟传输的比特数。

波特率通常以单位时间内传输的符号数或比特数来表示,单位为波特(Baud)。例如,一个波特率为 9600 的串口表示每秒传输 9600 个符号或比特。

波特率是串口通信中非常重要的参数,因为发送端和接收端需要以相同的波特率进行数据传输才能正确地解析和还原数据。如果发送端和接收端的波特率不一致,数据可能会被错误地解释,导致通信错误。

选择适当的波特率取决于特定的应用需求和通信环境。较高的波特率可以实现更高的数据传输速率。常见的波特率值包括 9600、19200、38400、57600、115200 等,具体选择取决于设备和通信要求。

# 常用波特率初值表

波特率 晶振频率 SMOD=0 SMOD=1
300 11.0592 0xA0 0x40
600 11.0592 0xD0 0xA0
1200 11.0592 0xE8 0xD0
1800 11.0592 0xF4 0xE8
3600 11.0592 0xF8 0xF0
4800 11.0592 0xFA 0xF4
7200 11.0592 0xFC 0xF8
9600 11.0592 0xFD 0xFA
14400 11.0592 0xFE 0xFC
19200 11.0592 0xFD
28800 11.0592 0xFF 0xFE

# 代码示例

串口通信初始化

void UART_Init()
{
SCON = 0x50; //01010000 //选择模式1
PCON &= 0x7F; //0111 1111 //波特率不翻倍
TMOD &= 0x0F; //00001111 //设置定时器模式
TMOD |= 0x20//00101111 //设置定时器模式
TL1 = 0xFD; //设定定时初值
TH1 = 0xFD; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
EA = 1; //中断总开关
ES = 1; //串口中断开关波特率品振
}

通过串口通信发送数据

void UART_SendByte(unsigned char Byte)
{
SBUF = Byte; //将发送数据存到寄存器SBUF中
while(TI==0);
TI = 0;
}
  • SBUF 是串口缓冲寄存器,用于存储发送和接收的数据或者用于将数据传输到串口或从串口接收数据

串口通信中断

void UART_Routine() interrupt 4
{
	if(RI==1) //如果接收标志位为1时表示收到数据
	{
		RI = 0; //接收标志位清0
		P2 = SBUF //将接收到的数据存储在P2中
		UART_SendByte(SBUF); //将接收到的数据发送出去
	}
}

# 蓝牙模块通过串口通信控制舵机

蓝牙模块有6个引脚,其中 STATEEN 不接 VCC接电源5VGNDGNDRXDTXDTXDRXD

舵机有三根线,其中两个分别为红色和黑色分别表示电源正极VCC和地线GND,另一根是信号线,接引脚P1_2

#include <REGX52.H>
 
#define SEH_PWM P1_2//舵机PWM口定义
 
unsigned char SEH_count;
unsigned char count=0;
 
 
unsigned char T0RH = 0xff;  //T0重载值的高字节
unsigned char T0RL = 0xa3;  //T0重载值的低字节
unsigned char RxdByte = 0;  //串口接收到的字节
 
void ConfigTimer0();
void ConfigUART(unsigned int baud);
 
void PWM(unsigned char x)
{
	SEH_count=x;
}
 
void main()
{
    EA = 1;       //使能总中断
;
    ConfigTimer0();   //配置T0定时1ms
    ConfigUART(9600);  //配置波特率为9600
    
    while (1)
    {  
					switch(RxdByte)
				{
					case 0x31:{
											PWM(5);//手机向单片机发送‘0’,舵机转到0度    
										};break;
					case 0x32:{
										PWM(10);//手机向单片机发送‘1’,舵机转到45度  
										};break;
					case 0x33:{
										PWM(15);//手机向单片机发送‘2’,舵机转到90度  
										};break;
					case 0x34:{
										PWM(20);//手机向单片机发送‘3’,舵机转到135度  
										};break;
					case 0x35:{
										PWM(25);//手机向单片机发送‘4’,舵机转到180度  
										};break;
					case 0x36:{
										;//														
										};break;
					case 0x37:{
									   ;//																
										};break;
					case 0x38:;break;
				}
    }
}
/* 配置并启动T0,ms-T0定时时间 */
void ConfigTimer0()
{
  
 
 
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH;     //加载T0重载值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中断
    TR0 = 1;        //启动T0
}
/* 串口配置函数,baud-通信波特率 */
void ConfigUART(unsigned int baud)
{
    SCON  = 0x50;  //配置串口为模式1
    TMOD &= 0x0F;  //清零T1的控制位
    TMOD |= 0x20;  //配置T1为模式2
    TH1 = 256 - (11059200/12/32)/baud;  //计算T1重载值
    TL1 = TH1;     //初值等于重载值
    ET1 = 0;       //禁止T1中断
    ES  = 1;       //使能串口中断
    TR1 = 1;       //启动T1
}
 
/* 中断服务函数,完成LED扫描 */
void InterruptTimer0() interrupt 1
{
    TH0 = T0RH;  //重新加载重载值
    TL0 = T0RL;
	  if(count <= SEH_count) 
    {
     
        SEH_PWM = 1;
    }
    else
    {
        SEH_PWM = 0;
    }
    count++;
    if (count >= 200) 
    {
        count = 0;
    }
	
}

/* UART中断服务函数 */
void InterruptUART() interrupt 4
{
    if (RI)  //接收到字节
    {
        RI = 0;  //手动清零接收中断标志位
        RxdByte = SBUF;  //接收到的数据保存到接收字节变量中
        SBUF = RxdByte;  //接收到的数据又直接发回,叫作-"echo",
                         //用以提示用户输入的信息是否已正确接收
    }
    if (TI)  //字节发送完毕
    {
        TI = 0;  //手动清零发送中断标志位
    }
}

参考

单片机——SG90舵机工作原理 (opens new window)