《单片机原理及运用》开课了(^_^)!!要好好学习,搞钱啊!!!

感谢B站UP猪知弦的教学指导

文章中安装的Keil软件和Proteus软件在我的个人网盘,这里要感谢吕老师上传的教学文件以及孜孜不倦的教导

文章中涉及的源码,均以我自己的单片机为准,例如我的LED灯有总开关,如果你没有请自行修改代码,祝你学有所成(^_^),我会注明实际和理论的代码

一、单片机开发与仿真环境搭建

简单的部分我就直接照搬吕老师的课件了,部分需要注意的地方我会做GIF演示,侵删

1.1 Keil 软件

1.1.1 Keil软件的简介

大家安装完Keil软件肯定是要学习使用的,这里不建议将软件汉化,软件本来原生态的样子就很看,要学会适应英化的软件。

Keil C51 是德国 Keil Software 公司(现已被 ARM 收购)推出的 8051 系列的 IDE(Integrated Development,集成开发环境)。它不仅支持汇编语言开发,更支持 C/C++等高级语言开发单片机。其可以完成从工程建立和管理、编译、链接、目标代码生成、软件仿真调试等完整的开发流程。

1.1.2 Keil C51的工作界面

1

1.1.3 Keil C51建立工程

首先你需要在电脑文件资源管理器中新建一个文件夹用来存放你的工程,下面是新建工程的GIF演示。注意的AT89C52是吕老师要求的,如果不一样请换成你们的,如果直接找AT89C52是找不到的,要先找对应的厂商然后点击+就可以看到相对应的芯片型号了。

1.1.4 建立/编辑C语言源程序文件

下面是GIF演示,比较简单,一定要记得后缀是.c

以上,我们就完成了最基本的软件使用,下面是一些进阶使用。

1.1.5 工程的设置

在工程建立后,还需要对工程进行设置。在 μVision5 的上方工具栏中,右击工程名 Target 1 框旁的魔术棒 ,即打开工程设置对话框。啊,好多我也不懂,看老师PPT吧,这些你说重要吧它也不是那么重要,看一遍过去吧。





1.1.6 Keil C51 的Debug

大家都知道Debug在调试程序中重要地位,所以我将Keil C51的Debug单独拿出来学习。

源程序编写完毕后还需要编译和链接才能够进行软件和硬件仿真。① 编译;②排错;在程序的编译/链接中,如果用户程序出现错误,还需要修正错误后重新编译/链接,重新烧录程序查看错误现象,十分消耗时间。因此需要单步调试,一步一步的查看代码运行效果来加快对错误的排查速度。

进入DEBUG模式后,黄色箭头为汇编程序运行位置光标,蓝黄三角形为当前 C 语言运行位置,指向当前等待运行程序行。其界面如下,其中单个黄色箭头为汇编程序运行位置光标(汇编一生之痛)。

在 μVision5 中,有 4 种DEBUG运行方式 :单步跟踪(Step Into),单步运行(Step Over),运行到光标处(Run to Cursor line),全速运行(Go)。

下面是使用Debug进行调试的GIF录像,测试用例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 理论
#include<reg52.h>

sbit led0 = P0^0;
sbit led1 = P0^1;
sbit led2 = P0^2;
sbit led3 = P0^3;


sbit led4 = P0^4;
sbit led5 = P0^5;
sbit led6 = P0^6;
sbit led7 = P0^7;


void main ()
{
led0 = 0;
led1 = 0;
led2 = 0;
led3 = 0;


led4 = 0;
led5 = 0;
led6 = 0;
led7 = 0;


}

1.1.7 存储空间资源的查看和修改

这一部分对我来说还是太早了,先看PPT吧



1.2 Proteus软件

1.2.1 Proteus软件的简介

① Proteus 是英国 Lab Center Electronics 公司推出的用于仿真单片机及其外围设备的EDA工具软件。

② Proteus 与 Keil C51 配合使用,可以在不需要硬件投入的情况下,完成单片机 C 语言应用系统的仿真开发。

③ Proteus 具有高级原理布图(proteus)、混合模式仿真(PROSPICE)、PCB 设计以及自动布线(ARES)等功能。

1.2.2 Proteus简单使用

继续看PPT吧,一定要看啊,我是太懒了就不想打字😀









1.2.3 Proteus 8 与 Keil C51 的联合使用

下面进行基本演示:① 将源程序编译、链接生成HEX文件,这里需要将工程设置中的Output设置中的Create HEX File勾选上。

1.2.4 Proteus 画出单片机的最小系统

使用Proteus选择元器件的方法如下

电气连接方法如下

1.2.5 实战教学:流水灯的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 实际
#include <reg52.h>

sbit LED=P0^0;
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void delay(unsigned int t) {
unsigned int i, j;
for(i=0;i<t;i++)
for(j=0;j<120;j++);
}

void led()
{
int i=0;
for(i=0;i<8;i++)
{
P0=~(0x01<<i);
delay(50);
}
}

void main(){

ENLED = 0;
ADDR3 = 1;
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;

while(1){
led();
}
}

搭建如图所示仿真电路,这里采用共阳极二极管接法

然后对C源码进行编译生成HEX文件,之后导入芯片中

1.3 STC-ISP软件

1.3.1 STC-ISP软件的简介

STC-ISP是一款又 STC 研发的单片机程序下载烧录软件,是针对 STC 系列单片机而设计的,可下载 STC89 系列、STC12 系列和 STC15 等系列的 STC 单片机,使用简便。

1.3.2 STC-ISP软件的使用

安装好STC-ISP和驱动后,当我们电脑插上板子之后,打开设备管理器,查看端口,我这里是COM12

1.3.3 实战教学: 点亮第一个LED灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 实际
#include<reg52.h>

sbit LED = P2^0;
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;


void main() {
ENLED = 0;
ADDR3 = 1;
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;

LED = 0;
while(1);
}

1.4 单片机最小系统

单片机最小系统也叫做单片机最小应用系统,是指用最少的原件组成单片机可以工作的系统。单片机最小系统的三要素就是电源、晶振、复位电路。如下图所示

1.4.1 电源

1.4.2 晶振

1.4.3 复位电路

复位电路是一种用来使电路恢复到起始状态的电路设备,它的操作原理与计算器有着异曲同工之妙,只是启动原理和手段有所不同。复位电路,就是利用它把电路恢复到起始状态。就像计算器的清零按钮的作用一样,以便回到原始状态,重新进行计算。

和计算器清零按钮有所不同的是,复位电路启动的手段有所不同。一是在给电路通电时马上进行复位操作;二是在必要时可以由手动操作;三是根据程序或者电路运行的需要自动地进行。复位电路都是比较简单的大都是只有电阻和电容组合就可以办到了,再复杂点就有三极管等配合程序来进行了。(上电复位和手动复位和程序自动复位)

上电复位:假如我们的单片机程序有 100 行,当某一次运行到第 50 行的时候,突然停电了,这个时候单片机内部有的区域数据会丢失掉,有的区域数据可能还没丢失。那么下次打开设备的时候,我们希望单片机能正常运行,所以上电后,单片机要进行一个内部的初始化过程,这个过程就可以理解为上电复位,上电复位保证单片机每次都从一个固定的相同的状态开始工作。这个过程跟我们打开电脑电源开电脑的过程是一致的。

手动复位:当我们的程序运行时,如果遭受到意外干扰而导致程序死机,或者程序跑飞的时候,我们就可以按下一个复位按键,让程序重新初始化重新运行,这个过程就叫做手动复位,最典型的就是我们电脑的重启按钮。

程序自动复位:当程序死机或者跑飞的时候,我们的单片机往往有一套自动复位机制,比如看门狗,具体应用以后再了解。在这种情况下,如果程序长时间失去响应,单片机看门狗模块会自动复位重启单片机。还有一些情况是我们程序故意重启复位单片机

程序跑飞是指系统受到某种干扰后,程序计数器PC的值偏离了给定的唯一变化历程,导致程序运行偏离正常的运行路径.程序跑飞因素及后果往往是不可预计的.在很多情况下,程序跑飞后系统会进入死循环而导致死机。

1.4.4 单片机看门狗

看门狗(WDT)是一个定时器。看门狗是用来防止万一单片机程序出错造成重大损失的。防错的原理很简单,它在硬件上就是一个定时器,当它溢出的时候就会让单片机强制复位使程序重新开始执行。

正常的情况下是不能让它溢出的,所以在程序上每隔一段时间要给他置一次值(俗称喂狗),只要程序中正常给它喂他就不会溢出。

一旦程序跑飞了,有干扰或者进入死循环之类的情况时,不能正常执行程序了,那么就永远执行不到喂狗的指令了,但此时定时器是硬件控制的,仍然会走,所以溢出了,单片机就复位了。

一般安全性要求比较高的,系统跑飞了会造成重大事故的都会加一个“狗”保安全。

通常,看门狗的溢出时间越短越灵敏,跑飞之后复位的时间越短,也就越安全,但是,喂狗的操作也要更频繁。

1.5 LED小灯原理

LED(light-emitting diode),即发光二极管,俗称 LED 小灯,它的种类很多,参数也不尽相同,我们板子上用的是普通的贴片发光二极管。这种二极管通常的正向导通电压是 1.8V到 2.2V 之间,工作电流一般在 1mA~20mA 之间。其中,当电流在 1mA~5mA之间变化时,随着通过 LED 的电流越来越大,我们的肉眼会明显感觉到这个小灯越来越亮,而当电流从5mA~20mA 之间变化时,我们看到的发光二极管的亮度变化就不是太明显了。当电流超过20mA 时,LED 就会有烧坏的危险了,电流越大,烧坏的也就越快。所以我们在使用过程中应该特别注意它在电流参数上的设计要求。

1.5.1 USB接口电路

image-20230331162131030

通过 USB 线,电脑给我们的开发板供电和下载程序以及实现电脑和开发板之间的通信。从图上可以看出,USB 座共有 6 个接口,其中 2
脚和 3 脚是数据通信引脚,1 脚和 4 脚是电源引脚,1 脚是VCC 正电源,4 脚是 GND 即地线。5 脚和 6 脚是外壳,我们直接接到了 GND 上,大家可以观察一下开发板上的这个 USB座的 6 个引脚。

我们现在主要来看 1 脚 VCC 和 4 脚 GND。1 脚通过 F1(自恢复保险丝)接到右侧,在正常工作的情况下,保险丝可以直接看成导线,因此左右两边都是 USB 电源+5V,自恢复保险丝的作用是,当你后级电路哪个地方有发生短路的时候,保险丝会自动切断电路,保护开发板以及电脑的 USB 口,当电路正常后,保险丝会恢复畅通,正常工作。右侧有 2 条支路,第一条是在+5V 和 GND 接了一个 470uF 的电容,电容是隔离直流的,所以这条支路是没有电流的,电容的作用,我们下节课再介绍,这节课我们主要看第二条支路。我们把第二条支路摘取出来就是如图 2-4 这个样子。

1.5.2 LED小灯电路

image-20230331163030645

发光二极管是二极管中的一种,因此和普通二极管一样,这个二极管也有阴极和阳极,习惯上也称之为负极和正极。我们接入的 VCC 电压是 5V,发光二极管自身压降大概是 2V,那么在右边 R34 这个电阻上承受的电压就是 3V。现在我们要求电流范围是 1~20mA 的话,就可以根据欧姆定律 R=U/I,把这个电阻的上限和下限值求出来:U=3V,当电流是 1mA 的时候,电阻值是 3K;当电流是 20mA 的时候,电阻值是 150欧,也就是 R34 的取值范围是 150~3K 欧姆。这个电阻值大小的变化,直接可以限制整条通路的电流的大小,因此这个电阻我们通常称之限流电阻。同理,我们在板子后级开关控制的地方,又添加了一个 LED10 发光二极管,作用就是当我们打开开关时,这个二极管才会亮起。

image-20230331163621172

这里的开关虽然只有一个,但是是 2 路的,2 路开关并联能更好的确保给后级提供更大的电流。电容 C19 和 C10,都是隔离断开直流的。把右侧的 GND 去掉,改成一个单片机的IO口,如图所示

image-20230331163940682

我们把右侧的原 GND 处接到单片机 P0.0 引脚上,那么如果我们单片机输出一个低电平,也就是跟 GND 一样的 0V 电压,就可以让 LED
小灯发光了。我们可以让 P0.0 这个引脚输出一个高电平,就是跟 VCC 一样的 5V 电压,那么这个时候,左侧 VCC 电压和右侧的 P0.0 的电压是一致的,那就没有电压差,没有电压差就不会产生电流,没有电流 LED 小灯就不会亮,也就是会处于熄灭状态。

1.5.3 单片机端口

STC89C52RC芯片有4*8=32个IO端口可以供我们用程序输出高低电平。拿P0表示它可以一次性控制8个引脚端口输出每个引脚想输出的电平状态。P0是一个寄存器,它的功能是控制所对应的8个IO口(P0.0-P0.7)。如果我们控制P0.0单个端口输出低电平,P0.1~P0.7这7个IO端口输出高电平,程序中使用这条语句P0=0xFE;为什么是0xFE呢?

C语言中的十六进制0xFE用二进制表示11111110,我们就知道了单片机的P0.0排在这八个位的最低端。我们控制P0.2单个IO端口输出低电平,其他IO端口输出高电平,则是P0=0xFB; 二进制表示11111011

显然我们使用P0=0xFE就是强制把其他7个IO端口都输出了固定高电平状态。要想实现单独控制一个IO端口,这时我们用这条语句来声明sbit LED=P0^0;也就是只控制一个位,此时想让单独的P0.0输出低电平只需LED=0;即可,LED不过是表示P0.0所用的名字而已,你可以根据喜好改写这个名字。值得注意的是,程序书写中如果没有sbit LED=P0^0;这个提前定义,直接P0.0=0;这样书写是不符合语法的,必须先给IO端口一个命名,而且sbit LED=P0.0;这样的写法同样也不符合语法,规定是P0^0。还有我们也不能命名已经在 #include<reg52.h>头文件中已有的名字,查看头文件内容可知,有些名字已经被使用,像sbit PSW=P0^0;sbit CY=P0^0;这样定义会编译报错的,因为命名冲突了。#include<reg52.h>头文件中已有的名字我们后期会使用到的。

1.5.4 实际LED硬件连接

1.5.4.1 三极管认识

先看左边的图,想要点亮LED灯,只需要+5vR1处于短接状态即可,那么PNP型三极管就是起到被单片机IO端口控制是否允许此处短接的作用,也叫三极管是否导通。如果单片机IO端口输出的是低电平(0V),此时我们可以直观的认为+5那端与单片机IO端口形成压差有电流通过,而电流流向正如箭头所示,则表明此时+5R1短接状态,三极管导通,灯就被点亮了。如果单片机IO端口输出的是高电平(5V)时,则没有形成压差也就产生不了电流,那么箭头不能代表此时有电流流向这个方向,所以+5R1是断开状态,灯没有被点亮。箭头起到的就是辅助我们理解的优势,这样我们可以根据箭头很形象的判断出单片机IO端口输出什么电平状态时就可以点亮LED。R1,R2电阻起到的是限流保护,右图使用的是NPN型三极管,那么单片机IO端口输出高电平时点亮LED,输出低电平时点不亮LED。

详细的可以看宋老师的《手把说教你51单片机》的第3.3节

1.5.4.2 三八译码器

该元器件只需要用到我们单片机三个IO端口就可以控制它的八个引脚其中的一个输出低电平,开发板就用到这种叫74HC138三八译码器。我们省略它的电源供电引脚,画出需要讲解的引脚。

首先有三个引脚E0,E0,E1需要固定给它默认的电平它才能正常工作,俗称使能器件

此时3个单片机IO端口输出不一样的电平时,IO0~IO7的其中一个引脚就会输出低电平,而其他7个IO都是输出高电平。

当我们的三个IO端口这样输出时,则有以下情况

1
三个IO端口输出的三个值从 IO端口3→IO端口2→IO端口1排成二进制数的得数就是哪个IOx输出低电平。比如三个IO端口这样输出:1 1 0。此时二进制值为十进制的6,那么IO6就输出低电平,其他输出高电平

1.6 蜂鸣器原理

1.6.1 单片机IO端口电流

不知道大家有没有发现,绝大部分单片机上的LED小灯电路基本都是确定+5v正压,而让单片机IO口输出低电平使小灯发光。为什么呢?

我们来看看宋老师的讲解吧。下面是两个LED小灯电路

左边电路即使单片机IO端口输出高电平5V,灯的亮度是很低的,因为单片机IO端口流出来的电流太小,无法驱动LED正常发光。这个是关联到单片机内部的集成电路原因的,大家可以上网上查查。

右边电路这时单片机IO端口输出低电平时灯却很亮,原因这是电源供给的5V,电流比较大,所以可以使LED发光较亮。举个例子:充电宝的接口输出电压5V,但是它流出的电流大,所以给手机充电就快,虽然电脑USB口接口电压也是5V但充电却非常慢,原因是USB口电流太小。

STC89C52RC这款芯片的P1,P2,P3这一共24个IO端口由于内部硬件的原因上电的时候都是输出高电平的,而P0一开始是一种不确定的状态(有时是高有时是低),但我们用的开发板已经把P0端口接了上拉电阻,所以开发板的P0.0~P0.7初始时也是输出高电平的。

1.6.2 有源蜂鸣器

蜂鸣器有源蜂鸣器无源蜂鸣器,两者表面长相相同,有源蜂鸣器有正负极之分,正极接5V,负极接地就可以响。和LED小灯一样,我们可以从简到繁的理解有源蜂鸣器。

1.6.3 无源蜂鸣器

开发板中无源蜂鸣器硬件连接除了一个续流二极管外就如有源蜂鸣器一样了,无源蜂鸣器实物可以不分正负极接,但它也标有“+”符号,也许是为了在我们不知道这是有源蜂鸣器还是无源蜂鸣器的情况上统一规定接法吧。

要让无源蜂鸣器发声,需要我们在单片机IO端口上输出500Hz~4.5kHz的脉冲频率信号。

用时间表示就是要输出周期为0.22ms~2ms((1/4500s)~(1/500s))范围的方波,这个周期内高电平时间和低电平时间各占一半。

1.6.4 无源蜂鸣器鸣叫

  1. 输出方波图解

现在要给单片机IO端口输出4KHz的方波,其方波周期为:$\frac{1}{4000}=0.25ms$,高电平时间:$\frac{0.25}{2}=0.125ms$,低电平时间:$\frac{0.25}{2}=0.25ms$。

1.7 独立按键

独立按键是单片机最常用的硬件部分

1.7.1 独立按键与矩阵按键

4个独立按键已经可以满足大部分的程序测试。学会了独立按键,矩阵按键是十分好学的。

首先看一下我们按键的分布

看一下是K4按键的电路图,我们让单片机的P2.3输出低电平,按键被按下,则被圈出部分的电压均为0V,因为都接地了。

1.7.2 按键的原理

单片机上的按键使用的是没有自锁的按钮,当我们按下按键后两个断点就被短接起来,松手后,按键自动弹起两个断点恢复原来的状态不再短接。

使用P2.7进行理解,我们按下K4按键,则断点之间导通,P2.7与GND连接被接地,此时P2.7的IO端口为0V,也就是低电平,俗称被拉低。这时候即使是程序令P2.7输出5V高电平都于事无补,因为这是外部电路直接导致,IO端口就是0V。内部硬件也能感觉到这个信号属于被强制拉低。因此程序中寄存器位的值是可以受外部电路影响而改变的

基于以上我们对按键的认识我们理想中的按键过程图如下。

image-20230408175442749

1.7.3 按键用法

联系生活按键的常用做法就像生活中的电磁炉或者门铃那样,按下电磁炉上的加热功能键后马上松开,发现电磁炉上的数就增加了。现在我们打算用开发板像按电磁炉上的按键一样,按一下灯被点亮,再按一次灯被熄灭这样的来回切换灯的状态的功能。

我们之前所讲的按键过程解析图只是理想中的效果,真正的按键过程图是这样的(按键的抖动)

按键按下的前期,IO端口并不是马上就接通地而处在稳定状态的,按键按下时,IO端口有短暂的时间接通地之后又松开了这么的一个来回的过程,这是自弹式按键本身的结构属性,此处称作按键抖动。如果我们一直按着不放,这时IO端口就会处在一种很稳定的接触状态,当我们松开按键时也会产生弹起抖动,这种抖动持续时间一般少于10ms。在之前的实验中,按键动作常速下“稳定接触状态”也会持续在50ms,这个时候程序去读取稳定的状态时是0,这样就可以判断是否按键已经按下,按下了就执行相应的程序功能。

那么我们如何处理这种抖动状态呢?

我们在软件上可以这样处理,当程序检测到上图的A段时“if(KEY4==0)”满足了条件,但是KEY4因为按键的抖动会在短时间里时而变为1时而变为0,所以我们在满足第一个“if(KEY4==0)”条件的时候马上做延时50ms,等待抖动过去,然后再次判断此时的KEY4还是等于0吗,是的话就执行切换灯的状态程序。(双层if嵌套,使用第一个if判断是否为低电平,延迟后判断是否是稳定态),图解如下

但是在代码实现后,如果我们按下按键一直不松手,则灯会一直闪烁。这是因为“稳定接触状态”一直保持着长时间的低电平,所以程序的二次“if(KEY4==0)”判断一直满足条件就会在“LED2=0;”和“LED2=1;”之间来回切换,再加上50ms延时才会保持亮一段时间灭一段时间所体现的LED闪烁。

1.7.4 按键模式

按键模式主要分为两种支持连按不支持连按。其中支持连按即我们使用遥控器放大电视的音量时只需按住“+”键不放,屏幕上的音量值在一直累加,松手后就停止累加了,这种就叫做按键支持连按功能。不支持连按”,例如我们用的电磁炉上的“+”键,按下不松手时数值只加一次就不加了,只有松手后再按才会进行数值的第二次累加。两种按键模式在单片机开发中都会经常使用。

1.7.5 不支持连按模式

我们要实现稳定接触状态的时间再怎么长,只能执行一次功能代码的目的。我们可以利用IO端口检测到按键按下然后执行完功能程序之后,下一个语句就写:如果IO端口还是保持着低电平(不松手状态),那程序就不往下执行了,让程序在这里“停止”,只有IO端口变成高电平(已松手)才允许“放行”程序去运行。我们知道按键抖动的时间少于10ms,所以在满足第一次“if(KEY4==0)”判断的时候只做“delay_ms(10);”的延时左右,过了这段时间就是“稳定接触状态”了,于是再去二次判断“if(KEY4==0)”即可。

以上虽然我们仅用while(KEY4==0)就把按键模式给切换了,不过这条语句严格来说是有缺陷的。如果我们编写更加复杂的程序时,while(KEY4==0)就成了Bug了。如果我们对按键按下不松手,那程序就一直在循环等待,不遇到高电平就不往下执行代码,如果这时有重要的程序要执行,那岂不是因按键而耽误。

1
2
3
4
5
6
7
8
while(1) {
if(KEY4==0) {
delay(50);
if(KEY4 == 0) {
//要执行某些功能的语句
}
}
}

即使我们不使用while(KEY4==0);,只要我们按键不松手,每次都要执行delay_ms(10);,这样的代码称不上高效率,我们只能再次完善代码了。

所以我们需要改善我们的程序,这里为了让程序做到通用性,我们定义一个key_upunsigned char类型的变量。我们新定义的key_up变量是用来记录此时按键IO端口的扫描值,进一步分析,当按键按下不松手时,此时的key_up等于KEY4(也就是0),当按键松手后,key_up就等于1了。然后想进入执行功能语句时,先过了“if(key_up==0)”这一关,因为我们一直让key_up在死循环里存取KEY4的值,所以只要没有按键动作,key_up一直等于1,这样连进入功能语句的第一关都没有资格。

当有按键按下时,key_up等于0了,进入了第一关,然后我们再设最后一关,如果通过了最后一关就可以执行功能语句了,最后一关是判断“if(KEY4==1)”,也就是说如果按键没松手,就无法执行功能语句了。没错,我们这次的不支持连按模式是只有按键松手之后才去执行的,上一讲则是按下之后过了10ms就执行代码了。大家可以根据这一思路写代码了。

1.7.6 回归按键

按键的执行任务也可以封装为函数,需要静态变量static去定义key_up,因为它需要跟着KEY4变化,而不是每次都被初始化为同一个值。

1.7.7 支持连按(全局变量)

支持连按的代码就是在不支持连按代码的思路上把if(KEY4==1/0)改为if(KEY4==0/1),这样的话按键不松手程序就能一直进入if(KEY4==0/1)的大括号里面。然后我们定义一个变量times,如果一直按着不放,times就一直累加,累加到1000,意味着低电平已经持续了一定的时间,我们就可以执行功能代码了,如图所示

全局变量就是先在所有函数前定义,这种变量可以在所有函数中使用,例如这个变量的值发生改变成为另一个值时,假设是12,其他函数此时运用这个变量的值就是12。全局变量有利有弊,可以自己学习一下

