51单片机学习(上)
最近收拾东西时居然发现了之前买的STM32单片机(普中STM32-F1),之前差点挂闲鱼卖了,很好奇是什么勇气让我直接买了STM32的板子?哈哈哈,买回来直接吃灰,连光盘都在。现在闲暇之余准备系统地学习一下单片机子,没准还能造些有意思的小玩意,那还是从89C51开始吧,哈哈。
单片机(Micro Controller Unit),简称MCU。内部集成了CPU、RAM、ROM、定时器、中断系统、通讯接口等一系列电脑的常用硬件功能。单片机的任务是信息采集(依靠传感器)、处理(依靠CPU)和硬件设备(例如电机,LED等)的控制。单片机跟计算机相比,单片机算是一个袖珍版计算机,一个芯片就能构成完整的计算机系统。但在性能上,与计算机相差甚远,但单片机成本低、体积小、结构简单,在生活和工业控制领域大有所用,而且学习使用单片机是了解计算机原理与结构的最佳选择。
我目前使用的是STC89C52单片机,这种类型的单片机资料也比较多。
STC89C52简介
STC(南通国芯微电子)公司的51单片机系列是8位单片机,所以RAM为512字节,ROM:8K(Flash),工作频率:11.0592MHz(有些是12MHz),命名规则如下:
STC89C52结构
STC89C52系列单片机的内部结构框图如下图所示。STC89C52单片机中包含中央处理器(CPU)、程序存储器(Flash)、数据存储器(SRAM)、定时/计数器、UART串口、I/O接口、EEPROM、看门狗等模块。STC89C52系列单片机几乎包含了数据采集和控制中所需的所有单元模块,可称得上一个微机片上系统。内部结构图:
GPIO与管脚图
GPIO(General Purpose Intput Output)是通用输入输出端口的简称,可以通过软件来控制其输入和输出。51 单片机芯片的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。不过GPIO 最简单的应用还属点亮LED 灯了,只需通过软件控制GPIO 输出高低电平即可。当然 GPIO 还可以作为输入控制,比如在引脚上接入一个按键,通过电平的高低判断按键是否按下等操作。
这些管脚只需要结合开发板原理图就可以确定,哪些管脚和哪些外部设备相连接:
(1)电源引脚:引脚图中的VCC、GND 都属于电源引脚。 (2)晶振引脚:引脚图中的XTAL1、XTAL2 都属于晶振引脚。 (3)复位引脚:引脚图中的RST/VPD 属于复位引脚,不做其他功能使用。 (4)下载引脚:51 单片机的串口功能引脚(TXD、RXD)可以作为下载引脚使用。 (5)GPIO 引脚:引脚图中带有Px.x 等字样的均属于GPIO 引脚。GPIO 占用了芯片大部分的引脚,共达32个,分为了4组,P0、P1、P2、P3,每组为8个IO,而且在P3组中每个IO都具备额外功能,只要通过相应的寄存器设置即可配置对应的附加功能,同一时刻,每个引脚只能使用该引脚的一个功能。
① 51单片机所有IO口都是双向的,即可以作为输入也可以作为输出使用。
② 由于P0口是漏极开路的,所以要操作P0 口必须外接上拉电阻,其他P1、P2、P3 口都内部自带上拉电阻,可以不加,如果要增强IO口驱动能力,可以外接上拉电阻。
开发环境搭建
1、安装Keil5(汇编器、编译器 + 依赖库 + 头文件),Keil5也是一个IDE,可以直接在里面写代码。
2、Keil5也可以用SDCC替代,http://sdcc.sourceforge.net/,SDCC 是一个可重定向、优化的标准 C(ANSI C89、ISO C99、ISO C11)编译器套件,它针对基于 Intel MCS51 的微处理器(8032、8051、8052 等)。
3、安装STC-ISP (烧录器),也可以使用普中ISP,STC-ISP功能会多一些。
4、但是最好还是用VSCode方便一些,快捷键用着也方便,安装 Embedded IDE(https://docs.em-ide.com)插件即可,Embedded IDE会提供8051、STM8、 Cortex-M、RISC-V… 项目的开发、编译、烧录等功能。安装好Embedded IDE后,直接可以Ctrl点进去就能查看源代码,比较方便。
注意:SDCC和Keil5的头文件是不一样的,SDCC用的是 8052.h,Keil5用的是 REGX52.H
1、GPIO之LED流水灯
通过LED流水灯实验,认识GPIO。通过STC-ISP软件生成延时代码,完成LED流水灯的功能:
//此文件中定义了单片机的一些特殊功能寄存器
#include <REGX52.H>
#include <INTRINS.H>
// 睡眠x毫秒
void DelayXms(unsigned int x){
unsigned char i, j;
while(x--){
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void main(){
while(1){
P2 = 0xFE; //1111 1110
DelayXms(500);
P2 = 0xFD; //1111 1101
DelayXms(500);
P2 = 0xFB; //1111 1011
DelayXms(500);
P2 = 0xF7; //1111 0111
DelayXms(500);
P2 = 0xEF; //1110 1111
DelayXms(500);
P2 = 0xDF; //1101 1111
DelayXms(500);
P2 = 0xBF; //1011 1111
DelayXms(500);
P2 = 0x7F; //0111 1111
DelayXms(500);
}
}
2、按键抖动与消抖
对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动。
#include <REGX52.H>
void DelayXms(unsigned int x)
{
unsigned char i, j;
while (x--)
{
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
}
void main(){
while (1)
{
P2_1 = 1;
if(P3_1 == 0){
DelayXms(20); // 消除按下抖动
while (P3_1 == 0);
DelayXms(20); // 消除松手抖动
P2_0 = ~P2_0; // 取反就是开和关
}
}
}
3、数码管MCU扫描
LED数码管:数码管是一种简单、廉价的显示器,是由多个发光二极管封装在一起组成8字型的器件。数码管一般有两种驱动方式:
① 单片机直接扫描:硬件设备简单,但会耗费大量的单片机CPU时间
② 专用驱动芯片:内部自带显存、扫描电路,单片机只需告诉它显示什么即可
下面是通过单片机直接扫描的方式来完成的数据显示示例:
#include <REG52.H>
#include <INTRINS.H>
#include <stdlib.h>
typedef unsigned char uint8;
uint8 hexArr[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F,
0x80, // -> 10 小数点
0x40, // -> 11 负 号
};
void DelayXms(unsigned int x) //@11.0592MHz
{
unsigned char i, j;
while (x--)
{
_nop_(); // 空语句
i = 2;
j = 199;
do
{
while (--j)
;
} while (--i);
}
}
/**
* x: LED 0-7
* num: 0-9,10为小数点, 11为负号
**/
void showNumLed(uint8 x, uint8 num)
{
switch (x)
{
case 0: P2_2 = 1; P2_3 = 1; P2_4 = 1; break;
case 1: P2_2 = 0; P2_3 = 1; P2_4 = 1; break;
case 2: P2_2 = 1; P2_3 = 0; P2_4 = 1; break;
case 3: P2_2 = 0; P2_3 = 0; P2_4 = 1; break;
case 4: P2_2 = 1; P2_3 = 1; P2_4 = 0; break;
case 5: P2_2 = 0; P2_3 = 1; P2_4 = 0; break;
case 6: P2_2 = 1; P2_3 = 0; P2_4 = 0; break;
case 7: P2_2 = 0; P2_3 = 0; P2_4 = 0; break;
default:
break;
}
// P0 = 0x7D; -> 6
P0 = hexArr[num];
}
void numToCharArray(char arr[], long num){ // 123456
int i;
if(num < 0) num = -num;
for (i = 7; i >= 0; i--)
{
if(num == 0) break;
arr[i] = num % 10;
num /= 10;
}
}
void showLongByLED(long num){
int i;
int tmp;
// 如果是负数
if(num < 0){
//showNumLed(1, 10);
if(num < -9999999L){
// 溢出
while (1)
{
for (i = 0; i < 8; i++)
{
showNumLed(i, 11);
DelayXms(2);
P0 = 0x00; // 消影
}
}
}else {
char ch[8] = {0};
numToCharArray(ch, num);
for(i = 0; i < 8; i++){
if(ch[i] != 0){ // 0390 9999
tmp = i;
while(1){
showNumLed(tmp - 1, 11);
DelayXms(2);
P0 = 0x00;
for (i = tmp; i < 8; i++)
{
showNumLed(i, ch[i]);
DelayXms(2);
P0 = 0x00; // 消影
}
}
}
}
}
}else if(num == 0){
showNumLed(8, 0);
}else {
char ch[8] = {0};
numToCharArray(ch, num);
for(i = 0; i < 8; i++){
if(ch[i] != 0){
tmp = i;
while(1){
for (i = tmp; i < 8; i++)
{
showNumLed(i, ch[i]);
DelayXms(2);
P0 = 0x00; // 消影
}
}
}
}
}
}
void showLongByLEDAuto(long num){
int i;
int tmp;
// 如果是负数
if(num < 0){
//showNumLed(1, 10);
if(num < -9999999L){
// 溢出
for (i = 1; i <= 8; i++)
{
showNumLed(i, 11);
DelayXms(2);
P0 = 0x00; // 消影
}
}else {
}
}else if(num == 0){
showNumLed(8, 0);
}else {
char ch[8] = {0};
numToCharArray(ch, num);
for(i = 0; i < 8; i++){
if(ch[i] != 0){
tmp = i;
for (i = tmp; i < 8; i++)
{
showNumLed(i, ch[i]);
DelayXms(2);
P0 = 0x00; // 消影
}
}
}
}
}
void main()
{
int i;
int flag = 0;
for (i = 0; i < 10; i++)
{
showNumLed(0, i);
DelayXms(100);
}
showLongByLED(-8900999L);
while(1);
}
这种通过单片机扫描的方式还是过于复杂,而且比较浪费单片机的计算机资源,推荐还是用专用驱动芯片,内部自带显存、扫描电路,单片机只需告诉它显示什么即可,TM1640就是这样一种芯片:
TM1640是一种LED(发光二极管显示器)驱动控制专用电路,内部集成有MCU数字接口、数据锁存器LED驱动等电路。本产品性能优良,质量可靠。主要应用于电子产品LED显示屏驱动。采用SOP28、SSOP28的封装形式。
4、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_H__
LCD1602.c:
#include <REG52.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');
}
}
5、定时器与中断系统
定时器
51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。
1、定时器用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作。 2、替代长时间的Delay,提高CPU的运行效率和处理速度。
51系列单片机一定有基本的2个定时器,但是STC89C52具有3个定时器(T0、T1、T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源。需要注意的是:定时器的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的定时器个数和操作方式,但一般来说,T0和T1的操作方式是所有51单片机所共有的。而且通常我们使用的都是基本的2个定时器:T0与T1。
定时器在单片机内部就像一个小闹钟一样,根据时钟的输出信号,每隔一段时间,计数单元的数值就增加一,当计数单元数值增加到"设定的闹钟提醒时间"时,计数单元就会向中断系统发出中断申请,产生"响铃提醒",使程序跳转到中断服务函数中执行。
STC89C52的T0和T1均有四种工作模式:
13位定时器/计数器、16位定时器/计数器(常用)、8位自动重装模式、两个8位计数器
以定时器T0为例,通过定时器中断控制D1指示灯间隔1秒闪烁。
中断系统
中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的。
当中央处理机CPU正在处理某件事的时候外界发生了紧急事件请求,要求CPU暂停当前的工作,转而去处理这个紧急事件,处理完以后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断。实现这种功能的部件称为中断系统,请示CPU中断的请求源称为中断源。微型机的中断系统一般允许多个中断源,当儿个中断源同时向CPU请求中断,要求为它服务的时候,这就存在CPU优先响应哪一个中断源请求的问题。通常根据中断源的轻重缓急排队,优先处理最紧急事件的中断请求源,即规定每一个中断源有一个优先级别。CPU总是先响应优先级别最高的中断请求。
当CPU正在处理一个中断源请求的时候(执行相应的中断服务程序),发生了另外一个优先级比它还高的中断源请求。如果CPU能够暂停对原来中断源的服务程序,转而去处理优先级更高的中断请求源,处理完以后,再回到原低级中断服务程序,这样的过程称为中断嵌套。这样的中断系统称为多级中断系统,没有中断嵌套功能的中断系统称为单级中断系统。
STC89C52系列单片机提供了8个中断请求源,它们分别是:外部中断O(INTO)、定时器0中断、外部中断1(INT1)、定时器1中断、串口(UART)中断、定时器2中断、外部中断2(INT2)、外部中断3(INT3)。所有的中断都具有4个中断优先级。中断的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的中断资源,例如中断源个数不同、中断优先级个数不同等等,下面是STC89C51的中断号:
寄存器是连接软硬件的媒介。在单片机中寄存器就是一段特殊的RAM存储器,一方面,寄存器可以存储和读取数据,另一方面,就像每一个寄存器背后都连接了一根导线,控制着电路的连接方式。寄存器相当于一个复杂机器的"操作按钮" 。
通过配置中断部分的寄存器就可以理解为什么需要做这样的配置:
时钟Demo
#include <REGX51.H>
#include "LCD1602.h"
unsigned int s, m, h;
void Timer0Init(void) //1毫秒@11.0592MHz
{
// 配置定时器 -----------------------
TMOD &= 0xF0; // 设置定时器模式
TMOD |= 0x01; // 设置定时器模式
TL0 = 0x66; // 设置定时初始值
TH0 = 0xFC; // 设置定时初始值
TF0 = 0; // 清除TF0标志
TR0 = 1; // 定时器0开始计时
// 配置中断 -------------------------
ET0 = 1;
EA = 1;
PT0 = 0;
}
void main()
{
LCD_Init();
Timer0Init();
LCD_ShowString(1, 1, "Clock:");
LCD_ShowString(2, 3, ":");
LCD_ShowString(2, 6, ":");
while (1){
// LCD显示属于耗时操作,不要放在定时器中断中完成
LCD_ShowNum(2, 1, h, 2);
LCD_ShowNum(2, 4, m, 2);
LCD_ShowNum(2, 7, s, 2);
}
}
// 中断函数
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count = 0;
TL0 = 0x66; // 设置定时初始值
TH0 = 0xFC; // 设置定时初始值
T0Count++;
if(T0Count > 1000){
T0Count = 0;
s++;
// 时、分、秒
if(s >= 60){
s = 0;
m++;
}
if(m >= 60){
m = 0;
h++;
}
if(h >= 24){
h = 0;
}
}
}
最终效果如下所示: