参考
- 51单片机入门教程
1. 单片机简介
1.1 定义
- 单片机(Micro Controller Unit,简称 MCU)
- 内部集成了 CPU、RAM、ROM、定时器、中断系统、通讯接口等一系列电脑的常用硬件功能
- 单片机的任务是信息采集(依靠传感器)、处理(依靠CPU)和硬件设备(例如电机,LED 等)的控制
- 单片机跟计算机相比,单片机算是一个袖珍版计算机,一个芯片就能构成完整的计算机系统。但在性能上,与计算机相差甚远,但单片机成本低、体积小、结构简单,在生活和工业控制领域大有所用
- 应用领域:智能仪表、实时工控、通讯设备、导航系统、家用电器等
1.2 STC89C52 单片机
-
STC 公司 51 单片机系列,8 位,RAM(512 字节),ROM(8K,Flash),工作频率 12MHz
- STC89C52RC 的晶振集成在芯片内部,工作频率为 11.0592MHz
- 单片机晶振的作用:利用一种能将电能和机械能相互转换的晶体,在单片机中提供一个精确的时钟信号,以便单片机能够正确执行其功能
51 单片机为什么叫 51?
- 是因为这种单片机最初采用的是英特尔公司的 8051 指令系统。随着时间的推移,有多家公司生产了兼容 8051 指令系统的单片机,它们虽然功能各异,但是核心架构相同,因此都被统称为 51 单片机
- “51” 在这里代表了 8051 架构,也就是 Intel 公司发明的一种早期的单片控制器架构
-
命名规则
1.3 单片机内部结构图
- 静态随机存取存储器(Static Random-Access Memory,SRAM)是随机存取存储器的一种,所谓的 “静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持
- 只读存储器(Read-Only Memory,ROM)以非破坏性读出方式工作,只能读出无法写入信息,信息一旦写入后就固定下来,即使切断电源,信息也不会丢失,所以又称为固定存储器
- 闪存是一种非易失性(Non-Volatile)内存,在没有电流供应的条件下也能够长久地保持数据
- 单片机通过配置寄存器来控制内部线路的连接,通过内部不同线路的连接来实现不同电路以完成不同功能
1.4 单片机管脚图
1.5 单片机最小系统
1.6 单片机核心原理图
2. LED
2.1 简介
- 发光二极管(Light Emitting Diode,LED),用于照明、广告灯、指引灯、屏幕,是一种冷光源,因此比较环保且响应速度快,左图:左边是正极、右边是负极
2.2 LED 原理图
- 单片机上 RP9 上的数字 102:前面两位数是一个有效数字 10,第三位数字就是倍率 00,102 = 10*10^2 = 1KΩ(电阻或电容的通用读数方式),此处的电阻也称为限流电阻
- 单片机可以通过控制 IO 口的输出模式和电平状态来实现对 IO 口输出高低电平的控制
- 1、设置 IO 口为输出模式:在单片机的相应寄存器中设置 IO 口对应的引脚为输出模式
- 2、控制 IO 口输出高低电平:将 IO 口对应的引脚设置为期望输出的电平,通常使用高电平表示逻辑 “1”,低电平表示逻辑 “0”
单片机 TTL 电平:高电平 5V,低电平 0V,LED 具有单向导电性,当 LED 的正端接了高电位,负端连接了低电位,且正负端电位差超过 1.8V 以上时,LED 就会亮起来
2.3 进制转换
2.4 C51 数据类型
2.5 示例代码
- 2-1 点亮一个 LED
#include <REGX52.H> // 定义寄存器和端口(识别 P2 口)void main() {P2 = 0xFE; // 1111 1110,0x 前缀表示 十六进制,引脚配置为 “低电平有效”// P2 = 0x55; // 0101 0101,8 个 LED 灯间隔点亮while (1) {}
}
- 2-2 LED 闪烁
#include <REGX52.H>
#include <INTRINS.H> // _nop_(); 需要的头文件void Delay500ms() { // @12.000 MHzunsigned char i, j, k;_nop_(); // 空操作命令,确保编译器不会对后续的循环优化i = 4;j = 205;k = 187;// 外层循环的条件是 i != 0// 内层两层循环的条件分别是 j != 0 && k != 0do {do {while (--k); // 循环耗时操作} while (--j); // 嵌套循环耗时操作} while (--i);
}void main() {while(1) {P2 = 0xFE; // 1111 1110Delay500ms(); // 单片机当中每次都是以 MHZ 速度运行,闪烁太快人眼看不出,因此要加延迟函数P2 = 0xFF; // 1111 1111Delay500ms();}
}
- 2-3 LED 流水灯(固定延时时间)
#include <REGX52.H>
#include <INTRINS.H>void Delay500ms() { // @12.000 MHzunsigned 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 1110Delay500ms();P2 = 0xFD; // 1111 1101Delay500ms();P2 = 0xFB; // 1111 1011Delay500ms();P2 = 0xF7; // 1111 0111Delay500ms();P2 = 0xEF; // 1110 1111Delay500ms();P2 = 0xDF; // 1101 1111Delay500ms();P2 = 0xBF; // 1011 1111Delay500ms();P2 = 0x7F; // 0111 1111Delay500ms();}
}
- 2-4 LED 流水灯2(自定义延时时间)
#include <REGX52.H>void Delay1ms(unsigned int xms); // @12.000MHzvoid main() {while (1) {P2 = 0xFE; // 1111 1110Delay1ms(100);P2 = 0xFD; // 1111 1101Delay1ms(100);P2 = 0xFB; // 1111 1011Delay1ms(100);P2 = 0xF7; // 1111 0111Delay1ms(100);P2 = 0xEF; // 1110 1111Delay1ms(100);P2 = 0xDF; // 1101 1111Delay1ms(100);P2 = 0xBF; // 1011 1111Delay1ms(100);P2 = 0x7F; // 0111 1111Delay1ms(100);}
}void Delay1ms(unsigned int xms) { // @12.000MHzunsigned char i, j;while (xms) {i = 2;j = 239;do {while (--j);} while (--i);xms--;}
}
3. 独立按键
3.1 按键介绍
- 轻触按键:相当于是一种电子开关,按下时开关接通,松开时开关断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开
3.2 独立按键原理图
- 单片机上电时所有 IO 口默认都是高电平,那么按键没有按下时这个 IO 口就是高电平,按下后这个 IO 口就变成低电平,寄存器会检测 IO 口的电平,然后再读回这个寄存器中
3.3 C51 数据运算
3.4 C51 基本语句
3.5 按键的抖动
- 对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动
3.6 示例代码
- 3-1 独立按键控制 LED 亮灭
#include <REGX52.H>void main() {while (1) {if (P3_1 == 0 || P3_0 == 0) { // 如果 K1 按键(RXD,P3_1)或 K2 按键(TXD,P3_0)按下P2_0 = 0; // LED1 输出 0,点亮} else {P2_0 = 1; // LED1 输出 1,熄灭}}
}
- 3-2 独立按键控制 LED 状态
#include <REGX52.H>void Delay(unsigned int xms) {unsigned char i, j;while (xms) {i = 2;j = 239;do {while (--j);} while (--i);xms--;}
}void main() {while (1) {if (P3_1 == 0) { // 如果 K1 按键按下Delay(20); // 延时,以消除按键抖动带来的影响while (P3_1 == 0); // 判断 K1 按键是否仍处于按下状态,松手检测Delay(20); // 延时,以消除按键抖动带来的影响P2_0 = ~P2_0; // LED1 取反}}
}
- 3-3 独立按键控制 LED 显示二进制
#include <REGX52.H>void Delay(unsigned int xms) {unsigned char i, j;while (xms--) {i = 2;j = 239;do {while (--j);} while (--i);}
}void main() {unsigned char LEDNum = 0; // 无符号字符型(所占1字节 = 8bit位)刚好对应着 8 位二进制的数据while (1) {if (P3_1 == 0) { // 如果 K1 按键按下Delay(20); // 延时消抖while (P3_1 == 0); // 判断 K1 按键是否仍处于按下状态,松手检测Delay(20); // 延时消抖LEDNum++; // 变量自增,用于切换 LED 灯的状态// P2 口上电之后和单片机的 IO 上电一样都是默认的是高电平:1111 1111P2 = ~LEDNum; // 变量取反输出给 LED,控制 LED 灯的亮灭}}
}
- 3-4 独立按键控制 LED 移位
// K1 = P3_1;K2 = P3_0;K3 = P3_2;K4 = P3_3
#include <REGX52.H>void Delay(unsigned int xms);
unsigned char LEDNum; // 全局变量定义默认为 0void main() {P2 = ~0x01; // 上电默认 LED1 点亮while (1) {if (P3_1 == 0) { // 如果 K1 按键按下Delay(20);while (P3_1 == 0);Delay(20);LEDNum++; // LEDNum 自增if (LEDNum >= 8) // 限制 LEDNum 自增范围LEDNum = 0;P2 = ~(0x01 << LEDNum); // LED 的第 LEDNum 位点亮}if (P3_0 == 0) { // 如果 K2 按键按下Delay(20);while (P3_0 == 0);Delay(20);if (LEDNum == 0) // LEDNum 减到 0 后变为 7LEDNum = 7;else // LEDNum 未减到 0,自减LEDNum--;P2 = ~(0x01 << LEDNum); // LED 的第 LEDNum 位点亮}}
}void Delay(unsigned int xms) {unsigned char i, j;while (xms--) {i = 2;j = 239;do {while (--j);} while (--i);}
}
4. 数码管
4.1 简介
- LED 数码管:一种简单、廉价的显示器,是由多个发光二极管封装在一起组成 “8” 字型的器件
- 数码管分共阳数码管和共阴数码管
- 共阳数码管:把 8 段 LED 的正极并在一起作为公共端连接在 5V 上(共阳极),然后 8 个 LED 通过单片机的 8 个 IO 端口输出高低电平使其决定点亮哪几个段
- 数码管其实就是 8 个段的发光二极管,只点亮其中的几个段即可显示出数字或字母用来表达信息
- 数码管分共阳数码管和共阴数码管
4.2 数码管的引脚定义
-
以共阴极为例(下图右上),若要显示数字 6
- 1、把共阴极的公共端(位选端)接地/负极,即给这个数据 “0” 或是低电平
- 2、把段码 A、C、D、E、F、G 接正极,即给这个数据 “1” 或是高电平
-
以共阴极为例(下图右上),若要在第三位数码管显示数字 1
- 1、把共阴极的公共端(位选端)当中的第三位数码管接地/负极,即给这个数据 “0” 或是低电平
- 2、再给 1、2、4 上的位选给 “1” 或是高电平
4.3 数码管原理图
-
LED1~LED8 都是接到 138 译码器上的输出端,138 译码器原理如下
- 把 P22、P23、P24 三个端口变成 8 个端口(LED1~LED8)来控制
- 左边的 A、B、C 是输入端(正极),右边 Y0~Y7 是输出端(负极)
- C 是高位、B 在中间、A 是低位
- C B A 按高、低位排序后,再将二进制转换为十进制数,对应着输出端 Y0~Y7,例如 C B A:0 0 0 = Y0,C B A:0 0 1 = Y1(对应 LED2),C B A:1 0 1 = Y5,C B A:1 1 0 = Y6
- 右下角三个引脚称为使能端(相当于一种开关,如果使能电平有效,它就可以工作)
-
74HC245 芯片作用:也称双向数据缓冲器,用来提高芯片驱动能力
-
电容 CC2 作用:起到电源滤波作用,使得芯片的供电更加稳定
-
RP4 电阻:限流作用,100R 单位为 Ω
-
位选
- 如 C B A = 0 1 1 = Y3 = LED4,LED4 就是有效的/允许显示数码管的,那么其它的数码管是不能被允许显示的/不是有效的
-
段选
- 选中之后,就是给 P0 口段码的数据:假设给上数据,经过缓冲送到公共端的段码端。那么,送到段码端就会显示数码管相对应的数字,P0 口给上数据是从高位到低位给上段码端的
4.3 C51 数组 & 子函数
-
数组:把相同类型的一系列数据统一编制到某一个组别中,可以通过数组名 + 索引号简单快捷的操作大量数据
int x[3]; // 定义一组变量(3个) int x[]={1,2,3}; // 定义一组变量并初始化x[0]; //引用数组的第0个变量 x[1]; //引用数组的第1个变量 x[2]; //引用数组的第2个变量 // 引用 x[3] 时,数组越界,读出的数值不确定,应避免这种操作
-
子函数:将完成某一种功能的程序代码单独抽取出来形成一个模块,在其它函数中可随时调用此模块,以达到代码的复用和优化程序结构的目的
void Function(unsigned char x, y) {}返回值 函数名(形参){函数体 }
4.4 数码管段码表
4.5 数码管驱动方式
- 单片机直接扫描:硬件设备简单,但会耗费大量的单片机 CPU 时间
- 专用驱动芯片:内部自带显存、扫描电路,单片机只需告诉它显示什么即可(如下述 TM1640)
4.6 示例代码
- 4-1 静态数码管显示
#include <REGX52.H>// 数码管段码表
unsigned char NixieTable[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};// Location:数码管的位置,Number:显示数码管的数字
void Nixie(unsigned char Location, Number) {switch (Location) { // 位码输出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[Number]; // 段码输出
}void main() {Nixie (2, 3); // 在数码管的第 2 位置显示 3while (1) {}
}
- 4-2 动态数码管显示
#include <REGX52.H>// 数码管段码表
unsigned char NixieTable[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};//延时子函数
void Delay(unsigned int xms) {unsigned char i, j;while (xms--) {i = 2;j = 239;do {while (--j);} while (--i);}
}// Location:数码管的位置,Number:显示数码管的数字
void Nixie(unsigned char Location, Number) {switch (Location) { // 位码输出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[Number]; // 段码输出Delay(1); // 显示一段时间P0 = 0x00; // 段码清 0,消影
}void main() {while (1) {Nixie(1, 1); // 在数码管的第 1 位置显示 1
// Delay(20);Nixie(2, 2); // 在数码管的第 2 位置显示 2
// Delay(20);Nixie(3, 3); // 在数码管的第 3 位置显示 3
// Delay(20);}
}
5. 模块化编程和 LCD 调试工具
5.1 模块化编程
- 传统方式编程
- 所有的函数均放在 main.c 里,若使用的模块比较多,则一个文件内会有很多的代码,不利于代码的组织和管理,而且很影响编程者的思路
- 模块化编程
- 把各个模块代码放在不同 .c 文件里,在 .h 文件里提供外部可调用函数的声明,其它 .c 文件想使用其中的代码时,只需 #include “XXX.h” 文件即可,模块化编程可极大的提高代码可读性、可维护性、可移植性等
- .c 文件:函数、变量的定义
- .h 文件:可被外部调用的函数、变量的声明
- 把各个模块代码放在不同 .c 文件里,在 .h 文件里提供外部可调用函数的声明,其它 .c 文件想使用其中的代码时,只需 #include “XXX.h” 文件即可,模块化编程可极大的提高代码可读性、可维护性、可移植性等
- 注意事项
- 任何自定义的变量、函数在调用前必须有定义或声明(同一个.c)
- 使用到的自定义函数的 .c 文件必须添加到工程参与编译
- 使用到的 .h 文件必须要放在编译器可寻找到的地方(工程文件夹根目录、安装目录、自定义)
5.2 C 预编译
- C 语言的预编译以 # 开头,作用是在真正的编译开始之前,对代码做一些处理(预编译)
5.3 LCD1602 调试工具
- 使用 LCD1602 液晶屏作为调试窗口,提供类似 printf 函数的功能,可实时观察单片机内部数据的变换情况,便于调试和演示
- LCD1602 连接的口是 P0 口 还占用三个 P2 口,所以使用 LCD1602 液晶屏后,三个 LED 就不能进行使用,数码管也不能使用
- LCD1602 原理图
5.4 使用示例
#include <REGX52.H>
#include <LCD1602.H>int main(void) {unsigned int Number = 51;signed int negative = -1;LCD_Init();while (1) {LCD_ShowChar(1, 1, 'W');LCD_ShowString(1, 2, "XH");LCD_ShowNum(1, 4, Number,2);LCD_ShowSignedNum(1, 7, negative, 1);LCD_ShowHexNum(2, 1, 0xFF, 2);LCD_ShowBinNum(2, 4, 0x00, 8);}
}
6. 矩阵键盘
6.1 简介
-
在键盘中按键数量较多时,为了减少 I/O 口的占用,通常将按键排列成矩阵形式,采用逐行或逐列的 “扫描”,就可以读出任何位置按键的状态
-
扫描
- 数码管扫描(输出扫描)
- 原理:显示第 1 位 → 显示第 2 位 → 显示第 3 位→……,然后快速循环这个过程,最终实现所有数码管同时显示的效果
- 矩阵键盘扫描(输入扫描)
- 原理:读取第 1 行(列) → 读取第 2 行(列) → 读取第 3 行(列) → ……,然后快速循环这个过程,最终实现所有按键同时检测的效果
以上两种扫描方式的共性:节省 I/O 口
- 数码管扫描(输出扫描)
-
单片机 IO 口模式
- 单片机的 IO 口是一种弱上拉的模式,又被称作是准双向口(input,output 既可以输入又可以输出)
-
为什么单片机它的 IO 口是默认为高电平呢?
- 是因为有一个上拉电阻把低电平变成高电平了,所以才导致单片机是高电平
- 还有一个是当口线输出为 1 的时候驱动能力很弱,允许外部装置将其拉低
- 当引脚的输出为低电平的时候,它的驱动能力很强,可以吸收相当大的电流
- 单片机中 P1、P2、P3 都是一种弱上拉的一种模式
6.2 矩阵键盘原理图
6.3 使用示例
- MatrixKey.h
#ifndef __MATRIXKEY_H__
#define __MATRIXKEY_H__unsigned char MatrixKey();#endif
- MatrixKey.c
#include <REGX52.H>
#include "Delay.h"/*** @brief 矩阵键盘读取按键键码* @param 无* @retval KeyNumber 按下按键的键码值如果按键按下不放,程序会停留在此函数,松手的一瞬间,返回按键键码,没有按键按下时,返回 0
*/
unsigned char MatrixKey() {unsigned char KeyNumber = 0;P1 = 0xFF;P1_3 = 0;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;P1_2 = 0;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;P1_1 = 0;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;P1_0 = 0;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;
}
- main.c
#include <REGX52.H>
#include "Delay.h" // 包含 Delay 头文件
#include "LCD1602.h" // 包含 LCD1602 头文件
#include "MatrixKey.h" // 包含矩阵键盘头文件unsigned char KeyNum;void main() {LCD_Init(); // LCD 初始化LCD_ShowString(1, 1, "MatrixKey:"); // LCD 显示字符串while (1) {KeyNum = MatrixKey(); // 获取矩阵键盘键码if (KeyNum) { // 如果有按键按下LCD_ShowNum(2, 1, KeyNum, 2); // LCD 显示键码}}
}
6.4 矩阵键盘密码锁
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"unsigned char KeyNum;
unsigned int Password, Count;void main() {LCD_Init();LCD_ShowString(1, 1, "Password:");while (1) {KeyNum = MatrixKey();if (KeyNum) {if (KeyNum <= 10) { // 如果 S1~S10 按键按下,输入密码if (Count<4) { // 如果输入次数小于 4Password *= 10; // 密码左移一位Password += KeyNum % 10; // 获取一位密码Count++; // 计次加一}LCD_ShowNum(2, 1, Password, 4); // 更新显示}if (KeyNum == 11) { // 如果 S11 按键按下,确认if (Password == 2345) { // 如果密码等于正确密码LCD_ShowString(1, 14, "OK "); // 显示 OKPassword = 0; // 密码清零Count = 0; // 计次清零LCD_ShowNum(2, 1, Password, 4); // 更新显示} else {LCD_ShowString(1, 14, "ERR"); // 显示 ERRPassword = 0; // 密码清零Count = 0; // 计次清零LCD_ShowNum(2, 1, Password, 4); // 更新显示}}if (KeyNum == 12) { // 如果 S12 按键按下,取消Password = 0; // 密码清零Count = 0; // 计次清零LCD_ShowNum(2, 1, Password, 4); // 更新显示}}}
}