编辑
2021-07-30
浅尝嵌入式开发
00
请注意,本文编写于 786 天前,最后修改于 39 天前,其中某些信息可能已经过时。

目录

STC89C52简介
STC89C52结构
GPIO与管脚图
开发环境搭建
1、GPIO之LED流水灯
2、按键抖动与消抖
3、数码管MCU扫描
4、LCD1602显示字符
5、定时器与中断系统
定时器
中断系统
时钟Demo

最近收拾东西时居然发现了之前买的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流水灯的功能:

c
//此文件中定义了单片机的一些特殊功能寄存器 #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、按键抖动与消抖

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

c
#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时间

② 专用驱动芯片:内部自带显存、扫描电路,单片机只需告诉它显示什么即可

下面是通过单片机直接扫描的方式来完成的数据显示示例:

c
#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:

c
#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:

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

c
#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; } } }

最终效果如下所示:

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!