1.7.8 双模式函数封装

以上我们学会了两种按键模式(不能连按模式和连按模式)的代码,我们现在决定将这两种模式封装成一个函数,然后通过参数选择是支持连按还是不支持连按。

考虑到,两种模式的选择,现在利用两个按键来实现按键模式的切换,K3负责用来给K4做支持连按还是不支持连按的选择。这里可不是说K3,K4各自负责一个模式。

在我的开发板的原理图,在P2.3输出低电平的情况下,K1,K2,K3,K4就可以当独立按键。

大家可以自己想一下如何实现这一功能,写写代码,然后参考第二章的按键部分。

1.8 外部中断

1.8.1 寄存器

1.8.1.1 单片机的内部资源

我们熟知的P0,P1,P2,P3,包括上图中的TCON以及#inlcude <reg52.h>头文件中的IE、SCON等等都叫做寄存器。这些寄存器都可以粗浅的认为就是一个8位的变量,其中像P0^0这个最低位就是控制单片机的外部IO端口输出高低电平。而像TOCN^1(IEO)这些有什么作用呢?这就涉及到单片机的内部资源了。

1.8.1.2 IE0的作用

我们先看一下原理图中的P3端口

P3寄存器中的每一位都在#include <reg52.h>头文件中声明好了。我们拿P3^2来说,如果这个IO端口被外部电路拉低,那么TCON的第一位(也就是TCON^1)就会被置1,在程序中IE0变为1。

我们来验证一下,我们用一根杜邦线的一头插入P3.2的引脚,然后另一头插入GND(拉低)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <reg52.h>
#include "function.h"

//请提前将P3.2和GND相连
void main() {
LED_Init(); //初始化LED硬件模块
while(1) {
if(IE0 == 0) {
LED2 = !LED2;
delay_ms(50);
LED2 = 1; //如果IE0不等于0了,要保证熄灭LED2;
}
if(IE0 == 1) {
LED9 = !LED9;
delay_ms(50);
LED9 = 1; //如果LED0不等于1了,要保证熄灭LED9
}
delay_ms(50); //总要执行`if(IE0 == 0)`或者`if(IE0 == 1)`,这个延时要保证其中一盏灯灭的时间,保证总有灯闪烁的现象
}
}

我们看到LED9在不停的闪烁,此时拔了连接GND那头的杜邦线看到LED9熄灭,LED2不停的闪烁,连着插拔几次观察得到当P3.2遇到低电平的时候IE0会一直等于1,只有当P3.2为高电平时IE0才会等于0

1.8.1.3 IT0的作用

再看看TCON^0(也就是IT0),我们把上面的代码中的IE0改为IT0的初始值是多少。经测试,IT0初始值为0。若是我们再主函数开头赋值给IT0为1,那么IE0只有当P3^2从高电平变成低电平(俗称下降沿)就会被1,即使P2^3后面变成高电平IE0也不会变成0,一直是1。

这时需要我们在程序里让IE0清零(俗称软件清零)才行。

我们把下面的代码下载进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <reg52.h> 
#include <function.h> //详见第六章第8讲

void main()
{
IT0 = 1;
LED_Init(); //初始化LED硬件模块

while(1)
{
if(IE0==1)//只有当P3.2从高电平变成低电平这个瞬间(下降沿)IE0才会被置1,后面P3.2不管是一直保持高电平还是低电平,IE0被清0之后都不会再次被置1,只有P3.2再次产生下降沿才会被置1
{
IE0=0;
LED2=!LED2;
}
delay_ms(50);
}
}

即使杜邦线一直都插着GND端口,也就是P3.2都保持着低电平,LED2也不会闪烁,只有不断拔插杜邦线,LED2才会有亮灭的跳变。

所以当IT0等于1时,P3.2遇到下降沿IE0就会被置1,我们软件把这个位清0之后,如果P3.2没有再次遇到下降沿,IE0都不会被置1。

1.8.2 中断函数

根据上述,当赋值IT0为1时,P3.2遇到下降沿,IE0被置1,要想使IE0自动清零,需要引用中断函数。中断函数就是当它满足一定的条件时就会暂停主函数的执行内容,转而去执行中断函数。

1.8.2.1 中断函数的书写

中断函数与其他我们封装过的子函数的写法不同,有个区分标志interrupt。至于后面为什么会有个数字0,我们后面会了解到。

1
2
3
void EXTI0_IRQHandler() interrupt 0 {

}

以上函数名是可以随意取的,只要不跟以前封装定义好的函数名冲突就行。这里我们取EXTI0_IRQHandler作为函数名是模仿STN32单片机的写法。

1.8.2.2 EX0和EA

想要进入中断函数,必须满足它的一些前提条件。这里EX0(IE^0)EA(IE^7)要初始化赋值给1才能进入中断程序取执行任务。

EA叫做中断总开关,EX0是针对P3.2的外部中断的开关,也就是说想要进入中断子程序必须满足两个开关都要闭合。

1.8.3 进一步理解中断函数

1.8.3.1 外部中断1

以上我们注意到我们使用标志interrupt 0,这表明我们使用的是外部中断0(P3.2的功能)。还有一个外部中断1(P3.3的功能),想要使用外部中断1,代码中需要把EX0改为EX1IT0改为IT1。interrupt后面的0要改为2,函数名我们改为EXTI1_IRQHandler()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <reg52.h>
#include "function.h"

void main() {
IT1 = 1; //下降沿触发模式
EA = 1;
EX1 = 1;

LED_Init(); //初始化LED硬件电路
while(1);
}



void EXIT1_IRQHandler() interrupt 2 {
LED2 = !LED2;
}

为什么是interrupt 2呢?

1.8.3.2 按键触发中断

每次要把P3.2和P3.3外部拉低或释放,都需要拔插杜邦线,这样太麻烦,我们可以用按键来取代这些拔插动作,按键按下不放就相当于一直拉低,跟杜邦线一直插着GND一个效果,松开按键就跟杜邦线没插GND一样。把杜邦线这样接,让P3.3和P2.3一起相连,K4的按键动作可以使P2.3和P3.3同步电平。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <reg52.h> 
#include <function.h>//详见第六章第8讲

void main()
{
IT1 = 1; //下降沿触发模式
EX1 = 1;
EA = 1;

LED_Init();//初始化LED硬件模块
KEY_Init();//初始化按键功能模块
while(1);
}

void EXTI1_IRQHandler() interrupt 2
{
LED2=!LED2;
}
1.8.3.3 按键触发中断消抖

以上按键似乎没能很灵敏的按一下松手LED2就跳变一次亮或灭的效果,那是因为我们没有延时消抖,一个按键动作就存在好几个下降沿了,导致中断函数被执行了几遍。

在中断函数中加延时就可以消除这种失灵现象了,不过在以后编程里不能在中断函数里使用延时,这样会使程序的执行效率大打折扣,我们本次只是作为测试代码才在中断函数里用延时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <reg52.h> 
#include <function.h>//详见第六章第8讲

void main()
{
IT1 = 1; //下降沿触发模式
EX1 = 1;
EA = 1;

LED_Init();//初始化LED硬件模块
KEY_Init();//初始化按键功能模块
while(1);
}

//请把P3.3和P2.3用杜邦线连接起来
void EXTI1_IRQHandler() interrupt 2
{
delay_ms(50);//去抖动
if(INT1==0) //P3.3是否还处于低电平的稳定接触状态,INT1已在“#include <reg52.h>”中定义好了
{
LED2=!LED2;
}
}

1.9 定时器

以上对中断知识的了解,外部中断这种需要物理动作才能触发中断函数执行,但很多情况下我们需要的是中断函数在适当时刻可以自动的去执行,所以就需要定时器来辅助了。

定时器是单片机内部的一个硬件资源。

用生活常见现象举例:定时器相当于一个闹钟,我们调5分钟后闹钟就响,在这5分钟里我们想做什么就做什么,相当于我们自己是主函数里的任务,想运行什么任务就运行什么,也不用去理会闹钟还剩多少时间就响,我们只是一直做我们想做的事就行。

之后闹钟响了,这时我们不能再任性去做主函数的事了,得赶紧把中断函数里的事办完先,也就是要去执行中断函数了,主函数的事要暂停不做先,执行完中断函数之后继续回到主函数做我们的事。可是又过了5分钟闹钟又响了,没办法,我们又要放停自己的事去执行中断函数了,当然闹钟是可以关闭的,这就相当于是把中断函数的触发开关给断开了。

1.9.1 溢出

我们知道秒表计时到59秒的时候,再过1秒就变成了00。同理,当我们的unsigned char类型的变量的数值为255时,再加1就变为0了, unsigned int的变量如果此时的值为65535,后面再加1也同样成为0,这些都叫溢出

再举一个例子来理解定时器溢出概念,中学时我们做过一些化学实验,其中有一种容器叫做试管,如果试管是空的,我们用一个导管往试管里加水,导管的水流流速是均匀不变的,试管里的水要想溢出,需要的是x个单位的时间,如果我们想把距离溢出的时间缩短为一半,那我们提前把试管里的水装满到一半,这样就可以改变了溢出时间。

通过这个例子我们就可以明白,要想确定好定时器溢出产生中断的间隔时间,我们就要往这个“试管”提前装好合适的水量。

1.9.2 两种定时模式

试管有65535个刻度的型号,也有255个刻度的型号,不过我们的导管水流流速是永远不变的。

如果我们用的是大试管(65535个刻度),那么溢出时间我们可以控制的长一些,也就是定时时间可以多一点。我们要是想定时36个单位刻度的时间,那就先往试管装好65500个刻度的水量先,这样等到水位到达65535那个刻度时,再加一个刻度就溢出了,这时溢出的就是36个单位刻度的时间。试管每次水溢出,试管里的水就会消失不见(归0),如果我们还想定时36个单位刻度的时间,还需要重新把65500个刻度的水量再次装好给试管

如果我们用的是小试管(255个刻度),虽然它的定时时间远远没有大试管的定时时间长,但是只要我们第一次装好水量,每次它溢出之后,不会马上归0,而是试管里的水重新归为我们第一次那个时候装好的水量,所以只要在第一次确定了水量,也就确定了以后的所有定时时长

这种叫做试管的东西就是我们单片机内部的另一个寄存器,51单片机有两个定时器,分别叫做定时器0和定时器1,跟外部中断有 0和1的两个硬件资源一样。这里我们先拿定时器0来讲解,定时器1大体原理都一样。

1.9.3 定时器模式的选择

我们想选用哪个试管作为定时时长,首先需要初始化寄存器TMOD相对应的位的值。先看到下面这张图

可以看到圈出来的左右两个紫色框,4-7位是控制定时器1的,0-3位是控制定时器0的。我们要确定选择的定时器模式们主要看M1M0两个参数。

  1. 如果M1=0,M0=1,选用的是大试管定时模式
  2. 如果M1=1,M1=0,选用的是小试管定时模式

因为我们暂时没有使用到定时器1,所以4-7位(俗称高四位)可以全置为0.2-3位用不到,也是置为0,我们先选择大使馆作为定时模式,所以TMOD这个寄存器初始化为TOMD=0x01;。这里注意,因为M1和M0#include<reg52.h>头文件中并没有sbit M0=TMOD^0;的内容,所以我们初始化只能是TMOD=0x01,一次性操作8个位,而在程序里书写M1=0M0=1是错位的。

之后我们把使用“大试管”称为定时器的工作模式1,使用“小试管”称为定时器的工作模式2。

1.9.4 定时时长的做法

定时器0有两个寄存器分别是TH0TL0,大家再次粗浅的把TH0和TL0认为是两个8位的变量吧。因为我们用的是定时器的工作模式1,这两个8位的变量相当于组合成了16位的变量,TH0是高8位(H:High),TL0是低8位(L:Low)。假如此时再过一个刻度的时间就溢出,那么此时会有TH0等于255,TL0等于255,因为二进制的1111111111111111等于十进制的65535。65535再加1就溢出嘛。因为我们用的是11.0592M的晶振,所以每增加1花费的时间是(12/11059200)秒。

如果我们用定时器的工作模式1定时20毫秒后触发中断该怎么实现呢?首先我们先往这两个寄存器填充数值

可以看到,两个“8位的变量”从高到低合起来成为16位的变量,至于定时20ms为什么是这样填充,我们先用反推法给大家演示。

十六进制的0XB800换算成十进制的值为47104,于是定时的时长为(65536-47104)=18432个刻度的单位时间。18432*(12/11059200)=0.02秒=20毫秒。

我们再正推,要定时50毫秒怎么给TH0和TL0赋初值?设距离溢出还剩x个刻度,x*(12/11059200)=0.05,解得x=46080。所以需要提前装好65536-46080=19456个刻度的水量。也就是填充给TH0和TL0合成的16位的变量的值就是19456。19456换算成十六进制为0x4C00。所以要定时50毫秒,那么TH0=0x4C;、“TL0=0x00;

1.9.5 定时器简单运用

·:我们知道TH0和TL0合成的“16位的变量”的初值最小要为0,不能是负数,所以我们要满足:65536-( x/(12/11059200) )>=0。解得x<= 0.071111秒=71.111毫秒。也就是用定时器的工作模式1最长的定时时间为71毫秒左右而已。

下面我们来学习定时器的简单运用,这方便我们理解相关的理论知识。

  1. 首先定时器也是有开关的,这个相当于我们的导管是否打开让水流进“试管”里。因为我们用的是定时器0,TCON^4这个位就是控制定时器0是否打开或关闭,所以TR0=1;就是打开了定时器开始计时,TR0=0;是关闭了定时器计时功能。只要打开了定时器,TH0和TL0合成的“16位的变量”就会每过(12/11059200)秒就自加1,直到定时器溢出。
  2. 我们的定时器0一旦溢出,TCON^5就会被置1(TF0==1),如果我们不使用中断函数也是可以在主函数里等待判断“if(TF0==1)”。TF0跟外部中断0的IE0一样,被置1了需要软件清零。

用定时器0来实现间隔50ms的流水灯实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<reg52.h>
#include "function.h"

void main() {
u8 i = 0;
LED_Init(); //初始化LED硬件模块
TMOD = 0x01; //设置定时器0位工作模式1
TH0 = 0x4C; //色湖之定时时间位50ms
TL0 = 0x00;
TR0 = 1; //启动定时器0

while(1) {
if(TF0 == 1) { //判断定时器0是否溢出,每隔50ms就可以进入一次这个if语句内部
TF0 = 0; //软件清零,定时器0溢出后,清0溢出标志
TH0 = 0x4C; //重新赋值,保证下次溢出时间间隔还是50ms
TL0 = 0x00;
P0 = ~(0x01 << i); //每盏灯的点亮时间都保持者50ms
i++;
}
if(i >= 8) i = 0;
}
}

用定时器1来实现间隔50ms的流水灯实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <reg52.h> 
#include <function.h>//详见第六章第8讲

void main()
{
u8 i=0;
LED_Init(); //初始化LED硬件模块
TMOD = 0x10; //设置定时器1为工作模式1
TH1 = 0x4C; //设置定时时间为50ms
TL1 = 0x00;
TR1 = 1; //启动定时器1

while(1)
{
if (TF1 == 1) //判断定时器1是否溢出,每隔50ms就可以进入一次这个if语句
{
TF1 = 0; //定时器1溢出后,清0溢出标志
TH1 = 0x4C; //重新赋初值,保证下次溢出间隔时间还是50ms
TL1 = 0x00;
P0=~(0x01<<i);//每盏灯的点亮时间都保持着50ms
i++;
}
if(i>=8)i=0;
}
}

1.9.6 定时器工作模式2

这里我们讲解一下定时器0的工作模式2,也就是使用小试管的方式,TMOD的初始化就为TMOD=0x02;。我们知道工作模式2的最长溢出时间仅为256*(12/11059200)=0.000277秒=277微秒。

我们想实现间隔51ms的流水灯实验该怎么做?我们需要利用循环,首先我们定义一个变量cnt用来记录每次的溢出次数,然后我们的定时时间为51微秒(因为最大定时只有278微秒),cnt记录的值等于1000的时候,证明时间已经过去了51ms,于是再去执行流水灯的任务。

我们已经知道工作模式2是不需要在溢出之后再填充初始值的,比如我们定时的时间为51微秒,那么计算出“距离溢出的刻度”就为47(实际计算出的是47.0016,所以定时为51微秒误差就不大)。所以给TL0赋初值为256-47=209=0xD1。那么TH0也是给初值为0xD1,这是保证每次TL0溢出之后新的初始值是等于TH0的值的,所以定时器工作模式2又叫做8位自动重装载模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <reg52.h>
#include "function.h"

void main() {
u8 i = 0;
u16 cnt = 0;
LED_Init(); //初始化LED硬件模块
TMOD = 0x02; //设置定时器0为工作模式2
TH0 = 0xD1; // 定时51us
TL0 = 0xD1;
TR0 = 1; // 启动定时器0
while(1) {
if(TF0 == 1) { //每隔51us,cnt加1
TF0 = 0; //定时器溢出TF0软件清零
cnt ++; //记录溢出次数
}
if(cnt >= 1000) { // 溢出次数为1000时表明过去了51ms
cnt = 0;
P0 = ~(0X01 << i); // 执行流水灯,保证每个流水灯亮51ms
i++;
}
}
if(i >= 8) i = 0;
}

1.9.7 定时器中断函数的使用

定时器和掩饰其的概念不同,延时函数需要占用CPU的使用权,正在延时的时候其他任务没有CPU使用权就会拖慢执行效率。而定时器是不需要占用CPU的使用权,它是独立运行的,所以上面的代码的实现原理就是每隔51微妙,有个变量会自动加1,过了1000个51微妙的时候LED的状态才会发生改变,可以说CPU在51ms的时间里基本什么都没有做,只是在51微妙到了的时候做了cnt++;的工作。

与外部中断一样,定时器中断也有中断函数,同理,程序去执行中断函数就会把TF0的中断标志位自动清0,所以只要我们用了定时器中断函数,那么TF0就可以不用再出现在程序书写中了。

外部中断

定时器0的中断函数

interrupt”后面的数字为什么是1?

  1. 这些编号是为了区分哪些硬件资源的相关中断函数,如果我们同时使用两个定时器,那么只能用“interrupt 1”和“interrupt 3”来区分谁是谁的中断函数了。
  2. 使用“TIM0_IRQHandler”作为函数名也是模仿STM32定时器中断函数名的写法。
  3. 如果我们使用的是工作模式1,每次触发中断函数的执行内容首先就是再次给TH0和TL0赋初值保证下次的定时时间还是一样。这里我们使用中断函数的执行方式来实现30ms的间隔流水灯,算出TH0和TL0合成的“16位的变量”要填充的值为37888=0x9400。
  4. 在中断函数里也是可以定义局部变量的,当然如果这个变量是用来辅助流水灯的,那么肯定是要定义成静态变量的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <reg52.h>
#include "function.h"

void main() {
LED_Init(); //初始化LED硬件模块
EA = 1; //打开总中断开关
TMOD = 0x01; //设置定时器0为工作模式1
TH0 = 0x94; // 设置定时时间为30ms
ET0 = 1; //打开定时器0中断的开关
TR0 = 1; //启动定时器0
while(1);
}

void TIM0_IRQHandler() interrupt 1 {
static u8 i;
TH0 = 0x94; //重新设定时间为30ms
TL0 = 0x00;

P0 = ~(0x80 >> i); //流水灯向左移动
i++;
if(i >= 8) i = 0;
}

觉得30ms的流速太快,想改为300ms的话,修改一下中断函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <reg52.h>
#include "function.h"

void main() {
LED_Init();
EA = 1;
TMOD = 0x01;
TH0 = 0x94;
TL1 = 0x00;
ET0 = 1;
TR0 = 1;
while(1);
}
//通过重复10次30ms实现定时300ms
void TIM0_IRQHandler() interrupt 1 {
static u8 i = 0;
static u8 cnt = 0;
TH0 = 0x94;
TL0 = 0x00;
cnt++;

if (cnt >= 10) {
cnt = 0;
P0 = ~(0x80 >> i);
i++;
if(i >= 8) i = 0;
}
}

1.9.8 定时器初始化优化

之前我们对定时器进行初始化(即填充初始值)我们写TH0=0x94;TL0=0x00;。我们这样写是因为我们提前计算了需要装填的数字,显然如果没有注释我们很难指导这是要定时多长时间,所以为了增强程序的可读性,我们需要优化定时器初始化的写法。

要优化初始化的写法,首先要明白在程序书写过程中,赋值给寄存器的值可以书写成16进制的数也可以是十进制的数。TH0填充的是高八位,TL0填充的是低8位,那么如果这个16位的变量的十进制值是258,二进制的值就是0000000100000010。所以高八位的值位1(258/256=1),低八位的值是2(258%256=2)。我们也可以认为258/256=1是0x0102(258)右移了8位等于0x0001,简化书写就是0x01。而58%256=2是0x0102(258)舍去了高8位等于0x0002,简化书写就是0x02。

下面是图解,以一个16位的变量的十进制值是47104,二进制的值是1011100000000000,所以高八位的值是184(47104/256=184),第八位的值是0(47104%256=0)

综上所述我们要定时20ms的话,给TH0和TL0赋值方式可以为TH0=184;TL0=0;

我们通过对TH0和TL0传递十进制数字对初始化过程进行了优化,但这仍需要我们自己进行计算。假定定时的时间为1ms。算出要填的16位的变量为64614=0xFC66按照之前的写法就是TH0=0xFC/252;TL0=0x66/102。而我们也可以像下面这样写以打掉相同的效果。

  1. TH0=( 65536-( 0.001/(12/11059200) ) )/256
  2. TL0=( 65536-( 0.001/(12/11059200) ) )%256

因为( 65536-( 0.001/(12/11059200) ) )/256 = 252;( 65536-( 0.001/(12/11059200) ) )%256 = 102。有了这个思路,我们就可以将初始化赋值过程的写法改写为一串数字公式即可。

比如我们定时的最小单位时间为1微秒,那么定时50000微秒(50毫秒)就可以这样写来给TH0和TL0赋初值

  1. TH0=( 65536-( (50000/1000000)/(12/11059200) )/256;

  2. TL0=( 65536-( (50000/1000000)/(12/11059200) )%256;

这里的(50000/1000000)代表定时的是0.05秒,也就是50毫秒。

如果要定时其他毫秒数, 数字公式中的其他数字我们不需要修改,只需要把50000改为想定时的时间就可以了,计算过程交给单片机算出来,我们也就不需要自己用计算器把最终值算出来再赋给TH0和TL0这么麻烦了。

我们再化简上式就是

  1. TH0=( 65536-( (50000*110592)/120000) )/256;

  2. TH0=( 65536-( (50000*110592)/120000) )%256;

1.9.9 初始化需要注意的点

按上一讲说的书写方式,实现定时50ms间隔的流水灯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <reg52.h>
#include "function.h"

void main() {
LED_Init(); //初始化LED硬件电路
EA = 1; //打开中断总开关
TMOD = 0x01; //设置定时器0为工作模式1
TH0 = (65536 - ( (50000 * 110592) / 120000 )) / 256; //设置定时时间为50ms
TL0 = (65536 - ( (50000 * 110592) / 120000 )) % 256;
ET0 = 1; //启动定时器0中断的开关
TR0 = 1; //启动定时器0
while(1);
}

void TIM0_IRQHanler() interrupt 1 {
static u8 i;
TH0 = (65536 - ( (50000 * 110592) / 120000 )) / 256; //重新初始化填充
TL0 = (65536 - ( (50000 * 110592) / 120000 )) % 256;
P0 = ~(0x01 << i);
i++;
if(i >= 8) i = 0;
}

下载进开发板发现根本不是间隔50ms!靠!!!!!!!!这是为什么呢?

首先我们要知道,51单片机能存储最大的一个整型数的大小只有4个字节,也就是最多能记忆这个数到4294967296(2的32次方),而在

( 65536-( (50000110592)/120000 )中明显不能把(50000\110592)给临时存储,因为这个等式的得数已经大过2的32次方。所以我们的定时器才会无法实现准确的50ms定时。如果我们对编程没有一定的积累是很难察觉出这个隐形漏洞的。

解决办法就是,我们的定时最小单位只能是10微秒,也就是定时的时间必须是10微秒的整数倍。书写如下TH0=( 65536-(5000*110592)/12000 ) )/256;把之前的50000120000都去掉一个零,这样就可以准确的定时50ms了,因为“(5000)*110592)”没有超过2的32次方,读者自行修改本讲提供的代码中的4处之后下载进开发板观察现象是不是又实现50ms的间隔流水了。

但是这样好像不是很通用,我们需要在此优化初始化的写法

上述程序中的5000意为定时的是50ms,也就是5000*10微秒,但我们希望潜意识里假如要定时200微秒,如果写成20我们的思维还要绕个弯再把20默默乘以10才领悟出这是定时200微秒。倒不如这样,我们看到关键的数字是多少那就是要定时多少微秒。

比如看到关键数字为50000时就知道定时的是50000微秒。所以我们这样改写:TH0=( 65536-( (50000/10)110592)/12000 )/256;这样既保证了“(50000/10)\110592”没有超过2的32次方,也使“50000”更直观的让我们知道要定时的是50000微秒。但是大家要记住,关键数字必须是10的整数倍,如果想定时个208微秒,“(208/10)”还是等于20,所以定时时间还是200微秒

1.9.10 代码参考

我们分析如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void ConfigTimer0(unsigned int ms)
{
unsigned long tmp; //临时变量

tmp = 11059200 / 12; //定时器计数频率
tmp = (tmp * ms) / 1000; //计算所需的计数值
tmp = 65536 - tmp; //计算定时器重载值
tmp = tmp + 13; //补偿中断响应延时造成的误差
T0RH = (unsigned char)(tmp>>8); //定时器重载值拆分为高低字节
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x01; //配置T0为模式1
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
}

第一句:tmp = 11059200 / 12;。定时器的寄存器计数,每加1计数就是经过(12/11059200)秒。那么计数了11059200次就是经过12秒了。经过1秒计数就是(11059200/12)= 921600。这样我们就明白了第一条语句的意思。

第二句:tmp = (tmp * ms) / 1000;我们想定时1ms,需要的计数就是(921600/1000)=921.6,需要定时xms就是需要计数921.6x。例如:如果我们想定时5ms,那么需要的计数值就是921.6*5=4608。

定时器初始化:TH0=(65536-4608)/256,TL0=(65536-4608)%256。当然也可以这样表达,两种书写方式的功能作用都一样:TH0= (unsigned char)(65536-4608)>>8;TL0= (unsigned char)(65536-4608)。这里先不考虑中断响应延时造成的误差。T0RH,T0RL是两个全局变量,为的是在中断函数中可以重新赋初值给寄存器TH0和TL0。

解释tmp + 13:由于中断函数的执行有时需要消耗不同的时间,所以定时时间会有误差导致进入中断函数时会产生与设想时的时间不同,所以需要调整计数值,也就是上面代码中的tmp需要微调节成一个合适的数,上面是固定加了13这个数,但是有时需要灵活改变这个微调值,所以我们改为传入不同的参数来改变这个微调值。

基于微调我们改进以上代码,上述代码定时的最小单位为1ms,下一章由于我们要学舵机,需要定时的最小单位是0.1ms,所以我们的代码定时的最小单位必须是0.1ms,也就是100us。看到我们的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void TIM0_Init(u32 us,int trim)//trim:微调
{
u32 tmp; //临时变量

tmp = 11059200 / 12; //定时器计数频率
tmp = ( tmp * (us/100) )/10000; //计算所需的计数值
tmp = 65536 - tmp; //计算定时器重载值
tmp = tmp+trim; //微调计数值使其定时更精确到我们想要的定时时间
T0RH = (unsigned char)(tmp>>8); //定时器重载值拆分为高低字节
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清0低四位
TMOD |= 0x01; //设置定时器0为工作模式1
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
ET0 = 1; //闭合定时器0中断的开关
TR0 = 1; //启动定时器0
}

要想定时2ms,传入的参数就是2000,我们的参数是us数,如果传入的是2000,第二条语句就是“(tmp*2)/1000”,与宋老师的代码原理一样。不过需要记住的是改代码不能定时小于100us的时间,最低单位只能是100us

1.9.11 微调定时精确时间

一般定时器中断函数里的内容最好是能够快速地去执行完,比如只执行几条简单的语句,这样与主函数配合才会使程序更加高效。前面,只使用定时器中断负责某个IO引脚间隔跳变或者使一个变量间隔自加1的简单语句。

现在要实现间隔50ms左右的时间让流水灯左右循环移动的同时,还需要无源蜂鸣器一直响,这样的功能,思路该怎么去实现?

首先我们知道无源蜂鸣器要想鸣叫的比较尖锐,那P1.6需要一个合适的脉冲信号,这个信号打算使P1.6高低电平保持的时间为300微秒不断循环。所以我们用定时器中断实现P1.6的电平间隔跳变,主函数里负责完成流水灯的任务即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <reg52.h>
#include <function.h>

u8 T0RH, T0RL;
void TIM0_Init(u32 us,int trim)//trim:微调
{
u32 tmp; //临时变量

tmp = 11059200 / 12; //定时器计数频率
tmp = ( tmp * (us/100) )/10000;//计算所需的计数值
tmp = 65536 - tmp; //计算定时器重载值
tmp = tmp+trim; //补偿中断响应延时造成的误差
T0RH = (unsigned char)(tmp>>8);//定时器重载值拆分为高低字节
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清0低四位
TMOD |= 0x01; //设置定时器0为工作模式1
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
ET0 = 1; //闭合定时器0中断的开关
TR0 = 1; //启动定时器0
}
void main()
{
u8 i,dir;
LED_Init(); //初始化LED硬件模块
EA = 1; //闭合总中断开关
TIM0_Init(300,0); //用定时器0定时300us,不微调
while(1)
{
if(i<8)dir=0;//向左移
if(dir==0)P0=~(0x01<<i);

if(i>=8)dir=1;//向右移
if(dir==1)P0=~( 0x80>>(i-7) );//当i大于等于8之后,(i-7)其实也还是在1~7之间变化

i++;
if(i>=15)i=1;//让i一直在1~14之间变化
delay_ms(50);
}
}

void TIM0_IRQHandler() interrupt 1
{
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
BEEP=!BEEP;
}

1.9.12 封装函数

封装timer.htime.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//time.c
#include <reg52.h>
#include "function.h" //详见第六章第8讲

u8 T0RH,T0RL,T1RH,T1RL;

void TIM0_Init(u32 us,int trim)//trim:微调
{
u32 tmp; //临时变量

tmp = 11059200 / 12; //定时器计数频率
tmp = ( tmp * (us/100) )/10000; //计算所需的计数值
tmp = 65536 - tmp; //计算定时器重载值
tmp = tmp+trim; //微调计数值使其定时更精确到我们想要的定时时间
T0RH = (unsigned char)(tmp>>8); //定时器重载值拆分为高低字节
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清0低四位
TMOD |= 0x01; //设置定时器0为工作模式1
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
ET0 = 1; //闭合定时器0中断的开关
TR0 = 1; //启动定时器0
}

void TIM1_Init(u32 us,int trim)//trim:微调
{
u32 tmp; //临时变量

tmp = 11059200 / 12; //定时器计数频率
tmp = ( tmp * (us/100) )/10000; //计算所需的计数值
tmp = 65536 - tmp; //计算定时器重载值
tmp = tmp+trim; //微调计数值使其定时更精确到我们想要的定时时间
T1RH = (unsigned char)(tmp>>8); //定时器重载值拆分为高低字节
T1RL = (unsigned char)tmp;
TMOD&=0x0F;//清0高四位
TMOD|=0x10;//设置定时器1为工作模式1
TH1 = T1RH;//加载T1重载值
TL1 = T1RL;
ET1 = 1; //闭合定时器1中断的开关
TR1 = 1; //启动定时器1
}
1
2
3
4
5
6
7
8
9
10
//timer.h
#ifndef __TIMER_H__
#define __TIMER_H__

extern u8 T0RH,T0RL,T1RH,T1RL;

void TIM0_Init(u32 us,int trim);//trim:微调
void TIM1_Init(u32 us,int trim);//trim:微调

#endif

1.9.13 呼吸灯

呼吸灯的实现原理就是让小灯的IO端口在一段时间里PWM由大到小变化的占空比输出,接着又由小到大的占空比输出,小灯显示效果就是时亮时暗地交替闪烁。首先我们用定时器0定时0.1ms,全局变量pwm在其中断函数里执行简单的从0到99的循环自加,这样周期就是10ms。假如主函数里我们这样执行

1
2
if(pwm<90)P0=0xFF;
else P0=0x00;

那么IO端口输出的一直是周期为10ms占空比为90%的波形,这样8盏小灯一直是处于较暗的显示状态不变,要想实现呼吸灯,就要不停改变PWM波形的占空比,所以我们使用定时器1来做间隔10ms地改变占空比,我们再定义一个全局变量highval,这个变量每隔10ms就会在定时器1中断函数中实现加1或者减1,加到99时又从99减回到0,减回到0又从0开始加到99的这样循环过程,大家回去研究左右往复循环流水灯的代码就会明白都是一样的思路。然后主函数这样执行

1
2
if(pwm<highval)P0=0xFF;//highval每隔10ms就会变化加1或者减1
else P0=0x00;

效果图如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <reg52.h>
#include "function.h"
#include "timer.h"

u8 pwm = 0; highval = 99;
void main() {
LED_Init(); //初始化LED硬件模块
EA = 1; //闭合中断总开关
TIM0_Init(100, 9); //定时0.1ms, 9是微调是定时时间更精确
TIM1_Init(10000, 10); //定时10ms,10是微调使定时时间更精确

while(1) {
if(pwm < highval) P0 = 0xFF; //highval每隔10ms就会变化加一或者减一
else P0 = 0x00;
}
}

void TIM0_IRQHandler() interrupt 1 {
TH0 = T0RH; //重新初始化
TL0 = T0RL;
pwm++;
if(pwm >= 100) pwm = 0; //pwm在0-99之间间隔每隔0.1ms变化,周期就为10ms
}

void TIM1_IRQHandler() interrupt 3 {
static u8 dir;
TH1 = T1RH; //重新初始化
TL1 = T1RL;

if(dir == 1) highval--; //占空比逐渐减小,小灯逐渐变亮
if(highval == 0) dir = 0;

if(dir == 0) highval++; //占空比逐渐增大,小灯逐渐变暗
if(highval >= 99) dir = 1;
}

1.10 舵机

1.10.1 认识舵机

舵机是单片机机械控制入门必学的模块,在一些机器人关节中也是采用舵机作为控制,学习并熟练掌握使用舵机是我们对单片机的进一步认识。所以我们不能一直局限地使用开发板进行学习,而是扩展一些电子模块来辅助我们更加深入了解单片机,请自信购买舵机。

小型的舵机与大型的舵机控制原理几乎大同小异,考虑到我没钱,我肯定选择小型舵机作为入门学习和简单实用。常见的小型舵机型号为9g舵机,外观如下

1.10.2 舵机控制原理

我们使用的舵机为9g尺寸,信号为MG90S。这种舵机的作用角度为0°~180°,也就是舵机桨可以任意在某个角度卡死不动,由于供电原因我们使用蛮力是很难掰动舵机桨的,只有舵机掉电之后舵机桨才会任意地为我们所扭动。

舵机有三根线,除了供电的两根电源线还有一根信号线,我们使用的是5v电压功率的舵机,所以其他电源线可以直接接到单片机的+5vGND上,信号线的话就接到单片机的一个IO端口上,这个IO端口通过输出PWM控制舵机桨的角度在任意位置上固定停留。

这个PWM的周期为20ms,高电平的时间在0.5ms~2.5ms之间可以控制舵机桨角度的停留位置。

0.5ms 1.0ms 1.5ms 2.0ms 2.5ms
0度 45度 90度 135度 180度

我们把舵机的电源线接好,然后信号线连接P1.7,把代码下载进去,舵机桨在上电后就会旋转到一个固定角度的位置上停留在那不动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <reg52.h> 
#include <function.h>//详见第六章第8讲
#include <timer.h> //详见第八章第11讲
sbit PWMOUT = P1^7; //舵机信号线引脚

void main()
{
LED_Init();//初始化LED硬件模块
EA = 1; //闭合总中断开关
TIM0_Init(100,9);//定时0.1ms,9是微调使定时精度更高
while(1);
}

void TIM0_IRQHandler() interrupt 1
{
static u8 pwm=0;
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;

pwm++;
if(pwm>=200)pwm=0; //pwm在0~199之间间隔0.1ms变化,周期为20ms

if(pwm<10)PWMOUT=1;//高电平在周期为20ms的PWM中持续的时间为1ms,低电平持续的时间就是19ms,舵机桨在45度处的位置停留不动
else PWMOUT=0;
}

1.10.3 按键控制舵机

使用按键控制舵机桨在我们想要的位置上停留。K8负责控制舵机桨往一个方向不停旋转,K16则控制相反方向旋转。按键模式为支持连按。然后数码管显示高电平持续的时间,如果数码管显示5,则高电平在20ms周期里持续的时间为0.5ms,如果显示的是20,那就是持续2ms的高电平时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <reg52.h> 
#include "function.h"
#include "timer.h"
sbit PWMOUT = P1^7; //舵机信号线引脚

u16 highval=15;
void main()
{
u8 key;
LED_Init();//初始化LED硬件模块
KEY_Init();//初始化按键模块
EA = 1; //闭合总中断开关
TIM0_Init(100,9); //定时0.1ms,9是微调使定时精度更高
TIM1_Init(1000,0);//定时1ms,用来刷新数码管显示,定时精度要求不高可不微调

ShowNumber(highval);
while(1)
{
key=KEY_Scan(1,500);//支持连按
if(key==8)
{
highval++;
if(highval>25)highval=25;//高电平持续时间不能超过2.5ms
ShowNumber(highval);
}

if(key==16)
{
highval--;
if(highval<5)highval=5;//高电平持续时间不能低于0.5ms
ShowNumber(highval);
}
}
}

void TIM0_IRQHandler() interrupt 1
{
static u8 pwm=0;
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;

pwm++;
if(pwm>=200)pwm=0;//pwm在0~199之间间隔0.1ms变化,周期为20ms

if(pwm<highval)PWMOUT=1;//highval的值决定舵机桨的停留位置
else PWMOUT=0;
}

void TIM1_IRQHandler() interrupt 3
{
TH1 = T1RH; //重新加载重载值
TL1 = T1RL;
SEG_Scan();
}

1.10.4 呼吸灯与舵机

呼吸灯的程序思想如果用在舵机控制上,那么舵机桨就是从0度的位置转到180度的位置又从180度处转回0度处如此循环往复,现在我们动手改写一下呼吸灯的程序使其能融入舵机的控制中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <reg52.h>
#include "function.h"
#include "timer.h"

sbit PWMOUT = P1^7; //舵机信号线引脚

u16 highval = 5;
void main() {
u8 key;
LED_Init(); //初始化LED硬件模块
KEY_Init(); //初始化按键模块
EA = 1; //闭合总中断开关
TIM0_Init(100, 9); //定时0.1ms, 9是微调使定时精度更高
TIM1_Init(50000, 0); //定时50ms,定时精度要求不高可以不微调
while(1);
}

void TIM0_IRQHandler() interrupt 1 {
static u8 pwm = 0;
TH0 = T0RH; //重装初始值
TL0 = T0RL;

pwm++;
if(pwm >= 200) pwm = 0; //pwm在0-199之间间隔0.1ms变化,周期为20ms
if(pwm < highval) PWMOUT = 1; //highval的值决定舵机桨的停留位置
else PWMOUT = 0;
}

void TIM1_IRQHandler() interrupt 3 {
static u8 dir;
TH1 = T1RH; //重装初始值
TL1 = T1RL;

//控制highval只能在5~24之间变化
if(dir == 1) highval--; //占空比逐渐减少,舵机桨往0度的位置走动
if(highval < 5) dir = 0;

if(dir == 0) highval++; //占空比逐渐增加,舵机桨往180度的位置走动
if(highval>=24)dir = 1; //测试发现,24已经是舵机桨的尽头,无需写25
}

1.11 串口通信

1.11.1 串口通信入门

对于单片机来说,通信则与传感器、存储芯片、外围控制芯片等技术紧密结合,成为整个单片机系统的神经中枢

UART(Universal Asynchronous Receiver/Transmitter,即通用异步收发器)串行通信是单片机最常用的一种通信技术,通常用于单片机和电脑之间以及单片机和单片机之间的通信。

通信按照基本类型可以分为并行通信串行通信。并行通信时数据的各个位同时传送,可以实现字节为单位通信,但是通信线多占用资源多,成本高。比如我们前边用到的P0 = 0xFE;一次给 P0 的 8 个 IO 口分别赋值,同时进行信号输出,类似于有 8 个车道同时可以过去 8 辆
车一样,这种形式就是并行的,我们习惯上还称 P0、P1、P2 和 P3 为 51 单片机的 4 组并行总线。 而串行通信,就如同一条车道,一次只能一辆车过去,如果一个 0xFE 这样一个字节的数据要传输过去的话,假如低位在前高位在后的话,那发送方式就是 0-1-1-1-1-1-1-1-1,一位一位的发送出去的,要发送 8 次才能发送完一个字节。

STC89C52 有两个引脚是专门用来做 UART 串行通信的,一个是P3.0一个是P3.1,它们还分别有另外的名字叫做RXDTXD,由它们组成的通信接口就叫做串行接口,简称串口。用两个单片机进行 UART 串口通信,基本的演示图如图所示。

图中,GND 表示单片机系统电源的参考地,TXD 是串行发送引脚,RXD 是串行接收引脚。两个单片机之间要通信,首先电源基准得一样,所以我们要把两个单片机的 GND 相互连接起来,然后单片机 1 的 TXD 引脚接到单片机 2 的 RXD 引脚上,即此路为单片机 1 发送
而单片机 2 接收的通道,单片机 1 的 RXD 引脚接到单片机 2 的 TXD 引脚上,即此路为单片机 2 发送而单片机 1 接收的通道。

当单片机 1 想给单片机 2 发送数据时,比如发送一个 0xE4 这个数据,用二进制形式表示就是 0b11100100,在 UART 通信过程中,是低位先发,高位后发的原则,那么就让 TXD首先拉低电平,持续一段时间,发送一位 0,然后继续拉低,再持续一段时间,又发送了一位 0,然后拉高电平,持续一段时间,发了一位 1……一直到把 8 位二进制数字 0b11100100全部发送完毕。

这里我们提及持续一段时间,那么要持续多久呢?下面我们学相关概念。

  • 波特率:波特率就是发送二进制数据位的速率,习惯上用baud表示,即我们发送一位二进制数据的持续时间=1/baud。在通信之前,单片机 1 和单片机 2 首先都要明确的约定好它们之间的通信波特率,必须保持一致,收发双方才能正常实现通信。常用的波特率为9600,所谓9600指的是一秒钟单片机可以发送9600个数据位,也就是1秒钟的时间里单片机可以发送(9600/8)=1200字节,一个字节等于八个位,一字节等于八比特,因此一个字节等于八个二进制位

我们知道了接受(发送)持续的时间,那么如何确定起始或停止发送的呢?

  • 规定当没有通信信号发生时,通信线路保持高电平,当要发送数据之前,先发一位 0 表示起始位,然后发送 8 位数据位,数据位是先低后高的顺序,数据位发完后再发一位 1 表示停止位。这样本来要发送一个字节的 8 位数据,而实际上我们一共发送了 10位,多出来的两位其中一位起始位,一位停止位。而接收方呢,原本一直保持的高电平,一旦检测到了一位低电平,那就知道了要开始准备接收数据了,接收到 8 位数据位后,然后检测到停止位,再准备下一个数据的接收。

我们以一个具体的例子感受一下,单片机的P3.1(TXD)是发送引脚,也就是说要发送字符“A”,这个引脚的变化如下图所示

单片机要发送一个字节给电脑端,首先发送引脚需要先拉高,然后拉低持续(1/9600)秒,电脑端检测到这个低电平信号就会准备接收数据字节。然后我们要发送的二进制位是01000001,但是串口通信发送的字节是低位在前,高位在后,所以上图的发送顺序就是01000001反过来为10000010。一个字节发送完成之后还要发送一个停止位1,电脑端接收到这个停止位就认为一个字节发送完成了。

我们用定时器来实现引脚的持续时间,怎么定时(1/9600)秒怎么设置,计算一下就可以了。

解得:$X = 96$,因为定时时间间隔比较短,所以我们使用定时器0的工作模式2就可以了。填充TH0的初始值就是$256-96=160 \rightarrow 0xA0$。因为P3^1在#include <reg52.h>已有定义为TXD, 我们可以直接使用。通过按K4来启动发送字节数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <reg52.h>
#include "function,h"
#include "timer.h"

void TIM0_Mode2_Init() {
TMOD &= 0xF0; //清零低四位
TMOD |= 0x02; //设置定时器0为工作模式2
TH0 = 0xA0; //计算出波特率9600
TL0 = 0x00;
ET0 = 1; //闭合定时器0中断的开关
TR0 = 1; //启动定时器0
}

void main() {
u8 key;
LED_Init(); //初始化LED硬件模块
KEY_Init(); //初始化按键模块
EA = 1; //闭合总中断开关
TIM0_Mode2_Init(); //定时(1 / 9600)秒
TR0 = 0; //关闭定时器
while(1) {
key = KEY_Scan(0, 1000);
if(key == 4) TR0 = 1; //开启定时器,启动一个字节传输,按一次一次发送
}
}

void TIM0_IRQHandler() interrupt 1 {
static u8 cnt = 0, i, TXDBUF = 65; //字符`A`的ASCII值为65
cnt++; //cnt一直在1-10之间变化

if(cnt == 1) TXD = 0; //cnt变为1,发送起始位,这次的中断函数就执行完了,持续够(1/9600)秒之后,再次进入中断函数,然后就是发送数据字节的8位的任务
if(cnt >= 2 && cnt <= 9) { //发送8位数据位,从低位开始引脚的变化为 1 0 0 0 0 0 1 0
TXD = TXDBUF & 0x01; //要么等于1要么等于0,这样P3.1的引脚要么保持高电平,要么保持低电平
TXDBUF >>= 1;
}
if (cnt == 10) {
TXD = 1; //发送停止位
TR0 = 0; //关闭定时器,结束一次字节传输
cnt = 0;
i++;
TXDBUF = 65+i; //下次按下按键发送的是B C D E...
}
}

这里简单说明一下,我们按下K4启动了定时器,然后第一次进入中断函数时,做的任务就是拉低P3.1,然后这次的中断函数的任务就结束了,等过了(1/9600)秒之后,再次进入中断函数,上一次拉低P3.1的时间已经持续够(1/9600)秒了,这第二次的中断函数任务就是拉高P3.1,因为发送字符A这个字节的最低位为1,持续够(1/9600)秒进入第三次执行中断函数,拉低P3.1,第四,第五,第六,第七都是拉低P3.1发送0,以此类推,到第10次中断函数执行就是拉高P3.1发送停止位,关闭定时器结束一次字节的传输,要想再次发送需要按K4启动定时器,TXDBUF=65+i;表示下次发送的是66这个数据,再下次就是发送67,68以此类推。

1.11.2 软件设置

下载以上代码,不断按下Key4可以看见按输出A B C D C等…

1.11.3 串口配置函数

这里讲解一两处配置的知识,首先SCON=0x50;是让SCON寄存器的第4位和第6位的置1,其他位置0。TH1=256-(11059200/12/32/buad);是波特率设置的计算公式,由于串口的使用是要占用定时器1,那么定时器1的定时中断将不能使用,所以必须使ET1=0;禁止其产生定时中断,也就是使用了串口,那么void TIM1_IRQHandler() interrupt 3将不能再出现在程序书写中。不过,串口也有相应的中断函数,像ET0,ET1一样,这些都是子开关,串口中断的子开关为ESES=1;EA=1;就开启了串口的中断函数。我们串口发送数据的时候会产生中断,接收到数据的时候也会产生中断,这两个瞬间我们在中断函数里需要执行相关任务。要知道串口有动作的时候,总会有RI或者TI被置1,前者意为接收到完整的8位的数据,也就使接收到一个字节然后RI就被置1.后者意为单片机发送完一个完整的字节了TI就被置1.这些瞬间都需要我们在串口中断函数中让其清零(软件清零),以备下次它们能再次被置1。

下面实现的功能是电脑端通过串口发送一个数据给单片机,这个数据被单片机接收到之后,让这个数据再加1,然后单片机再通过串口把加1后的数据发送回去给电脑端让它在窗口上显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <reg52.h>
#include "function.h"
#include "timer.h"

u8 RXDBUF; //缓存接收到的数据
void ConfigUART(u16 baud) {
SCON = 0x50; //配置串口为模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2

TH1 = 256 - (11059200/12/32)/baud; //计算T1的重载值
TL1 = TH1; //初值为重载值

ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //开启定时器1
}

void main() {
LED_Init(); //初始化LED硬件模块
EA = 1; //闭合总中断开关
ConfigUART(9600); //串口波特率设置为9600
while(1);
}

void InterruptUART() interrupt 4 {
if (RI) { //RI等于1就满足if条件语句,意为接收到一个字节
RI = 0; //软件清零接受中断标志位
RXDBUF = SBUF; //接收到的数据保存到接收缓存变量中
SBUF = RXDBUF + 1; //发送回去给电脑端的数据
}
if (TI) {
TI = 0; //软件清零发送中断标志位
}
}

这里的串口中断函数,像if(RI)TR=0;if(TI)TI=0;这些都是在串口中断函数中必须要执行的任务,当然如果在其他函数里有清零这两个位,可以不用在串口中断函数中书写,但是一定要保证每次都要清0

SBUF是名字相同但作用不同的缓冲区,SBUF=RXDBUF+1;SBUF在等于号前面意为这个是单片机发送给电脑端数据的缓冲区,一旦出现SBUF=xxxx;这样的语句,那就是单片机开始通过串口发送数据出去。上图可以看见现象。

1.11.4 ASCII码与通信数据

通过实验操作来熟悉ASCII码与通信数据之间的关系。

现在实现这样的功能:在电脑端上发送“十六进制”模式或“字符格式”模式的字节给单片机,数码管则显示出这些数据的十进制值,然后观察数码管上显示的数值与发送数据的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <reg52.h>
#include "function.h"
#include "timer.h"

u8 RXDBUF; //缓存接收到的数据
void ConfigUART(u16 baud) {
SCON = 0x50; //配置串口为模式1
TMOD &= 0X0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2

TH1 = 256 - (11059200/12/32)/baud; //计算重载值
TL1 = TH1; //初始值为重载值

ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //启动定时器1
}

void main() {
LED_Init(); //初始化LED硬件模块
EA = 1; //闭合中断总开关
ConfigUART(9600); //配置波特率为9600
TIM0_Init(1000, 0); //定时1ms,用来刷新数码管显示,定时精度要求不高可以不使用微调

while(1) {
ShowNumber(RXDBUF);
}
}

void TIM0_IRQHandler() interrupt 1 {
TH0 = T0RH; //重新加载初始值
TL0 = T0RL;
SEG_Scan();
}

void InterruptUART() interrupt 4 {
if(RI) {
RI = 0; //手动清零接收中断标志位
RXDBUF = SBUF; //接收到的数据保存到接收缓存变量中
SBUF = RXDBUF; //接收到的数据又直接发回,叫做-"echo",用以提示用户输入的信息是否以正确接收
}
if(TI) {
TI = 0; //软件清零发送中断标志位
}
}

在发送窗口和接收窗口都选“十六进制”模式,电脑端发送一个8的数据(也就是0x08)的字节给单片机,然后单片机会发送回来这个数据给电脑端显示在窗口中。此时开发板上的数码管显示的是8。

image-20230512233102479

接着我们在电脑端发送的是10,这是0x10,不要与十进制的10混淆。数码管显示的是16,所以发送的数据用十进制显示在数码管上是正确的。

我们切换到字符格式模式下发送字符A。数码管显示的是65,电脑端接收到单片机发送回来的数据是0x41,也就是十进制下的65。

找到ASCII表,字符“A”对应的数刚好是65。

如果把电脑端接收窗口改为“字符格式”显示,那么显示的内容就跟发送窗口的字符一样了。

如果我们在电脑端发送窗口输入一串字符比如“ABCD123”点击发送给单片机,如果我们看到的是下面这个现象

那么结合以下的程序代码分析RXDBUF = SBUF;SBUF = RXDBUF;也就是电脑端先发送字符A给单片机,单片机马上发送回字符A给电脑端,接着电脑端又发送字符B,如此执行下去,直到发送完最后一个字符4

这些发送和接收过程是非常快的,因为单片机接收缓存区SBUF每次只能暂存一个字节,所以前面8个字节都会很快被替换,导致数码管在一瞬间里只显示字符4的ASCII码值52。

1.11.5 printf系列的函数实现

懂得了单片机通过串口传输数据给电脑端口窗口查看,那么,有时我们需要发送字符串,有时需要发送一个变量的数值是多少,以及还要发送回车换行这三种情况,所以我们把这三个功能函数封装起来,方便后续的串口使用。

  1. void printf_str(u8 *str)函数就是专门用来发送字符串给电脑端的。
  2. void printf_num(u32 num)函数发送的变量数值只支持显示十进制数0-4294967295,也就是参数u32类型。
  3. void printf_rn()就是发送回车换行符。

这里的代码,我们需要慢慢弄懂,现在不懂也没有关系,后面慢慢消化。我们希望串口这部分功能模块也能像定时器那样封装一个单独文件使用,所以创建号uart.cuart.h文件,复制以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// uart.c
#include <reg52.h>
#include <function.h>//详见第六章第8讲

void ConfigUART(u16 baud)
{
SCON = 0x50; //配置串口为模式1
TMOD &= 0x0F; //清零T1的控制位
TMOD |= 0x20; //配置T1为模式2
TH1 = 256 - (11059200/12/32)/baud;//计算T1重载值
TL1 = TH1; //初值等于重载值
ET1 = 0; //禁止T1中断
ES = 1; //使能串口中断
TR1 = 1; //启动T1
}

void printf_str(u8 *str)
{
while(*str != '\0')//连续发送字符串数据,直到检测到结束符
{
SBUF=*str++;
while(!TI); //等待字节发送完成TI被置1就退出这个while循环
TI = 0; //清0标志位
}
}

void printf_num(u32 num)
{
u8 buf[10];
char i;//取值范围为-128~127

for (i=0; i<10; i++) //把长整型数转换为10位十进制的数组
{
buf[i] = num % 10;
num = num / 10; //舍掉个位数,重新装载
}

for (i=9; i>=1; i--)
{
if (buf[i] != 0)break; //从最高位起,遇到0不理会,遇到非0则退出循环
}

while(i>=0) //剩余低位都如实发送出去
{
SBUF='0'+buf[i]; //如果此时的buf[i]的值是1,那么电脑端窗口在“字符格式”模式下要想显示字符“1”,只需'0'+1,因为‘0’就是ASCII码值48
while(!TI); //等待字节发送完成TI被置1就退出这个while循环
TI = 0; //清0标志位
i--;
}
}

void printf_rn() //发送回车换行符
{
SBUF='\r';
while(!TI); //等待字节发送完成TI被置1就退出这个while循环
TI = 0; //清0标志位

SBUF='\n';
while(!TI); //等待字节发送完成TI被置1就退出这个while循环
TI = 0; //清0标志位
}
1
2
3
4
5
6
7
8
9
10
// uart.h
#ifndef __UART_H__
#define __UART_H__

void ConfigUART(u16 baud);
void printf_str(u8 *str);//发送字符串
void printf_num(u32 num);//发送参数的数值
void printf_rn(); //发送回车换行符

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//main
#include <reg52.h>
#include <function.h>//详见第六章第8讲
#include <uart.h>

void main()
{
u8 key;
u32 value=65535;
LED_Init();//初始化LED硬件模块
KEY_Init();//初始化按键模块
EA = 1; //闭合总中断开关
ConfigUART(9600);

while(1)
{
key=KEY_Scan(0,1000);
if(key==4)
{
printf_str("value=");//发送字符串
printf_num(value); //发送变量的数值
printf_rn(); //发送回车换行符
value++;
}
}
}

void InterruptUART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0;//手动清零接收中断标志位
}
}

在串口中断函数中我们没有再写“if(TI){ TI = 0; }”,那是因为我们在所有的发送函数中都做了TI的清0处理,所以可以在串口中断函数中不用再书写TI。

不断按下Key4按键,可以观察到串口助手出现字符串。

1.12 1602液晶屏

二、实战教学以及作品展示

2.1 LED

2.1.1 点亮第一个LED灯

2.1.1.1 源码
1
2
3
4
5
6
7
// 理论
#include <reg52.h>
sbit LED= P2^0; // 直接使用sbit 控制P0^0一个位
void main() {
LED = 0; // 低电平点灯
while(1); // 死循环
}
2.1.1.2 仿真

2.1.1.3 实物
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 实际
#include<reg52.h>

sbit LED = P0^0;
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void main() {
ENLED = 0; //
ADDR3 = 1; // 使能三八译码器

ADDR2 = 1; // **************************
ADDR1 = 1; // 让三八译码器的IO6输出低电平
ADDR0 = 0; // ***************************

LED = 0; //点亮最右边的灯
while(1);
}

2.1.2 闪烁的LED灯

延时是单片机入门必学的应用!

2.1.2.1 源码

有了前面的基础,点灯肯定不是问题,这里实现让LED从点亮一段时间到熄灭一段时间再点亮一段时间如此循环下去,实现闪烁LED的功能。为了实现这个目的我们需要实现延迟函数for(i=0;i<30000;i++);是最简单的延时语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 理论
#include <reg52.h>

sbit LED = P2^0;

void main() {
unsigned int i;
while(1) {
LED = 0;
for(i=0; i<3000; i++);
LED = 1;
for(i=0; i<3000; i++);
}
}
2.1.2.2 仿真

2.1.2.3 实物
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 实际
#include <reg52.h>

sbit LED = P0^0;
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void main() {
unsigned int i;
ENLED = 0; //定义一个16位无符号整形变量
ADDR3 = 1; // 三八译码器使能

ADDR2 = 1; //******************************
ADDR1 = 1; //使三八译码器的IO6口输出低电平
ADDR0 = 0; //******************************

while(1) {
LED = 0; //点亮最右边的小灯
for(i=0; i<3000; i++); //延迟一段时间
LED = 1; //熄灭最右边的小灯
for(i=0; i<3000; i++); //延迟一段时间
}
}

关于单片机设计精确的延迟,可以看这篇文章的第三章的Delay()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 实际,延迟一秒
#include <reg52.h>
sbit LED2 = P0^0;
sbit ADDR2 = P1^2;
sbit ADDR1 = P1^1;
sbit ADDR0 = P1^0;
sbit ENLED = P1^4;
sbit ADDR3 = P1^3;

void main()
{
unsigned int i,j;//定义两个16位的变量
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 1;//**************************
ADDR1 = 1;//让三八译码器的IO6输出低电平
ADDR0 = 0;//**************************

while (1)
{
LED2=0;//点亮最右端的灯
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
LED2=1;//熄灭最右端的灯
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
}

2.1.3 三个小灯同时闪烁

2.1.3.1 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 理论
// 原理很简单控制P2^0,P2^1,P2^2口的高低电平即可
#include "reg52.h" // 引入8051单片机头文件

sbit LED1 = P2^0; // 定义LED1引脚为P1.0
sbit LED2 = P2^1; // 定义LED2引脚为P1.1
sbit LED3 = P2^2; // 定义LED3引脚为P1.2

void main()
{
int i=0;
while(1) // 无限循环
{
LED1 = 0;
LED2 = 0;
LED3 = 0;

// 延时一段时间,使LED保持熄灭状态
// 可根据需要调整延时时间
for(i = 0;i < 500;i++);

LED1 = 1;// 将LED1引脚置为高电平,熄灭LED1
LED2 = 1;// 将LED2引脚置为高电平,熄灭LED2
LED3 = 1;// 将LED3引脚置为高电平,熄灭LED3

// 延时一段时间,使LED保持熄灭状态
// 可根据需要调整延时时间
for(i = 0; i < 500;i++);
}
}
2.1.3.2 仿真

2.1.3.3 实物
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 实际
#include "reg52.h" // 引入8051单片机头文件

sbit LED1 = P0^0; // 定义LED1引脚为P1.0
sbit LED2 = P0^1; // 定义LED2引脚为P1.1
sbit LED3 = P0^2; // 定义LED3引脚为P1.2

// 开启总开关
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void main()
{
int i=0;

ENLED = 0;
ADDR3 = 1;
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;

while(1) // 无限循环
{
LED1 = 0;
LED2 = 0;
LED3 = 0;

// 延时一段时间,使LED保持熄灭状态
// 可根据需要调整延时时间
for(i = 0;i < 5000;i++);

LED1 = 1;// 将LED1引脚置为高电平,熄灭LED1
LED2 = 1;// 将LED2引脚置为高电平,熄灭LED2
LED3 = 1;// 将LED3引脚置为高电平,熄灭LED3

// 延时一段时间,使LED保持熄灭状态
// 可根据需要调整延时时间
for(i = 0; i < 5000;i++);
}
}
2.1.3.4 优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 理论,通过控制P0
// 通过P0
#include <reg52.h>

void delay(unsigned int x) {
unsigned int i, j;
for(i=0; i<x; i++){
for(j=0; j<120;j++);
}
}

void main() {
P2 = 0xF8; // 1111 1000 -> 0xF8
delay(500);
P2 = 0xFF;
delay(500);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 实际
#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;


void delay(unsigned int x) {
unsigned int i, j;
for(i=0; i<x; i++){
for(j=0; j<120;j++);
}
}

void main() {
ENLED = 0;
ADDR3 = 1;

ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;

P0 = 0xF8; // 1111 1000 -> 0xF8
delay(500);
P0 = 0xFF;
delay(500);
}

2.1.4 流水灯

实现流水灯的代码我们在第一章的教学中就已经给出了,这里给出使用P0口实现的方式,我们知道到P0控制的是8个IO端口,我们使用0xFF等控制语句能实现控制八个端口输出不同的高低电平,那么我们来使用P0来实现一下。

2.1.4.1 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 理论
#include <reg52.h>

void delay(unsigned int t) {
unsigned int i,j;
for(i=0;i<t;i++)
for(j=0;j<120;j++);
}

void main() {
while(1) {
P2 = 0xFE; // 0xFE -> 1111 1110 -> 只有最右边亮
delay(500); // 延迟
P2 = 0xFD; // 0xFD -> 1111 1101 *********************
delay(500); // 延迟
P2 = 0xFB; // 0xFB -> 1111 1011
delay(500); // 延迟
P2 = 0xF7; // 0xF7 -> 1111 0111
delay(500); // 延迟
P2 = 0xEF; // 0xEF -> 1110 1111 低电平依次向右移动,控制不同的灯亮
delay(500); // 延迟
P2 = 0xDF; // 0xDF -> 1101 1111
delay(500); // 延迟
P2 = 0xBF; // 0xBF -> 1011 1111
delay(500); // 延迟
P2 = 0x7F; // 0x7F -> 0111 1111 **********************
delay(500);
}

}
2.1.4.2 仿真

2.1.4.3 实物
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 实际
# include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void delay(unsigned int x) {
unsigned int i,j;
for(i=0; i<x; i++){
for(j=0; j<120; j++);
}
}

void main() {
ENLED = 0;
ADDR3 = 1; // 使能三八译码器

ADDR2 = 1; // ***************************
ADDR1 = 1; // 使三八译码器IO6口输出低电平
ADDR0 = 0; // ***************************

while(1) {
P0 = 0xFE; // 0xFE -> 1111 1110 -> 只有最右边亮
delay(500); // 延迟
P0 = 0xFD; // 0xFD -> 1111 1101 *********************
delay(500); // 延迟
P0 = 0xFB; // 0xFB -> 1111 1011
delay(500); // 延迟
P0 = 0xF7; // 0xF7 -> 1111 0111
delay(500); // 延迟
P0 = 0xEF; // 0xEF -> 1110 1111 低电平依次向右移动,控制不同的灯亮
delay(500); // 延迟
P0 = 0xDF; // 0xDF -> 1101 1111
delay(500); // 延迟
P0 = 0xBF; // 0xBF -> 1011 1111
delay(500); // 延迟
P0 = 0x7F; // 0x7F -> 0111 1111 **********************
delay(500);
}
}
2.1.4.4 优化

Switch case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 理论,使用switch case语句使程序更加公正
#include <reg52.h>

void delay(unsigned int x) {
unsigned int i,j;
for(i=0; i<x; i++){
for(j=0; j<120; j++);
}
}

void main() {
unsigned char i=1; //定义新的变量用来轮流调用不同的case语句
while (1) {
switch(i) {
case 1: P2 = 0xFE; break;
case 2: P2 = 0xFD; break;
case 3: P2 = 0xFB; break;
case 4: P2 = 0xF7; break;
case 5: P2 = 0xEF; break;
case 6: P2 = 0xDF; break;
case 7: P2 = 0xBF; break;
case 8: P2 = 0x7F; i=0; break; //小细节
}
i++;
delay(500);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 实际
#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void delay(unsigned int x) {
unsigned int i,j;
for(i=0; i<x; i++) {
for(j=0; j<120; j++);
}
}

void main() {
unsigned char i=0;
ENLED = 0;
ADDR3 = 1; // 使能三八译码器

ADDR2 = 1; //*****************************
ADDR1 = 1; //使三八译码器的IO6口输出低电平
ADDR0 = 0; //*****************************

while(1) {
switch(i) {
case 1: P0 = 0xFE; break;
case 2: P0 = 0xFD; break;
case 3: P0 = 0xFB; break;
case 4: P0 = 0xF7; break;
case 5: P0 = 0xEF; break;
case 6: P0 = 0xDF; break;
case 7: P0 = 0xBF; break;
case 8: P0 = 0x7F; break;
}
i++;
delay(500);
}
}

② 数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 理论
#include <reg52.h>

void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void main()
{
unsigned char i = 0;
unsigned char LEDSET[8]={0xFE,0xFD,0xFB,0xF7,0xEF,0xDF,0xBF,0x7F};

while(1)
{
P0=LEDSET[i];
i++;
if(i>=8)i=0;//i需归0使P0只能使用数组中的8个元素,防止使用到超出数组元素以外的值,其实就是让i在0~7之间变化
delay_ms(100);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 实际
#include <reg52.h>
sbit ADDR2 = P1^2;
sbit ADDR1 = P1^1;
sbit ADDR0 = P1^0;
sbit ENLED = P1^4;
sbit ADDR3 = P1^3;

void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void main()
{
unsigned char i = 0;
unsigned char LEDSET[8]={0xFE,0xFD,0xFB,0xF7,0xEF,0xDF,0xBF,0x7F};
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 1;//**************************
ADDR1 = 1;//让三八译码器的IO6输出低电平
ADDR0 = 0;//**************************

while(1)
{
P0=LEDSET[i];
i++;
if(i>=8)i=0;//i需归0使P0只能使用数组中的8个元素,防止使用到超出数组元素以外的值,其实就是让i在0~7之间变化
delay_ms(100);
}
}

③ 移位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 理论
#include <reg52.h>

void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void main()
{
unsigned char i = 0;
while(1)
{
P0=~(0x01<<i);//第一次运行这条语句i是等于0的,先算出括号中的值:0x01左移0位还是0x01,算出了括号中的值再去取反就得0xFE,
//所以第一次运行这条语句时就相当于“P0=0xFE;”,第二次循环运行时i已经等于1,0x01左移1位就为0x02,取反得0xFD,所以第二次执行这条语句是“P0=0xFD;”
i++;
if(i>=8)i=0;
delay_ms(100);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 实际
#include <reg52.h>
sbit ADDR2 = P1^2;
sbit ADDR1 = P1^1;
sbit ADDR0 = P1^0;
sbit ENLED = P1^4;
sbit ADDR3 = P1^3;

void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}
void main()
{
unsigned char i = 0;
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 1;//**************************
ADDR1 = 1;//让三八译码器的IO6输出低电平
ADDR0 = 0;//**************************

while(1)
{
P0=~(0x01<<i);//第一次运行这条语句i是等于0的,先算出括号中的值:0x01左移0位还是0x01,算出了括号中的值再去取反就得0xFE,
//所以第一次运行这条语句时就相当于“P0=0xFE;”,第二次循环运行时i已经等于1,0x01左移1位就为0x02,取反得0xFD,所以第二次执行这条语句是“P0=0xFD;”
i++;
if(i>=8)i=0;
delay_ms(100);
}
}

2.1.4 交通灯的实现

2.1.4.1 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 理论
#include <reg52.h>

sbit RED1 = P2^0; //第一组红灯
sbit YELLOW1 = P2^1; //第一组黄灯
sbit GREEN1 = P2^2; //第一组绿灯
sbit RED2 = P2^3; //第二组红灯
sbit YELLOW2 = P2^4; //第二组黄灯
sbit GREEN2 = P2^5; //第二组绿灯

void delay(unsigned int t) //简单的延时函数
{
unsigned int i, j;
for(i = 0; i < t; i++)
{
for(j = 0; j < 120; j++);
}
}

void main()
{
while(1)
{
//第一组绿灯,第二组红灯
GREEN1 = 0;
YELLOW1 = 1;
RED1 = 1;
GREEN2 = 1;
YELLOW2 = 1;
RED2 = 0;
delay(5000); //绿灯持续5秒钟

//第一组黄灯,第二组红灯
YELLOW1 = 0;
GREEN1 = 1;
delay(1000); //黄灯持续1秒钟

//第一组红灯,第二组绿灯
RED1 = 0;
YELLOW1 = 1;
GREEN1 = 1;
RED2 = 1;
YELLOW2 = 1;
GREEN2 = 0;
delay(5000); //绿灯持续5秒钟

//第一组红灯,第二组黄灯
YELLOW2 = 0;
GREEN2 = 1;
delay(1000); //黄灯持续1秒钟
}
}

陈同学版本,主打就是一个位移,大家可以体会一下他的逻辑,注意|按位取或

注意|按位取或,就是说有11,全00

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <reg52.h>

void delay(unsigned int x) {
unsigned int i, j;
for(i=0; i<x; i++){
for(j=0; j<120; j++);
}
}

void main() {
unsigned char a,b;
unsigned char i, j;
a = 0xfe;
b = 0xfe;

while(1) {
P0 = a;
P1 = b;
delay(5000);

if(a == 0xfe){
a = 1|(a << 1);
P0 = a;
delay(3000);
}

if(a == 0xfd){
a = 1|(a << 1); // yellow -> red
b = 1|(b << 1); // red -> green
P0 = a;
P1 = b;
delay(5000);
}

if(b == 0xfd){
b = 1|(b << 1); // green -> yellow
P1 = b;
delay(3000);
}

if(b == 0xfb){
a = 0xfe;
b = 0xfe;
}
}
}
2.1.4.2 仿真

2.1.5 花式流水灯

当我们想实现花式流水灯时还是需要运用到数组的,毕竟数组可以修改LED的状态值。

2.1.5.1 右移流水灯源码

我将各种实现方法写在了一起,大家根据自己的需要选取

感谢陈兴龙同学提供的代码(^_^)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 理论
// P0口,switch case
#include <reg52.h>

void delay(unsigned int x) {
unsigned int i,j;
for(i=0; i<x; i++){
for(j=0; j<120; j++);
}
}

void main() {
unsigned char i=1;
while(1){
switch(i) {
case 1: P2 = 0x7F; break;
case 2: P2 = 0xBF; break;
case 3: P2 = 0xDF; break;
case 4: P2 = 0xEF; break;
case 5: P2 = 0xF7; break;
case 6: P2 = 0xFB; break;
case 7: P2 = 0xFD; break;
case 8: P2 = 0xFE; i=0; break;
}
i++;
delay(500);
}
}

// 数组
#include <reg52.h>

void delay(unsigned int x) {
unsigned int i,j;
for(i=0; i<x; i++){
for(j=0; j<120; j++);
}
}

void main() {
unsigned char i=0;
unsigned char j[8] = {0x7F, 0xBF, 0xDF, 0xEF, 0xF7, 0xFB,0xFD, 0xFE};
while(1){
P2 = j[i];
delay(500);
i++;
if(i==8){
i=0;
}
}
}

// 移位
#include <reg52.h>

void delay(unsigned int x) {
unsigned int i,j;
for(i=0; i<x; i++){
for(j=0; j<120; j++);
}
}

void main() {
unsigned char i;
unsigned char a, b=10000000;
while(1) {
a = 0x7F;
for(i=0; i<8; i++){
P2 = a;
a = b|(a>>1);
delay(500);
}
}
}
2.1.5.2 循环流水灯
2.1.5.3 奇偶流水灯

感谢陈兴龙同学提供的数组代码(^_^)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 理论
// 移位方法
#include <reg52.h>
#define uint unsigned int

// 延时函数
void delay(uint x)
{
uint i,j;
for(i=x;i>0;i--)
for(j=120;j>0;j--);
}

// 偶数灯逐个点亮
void even_light()
{
unsigned char i;
for(i=0;i<8;i+=2) //循环控制点亮偶数灯
{
P2 = ~(1<<i); //点亮第i个LED灯, 偶数
delay(500); //延时
}
}

// 奇数灯逐个点亮
void odd_light()
{
unsigned char i;
for(i=1;i<8;i+=2) //循环控制点亮奇数灯
{
P2 = ~(1<<i); //点亮第i个LED灯,奇数
delay(500); //延时
}
}

// 全部点亮然后熄灭
void all_light()
{
unsigned char i;
for(i=0; i<4; i++) {
P2 = 0x00;
delay(500); //延时1s
P2 = 0xFF; //全灭
delay(500); //延时
}
}

// 奇偶逐个点亮后全灭
void even_odd_light_out()
{
unsigned char i;
for(i=0; i<8; i++){
P2 = ~(1<<i);
delay(500);
}
P2 = 0xFF; //全灭
delay(1000); //延时
}

// 主函数
void main() {
while(1) //循环
{
even_light(); // 偶数灯逐渐点亮
odd_light(); // 奇数灯逐渐点亮
all_light(); // 所有灯全亮
even_odd_light_out(); //慢慢来
}
}

// 数组
# include <reg52.h>
void delay(){
unsigned int i,j;
for(i=500;i>0;i--){
for(j=120;j>0;j--);
}
}
void main(){
unsigned char code ledj[4]={0xfe,0xfb,0xef,0xbf};//奇数灯
unsigned char code ledo[4]={0xfd,0xf7,0xdf,0x7f};//偶数灯
unsigned char i;
while(1){
for(i=0;i<4;i++){
P0=ledo[i];
delay();
}
for(i=0;i<4;i++){
P0=ledj[i];
delay();
}
for(i=0;i<4;i++){
P0=0x00;
delay();
P0=0xff;
delay();
}
for(i=0;i<4;i++){
P0=ledj[i];
delay();
P0=ledo[i];
delay();
}
P0=0xff;
delay();
}
}

2.1.5.4 按键控制流水灯

感谢李肖坤同学提供了该部分的代码,这里需要注意不能直接写语句P3^0==0的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 实际 普中开发板
#include <reg52.h>
unsigned int r=0;
sbit KEY2 = P3^0;

// 延时
void delay() //@12.000MHz
{
unsigned char i, j;

i = 39;
j = 230;
do
{
while (--j);
} while (--i);
}



void main()
{
while(1)
{
if(KEY2 == 0) // 按下给0, 松开给1
{
delay(); // 防抖20ms

if(KEY2 == 0) {
while(KEY2 == 0);
if(KEY2 == 1) {
P2 = ~(0x01<<r); //移位
r++;
if(r==9)r=0;
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//实际 普中开发板 按键控制左右循环流水灯
#include <reg52.h>
unsigned int r=0;
unsigned int n=6;
sbit KEY2 = P3^0;

// 延时
void delay() //@12.000MHz
{
unsigned char i, j;

i = 39;
j = 230;
do
{
while (--j);
} while (--i);
}



void main()
{
while(1)
{
if(KEY2 == 0) // 按下给0, 松开给1
{
delay(); // 防抖20ms

if(KEY2 == 0) {
while(KEY2 == 0);
if(KEY2 == 1) {
if(r<8){
P2 = ~(0x01<<r); //移位
r++;
}
else
{
P2 = ~(0x01<<n);
n--;
if(n==0)
{r=0;
n=6;}
}
}
}
}
}
}

2.2 蜂鸣器

2.2.1 蜂鸣器驱动代码

无源蜂鸣器的硬件连接因为不像LED那样都接了很多其他器件来初始化,所以驱动代码比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 实际
#include <reg52.h>
sbit BEEP = P1^6;
void main()
{
unsigned int i;
while(1)
{
BEEP=1;
for(i=0;i<14;i++);//延时接近0.125ms,给P1.6保持了0.125ms高电平时间
BEEP=0;
for(i=0;i<14;i++);//低电平时间保持了0.125ms, 我们可以尝试把两个for语句里的14改大一点就会发现鸣叫的音调变得低沉一些了
}
}

2.2.2 蜂鸣器鸣叫

1
2
3
4
5
6
7
8
9
10
11
12
// 实际 简化
#include <reg52.h>
sbit BEEP = P1^6;
void main()
{
unsigned int i;
while(1)
{
BEEP=!BEEP; // 蜂鸣器鸣叫,通过P1.6就要以一定的时间间隔(也可叫一定频率)不停的高低电平切换。这里小细节,取非符号
for(i=0;i<14;i++);//只需改变一次for语句中的14就可以实现不同音调的鸣叫了
}
}

这里启发我们使用LED=!LED 可以实现LED灯闪烁

2.2.3 控制蜂鸣器

我们先演示一个错误代码,借助于控制LED小灯亮灭的启发,我们可能写出如下的主要程序控制蜂鸣器的高低电平

1
2
3
4
5
6
7
8
while(1){
BEEP = 1;
for(i=0;i<14;i++); //延迟0.125ms
BEEP = 0;
for(i=0;i<14;i++); //延迟0.125ms

delay(5000);
}

我们简单分析以上代码,下面的图解可以看出来以上代码和上面的驱动代码的区别

image-20230405002829461

很明显使用延迟函数Delay()函数延迟控制不满足蜂鸣器的驱动条件:用时间表示就是要输出周期为0.22ms~2ms((1/4500s)~(1/500s))范围的方波,这个周期内高电平时间和低电平时间各占一半。

那么我们要控制蜂鸣器的响与不响该如何做?

驱动代码之所以能一直响,是因为在死循环里这些方波持续的时间无限长。那么我们要让它响一段时间,就让这个方波持续着这段时间;然后把IO端口电平固定住不发生改变,无源蜂鸣器没有脉冲信号所以就不响了。这时我们再延时1秒,在这一秒里无源蜂鸣器不会响,过了这一秒之后再让IO端口持续输出一段方波时间,这时就可以再次弄响无源蜂鸣器,就这样死循环下去也就达到自己想的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 实际
#include <reg52.h>
sbit BEEP = P1^6;

// 延迟
void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

// 使用循环嵌套,内层循环控制蜂鸣器一直鸣叫,内层循环完成后,电平不在发生变换,蜂鸣器不在鸣叫,延迟1s钟后继续进入内层循环进行鸣叫
void main()
{
unsigned int i,time;

while(1)
{
for(time=0;time<800;time++)//800决定鸣叫的时长
{
BEEP=!BEEP;
for(i=0;i<30;i++);//这里改为30延时长一点把鸣叫音调调低一些
}
delay_ms(1000);//延时1s
}
}

2.2.4 蜂鸣器+LED实际案例

机器报警时,灯亮的时候蜂鸣器就响,灯灭的时候就不响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 实际
#include <reg52.h>
sbit BEEP = P1^6; // 控制蜂鸣器的高低电平
sbit LED2 = P0^0;
sbit ADDR2 = P1^2;
sbit ADDR1 = P1^1;
sbit ADDR0 = P1^0;
sbit ENLED = P1^4;
sbit ADDR3 = P1^3;

//延迟函数
void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void main()
{
unsigned int i,time;
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 1;//**************************
ADDR1 = 1;//让三八译码器的IO6输出低电平
ADDR0 = 0;//**************************

while(1)
{
LED2=0; //灯亮,同时蜂鸣器鸣叫
for(time=0;time<3700;time++)//软件调试出此处for循环用了1秒
{
BEEP=!BEEP;
for(i=0;i<30;i++);
}
BEEP=0;//固定住蜂鸣器的电平使其不响,其实这条语句也可不写,因为上面的for语句执行完就没有方波产生了也就不响了,大家可以注释掉这个语句看看现象是不是一样的
LED2=1; //蜂鸣器不响,灯灭
delay_ms(1000);//延时1s
}
}

2.3 数码管

2.3.1 数码管原理

pass

2.3.2 静态数码管显示

1
2
3
4
5
6
7
8
9
10
11
12
// 显示单个数字 0
// 理论
#include<reg52.h>

void seg() {
P2 = 0x3F; //0011 1111 -> 0x3F
}

void main(){
seg();
while(1);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 显示零到九的循环
// 理论
unsigned char s[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};

void delay(unsigned int n) {
unsigned int i = 0,j = 0;
for (i=0; i<n; i++)
for(j=0; j<120; j++);
}


void seg() {
int i=0;
for(i=0; i<10; i++)
{
P2 = s[i];
delay(800);
}
}

void main() {
while(1) {
seg();
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 实际
// 0~F显示
#include<reg52.h>

unsigned char code LedChar[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E}; // 0~F的真值表,使用code关键字使char类型LedChar放入Flash而不放入RAM,之所以放入Flash是因为我们不需要改变该真值表的值

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

void main() {
unsigned char cnt = 0; // 记录T0中断次数
unsigned char sec = 0; // 记录经过的秒数

ENLED = 0; //使能 U3,选择数码管 DS1
ADDR3 = 1; //为 T0 赋初值 0xB800
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0; //启动 T0

TMOD = 0x01; //设置 T0 为模式 1
TH0 = 0xB8;
TL0 = 0x00;
TR0 = 1;

while(1) {
if (TF0 == 1) { //判断 T0 是否溢出
TF0 = 0; //T0 溢出后,清零中断标志
TH0 = 0XB8; //并重新赋初值
TL0 = 0x00;
cnt++; //计数值自加 1
if (cnt >= 50){ //判断 T0 溢出是否达到 50 次
cnt = 0; //达到 50 次后计数值清零
P0 = LedChar[sec]; //当前秒数对应的真值表中的值送到 P0 口
sec++; //秒数记录自加 1
if (sec >= 16){ //当秒数超过 0x0F(15)后,重新从 0 开始
sec = 0;
}
}
}
}
}

2.3.3 动态数码管显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<reg52.h>

unsigned char str[] = {0x76, 0x79, 0x38, 0x38, 0x3F}; //HELLO 的段选
unsigned char wei[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; //共阴极的线选

void delay(unsigned int n) {
unsigned int i = 0,j = 0;
for (i=0; i<n; i++)
for(j=0; j<120; j++);
}


void seg() {
int i=0;
for(i=0; i<5; i++) // 线选为5
{
P3 = ~wei[i];
P2 = str[i];
delay(5); // 调小点是为了视觉残留的效果
}
}

void main() {
while(1) {
seg();
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 实际
#include<reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

unsigned char code LedChar[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E}; // 数码管显示字符转换表表
unsigned char LedBuff[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 数码管显示缓冲区,初值0xFF确保启动时都不亮
void main() {
unsigned char i = 0; //动态扫描索引
unsigned int cnt = 0; // 记录T0中断次数
unsigned long sec = 0; //记录经过的秒数

ENLED = 0;
ADDR3 = 1;

TMOD = 0x01; // 设置T0模式为1 -> 0x01
TH0 = 0xFC; //为T0设置初值为0xFC67,定时为1ms,TH高
TL0 = 0x67; // TL低
TR0 = 1; // 启动T0

while(1) {
if (TF0 == 1) { // 判断T0是否溢出
TF0 = 0;
TH0 = 0xFC; //为T0重新赋值0xFC67
TL0 = 0x67;
cnt++; //计数器自加1
if (cnt >= 1000) { // 判断T0溢出是否达到1000次
cnt = 0; // 达到1000次后计数值清零
sec++; // 秒计数加1
// 以下代码将sec按十进制位从低到高依次提取并转为数码管显示字符
LedBuff[0] = LedChar[sec%10];
LedBuff[1] = LedChar[sec/10%10];
LedBuff[2] = LedChar[sec/100%10];
LedBuff[3] = LedChar[sec/1000%10];
LedBuff[4] = LedChar[sec/10000%10];
LedBuff[5] = LedChar[sec/100000%10];
}
// 以下代码完成数码管动态扫描刷新
if (i == 0){
ADDR2 = 0; ADDR1 = 0; ADDR0 = 0; i++; P0 = LedBuff[0];
}
else if (i == 1) {
ADDR2 = 0; ADDR1 = 0; ADDR0 = 1; i++; P0 = LedBuff[1];
}
else if (i == 2) {
ADDR2 = 0; ADDR1 = 1; ADDR0 = 0; i++; P0 = LedBuff[2];
}
else if (i == 3) {
ADDR2 = 0; ADDR1 = 1; ADDR0 = 1; i++; P0 = LedBuff[3];
}
else if (i == 4) {
ADDR2 = 1; ADDR1 = 0; ADDR0 = 0; i++; P0 = LedBuff[4];
}
else if (i == 5) {
ADDR2 = 1; ADDR1 = 0; ADDR0 = 1; i = 0; P0 = LedBuff[5];
}
}
}
}

2.3.4 4只数码管滚动显示0~3

2.3.4.1 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 理论
#include <reg52.h>

// 数码管段选信号数组
unsigned char code digit[4] = {0x01, 0x02, 0x04, 0x08};
// 显示数字数组
unsigned char code num[4] = {0x3F, 0x06, 0x5B, 0x4F};
// 定义计数器
unsigned int count = 0;

// 延时函数
void delay(unsigned int xms)
{
unsigned int i, j;
for (i = xms; i > 0; i--)
for (j = 110; j > 0; j--);
}

void main()
{
// 定义计数器循环控制变量i
unsigned char i = 0;
// 循环控制变量j
unsigned char j = 0;
// 定义数码管位选信号P2口为输出
P3 = 0xff;
// 定义数码管段选信号P0口为输出
P2 = 0xff;
while (1)
{
// 如果j等于4,则将j赋值为0,继续从0开始
if (j == 4)
{
j = 0;
}
// 将数码管位选信号设置为第j位
P3 = ~digit[j];
// 将数码管段选信号设置为对应数字
P2 = num[count % 4];
// 延时200ms
delay(200);
// 计数器加1
count++;
// 循环控制变量j每次加1
j++;
}
}
2.3.4.2 仿真

2.3.5 8只数码管滚动显示8~F

2.3.5.1 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <reg52.h>

// 数码管段选信号数组
unsigned char code digit[8] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};
// 显示数字数组
unsigned char code num[8] = {0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};
// 定义计数器
unsigned int count = 0;

// 延时函数
void delay(unsigned int xms)
{
unsigned int i, j;
for (i = xms; i > 0; i--)
for (j = 110; j > 0; j--);
}

void main()
{
// 定义计数器循环控制变量i
unsigned char i = 0;
// 循环控制变量j
unsigned char j = 0;
// 定义数码管位选信号P2口为输出
P3 = 0xff;
// 定义数码管段选信号P0口为输出
P2 = 0xff;
while (1)
{
// 如果j等于4,则将j赋值为0,继续从0开始,count也负值为0;
if (j == 8)
{
j = 0;
count = 0;
}
// 将数码管位选信号设置为第j位
P3 = ~digit[j];
// 将数码管段选信号设置为对应数字
P2 = num[count];
// 延时200ms
delay(200);
// 计数器加1
count++;
// 循环控制变量j每次加1
j++;
}
}
2.3.5.2 仿真

2.3.6 8只数码管显示不同字符

2.3.6.1 源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 理论
#include <reg52.h>

// 数码管段选信号数组
unsigned char code digit[8] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};
// 显示数字数组
unsigned char code num[8] = {0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71};
// 定义计数器
unsigned int count = 0;

// 延时函数
void delay(unsigned int xms)
{
unsigned int i, j;
for (i = xms; i > 0; i--)
for (j = 110; j > 0; j--);
}

void main()
{
// 定义计数器循环控制变量i
unsigned char i = 0;
// 定义数码管位选信号P2口为输出
P3 = 0xff;
// 定义数码管段选信号P0口为输出
P2 = 0xff;
while (1)
{
for(i=0; i<8; i++){
// 将数码管位选信号设置为第j位
P3 = ~digit[i];
// 将数码管段选信号设置为对应数字
P2 = num[i];
// 延时5ms
delay(5); // 延时够短,人眼分辨不出来就行
}
}
}
2.3.6.2 仿真

2.4 独立按键

2.4.1 按下按键LED小灯亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 实际
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit KEY4 = P2^7;
sbit GND = P2^3;

void main() {
ENLED = 0;
ADDR3 = 1; // 使能三八译码器

ADDR2 = 1;//*****************************
ADDR1 = 1;//使三八译码器的IO6口输出低电平
ADDR0 = 0;//*****************************

GND = 0; //使P2.7能具备被拉低的条件

while(1) {
P0 = KEY4*0xFF; //很有意思,这里独立按键就好像输出0(按下按键)和1(松开按键)一样
// 0时8个小灯全亮(0x00),1时全灭(0xff)
//P0 = ~(KEY4*0xFF)
}

}

2.4.2 按键按下蜂鸣器鸣响

使用无源蜂鸣器,按键按下时,蜂鸣器就响,松开不按时就不响。我们知道在P2^3输出低电平的情况下,K4按下的时候程序钟KEY4等于0,松开不按时KEY4等于1。所以我们想要实现目的,可以在主函数中的死循环中用if(KEY4==0)来一直等待K4被按下,于是KEY4的值等于0才能进入if语句钟执行程序。不按下时KEY4等于1就不能进入执行程序,CPU之只能执行空循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//实际
#include <reg52.h>

sbit BEEP = P1^6;
sbit KEY4 = P2^7;

void main() {
unsigned int i;
P2 = 0xF7; //让K4具备有被拉低的条件 ->1111 0111 -> P2^3输出低电平

while(1) {
if(KEY4 == 0) {
BEEP = !BEEP;
for(i=0; i<25; i++); //不同时间的延时鸣叫的音调不同
}
}
}

2.4.3 测试“按键按下时P2.7的状态”持续的时间到底是多长

“按键按下时P2.7的状态”持续的时间到底是多长,用流水灯的方式来查看按下之后迅速松开,小灯会跳到哪里显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//实际
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit KEY4 = P2^7;

// 延时函数
void delay(unsigned int x) {
unsigned char i, j;
for(i=0; i<x; i++) {
for(j=0; j<120; j++);
}
}

void main() {
unsigned char i = 1;
ENLED = 0;
ADDR3 = 1; //使能三八译码器

ADDR2 = 1;//*****************************
ADDR1 = 1;//使三八译码器的IO6口输出低电平
ADDR0 = 0;//*****************************

P2 = 0xF7;//使P2^7具备被拉低的条件
P0 = 0xFE;//先点亮最右端的小灯

while(1) {
if(KEY4 == 0) {
delay(10);
P0 = ~(0x01<<i);
i++;
if(i>=8)i=0;
}
}
}

根据dotcpp网站的测试得出一下结论:即使手速再怎么快地按下K4马上松开,可在这段时间里点亮的LED2跳到了LED5点亮,也就是说按键在物理上的导通时间超过了30ms后面i绝对大于3,因为LED5亮,所以有“P0=0xF7;”推出“ ~(0x01<<3)”,然后再有“i++;”,i绝对大于3。

P2.7在按键动作中被拉低的持续时间就有60~90ms

2.4.4 按键控制灯亮灯灭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//实际 大家会发现这种方法是存在缺陷的
#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit KEY4 = P2^7;

sbit LED = P0^0;

void delay(unsigned int x) {
unsigned char i, j;
for(i=0; i<x; i++) {
for(j=0; j<120; j++);
}
}

void main() {
ENLED = 0;
ADDR3 = 1; //使能三八译码器

ADDR2 = 1; //*************************
ADDR1 = 1; //使三八译码器IO6输出低电平
ADDR0 = 0; //*************************

P2 = 0xF7; //使KEY4具备被拉低的条件
while(1){
if(KEY4 == 0) {
delay(50); //延迟50ms
if(KEY4 == 0) { //在此判断,如果仍为0就说明是稳定态的按下0
// 执行代码
LED = !LED; // 每进入一次都将改变一次LED的状态,而进入需要判断KEY4的状态,实现按键控制LED。
}
}
}
}

可是每个人的按键手速不同,我们发现如果按下的时间稍微长一点(没松手),那么LED2就会闪烁,也就是“LED2=!LED2;”被多次执行,要是刻意快速按下就松手,LED2没反应,所以这样的代码是做不到普遍通用的。还有我们用50ms做延时太影响CPU的运行效率了,所以我们要引入支持连按不支持连按的按键概念。

2.4.5 不支持连按模式

基于1.7.5节的分析,我们可以使用以下代码实现不支持连按模式。可以发现无论我们的按下手速有多快或多慢,“ LED2=!LED2;”只能被执行一次而已。这样就像按电磁炉上的按键一样,一次只能切换一回灯的亮灭,即使不松手也不会出现灯的闪烁,这就是不支持连按的代码书写方式,不过这样的代码还是存在缺陷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 实际
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit LED = P0^0;

sbit KEY4 = P2^7;

//延时函数
void delay_ms(unsigned int x){
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

//主函数
void main() {
ENLED = 0;
ADDR3 = 1; // 使能三八译码器

ADDR2 = 1; //************************
ADDR1 = 1; //使三八译码器的IO6口输出低电平
ADDR0 = 0; //************************

P2 = 0xF7; //使P2^7具备能被拉低的条件

while(1) {
if(KEY4 == 0) {
delay_ms(10); //等待抖动过去
if (KEY4 == 0) { //二次判断
LED = !LED;
while(KEY4 == 0); //如果IO端口还是保持低电平,此时也就是没有松手,那
//括号里的条件满足,程序一直在循环这条语句,所以程序停止不往下执行了,
//直到KEY4等于1,也就是按键松手了,while里面的条件不成立才退出循环,放行程序
}
}
}
}

改善我们的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 实际 金沙滩工作室开发板 按下松开后小灯左移
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit LED = P0^0;

sbit KEY4 = P2^7;

// 延时函数
void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void main() {
unsigned char key_up=1; //定义记录按键状态值的变量,初始值为1是为了
// 避免程序一开始就进入if(key_up==0)
unsigned char i = 1;

ENLED = 0;
ADDR3 = 1; //使能三八译码器

ADDR2 = 1; //***************************
ADDR1 = 1; //使三八译码器IO6口输出低电平
ADDR0 = 0; //***************************

P2 = 0xF7; //使P2^7具备能被拉低的条件
P0 = 0xFE; //先点亮LED灯

while(1) {
if(key_up == 0) {
if(KEY4 == 1) { // 只要不松手,KEY4就会等于零, 只要在按键抬起之后才执行功能代码
P0 = ~(0x01<<i);
i++;
if(i>=8)i=0;
}
}
key_up = KEY4; //如果不松手,key_up就会等于0
delay_ms(2); //假设这部分是要执行的其他程序
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 实际 金沙滩工作室单片机 按下小灯左移 松开不变
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit LED = P0^0;

sbit KEY4 = P2^7;

// 延时函数
void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void main() {
unsigned char key_up=0; //定义记录按键状态值的变量,初始值为0是为了
// 避免程序一开始就进入if(key_up==0)
unsigned char i = 1;

ENLED = 0;
ADDR3 = 1; //使能三八译码器

ADDR2 = 1; //***************************
ADDR1 = 1; //使三八译码器IO6口输出低电平
ADDR0 = 0; //***************************

P2 = 0xF7; //使P2^7具备能被拉低的条件
P0 = 0xFE; //先点亮LED灯

while(1) {
if(key_up == 1) {
if(KEY4 == 0) { // 只要不松手,KEY4就会等于零, 只要在按键抬起之后才执行功能代码
P0 = ~(0x01<<i);
i++;
if(i>=8)i=0;
}
}
key_up = KEY4; //如果不松手,key_up就会等于0
delay_ms(2); //假设这部分是要执行的其他程序
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 实际 金沙滩工作室 按键控制左右移流水灯
#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit LED = P0^0;
sbit KEY4 = P2^7;

// 延迟函数
void delay(unsigned int x) {
unsigned char i, j;
for(i=0; i<x; i++) {
for(j=0; j<120; j++);
}
}

// 主函数
void main() {
unsigned char key_up = 0; //key_up用于监控KEY4的状态,初值0是为了避免直接进入if(key_up==1)
unsigned char i = 1;
unsigned int mask = 10000000; //掩码
unsigned char a = 0x7F;

ENLED = 0;
ADDR3 = 1; //使能三八译码器

ADDR2 = 1; //*****************************
ADDR1 = 1; //使是三八译码器IO6口输出低电平
ADDR0 = 0; // ****************************

LED = 0; //事先点亮一个LED
P2 = 0xF7; //使P2^7具有能被拉低的条件

while(1) {
if(key_up == 1) {
if(i == 15){
i = 1;
a = 0x7F;
}
if(KEY4 == 0) {
if(i < 8){
P0 = ~(0x01 << i);
i++;
} // 左移
else{
a = mask|(a >> 1);
P0 = a;
i++;
}
}
}
key_up = KEY4;
delay(50);
}

}

2.4.6 回归按键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 实际 回归按键 使用static静态变量关键字
#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit KEY4 = P2^7;
sbit LED2 = P0^0;
// 延迟函数
void delay_ms(unsigned int x) {
unsigned char i, j;
for(i=0; i<x; i++) {
for(j=0; j<120; j++);
}
}

void KEY_task()
{
static unsigned char key_up=1;
if(key_up==0)
{
if(KEY4==1)//不支持连按
{
LED2 = !LED2;
}
}
key_up=KEY4; //如果不松手,key_up就会等于0
}

void main() {
ENLED = 0;
ADDR3 = 1; //使能三八译码器

ADDR2 = 1; //*****************************
ADDR1 = 1; //使三八译码器的IO6口输出低电平
ADDR0 = 0; //*****************************

P2 = 0xF7; //使P2^7具备被拉低的条件
while(1){
KEY_task(); // 按键功能任务
delay_ms(2); // 这里可以放其他程序
}
}

2.4.7 支持连按

这次我们把数码管显示的内容代码封装成函数,定义一个全局变量cnt,cnt在主函数中通过按键动作来改变这个值,然后数码管负责显示这个数;实验现象就是按着K4不放,数码管显示cnt的值一直累加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <reg52.h>

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit KEY4 = P2^7;

unsigned char code LedChar[16] = {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E}; //数码管状态值初始化
unsigned char LedBuff[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管显示缓存区
unsigned char cnt = 0; //可以在SEG_task()和main中使用的全局变量

void SEG_task() //数码管显示函数
{
static unsigned char i=0;
LedBuff[0] = LedChar[cnt%10];
if(cnt>=10)LedBuff[1] = LedChar[(cnt/10)%10]; //cnt没有达到10之前不更新LedBuff[1]的初始值
if(cnt>=100)LedBuff[2] = LedChar[(cnt/100)%10]; //cnt没有达到100之前不更新LedBuff[2]的初始值
if(cnt==0){
LedBuff[1] = 0xFF;
LedBuff[2] = 0xFF;
} //cnt到达255之后再加1就溢出变为0了,这时候要再次熄灭这两个数码管

P0 = 0xFF; // 端口状态全部熄灭数码管里的LED达到刷新作用
switch(i) {
case 0: ADDR2 = 0; ADDR1 = 0; ADDR0 = 0; P0 = LedBuff[0]; i++; break;
case 1: ADDR2 = 0; ADDR1 = 0; ADDR0 = 1; P0= LedBuff[1]; i++; break;
case 2: ADDR2 = 0; ADDR1 = 1; ADDR0 = 0; P0 = LedBuff[2]; i=0; break;
}
}

void main()
{
unsigned char key_up=1;//定义记录按键状态值的变量,初始值为1避免程序一开始就进入了“if(key_up==0)”
unsigned int times=0; //用来记录进入过按键判断语句的次数
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 1;//**************************
ADDR1 = 1;//让三八译码器的IO6输出低电平
ADDR0 = 0;//**************************
P2 = 0xF7;//让K4能具备有被拉低的条件先

while(1)
{
SEG_task();//数码管显示任务

//按键功能部分
if(key_up==0)
{
if(KEY4==0) //之前“KEY4==1”是不支持连按,现在改为“KEY4==0”就成为支持连按了
{
times++;
if(times>=1000) //按键IO端口一直是低电平times就一直累加,累加到1000意味低电平持续了一段时间了,该执行功能代码了,修改1000这个数的话那么cnt自加的速度就会改变
{
times=0;
cnt++; //执行功能代码
}
}
}
key_up=KEY4; //如果不松手,key_up就会等于0
}
}

2.4.8 优化不支持连按的代码

按键不支持连按2.4.5节的代码中,死循环都有delay_ms(2);\delay(2),因为大多数时候主循环都要做很多事,所以我们认为这2ms的延时是很多复杂程序要执行所消耗的时间,而正是因为这个延时函数的存在把按键的物理抖动给滤掉了,误导了我们以为这样的不支持连按代码是合格的。如果我们还是用这种写法去实现不支持连按功能,那么请把下面的代码下载进开发板通过快按慢按K4,观察数码管的显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <reg52.h> 
sbit ADDR2 = P1^2;
sbit ADDR1 = P1^1;
sbit ADDR0 = P1^0;
sbit ENLED = P1^4;
sbit ADDR3 = P1^3;

sbit LED2 = P0^0;
sbit KEY4 = P2^7;
unsigned char code LedChar[16]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E};//数码管状态值初始化
unsigned char LedBuff[6]={0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 数码管缓冲
unsigned char cnt=0;//可以在SEG_task()和main()中使用的全局变量

void SEG_task()//数码管显示函数
{
static unsigned char i=0;
LedBuff[0]= LedChar[cnt%10];
if(cnt>=10) LedBuff[1]= LedChar[(cnt/10)%10]; //cnt没到达10之前不更新LedBuff[1]的初始值
if(cnt>=100)LedBuff[2]= LedChar[(cnt/100)%10];//cnt没到达100之前不更新LedBuff[2]的初始值
if(cnt==0){ LedBuff[1]=0xFF;LedBuff[2]=0XFF; }//cnt到达255之后再加1就溢出变为0了,这时候要再次熄灭这两个数码管

P0=0xFF;
switch(i)
{
case 0:
ADDR2 = 0;ADDR1 = 0;ADDR0 = 0;P0=LedBuff[0];i++;break;
case 1:
ADDR2 = 0;ADDR1 = 0;ADDR0 = 1;P0=LedBuff[1];i++;break;
case 2:
ADDR2 = 0;ADDR1 = 1;ADDR0 = 0;P0=LedBuff[2];i=0;break;
}
}

void main()
{
unsigned char key_up=1;//定义记录按键状态值的变量,初始值为1避免程序一开始就进入了“if(key_up==0)”
unsigned int times=0; //用来记录进入过按键判断语句的次数
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 1;//**************************
ADDR1 = 1;//让三八译码器的IO6输出低电平
ADDR0 = 0;//**************************
P2 = 0xF7;//让K4能具备有被拉低的条件先

while(1)
{
SEG_task();//数码管显示任务

//按键功能部分
if(key_up==0)
{
if(KEY4==1)//按键已弹起
{
cnt++; //执行功能代码
}
}
key_up=KEY4; //如果不松手,key_up就会等于0
}
}

我们多按几次,会发现有时抬起之后cnt就被加2了或者更多,也就是在一次按键的动作里cnt++;被执行了两次,这是因为SEG_task();的执行时间太短没有滤掉按键的抖动,大家再次对照下图自己分析

有了支持连按的代码思路,实现消抖还是很容易的,我们同样用上times记录按键IO端口进入低电平的时间,只要times大于500证明抖动的时间已经过去,此时再判断按键是否抬起就可以决定该不该执行功能代码了。很有意思

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <reg52.h> 
sbit ADDR2 = P1^2;
sbit ADDR1 = P1^1;
sbit ADDR0 = P1^0;
sbit ENLED = P1^4;
sbit ADDR3 = P1^3;

sbit LED2 = P0^0;
sbit KEY4 = P2^7;
unsigned char code LedChar[16]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E};//数码管状态值初始化
unsigned char LedBuff[6]={0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 数码管缓冲
unsigned char cnt=0;//可以在SEG_task()和main()中使用的全局变量

void SEG_task()//数码管显示函数
{
static unsigned char i=0;
LedBuff[0]= LedChar[cnt%10];
if(cnt>=10) LedBuff[1]= LedChar[(cnt/10)%10]; //cnt没到达10之前不更新LedBuff[1]的初始值
if(cnt>=100)LedBuff[2]= LedChar[(cnt/100)%10];//cnt没到达100之前不更新LedBuff[2]的初始值
if(cnt==0){ LedBuff[1]=0xFF;LedBuff[2]=0XFF; }//cnt到达255之后再加1就溢出变为0了,这时候要再次熄灭这两个数码管

P0=0xFF;
switch(i)
{
case 0:
ADDR2 = 0;ADDR1 = 0;ADDR0 = 0;P0=LedBuff[0];i++;break;
case 1:
ADDR2 = 0;ADDR1 = 0;ADDR0 = 1;P0=LedBuff[1];i++;break;
case 2:
ADDR2 = 0;ADDR1 = 1;ADDR0 = 0;P0=LedBuff[2];i=0;break;
}
}

void main()
{
unsigned char key_up=1;//定义记录按键状态值的变量,初始值为1避免程序一开始就进入了“if(key_up==0)”
unsigned int times=0; //用来记录进入过按键判断语句的次数
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 1;//**************************
ADDR1 = 1;//让三八译码器的IO6输出低电平
ADDR0 = 0;//**************************
P2 = 0xF7;//让K4能具备有被拉低的条件先

while(1)
{
SEG_task();//数码管显示任务
unsigned int times=0; //用来记录进入过按键判断语句的次数

//按键功能部分
if(key_up==0)
{
times++;
if(times>=500&&KEY4==1)//低电平持续够一定的时间了,证明抖动时间已经过去了,如果现在按键已经抬起就执行功能代码
{
times=0;
cnt++;//执行功能代码
}
}
key_up=KEY4; //如果不松手,key_up就会等于0
}
}

2.4.9 双模式函数封装


  • 题目

    我们用最左端的数码管来提示此时的K4是支持连按还是不支持连按,这个数码管显示0的时候不支持连按,显示1的时候支持连按。

    用K3来切换按键模式,K3的按键模式是不支持连按的,按下松开就是把K4切换为另一种按键模式。

    然后我们通过按K4,同样右边的3个数码管显示cnt的值,支持连按时,按下不放就一直自动累加,不支持连按时,按下松开才累加1。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <reg52.h>
sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit LED2 = P0^0;
sbit KEY4 = P2^7;
sbit KEY3 = P2^6;

unsigned char code LedChar[16] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E}; //数码管状态值初始化
unsigned char LedBuff[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; // 数码管显示缓存区,全局变量
unsigned char cnt; //在KEY_task()和SEG_task()里用

void SEG_task() { // 数码管显示函数
static unsigned char i = 0;

// 调整数码管的数字
LedBuff[0] = LedChar[cnt%10]; // 个位
if(cnt>=10) LedBuff[1] = LedChar[(cnt/10)%10]; //cnt没到达10之前不更新LedBuff[1]的初始值 十位
if(cnt>=100) LedBuff[2] = LedChar[(cnt/100)%10]; //cnt没到达100之前不更新LedBuff[2]的初始值 百位
if(cnt==0){ LedBuff[1] = 0xFF; LedBuff[2] = 0xFF;} //cnt达到255之后在加1就溢出变为0了,这时候再次熄灭这两个数码管

P0 = 0xFF; //端口状态全部熄灭数码管里的LED达到刷新作用
switch(i) {
case 0: ADDR2 = 0; ADDR1 = 0; ADDR0 = 0; P0 = LedBuff[0]; i++; break; //线选数码管0,并显示数字
case 1: ADDR2 = 0; ADDR1 = 0; ADDR0 = 1; P0 = LedBuff[1]; i++; break; //线选数码管1,并显示数字
case 2: ADDR2 = 0; ADDR1 = 1; ADDR0 = 0; P0 = LedBuff[2]; i++; break; //线选数码管2,并显示数字
case 3: ADDR2 = 1; ADDR1 = 0; ADDR0 = 1; P0 = LedBuff[5]; i=0; break; //线选数码管5,并显示按键模式对应的数字
}
}

void KEY_task() { //按键按下所需要执行的任务
cnt++;
}

void KEY_mode(unsigned char mode) {
static unsigned char key_up = 1;
static unsigned int times = 0; //用来记录进入过按键判断语句的次数,可以拿来防抖

if(key_up == 0) {
times++;
if(mode == 1 && times >= 1000) //mode等于1,该部分代码是用来实现支持连按的,1000是为了让连按速度没那么快,如果改为500, 那么连按速度将加快
{
times = 0;
KEY_task();
}

else if(mode == 0 && times >=500) // mode等于0,该部分代码是用来实现不支持连按的,这里的times起到消抖的作用
{
if(KEY4 == 1) { // 按键已抬起
times = 0;
KEY_task();
}
}
}
key_up = KEY4; //如果不松手,key_up就会等于0
}

void main() {

unsigned char mode = 0; //初始时是不支持连按
unsigned char key_up = 1;
unsigned int times = 0; //用来记录进入过按键判断语句的次数

ENLED = 0;
ADDR3 = 1; //使能三八译码器

P2 = 0xF7; //使K3, K4具有能被拉低的条件
LedBuff[5] = LedChar[mode]; //填充好数码管5要显示的按键模式参数
while(1) {
SEG_task(); // 数码管显示函数
KEY_mode(mode); // K4的执行函数,计数器cnt的加法

// 以下是K3按键的功能代码
if(key_up == 0) {
times++;
if(KEY3 == 1 && times >= 500) {
times = 0;
mode = !mode; //非0及1
LedBuff[5] = LedChar[mode]; // 用来显示此时的K4是否支持连按,显示0表示不支持,显示1表示支持
}
}
key_up = KEY3;
}
}

2.4.10 综合案例

综合案例结合以上所学的知识,针对数码管\LED蜂鸣器按键的知识,模拟得分的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
题目:
假设两个人完掷硬币游戏,游戏规则如下
A和B各持一枚硬币
两人都出正面时:A得3块钱
两人都出反面时:A得1块钱
两人出一正一反时:B得2块钱
思路:
这样做模拟,程序的开始定义两个全局变量A和B初始化为30,意为每人各持30块钱用来显示在左右端的两个数码管上(3/3),A的钱显示在左边的数码管,B的钱显示在右边的数码管。我们用K13作为给A加3块钱的同时也给B减3块的功能。用K14作为给A加1块钱的同时也给B减1块的功能。用K15作为给A减2块钱的同时给B加2块的功能。三个按键都是不支持连按!(你也可以自由发挥搞一个封装玩玩)
规定谁先赢得45元时就算胜利,游戏结束。比如A的钱到达或超过45块钱时,B的钱就不显示了,这两个数码管熄灭,如果是B的钱到达或超过45块时,A的钱就不显示了。游戏结束蜂鸣器鸣响。蜂鸣器就间隔鸣叫,鸣叫的时候所有数码管熄灭,不鸣叫的时候就只有4个数码管亮着,数码管呈现间隔显示,蜂鸣器间隔鸣叫。要想重新开始游戏必须复位开发板重启!
开发板细节:
1. 初始化LedBuff[]数组的时候第2和第3号元素为0xBF,让数码管2和数码管3显示中间那一杠。
2. 死循环执行完一次循环的时间比以往的例程要多,所以times的判断我们只需要它超过300即可。
3. 因为用上了3个按键,每个按键功能被封装为一个函数了,所以我们定义了一个宏“#define TIMES 300”,3个按键的函数判断times时,只需书写“if(times>=TIMES&&KEYxx==1)”即可,以后我们写的代码死循环里比这次的例程执行一遍循环的时间还长的话,把宏改为比300还小的数就可以了。
4. 模拟掷骰子,使用python吧,比较简单
5. 不要放弃。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 实际 金沙滩开发板
#include <reg52.h>
#define TIMES 300 // 该程序主函数循环一次所要花费的时间比以往的长,所以times设置为300

sbit ADDR0 = P1^0;
sbit ADDR1 = P1^1;
sbit ADDR2 = P1^2;
sbit ADDR3 = P1^3;
sbit ENLED = P1^4;

sbit BEEP = P1^6;

sbit KEY13 = P2^4;
sbit KEY14 = P2^5;
sbit KEY15 = P2^6;

// 数码管状态初始化
unsigned char code LedChar[16] = {0xC0, 0xF9, 0xA4, 0xB0,0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E};
unsigned char LedBuff[6] = {0xFF, 0xFF, 0xBF, 0xBF, 0xFF, 0xFF}; //数码管缓冲区
unsigned char A = 30; //初始化A和B的钱
unsigned char C = 30;

void UPDATE_LED() { //更新数码管显示缓存区
LedBuff[5] = LedChar[A/10];
LedBuff[4] = LedChar[A%10];
LedBuff[1] = LedChar[C/10];
LedBuff[0] = LedChar[C%10];
}

void SEG_task() { // 数码管显示函数
static unsigned char i = 0;
P0 = 0xFF;
switch(i) {
case 0: ADDR2 = 0; ADDR1 = 0; ADDR0 = 0; P0 = LedBuff[0]; i++; break;
case 1: ADDR2 = 0; ADDR1 = 0; ADDR0 = 1; P0 = LedBuff[1]; i++; break;
case 2: ADDR2 = 0; ADDR1 = 1; ADDR0 = 0; P0 = LedBuff[2]; i++; break;
case 3: ADDR2 = 0; ADDR1 = 1; ADDR0 = 1; P0 = LedBuff[3]; i++; break;
case 4: ADDR2 = 1; ADDR1 = 0; ADDR0 = 0; P0 = LedBuff[4]; i++; break;
case 5: ADDR2 = 1; ADDR1 = 0; ADDR0 = 1; P0 = LedBuff[5]; i=0; break;
}
}

void KEY13_task() { // K13按键作用
static unsigned char key_up = 1;
static unsigned int times = 0; //用来记录进入过按键判断语句的次数

if(key_up == 0) {
times++;
if(KEY13 == 1 && times >= TIMES) {
times = 0;
A += 3; // A的钱加3元
C -= 3; // B的钱减3元
UPDATE_LED(); //更新数码管显示缓存区
}
}
key_up = KEY13;
}

void KEY14_task() { //KEY13按键任务
static unsigned key_up = 1;
static unsigned int times = 0; //用来记录进入过按键判断语句的次数

if(key_up == 0) {
times++;
if(KEY14 == 1 && times >= TIMES) {
times = 0;
A += 1; //A的钱加1
C -= 1; //B的钱减1
UPDATE_LED(); // 更新数码管缓存区
}
}
key_up = KEY14;
}

void KEY15_task() { // 按键15的任务
static unsigned char key_up = 1;
static unsigned int times = 0; //用来记录进入过按键判断的次数

if(key_up == 0) {
times++;
if(KEY15 == 1 && times >= TIMES) {
times = 0;
A -= 2; //A的钱减2
C += 2; //B的钱加2
UPDATE_LED(); //更新数码管缓存区
}
}
key_up = KEY15;
}

void BEEP_ON(unsigned char x) { //游戏结束蜂鸣器鸣叫
unsigned int i, time;
for(time = 0; time < 2000; time++) { //time<2000决定鸣叫的时间
if(x == 1)BEEP = !BEEP; //x=1表明游戏结束
else BEEP = 0;
for(i=0; i<30; i++);
}
}

void main() {
unsigned int i, x; //做循环和延时用

ENLED = 0;
ADDR3 = 1; //使能三八译码器

P2 = 0xFE; //使KEY13,KEY14, KEY15具备能被拉低的条件
UPDATE_LED(); //初始化数码管缓存区

while(1) {
SEG_task(); // 数码管显示函数
KEY13_task(); //KEY13按键的任务
KEY14_task(); // KEY14按键的任务
KEY15_task(); //KEY15按键的任务

//游戏结束要完成的任务
if(A>=45 || C >= 45) { // 只要其中一方的钱达到45块以上就结束游戏,程序进入死循环
if(A>=45) {
LedBuff[1] = 0xFF;
LedBuff[0] = 0xFF; // A赢得比赛,B的钱不再显示
}
else {
LedBuff[5] = 0xFF;
LedBuff[4] = 0xFF; //B赢得比赛,A的钱不再显示
}
while(1) {
P0 = 0xFF; //熄灭所有数码管
BEEP_ON(1); // 蜂鸣器鸣叫一段时间
for(i=0; i<300; i++) { //让数码管显示一段时间
SEG_task(); //数码管显示函数
for(x = 0; x < 200; x++); //加此延时是为了让数码管显示亮亿点点,不然只循环数码管显示函数,显示就会暗一些
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
# 模拟掷硬币
import random

def coin_toss_simulation(num_trials):
for i in range(num_trials):
player_1_coin = random.randint(0,1)
player_2_coin = random.randint(0,1)
print('[%d %d] '%(player_1_coin, player_2_coin), end='')

# 测试代码,模拟10次硬币正反面
coin_toss_simulation(50)

2.5 多模块编程

多模块编程非常方便,博主看很多教程都写#include <function.h>,但是我这边建议,我们自己写的头文件最好是用""表示,由下面的代码我们可以简单理解为由function.h建立了main.c文件与function.c的联系。针对里面一些新的定义和简洁函数详情见第第三章第3.5节

文件只需要将function.cmain.c加入即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//实际 金沙滩开发板 main.c
#include <reg52.h>
#include "function.h"

void main()
{
u16 i,x,NUM=12345;
LED_Init();//初始化LED硬件模块

LED2=0;LED9=0;
delay_ms(100);
LED3=0;LED8=0;
delay_ms(100);
LED4=0;LED7=0;
delay_ms(100);
LED5=0;LED6=0;
delay_ms(100);

for(i=0;i<5000;i++)//蜂鸣器响一下
{
BEEP=!BEEP;
for(x=0;x<30;x++);
}

ShowNumber(NUM);//更新缓存区的内容,首次显示12345在数码管上
while(1)
{
SEG_Scan();
i++;
if(i>=8000)//隔一段时间更新数码管显示的内容
{
i=0;
ShowNumber(NUM++);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
//实际 金沙滩开发板 function.c
#include <reg52.h>
#include "function.h"

u8 code LedChar[16]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E};//数码管状态值初始化
u8 LedBuff[6]={0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};//初始化数码管显示缓存区

void delay_ms(u16 x)
{
u16 i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void LED_Init()
{
P1|=0x0E;//让P1.1,P1.2,P1.3强制输出1
P1&=0xEE;//让P1.0和P1.4强制输出0
}

void SEG_Scan()
{
static u8 i = 0;
P0 = 0xFF; //端口状态全部熄灭数码管里的LED达到刷新作用
P1 = (P1 & 0xF8) | i; //i等于0时,就是“ADDR2=0; ADDR1=0; ADDR0=0;”,i等于1时,就是“ADDR2=0; ADDR1=0; ADDR0=1;”,以此类推
P0 = LedBuff[i]; //6个缓冲区的值轮流赋给P0
i++;
if(i>=6)i=0; //让i在0~5之间循环变化
}

void ShowNumber(u32 num)
{
char i;//取值范围-128~127
u8 buf[6];
for (i=0; i<6; i++) //把长整型数转换为6位十进制的数组
{
buf[i] = num % 10;
num = num / 10; //舍掉个位数,重新装载
}
for (i=5; i>=1; i--) //从最高位起,遇到0填充不显示的代码,遇到非0则退出循环
{
if (buf[i] == 0)
LedBuff[i] = 0xFF;
else
break;
}
for ( ; i>=0; i--) //剩余低位都如实转换为数码管显示字符
{
LedBuff[i] = LedChar[buf[i]];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//实际 金沙滩开发板 function.h
#ifndef _function_h_
#define _function_h_

sbit BEEP = P1^6;

sbit LED2 = P0^0;
sbit LED3 = P0^1;
sbit LED4 = P0^2;
sbit LED5 = P0^3;
sbit LED6 = P0^4;
sbit LED7 = P0^5;
sbit LED8 = P0^6;
sbit LED9 = P0^7;

typedef unsigned char u8; //对数据类型进行声明定义
typedef unsigned int u16;
typedef unsigned long u32;

extern u8 LedBuff[6]; //对数码管缓存区进行外部声明
extern u8 code LedChar[16]; //对数码管真值表进行外部声明

//只要在“function.c”文件中封装有的函数都需要在头文件中声明一下
void delay_ms(u16 x);
void LED_Init();
void SEG_Scan();
void ShowNumber(u32 num);

#endif

2.5.1 带返回值的函数(不支持连按的按键)

之前封装的函数都是void类型无返回值的函数。随着学习的深入,我们需要把C语言的精髓学到家。

前面讲的function.c中没有提及过按键的函数封装。按键功能函数都需要定义全局变量,这种过多的使用全局变量是编程的大忌。随着我们使用按键越来越灵活,就不是简单的让一两个变量加加减减而已了,所以现在我们要更加的去贴合嵌入式编程的方式,那么按键的使用如果用函数封装的话,需要用到函数返回值的相关知识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <reg52.h>
#include "function.h"
# define TIMES 1000 //死循环里的代码量少,所以把阈值调大些

sbit KEY4 = P2^7;

u8 KEY_Scan() {
static u8 key_up = 1;
static u16 times;
if(key_up == 0) {
times++;
if(KEY4 == 1 && times >= TIMES) {
times = 0;
return 1;
}
}
key_up = KEY4;
return 0
}

void main() {
u8 key; //用来读取按键动作的返回值
LED_Init(); //初始化LED硬件模块
P2 = 0xF7; //让K4具备能被拉低的条件
while(1) {
key=KEY4_Scan();
if(key==1)LED2=!LED2;
}
}

2.5.2 支持连按的按键

实验现象就是按着按键不放,那么“key=1;”出现的频率就会比不支持连按代码的时候多,所以就会有左右不断流水的现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <reg52.h> 
#include <function.h>
#define TIMES 2000 //让流速慢一点
sbit KEY4 = P2^7;

u8 KEY4_Scan()
{
static u8 key_up=1;
static u16 times;
if(key_up==0)
{
times++;
if(times>=TIMES)
{
times=0;
return 1;
}
}
key_up=KEY4;
return 0;
}

void main()
{
u8 key; //用来读取按键动作的返回值
u8 i=0,dir; //dir是作为切换流水方向
LED_Init(); //初始化LED硬件模块
P2=0xF7; //让K4能具备有被拉低的条件先
P0=0xFE; //先点亮LED2
while(1)
{
key=KEY4_Scan();
if(key==1)//执行功能代码
{
i++;
if(i>=15)i=1;//让i一直在1~14之间变化

if(i<8)dir=0;//向左移
if(dir==0)P0=~(0x01<<i);

if(i>=8)dir=1;//向右移
if(dir==1)P0=~( 0x80>>(i-7) );//当i大于等于8之后,(i-7)其实也还是在1~7之间变化
}
}
}

考虑到我们最常用的按键是K4、K8、K12、K16

所以只有P2^7输出低电平之后,对应的4个按键的IO端口才有被拉低的条件

1
2
3
4
void KEY_Init()
{
P2=0X7F;//让P2.7输出低电平,其他IO端口输出高电平,这样就可以使能4个按键了
}

2.5.3 最终按键程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
u8 KEY_Scan(u8 mode,u16 TIMES)
{
static u8 key_up=1; //按键松开标志
static u16 times;
if(mode)key_up=1; //如果mode等于1,支持连按
if(key_up&&(KEY4==0||KEY8==0||KEY12==0||KEY16==0))//只要在key_up等于1时,其中一个按键被按下就可以进入执行代码
{
times++; //记录进入低电平的时间
if(times>=TIMES)//抖动的时间已经过去
{
times=0;
key_up=0;
if(KEY4==0)return 4;
else if(KEY8==0)return 8;
else if(KEY12==0)return 12;
else if(KEY16==0)return 16;
}
}
else if(KEY4==1&&KEY8==1&&KEY12==1&&KEY16==1)key_up=1;
return 0;// 无按键按下
}
  1. 假设我们传入的参数mode为0,进入函数,第一次初始化时key_up为1,然后没有去执行“if(mode)key_up=1;”,此时若没有按键按下,则满足“else if(KEY4==1&&KEY8==1&&KEY12==1&&KEY16==1)key_up=1;”,所以key_up还是等于1,返回值为0。
  1. 假设有按键按下,持续够一定的低电平时间了(抖动时间过去了),清零times,让key_up等于0,然后判断此时是哪个按键按下就返回对应的值。
  1. 返回对应的值之后,如果我们一直按着不放,第二次执行这个函数就会因为key_up在前一次函数执行中已经等于0,所以我们就算按着按键不放也进入不了“if(key_up&&(KEY4==0||KEY8==0||KEY12==0||KEY16==0))”,那么一次按键动作只能有一次返回值为4、8、12或16的机会,其他时候都是返回0。如果我们按键松手了,那就满足“else if(KEY4==1&&KEY8==1&&KEY12==1&&KEY16==1)key_up=1;”,这样key_up恢复为1了,下次按键动作又能够进入“if(key_up&&(KEY4==0||KEY8==0||KEY12==0||KEY16==0))”从而可以返回对应的按键值。不支持连按模式就讲解完了。

  2. 参数mode为1时,总会执行“if(mode)key_up=1;”,所以按键按着不放函数的执行都会进入“if(key_up&&(KEY4==0||KEY8==0||KEY12==0||KEY16==0))”,这样返回的按键值的机会比不支持连按时候还要多,这就是mode等于1时呈现的支持连按功能。

  3. 我们不再使用“#define TIMES 1000”,因为有时“KEY_Scan()”在各种不同的循环体里扫描返回值,有些循环一次执行时间很快,有些却很慢,我们在第五章已经分析过这些情况了,所以TIMES的值需要随机应变。我们决定让TIMES作为按键程序的第二个参数,这样在某些循环体里如果循环一次的时间很快,我们调为“KEY_Scan(0,1000);”,循环一次的时间很慢就改为“KEY_Scan(0,300);”

原理解析就讲解完了,可以看到,该代码在不支持连按模式下是按下之后就执行返回值了的,而不是像以前一样要抬起按键之后才会执行返回值的语句,所以不管我们的按键手速是快是慢,程序都会在最快时间内去执行返回值的语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 实际 金沙滩开发板 把“KEY_Scan(0,1000);”改为“KEY_Scan(1,1000);”就是支持连按了。
#include <reg52.h>
#include <function.h>
sbit KEY4 = P2^3;
sbit KEY8 = P2^2;
sbit KEY12 = P2^1;
sbit KEY16 = P2^0;

u8 KEY_Scan(u8 mode,u16 TIMES)
{
static u8 key_up=1; //按键松开标志
static u16 times;
if(mode)key_up=1; //如果mode等于1,支持连按
if(key_up&&(KEY4==0||KEY8==0||KEY12==0||KEY16==0))//只要在key_up等于1时,其中一个按键被按下就可以进入执行代码
{
times++; //记录进入低电平的时间
if(times>=TIMES)//抖动的时间已经过去
{
times=0;
key_up=0;
if(KEY4==0)return 4;
else if(KEY8==0)return 8;
else if(KEY12==0)return 12;
else if(KEY16==0)return 16;
}
}
else if(KEY4==1&&KEY8==1&&KEY12==1&&KEY16==1)key_up=1;
return 0;// 无按键按下
}

void KEY_Init()
{
P2=0X7F;//让P2.7输出低电平,其他输出高电平,这样就可以使能4个按键了
}

void main()
{
u8 key; //用来读取按键动作的返回值
LED_Init();//初始化LED硬件模块
KEY_Init();//初始化按键模块
P0=0xFE; //先点亮LED2
while(1)
{
key=KEY_Scan(0,1000); //不支持连按模式,判断阈值为1000
if(key==4)LED2=!LED2; //执行功能代码
if(key==8)LED4=!LED4; //执行功能代码
if(key==12)LED6=!LED6;//执行功能代码
if(key==16)LED8=!LED8;//执行功能代码
}
}

2.5.4 最终的Function文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// 实际 金沙滩开发板 functin.c
#include <reg52.h>
#include <function.h>

u8 code LedChar[16]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E};//数码管状态值初始化
u8 LedBuff[6]={0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};//初始化数码管显示缓存区

void delay_ms(u16 x)
{
u16 i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void LED_Init()
{
P1|=0x0E;//让P1.1,P1.2,P1.3强制输出1
P1&=0xEE;//让P1.0和P1.4强制输出0
}

void KEY_Init()
{
P2=0X7F;//让P2.7输出低电平,其他输出高电平,这样就可以使能4个按键了
}

u8 KEY_Scan(u8 mode,u16 TIMES)
{
static u8 key_up=1; //按键松开标志
static u16 times;
if(mode)key_up=1; //如果mode等于1,支持连按
if(key_up&&(KEY4==0||KEY8==0||KEY12==0||KEY16==0))//只要在key_up等于1时,其中一个按键被按下就可以进入执行代码
{
times++; //记录进入低电平的时间
if(times>=TIMES)//抖动的时间已经过去
{
times=0;
key_up=0;
if(KEY4==0)return 4;
else if(KEY8==0)return 8;
else if(KEY12==0)return 12;
else if(KEY16==0)return 16;
}
}
else if(KEY4==1&&KEY8==1&&KEY12==1&&KEY16==1)key_up=1;
return 0;// 无按键按下
}

void SEG_Scan()
{
static u8 i = 0;
P0 = 0xFF; //端口状态全部熄灭数码管里的LED达到刷新作用
P1 = (P1 & 0xF8) | i; //i等于0时,就是“ADDR2=0; ADDR1=0; ADDR0=0;”,i等于1时,就是“ADDR2=0; ADDR1=0; ADDR0=1;”,以此类推
P0 = LedBuff[i]; //6个缓冲区的值轮流赋给P0
i++;
if(i>=6)i=0; //让i在0~5之间循环变化
}

void ShowNumber(u32 num)
{
char i;//取值范围-128~127
u8 buf[6];
for (i=0; i<6; i++) //把长整型数转换为6位十进制的数组
{
buf[i] = num % 10;
num = num / 10; //舍掉个位数,重新装载
}
for (i=5; i>=1; i--) //从最高位起,遇到0填充不显示的代码,遇到非0则退出循环
{
if (buf[i] == 0)
LedBuff[i] = 0xFF;
else
break;
}
for ( ; i>=0; i--) //剩余低位都如实转换为数码管显示字符
{
LedBuff[i] = LedChar[buf[i]];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//实际 金沙滩开发板 function.h
#ifndef __FUNCTION_H__
#define __FUNCTION_H__

sbit BEEP = P1^6;
sbit KEY4 = P2^3;
sbit KEY8 = P2^2;
sbit KEY12 = P2^1;
sbit KEY16 = P2^0;
sbit LED2 = P0^0;
sbit LED3 = P0^1;
sbit LED4 = P0^2;
sbit LED5 = P0^3;
sbit LED6 = P0^4;
sbit LED7 = P0^5;
sbit LED8 = P0^6;
sbit LED9 = P0^7;
typedef unsigned char u8; //对数据类型进行声明定义
typedef unsigned int u16;
typedef unsigned long u32;
extern u8 LedBuff[6]; //对数码管缓存区进行外部声明
extern u8 code LedChar[16];//对数码管真值表进行外部声明

//只要在“function.c”文件中封装有的函数都需要在头文件中声明一下
void delay_ms(u16 x);
void LED_Init();
void KEY_Init();
u8 KEY_Scan(u8 mode,u16 TIMES);
void SEG_Scan();
void ShowNumber(u32 num);

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//实际 金沙滩开发板 main.c
#include <reg52.h>
#include <function.h>

void main()
{
u8 key;//用来读取按键动作的返回值
u8 beep_ok=0;//定义标志蜂鸣器允许鸣叫的变量,为1时可以鸣叫,为0时不能鸣叫
u8 x;
u32 NUM=10086;
LED_Init();//初始化LED硬件模块
KEY_Init();//初始化按键模块
ShowNumber(NUM);//更新数码管缓存区内容
while(1)
{
SEG_Scan();
key=KEY_Scan(0,700); //不支持连按模式,判断阈值为700
if(key==4){NUM++;ShowNumber(NUM);} //K4使NUM自加
if(key==12){NUM--;ShowNumber(NUM);} //K12使NUM自减
if(key==8)beep_ok=1; //K8开启蜂鸣器鸣叫
if(key==16)beep_ok=0; //K16关闭蜂鸣器鸣叫

if(beep_ok==1)
{
x++;
}
else x=0;

if(x>=3)
{
x=0;
BEEP=!BEEP;
}
}
}

success 这样我们的function文件就囊括了之前所学的单片机外设的代码了,大家要好好消化这一文件

2.6 外部中断

2.6.1 IE0的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <reg52.h>
#include "function.h"

//请提前将P3.2和GND相连
void main() {
LED_Init(); //初始化LED硬件模块
while(1) {
if(IE0 == 0) {
LED2 = !LED2;
delay_ms(50);
LED2 = 1; //如果IE0不等于0了,要保证熄灭LED2;
}
if(IE0 == 1) {
LED9 = !LED9;
delay_ms(50);
LED9 = 1; //如果LED0不等于1了,要保证熄灭LED9
}
delay_ms(50); //总要执行`if(IE0 == 0)`或者`if(IE0 == 1)`,这个延时要保证其中一盏灯灭的时间,保证总有灯闪烁的现象
}
}

2.6.2 中断函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <reg52.h>
#include "function.h"

void main() {
IT0 = 1; //下降沿触发
EX0 = 1;
EA = 1;

LED_Init(); //初始化LED硬件模块
while(1); // 主函数什么事都不用做,空循环这条语句
}

void EXTI0_IRQHandler() interrupt 0 {
LED2 = !LED2;
}

看起来主函数什么内容也没有,但是在拔插杜邦线的过程中触发了中断函数,所以LED2被执行跳变,这就是中断函数的作用。即使主函数一直执行while(1);,中断函数还是能执行的了的。把EX0=1;改为EX0=0;,或者把EA=1;改为EA=0;再编译下载进去拔插杜邦线发现LED2没有反应,因为这样做已经不满足触发中断函数响应了。

2.6.3 外部中断1

想要使用外部中断1,代码中需要把EX0改为EX1IT0改为IT1。interrupt后面的0要改为2,函数名我们改为EXTI1_IRQHandler()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <reg52.h>
#include "function.h"

void main() {
IT1 = 1; //下降沿触发模式
EA = 1;
EX1 = 1;

LED_Init(); //初始化LED硬件电路
while(1);
}



void EXIT1_IRQHandler() interrupt 2 {
LED2 = !LED2;
}

2.6.4 按键控制中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <reg52.h>
#include "function.h"

void main() {
IT1 = 1; //设置下降沿触发模式
EA = 1;
EX1 = 1;

LED_Init(); //初始化LED电路
KEY_Init(); //初始化按键电路
while(1);
}

void EXTI1_IRQHandler() interrupt 2 {
LED2 = !LED2;
}

我们需要对按键进行消抖,这样就可以达到按下松手一次,LED2的状态只能取反一次的灵敏效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 通过在中断函数中添加延时函数消抖,一般不这样做,但编程本就是逆天而行
#include <reg52.h>
#include "function.h"

void main() {
IT1 = 1; //设置下降沿触发
EA = 1;
EX1 = 1;

LED_Init(); //初始化LED电路
KEY_Init(); //初始化键盘电路
while(1);
}

void EXIT1_IRQHandler() interrupt 2 {
delay_ms(50);

if(INT1 == 0) { // 消抖
LED2 = !LED2; // P3.3是否还处于低电平的稳定接触状态,INT1已在“#include <reg52.h>”中定义好了
}
}

这样就可以达到按下松手一次,LED2的状态只能取反一次的灵敏效果。如果我们一直按着不放,LED2也只是跳变一次而已,有点像不支持连按功能,这是因为IT1=1;的原因。要是改为IT1=0;,这样K4按键就有种支持连按的感觉从而LED2就会不停闪烁了。按键按下一直不放就会不停地进入中断函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 通过在中断函数中添加延时函数消抖,一般不这样做,但编程本就是逆天而为
#include <reg52.h>
#include "function.h"

void main() {
IT1 = 0; //低电平下触发中断
EA = 1;
EX1 = 1;

LED_Init(); //初始化LED电路
KEY_Init(); //初始KEY电路
while(1);
}

void EXIT_IRQHandler() interrupt 2 {
delay_ms(50);
if(INT1 == 0) {
LED2 = !LED2;
}
}

2.7 定时器

2.7.1 工作模式一之流水灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 定时器0
#include<reg52.h>
#include "function.h"

void main() {
u8 i = 0;
LED_Init(); //初始化LED硬件模块
TMOD = 0x01; //设置定时器0位工作模式1
TH0 = 0x4C; //色湖之定时时间位50ms
TL0 = 0x00;
TR0 = 1; //启动定时器0

while(1) {
if(TF0 == 1) { //判断定时器0是否溢出,每隔50ms就可以进入一次这个if语句内部
TF0 = 0; //软件清零,定时器0溢出后,清0溢出标志
TH0 = 0x4C; //重新赋值,保证下次溢出时间间隔还是50ms
TL0 = 0x00;
P0 = ~(0x01 << i); //每盏灯的点亮时间都保持者50ms
i++;
}
if(i >= 8) i = 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 定时器1
#include <reg52.h>
#include <function.h>//详见第六章第8讲

void main()
{
u8 i=0;
LED_Init(); //初始化LED硬件模块
TMOD = 0x10; //设置定时器1为工作模式1
TH1 = 0x4C; //设置定时时间为50ms
TL1 = 0x00;
TR1 = 1; //启动定时器1

while(1)
{
if (TF1 == 1) //判断定时器1是否溢出,每隔50ms就可以进入一次这个if语句
{
TF1 = 0; //定时器1溢出后,清0溢出标志
TH1 = 0x4C; //重新赋初值,保证下次溢出间隔时间还是50ms
TL1 = 0x00;
P0=~(0x01<<i);//每盏灯的点亮时间都保持着50ms
i++;
}
if(i>=8)i=0;
}
}

2.7.2 定时器中断

右移流水灯,定时时长为30ms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <reg52.h>
#include "function.h"

void main() {
LED_Init(); //初始化LED硬件模块
EA = 1; //打开总中断开关
TMOD = 0x01; //设置定时器0为工作模式1
TH0 = 0x94; // 设置定时时间为30ms
ET0 = 1; //打开定时器0中断的开关
TR0 = 1; //启动定时器0
while(1);
}

void TIM0_IRQHandler() interrupt 1 {
static u8 i;
TH0 = 0x94; //重新设定时间为30ms
TL0 = 0x00;

P0 = ~(0x80 >> i); //流水灯向左移动
i++;
if(i >= 8) i = 0;
}

三、单片机C语言

该章只记录博主不会的单片机相关C语言知识,不做C语言的详细解释,谁叫博主是个菜鸡呢。对刚入门单片机编程的初学者来说,以前我们C语言基础上机操作的时候都是只见过别人int,char的去定义变量,而在单片机入门编程中看到的都是unsigned int,unsigned char这样定义,原因是单片机在入门的时候用的变量的取值都是0和正整数(正整数不包括0),所以才会用unsigned去定义无符号整形变量

3.1 特殊功能寄存器和位定义

3.1.1 sfr P0=0x80

sfr 这个关键字,是 51 单片机特有的,它的作用是定义一个单片机特殊功能寄存器(special function register)。

51 单片机内部有很多个小模块,每个模块居住在拥有唯一房间号的房间内,同时每个模块都有 8 个控制开关。P0就是一个功能模块,就住在了 0x80 这个房间里,我们就是通过设置 P0 内部这个模块的 8 个开关,来让单片机的 P0 这 8 个 IO 口输出高电平或者低电平的。而 51 单片机内部有很多寄存器,如果我们想使用的话必须提前进行 sfr 声明。不过 Keil 软件已经把所有这些声明都预先写好并保存到一个专门的文件中去了,我们要用的话只要文件开头添加一行#include即可。

3.1.2 sbit LED = P0^0

这个 sbit,就是对刚才所说的 SFR 里边的 8 个开关其中的一个进行定义。经过上边第二条语句后,以后只要在程序里写 LED,就代表了 P0.0 口。注意这个 P 必须大写,也就是说我们给 P0.0 又取了一个更形象的名字叫做 LED。

3.1.3 单片机的特殊功能寄存器

请注意,每个型号的单片机都会配有生产厂商所编写的数据手册(Datasheet),所以我们来看一下 STC89C52 的数据手册,从 21 页到 24 页,全部是对特殊功能寄存器的介绍以及地址映射列表。

image-20230331165423783

我们来看一下这个表,其中 P4 口 STC89C52 对标准 51 的扩展,我们先忽略它,只看前边的 P0、P1、P2、P3 这 4 个,每个 P 口本身又有 8 个控制端口。这样就确定了我们的单片机一共有 32 个 IO 口

其中P0 口所在的地址是0x80,一共有从7 到0 这8 个IO 口控制位,后边有个Reset Value(复位值),这个很重要,是我们看寄存器必看的一个参数,8 个控制位复位值全部都是 1。这就是告诉我们,每当单片机上电复位的时候,所有的引脚的值默认是都是 1,即高电平,
我们在设计电路的时候也要充分的考虑这个问题。

3.2 Delay()函数

前期我们在设计时间间隔时经常使用$Delay()$函数,最近几位同学一直在研究这个函数,简单的两层for循环,真的能很准确的设置延迟吗?让我们看一下这个简单的延迟的函数。

1
2
3
4
5
6
7
8
9
void delay(unsigned int t) //简单的延时函数
{
unsigned int i, j;
for(i = 0; i < t; i++)
{
for(j = 0; j < 120; j++);
}
}
delay(5000); //绿灯持续5秒钟

现在思考为什么我们调用函数$delay(5000)$就能延迟5秒钟呢?

1
2
3
4
5
6
7
8
<-- 这里是根据我查阅了一些资料得到的一点理解,如有不对请指正 -->

根据代码其实很明显,调用delay函数延时的时长是通过循环执行空操作来实现的,也就是说,delay函数并没有真正的即使功能,而是通过程序执行的速度来控制延时的时长。因此,delay(5000)实际上是在循环执行一定次数的空操作后,才返回函数,达到延时5s的效果。

具体来求解这个5s其实是要考虑很多因素的,延时时长的计算与单片机所使用的时钟频率有关。以一个8MHz的晶振为例,每个时钟周期的时间为1/(8MHz)=0.125us。delay函数中的循环次数是通过将所需延时时间转换为循环次数来实现的,计算公式为:循环次数 = 延时时间 / 单次循环所需时间。对于delay(5000)来说,延时为5s,即5000ms=5000000us。而单次循环所需时间是根据时钟频率计算得到的,对于8MHz晶振,单次循环所需时间为1/(8MHz)·4 = 0.5us(其中4是空操作的指令执行数)。因此,延迟5s需要执行的循环次数为:5000000us/0.5us=10000000次,才能实现延迟5秒的效果。

<-- 博主的单片机是11.9....MHz的晶振,protues是12MHz的,让我计算一下,以上代码是否真的是5s -->
按照protues中的12MHz,我们计算一下5s需要循环多少次。根据上述原理,我们每个时钟周期的时间为1/(12MHZ),单次循环所需时间为1/(12MHz)·120 = 10us(这里120是空操作的指令执行数见代码for(j = 0; j < 120; j++))。因此,我们要延迟5s需要执行的循环次数为:5000000us/10us = 500000次,但是我们使用delay(5000)一共循环了5000·120=600000次,不管是500000次还是600000次我们发现这都和我们传入的参数5000不符合呀,麻了,这是怎么回事呢?这是因为以上计算公式都是基于单片机汇编语言的,我们使用c语言绝不能这样计算,很明显一条c语句应该是对应很多调汇编语句的,你可以Dedug试试,所以该方法是计算不出来的。

如何设计我们的循环达到想要的延迟效果?综合上述所讲,我们的误区就落在C语言语句的执行时间,那么我们来实际调试一下看看我们的代码到底运行多久。

3.2.1 空操作循环运行的时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

#include <reg52.h>

sbit RED1 = P2^0; //第一组红灯
sbit YELLOW1 = P2^1; //第一组黄灯
sbit GREEN1 = P2^2; //第一组绿灯
sbit RED2 = P2^3; //第二组红灯
sbit YELLOW2 = P2^4; //第二组黄灯
sbit GREEN2 = P2^5; //第二组绿灯

void delay(unsigned int t) //简单的延时函数
{
unsigned int i, j;
for(i = 0; i < t; i++)
{
for(j = 0; j < 120; j++);
}
}

void main()
{
unsigned int i;
while(1)
{
//第一组绿灯,第二组红灯
GREEN1 = 0;
YELLOW1 = 1;
RED1 = 1;
GREEN2 = 1;
YELLOW2 = 1;
RED2 = 0;
for(i=0; i<120; i++); //验证空操作循环所需时间
delay(5000); //绿灯持续5秒钟

//第一组黄灯,第二组红灯
YELLOW1 = 0;
GREEN1 = 1;
delay(1000); //黄灯持续1秒钟

//第一组红灯,第二组绿灯
RED1 = 0;
YELLOW1 = 1;
GREEN1 = 1;
RED2 = 1;
YELLOW2 = 1;
GREEN2 = 0;
delay(5000); //绿灯持续5秒钟

//第一组红灯,第二组黄灯
YELLOW2 = 0;
GREEN2 = 1;
delay(1000); //黄灯持续1秒钟
}
}

  1. 首先将我们的晶振频率设置为12MHz,因为新建工程时默认是24MHz

image-20230402090540447

  1. 开始调试(为了验证的简单,我们直接在main函数中添加空操作循环)

image-20230402090813183

  1. 打断点

我们将断点打到第30行和第31行,点击RST按钮,然后返回main函数,按下run键,通过右侧sec时间可以看到当前程序执行到第30行所用的时间。可以看到程序运行到第30行时花了0.00039400s

image-20230402092201907

我们继续执行第30条语句,查看时间为0.00039500,这条语句花了0.00039500-0.00039400=0.00000100s,也就是1微秒。

image-20230402092421453

下面我们执行空操作循环语句,我们继续将断点打在32行(为了计算时间,如果不打断点程序会将31行语句后面的程序全部执行完)。可以看到时间变成0.00135800s。也就是说我们的空操作语句花了0.00135800s-0.00039500=0.000963s,也就是0.963毫秒接近1ms。

image-20230402092931979

如此我们就知道为什么我们调用delay(5000)就能够延迟5s秒了,视5000为5000次1ms,就得到5s了。

3.2.2 存在的问题

如果你调试时候我们的时间是有差异的,那就说明我们使用空操作循环进行延时操作是不完全的。这是为什么呢?

1
2
3
我们设置的时间参数5000,利用空循环等待一段时间,而这个时间正是通过计算机(晶振)时钟周期数来实现的,它的具体时间取决于计算机的性能和系统负载情况。也就是我们在调试时,这样的结果还和我们的单片机(计算机)硬件有关,因此存在误差是很正常。

其实我们会发现我们调试出的for(i=0;i<120;i++)并不是1ms,而是0.963ms。我们用此循环5000次,这样的误差会随着循环次数的增加被不断地放大为1535ms,wok,差了1s,这太可怕了。下面是一个验证的GIF,可以看到确实6s(我后点按的倒计时)

所以我们单纯的使用空操作循环语句进行延迟实际上是不准确的,但是勉强能用。

3.2.3 设计精确的5s

要设计精确的5s我们可以借助于看门狗(定时器)或者再次调试,后面更新,博主困了,拜拜。

3.3 移位

在我们的 C 语言当中,有一个移位操作,其中<<代表的是左移,>>代表的是右移。比如a = 0x01<<1;就是 a 的结果等于 0x01 左移一位。大家注意,移位都是指二进制移位,那么移位完了,本来在第 0 位的 1 移动到了第一位上,移动完了低位是补 0 的。所以 a 的值最终是等于 0x02。 还要学习另外一个运算符~,这个符号是按位取反的意思,同理按位取反也是针对二进制而言。比如 a = ~(0x01); 0x01 的二进制是 0b00000001,按位取反后就是 0b11111110,那么a 的值就是 0xFE 了。这里要注意的是二进制移位

3.4 静态变量

static一般不用在主函数中,大多数在中断函数和封装好的子函数里运用,它的作用是保证在子函数或中断函数中定义的变量每次调用完之后都可以保持调用完时候的值,也就是保证变量在两个函数中的值使相同且通用的。下面是一个举例,可以下载到单片机中观察现象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <reg52.h> 
sbit ADDR2 = P1^2;
sbit ADDR1 = P1^1;
sbit ADDR0 = P1^0;
sbit ENLED = P1^4;
sbit ADDR3 = P1^3;

unsigned char code LedChar[16]={0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,0x88,0x83,0xC6,0xA1,0x86,0x8E};//数码管状态值初始化

void delay_ms(unsigned int x)
{
unsigned int i,j;
if(x==1000)
{
for(i=0;i<19601;i++)//延时1s
{
for(j=5;j>0;j--);
}
}
else while(x--)for(j=115;j>0;j--);
}

void SEG0_task()
{
static unsigned char i=0; //静态变量
P0=LedChar[i];
i++;
if(i>=16)i=0; //让i在0~15之间变化
}

void main()
{
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 0;//**************************
ADDR1 = 0;//只让数码管0显示
ADDR0 = 0;//**************************

while(1)
{
SEG0_task();
delay_ms(1000);//延时1s
}
}

3.5 多文件编程

运用多文件编程是为了使我们教程无需每次都贴出相同功能的代码,编程的时候已经有很多代码完全可以前往以前的例程里复制即可。

后期的例程里代码量越来越大,显然全部给出就不合适了,所以我们把经常使用的功能模块代码封装起来放在另一个“.c”文件里提供给我们调用,这个“.c”文件命名为“function.c”。以后我们提供的代码就是“main.c”的内容,这样教程供给的代码数量就会精简一些。

3.5.1 前期设置

新建function.cfunction.h,并添加到Source Group 1

function.h中添加以下代码

1
2
3
4
5
6
7
8
9
#ifndef __FUNCTION_H__
#define __FUNCTION_H__

typedef unsigned char u8;//对数据类型进行声明定义
typedef unsigned int u16;
typedef unsigned long u32;


#endif

3.5.2 硬件的初始化

回顾一下要点亮小灯需要的条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 首先
sbit LED2 = P0^0;
sbit ADDR2 = P1^2;
sbit ADDR1 = P1^1;
sbit ADDR0 = P1^0;
sbit ENLED = P1^4;
sbit ADDR3 = P1^3;
// 然后
ADDR3 = 1;//使能三八译码器
ENLED = 0;//

ADDR2 = 1;//**************************
ADDR1 = 1;//让三八译码器的IO6输出低电平
ADDR0 = 0;//**************************

上面这些条件满足了就叫LED的初始化,也就是说,要想使用硬件上的模块,我们必须配置好它的IO端口输出情况或者设置好函数的参数使其能工作。初始化不是放在while(1)循环中反复执行,而是在主函数开头把相应的IO端口该输出低电平的输出低电平,该输出高电平的输出高电平,配置好这些器件,它只需在主函数开头执行一次即可,后面就是进入死循环去真正的实现相应的功能了。

3.5.3 小灯的初始化

小灯要能正常工作,就要满足P1的5个IO端口的条件,可以将LED小灯的初始化就封装为

1
2
3
4
void LED_Init() {
P1 |= 0X0E; //让P1.1, P1.2, P1.3强制输出1
P1&=0xEE; //让P1.0和P1.4强制输出0
}

当数码管工作时,小灯是暂时发挥不了作用的,因为三八译码器的IO6端口的输出已经切换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void SEG_task() { //数码管显示函数
static unsigned char i = 0;
P0 = 0xFF; // 端口状态全部熄灭数码管理的LED达到刷新作用
switch(i)
{
case 0:
ADDR2 = 0;ADDR1 = 0;ADDR0 = 0;P0=LedBuff[0];i++;break;
case 1:
ADDR2 = 0;ADDR1 = 0;ADDR0 = 1;P0=LedBuff[1];i++;break;
case 2:
ADDR2 = 0;ADDR1 = 1;ADDR0 = 0;P0=LedBuff[2];i++;break;
case 3:
ADDR2 = 0;ADDR1 = 1;ADDR0 = 1;P0=LedBuff[3];i++;break;
case 4:
ADDR2 = 1;ADDR1 = 0;ADDR0 = 0;P0=LedBuff[4];i++;break;
case 5:
ADDR2 = 1;ADDR1 = 0;ADDR0 = 1;P0=LedBuff[5];i=0;break;
}
}
}

这段代码我们可以简化为

1
2
3
4
5
6
7
8
void SEG_task() {
static unsigned char i = 0;
P0 = 0xFF; // 端口状态全部熄灭数码管里的LED达到刷新作用
P1 = (p1 & 0xF8) | i; //i等于0时,就是ADDR2 = 0; ADDR1 = 0; ADDR0 = 0; i等于1时,就是ADDR2=0;ADDR1=0;ADDR2=1;一次类推
P0 = LedBuff[i]; //6个缓冲区的值轮流赋给P0
i++;
if(i>=6)i=0; //让i在0-5之间循环变化
}

3.5.4 function.h的内容

分析一下reg52.h文件里发现有P3的8个IO端口的定义

1
2
3
4
5
6
7
8
sbit RD   = P3^7;
sbit WR = P3^6;
sbit T1 = P3^5;
sbit T0 = P3^4;
sbit INT1 = P3^3;
sbit INT0 = P3^2;
sbit TXD = P3^1;
sbit RXD = P3^0;

那我们也可以懂得把之前的sbit LED2 = P0^0;等放在function.h中,以及在function.c封装有的函数和初始化的数组都需要在function.h中声明一下。比如在function.c中定义好了LED_Init(),在function.h就要void LED_Init();声明一下。

3.5.5 数码管显示函数

有时我们需要6个数码管显示我们想要看到的数字,比如计算结果呈现在数码管上,然而当要显示的数为“520”时又不想前面的3个数码管亮(也就是高位为0不显示),我们把这部分功能代码封装为带参数的函数,传入的参数就是要显示的数值。参数定义的是unsigned long类型,也就是支持0~4294967296的数值(2的32次方等于4294967296)。

示例一

1
2
3
4
5
6
7
8
9
10
11
12
13
void ShowNumber(u32 num) {
LedBuff[0]=LedChar[num%10];
LedBuff[1]=LedChar[(num/10)%10];
LedBuff[2]=LedChar[(num/100)%10];
LedBuff[3]=LedChar[(num/1000)%10];
LedBuff[4]=LedChar[(num/10000)%10];
LedBuff[5]=LedChar[(num/100000)%10];
if(num<100000)LedBuff[5] = 0xFF;
if(num<10000) LedBuff[4] = 0xFF;
if(num<1000) LedBuff[3] = 0xFF;
if(num<100) LedBuff[2] = 0xFF;
if(num<10) LedBuff[1] = 0xFF;
}

示例二

1
2
3
4
5
6
7
8
9
10
11
void ShowNumber(u32 num)
{
u32 i;
u8 x=5;//每次被调用都会从5开始
for(i=100000;i>0;i/=10)
{
if(num<i)LedBuff[x] = 0xFF;
else LedBuff[x]=LedChar[(num/i)%10];
x--;
}
}

这两段代码都能实现我们想要的功能,示例1可读性强,让人一看就明白了代码的作用。而示例2书写量少,比较简洁,但在阅读理解上需要我们转一下弯,经过测试这两段代码的执行时间,发现示例1比示例2快5ms左右,这种情况下毫无疑问我们肯定是选择示例1暂时作为实用代码。

可是在后期的教程中用到定时器中断来扫描数码管显示时,示例1的代码效果不尽人意,关于漏洞在哪大家后面可以尝试自己寻找。

在使用示例2时虽然显示效果比示例1的好,但是费时长,所以这两段代码都不是实用型代码。

代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void ShowNumber(u32 num)
{
char i;//取值范围-128~127
u8 buf[6];
for (i=0; i<6; i++) //把长整型数转换为6位十进制的数组
{
buf[i] = num % 10;
num = num / 10; //舍掉个位数,重新装载
}
for (i=5; i>=1; i--) //从最高位起,遇到0填充不显示的代码,遇到非0则退出循环
{
if (buf[i] == 0)
LedBuff[i] = 0xFF;
else
break;
}
for ( ; i>=0; i--) //剩余低位都如实转换为数码管显示字符
{
LedBuff[i] = LedChar[buf[i]];
}
}

这部分代码我们来举个例子理解,假如传入的参数为125,第一个for循环就是实现以下功能

buf[5]=0; buf[4]=0; buf[3]=0; buf[2]=1; buf[1]=2; buf[0]=5;

第二个for循环i等于2之后就退出了循环,第三个for循环i是等于2,然后填充好要显示的1,2,5给数码管缓存区。

3.5.6 带返回值的函数

之前封装的函数都是void类型无返回值的函数。随着学习的深入,我们需要把C语言的精髓学到家。

前面讲的function.c中没有提及过按键的函数封装。按键功能函数都需要定义全局变量,这种过多的使用全局变量是编程的大忌。随着我们使用按键越来越灵活,就不是简单的让一两个变量加加减减而已了,所以现在我们要更加的去贴合嵌入式编程的方式,那么按键的使用如果用函数封装的话,需要用到函数返回值的相关知识。

  • 举例:首先我们定义一个不带参数且返回值为unsigned char型的函数,函数名为KEY4_Scan(),里面的内容跟以前的不支持连按函数都差不多,只是我们把执行功能代码部分改为return 1。当按键按下抬起时我们函数的返回值为1,其他情况返回值为0,这样死循环里不断更新key的值,而大多数时候没有按键动作,所以死循环里的key的值都是0,有按键动作了,key等于1就执行功能代码,下一个循环没有按键动作了key又等于0就不执行功能代码了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <reg52.h>
#include "function.h"
# define TIMES 1000 //死循环里的代码量少,所以把阈值调大些

sbit KEY4 = P2^7;

u8 KEY_Scan() {
static u8 key_up = 1;
static u16 times;
if(key_up == 0) {
times++;
if(KEY4 == 1 && times >= TIMES) {
times = 0;
return 1;
}
}
key_up = KEY4;
return 0
}

void main() {
u8 key; //用来读取按键动作的返回值
LED_Init(); //初始化LED硬件模块
P2 = 0xF7; //让K4具备能被拉低的条件
while(1) {
key=KEY4_Scan();
if(key==1)LED2=!LED2;
}
}

3.6 typedef关键字

使用typedef我们可以对代码进行简化,比如unsigned int的定义,我们用typedef unsigned int u16;声明,u16就是用一个更加简洁的名字来取代unsigned int区定义16位的变量。

typedef unsigned char u8;的话,u8就是用来定义8位的变量,或者像之前定义的数组也可以像这样u8 LEDSET[8]={0xFE,0xFD,0xFB,0xF7,0xEF,0xDF,0xBF,0x7F};去定义。

有时需要定义32位的变量,就用“typedef unsigned long u32;”即可。虽然#definetypedef的作用有点意思相近,但“#define u16 unsigned int”是不严谨的编写习惯,不建议这样使用,原因大家可上网了解。

3.7 指针

指针是C语言的灵魂,利用指针可以直接而快速的处理内存中的各种数据结构中的数据,特别是数组、字符串和内存的动态分配等,它为函数之间各类数据传递提供了简洁便利的方法。

3.7.1 指针的概念与指针变量的声明

3.7.1.1 变量的地址

要研究指针,我们得先来深入理解内存地址这个概念。打个比方:整个内存就相当于一个拥有很多房间的大楼,每个房间都有房间号,比如从 101、102、103 一直到 NNN,我们可以说这些房间号就是房间的地址。相对应的内存中的每个单元也都有自己的编号,比如从0x00、0x01、0x02 一直到 0xNN,我们同样可以说这些编号就是内存单元的地址。房间里可以住人,对应的内存单元里就可以“住进”变量了:假如一位名字叫 A 的人住在 101 房间,我们可以说 A 的住址就是 101,或者 101 就是 A 的住址;对应的,假如一个名为 x 的变量住在编号为 0x00 的这个内存单元中,那么我们可以说变量 x 的内存地址就是 0x00,或者 0x00就是变量 x 的地址。

基本的内存单元是字节,英文单词为 Byte,我们所使用的 STC89C52 单片机共有 512字节的 RAM,就是我们所谓的内存,但它分为内部 256 字节和外部 256 字节,我们仅以内部的 256 字节为例,很明显其地址的编号从 0 开始就是0x00~0xFF。我们用 C 语言定义的各
种变量就存在 0x00~0xFF 的地址范围内,而不同类型的变量会占用不同数量的内存单元,即字节,可以结合前面讲过的 C 语言变量类型深入理解。假如现在定义了 unsigned char a = 1; unsigned char b = 2; unsigned int c = 3; unsigned long d = 4; 这样 4 个变量,我们把这 4 个变量分别放到内存中,就会是表 12-1 中所列的样子,我们先来大概了解一下他们的存储方式。

变量 a、b 和 c 和 d 之间的变量类型不同,因此在内存中所占的存储单元也不一样,a 和b 都占一个字节,c 占了 2 个字节,而 d 占了 4 个字节。那么,a 的地址就是 0x00,b 的地址就是 0x01,c 的地址就是 0x02,d 的地址就是 0x04,它们的地址的表达方式可以写成:&a,&b,&c,&d。这样就代表了相应变量的地址,C 语言中变量前加一个&表示取这个变量的地址,&在这里就叫做取址符

讲到这里,有一点延伸内容,大家可以了解下:比如变量 c 是 unsigned int 类型的,占了2 个字节,存储在了0x02 和0x03 这两个内存地址上,那么0x02 是它的低字节还是高字节呢?这个问题由所用的 C 编译器与单片机架构共同决定,单片机类型不同就有可能不同,大家知道这么回事即可。比如:在我们使用的 Keil+51 单片机的环境下,0x02 存的是高字节,0x03存的是低字节。这是编译底层实现上的细节问题,并不影响上层的应用,如下这两种情况在应用上丝毫不受这个细节的影响:强制类型转换——b = (unsigned char) c,那么 b 的值一定是 c 的低字节;取地址——&c,则得到的一定是 0x02,这都是 C 语言本身所决定的规则,不因单片机编译器的不同而有所改变。

实际生活中,我们要寻找一个人有两种方式,一种方式是通过它的名字来找人,还有第二种方式就是通过它的住宅地址来找人。我们在派出所的户籍管理系统的信息输入方框内,输入小明的家庭住址,系统会自动指向小明的相关信息,输入小刚的家庭住址,系统会自动指向小刚的相关信息。这个供我们输入地址的方框,在户籍管理系统叫做地址输入框

那么,在 C 语言中,我们要访问一个变量,同样有两种方式:

  1. 一种是通过变量名来访问。
  2. 另一种自然是通过变量的地址来访问。

在 C 语言中,地址就等同于指针变量的地址就是变量的指针。我们要把地址送到上边那个所谓的地址输入框内,这个地址输入框既可以输入 x 的指针,又可以输入 y 的指针,所以相当于一个特殊的变量——保存指针的变量,因此称之为指针变量,简称为指针,而通常我们说的指针就是指指针变量。 地址输入框输入谁的地址,指向的就是这个人的信息,而给指针变量输入哪个普通变量的地址,它自然就指向了这个变量的内容,通常的说法就是指针指向了该变量。

3.7.1.2 指针变量的声明

在 C 语言中,变量的地址往往都是编译系统自动分配的,对我们用户来说,我们是不知道某个变量的具体地址的。所以我们定义一个指针变量 p,把普通变量 a 的地址直接送给指针变量 p 就是p = &a;这样的写法

对于指针变量 p 的定义和初始化,一般有两种方式:

  1. 定义时直接进行初始化赋值
1
2
unsigned char a;
unsigned char *p = &a;
  1. 定义后再进行赋值
1
2
3
unsigned char a;
unsigned char *p;
p = &a;

大家仔细看会看出来这两种写法的区别,它们都是正确的。我们在定义的指针变量前边加了个*,这个*p 就代表了这个p是个指针变量,不是个普通的变量,它是专门用来存放变量地址的。此外,我们定义*p 的时候,用了unsigned char来定义,这里表示的是这个指针指向的变量类型是 unsigned char 型的

指针变量似乎比较好理解,大家也能很容易就听明白。但是为什么很多人弄不明白指针呢?因为在 C 语言中,有一些运算和定义,他们是有区别的,很多同学就是没弄明白它们的区别,指针就始终学不好。这里我要重点强调两个区别,只要把这两个区别弄明白了,起码指针变量这部分就不是问题了。这两个重点现在大家死记硬背,直接记住即可,靠理解有可能混淆概念。

  1. 指针变量 p 和普通变量 a 的区别:我们定义一个变量 a,同时也可以给变量 a 赋值 a = 1,也可以赋值 a = 2。但是,我们我们定义一个指针变量 p,另外还定义了一个普通变量 a=1,普通变量 b=2,那么这个指针变量可以指向 a 的地址,也可以指向 b 的地址,可以写成 p = &a,也可以写成 p = &b,但就是不能写成p = 1 或者 p = 2 或者 p = a,这三种表达方式都是错的。因此这个地方,不要看到定义*p 的时候前边有个 unsigned char 型,就错误的赋值 p=1,这个只是说明 p 指向的变量是这个 unsigned char 类型的,而 p 本身,是指针变量,不可以给它赋值普通的值或者变量,后边我们会直接把指针变量称之为指针,大家要注意一下这个小细节。
  2. 定义指针变量*p 和取值运算*p 的区别: *这个符号,在我们的 C 语言有三个用法,第一个用法很简单,乘法操作就是用这个符号;第二个用法,是定义指针变量的时候用的,比如 unsigned char *p,这个地方使用“*”代表的意思是 p 是一个指针变量,而非普通的变量。还有第三种用法,就是取值运算,和定义指针变量是完全两码事,比如
1
2
3
4
5
unsigned char a = 1;
unsigned char b = 2;
unsigned char *p;
p = &a;
b = *p;

这样两步运算完了之后,b 的值就成了 1 了。在这段代码中,&a 表示取 a 这个变量的地址,把这个地址送给 p 之后,再用*p运算表示的是取指针变量p指向的地址的变量的值,又把这个值送给了 b,最终的结果相当于 b=a。同样是*p,放在定义的位置就是定义指针变量,放在执行代码中就是取值运算。

3.7.1.3 简单示例

使用指针实现流水灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <reg52.h>
#include "function.h"

void ShiftLeft(unsigned char *p);

void main() {
unsigned int i;
unsigned char buf = 0x01;
LED_Init();
while(1) {
P0 = ~buf; //缓冲值取反发送到P0口
delay_ms(2000);
ShiftLeft(&buf); //缓冲值左移一位
if (buf == 0) {
buf = 0x01;
}
}
}

/*将指针变量p指向的字节左移一位*/
void ShiftLeft(unsigned char *p) {
*p = *p << 1; //利用指针变量可以向函数外输出运算结果
}

对比之前的函数调用,大家是否看明白,如果是普通变量传递,只能单向的,也就是说,主函数传递给子函数的值,子函数只能使用却不能改变。而现在我们传递的是指针,不仅仅子函数可以使用 buf 里边的值,而且还可以对 buf 里边的值进行修改

只要是*p 前边带了变量类型如 unsigned char,就是表示定义了一个指针变量 p,而执行代码中的*p,是指 p 所指向的内容。

3.7.2 指向数组元素的指针

3.7.2.1 指向数组元素的指针和运算法则

所谓指向数组元素的指针,其本质还是变量的指针。因为数组中的每个元素,其实都可以直接看成是一个变量,所以指向数组元素的指针,也就是变量的指针。

指向数组元素的指针不难,但很常用。

1
2
unsigned char number[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
unsigned char *p

如果我们写p = &number[0];那么指针p就指向了number的第0号元素,也就是把number[0]的地址赋值给了p,同理,如果写p=&number[1];p 就指向了数组 number 的第 1号元素。p = &number[x];其中 x 的取值范围是0~9,就表示p指向了数组number的第 x 号元素。

指针本身,也可以进行几种简单的运算,这几种运算对于数组元素的指针来说应用最多。

  1. 比较运算。比较的前提是两个指针指向同种类型的对象,比如两个指针变量pq它们指向了具有同种数据类型的数组,那它们可以进行<,>,>=,<=,==等关系运算。如果p==q为真的话,表示这两个指针指向的是同一个元素
  2. 指针整数可以直接进行加减运算。比如还是上边我们那个指针p和数组number,如果p = &number[0],那么p+1就指向了number[1],p+9就指向了number[9]。当然了,如果p = &number[9],p-9 也就指向了number[0]。
  3. 两个指针变量在一定条件下可以进行减法运算。如p = &number[0]; q = &number[9];那么q-p的结果就是 9。但是这个地方要特别注意,这个 9 代表的是元素的个数,而不是真正的地址差值。如果我们的 number 的变量类型是 unsigned int 型,占 2 个字节,q-p 的结果依然是 9,因为它代表的是数组元素的个数

在数组元素指针这里还有一种情况,就是数组名字其实就代表了数组元素的首地址,即

1
2
p = &number[0];
p = number;

这两种表达方式是等价的,因此以下几种表达形式和内容需要注意:

根据指针的运算规则,p+x 代表的是 number[x]的地址,那么number+x代表的也是number[x]的地址。或者说,它们指向的都是number数组的第 x 号元素。*(p+x)和*(number+x)都表示 number[x]。

指向数组元素的指针也可以表示成数组的形式,也就是说,允许指针变量带下标,即 p[i]和*(p+i)是等价的。但是为了避免混淆与规范起见,这里我们建议大家不要写成前者,而一律采用后者的写法。但如果看到别人那么写,也知道是怎么回事即可。

二维数组元素的指针和一维数组类似,需要介绍的内容不多。假如现在一个指针变量p和一个二维数组number[3][4],它的地址的表达方式也就是 p=&number[0][0],有一个地方要注意,既然数组名代表了数组元素的首地址,那么也就是说p和 number 都是指数组的首地址。对二维数组来说,number[0],number[1],number[2]都可以看成是一维数组的数组名字,所以number[0]等价于&number[0[0],number[1]等价于&number[1][0],number[2]等价于&number[2][0]。加减运算和一维数组是类似的,不再详述。

3.7.2.1 指向数组元素指针的实例

在 C 语言里,sizeof()可以用来获取括号内的对象所占用的内存字节数,虽然它写作函数的形式,但它并不是一个函数,而是 C 语言的一个关键字,sizeof()整体在程序代码中就相当于一个常量,也就是说这个获取操作是在程序编译的时候进行的,而不是在程序运行的时候进行。这是一个实际编程中很有用的关键字,灵活运用它可以为程序带来更好的可读性、易维护性和可移植性,在后续的例程学习中将会慢慢有所体会的。

sizeof()括号中可以是变量名,也可以是变量类型名,其结果是等效的。而其更大的用处是与数组名搭配使用,这样可以获取整个数组占用的字节数,就不用自己动手计算了,可以避免错误,而如果日后改变了数组的维数时,也不需要再到执行代码中逐个修改,便于程序的维护和移植。

下面是一个简单的例程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <reg52.h>

bit cmdArrived = 0; //命令到达标志,即接收到上位机下发的命令
unsigned char cmdIndex = 0; //命令索引,即与上位机约定好的数组编号
unsigned char cntTxd = 0; //串口发送计数器
unsigned char *ptrTxd; //串口发送指针

unsigned char array1[1] = {1};
unsigned char array2[2] = {1,2};
unsigned char array3[4] = {1,2,3,4};
unsigned char array4[8] = {1,2,3,4,5,6,7,8};

void ConfigUART(unsigned int baud);

void main()
{
EA = 1; //开总中断
ConfigUART(9600); //配置波特率为 9600

while (1)
{
if (cmdArrived)
{
cmdArrived = 0;
switch (cmdIndex)
{
case 1:
ptrTxd = array1; //数组 1 的首地址赋值给发送指针
cntTxd = sizeof(array1); //数组 1 的长度赋值给发送计数器
TI = 1; //手动方式启动发送中断,处理数据发送
break;
case 2:
ptrTxd = array2;
cntTxd = sizeof(array2);
TI = 1;
break;
case 3:
ptrTxd = array3;
cntTxd = sizeof(array3);
TI = 1;
break;
case 4:
ptrTxd = array4;
cntTxd = sizeof(array4);
TI = 1;
break;
default:
break;
}
}
}
}
/* 串口配置函数,baud-通信波特率 */
void ConfigUART(unsigned int baud)
{
SCON = 0x50; //配置串口为模式 1
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x20; //配置 T1 为模式 2
TH1 = 256 - (11059200/12/32)/baud; //计算 T1 重载值
TL1 = TH1; //初值等于重载值
ET1 = 0; //禁止 T1 中断
ES = 1; //使能串口中断
TR1 = 1; //启动 T1
}
/* UART 中断服务函数 */
void InterruptUART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0; //清零接收中断标志位
cmdIndex = SBUF; //接收到的数据保存到命令索引中
cmdArrived = 1; //设置命令到达标志
}
if (TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
if (cntTxd > 0) //有待发送数据时,继续发送后续字节
{
SBUF = *ptrTxd; //发出指针指向的数据
cntTxd--; //发送计数器递减
ptrTxd++; //发送指针递增
}
}
}