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流水灯的功能:
1//此文件中定义了单片机的一些特殊功能寄存器
2#include <REGX52.H>
3#include <INTRINS.H>
4
5// 睡眠x毫秒
6void DelayXms(unsigned int x){
7 unsigned char i, j;
8 while(x--){
9 _nop_();
10 i = 2;
11 j = 199;
12 do
13 {
14 while (--j);
15 } while (--i);
16 }
17}
18
19void main(){
20 while(1){
21 P2 = 0xFE; //1111 1110
22 DelayXms(500);
23
24 P2 = 0xFD; //1111 1101
25 DelayXms(500);
26
27 P2 = 0xFB; //1111 1011
28 DelayXms(500);
29
30 P2 = 0xF7; //1111 0111
31 DelayXms(500);
32
33 P2 = 0xEF; //1110 1111
34 DelayXms(500);
35
36 P2 = 0xDF; //1101 1111
37 DelayXms(500);
38
39 P2 = 0xBF; //1011 1111
40 DelayXms(500);
41
42 P2 = 0x7F; //0111 1111
43 DelayXms(500);
44 }
45}
2、按键抖动与消抖
对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动。
1#include <REGX52.H>
2
3void DelayXms(unsigned int x)
4{
5 unsigned char i, j;
6 while (x--)
7 {
8 i = 2;
9 j = 199;
10 do
11 {
12 while (--j);
13 } while (--i);
14 }
15}
16
17void main(){
18 while (1)
19 {
20 P2_1 = 1;
21 if(P3_1 == 0){
22 DelayXms(20); // 消除按下抖动
23 while (P3_1 == 0);
24 DelayXms(20); // 消除松手抖动
25 P2_0 = ~P2_0; // 取反就是开和关
26 }
27 }
28}
3、数码管MCU扫描
LED数码管:数码管是一种简单、廉价的显示器,是由多个发光二极管封装在一起组成8字型的器件。数码管一般有两种驱动方式:
① 单片机直接扫描:硬件设备简单,但会耗费大量的单片机CPU时间
② 专用驱动芯片:内部自带显存、扫描电路,单片机只需告诉它显示什么即可
下面是通过单片机直接扫描的方式来完成的数据显示示例:
1#include <REG52.H>
2#include <INTRINS.H>
3#include <stdlib.h>
4
5typedef unsigned char uint8;
6
7uint8 hexArr[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F,
8 0x80, // -> 10 小数点
9 0x40, // -> 11 负 号
10 };
11
12void DelayXms(unsigned int x) //@11.0592MHz
13{
14 unsigned char i, j;
15 while (x--)
16 {
17 _nop_(); // 空语句
18 i = 2;
19 j = 199;
20 do
21 {
22 while (--j)
23 ;
24 } while (--i);
25 }
26}
27
28
29/**
30 * x: LED 0-7
31 * num: 0-9,10为小数点, 11为负号
32 **/
33void showNumLed(uint8 x, uint8 num)
34{
35 switch (x)
36 {
37 case 0: P2_2 = 1; P2_3 = 1; P2_4 = 1; break;
38 case 1: P2_2 = 0; P2_3 = 1; P2_4 = 1; break;
39 case 2: P2_2 = 1; P2_3 = 0; P2_4 = 1; break;
40 case 3: P2_2 = 0; P2_3 = 0; P2_4 = 1; break;
41 case 4: P2_2 = 1; P2_3 = 1; P2_4 = 0; break;
42 case 5: P2_2 = 0; P2_3 = 1; P2_4 = 0; break;
43 case 6: P2_2 = 1; P2_3 = 0; P2_4 = 0; break;
44 case 7: P2_2 = 0; P2_3 = 0; P2_4 = 0; break;
45 default:
46 break;
47 }
48
49 // P0 = 0x7D; -> 6
50 P0 = hexArr[num];
51}
52
53void numToCharArray(char arr[], long num){ // 123456
54 int i;
55 if(num < 0) num = -num;
56 for (i = 7; i >= 0; i--)
57 {
58 if(num == 0) break;
59 arr[i] = num % 10;
60 num /= 10;
61 }
62}
63
64void showLongByLED(long num){
65 int i;
66 int tmp;
67 // 如果是负数
68 if(num < 0){
69 //showNumLed(1, 10);
70 if(num < -9999999L){
71 // 溢出
72 while (1)
73 {
74 for (i = 0; i < 8; i++)
75 {
76 showNumLed(i, 11);
77 DelayXms(2);
78 P0 = 0x00; // 消影
79 }
80 }
81 }else {
82 char ch[8] = {0};
83 numToCharArray(ch, num);
84 for(i = 0; i < 8; i++){
85 if(ch[i] != 0){ // 0390 9999
86 tmp = i;
87 while(1){
88 showNumLed(tmp - 1, 11);
89 DelayXms(2);
90 P0 = 0x00;
91 for (i = tmp; i < 8; i++)
92 {
93 showNumLed(i, ch[i]);
94 DelayXms(2);
95 P0 = 0x00; // 消影
96 }
97 }
98 }
99 }
100 }
101 }else if(num == 0){
102 showNumLed(8, 0);
103 }else {
104 char ch[8] = {0};
105 numToCharArray(ch, num);
106 for(i = 0; i < 8; i++){
107 if(ch[i] != 0){
108 tmp = i;
109 while(1){
110 for (i = tmp; i < 8; i++)
111 {
112 showNumLed(i, ch[i]);
113 DelayXms(2);
114 P0 = 0x00; // 消影
115 }
116 }
117 }
118 }
119 }
120}
121
122
123void showLongByLEDAuto(long num){
124 int i;
125 int tmp;
126 // 如果是负数
127 if(num < 0){
128 //showNumLed(1, 10);
129 if(num < -9999999L){
130 // 溢出
131 for (i = 1; i <= 8; i++)
132 {
133 showNumLed(i, 11);
134 DelayXms(2);
135 P0 = 0x00; // 消影
136 }
137 }else {
138
139 }
140 }else if(num == 0){
141 showNumLed(8, 0);
142 }else {
143 char ch[8] = {0};
144 numToCharArray(ch, num);
145 for(i = 0; i < 8; i++){
146 if(ch[i] != 0){
147 tmp = i;
148 for (i = tmp; i < 8; i++)
149 {
150 showNumLed(i, ch[i]);
151 DelayXms(2);
152 P0 = 0x00; // 消影
153 }
154 }
155 }
156 }
157}
158
159void main()
160{
161 int i;
162 int flag = 0;
163 for (i = 0; i < 10; i++)
164 {
165 showNumLed(0, i);
166 DelayXms(100);
167 }
168
169 showLongByLED(-8900999L);
170 while(1);
171}
这种通过单片机扫描的方式还是过于复杂,而且比较浪费单片机的计算机资源,推荐还是用专用驱动芯片,内部自带显存、扫描电路,单片机只需告诉它显示什么即可,TM1640就是这样一种芯片:
TM1640是一种LED(发光二极管显示器)驱动控制专用电路,内部集成有MCU数字接口、数据锁存器LED驱动等电路。本产品性能优良,质量可靠。主要应用于电子产品LED显示屏驱动。采用SOP28、SSOP28的封装形式。
4、LCD1602显示字符
直接使用下面封装好的函数即可,以后需要模块化的地方一定要模块化,形成代码复用:
LCD1602.h:
1#ifndef __LCD1602_H__
2#define __LCD1602_H__
3
4//用户调用函数:
5void LCD_Init();
6void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
7void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
8void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
9void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
10void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
11void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
12
13#endif //!__LCD1602_H__
LCD1602.c:
1#include <REG52.H>
2
3//引脚配置:
4sbit LCD_RS=P2^6;
5sbit LCD_RW=P2^5;
6sbit LCD_EN=P2^7;
7#define LCD_DataPort P0
8
9//函数定义:
10/**
11 * @brief LCD1602延时函数,12MHz调用可延时1ms
12 * @param 无
13 * @retval 无
14 */
15void LCD_Delay()
16{
17 unsigned char i, j;
18
19 i = 2;
20 j = 239;
21 do
22 {
23 while (--j);
24 } while (--i);
25}
26
27/**
28 * @brief LCD1602写命令
29 * @param Command 要写入的命令
30 * @retval 无
31 */
32void LCD_WriteCommand(unsigned char Command)
33{
34 LCD_RS=0;
35 LCD_RW=0;
36 LCD_DataPort=Command;
37 LCD_EN=1;
38 LCD_Delay();
39 LCD_EN=0;
40 LCD_Delay();
41}
42
43/**
44 * @brief LCD1602写数据
45 * @param Data 要写入的数据
46 * @retval 无
47 */
48void LCD_WriteData(unsigned char Data)
49{
50 LCD_RS=1;
51 LCD_RW=0;
52 LCD_DataPort=Data;
53 LCD_EN=1;
54 LCD_Delay();
55 LCD_EN=0;
56 LCD_Delay();
57}
58
59/**
60 * @brief LCD1602设置光标位置
61 * @param Line 行位置,范围:1~2
62 * @param Column 列位置,范围:1~16
63 * @retval 无
64 */
65void LCD_SetCursor(unsigned char Line,unsigned char Column)
66{
67 if(Line==1)
68 {
69 LCD_WriteCommand(0x80|(Column-1));
70 }
71 else if(Line==2)
72 {
73 LCD_WriteCommand(0x80|(Column-1+0x40));
74 }
75}
76
77/**
78 * @brief LCD1602初始化函数
79 * @param 无
80 * @retval 无
81 */
82void LCD_Init()
83{
84 LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
85 LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
86 LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
87 LCD_WriteCommand(0x01);//光标复位,清屏
88}
89
90/**
91 * @brief 在LCD1602指定位置上显示一个字符
92 * @param Line 行位置,范围:1~2
93 * @param Column 列位置,范围:1~16
94 * @param Char 要显示的字符
95 * @retval 无
96 */
97void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
98{
99 LCD_SetCursor(Line,Column);
100 LCD_WriteData(Char);
101}
102
103/**
104 * @brief 在LCD1602指定位置开始显示所给字符串
105 * @param Line 起始行位置,范围:1~2
106 * @param Column 起始列位置,范围:1~16
107 * @param String 要显示的字符串
108 * @retval 无
109 */
110void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
111{
112 unsigned char i;
113 LCD_SetCursor(Line,Column);
114 for(i=0;String[i]!='\0';i++)
115 {
116 LCD_WriteData(String[i]);
117 }
118}
119
120/**
121 * @brief 返回值=X的Y次方
122 */
123int LCD_Pow(int X, int Y)
124{
125 unsigned char i;
126 int Result=1;
127 for(i=0;i<Y;i++)
128 {
129 Result*=X;
130 }
131 return Result;
132}
133
134/**
135 * @brief 在LCD1602指定位置开始显示所给数字
136 * @param Line 起始行位置,范围:1~2
137 * @param Column 起始列位置,范围:1~16
138 * @param Number 要显示的数字,范围:0~65535
139 * @param Length 要显示数字的长度,范围:1~5
140 * @retval 无
141 */
142void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
143{
144 unsigned char i;
145 LCD_SetCursor(Line,Column);
146 for(i=Length; i>0; i--)
147 {
148 LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
149 }
150}
151
152/**
153 * @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
154 * @param Line 起始行位置,范围:1~2
155 * @param Column 起始列位置,范围:1~16
156 * @param Number 要显示的数字,范围:-32768~32767
157 * @param Length 要显示数字的长度,范围:1~5
158 * @retval 无
159 */
160void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
161{
162 unsigned char i;
163 unsigned int Number1;
164 LCD_SetCursor(Line,Column);
165 if(Number >= 0)
166 {
167 LCD_WriteData('+');
168 Number1=Number;
169 }
170 else
171 {
172 LCD_WriteData('-');
173 Number1=-Number;
174 }
175 for(i=Length; i>0; i--)
176 {
177 LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
178 }
179}
180
181/**
182 * @brief 在LCD1602指定位置开始以十六进制显示所给数字
183 * @param Line 起始行位置,范围:1~2
184 * @param Column 起始列位置,范围:1~16
185 * @param Number 要显示的数字,范围:0~0xFFFF
186 * @param Length 要显示数字的长度,范围:1~4
187 * @retval 无
188 */
189void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
190{
191 unsigned char i,SingleNumber;
192 LCD_SetCursor(Line,Column);
193 for(i=Length; i>0; i--)
194 {
195 SingleNumber = Number/LCD_Pow(16,i-1)%16;
196 if(SingleNumber < 10)
197 {
198 LCD_WriteData(SingleNumber+'0');
199 }
200 else
201 {
202 LCD_WriteData(SingleNumber-10+'A');
203 }
204 }
205}
206
207/**
208 * @brief 在LCD1602指定位置开始以二进制显示所给数字
209 * @param Line 起始行位置,范围:1~2
210 * @param Column 起始列位置,范围:1~16
211 * @param Number 要显示的数字,范围:0~1111 1111 1111 1111
212 * @param Length 要显示数字的长度,范围:1~16
213 * @retval 无
214 */
215void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
216{
217 unsigned char i;
218 LCD_SetCursor(Line,Column);
219 for(i=Length; i>0; i--)
220 {
221 LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
222 }
223}
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
1#include <REGX51.H>
2#include "LCD1602.h"
3
4unsigned int s, m, h;
5
6void Timer0Init(void) //1毫秒@11.0592MHz
7{
8 // 配置定时器 -----------------------
9 TMOD &= 0xF0; // 设置定时器模式
10 TMOD |= 0x01; // 设置定时器模式
11 TL0 = 0x66; // 设置定时初始值
12 TH0 = 0xFC; // 设置定时初始值
13 TF0 = 0; // 清除TF0标志
14 TR0 = 1; // 定时器0开始计时
15
16 // 配置中断 -------------------------
17 ET0 = 1;
18 EA = 1;
19 PT0 = 0;
20}
21
22
23void main()
24{
25 LCD_Init();
26 Timer0Init();
27
28 LCD_ShowString(1, 1, "Clock:");
29 LCD_ShowString(2, 3, ":");
30 LCD_ShowString(2, 6, ":");
31 while (1){
32 // LCD显示属于耗时操作,不要放在定时器中断中完成
33 LCD_ShowNum(2, 1, h, 2);
34 LCD_ShowNum(2, 4, m, 2);
35 LCD_ShowNum(2, 7, s, 2);
36 }
37}
38
39 // 中断函数
40void Timer0_Routine() interrupt 1
41{
42 static unsigned int T0Count = 0;
43 TL0 = 0x66; // 设置定时初始值
44 TH0 = 0xFC; // 设置定时初始值
45 T0Count++;
46
47 if(T0Count > 1000){
48 T0Count = 0;
49 s++;
50 // 时、分、秒
51 if(s >= 60){
52 s = 0;
53 m++;
54 }
55
56 if(m >= 60){
57 m = 0;
58 h++;
59 }
60
61 if(h >= 24){
62 h = 0;
63 }
64 }
65}
最终效果如下所示: