基于江科大自化协B站教学视频《51单片机入门教程-2020版 程序全程纯手打 从零开始入门》
一、单片机介绍
单片机,英文Micro Controller Unit,简称MCU
内部集成了CPU、RAM、ROM、定时器、中断系统、通讯接口等一系列电脑的常用硬件功能
单片机的任务是信息采集(依靠传感器)、处理(依靠CPU)和硬件设备(例如电机,LED等)的控制
单片机跟计算机相比,单片机算是一个袖珍版计算机,一个芯片就能构成完整的计算机系统。但在性能上,与计算机相差甚远,但单片机成本低、体积小、结构简单,在生活和工业控制领域大有所用
同时,学习使用单片机是了解计算机原理与结构的最佳选择
51单片机是指80年代Intel开发的8051单片机内核的统称,就是因为这个“8051”有个51,所以凡是与“8051”内核一样的单片机都统称为51系列单片机
二、LED代码
2-1点亮一个LED
这里使用的普中单片机的LED是共阳极,所以将P2寄存器设置值为如下0xFE //1111 1110
#include <REGX52.H>voidmain){P2=0xFE; //1111 1110}
2-2LED循环闪烁
通过调用延时函数实现闪烁
#include <REGX52.H>#include <INTRINS.H> //_nop_)函数所在库voidDelay500ms) //@11.0592MHz{unsignedchari, j, k;_nop_); //空操作,延时一个指令的时间i=4;j=129;k=119;do{do{while --k);} while --j);} while --i);}voidmain){while1){P2=0x00;Delay500ms); //这里是通过STC-ISP软件生成的延时函数调用P2=0xFF;Delay500ms);}}
2-3LED流水灯
通过进行P2寄存器的不断延时赋值实现流水灯-基础版
#include <REGX52.H>#include <INTRINS.H> //_nop_)函数所在库voidDelay500ms) //@11.0592MHz{unsignedchari, j, k;_nop_); //空操作,延时一个指令的时间i=4;j=129;k=119;do{do{while --k);} while --j);} while --i);}voidmain){while1){P2=0xFE; //1111 1110Delay500ms); //这里是通过STC-ISP软件生成的延时函数调用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-4LED流水灯Plus
优化含参延时函数,但是会有一定的误差
#include <REGX52.H>#include <INTRINS.H> //_nop_)函数所在库voidDelay1msxms) //@11.0592MHz{unsignedchari, j;whilexms){_nop_);i=2;j=199;do{while --j);} while --i);xms--;}}voidmain){while1){P2=0xFE; //1111 1110Delay1ms500); //优化含参延时函数P2=0xFD; //1111 1101Delay1ms500);P2=0xFB; //1111 1011Delay1ms500);P2=0xF7; //1111 0111Delay1ms500);P2=0xEF; //1110 1111Delay1ms500); P2=0xDF; //1101 1111Delay1ms500);P2=0xBF; //1011 1111Delay1ms500);P2=0x7F; //0111 1111Delay1ms500);}}
三、独立按键与LED
-
轻触按键:相当于是一种电子开关,按下时开关接通,松开时开关断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开
-
LED发光二极管:Light Emitting Diode
-
LED正极引脚较长,负极引脚较短
-
LED正极闪电极较小,负极闪电极较小(在玻璃灯罩里面的)
3-1独立按键控制LED亮灭
通过循环和判断实现按键控制LED亮灭功能
#include <REGX52.H>voidmain){while1){ifP3_1==0)P2_0=0;elseP2_0=1;}}
3-2独立按键控制LED状态
-
按键的抖动
对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动
为了避免误操作,需要进行消抖处理:硬件消抖、软件消抖
硬件消抖:RS触发器、电容充放电滤波等方式
软件消抖:Delay函数延时消抖,定时消抖以及外部中断消抖
#include <REGX52.H>
#include <intrins.h>void Delayunsigned int xms) //@11.0592MHz
{unsigned char i, j;whilexms){ _nop_);i = 2;j = 199;do{while --j);} while --i);xms--;}
}void main)
{while1){ifP3_1==0){Delay20);whileP3_1==0); //判断是否还在按下按键,等松开时跳出循环,并延时消抖,执行相关操作Delay20);P2_0=~P2_0;}}
}
3-3独立按键控制LED显示二进制
通过二进制值的输出来让LED显示
#include <REGX52.H>
#include <intrins.h>void Delayunsigned int xms) //@11.0592MHz
{unsigned char i, j;whilexms--){ _nop_);i = 2;j = 199;do{while --j);} while --i);}
}
void main)
{unsigned char LEDnum; //定义无符号字符变量用来存储二进制while1){ ifP3_1==0){Delay20);whileP3_1==0);Delay20);LEDnum++; //逐渐递增P2=~LEDnum; //并将二进制结果传给P2寄存器,因为LED共阳极,低电平亮,所以取反// P2_0=~P2_0; //这样不行是因为会导致P2_0值改变}}
}
3-4独立按键控制LED移位
通过C语言移位指令以及对LEDNum变量值的控制来完成LED移位显示操作
#include <REGX52.H>
#include <intrins.h>void Delayunsigned int xms) //@11.0592MHz
{unsigned char i, j;whilexms--){ _nop_);i = 2;j = 199;do{while --j);} while --i);}
}unsigned char LEDnum;void main)
{while1){ifP3_1==0){Delay20);whileP3_1==0);Delay20);LEDnum++;ifLEDnum>=8) //当到8的时候应该先清零再进行取反,不然灯会不亮LEDnum=0;P2=~0x01<<LEDnum);}ifP3_0==0){Delay20);whileP3_0==0);Delay20);ifLEDnum==0) //因为定义的是无符号字符变量,所以为了避免向下溢出,进行0值判断置7操作LEDnum=7;elseLEDnum--;P2=~0x01<<LEDnum);}}}
四、数码管
4-1静态数码管显示
注意若用开发板的时候不要接LCD1602显示屏,因为引脚共用原因会导致数字显示异常
#include <REGX52.H>#include <intrins.h>voidDelayunsignedintxms) //@11.0592MHz{unsignedchari, j;whilexms){ _nop_);i=2;j=199;do{while --j);} while --i);xms--;}}unsignedcharNixieTable[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0x00}; //定义数码管数组,用来存储显示对应数字时应该给寄存器赋的值voidNixieunsignedcharLocation,Number){switchLocation) //Location用来接收数字在哪一个数码管显示,通过三八译码器来实现数码管的选择{case1:P2_4=1;P2_3=1;P2_2=1;break; case2:P2_4=1;P2_3=1;P2_2=0;break;case3:P2_4=1;P2_3=0;P2_2=1;break;case4:P2_4=1;P2_3=0;P2_2=0;break;case5:P2_4=0;P2_3=1;P2_2=1;break;case6:P2_4=0;P2_3=1;P2_2=0;break;case7:P2_4=0;P2_3=0;P2_2=1;break;case8:P2_4=0;P2_3=0;P2_2=0;break;}P0=NixieTable[Number];}voidmain) {while1){Nixie1,1); //测试数字循环显示Delay500);Nixie2,2);Delay500);Nixie3,3);Delay500);Nixie4,4);Delay500);Nixie5,5);Delay500);Nixie6,6);Delay500);Nixie7,7);Delay500);Nixie8,8);Delay500);Nixie1,9);Delay500); Nixie2,0);Delay500);Nixie3,10); //以下为ABCDEFDelay500);Nixie4,11);Delay500);Nixie5,12);Delay500);Nixie6,13);Delay500);Nixie7,14);Delay500);Nixie8,15);Delay500);}}
4-2动态数码管显示
想要进行多个数码管同时显示,需要进行消影操作
出现串位显示的原因是因为数码管的位选和段选出现了交叉,导致串位:位选-段选-位选-段选-位选-段选
消影:位选-段选-清零-位选-段选-清零
#include <REGX52.H>#include <intrins.h>voidDelayunsignedintxms) //@11.0592MHz{unsignedchari, j;whilexms){ _nop_);i=2;j=199;do{while --j);} while --i);xms--;}}unsignedcharNixieTable[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0x00};voidNixieunsignedcharLocation,Number){switchLocation){case1:P2_4=1;P2_3=1;P2_2=1;break;case2:P2_4=1;P2_3=1;P2_2=0;break;case3:P2_4=1;P2_3=0;P2_2=1;break;case4:P2_4=1;P2_3=0;P2_2=0;break;case5:P2_4=0;P2_3=1;P2_2=1;break;case6:P2_4=0;P2_3=1;P2_2=0;break;case7:P2_4=0;P2_3=0;P2_2=1;break;case8:P2_4=0;P2_3=0;P2_2=0;break;}P0=NixieTable[Number];Delay1); //保持显示,避免变暗P0=0X00; //清零进行消影}voidmain) {while1){Nixie1,1);// Delay500); //消除延时之后,再加上消影功能就可以实现同时显示Nixie2,2);// Delay500);Nixie3,3);}}
数码管驱动方式:
1.单片机直接扫描:硬件设备简单,但会耗费大量的单片机CPU时间
2.专用驱动芯片:内部自带显存、扫描电路,单片机只需告诉它显示什么即可
以上代码均为针对单片机直接扫描实现的,while1)不断循环实现
五、LCD1602(模块化编程开始)
5-1模块化编程
模块化编程:
•传统方式编程:所有的函数均放在main.c里,若使用的模块比较多,则一个文件内会有很多的代码,不利于代码的组织和管理,而且很影响编程者的思路
•模块化编程:把各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用函数的声明,其它.c文件想使用其中的代码时,只需要#include "XXX.h"文件即可。使用模块化编程可极大的提高代码的可阅读性、可维护性、可移植性等
以Delay函数为例
-
main.c
#include "Delay.h"voidmain){Delay10);}
-
Delay.h
#ifndef __DELAY_H__#define __DELAY_H__voidDelayunsignedintxms);#endif
-
Delay.c
voidDelayunsignedintxms){unsignedchari, j;whilexms--){i=2;j=239;do{while --j);} while --i);}}
-
图示
模块化实现数码管显示
-
main.c
#include <REGX52.H>#include "Delay.h"#include "Nixie.h"voidmain) {while1){Nixie1,1);// Delay500);Nixie2,2);// Delay500);Nixie3,3);Nixie4,4);Nixie5,5);}}
-
Delay.h
#ifndef __DELAY_H__
#define __DELAY_H__void Delayunsigned int xms);
#endif
-
Nixie.h
#ifndef __NIXIE_H__
#define __NIXIE_H__void Nixieunsigned char Location,Number);
#endif
-
Delay.c
#include <intrins.h>void Delayunsigned int xms) //@11.0592MHz
{unsigned char i, j;whilexms){ _nop_);i = 2;j = 199;do{while --j);} while --i);xms--;}
}
-
Nixie.c
#include "Delay.h"
#include <REGX52.H>unsigned char NixieTable[]=
{0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0x00};void Nixieunsigned char Location,Number)
{switchLocation){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];Delay1);P0=0X00;
}
5-2LCD1602调试工具*
LCD1602调试函数模块
不同的LCD1602调试函数代码决定了不同的调试函数以及使用方法
使用时应注意对应的文件函数的异同
函数 |
作用 |
LCD_Init); |
初始化 |
LCD_ShowChar1,1,'A'); |
显示一个字符 |
LCD_ShowString1,3,"Hello"); |
显示字符串 |
LCD_ShowNum1,9,123,3); |
显示十进制数字 |
LCD_ShowSignedNum1,13,-66,2); |
显示有符号十进制数字 |
LCD_ShowHexNum2,1,0xA8,2); |
显示十六进制数字 |
LCD_ShowBinNum2,4,0xAA,8); |
显示二进制数字 |
代码暂略,等后续对头文件以及源代码进行全方面分析之后再复盘补充
六、矩阵键盘
6-1矩阵键盘*
涉及到了施密特触发电路和上拉电阻和下拉电阻,等模电再学一学复盘一下
这里同样使用了前面的LCD1602的调试文件:LCD1602.H、LCD1602.C
编译时会出现WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS的报错,是因为引用的LCD1602的文件中的函数虽然定义了,但是并没有在程序中显式调用到定义过的函数,可以无视,但是会浪费RAM
-
main.c
#include "DELAY.H"#include "lcd1602.h"#include "Delay.h"#include "MatrixKey.h"unsignedcharKeyNum; //定义无符号整型用来接收键码voidmain){lcd1602_init); //初始化LCD1602lcd1602_show_string0,0,"MatrixKey:");//在第一列第一行显示MatrixKey:while1){KeyNum=MatrixKey); //调用函数接收键码ifKeyNum) //接受到键码则判断键码值并显示{switchKeyNum){case1: lcd1602_show_string0,1,"1");break;case2: lcd1602_show_string0,1,"2");break;case3: lcd1602_show_string0,1,"3");break;case4: lcd1602_show_string0,1,"4");break;case5: lcd1602_show_string0,1,"5");break;case6: lcd1602_show_string0,1,"6");break;case7: lcd1602_show_string0,1,"7");break;case8: lcd1602_show_string0,1,"8");break;case9: lcd1602_show_string0,1,"9");break;case10: lcd1602_show_string0,1,"10");break;case11: lcd1602_show_string0,1,"11");break;case12: lcd1602_show_string0,1,"12");break;case13: lcd1602_show_string0,1,"13");break;case14: lcd1602_show_string0,1,"14");break;case15: lcd1602_show_string0,1,"15");break;case16: lcd1602_show_string0,1,"16");break;}}}}
-
MatrixKey.h
#ifndef __MATRIXKEY_H__
#define __MATRIXKEY_H__unsigned char MatrixKey);#endif
-
MatrixKey.c
#include <REGX52.H>#include "Delay.h"unsignedcharMatrixKey){unsignedcharKeyNumber=0;P1=0xff; // 全部拉高P1_3=0; //单独留一个低电位进行扫描按键ifP1_7==0){Delay20);whileP1_7==0);Delay20);KeyNumber=1;}ifP1_6==0){Delay20);whileP1_6==0);Delay20);KeyNumber=5;}ifP1_5==0){Delay20);whileP1_5==0);Delay20);KeyNumber=9;}ifP1_4==0){Delay20);whileP1_4==0);Delay20);KeyNumber=13;}P1=0xff; // 全部拉高P1_2=0; //单独留一个低电位进行扫描按键ifP1_7==0){Delay20);whileP1_7==0);Delay20);KeyNumber=2;}ifP1_6==0){Delay20);whileP1_6==0);Delay20);KeyNumber=6;}ifP1_5==0){Delay20);whileP1_5==0);Delay20);KeyNumber=10;}ifP1_4==0){Delay20);whileP1_4==0);Delay20);KeyNumber=14;}P1=0xff; // 全部拉高P1_1=0; //单独留一个低电位进行扫描按键ifP1_7==0){Delay20);whileP1_7==0);Delay20);KeyNumber=3;}ifP1_6==0){Delay20);whileP1_6==0);Delay20);KeyNumber=7;}ifP1_5==0){Delay20);whileP1_5==0);Delay20);KeyNumber=11;}ifP1_4==0){Delay20);whileP1_4==0);Delay20);KeyNumber=15;}P1=0xff; // 全部拉高P1_0=0; //单独留一个低电位进行扫描按键ifP1_7==0){Delay20);whileP1_7==0);Delay20);KeyNumber=4;}ifP1_6==0){Delay20);whileP1_6==0);Delay20);KeyNumber=8;}ifP1_5==0){Delay20);whileP1_5==0);Delay20);KeyNumber=12;}ifP1_4==0){Delay20);whileP1_4==0);Delay20);KeyNumber=16;}returnKeyNumber;}
6-2矩阵键盘密码锁
所使用的前述模块化列表
LCD1602
Delay
MatrixKey
-
main.c
#include "lcd1602.h"#include "Delay.h"#include "MatrixKey.h"unsignedcharKeyNum; //定义KeyNum用于接收键码值unsignedintPassword,Count; //定义无符号整型用于存储 密码值 和 密码位数voidmain){LCD_Init);LCD_ShowString0,1,"Password:");while1){KeyNum=MatrixKey);ifKeyNum){ifKeyNum<=10) //如果S1~S10按键按下,输入密码{ifCount<4) //如果位数不超过设定范围{Password*=10; //向左移动一位Password+=KeyNum%10; //累加密码Count++; //计次加一,确保四位} LCD_ShowNum1,1,Password,4); //更新显示}ifKeyNum==11) //确认键{ifPassword==2345) //设定密码为2345{LCD_ShowString1,14,"OK!");Password=0; //密码清零Count=0; //计次清零LCD_ShowNum1,1,Password,4); //更新显示}else{LCD_ShowString1,14,"ERR");Password=0; //密码清零Count=0; //计次清零LCD_ShowNum1,1,Password,4); //更新显示}}ifKeyNum==12) //删除键{Password=Password/10; //删除数字LCD_ShowNum1,1,Password,4); //更新显示Count--; //计次减少}}}}
七、定时器
7-1定时器
定时器介绍:
51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成
定时器作用:
(1)用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作
(2)替代长时间的Delay,提高CPU的运行效率和处理速度
(3)多任务处理
(…)
STC89C52定时器资源
•定时器个数:3个(T0、T1、T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源
•注意:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机所共有的
定时器框图
定时器在单片机内部就像一个小闹钟一样,根据时钟的输出信号,每隔“一秒”,计数单元的数值就增加一,当计数单元数值增加到“设定的闹钟提醒时间”时,计数单元就会向中断系统发出中断申请,产生“响铃提醒”,使程序跳转到中断服务函数中执行
定时器工作模式
STC89C52的T0和T1均有四种工作模式:
模式0 |
13位定时器/计数器 |
模式1 |
16位定时器/计数器(常用) |
模式2 |
8位自动重装模式 |
模式3 |
两个8位计数器 |
工作模式1(16位定时器/计数器)框图:
-
SYSclk:系统时钟,用来产生固定频率的脉冲(晶振电路)来使后面的计数器的技术值累加,此时作定时器使用,
-
MCU in 12T/6T mode:分频模式,例如当晶振频率为12MHz,采用MCU in 12T mode时,产生的脉冲频率就是1MHz
-
T0 Pin:外部接口,当由外部接口来提供脉冲时,作计数器使用
-
C/T非:配置寄存器时若其对应的控制字为1,则为Count计数器模式,为0,则为Time定时器模式
-
TH0和TL0:Time High 0和Time Low 0,代表了定时器的高位和低位,后面的0表示计时器0,计数器共十六位,最大计数值为65535
-
TF0:Time Flag 0,表示计时器0的标志位,当达到最大技术值后,下一次计数就会溢出,产生脉冲传送到标志位从而影响后面的Interrupt,从而申请中断