开发板介绍

这个板子是KST-51的开发板,芯片是STC89C52RC

新建工程

对于单片机程序来说,每个功能程序,都必须要有一个配套的工程(Project),先在文件夹创建一个工程,如图所示:

接着打开 Keil 软件,点击:Project–>New uVision Project…然后会出现一个新建工程的界面,如图所示:

给这个工程起一个名字叫做 temple,如图所示:

保存之后会弹出一个对话框,这个对话框让我们选择单片机型号。因为 Keil 软件是外国人开发的,所以我们国内的 STC89C52 并没有上榜,但是只要选择同类型号就可以了。这里我们直接选择 Intel 公司名下的 80/87C52 来代替,如图所示:

点击 OK 之后,会弹出一个对话框,如图所示,每个工程都需要一段启动代码,如果点“否”编译器会自动处理这个问题,如果点“是”,这部分代码会提供给我们用户,我们就可以按需要自己去处理这部分代码,那这部分代码在我们初学 51 的这段时间内,一般是不需要去修改的,但是随着技术的提高和知识的扩展,我们就有可能会需要了解这块内容,所以这里先点“否”

这样工程就建立好了,我们现在工程文件下先新建两个文件,一个是USER,存放main.c文件,一个是APP,存放一些外设文件,因为一个工程里面肯定会包括很多外设,这样子方便管理,如图所示:

工程有了之后,我们要建立编写代码的文件,点击 File–>New,新建一个文件,也就是我们编写程序的平台。然后点 File–>Save,可以保存文件,也可以用键盘上的快捷键Ctrl + N,新建文件,Ctrl + S保存文件,如图所示:

文件夹保存在我们刚刚新建的USER目录下,命名为main.c,如图所示:

我们每做一个功能程序,必须要新建一个工程,一个工程代表了单片机要实现的一个功能。但是一个工程,有时候可以把我们的程序分多个文件写,所以每写一个文件,我们都要添加到我们所建立的工程中去

1、点击三色正方形

2、在Groups那里同样建立USER和APP文件夹

3、接着在USER中添加我们刚才建立的main.c文件

接着我们还要来配置一些选项

点击这个魔法棒,接着点击Output,把这个HEX文件勾选上,这样子编译才可以找到这个HEX文件,如图所示:

这样子这个工程就配置好啦

如果之后还要添加一些外设也是这个步骤,外设建立一个.c文件和一个.h文件(头文件),不过外设文件可以放在我们自己建立的APP目录下,例如要新建LED外设,先在APP目录下新建LED文件,接着建立LED.c和LED.h文件,然后把LED.c文件添加到这个LED文件中

要保证编译时可以找到我们建立的.h文件,我们要把头文件的路径给包含进来,这样子才可以识别到.h文件

点击这个魔法棒,接着点击C51,在Include Paths这里点击那三个点,把头文件路径包含进来,如图所示:

头文件里面有固定的模板,跟着敲就行,后面的内容就是我们建立头文件的名字,不过要用大写,这个主要是防止重复定义

1
2
3
4
5
6
#ifndef __LED_H
#define __LED_H

//内容写在这里面,主要是写一些宏定义,定义变量,函数调用

#endif

单片机的内部资源

这里的单片机内部资源,是指作为单片机用户,单片机提供给我们可使用的东西。总结起来,主要是三大资源:

Flash——程序存储空间,早期单片机是 OTPROM

RAM——数据存储空间

SFR——特殊功能寄存器

这个STC89C52RC 的资源情况:Flash 程序空间是 8K 字节(1K=1024,1 字节= 8 位),RAM 数据空间是 512 字节,SFR 我们后边会逐一提到并且应用。

单片机最小系统

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

电源

我们所选用的 STC89C52,它需要 5V 的供电系统,我们的开发板是使用 USB 口输出的 5V 直流直接供电的。供电电路在 40 脚 和 20 脚的位置上,40 脚接的是 +5V,通常也称为 VCC 或 VDD,代表的是电源正极,20 脚接的是 GND,代表的是电源的负极。

晶振

晶振,又叫晶体振荡器,作用是为单片机系统提供基准时钟信号,单片机内部所有的工作都是以这个时钟信号为步调基准来进行工作的。STC89C52 单片机的 18 脚 和 19 脚是晶振引脚,我们接了一个 11.0592M 的晶振(它每秒钟振荡 11,059,200 次),外加 两个 20pF 的电容,电容的作用是帮助晶振起振,并维持振荡信号的稳定。

复位电路

复位电路,接到了单片机的 9 脚 RST(Reset)复位引脚上,单片机复位一般是 3 种情况:上电复位、手动复位、程序自动复位

基础知识点

三极管

三极管的初步认识

三极管是一种很常用的控制和驱动器件,主要用来 控制电流的大小,箭头方向表示 电流的流向,同时表示了三极管的极性,常用的三极管根据材料分有硅管锗管两种, 三极管有 2 种类型,分别是 PNP 型NPN 型, 箭头朝外的表示为NPN型、箭头方向朝里的表示为PNP型

三极管一共有 3 个极,横向左侧的引脚叫做基极b,中间有一个箭头, 一头连接基极,另外一头连接的是发射极 e,那剩下的一个引脚就是集电极 c

三极管的原理

三极管有截止、放大、饱和三种工作状态。放大状态主要应用于模拟电路中,而数字电路主要使用的是三极管的开关特性,只用到了截止与饱和两种状态。

  • 三极管饱和-----实现电子开关的“开”功能
  • 三极管截止-----实现电子开关的“关”功能

口诀:导通电压顺箭头过,电压导通,电流控制

三极管用法特点:

关键点在于 b 极(基极)和 e 极(发射极)之间的电压情况, 对于PNP 而言,e 极电压只要高于 b 极 0.7V 以上,这个三极管 e 极和 c 极之间就可以顺利导通。也就是说,控制端在 b 和 e 之间,被控制端是 e 和 c 之间。 同理,NPN 型三极管的导通电压是 b 极比 e 极高 0.7V,总之是箭头的始端比末端高 0.7V 就可以导通三极管的 e 极和 c 极

b作为控制端,NPN型三极管,高电平导通,低电平关断;PNP型三极管,高电平关断,低电平导通

NPN和PNP三极管的接法有些不同, NPN型三极管当下管使用,控制灯泡的负极;PNP型三极管当上管使用,控制灯泡的正极

逻辑运算

逻辑运算符

以下逻辑运算符都是按照变量整体值进行运算的,通常就叫做逻辑运算符

&& 逻辑与 两个为真才为真,一个为假就是假
|| 逻辑或 一个为真就是真,两个为假才是假
! 逻辑非 F = ! A,当 A 值为假时,其结果 F 为真;当 A 值为真时,结果 F 为假

位运算符

以下逻辑运算符都是按照变量内的每一个位来进行运算的,通常就叫做位运算符

& 按位与 F = A & B,将 A、B 两个字节中的每一位都进行与运算
| 按位或 将 A、B 两个字节中的每一位都进行或运算
~ 按位取反 F = ~A,将 A 字节内的每一位进行非运算(就是取反)
^ 按位异或 如果运算双方的值不同(即相异)则结果为真,双方值相同则结果为假(相同为0,不同为1)

F = A ^ B,A = 0b11001100,B = 0b11110000,F = 0b00111100

逻辑电路符号

不同数据类型间的相互转换

当不同数据类型之间混合运算的时候,不同类型的数据首先会转换为同一类型,转换的主要原则是:短字节的数据向长字节数据转换

比如:unsigned char a; unsigned int b; unsigned int c; c = a *b; 在运算的过程中,程序会自动全部按照 unsigned int 型来计算,c 的数据类型是 unsigned int 型,取值范围是 0~65535,而 70000 超过 65535 了,其结果会溢出,最终 c 的结果是 (70000 - 65536) = 4464

不同类型变量之间的相互赋值,短字节类型变量向长字节类型变量赋值时,其值保持不变。比如 unsigned char a=100; unsigned int b=700; b=a;那么最终 b 的值就是 100 了。但是如果我们的程序是 unsigned char a=100; unsigned int b=700; a=b;那么 a 的值仅仅是取了 b 的低 8 位,我们首先要把 700 变成一个 16 位的二进制数据,然后取它的低 8 位出来,也就 是 188,这就是长字节类型给短字节类型赋值的结果,会从长字节类型的低位开始截取刚好等于短字节类型长度的位,然后赋给短字节类型

C 语言不同类型运算的时候数值会转换同一类型运算,但是每一步运算都会进行识别判断,不会进行一个总的分析判断,避免这类问题 的产生可以采用强制类型转换的方法

在一个变量前边加上一个数据类型名,并且这个类型名用小括号括起来,就表示把这个变量强制转换成括号里的类型。如 c = (unsigned long)a * b ;由于强制类型转换运算符优先级高于*,所以这个地方的运算是先把 a 转换成一个 unsigned long 型的变量,而后与 b 相乘, 根据 C 语言的规则 b 会自动转换成一个 unsigned long 型的变量,而后运算完毕结果也是一个 unsigned long 型的,最终赋值给了 c

在 51 单片机里边,有一种特殊情况,就是 bit 类型的变量,这个 bit 类型的强制类型转换,是不符合上边讲的这个原则的,比如 bit a=0; unsigned char b; a=(bit)b;这个地方要特别注意,使用 bit 做强制类型转换,不是取 b 的最低位,而是它会判断 b 这个变量是 0 还是非 0 的值,如果 b 是 0,那么 a 的结果就是 0,如果 b 是任意非 0 的其它值,那么 a 的结果都是 1

51 单片机 RAM 区域的划分

51 单片机的 RAM 分为两个部分,一块是片内 RAM,一块是片外 RAM,片内 RAM 和片外 RAM 的地址不是连起来的, 片内是从 0x00 开始,片外是从 0x0000 开始的

以下是几个 Keil C51 语言中的关键字,代表了 RAM 不同区域的划分

  1. data:片内 RAM 从 0x00~0x7F

  2. idata:片内 RAM 从 0x00~0xFF

  3. pdata:片外 RAM 从 0x00~0xFF

  4. xdata:片外 RAM 从 0x0000~0xFFFF

data 是 idata 的一部分,pdata 是 xdata 的一部分,在 Keil 默认设置下,data 是可以省略的,即什么都不加的时候变量就是定义到 data 区域中的。data 区域 RAM 的访问在汇编语言中用的是直接寻址,执行速度是最快的。如果定义成 idata,不仅仅可以访问 data 区域,还可以访问 0x80H~0xFF 的范围,但加了 idata 关键字后,访问的时候 51 单片机用的是通用寄存器间接寻址,速度较 data 会慢一些,而且我们平时大多数情况下不太希望访问到 0x80H~0xFF,因为这块通常用于中断与函数调用的堆栈,所以在绝大多数情况下,我们使用内部 RAM 的时候,只用 data 就可以了

指针

变量 a 的地址就是 0x00,它的地址的表达方式可以写成:&a,这样就代表了相应变量的地址,C 语言中变量前加一个&表示取这个变量的地址,&在这里就叫做“取址符”

在 C 语言中,我们要访问一个变量,有两种方式:一种是通过变量名来访问, 另一种就是通过变量的地址来访问。在 C 语言中,地址就等同于指针,变量的地址就是变量的指针。保存指针的变量,称之为指针变量,简称为指针,而通常我们说的指针就是指针变量

指针变量的声明

在 C 语言中,变量的地址往往都是编译系统自动分配的,我们是不知道某个变量的具体地址的。所以我们定义一个指针变量 p,把普通变量 a 的地址直接送给指针变量 p

指针变量 p 的定义和初始化,有两种方式:

方法 1:定义时直接进行初始化赋值

1
2
unsigned char a;
unsigned char *p = &a; //普通变量 a 的地址直接送给指针变量 p

方法 2:定义后再进行赋值

1
2
3
4
unsigned char a; 
//这个指针指向的变量类型是 unsigned char 型的, p 本身,是指针变量,不可以给它赋普通的值或者变量
unsigned char *p; //*p代表了这个 p 是个指针变量,专门用来存放变量地址的
p = &a; //普通变量 a 的地址直接送给指针变量 p

定义指针变量 *p 和取值运算 *p 的区别

1
2
3
4
5
6
7
//同样是*p,放在定义的位置就是定义指针变量,放在执行代码中就是取值运算

unsigned char *p; // * 代表的意思是 p 是一个指针变量,而非普通的变量
unsigned char a = 1;
unsigned char b = 2;
p = &a; // &a 表示取 a 这个变量的地址,把这个地址送给 p
b = *p; // *p 运算表示的是取指针变量 p 指向的地址的变量的值,把这个值送给了 b,最终的结果相当于 b = a

指向数组元素的指针

指向数组元素的指针,其本质还是变量的指针。因为数组中的每个元素,其实都可以直接看成是一个变量,所以指向数组元素的指针,也就是变量的指针

1
2
3
4
5
6
7
8
9
10
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 = p + 1; //表示指针 p 指向了 number[1]

//如果
p = &number[0];
q = &number[9];
// q - p 的结果就是 9,这个 9 代表的是元素的个数,而不是真正的地址差值

数组元素指针还有一种情况,就是数组名字其实就代表了数组元素的首地址

1
2
3
4
5
6
7
//这两种表达方式是等价的
p = &number[0];
p = number;

// *(p+x) 和 *(number+x) 都表示 number[x]
//指向数组元素的指针也可以表示成数组的形式,也就是说,允许指针变量带下标,即 p[i]和*(p+i)是等价的
//不过一般采用后者的写法

二维数组元素的指针和一维数组类似

1
2
3
4
5
6
7
8
unsigned char *p;
unsigned char 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]

在 C 语言里边,sizeof() 可以用来获取括号内的对象所占用的内存字节数,sizeof() 括号中可以是变量名,也可以是变量类型名,其结果是等效的

复合数据类型

结构体数据类型

结构体本身不是一个基本的数据类型,而是构造的,它每个成员可以是一个基本的数据类型或者是一个构造类型。结构体既然是一种构造而成的数据类型,那么在使用之前必须先定义

声明结构体变量的一般格式如下:

1
2
3
4
5
6
7
8
struct 结构体名
{
类型 1 变量名 1
类型 2 变量名 2
……
类型 n 变量名 n;
};
struct 结构体名 结构体变量名 1, 结构体变量名 2, ... 结构体变量名 n;

我们来构造一个实际的表示日期时间的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct sTime
{
//日期时间结构体定义
unsigned int year; //年
unsigned char mon; //月
unsigned char day; //日
unsigned char hour; //时
unsigned char min; //分
unsigned char sec; //秒
unsigned char week; //星期
};
struct sTime bufTime;

//struct 是结构体类型的关键字,sTime 是这个结构体的名字,bufTime 就是定义了一个具体的结构体变量

//如果要给结构体变量的成员赋值
bufTime.year = 0x2023;
bufTime.mon = 0x10;

一个指针变量如果指向了一个结构体变量的时候,称之为结构指针变量。结构指针变量是指向的结构体变量的首地址,通过结构体指针也可以访问到这个结构变量

结构指针变量声明的一般形式如下:

1
2
3
4
5
6
7
8
struct sTime *pbufTime;

//使用结构体指针对结构体成员的访问,和使用结构体变量名对结构体成员的访问,其表达式有所不同
//结构体指针对结构体成员的访问表达式为:
pbufTime->year = 0x2013;
//或者
(*pbufTime).year = 0x2013;
//一般使用前者

共用体数据类型

共用体也称之为联合体,共用体定义和结构体十分类似

1
2
3
4
5
6
7
8
union 共用体名
{
数据类型 1 成员名 1
数据类型 2 成员名 2
……
数据类型 n 成员名 n;
};
union 共用体名 共用体变量;

共用体表示的是几个变量共用一个内存位置,也就是成员 1、成员 2……成员 n 都用一个内存位置。共用体成员的访问方式和结构体是一样的,成员访问的方式是:共用体名.成员名,使用指针来访问的方式是:共用体名->成员名

共用体和结构体的主要区别如下:

  1. 结构体和共用体都是由多个不同的数据类型成员组成,但在任何一个时刻,共用体只能存放一个被选中的成员,而结构体所有的成员都存在。

  2. 对于共同体的不同成员的赋值,将会改变其它成员的值,而对于结构体不同成员的赋值是相互之间不影响的

枚举数据类型

在实际问题中,有些变量的取值被限定在一个有限的范围内。例如,一个星期从周一到周日有 7 天,一年从 1 月到 12 月有 12 个月,蜂鸣器有响和不响两种状态等等。如果把这些变量定义成整型或者字符型不是很合适,因为这些变量都有自己的范围。C 语言提供了一种 称为“枚举”的类型,在枚举类型的定义中列举出所有可能的值,并可以为每一个值取一个 形象化的名字,它的这一特性可以提高程序代码的可读性

枚举的说明形式如下:

1
2
3
4
5
6
7
8
enum 枚举名
{
标识符 1[ = 整型常数],
标识符 2[ = 整型常数],
……
标识符 n[ = 整型常数]
};
enum 枚举名 枚举变量;

枚举的说明形式中,如果没有被初始化,那么“=整型常数”是可以被省略的,如果是默认值的话,从第一个标识符顺序赋值 0、1、2……,但是当枚举中任何一个成员被赋值后, 它后边的成员按照依次加 1 的规则确定数值

枚举的使用,有几点要注意:

  1. 枚举中每个成员结束符是逗号,而不是分号,最后一个成员可以省略逗号

  2. 枚举成员的初始化值可以是负数,但是后边的成员依然依次加 1

  3. 枚举变量只能取枚举结构中的某个标识符常量,不可以在范围之外

转义字符

字符串常量在内存中按顺序逐个存储字符串中的字符的 ASCII 码值,并且特别注意,最后还有一个字符 \0,‘\0’ 字符的 ASCII 码值是 0, 它是字符串结束标志,在写字符串的时候,这个‘\0’是隐藏的,但是实际是存在的

LED模块

点亮LED用到的原理图

三极管基极通过一个 1K 的电阻接到了单片机的 74HC138芯片上的 LEDS6端口上, 发射极直接接到 5V 的电源上,集电极上分别连了8个 LED 小灯,8个LED小灯分别连了 330Ω电阻。如果 LEDS6 由我们的程序给一个 高电平 1,那么 基极 b 和发射极 e 都是 5V,也就是说 e到 b 不会产生一个 0.7V 的压降,这个时候,发射极和集电极也就不会导通,LED 小灯也就不会亮。如果程序给 LEDS6 一个低电平 0,这时 e 极还是 5V,于是 e 和 b 之间产生了压差,三极管 e 和 b 之间也就导通了,三极管 e 和 b 之间大概有 0.7V 的压降,那还有(5-0.7)V 的电压会在电阻 R47 上;这时候根据二极管特性只要负极加个 低电平0 就可以点亮LED 小灯了。

74HC245芯片

原理图

74HC245 是个 双向缓冲器(DIR 是方向引脚,当它高电平时:右侧B编号电压等于左侧A编号对应电压;当 DIR低电平:左侧A编号电压等于右侧B编号对应电压);作用是电流驱动缓冲,不起到任何逻辑控制的效果,稳定工作在 70mA 电流是没有问题的,比单片机的 8 个 IO 口大多了,所以我们可以把他接在 小灯和 IO 口之间做缓冲,这个地方 控制端是左侧接的是 P0 口,我 们要求 B 等于 A 的状态,所以 1 脚我们直接接的 5V 电源,即高电平

问:已经在电源 VCC 那地方加了一个三极管驱动了, 为何还要再加 245 驱动芯片呢?

从电源正极到负极的电流水管的粗细都要满足要求,任何一个位置的管子过细,都会出现瓶颈效应,电流在整个通路中细管处会受到限制而降低,所以在电路通路的每个位置上,都要保证通道足够畅通,这个 74HC245 的作用就是消除单片机 IO 这一环节的瓶颈

74HC138芯片

三八译码器,就是把 3 种输入状态翻译成 8 种输出状态

想让这个 74HC138 正常工作,ENLED 那个输入位置必须输入低电平ADDR3 位置必须输入高电平

原理图

头上有一横表示在低电平时有效
真值:
L:表示低电平0
H:表示高电平1
X:表示无论是高电平还是低电平都不影响真值

从上面的管脚图及真值表可以知道该芯片使用方法很简单,给 G1(E3) 使能管脚高电平,G2(ENLED) 管脚为低电平,至于哪个管脚输出有效电平(低电平),要看 C B A 输入管脚的电平状态。如果 C B A 都为低电平,则 Y0 输出有效电平(低电平),其他管脚均输出高电平。

方法:C B A 输入就相当于 3 位 2 进制数,A 是低位,B 是次高位,C 是高位。而 Y0-Y7 具体哪一个输出有效电平,就看输入二进制对应的十进制数值。比如输入是 110(C,B,A),其对应的十进制数是 6,所以Y6输出有效电平(低电平)。

C对应A2,B对应A1,A对应A0,则A2为高位,A0为低位

前面我们知道需要给 LEDS6 一个 低电平 0才能点亮LED,所以需要设置 Y6,则需要看真值表

由真值表可知,想让 Y6为低电平,则需要设置:

  • ADDR3(E3):1
  • ADDR2(A2):1
  • ADDR1(A1):1
  • ADDR0(A0):0
  • ENLED(E1+E2):0

LED小灯整体电路图

我们来看下点亮 LED 小灯的过程,首先看 74HC138,我们要让 LEDS6低电平才能导通三极管 Q16,所以 ENLED = 0,ADDR3 = 1;保证 74HC138 使能。然后 ADDR2 = 1; ADDR1 = 1; ADDR0 = 0;这样保证了三极管 Q16 这个开关开通,5V 电源加到 LED 上。 而 74HC245 左侧是通过 P0 口控制,我们让 P0.0 引脚等于 0,就是 DB_0 等于 0,而右侧 DB0 等于 DB_0 的状态,也是 0,那么这样在这一排共 8 个 LED 小灯当中,只有最右侧的小灯和 5V 之间有压差,有压差就会有电流通过,有电流通过我们的 LED2 就会发光了。 74HC245 左侧我们可以看出来,是直接接到 P0 口上的,而 74HC138 的 ADDR0 ~ ADDR3 接在何处呢?来看下面的图

从图中可以看出,把跳线帽右侧和中间的针连到了一 起,这样实现的就是图中的 P1.0 和 ADDR0 连接到一起、P1.1 和 ADDR1 接一起、P1.2 和 ADDR2 接一起、P1.3 和 ADDR3 接一起,这样子就可以很清晰地知道为什么小灯点亮了

LED程序

点亮一个LED

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "reg52.h"
sbit LED2 = P0 ^ 0;
sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

void main()
{
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
ADDR2 = 1; //A2到A0的值为110,则使Y6为低电平,则LEDS6为低电平,使三极管导通,点亮LED灯
ADDR1 = 1;
ADDR0 = 0;

LED2 = 0; //点亮LED2
while(1);
}

LED灯闪烁

这里我另外新建了Delay文件,里面存放延时函数,方便管理,要用的时候直接调用头文件就好

Dealy.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
#include <reg52.h>
#include "Delay.h"

/* -------------------------------- begin ------------------------------ */
/**
* @从STC-ISP生成复制延时函数
* @函数名: Delay_ms
* @参数1 : 需要延时多少毫秒
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */
void Delay_ms(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
}

Dealy.h

1
2
3
4
5
6
7
8
#ifndef _DELAY_
#define _DELAY_

#include <reg52.h>

void Delay_ms(unsigned int xms); //函数声明

#endif

main.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
#include "reg52.h"
#include "Delay.h"

sbit LED2 = P0 ^ 0;
sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

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

while(1)
{
LED2 = 0; //点亮LED2
Delay_ms(20); //延时20毫秒
LED2 = 1; //熄灭LED2
Delay_ms(20);
}
}

LED左右流水

Dealy.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
#include <reg52.h>
#include "Delay.h"

/* -------------------------------- begin ------------------------------ */
/**
* @从STC-ISP生成复制延时函数
* @函数名: Delay_ms
* @参数1 : 需要延时多少毫秒
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */
void Delay_ms(unsigned int xms) //@11.0592MHz
{
unsigned char data i, j;
while(xms--)
{
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
}

Dealy.h

1
2
3
4
5
6
7
8
#ifndef _DELAY_
#define _DELAY_

#include <reg52.h>

void Delay_ms(unsigned int xms); //函数声明

#endif

main.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
42
43
44
45
#include "reg52.h"
#include "Delay.h"

#define LED_PORT P0 //定义LED端口为 P0

sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

void main()
{
u8 i = 0;
u8 dir = 0;
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
ADDR2 = 1; //A2到A0的值为110,则使Y6为低电平,则LEDS6为低电平,使三极管导通,点亮LED灯
ADDR1 = 1;
ADDR0 = 0;

while(1)
{
for(i = 0; i < 8; i++)
{
if(dir == 0) //如果dir为0,则流水灯向左移动
{
LED_PORT = ~(0x01 << i); //0000 0001 左移i位 按位取反变成 1111 1110 (点亮第一个灯)以此类推循环8次
Delay_ms(20); //延时20毫秒
if(LED_PORT == 0x7f) //0x7f --> 0111 1111 ,如果第八个灯点亮
dir = 1;
}
if(dir == 1) //如果dir为1,则流水灯向右移动
{
for(i = 0; i < 8; i++) //循环八次
{
LED_PORT = ~(0x80 >> i); //1000 0000 右移i位 按位取反变成 0111 1111 (点亮第八个灯)以此类推循环8次
Delay_ms(20); //延时20毫秒
if(LED_PORT == 0xfe) //0xfe --> 1111 1110 ,如果第一个灯点亮
dir = 0;
}
}
}
}
}

定时器

定时器的初步认识

①振荡周期:石英晶体振荡器的工作频率的倒数,为单片机提供定时信号的振荡源的周期(晶振周期或外加振荡周期)。

②时钟周期:2 个振荡周期为 1 个时钟周期

③机器周期:单片机完成一个操作的最短时间1 个机器周期含 6 个时钟周期,12 个振荡周期

④指令周期:完成 1 条指令所占用的全部时间,它以机器周期为单位。

例如:外接晶振为 12MHz 时,51 单片机相关周期的具体值为:

振荡周期=1/12us;
时钟周期=1/6us;
机器周期=1us;
指令周期=1~4us

定时器介绍

①51 单片机有两组定时器/计数器,因为既可以定时,又可以计数,故称之为定时器/计数器。
②定时器/计数器和单片机的 CPU 是相互独立的。定时器/计数器工作的过程是自动完成的,不需要 CPU 的参与
③51 单片机中的定时器/计数器是根据机器内部的时钟或者是外部的脉冲信号对寄存器中的数据加 1。
有了定时器/计数器之后,可以增加单片机的效率,一些简单的重复加 1 的工作可以交给定时器/计数器处理。CPU 转而处理一些复杂的事情。同时可以实现精确定时作用。

定时器原理

STC89C5X 单片机内有两个可编程的定时/计数器 T0、T1 和一个特殊功能定时器 T2。定时/计数器的实质是加 1 计数器(16 位),由高 8 位和低 8 位两个寄存器 THx 和 TLx 组成。它随着计数器的输入脉冲进行自加 1,也就是每来一个脉冲,计数器就自动加 1,当加到计数器为全 1 时,再输入一个脉冲就使计数器回零,且计数器的溢出使相应的中断标志位置 1,向 CPU 发出中断请求(定时 /计数器中断允许时)。如果定时/计数器工作于定时模式,则表示定时时间已到; 如果工作于计数模式,则表示计数值已满。可见,由溢出时计数器的值减去计数初值才是加 1 计数器的计数值。

定时器就是用来进行定时的,定时器内部有一个寄存器,我们让它开始计数后,这个寄存器的值每经过一个机器周期就会自动加 1,因此,我们可以把机器周期理解为定时器的计数周期。就像我们的钟表,每经过一秒,数字自动加 1,而这个定时器就是每过一个机器周期的时间,也就是 12/11059200 秒,数字自动加 1。还有一个特别注意的地方, 就是钟表是加到 60 后,秒就自动变成 0 了,这种情况在单片机或计算机里我们称之为溢出。 假如是 16 位的定时器,也就是 2 个字节,最大值就是 65535, 那么加到 65535 后,再加 1 就算溢出,对于 51 单片机来说,溢出后,这个值会直接变成 0。从某一个初始值开始,经过确定的时间后溢出,这个过程就是定时的含义

51 单片机定时器/计数器内部结构图:

定时器的寄存器

标准的 51 单片机内部有 T0 和 T1 这两个定时器,对于单片机 的每一个功能模块,都是由它的 SFR,也就是特殊功能寄存器来控制。与定时器有关的特殊功能寄存器,有以下几个,以下表的寄存器是存储定时器的计数值的。TH0/TL0 用于 T0,TH1/TL1 用于 T1

计数器的工作由两个特殊功能寄存器控制

TCON控制寄存器,控制 T0、T1启动停止及设置溢出标志

TMOD 是定时/计数器的工作方式寄存器,确定工作方式和功能

TCON 是“可位寻址”,TMOD 是“不可位寻址”;可位寻址就是可以直接操作其中一个位不可位寻址只能操作整个字节

控制寄存器 TCON

TCON 的低 4 位用于控制外部中断。TCON 的高 4 位用于控制定时/计数器的启动和中断申请

其格式如下:

TF1(TCON.7):T1(定时器1) 溢出中断请求标志位。T1 计数溢出时由硬件自动置 TF1 为 1。CPU 响应中断后 TF1 由硬件自动清 0。T1 工作时,CPU 可随时查询 TF1 的状态。所以,TF1 可用作查询测试的标志。TF1 也可以用软件置 1 或清 0,同硬件置 1 或清 0 的效果一样。

TR1(TCON.6):T1(定时器1) 运行控制位。TR1 置 1 时,T1 开始工作;TR1 置 0 时, T1 停止工作。TR1 由软件置 1 或清 0。所以,软件置位/清零来进行启动/停止定时器。

TF0(TCON.5):T0 溢出中断请求标志位,其功能与 TF1 类同。

TR0(TCON.4):T0 运行控制位,其功能与 TR1 类同。

当我们程序中写 TR1 = 1 以后,定时器值就会每经过一个机器周期自动加 1,当我们程序中写 TR1 = 0 以后,定时器就会停止加 1,其值会保持不变化。TF1是一个标志位,他的作用是告诉我们定时器溢出了。比如我们的定时器设置成 16 位的模式,那么每经过一个机器周期,TL1 加 1 一次,当 TL1 加到 255 后,再加 1,TL1 变成 0,TH1 会加 1 一次,如此一直加到 TH1 和 TL1 都是 255(即 TH1 和 TL1 组成的 16 位整型数为 65535)以后,再加 1 一次,就会溢出了,TH1 和 TL1 同时都变为 0,只要一溢出,TF1 马上自动变成 1,告诉我们定时器溢出了,仅仅是提供给我们一个信号,让我们知道定时器溢出了,它不会对定时器是否继续运行产生任何影响

工作方式寄存器 TMOD

工作方式寄存器 TMOD 用于设置定时/计数器的工作方式低四位用于 T0,高四位用于 T1

其格式如下:

GATE 是门控位, GATE=0 时,用于控制定时器的启动是否受外部中断源信号的影响。只要用软件使 TCON 中的 TR0 或 TR1 为 1,就可以启动定时/计数器工作;

GATE=1 时,要用软件使 TR0 或 TR1 为 1同时外部中断引脚 INT0/1 也为高电平时,才能启动定时/计数器工作。即此时定时器的启动条件,加上了 INT0/1 引脚为高电平这一条件。

C/T :定时/计数模式选择位。C/T =0 为定时模式;C/T =1 为计数模式

M1M0:工作方式设置位。定时/计数器有四种工作方式。

定时/计数器的工作方式

(1)方式 0

方式 0 为 13 位计数,由 TL0 的低 5 位(高 3 位未用)和 TH0 的 8 位组成。 TL0 的低 5 位溢出时向 TH0 进位,TH0 溢出时,置位 TCON 中的 TF0 标志

门控位 GATE 具有特殊的作用。当 GATE=0 时,经反相后使或门输出为 1,此时仅由 TR0 控制与门的开启,与门输出 1 时,控制开关接通,计数开始;

GATE=1 时,由外中断引脚信号控制或门的输出,此时控制与门的开启由外部中断引脚信号和 TR0 共同控制。当 TR0=1 时,外部中断引脚信号引脚的高电平启动计数,外部中断引脚信号引脚的低电平停止计数。这种方式常用来测量外中断引脚上正脉冲的宽度。计数模式时,计数脉冲是 T0 引脚上的外部脉冲。

GATE = 0 时,经过 "非门"变成 1,然后再到 “或门”(只要一个为真,都为真),这时候不管 INTO 引脚是否为真,然后来到 “与门”,这时候取决于 TR0,当 TR0 为1(真) 开关才会闭合工作。计数初值与计数个数的关系为:

X=213NX=2^{13}-N

(2)方式 1

方式 1 的计数位数是 16 位,由 TL0 作为低 8 位,TH0 作为高 8 位,组成了 16 位加 1 计数器

OSC框:表示时钟频率,因为 1 个机器周期等于 12 个时钟周期,所以那个 d 就等于 12

● 在 GATE 位为 1 的情况下,经过一个非门变成 0,或门电路结果要想是 1 的话,那 INT0 即 P3.2 引脚必须是 1 的情况下,这个时候定时器才会工作,而 INT0 引脚是 0 的情况下,定时器不工作,这就是 GATE 位的作用
● 当 GATE 位为 0 的时候,经过一个非门会变成 1,那么不管 INT0 引脚是什么电平,经过或门电路后都肯定是 1,定时器就会工作
● 要想让定时器工作,就是自动加 1,从图上看有两种方式,第一种方式是那个开关打到上边的箭头,就是 C/T = 0 的时候,一个机器周期 TL 就会加 1 一次,当开关打到下边的箭头,即 C/T =1 的时候,T0 引脚即 P3.4 引脚来一个脉冲,TL 就加 1 一次,这也就是计数器功能。

原理跟方式 0 差不多唯一不同的就是计数方式不同, 计数初值与计数个数的关系为:

X=216NX=2^{16}-N

(3)方式 2

方式 2 为自动重装初值的 8 位计数方式。工作方式 2 特别适合于用作较精确的脉冲信号发生器

计数初值与计数个数的关系为:

X=28NX=2^8-N

(4)方式 3

方式 3 只适用于定时/计数器 T0,定时器 T1 处于方式 3 时相当于 TR1=0, 停止计数。工作方式 3 将 T0 分成为两个独立的 8 位计数器 TL0 和 TH0。

这几种工作方式中应用较多的是方式 1 和方式 2定时器中通常使用定时器方式 1串口通信中通常使用方式 2

计算定时/计数器初值

12MHz晶振情况下,1个机器周期=1us,假如需要定时1ms则

1ms/1us=10001ms/1us=1000\text{次}

溢出是65536,则用

655361000=64536=FC18H65536-1000=64536=FC18H

所以初值为 THx=0XFC,TLx=0X18

开发板上使用的外部晶振不同,换算的初值是不一样的

我们开发板的晶振是 11.0592M时钟周期就是 1/11059200,机器周期是 12/11059200,假如要定时 20ms,就是 0.02 秒,要经过 x 个机器周期得到 0.02 秒,我们来算一下 x*12/11059200=0.02,得到 x= 18432
先给 TH0 和 TL0 一个初始值,让它们经过 18432 个机器周期后刚好达到 65536,也就是溢出,溢出后可以通过检测 TF0 的值得知,就刚好是 0.02 秒。那么初值 y = 65536 - 18432 = 47104,转成 16 进制就是 0xB800,也就是 TH0 = 0xB8,TL0 = 0x00

小工具

使用小工具进行换算不用手动算

也可以使用STC-ISP里面的定时器计算器

如果要实现很长时间的定时,比如定时 1 秒钟。可以通过初值设置定时 1ms,每当定时 1ms 结束后又重新赋初值,并且设定一个全局变量累计定时 1ms 的次数,当累计到 1000 次,表示已经定时 1 秒了。需要其他定时时间类似操作,这样我们就可以使用定时器来实现精确延时来替代之前的 delay 函数。

定时器配置

在使用定时器时,应该如何配置使其工作?其步骤如下(各步骤顺序可任意):

  1. 对 TMOD 赋值,以确定 T0 和 T1 的工作方式,如果使用定时器 0 即对 T0 配 置,如果使用定时器 1 即对 T1 配置

  2. 根据所要定时的时间计算初值,并将其写入 TH0、TL0 或 TH1、TL1

  3. 如果使用中断,则对 EA 赋值,开放定时器中断

  4. 使 TR0 或 TR1 置位,启动定时/计数器定时或计数

  5. 判断 TCON 寄存器的 TF0 位,监测定时器溢出情况

定时器程序

定时器0控制LED 0.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
#include <reg52.h>
sbit LED2 = P0 ^ 0;
sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

void main()
{
unsigned int count = 0; //记录T0溢出次数
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
ADDR2 = 1; //A2到A0的值为110,则使Y6为低电平,则LEDS6为低电平,使三极管导通,点亮LED灯
ADDR1 = 1;
ADDR0 = 0;
TMOD |= 0x01; //选择为定时器0模式,工作方式1(为了不干扰T1定时器所以用|);如果是定时器1则把0x01改成0x10
TL0 = 0x66; //给定时器赋初值,定时1ms(使用STC-ISP里面的定时器计算器);如果是定时器1则把TH0改成TH1
TH0 = 0xFC; //如果是定时器1则把TL0改成TL1
TR0 = 1; //定时器0开始计时

while(1)
{
if(TF0 == 1) //如果溢出
{
TF0 = 0; //清除溢出标志位
TL0 = 0x66; //重新赋值
TH0 = 0xFC; //重新赋值
count++; //TF0溢出一次计数次数+1
}
if(count == 500) //如果count = 500则代表溢出500次,500ms = 0.5s
{
count = 0; //溢出次数归0
LED2 = !LED2; //LED2状态取反
}
}
}

使用定时器定时0.5秒控制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
#include <reg52.h>
#define LED_PORT P0 //设置LED端口为P0
sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

void main()
{
unsigned int count = 0; //记录T0溢出次数
unsigned char led_state = 0x01; //LED状态
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
ADDR2 = 1; //A2到A0的值为110,则使Y6为低电平,则LEDS6为低电平,使三极管导通,点亮LED灯
ADDR1 = 1;
ADDR0 = 0;
TMOD |= 0x01; //选择为定时器0模式,工作方式1(为了不干扰T1定时器所以用|);如果是定时器1则把0x01改成0x10
TL0 = 0x66; //给定时器赋初值,定时1ms(使用STC-ISP里面的定时器计算器);如果是定时器1则把TH0改成TH1
TH0 = 0xFC; //如果是定时器1则把TL0改成TL1
TR0 = 1; //定时器0开始计时
while(1)
{
if(TF0 == 1) //如果溢出
{
TF0 = 0; //清除溢出标志位
TL0 = 0x66; //重新赋值
TH0 = 0xFC; //重新赋值
count++; //TF0溢出一次计数次数+1
if(count == 500) //如果count = 500则代表溢出500次,500ms = 0.5s
{
count = 0; //溢出次数归0
LED_PORT = ~led_state; // LED按照led_state的状态取反输出
led_state <<= 1; // 左移一位
if(led_state == 0) // 到达最高位后将 led_state 置 1
{
led_state = 0x01; //重新设置为第一个LED灯
}
}
}
}
}

中断

标准 51 单片机中控制中断的寄存器有两个,一个是中断使能寄存器,另一个是中断优先级寄存器

中断概念

中断是为使单片机具有对外部或内部随机发生的事件实时处理而设置的, 中断功能的存在,很大程度上提高了单片机处理外部或内部事件的能力。对于单片机来讲,中断是指 CPU 在处理某一事件 A 时,发生了另一事件 B, 请求 CPU 迅速去处理(中断发生);CPU 暂时停止当前的工作(中断响应), 转去处理事件 B(中断服务);待 CPU 将事件 B 处理完毕后,再回到原来事件 A 被中断的地方继续处理事件 A(中断返回),这一过程称为中断。

单片机在执行程序时其程序流程图如下:

引起 CPU 中断的根源称为中断源。中断源向 CPU 提出中断请求,CPU 暂时中断原来的事务 A,转去处理事件 B,对事件 B 处理完毕后,再回到原来被中断的地方(即断点),称为中断返回。实现上述中断功能的部件称为中断系统(中断机构)。

CPU总是先响应优先级别最高的中断请求。如果 CPU 能够暂停对原来中断源的服务程序,转而去处理优先级更高的中断请求源,处理完以后,再回到原低级中断服务程序,这样的过程称为 中断嵌套。这样的中断系统称为 多级中断系统,没有中断嵌套功能的中断系统称为单级中断系统

中断优点:

1、分时操作
CPU 可以分时为多个I/O 设备服务,提高了计算机的利用率;

2、实时响应
CPU 能够及时处理应用系统的随机事件,系统的实时性大大增强;

3、可靠性高
CPU具有处理设备故障及掉电等突发性事件能力,从而使系统可靠性提高

中断结构

STC89C5X 系列单片机提供了 8 个中断请求源,它们分别是:外部中断 0(INT0)、外部中断 1(INT1)、外部中断 2(INT2)、外部中断 3(INT3)、定时器 0 中断、定时器 1 中断、定时器 2 中断、串口(UART)中断

注意:51 系列单片机一定有基本的 5 个中断,但不全有 8 个中断,需要查看芯片手册,通常我们使用的都是基本的 5 个中断:INT0、INT1、定时器 0/1,串口中断。所有的中断都具有四个中断优先级(基本型只有两个)。

1
2
3
高优先级的中断请求可以打断低优先级的中断,反之,低优先级的中断请求不可以打断高优先级及同优先级的中断。
当两个相同优先级的中断同时产生时,将由查询次序来决定系统先响应哪个中断。
中断查询次序即为中断号,这个中断号在编程时非常重要,当中断来临时,只有中断号正确才能进入中断。

STC89C5X 系列单片机的各个中断查询次序表如下图所示:

下面是51单片机均有的5个基本中断:

INT0 对应的是 P3.2 口的附加功能,可由 IT0(TCON.0)选择其为低电平有效还是下降沿有效。当 CPU 检测到 P3.2 引脚上出现有效的中断信号时,中断标志IE0(TCON.1)置 1,向 CPU 申请中断。

INT1 对应的是 P3.3 口的附加功能,可由 IT1(TCON.2)选择其为低电平有效还是下降沿有效。当 CPU 检测到 P3.3 引脚上出现有效的中断信号时,中断标志 IE1(TCON.3)置 1,向 CPU 申请中断。

T0 对应的是 P3.4 口的附加功能,TF0(TCON.5),片内定时/计数器 T0 溢出中断请求标志。当定时/计数器 T0 发生溢出时,置位 TF0,并向 CPU 申请中断。

T1 对应的是 P3.5 口的附加功能,TF1(TCON.7),片内定时/计数器 T1 溢出中断请求标志。当定时/计数器 T1 发生溢出时,置位 TF1,并向 CPU 申请中断。

RXD 和 TXD 对应的是 P3.0 和 P3.1 口的附加功能,RI(SCON.0)或 TI (SCON.1),串行口中断请求标志。当串行口接收完一帧串行数据时置位 RI 或当串行口发送完一帧串行数据时置位 TI,向 CPU 申请中断。

中断相关寄存器

中断允许控制

CPU 对中断系统所有中断以及某个中断源的开放和屏蔽是由中断允许寄存器 IE 控制的。

1
2
3
4
5
6
EX0(IE.0):外部中断 0 允许位; 
ET0(IE.1):定时/计数器 T0 中断允许位;
EX1(IE.2):外部中断 1 允许位;
ET1(IE.3):定时/计数器 T1 中断允许位;
ES(IE.4):串行口中断允许位;
EA (IE.7):CPU 中断允许(总允许)位。

中断请求标志 TCON

1
2
3
4
5
6
IT0(TCON.0),外部中断 0 触发方式控制位。 当 IT0=0 时,为电平触发方式。 当 IT0=1 时,为边沿触发方式(下降沿有效)。 
IE0(TCON.1),外部中断 0 中断请求标志位。
IT1(TCON.2),外部中断 1 触发方式控制位。
IE1(TCON.3),外部中断 1 中断请求标志位。
TF0(TCON.5),定时/计数器 T0 溢出中断请求标志位。
TF1(TCON.7),定时/计数器 T1 溢出中断请求标志位。

中断优先级

同一优先级中的中断申请不止一个时,则有中断优先权排队问题。同一优先级的中断优先权排队,由中断系统硬件确定的自然优先级形成,其排列如所示:

中断号

问:单片机又怎样找到这个中断函数呢?

靠的就是中断向量地址,所以 interrupt 后面中断函数编号的数字 x 就是根据中断向 量得出的,它的计算方法是 x*8+3=向量地址

中断优先级有两种,一种是 抢占优先级,一种是 固有优先级

抢占优先级寄存器:

中断响应条件

①中断源有中断请求;
②此中断源的中断允许位为 1;
③CPU 开中断(即 EA=1)。
以上三条同时满足时,CPU 才有可能响应中断。

在使用中断时我们需要做什么呢?

①你想使用的中断是哪个?选择相应的中断号;
②你所希望的触发条件是什么?
③你希望在中断之后干什么?

外部中断配置

以外部中断 0 为例,如下:

主程序中需要有以下代码

如果要配置的是外部中断 1,只需将 EX0 改为 EX1IT0 改为 IT1,通常使用外部中断都是配置为下降沿触发,即 IT0=1

当触发中断后即会进入中断服务函数,外部中断 0 中断服务函数如下:

在中断函数中 exti0 是函数名,可自定义,但必须符合 C 语言标识符定义规则,interrupt 是一个关键字,表示 51 单片机中断。后面的 0 是中断号,外部中断 0 中断号为 0,如果是外部中断 1,则中断号为 2

数码管

数码管简介

数码管是一种半导体发光器件,其基本单元是发光二极管。按发光二极管单元连接方式可分为共阳极数码管共阴极数码管

共阳数码管 是指将所有发光二极管的阳极接到一起形成公共阳极(COM)的数码管,共阳数码管在应用时应将公共极 COM 接到+5V,当某一字段发光二极管的阴极为低电平时,相应字段就点亮,当某一字段的阴极为高电平时,相应字段就不亮

共阴数码管 是指将所有发光二极管的阴极接到一起形成公共阴极(COM)的数码管,共阴数码管在应用时应将公共极 COM 接到地线 GND 上,当某一字段发光二极管的阳极为高电平时,相应字段就点亮,当某一字段的阳极为低电平时,相应字段就不亮

数码表

共阴数码管码表

共阳数码管码表

从上述共阳和共阴码表中不难发现,它们的数据正好是相互取反的值。比如共阴数码管数字 0 段码:0x3f,其二进制是:0011 1111,取反后为:1100 0000, 转换成 16 进制即为 0XC0。其他段码依此类推。该段码数据由来,是将 a 段作为最低位,b 段作为次低位,其他按顺序类推,dp 段为最高位,共 8 位,正好和 51 单片机的一组端口数一样,因此可以直接使用某一组端口控制数码管的段选数据口,比如 P0 口

数码管原理图

我们开发板的所用的数码管都是共阳数码管,一共有 6 个

6 个数码管的 com 都是接到了正极上,和 LED 小灯电路一样,也是由 74HC138 控制三极管的导通来控制整个数码管的使能。先来看最右边的 DS1 这个数码管,原理图上可 以看出,控制 DS1 的三极管是 Q17,控制 Q17 的引脚是 LEDS0,对应到 74HC138 上边就是 U3 的 Y0 输出,如图所示:

问:为什么数码管上边有 2 个 com 呢?

一方面是 2 个可以起到对称的效果,刚好是 10 个引脚,另外一个方面,公共端通过的电流较大,并联电路电流之和等于总电流,用2 个 com 可以把公共电流平均到 2 个引脚上去,降低单条线路承受的电流

数码管的 8 个段,我们直接当成 8 个 LED 小灯来控制,那就是 a、b、c、d、e、f、g、dp

数码管显示

静态显示:多位数码管依然可以静态显示,但是显示时要么只显示一位数码管,要么多位同时显示相同内容。当多位数码管应用于某一系统时,它们的“位选”是可独立控制的,而“段选”是连接在一起的,我们可以通过位选信号控制哪几个数码管亮,而在同一时刻,位选选通的所有数码管上显示的数字始终都是一样的,因为它们的段选是连接在一起的,送入所有数码管的段选信号都是相同的,所以它们显示的数字必定一样,数码管的这种显示方法叫做静态显示

动态显示:就是利用减少段选线,分开位选线,利用位选线不同时选择通断,改变段选数据来实现的。比如在第一次选中第一位数码管时,给段选数据 0, 下一次位选中第二位数码管时显示 1。为了在显示 1 的时候,0 不会消失(当然实际上是消失了),必须在人肉眼观察不到的时间里再次点亮第一次点亮的 0。 而这时就需要记住,人的肉眼正常情况下只能分辨变化超过 24ms 间隔的运动。 也就是说,在下一次点亮 0 这个数字的时间差不得大于 24ms。这时就会发现, 数码管点亮是在向右或者向左一位一位点亮,形成了动态效果。如果把间隔时间改长就能直接展现这一现象。

静态数码管程序

最右边的数码管显示0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <reg52.h>
#define LED_SMG_PORT P0 //宏定义LED和数码管的端口为P0
sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

void main()
{
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
ADDR2 = 0; //A2到A0的值为000,则使Y0为低电平,则LEDS0为低电平,使三极管导通,点亮最右边的数码管
ADDR1 = 0; //选择数码管 DS1
ADDR0 = 0;

while(1)
{
LED_SMG_PORT = 0xc0; //点亮数码管段 a b c d e f 段,显示数字0
}
}

最右边的数码管显示0~F

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
#include <reg52.h>
typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16; //对系统默认数据类型进行重命名
#define LED_SMG_PORT P0 //宏定义LED和数码管的端口为P0

sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

u8 code gsmg[] =
{
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
}; //数码管段码0~F

void main()
{
u16 count = 0;//记录TF0溢出次数
u8 sec = 0; //记录经过的秒数
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
ADDR2 = 0; //A2到A0的值为000,则使Y0为低电平,则LEDS0为低电平,使三极管导通,点亮最右边的数码管
ADDR1 = 0; //选择数码管 DS1
ADDR0 = 0;
TMOD |= 0x01; //选择为定时器0模式,工作方式1(为了不干扰T1定时器所以用|);如果是定时器1则把0x01改成0x10
TL0 = 0x66; //给定时器赋初值,定时1ms(使用STC-ISP里面的定时器计算器);如果是定时器1则把TH0改成TH1
TH0 = 0xFC; //如果是定时器1则把TL0改成TL1
TR0 = 1; //定时器0开始计时
while(1)
{
if(TF0 == 1) //如果溢出
{
TF0 = 0; //清除溢出标志位
TL0 = 0x66; //重新赋值
TH0 = 0xFC;
count++; //TF0溢出一次计数次数+1
}
if(count == 1000) //如果count = 1000则代表溢出1000次,1000ms = 1s
{
count = 0; //溢出次数归0
LED_SMG_PORT = gsmg[sec]; //数码管端口输出数码管段码0~F
sec++; //秒数记录自加 1
if(sec >= 16) //当秒数超过 0x0F(15)后,重新从 0 开始
{
sec = 0;
}
}
}
}

另外一种写法

将中断写成一个函数,这样子就不需要清除溢出标志位(TF0 = 0),因为定时器中断服务函数会自动清除标志位,得加上ET0或者ET1 = 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>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16; //对系统默认数据类型进行重命名
#define LED_PORT P0 //宏定义LED和数码管的端口为P0

u8 code gsmg[] =
{
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
}; //数码管段码0~F

sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

void main()
{
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
ADDR2 = 0; //选择数码管 DS1
ADDR1 = 0;
ADDR0 = 0;
EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0XFC; //为 T0 赋初值 0xFC66 ,定时 1ms
TL0 = 0x66;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
while(1);
}

void time0() interrupt 1 //定时器0中断服务函数
{
static u16 count = 0; //记录溢出次数
static u8 sec = 0; //记录经过的秒数
TH0 = 0XFC; //重新加载初值
TL0 = 0x66;
count++; //溢出一次计数次数+1
if(count == 1000) //如果count = 1000则代表溢出1000次,1000ms = 1s
{
count = 0; //溢出次数归0
LED_PORT = gsmg[sec];//数码管端口输出数码管段码0~F
sec++; //秒数记录自加 1
if(sec > 15) //当秒数超过 0x0F(15)后,重新从 0 开始
{
sec = 0;
}
}
}

动态数码管程序

数码管计数,从0到999999

解决数码管抖动的方法就是用中断

中断函数写好后, 每当满足中断条件而触发中断后,系统就会自动来调用中断函数。比如平时一直在主程序 while(1)的循环中执行,假如程序有 100 行,当执 行到 50 行时,定时器溢出了,那么单片机就会立刻跑到中断函数中执行中断程序,中断程序执行完毕后再自动返回到刚才的第 50 行处继续执行下面的程序,这样 就保证了动态显示间隔是固定的 1ms,不会因为程序执行时间不一致的原因导致数码管显示的抖动了

还有一点需要注意:程序应该尽量减少全局变量的使用,能用局部变量代替尽量代替
全局变量在其定义后所有函数都能用,但是静态局部变量只能在一个函数里面用

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#define LED_SMG_PORT P0 //宏定义LED和数码管的端口为P0

sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

u8 code gsmg[] =
{
0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
}; //数码管段码0~F

u8 Led_Buff[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管显示缓冲区,初值 0xFF 确保启动时都不亮

u32 sec = 0; //记录经过的秒数(注意用long(u32),因为999999超出int类型范围)
u8 flag = 0; //1s定时标志

void main()
{
EA = 1; //使能总中断
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
TMOD |= 0x01; //选择为定时器0模式,工作方式1(为了不干扰T1定时器所以用|);如果是定时器1则把0x01改成0x10
TL0 = 0x66; //给定时器赋初值,定时1ms(使用STC-ISP里面的定时器计算器);如果是定时器1则把TH0改成TH1
TH0 = 0xFC; //如果是定时器1则把TL0改成TL1
ET0 = 1; //使能定时器0中断
TR0 = 1; //启动定时器0

while(1)
{
if(flag == 1) //判断1s定时标志
{
flag = 0; //清除中断标志
sec++;
if(sec <= 0) //如果秒数小于0则清0
{
sec = 0;
}
Led_Buff[0] = gsmg[sec % 10]; //个位
Led_Buff[1] = gsmg[sec / 10 % 10]; //十位
Led_Buff[2] = gsmg[sec / 100 % 10]; //百位
Led_Buff[3] = gsmg[sec / 1000 % 10]; //千位
Led_Buff[4] = gsmg[sec / 10000 % 10]; //万位
Led_Buff[5] = gsmg[sec / 100000 % 10]; //十万位
}
}
}

void Time0() interrupt 1 //定时器0中断服务函数
{
static u16 count = 0;//记录TF0溢出次数(注意不要使用u8类型否则数码管不会亮) 使用静态变量static,使计数次数不会清0
static u8 i = 0; //动态扫描的索引(同样使用静态变量)
TL0 = 0x66; //重新加载初值
TH0 = 0xFC; //重新加载初值
count++;
if(count == 1000) //中断1000次相当于 1000*1ms=1s
{
count = 0; //溢出计数清0;
flag = 1; //设置中断标志位
}

//只显示有效位
if(sec <= 9)
{
Led_Buff[1] = 0xFF;
Led_Buff[2] = 0xFF;
Led_Buff[3] = 0xFF;
Led_Buff[4] = 0xFF;
Led_Buff[5] = 0xFF;
}
if(sec <= 99)
{
Led_Buff[2] = 0xFF;
Led_Buff[3] = 0xFF;
Led_Buff[4] = 0xFF;
Led_Buff[5] = 0xFF;
}
if(sec <= 999)
{
Led_Buff[3] = 0xFF;
Led_Buff[4] = 0xFF;
Led_Buff[5] = 0xFF;
}
if(sec <= 9999)
{
Led_Buff[4] = 0xFF;
Led_Buff[5] = 0xFF;
}
if(sec <= 99999)
{
Led_Buff[5] = 0xFF;
}
//以下代码完成数码管动态扫描刷新
LED_SMG_PORT = 0xFF; //消影
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[3];
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[4];
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i = 0;
LED_SMG_PORT = Led_Buff[5];
break;
default:
break;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//switch语句那部分可以拿下面的代替,更加简洁
//以下代码完成数码管动态扫描刷新

SMG_PORT = 0xFF; //显示消影
P1 = (P1 & 0xF8) | i; //0xF8:1111 1000,低3位对应ADDR0~ADDR2,跟i又是对应关系
//先将P1的第三位取0(低三位与0进行与运算),然后再与i进行或运算,如果i为1,P1第三位(也就是ADDR2-ADDR0)为001,以此类推
LED_SMG_PORT = LedBuff[i];
if(i < 5)
i++;
else
i = 0;

使用定时器做一个时钟

smg.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <reg52.h>
#include "smg.h"
#define LED_SMG_PORT P0
u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 Led_Buff[6] = {0XFF, 0XFF, 0XFF, 0XFF, 0XFF, 0XFF}; //数码管缓存区,初值0xFF,确保启动时都不亮
void smg_display()
{
static u8 i = 0; //动态扫描的索引
//以下代码完成数码管动态刷新
SMG_PORT = 0xFF; //消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[3];
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[4];
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i = 0;
LED_SMG_PORT = Led_Buff[5];
break;
default:
break;
}
}

smg.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _SMG_H
#define _SMG_H

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
#define SMG_PORT P0 //宏定义数码管P0端口
sbit ADDR0 = P1 ^ 0;
sbit ADDR1 = P1 ^ 1;
sbit ADDR2 = P1 ^ 2;
sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;

extern u8 gsmg[10]; //前面加extern让函数可以在外部使用
extern u8 Led_Buff[6];
void smg_display();
#endif

time.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <reg52.h>
#include "time.h"
/* -------------------------------- begin ------------------------------ */
/**
* @函数名: Time0_Init
* @功能 : 定时器0中断配置函数,通过设置 TH 和 TL 即可确定定时时间(使用工具可确定TH 与TL的值)
* @返回值: 无 晶振频率为11.0592
**/
/* -------------------------------- end -------------------------------- */
void Time0_Init()
{
EA = 1; //开启总中断
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //定时器模式1
TH0 = 0XFC; //定时1毫秒
TL0 = 0x66;
ET0 = 1; //使能定时器0中断
TR0 = 1; //启动定时器0
}

time.h

1
2
3
4
5
6
#ifndef _TIME_H
#define _TIME_H

void Time0_Init();//函数声明

#endif

main.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
42
43
44
45
46
47
48
49
50
51
52
53
#include <reg52.h>
#include "time.h"
#include "smg.h"
u8 flag = 0; //标志位
u8 hour, min, sec; //时分秒
void main()
{
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
Time0_Init(); //调用定时器初始化函数
while(1)
{
if(flag == 1) //判断1s定时标志
{
flag = 0; //清除中断标志
sec++;
if(sec >= 60)
{
sec = 0;
min++;
if(min >= 60)
{
min = 0;
hour++;
if(hour >= 24)
{
hour = 0;
}
}
}
}
Led_Buff[0] = gsmg[sec % 10]; //秒个位
Led_Buff[1] = gsmg[sec / 10]; //秒十位
Led_Buff[2] = gsmg[min % 10]; //分个位
Led_Buff[3] = gsmg[min / 10]; //分十位
Led_Buff[4] = gsmg[hour % 10];//小时个位
Led_Buff[5] = gsmg[hour / 10];//时十位
}
}

void time0() interrupt 1 //定时器0中断服务函数
{
static u16 count = 0; //记录T0中断次数
TH0 = 0XFC; //重载初值
TL0 = 0x66;
count++; //中断计数+1
if(count == 1000) //中断1000次相当于1秒 (1000ms = 1s)
{
count = 0; //溢出中断清0
flag = 1; //设置中断标志位
}
smg_display();
}

使用定时器做一个秒表

第一个数码管显示毫秒的十位,第二个数码管显示百位,第三数码管显示秒的个位,第四个数码管显示秒的十位

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
#include <reg52.h>
typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
#define SMG_PORT P0 //宏定义数码管端口
sbit ADDR0 = P1 ^ 0;
sbit ADDR1 = P1 ^ 1;
sbit ADDR2 = P1 ^ 2;
sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;

u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 Led_Buff[4] = {0xFF, 0xFF, 0xFF, 0xFF}; //数码管缓存区,初值0xFF
u8 flag = 0; //标志位
u8 sec = 0; //秒
u16 msec = 0; //毫秒
void main()
{
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
EA = 1; //开启总中断
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //定时器模式1
TH0 = 0xFC; //定时1毫秒
TL0 = 0x66;
ET0 = 1; //使能定时器0中断
TR0 = 1; //启动定时器0
while(1)
{
if(flag == 1) //判断1ms定时标志
{
flag = 0; //清除中断标志
msec++;
if(msec == 1000)
{
msec = 0;
sec++;
if(sec >= 60)
{
sec = 0;
}
}
}
Led_Buff[0] = gsmg[msec / 10 % 10]; //毫秒十位
Led_Buff[1] = gsmg[msec / 100 % 10]; //毫秒百位
Led_Buff[2] = gsmg[sec % 10]; //秒个位
Led_Buff[3] = gsmg[sec / 10]; //秒十位
}
}

/*定时器0中断服务函数*/
void time0() interrupt 1
{
static u8 i = 0; //动态扫描的索引
TH0 = 0xFC;
TL0 = 0x66;
flag = 1;
//以下代码完成数码管动态刷新
SMG_PORT = 0xFF; //消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
SMG_PORT = Led_Buff[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
SMG_PORT = Led_Buff[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
SMG_PORT = Led_Buff[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i = 0;
SMG_PORT = Led_Buff[3];
break;
default:
break;
}
}

使用定时器做一个秒表倒计时

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
136
137
138
139
140
141
142
143
144
145
#include <reg52.h>
typedef unsigned char u8; //对系统默认数据类型进行重命名
#define LED_SMG_PORT P0 //宏定义数码管端口

u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 Led_Buff[6] = {0XFF, 0XFF, 0XFF, 0XFF, 0XFF, 0XFF}; //数码管缓存区,初值0xFF,确保启动时都不亮

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

u8 flag = 0; //定义标志位

void Time0_Init();
void SMG_Show();
void SMG_Scan();

void main()
{
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
Time0_Init();
while(1)
{
if(1 == flag) //判断定时标志
{
flag = 0; //清除中断标志
SMG_Show(); //调用数码管显示函数
}
}
}

void Time0_Init()
{
EA = 1; //开启总中断
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x02; //定时器0模式2
TH0 = 0xA4; //100us
TL0 = 0xA4;
ET0 = 1; //使能定时器0中断
TR0 = 1; //启动定时器0
}

void SMG_Show() //数码管显示函数
{
static u8 min = 1; //定义初始值
static u8 sec = 30;
static u8 msec = 30;

Led_Buff[0] = gsmg[msec % 10]; //毫秒个位
Led_Buff[1] = gsmg[msec / 10]; //毫秒十位
Led_Buff[2] = gsmg[sec % 10]; //秒个位
Led_Buff[3] = gsmg[sec / 10]; //秒十位
Led_Buff[4] = gsmg[min % 10]; //分个位
Led_Buff[5] = gsmg[min / 10]; //分十位

if(msec > 0) //如果毫秒大于0
{
msec--; //毫秒-1
if((msec == 0) && (sec > 0)) //在秒大于0的情况下毫秒才能在等于0的时候重新赋值
{
msec = 99;
sec--; //秒-1
if((sec == 0) && (min > 0)) //在分钟大于0的情况下秒才能在等于0的时候重新赋值
{
sec = 59;
min--;
if(min == 0) //分=0
{
min = 0; //清零
}
}
}
}
}

void SMG_Scan()
{
static u8 i = 0; //动态扫描的索引
//以下代码完成数码管动态刷新
LED_SMG_PORT = 0xFF; //消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[3];
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[4];
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i = 0;
LED_SMG_PORT = Led_Buff[5];
break;
default:
break;
}
}

void time0() interrupt 1
{
static u8 count = 0; //记录中断次数
TH0 = 0xA4; //重载初值
TL0 = 0xA4;
count++; //中断计数+1
if(count > 100)
{
count = 0; //溢出中断清0
flag = 1; //设置中断标志位
}
SMG_Scan(); //数码管扫描函数
}

数码管显示时钟,设置LED左右流水

smg.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
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
#include "AllHead.h"
u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 Led_Buff[7] = {0XFF, 0XFF, 0XFF, 0XFF, 0XFF, 0XFF, 0XFF}; //数码管缓存区,初值0xFF,确保启动时都不亮

/*时钟计数函数*/
void smg_display()
{
static u8 i = 0; //动态扫描的索引
//以下代码完成数码管动态刷新
LED_SMG_PORT = 0xFF; //消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[3];
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = Led_Buff[4];
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_SMG_PORT = Led_Buff[5];
break;
case 6:
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;
i = 0;
LED_SMG_PORT = Led_Buff[6];
break; //点亮LED需要使能LEDS6
default:
break;
}
}

/*动态数码管显示函数*/
void smg_show()
{
static u8 hour, min, sec; //时分秒
Led_Buff[0] = gsmg[sec % 10]; //秒个位
Led_Buff[1] = gsmg[sec / 10]; //秒十位
Led_Buff[2] = gsmg[min % 10]; //分个位
Led_Buff[3] = gsmg[min / 10]; //分十位
Led_Buff[4] = gsmg[hour % 10];//小时个位
Led_Buff[5] = gsmg[hour / 10];//时十位
sec++;
if(sec >= 60)
{
sec = 0;
min++;
if(min >= 60)
{
min = 0;
hour++;
if(hour >= 24)
{
hour = 0;
}
}
}
}

smg.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef _SMG_H
#define _SMG_H

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
#define LED_SMG_PORT P0 //宏定义数码管P0端口

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

extern u8 gsmg[10]; //前面加extern让函数可以在外部使用
extern u8 Led_Buff[7];
void smg_display();
void smg_show();

#endif

LED.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
#include "AllHead.h"
void LED_Contral(u8 dir)
{
if(0 == dir) //左移 如果遇到==符号,要写成常量==变量,防止漏写了一个=,这样子容易找出bug(因为编译会报错)
{
static u8 Led_state = 0x01; //LED状态
//注意这里要把LED的状态变量赋给Led_Buff[6],因为数码管和LED共用端口,所以这里不能直接赋给P0
Led_Buff[6] = ~Led_state; //取反赋给数组里的第七个元素(也就是使能LEDS6,才可以点亮LED)
Led_state <<= 1; //左移一位
if(Led_state == 0)
{
Led_state = 0x01; //左移到最高位后重新设置为第一个LED灯
}
}
if(1 == dir) //右移
{
static u8 Led_state = 0x80;
Led_Buff[6] = ~Led_state; //取反赋给数组里的第七个元素(也就是使能LEDS6,才可以点亮LED)
Led_state >>= 1; //右移一位
if(Led_state == 0)
{
Led_state = 0x80; //右移到最低位后重新设置为第八个LED灯
}
}
}

LED.h

1
2
3
4
5
6
#ifndef _LED_H
#define _LED_H

void LED_Contral(u8 dir); //函数调用

#endif

time.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <reg52.h>
#include "time.h"
/* -------------------------------- begin ------------------------------ */
/**
* @函数名: Time0_Init
* @功能 : 定时器0中断配置函数,通过设置 TH 和 TL 即可确定定时时间(使用工具可确定TH 与TL的值)
* @返回值: 无 晶振频率为11.0592
**/
/* -------------------------------- end -------------------------------- */
void Time0_Init()
{
EA = 1; //开启总中断
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //定时器模式1
TH0 = 0XFC; //定时1毫秒
TL0 = 0x66;
ET0 = 1; //使能定时器0中断
TR0 = 1; //启动定时器0
}

time.h

1
2
3
4
5
6
#ifndef _TIME_H
#define _TIME_H

void Time0_Init();//函数声明

#endif

main.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
42
43
44
45
#include "AllHead.h"
u8 flag1 = 0; //标志位
u8 flag2 = 0; //标志位
u8 hour, min, sec; //时分秒
void main()
{
ENLED = 0; //E1 E2低电平有效
ADDR3 = 1; //G1(E3) 使能管脚高电平
Time0_Init(); //调用定时器初始化函数
while(1)
{
if(flag1 == 1) //判断1s定时标志
{
flag1 = 0; //清除中断标志
smg_show(); //时钟计数函数
}
if(flag2 == 1)
{
flag2 = 0;
LED_Contral(1); //LED流水灯函数(0代表左移,1代表右移)
}
}
}

void time0() interrupt 1 //定时器0中断服务函数
{
static u16 count1 = 0; //记录T0中断次数
static u16 count2 = 0; //记录T0中断次数
TH0 = 0XFC; //重载初值
TL0 = 0x66;
count1++; //中断计数+1
if(count1 == 1000) //中断1000次相当于1秒 (1000ms = 1s)
{
count1 = 0; //溢出中断清0
flag1 = 1; //设置中断标志位
}
count2++;
if(count2 == 1000)
{
count2 = 0;
flag2 = 1;
}
smg_display(); //动态数码管显示函数
}

AllHead.h

1
2
3
4
5
6
7
8
9
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

#include <reg52.h>
#include "time.h"
#include "smg.h"
#include "LED.h"

#endif

红绿灯

左边 LED8 和 LED9 一起亮作为绿灯,中间 LED5 和 LED6 一起亮作为黄灯,右边 LED2 和 LED3 一起亮作为红灯,用数码管的低 2 位做倒计时,让 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
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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;
#define LED_SMG_PORT P0 //宏定义数码管端口

sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;

u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 SMG_BUFF[7] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管+独立LED显示缓冲区,初值0xFF

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节
bit flag = 1; //1 秒定时标志

void Smg_Scan(); //函数声明
void Config_Time0(u16 ms);
void Traffic_Refresh();

void main()
{
ENLED = 0; //使能数码管和 LED
ADDR3 = 1;
Config_Time0(1); //配置 T0 定时 1ms
while(1)
{
if(flag) //每秒执行一次交通灯刷新
{
flag = 0;
Traffic_Refresh(); //调用交通灯显示刷新函数
}
}
}

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

/* 交通灯显示刷新函数 */
void Traffic_Refresh()
{
static u8 color = 2; //颜色索引:0-绿色 1-黄色 2-红色
static u8 time = 0; //倒计时定时器
if(0 == time) //倒计时到 0 时,切换交通灯
{
switch(color) //LED8/9 代表绿灯,LED5/6 代表黄灯,LED2/3 代表红灯
{
case 0:
color = 1;
time = 5;
SMG_BUFF[6] = 0xE7;
break; //切换到黄色,亮5秒,LED5、6亮(1110 0111 --> 0xE7)
case 1:
color = 2;
time = 30;
SMG_BUFF[6] = 0xFC;
break; //切换到红色,亮30秒,LED2、3亮(1111 1100 --> 0xFC)
case 2:
color = 0;
time = 15;
SMG_BUFF[6] = 0x3F;
break; //切换到绿色,亮15秒,LED8、9亮(0011 1111 --> 0x3F)
default:
break;
}
}
else //倒计时未到 0 时,递减其计数值
time--;
SMG_BUFF[0] = gsmg[time % 10]; //倒计时数值个位显示
SMG_BUFF[1] = gsmg[time / 10]; //倒计时数值十位显示
}

/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void Smg_Scan()
{
static u8 i; //动态扫描索引
LED_SMG_PORT = 0xFF; //关闭所有段选位,显示消隐
//先将P1的第三位取0(低三位与0进行与运算),然后再与i进行或运算,如果i为1,P1第三位(也就是ADDR2-ADDR0)为001,以此类推
P1 = (P1 & 0xF8) | i; //位选索引值赋值到P1口低3位
LED_SMG_PORT = SMG_BUFF[i]; //缓冲区中索引位置的数据送到数码管端口
if(i < 6) //索引递增循环,遍历整个缓冲区,由于加了LED显示,点亮LED需要使能LEDS6,所以i小于6不是小于5
i++;
else
i = 0;
}

/* T0 中断服务函数,完成 LED 扫描和秒定时 */
void time0() interrupt 1
{
static u16 count = 0; //1 秒定时计数
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
Smg_Scan(); //LED 扫描显示
count++;
if(count >= 1000) //1秒
{
count = 0;
flag = 1; //1秒定时标志置1
}
}

点阵

点阵 LED 显示屏作为一种现代电子媒体,具有灵活的显示面积(可任意分割和拼装)、 高亮度、长寿命、数字化、实时性等特点,应用非常广泛,一个 8*8 的点阵就是由 64 个 LED 小灯组成

点阵 LED 最小单元如图所示:

LED点阵原理图

在图中大方框外侧的就是 点阵 LED 的引脚号,左侧的 8 个引脚是接的内部 LED 的阳极上侧的 8 个引脚接的是内部 LED 的阴极。那么如果我们把 9 脚置成高电平、13 脚置成低电平的话,左上角的那个 LED 小灯就会亮了。特别注意,控制点阵左侧引脚的 74HC138 是原理图上的 U4,8 个引脚自上而下依次由 U4 的 Y0~Y7 输出来控制

74HC138原理图

由于ENLED接的是5号管脚,低电平使能ADDR3接在4号管脚,同样也是低电平使能,所以要驱动U4的话,得让ENLED和ADDR3都为0,这样子芯片才能正常工作,这样子不会使U3驱动,所以各个功能模块互不打扰

取字模软件

双击打开该软件,首先选择“基本操作->新建图像”,设置图像的宽度和高度为8,点击确定后将在显示窗口出现一个8x8的白色格子,这个就类似于8x8LED 点阵,具体操作如下

可以看到上图 8*8 点阵区域非常小,我们可以将其放大,选择“模拟动画”, 后点击“放大格点”,
如下所示

由于取模软件是把黑色取为 1,白色取为 0,但我们点阵是 1 对应 LED 熄灭,0 对应 LED 点亮,所以我们要选“修改图像”菜单里的“黑白反显图像”这个选项,再点击“基本操作”菜单里边的“保存图像”可以把我们设计好的图片进行保存

然后设置取模数据的取模方式等内容,选择“参数设置”后点击“其他 选项”,具体操作如下:

这里的选项,要结合原理图,可以看到 P0 口控制的是一行,所以用“横向取模”,如果控制的是一列,就要选“纵向取模”。选中“字节倒序”这个选项,是因为原理图左边是低位 DB0,右边是高位 DB7,所以是字节倒序,点确定后,选择“取模方式”这个菜单,点一下“C51 格式”后,在“点阵生成区”自动产生了 8 个字节的数据,这 8 个字节的数据就是取出来的“模”,如图所示

在这个图片里,黑色的一个格子表示一位二进制的 1白色的一个格子表示一位二进制的 0。第一个字节是0xFF,其实就是这个 8*8 图形的第一行,全黑就是 0xFF;第二个字节是 0x99,低位在左边,高位在右边

程序

点亮一个点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <reg52.h>
sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E1
sbit ENLED = P1 ^ 4; //E2
sbit LED = P0 ^ 0;

void main()
{
ENLED = 0;
ADDR3 = 0; //使能U4使之正常输出
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
LED = 0; //向P0.0写入0来点亮左上角的一个点
while(1);
}

点亮一行

1
//直接操作 P0=0x00 即可

一个数码管就是 8 个 LED 小灯,一个点阵是 64 个 LED 小灯。同样的道理,我们还可以把一个点阵理解成是 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
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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
#define LED_PORT P0 //宏定义LED和数码管的端口为P0

sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E1
sbit ENLED = P1 ^ 4; //E2

void main()
{
ENLED = 0; //使能 U4,选择 LED 点阵
ADDR3 = 0; //使能U4使之正常输出
EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0XFC; //为 T0 赋初值 0xFC67,定时 1ms
TL0 = 0x66;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
while(1); //程序停在这里,等待定时器中断
}

/* 定时器 0 中断服务函数 */
void Time0() interrupt 1
{
static u8 i = 0; //动态扫描的索引
TH0 = 0XFC; //重新加载初值
TL0 = 0x66;
//以下代码完成 LED 点阵动态扫描刷新
LED_PORT = 0xFF; //显示消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_PORT = 0x00;
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_PORT = 0x00;
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_PORT = 0x00;
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_PORT = 0x00;
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_PORT = 0x00;
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_PORT = 0x00;
break;
case 6:
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_PORT = 0x00;
break;
case 7:
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 1;
i = 0;
LED_PORT = 0x00;
break;
default:
break;
}
}

显示爱心

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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
#define LED_PORT P0 //宏定义LED和数码管的端口为P0
u8 code image[] = {0xFF, 0x99, 0x00, 0x00, 0x00, 0x81, 0xC3, 0xE7}; //显示爱心的字模表

sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E1
sbit ENLED = P1 ^ 4; //E2

void main()
{
ENLED = 0; //使能 U4,选择 LED 点阵
ADDR3 = 0; //使能U4使之正常输出
EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0XFC; //为 T0 赋初值 0xFC66 ,定时 1ms
TL0 = 0x66;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
while(1); //程序停在这里,等待定时器中断
}

/* 定时器 0 中断服务函数 */
void Time0() interrupt 1
{
static u8 i = 0; //动态扫描的索引
TH0 = 0XFC; //重新加载初值
TL0 = 0x66;
//以下代码完成 LED 点阵动态扫描刷新
LED_PORT = 0xFF; //显示消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_PORT = image[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_PORT = image[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_PORT = image[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_PORT = image[3];
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_PORT = image[4];
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_PORT = image[5];
break;
case 6:
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_PORT = image[6];
break;
case 7:
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 1;
i = 0;
LED_PORT = image[7];
break;
default:
break;
}
}

点阵的动画显示

点阵的动画显示,就是对多张图片分别进行取模,使用程序算法巧妙的切换图片, 多张图片组合起来就成了一段动画了,我们所看到的动画片、游戏等等,它们的基本原理也都是这样的

点阵的纵向移动(向上移动)

我们要让点阵显示一个 I ❤ U 的动画,首先我们要把这个图形用取模软件画出来,如图所示:

这张图片共有 40 行,每 8 行组成一张点阵图片,并且每向上移动一行就出现了一张新图片,一共组成了 32 张图片

用一个变量 index 来代表每张图片的起始位置,每次从 index 起始向下数 8 行代表了当前的图片250ms 改变一张图片,然后不停的动态刷新,这样图片就变成动画了。首先我们要对显示的图片进行横向取模,虽然这是 32 张图片,由于我们每一张图片都是和下一行连续的,所以实际的取模值只需要 40 个字节就可以完成

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
//向上移动是默认的,刷新索引是++的
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
#define LED_PORT P0 //宏定义LED和数码管的端口为P0
u8 code image[] =
{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xC3, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xC3, 0xFF,
0x99, 0x00, 0x00, 0x00, 0x81, 0xC3, 0xE7, 0xFF,
0x99, 0x99, 0x99, 0x99, 0x99, 0x81, 0xC3, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
}; //图片的字模表

sbit ADDR0 = P1 ^ 0; //A0
sbit ADDR1 = P1 ^ 1; //A1
sbit ADDR2 = P1 ^ 2; //A2
sbit ADDR3 = P1 ^ 3; //E3
sbit ENLED = P1 ^ 4; //E1 + E2

void main()
{
ENLED = 0; //使能 U4,选择 LED 点阵
ADDR3 = 0; //使能U4使之正常输出
EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0XFC; //为 T0 赋初值 0xFC66 ,定时 1ms
TL0 = 0x66;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
while(1); //程序停在这里,等待定时器中断
}

/* 定时器 0 中断服务函数 */
void Time0() interrupt 1
{
static u8 i = 0; //动态扫描的索引
static u8 tmr = 0; //250ms软件定时器
static u8 index = 0; //图片刷新索引

TH0 = 0XFC; //重新加载初值
TL0 = 0x66;
//以下代码完成 LED 点阵动态扫描刷新
LED_PORT = 0xFF; //显示消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_PORT = image[index + 0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_PORT = image[index + 1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_PORT = image[index + 2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_PORT = image[index + 3];
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_PORT = image[index + 4];
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_PORT = image[index + 5];
break;
case 6:
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_PORT = image[index + 6];
break;
case 7:
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 1;
i = 0;
LED_PORT = image[index + 7];
break;
default:
break;
}
//以下代码完成每250ms改变一帧图像
tmr++;
if(tmr >= 250) //达到250ms时改变一次图片索引
{
tmr = 0;
index++;
if(index >= 32) //图片索引达到32后归零
{
index = 0;
}
}
}

向下移动(纵向)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//向下移的话只需改数组顺序即可(即U放第一I放最后中间不用动)
//然后使图片刷新索引--即可
unsigned char code image[] = { //图片的字模表
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0x99,0x99,0x99,0x99,0x99,0x81,0xC3,0xFF,
0x99,0x00,0x00,0x00,0x81,0xC3,0xE7,0xFF,
0xC3,0xE7,0xE7,0xE7,0xE7,0xE7,0xC3,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
};

static unsigned char index = 31; //图片刷新索引
if (tmr >= 250) //达到250ms时改变一次图片索引
{
tmr = 0;
if (index == 0) //图片索引31~0递减循环
index = 31;
else
index--; //注意这里要判断了再--防止内存溢出
}

向左移动(横向)

我们在进行硬件电路设计的时候,也得充分考虑软件编程的方便性。因为我们的程序是用 P0 来控制点阵的整行,所以对于我们这样的电路设计,上下移动程序是比较好编写的。那如果我们设计电路的时候知道我们的图形要左右移动,那我们设计电路画板子的时候就要尽可能的把点阵横过来放,有利于我们编程方便,减少软件工作量

利用二维数组实现

最上面的图形是横向连在一起的效果,而实际上我们要把它分解为 30 个帧, 每帧图片单独取模,取出来都是 8 个字节的数据,一共就是 30*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
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
# include <reg52.h>
typedef unsigned char u8; //对系统默认数据类型进行重命名
#define LED_PORT P0 //宏定义LED和数码管的端口为P0

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

u8 code image[30][8] = {
{0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}, //动画帧1
{0xFF,0x7F,0xFF,0xFF,0xFF,0xFF,0xFF,0x7F}, //动画帧2
{0xFF,0x3F,0x7F,0x7F,0x7F,0x7F,0x7F,0x3F}, //动画帧3
{0xFF,0x1F,0x3F,0x3F,0x3F,0x3F,0x3F,0x1F}, //动画帧4
{0xFF,0x0F,0x9F,0x9F,0x9F,0x9F,0x9F,0x0F}, //动画帧5
{0xFF,0x87,0xCF,0xCF,0xCF,0xCF,0xCF,0x87}, //动画帧6
{0xFF,0xC3,0xE7,0xE7,0xE7,0xE7,0xE7,0xC3}, //动画帧7
{0xFF,0xE1,0x73,0x73,0x73,0xF3,0xF3,0xE1}, //动画帧8
{0xFF,0x70,0x39,0x39,0x39,0x79,0xF9,0xF0}, //动画帧9
{0xFF,0x38,0x1C,0x1C,0x1C,0x3C,0x7C,0xF8}, //动画帧10
{0xFF,0x9C,0x0E,0x0E,0x0E,0x1E,0x3E,0x7C}, //动画帧11
{0xFF,0xCE,0x07,0x07,0x07,0x0F,0x1F,0x3E}, //动画帧12
{0xFF,0x67,0x03,0x03,0x03,0x07,0x0F,0x9F}, //动画帧13
{0xFF,0x33,0x01,0x01,0x01,0x03,0x87,0xCF}, //动画帧14
{0xFF,0x99,0x00,0x00,0x00,0x81,0xC3,0xE7}, //动画帧15
{0xFF,0xCC,0x80,0x80,0x80,0xC0,0xE1,0xF3}, //动画帧16
{0xFF,0xE6,0xC0,0xC0,0xC0,0xE0,0xF0,0xF9}, //动画帧17
{0xFF,0x73,0x60,0x60,0x60,0x70,0x78,0xFC}, //动画帧18
{0xFF,0x39,0x30,0x30,0x30,0x38,0x3C,0x7E}, //动画帧19
{0xFF,0x9C,0x98,0x98,0x98,0x9C,0x1E,0x3F}, //动画帧20
{0xFF,0xCE,0xCC,0xCC,0xCC,0xCE,0x0F,0x1F}, //动画帧21
{0xFF,0x67,0x66,0x66,0x66,0x67,0x07,0x0F}, //动画帧22
{0xFF,0x33,0x33,0x33,0x33,0x33,0x03,0x87}, //动画帧23
{0xFF,0x99,0x99,0x99,0x99,0x99,0x81,0xC3}, //动画帧24
{0xFF,0xCC,0xCC,0xCC,0xCC,0xCC,0xC0,0xE1}, //动画帧25
{0xFF,0xE6,0xE6,0xE6,0xE6,0xE6,0xE0,0xF0}, //动画帧26
{0xFF,0xF3,0xF3,0xF3,0xF3,0xF3,0xF0,0xF8}, //动画帧27
{0xFF,0xF9,0xF9,0xF9,0xF9,0xF9,0xF8,0xFC}, //动画帧28
{0xFF,0xFC,0xFC,0xFC,0xFC,0xFC,0xFC,0xFE}, //动画帧29
{0xFF,0xFE,0xFE,0xFE,0xFE,0xFE,0xFE,0xFF} //动画帧30
};

void main()
{
EA = 1; //使能总中断
ENLED = 0; //使能U4,选择LED点阵
ADDR3 = 0;
TMOD = 0x01; //设置T0为模式1
TH0 = 0xFC; //为T0赋初值0xFC67,定时1ms
TL0 = 0x66;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
while (1);
}
/* 定时器0中断服务函数 */
void InterruptTimer0() interrupt 1
{
static u8 i = 0; //动态扫描的索引
static u8 tmr = 0; //250ms软件定时器
static u8 index = 0; //图片刷新索引

TH0 = 0xFC; //重新加载初值
TL0 = 0x66;
//以下代码完成LED点阵动态扫描刷新
LED_PORT = 0xFF; //显示消隐
switch (i)
{
case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; LED_PORT=image[index][0]; break;
case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; LED_PORT=image[index][1]; break;
case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; LED_PORT=image[index][2]; break;
case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; LED_PORT=image[index][3]; break;
case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; LED_PORT=image[index][4]; break;
case 5: ADDR2=1; ADDR1=0; ADDR0=1; i++; LED_PORT=image[index][5]; break;
case 6: ADDR2=1; ADDR1=1; ADDR0=0; i++; LED_PORT=image[index][6]; break;
case 7: ADDR2=1; ADDR1=1; ADDR0=1; i=0; LED_PORT=image[index][7]; break;
default: break;
}
//以下代码完成每250ms改变一帧图像
tmr++;
if (tmr >= 250) //达到250ms时改变一次图片索引
{
tmr = 0;
index++;
if (index >= 30) //图片索引达到30后归零
{
index = 0;
}
}
}

向右移动(横向)

1
代码跟左移一样,只需把模的数组生成时把 "字节倒序" 关闭了重新生成即可

0~9的模

1
2
3
4
5
6
7
8
9
10
11
12
13
    unsigned char code image[11][8] = {
{0xC3, 0x81, 0x99, 0x99, 0x99, 0x99, 0x81, 0xC3}, //数字0
{0xEF, 0xE7, 0xE3, 0xE7, 0xE7, 0xE7, 0xE7, 0xC3}, //数字1
{0xC3, 0x81, 0x9D, 0x87, 0xC3, 0xF9, 0xC1, 0x81}, //数字2
{0xC3, 0x81, 0x9D, 0xC7, 0xC7, 0x9D, 0x81, 0xC3}, //数字3
{0xCF, 0xC7, 0xC3, 0xC9, 0xC9, 0x81, 0xCF, 0xCF}, //数字4
{0x81, 0xC1, 0xF9, 0xC3, 0x87, 0x9D, 0x81, 0xC3}, //数字5
{0xC3, 0x81, 0xF9, 0xC1, 0x81, 0x99, 0x81, 0xC3}, //数字6
{0x81, 0x81, 0x9F, 0xCF, 0xCF, 0xE7, 0xE7, 0xE7}, //数字7
{0xC3, 0x81, 0x99, 0xC3, 0xC3, 0x99, 0x81, 0xC3}, //数字8
{0xC3, 0x81, 0x99, 0x81, 0x83, 0x9F, 0x83, 0xC1}, //数字9
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, //全亮
};

按键

常用的按键电路有两种形式,独立式按键矩阵式按键

独立按键

原理图

当按键 K1 按下时,+5V 通过电阻 R1 然后再通过按键 K1 最终进入 GND 形成一条通路,那么这条线路的全部电压都加到了 R1 这个电阻上, KeyIn1 这个引脚就是个低电平。当松开按键后,线路断开,就不会有电流通过,那么 KeyIn1 和+5V 就应该是等电位,是一个高电平。我们就可以通过 KeyIn1 这个 IO 口的高低电平来判断是否有按键按下。低电平代表按键按下,高电平代表按键弹起

按键是接到了 P2 口上,P2 口上电默认是准双向 IO 口\

准双向 IO 口的电路,如图所示:

我们要读取外部按键信号的时候,单片机必须先给该引脚写“1”,也就是高电平,这样我们才能正确读取到外部按键信号,这种具有上拉的准双向 IO 口,如果要正常读取外部信号的状态,必须首先得保证自己内部输出的是 1,如果内部输出 0,则无论外部信号是 1 还是 0,这个引脚读进来的都是 0

矩阵按键

如果需要使用很多的按键时,做成独立按键会大量占用 IO 口, 所以一般都会引入矩阵按键

原理图

如果 KeyOut1 输出一个低电平,KeyOut1 就相当于是 GND,这时候 KeyOut2、 KeyOut3、KeyOut4 都必须输出高电平它们都输出高电平才能保证与它们相连的三路按键不会对这一路产生干扰

按键消抖

通常按键所用的开关都是机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定的接通,在断开时也不会一下子彻底断开, 而是在闭合和断开的瞬间伴随了一连串的抖动

抖动时间是由按键的机械特性决定的,一般都会在 10ms 以内,为了确保程序对按键的一次闭合或者一次断开只响应一次,必须进行按键的消抖处理。 当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或断开稳定后再进行处理。 按键消抖可分为硬件消抖软件消抖。在绝大多数情况下,我们是用软件即程序来实现消抖的。最简单的消抖原理,就是当检测到按键状态变化后,先等待一个 10ms 左右的延时时间,让抖动消失后再进行一次按键状 检测,如果与刚才检测到的状态相同,就可以确认按键已经稳定的动作

程序

按键按一次,数码管数值+1(使用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
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
#include <reg52.h>
#include "Delay.h"

#define LED_SMG_PORT P0 //宏定义LED与数码管端口
#define KEY_PORT P2 //宏定义按键端口

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

sbit KeyIn1 = P2 ^ 4;
sbit KeyIn2 = P2 ^ 5;
sbit KeyIn3 = P2 ^ 6;
sbit KeyIn4 = P2 ^ 7;


u8 code gsmg[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
}; //数码管显示字符转换表

void main()
{
bit backup = 1; //按键值备份,保存前一次的扫描值(bit 型是 1 位数据,51单片机特殊的变量类型)
bit keybuf = 1; //按键值暂存,临时保存按键的扫描值
u8 count = 0; //按键计数,记录按键按下的次数

ENLED = 0; //选择数码管 DS1 进行显示
ADDR3 = 1;
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
KEY_PORT = 0xF7; //P2.3 置 0,即 KeyOut1 输出低电平
LED_SMG_PORT = gsmg[count]; //显示按键次数初值
while(1)
{
keybuf = KeyIn1; //把当前扫描值暂存
if(keybuf != backup) //当前值与前次值不相等说明此时按键有动作
{
Delay_ms(10); //延时大约 10ms,按键消抖
if(keybuf == KeyIn1) //判断扫描值有没有发生改变,即按键抖动
{
if(backup == 0) //如果前次值为 0,则说明当前是弹起动作
{
count++; //按键次数+1
if(count == 10) //只用 1 个数码管显示,所以加到 10 就清零重新开始
{
count = 0;

}
LED_SMG_PORT = gsmg[count]; //计数值显示到数码管上
}
backup = keybuf; //更新备份为当前值,以备进行下次比较
}
}
}
}

实际做项目开发的时候,程序量往往很大,各种状态值也很多,如果使用Delay的话,会影响主函数的运行,所以我们可以采用定时器中断进行按键消抖

我们启用一个定时中断,每 2ms 进一次中断,扫描一次按键状态并且存储起来,连续扫描 8 次后,看看这连续 8 次的按键状态是否是一致的。 8 次按键的时间大概是 16ms,这 16ms 内如果按键状态一直保持一致,那就可以确定现在按键处于稳定的阶段,假如左边时间是起始 0 时刻,每经过 2ms 左移一次,每移动一次,判断当前连续的 8 次按键状态是不是全 1 或者全 0,如果是全 1 则判定为弹起,如果是全 0 则判定为按下,如果 0 和 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
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
#include <reg52.h>
typedef unsigned char u8;
typedef unsigned int u16;
#define LED_SMG_PORT P0 //宏定义LED与数码管端口
#define KEY_PORT P2 //宏定义按键端口

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

sbit KeyIn1 = P2 ^ 4;
sbit KeyIn2 = P2 ^ 5;
sbit KeyIn3 = P2 ^ 6;
sbit KeyIn4 = P2 ^ 7;

u8 code gsmg[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
}; //数码管显示字符转换表
bit key_state = 1; //当前按键状态(因为后面定时器中断服务函数有用到该变量,所以定义为全局变量)
void main()
{
bit backup = 1; //按键值备份,保存前一次的扫描值
u8 count = 0; //按键计数,记录按键按下的次数

ENLED = 0; //选择数码管 DS1 进行显示
ADDR3 = 1;
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
EA = 1; //使能总中断
TMOD &= 0x0F;
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0xF8; //定时 2ms
TL0 = 0xCD;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
KEY_PORT = 0xF7; //P2.3 置 0,即 KeyOut1 输出低电平
LED_SMG_PORT = gsmg[count]; //显示按键次数初值
while(1)
{
if(key_state != backup) //当前值与前次值不相等说明此时按键有动作
{
if(backup == 0) //如果前次值为 0,则说明当前是弹起动作
{
count++; //按键次数+1
if(count == 10) //只用 1 个数码管显示,所以加到 10 就清零重新开始
{
count = 0;
}
LED_SMG_PORT = gsmg[count]; //计数值显示到数码管上
}
backup = key_state; //更新备份为当前值,以备进行下次比较
}
}
}

/*定时器中断服务函数*/
void time0() interrupt 1
{
static u8 key_buf = 0xFF; //扫描缓冲区,保存一段时间内的扫描值
TH0 = 0xF8; //重新加载初值
TL0 = 0xCD;
key_buf = (key_buf << 1) | KeyIn1; //缓冲区左移一位,并将当前扫描值移入最低位
//1111 1111 << 1 --> 1111 1110 | KeyIn1 --> 1111 1111
if(key_buf == 0x00) //0000 0000
{
//连续 8 次扫描值都为 0,即 16ms 内都只检测到按下状态时,可认为按键已按下
key_state = 0;
}
if(key_buf == 0xFF) //1111 1111
{
//连续 8 次扫描值都为 1,即 16ms 内都只检测到弹起状态时,可认为按键已弹起
key_state = 1;
}
else
{} //其它情况则说明按键状态尚未稳定,则不对 KeySta 变量值进行更新
}

矩阵按键相当于 4 组每组各 4 个独立按键,一共是 16 个按键,在按键扫描中断中,我们每次让矩阵按键的一个 KeyOut 输出低电平,其它三个输出高电平,判断当前所有 KeyIn 的状态,下次中断时再让下一个 KeyOut 输出低电平,其它三个输出高电平,再次判断所有 KeyIn,通过 速的中断不停的循环进行判断,就可以最终确定哪个按键按下了,现在有 4 个 KeyOut 输出,要中断 4 次才能完成一次全部按键的扫描,显然再采用 2ms 中断判断 8 次扫描值的方式时间就太长了 (2*4*8=64ms),那么我们就改用 1ms 中断判断 4 次采样值( 即只判断低4位0000 xxxx ),这样消抖时间还是 16ms(1*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
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
#include <reg52.h>
typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
#define LED_SMG_PORT P0 //宏定义LED与数码管端口

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

sbit KeyOut4 = P2 ^ 0;
sbit KeyOut3 = P2 ^ 1;
sbit KeyOut2 = P2 ^ 2;
sbit KeyOut1 = P2 ^ 3;
sbit KeyIn1 = P2 ^ 4;
sbit KeyIn2 = P2 ^ 5;
sbit KeyIn3 = P2 ^ 6;
sbit KeyIn4 = P2 ^ 7;

u8 code gsmg[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
}; //数码管显示字符转换表
u8 key_state[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //全部矩阵按键的当前状态
void main()
{
u8 i, j;
u8 backup[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //按键值备份,保存前一次的扫描值

ENLED = 0; //选择数码管 DS1 进行显示
ADDR3 = 1;
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
EA = 1; //使能总中断
TMOD &= 0x0F;
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0xFC; //定时 1ms
TL0 = 0x66;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
LED_SMG_PORT = gsmg[0]; //默认显示0
while(1)
{
for(i = 0; i < 4; i++) //循环检测4*4的矩阵按键
{
for(j = 0; j < 4; j++)
{
if(backup[i][j] != key_state[i][j]) //当前值与前次值不相等说明此时按键有动作
{
if(backup[i][j] != 0) //前次值不等于0,则当前值等于0,按键按下
{
LED_SMG_PORT = gsmg[i * 4 + j]; //将编号显示到数码管
}
backup[i][j] = key_state[i][j]; //更新前一次的备份值
}
}
}
}
}

/*定时器中断服务函数*/
void time0() interrupt 1
{
u8 i = 0;
static u8 KeyOut = 0; //矩阵按键扫描输出索引
static u8 key_buf[4][4] = {
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
}; //矩阵按键扫描缓冲区
TH0 = 0xFC; //重新加载初值
TL0 = 0x66;
//将一行的4个按键值移入缓冲区
key_buf[KeyOut][0] = (key_buf[KeyOut][0] << 1) | KeyIn1;
key_buf[KeyOut][1] = (key_buf[KeyOut][1] << 1) | KeyIn2;
key_buf[KeyOut][2] = (key_buf[KeyOut][2] << 1) | KeyIn3;
key_buf[KeyOut][3] = (key_buf[KeyOut][3] << 1) | KeyIn4;
//消抖后更新按键状态
for(i = 0; i < 4; i++) //每行4个按键,所以循环4次
{
if((key_buf[KeyOut][i] & 0x0F) == 0x00) //只判断低4位即可
{
//连续4次扫描值为0,即4*4ms内都是按下状态时,可认为按键已稳定的按下
key_state[KeyOut][i] = 0;
}
else if((key_buf[KeyOut][i] & 0x0F) == 0x0F)
{
//连续4次扫描值为1,即4*4ms内都是弹起状态时,可认为按键已稳定的弹起
key_state[KeyOut][i] = 1;
}
}
//执行下一次的扫描输出
KeyOut++; //输出索引递增
if(KeyOut == 4) //也可以写成 keyout = keyout & 0x03; //索引值加到4即归零
{
KeyOut = 0;
}
switch(KeyOut) //根据索引,释放当前输出引脚,拉低下次的输出引脚,P2口默认高电平,而且只要不去改变它它状态是不会改变所以不必重新设置
{
case 0:
KeyOut4 = 1;
KeyOut1 = 0;
break;
case 1:
KeyOut1 = 1;
KeyOut2 = 0;
break;
case 2:
KeyOut2 = 1;
KeyOut3 = 0;
break;
case 3:
KeyOut3 = 1;
KeyOut4 = 0;
break;
default:
break;
}
}

行列式扫描法 – 在数码管上显示对应键值(01 ~ 16)

key.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <reg52.h>
#include "key.h"

u8 Key_Scan()
{
static u8 key1 = NO_KEY, key2, key3; //定义三个按键值,key1一开始先初始化为 按键未按下
static u8 time = 0; //计算三个按键传递的次数
u8 key_sta = 1; //允许识别按键
u8 Key_value; //按键键值
u8 temp; //中间变量

key3 = key2; //把第二个按键值传给第三个按键值
key2 = key1; //把第一个按键值传给第二个按键值
time++; //三个按键传递的次数累加
KEY_PORT = 0xF0; //矩阵按键列全为1,行全为0
if((key_sta == 1) && ((KEY_PORT & 0xF0) != 0xF0)) //如果允许识别按键且按键与原来键值不一样则表示有按键按下
{
KEY_PORT = 0xF7; // 第一行某个按键按下按下
if((KEY_PORT & 0xF7) != 0xF7) //按键发生变化表示第一行有按键按下
{
temp = 0x07 | (KEY_PORT & 0xF0); //把按键值赋给中间变量(例如第一个按键按下, 0x07 | (0xE7 & 0xF0) --> 0x07 | 0xE0 --> 0xE7)
key1 = temp; //再把中间变量的按键值赋给第一个按键值
} //按键值会传递到三个按键值都一样表示按键按下(这个过程可以把抖动给过滤掉)
KEY_PORT = 0xFB; // 第二行某个按键按下
if((KEY_PORT & 0xFB) != 0xFB)
{
temp = 0x0B | (KEY_PORT & 0xF0);
key1 = temp;
}
KEY_PORT = 0xFD; // 第三行某个按键按下按下
if((KEY_PORT & 0xFD) != 0xFD)
{
temp = 0x0D | (KEY_PORT & 0xF0);
key1 = temp;
}
KEY_PORT = 0xFE; // 第四行某个按键按下按下
if((KEY_PORT & 0xFE) != 0xFE)
{
temp = 0x0E | (KEY_PORT & 0xF0);
key1 = temp;
}

if(time == 3) //三个按键传递的次数等于3表示三个按键值传递到都一样了
{
time = 0; //次数清0
if((key1 == key2) && (key1 == key3) && (key1 != NO_KEY)) //如果按键值1 2 3都相等且按下的话
{
Key_value = key1;
key_sta = 0; //不允许识别按键(按键按下所以不允许识别按键)
}
}
}
else if((KEY_PORT & 0xF0) == 0xF0) //如果按键列全为高电平表示没有按键按下
{
key_sta = 1; //允许识别按键
time = 0;
}
return Key_value;
}

key.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef _KEY_H_
#define _KEY_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名

#define NO_KEY 0XFF //表示按键未按下

#define KEY_PORT P2 //宏定义按键端口

u8 Key_Scan();

#endif

main.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
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
#include <reg52.h>
#include "key.h"

#define LED_SMG_PORT P0 //定义LED和数码管端口

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

u8 code KeyCode[] =
{
0xE7, 0xD7, 0xB7, 0x77,
0xEB, 0xDB, 0xBB, 0x7B,
0xED, 0xDD, 0xBD, 0x7D,
0xEE, 0xDE, 0xBE, 0x7E
}; //对应某一个按键按下的键值(按键键值表)

u8 code SMG_Char[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; //数码管段码0~9
u8 SMG_buff[2] = {0xFF, 0xFF}; //数码管缓冲区
u8 num = NO_KEY;

void Time0_Init();
void SMG_Dispaly();

void main()
{
u8 key_num = NO_KEY; //按键未按下
u8 i;
ADDR3 = 1; //G1(E3) 使能管脚高电平
ENLED = 0; //E1 E2低电平有效
Time0_Init(); //定时器0初始化

while(1)
{
key_num = Key_Scan(); //接收键值
if(key_num != NO_KEY) //如果按键按下
{
for(i = 0; i < 16; i++)
{
if(key_num == KeyCode[i]) //表示某一个按键按下
{
num = i; //把按键键值表中对应的位置取出来赋给num
break;
}
}
}
SMG_buff[0] = SMG_Char[(num + 1) % 10]; //按键值与数码管上显示的数相对应(所以要num + 1)
SMG_buff[1] = SMG_Char[(num + 1) / 10];
}
}

void Time0_Init()
{
EA = 1; //开启总中断
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //定时器模式1
TH0 = 0XFC; //定时1毫秒
TL0 = 0x66;
ET0 = 1; //使能定时器0中断
TR0 = 1; //启动定时器0
}

void SMG_Dispaly()
{
static u8 i = 0; //动态扫描的索引
//以下代码完成数码管动态刷新
LED_SMG_PORT = 0xFF; //消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = SMG_buff[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i = 0;
LED_SMG_PORT = SMG_buff[1];
break;
default:
break;
}
}

void time0() interrupt 1
{
TH0 = 0xFC; //重载初值
TL0 = 0x66;
if(num != NO_KEY) //按键按下才在数码管上表示数值
SMG_Dispaly();
}

线翻转扫描法-- 在数码管上显示对应键值(01 ~ 16)

使所有列为 高电平 时,检测所有行是否有 低电平 ,如果有,就记录行线值;然后再翻转,使所有行都为 高电平,检测所有列的值,由于有按键按下,列的值也会有变化,记录列的值,将行与列的键值拼接起来,从而就可以检测到全部按键。

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 "key.h"

u8 Key_Scan()
{
static u8 key1 = NO_KEY, key2, key3; //定义三个按键值,key1一开始先初始化为 按键未按下
static u8 time = 0; //计算三个按键传递的次数
u8 key_sta = 1; //允许识别按键
u8 Key_value; //按键键值
u8 temp; //中间变量

key3 = key2; //把第二个按键值传给第三个按键值
key2 = key1; //把第一个按键值传给第二个按键值
time++; //三个按键传递的次数累加
KEY_PORT = 0xF0; //矩阵按键列全为1,行全为0

if((key_sta == 1) && ((KEY_PORT & 0xF0) != 0xF0)) //如果允许识别按键且按键与原来键值不一样则表示有按键按下
{
KEY_PORT = 0xF0; //获取列的编号
if((KEY_PORT & 0xF0) != 0xF0) //如果按键值发生变化则表示按键按下
{
temp = KEY_PORT & 0xF0; //取出列对应的按键值
}
KEY_PORT = 0x0F; //获取行的编号
if((KEY_PORT & 0x0F) != 0x0F) //如果按键值发生变化则表示按键按下
{
key1 = temp | (KEY_PORT & 0x0F); //将列的键值与行的键值拼接在一起获取按下按键的键值
}

if(time == 3) //三个按键传递的次数等于3表示三个按键值传递到都一样了
{
time = 0; //次数清0
if((key1 == key2) && (key1 == key3) && (key1 != NO_KEY)) //如果按键值1 2 3都相等且按下的话
{
Key_value = key1;
key_sta = 0; //不允许识别按键(按键按下所以不允许识别按键)
}
}
}

else if((KEY_PORT & 0xF0) == 0xF0) //如果按键列全为高电平表示没有按键按下
{
key_sta = 1; //允许识别按键
time = 0;
}
return Key_value;
}

简单加减法计算器

smg.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
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
#include "AllHead.h"

u8 code gsmg[] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
}; //数码管显示字符转换表
u8 SMG_BUFF[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管显示缓冲区

/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void SMG_Scan()
{
static u8 i = 0; //动态扫描的索引
LED_SMG_PORT = 0xFF; //显示消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = SMG_BUFF[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_SMG_PORT = SMG_BUFF[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_SMG_PORT = SMG_BUFF[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_SMG_PORT = SMG_BUFF[3];
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = SMG_BUFF[4];
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i = 0;
LED_SMG_PORT = SMG_BUFF[5];
break;
default:
break;
}
}

/* 将一个无符号长整型的数字显示到数码管上,num-待显示数字 */
void SMG_Show(u32 num)
{
signed char i;
u8 sign = 0;
u8 buf[6];
if(num < 0) //首先提取并暂存符号位
{
sign = 1;
num = -num;
}
else
{
sign = 0;
}
for (i = 0; i < 6; i++) //把长整型数转换为 6 位十进制的数组
{
buf[i] = num % 10;
num /= 10;
}

for (i = 5; i > 0; i--) //从最高位起,遇到 0 转换为空格,遇到非 0 则退出循环
{
if (0 == buf[i])
{
buf[i] = 0xFF;
}
else
{
break;
}
}
if(sign == 1) //负数时,需在最前面添加负号
{
if(i < 5) //当有效位数小于6位时添加负号,否则显示结果将是错的
{
SMG_BUFF[i + 1] = 0xBF; //负号的数码管值为0xBF
}
}
for (; i >= 0; i--) //剩余低位都如实转换为数码管显示字符
{
SMG_BUFF[i] = gsmg[buf[i]];
}
}

smg.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef __SMG_H
#define __SMG_H

#define LED_SMG_PORT P0

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

void SMG_Scan();
void SMG_Show(u32 num);

extern u8 code gsmg[];
extern u8 SMG_BUFF[6];

#endif

key.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
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 "AllHead.h"

u8 code KeyCodeMap[4][4] = //矩阵按键编号到标准键盘键码的映射表
{
{ 0x31, 0x32, 0x33, 0x26 }, //数字键 1、数字键 2、数字键 3、向上键
{ 0x34, 0x35, 0x36, 0x25 }, //数字键 4、数字键 5、数字键 6、向左键
{ 0x37, 0x38, 0x39, 0x28 }, //数字键 7、数字键 8、数字键 9、向下键
{ 0x30, 0x1B, 0x0D, 0x27 } //数字键 0、ESC 键、 回车键、 向右键
};
u8 key_sate[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //全部矩阵按键的当前状态

/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void Key_Driver()
{
u8 i, j;
static u8 backup[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //按键值备份,保存前一次的值
for(i = 0; i < 4; i++) //循环检测 4*4 的矩阵按键
{
for(j = 0; j < 4; j++)
{
if(backup[i][j] != key_sate[i][j]) //检测按键动作
{
if(backup[i][j] != 0) //按键按下时执行动作
{
Key_Action(KeyCodeMap[i][j]); //调用按键动作函数
}
backup[i][j] = key_sate[i][j]; //刷新前一次的备份
}
}
}
}

/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void Key_Action(u8 keycode)
{
static char oprt = 0; //用于保存加减运算符
static long result = 0; //用于保存运算结果
static long addend = 0; //用于保存输入的加数

if ((keycode >= 0x30) && (keycode <= 0x39)) //输入 0-9 的数字
{
addend = (addend * 10) + (keycode - 0x30); //整体十进制左移,新数字进入个位
SMG_Show(addend); //运算结果显示到数码管
}
else if (0x26 == keycode) //向上键用作加号,执行加法或连加运算
{
oprt = 0; //设置运算符变量
result = addend; //运算数存到结果中,准备进行加减
addend = 0; //清零运算数,准备接收下一个运算数
SMG_Show(addend);//刷新数码管显示
}
else if(0x28 == keycode) //向下键用作减号
{
oprt = 1; //设置运算符变量
result = addend; //运算数存到结果中,准备进行加减
addend = 0; //清零运算数,准备接收下一个运算数
SMG_Show(addend); //刷新数码管显示
}
else if (0x0D == keycode) //回车键,执行加减运算
{
if(0 == oprt)
{
result += addend; //进行加法运算
}
else
{
result -= addend; //进行减法运算
}
addend = 0;
SMG_Show(result); //运算结果显示到数码管
}
else if (0x1B == keycode) //Esc 键,清零结果
{
addend = 0;
result = 0;
SMG_Show(addend); //运算结果显示到数码管
}
}

/* 按键扫描函数,需在定时中断中调用,推荐调用间隔 1ms */
void Key_Scan()
{
u8 i = 0;
static u8 KeyOut = 0; //矩阵按键扫描输出索引
static u8 keybuf[4][4] = {{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
}; //矩阵按键扫描缓冲区
//将一行的 4 个按键值移入缓冲区
keybuf[KeyOut][0] = (keybuf[KeyOut][0] << 1) | KeyIn1;
keybuf[KeyOut][1] = (keybuf[KeyOut][1] << 1) | KeyIn2;
keybuf[KeyOut][2] = (keybuf[KeyOut][2] << 1) | KeyIn3;
keybuf[KeyOut][3] = (keybuf[KeyOut][3] << 1) | KeyIn4;
//消抖后更新按键状态
for (i = 0; i < 4; i++) //每行 4 个按键,所以循环 4 次
{
if (0x00 == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
key_sate[KeyOut][i] = 0;
}
else if (0x0F == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
key_sate[KeyOut][i] = 1;
}
}
//执行下一次的扫描输出
KeyOut++; //输出索引递增
if (KeyOut >= 4)
{
KeyOut = 0;
}
// KeyOut = KeyOut & 0x03; //索引值加到 4 即归零
switch(KeyOut) //根据索引,释放当前输出引脚,拉低下次的输出引脚
{
case 0:
KeyOut4 = 1;
KeyOut1 = 0;
break;
case 1:
KeyOut1 = 1;
KeyOut2 = 0;
break;
case 2:
KeyOut2 = 1;
KeyOut3 = 0;
break;
case 3:
KeyOut3 = 1;
KeyOut4 = 0;
break;
default:
break;
}
}

key.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef __KEY_H
#define __KEY_H

sbit KeyIn1 = P2^4;
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;
sbit KeyOut1 = P2^3;
sbit KeyOut2 = P2^2;
sbit KeyOut3 = P2^1;
sbit KeyOut4 = P2^0;

void Key_Driver();
void Key_Action(u8 keycode);
void Key_Scan();

extern u8 code KeyCodeMap[4][4];
extern u8 key_sate[4][4];

#endif

time.c

1
2
3
4
5
6
7
8
9
10
11
#include "AllHead.h"

void Time_Init()
{
EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0xFC; //为 T0 赋初值,定时 1ms
TL0 = 0x66;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
}

time.h

1
2
3
4
5
6
#ifndef __TIME_H
#define __TIME_H

void Time_Init();

#endif

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include "AllHead.h"
void main()
{
ENLED = 0; //选择数码管进行显示
ADDR3 = 1;
Time_Init(); //定时器初始化
SMG_BUFF[0] = gsmg[0]; //上电显示 0
while(1)
{
Key_Driver(); //调用按键驱动函数
}
}

/* T0 中断服务函数,用于数码管显示扫描与按键扫描 */
void time0() interrupt 1
{
TH0 = 0xFC; //重新加载初值
TL0 = 0x66;
SMG_Scan(); //调用数码管显示扫描函数
Key_Scan(); //调用按键扫描函数
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8;
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "key.h"
#include "smg.h"
#include "time.h"

#endif

按键1实现流水灯,按键2数码管显示0到99,按键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
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#include <reg52.h>
typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
#define LED_SMG_PORT P0 //宏定义LED和数码管端口

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

sbit KeyOut1 = P2 ^ 3;
sbit KeyIn1 = P2 ^ 4;
sbit KeyIn2 = P2 ^ 5;
sbit KeyIn3 = P2 ^ 6;
sbit KeyIn4 = P2 ^ 7;

u8 code gsmg[10] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90}; //数码管显示字符转换表(0到9)
u8 SMG_BUFF[7] = {0XFF, 0XFF, 0XFF, 0XFF, 0XFF, 0XFF, 0xFF}; //数码管显示缓冲区
u8 key_state[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //全部矩阵按键的当前状态

u8 flag1 = 0; //0.5s标志位
u8 flag2 = 0; //1s标志位

u8 KeyDown_State; //按键按下状态

/*============================函数声明区============================*/
void time0_Init();
void Key_Driver();
void Key_Test();
void Key_Scan();
void SMG_Scan();
void SMG_show();
void LED_Contrl();
void SMG_count();
void Key_Handler();
void Parameter_Init();

void main()
{
ENLED = 0; //使能74HC138芯片
ADDR3 = 1;
time0_Init(); //定时器初始化
while(1)
{
Key_Driver(); //按键驱动函数
Key_Handler(); //按键处理函数
}
}

/*============================定时器初始化============================*/

void time0_Init()
{
EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0xFC; //为 T0 赋初值,定时 1ms
TL0 = 0x66;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
}

/*============================按键驱动============================*/
//按键驱动函数,检测按键动作,调度相应动作函数
void Key_Driver()
{
u8 i , j;
static u8 backup[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //按键值备份,保存前一次的值
for(i = 0; i < 4; i++) //循环检测 4*4 的矩阵按键
{
for(j = 0; j < 4; j++)
{
if(key_state[i][j] != backup[i][j]) //当前值与前次值不相等说明此时按键有动作
{
if(backup[i][j] != 0) //前次值不等于0,则当前值等于0,按键按下
{
Key_Test(); //调用按键检测函数
}
backup[i][j] = key_state[i][j]; //更新前一次的备份值
}
}
}
}

/*============================按键检测============================*/

void Key_Test() //按键检测函数
{
if(0 == KeyIn1) //如果按键1按下
{
Parameter_Init(); //调用参数初始化函数
KeyDown_State = 1; //按键按下状态为1
}
else if(0 == KeyIn2) //如果按键2按下
{
Parameter_Init(); //调用参数初始化函数
KeyDown_State = 2; //按键按下状态为2
}
else if(0 == KeyIn3) //如果按键3按下
{
Parameter_Init(); //调用参数初始化函数
KeyDown_State = 3; //按键按下状态为3
}
}

/*============================按键处理函数============================*/
void Key_Handler()
{
if(1 == KeyDown_State) //如果按键按下状态为1,则表示按键1按下
{
if(1 == flag1) //如果标志位为1
{
flag1 = 0; //标志位清0
LED_Contrl();//调用LED流水灯函数
}
}
if(2 == KeyDown_State) //如果按键按下状态为2,则表示按键2按下
{
if(1 == flag2) //如果标志位为1
{
flag2 = 0; //标志位清0
SMG_count(); //调用数码管计数函数
}
}
if(3 == KeyDown_State) //如果按键按下状态为3,则表示按键3按下
{
if(1 == flag2) //如果标志位为1
{
flag2 = 0; //标志位清0
SMG_show(); //调用数码管显示时钟函数
}
}
}

/* 按键扫描函数,需在定时中断中调用*/
void Key_Scan()
{
static u8 i = 0;
static KeyOut = 0; //矩阵按键扫描输出索引
static keybuf[4][4] = {{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
}; //矩阵按键扫描缓冲区
//将一行的 4 个按键值移入缓冲区(按键消抖)
keybuf[KeyOut][0] = (keybuf[KeyOut][0] << 1) | KeyIn1; //缓冲区左移一位,并将当前扫描值移入最低位
keybuf[KeyOut][1] = (keybuf[KeyOut][1] << 1) | KeyIn2; //1111 1111 << 1 --> 1111 1110 | KeyIn1 --> 1111 1111
keybuf[KeyOut][2] = (keybuf[KeyOut][2] << 1) | KeyIn3;
keybuf[KeyOut][3] = (keybuf[KeyOut][3] << 1) | KeyIn4;
//消抖后更新按键状态
for(i = 0; i < 4; i++) //每行 4 个按键,所以循环 4 次
{
if (0x00 == (keybuf[KeyOut][i] & 0x0F)) //由于只需要判断最后四位,所以要 & 上0x0F
{
//连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
key_state[KeyOut][i] = 0;
}
if (0x0F == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
key_state[KeyOut][i] = 1;
}
}
KeyOut1 = 0; //由于这里只需要三个按键,选了按键1 2 3,所以只需要KeyOut1 = 0就好
}

/*============================数码管驱动============================*/

/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void SMG_Scan()
{
static u8 i = 0; //动态扫描的索引
LED_SMG_PORT = 0xFF; //显示消隐
switch(i)
{
case 0:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = SMG_BUFF[0];
break;
case 1:
ADDR2 = 0;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_SMG_PORT = SMG_BUFF[1];
break;
case 2:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 0;
i++;
LED_SMG_PORT = SMG_BUFF[2];
break;
case 3:
ADDR2 = 0;
ADDR1 = 1;
ADDR0 = 1;
i++;
LED_SMG_PORT = SMG_BUFF[3];
break;
case 4:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 0;
i++;
LED_SMG_PORT = SMG_BUFF[4];
break;
case 5:
ADDR2 = 1;
ADDR1 = 0;
ADDR0 = 1;
i++;
LED_SMG_PORT = SMG_BUFF[5];
break;
case 6:
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;
i = 0;
LED_SMG_PORT = SMG_BUFF[6];
break; //点亮LED需要使能LEDS6
}
}

/* LED流水灯函数 */
void LED_Contrl()
{
static u8 Led_State = 0x80; // led初始状态
//注意这里要把LED的状态变量赋给Led_Buff[6],因为数码管和LED共用端口,所以这里不能直接赋给P0
SMG_BUFF[6] = ~Led_State; //取反赋给数码管缓存数组里的第七个元素(也就是使能LEDS6,才可以点亮LED)
Led_State >>= 1; //右移一位
if(Led_State == 0)
{
Led_State = 0x80; //右移到最低位后重新设置为第八个LED灯
}
}

/* 数码管计数函数 */
void SMG_count()
{
static u8 sec = 0; //定义静态变量秒为0
sec++;
if(sec >= 100) //秒加到99就清0
{
sec = 0;
}
SMG_BUFF[0] = gsmg[sec % 10]; //秒个位
SMG_BUFF[1] = gsmg[sec / 10]; //秒十位
}

/* 时钟显示函数 */
void SMG_show()
{
static u8 hour = 15; //初始化时钟时间
static u8 min = 35;
static u8 sec = 30;
SMG_BUFF[0] = gsmg[sec % 10]; //秒个位
SMG_BUFF[1] = gsmg[sec / 10]; //秒十位
SMG_BUFF[2] = gsmg[min % 10]; //分个位
SMG_BUFF[3] = gsmg[min / 10]; //分十位
SMG_BUFF[4] = gsmg[hour % 10]; //小时个位
SMG_BUFF[5] = gsmg[hour / 10]; //小时十位
sec++;
if(sec >= 60)
{
sec = 0;
min++;
if(min >= 60)
{
min = 0;
hour++;
if(hour >= 24)
{
hour = 0;
}
}
}
}

/*============================其他============================*/
void Parameter_Init() //参数初始化函数 调用这个函数主要是当按下其他按键时,前一个按键的功能不显示,
{
//继续按下前一个按键时该按键的功能才会继续显示
u8 i;
for (i = 0; i < 7; i++)
{
SMG_BUFF[i] = 0xFF; //将数码管缓冲区全部关闭
}
}

/*============================定时器============================*/
/* 定时器0中断服务函数 */
void time0() interrupt 1
{
static u16 count1 = 0; //记录T0中断次数
static u16 count2 = 0;
TH0 = 0xFC; //重载初值
TL0 = 0x66;
count1++; //中断计数+1
count2++;
if(count1 >= 500) //中断500次相当于0.5秒
{
count1 = 0; //溢出中断清0
flag1 = 1; //设置中断标志位
}
if(count2 >= 1000) //中断1000次相当于1秒 (1000ms = 1s)
{
count2 = 0; //溢出中断清0
flag2 = 1; //设置中断标志位
}
Key_Scan(); //调用按键扫描函数
SMG_Scan(); //调用数码管扫描函数(驱动函数)
}

四个按键控制四个LED亮灭

KEY.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
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
#include <reg52.h>
#include "KEY.h"

u8 Key_State[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //全部矩阵按键的当前状态
u8 KeyDown_State; //按下状态

/*============================按键驱动============================*/
//按键驱动函数,检测按键动作,调度相应动作函数
void Key_Driver()
{
static u8 i, j;
static u8 backup[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //按键值备份,保存前一次的值
for(i = 0; i < 4; i++) //循环检测 4*4 的矩阵按键
{
for(j = 0; j < 4; j++)
{
if(Key_State[i][j] != backup[i][j]) //当前值与前次值不相等说明此时按键有动作
{
if(backup[i][j] != 0) //前次值不等于0,则当前值等于0,按键按下
{
Key_Test(); //调用按键检测函数
}
backup[i][j] = Key_State[i][j]; //更新前一次的备份值
}
}
}
}

/*============================按键检测============================*/
void Key_Test() //按键检测函数
{
if(0 == KeyIn1) //如果按键1按下
{
KeyDown_State = 1; //按下状态为1
}
else if(0 == KeyIn2) //如果按键2按下
{
KeyDown_State = 2; //按下状态为2
}
else if(0 == KeyIn3) //如果按键3按下
{
KeyDown_State = 3; //按下状态为3
}
else if(0 == KeyIn4) //如果按键4按下
{
KeyDown_State = 4; //按下状态为4
}
}

/*============================按键处理函数============================*/
void Key_Handler()
{
if(1 == KeyDown_State) //如果按键按下状态为1,则表示按键1按下
{
KeyDown_State = 0; //按键状态清0
LED2 = !LED2; //LED2状态翻转
}
else if(2 == KeyDown_State) //如果按键按下状态为2,则表示按键2按下
{
KeyDown_State = 0; //按键状态清0
LED3 = !LED3; //LED3状态翻转
}
else if(3 == KeyDown_State) //如果按键按下状态为3,则表示按键3按下
{
KeyDown_State = 0; //按键状态清0
LED4 = !LED4; //LED4状态翻转
}
else if(4 == KeyDown_State) //如果按键按下状态为4,则表示按键4按下
{
KeyDown_State = 0; //按键状态清0
LED5 = !LED5; //LED4状态翻转
}
}

/* 按键扫描函数,需在定时中断中调用*/
void Key_Scan()
{
static u8 i = 0;
static u8 KeyOut = 0; //矩阵按键扫描输出索引
static u8 keybuf[4][4] = {{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
}; //矩阵按键扫描缓冲区
keybuf[KeyOut][0] = (keybuf[KeyOut][0] << 1) | KeyIn1; //缓冲区左移一位,并将当前扫描值移入最低位
keybuf[KeyOut][1] = (keybuf[KeyOut][1] << 1) | KeyIn2; //1111 1111 << 1 --> 1111 1110 | KeyIn1 --> 1111 1111
keybuf[KeyOut][2] = (keybuf[KeyOut][2] << 1) | KeyIn3;
keybuf[KeyOut][3] = (keybuf[KeyOut][3] << 1) | KeyIn4;
//消抖后更新按键状态
for(i = 0; i < 4; i++) //每行 4 个按键,所以循环 4 次
{
if(0x00 == (keybuf[KeyOut][i] & 0x0F)) //由于只需要判断最后四位,所以要 & 上0x0F
{
//连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
Key_State[KeyOut][i] = 0;
}
else if(0x0F == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
Key_State[KeyOut][i] = 1;
}
}
KeyOut1 = 0; //由于这里只需要第一行的四个按键,所以只需要KeyOut1 = 0就好
}

KEY.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef _KEY_H_
#define _KEY_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名

sbit KeyIn1 = P2^4; //定义按键管脚
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;
sbit KeyOut1 = P2^3;

sbit LED2 = P0^0; //定义LED管脚
sbit LED3 = P0^1;
sbit LED4 = P0^2;
sbit LED5 = P0^3;

void Key_Driver();
void Key_Scan();
void Key_Test();
void Key_Handler();

#endif

main.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
42
43
#include <reg52.h>
#include "KEY.h"

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

void main()
{
LED2 = 1; //先让LED熄灭
LED3 = 1;
LED4 = 1;
LED5 = 1;

ENLED = 0; //使能LEDS6
ADDR3 = 1;
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;

EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0xF8; //定时2ms
TL0 = 0xCD;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0

while(1)
{
Key_Driver(); //按键驱动函数
Key_Handler(); //按键处理函数
}
}

/* 定时器0中断服务函数 */
void time0() interrupt 1
{
TH0 = 0xF8; //重载初值
TL0 = 0xCD;
Key_Scan(); //调用按键扫描函数
}

使用按键控制秒表的启动与停止

  • 回车键:开启/暂停;Esc:清0
  • 最右边的两个数码管显示毫秒,左边四个数码管显示秒数

smg.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
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
#include "AllHead.h"

u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 SMG_BUFF[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管缓存区,初值0xFF

bit Watch_Run = 0; //秒表运行标志
bit Watch_Refresh = 1; //秒表计数刷新标志

u8 Decimal = 0; //秒表的小数部分
u16 Integer = 0; //秒表的整数部分

/* 秒表计数显示函数 */
void Watch_Display()
{
signed char i; //由于i是--,最后会减为0,如果换成无符号的话再减就变成256了,所以这里要写出有符号的变量
u8 buf[4]; //数据转换的缓冲区

//小数部分转换到低2位
SMG_BUFF[0] = gsmg[Decimal % 10];
SMG_BUFF[1] = gsmg[Decimal / 10];
//整数部分转换到高4位
buf[0] = Integer % 10;
buf[1] = (Integer / 10) % 10;
buf[2] = (Integer / 100) % 10;
buf[3] = (Integer / 1000) % 10;

for(i = 3; i > 0; i--) //从最高位起,遇到 0 转换为空格,遇到非 0 则退出循环
{
if(0 == buf[i])
{
SMG_BUFF[i + 2] = 0xFF; //如果该数码管显示的数为0则关闭该数码管
}
else
break;
}
for(; i >= 0; i--) //剩余低位都如实转换为数码管显示字符
{
SMG_BUFF[i + 2] = gsmg[buf[i]];
}
SMG_BUFF[2] &= 0x7F; //在倒数第三个数码管点亮小数点(0111 1111)
}

/* 秒表启停函数 */
void Watch_Action()
{
if(Watch_Run)
Watch_Run = 0; //已启动则停止
else
Watch_Run = 1; //未启动则启动
}

/* 秒表复位函数 */
void Watch_Reset()
{
Watch_Run = 0; //停止秒表
Decimal = 0; //整数小数清零计数值
Integer = 0;
Watch_Refresh = 1; //置刷新标志
}

/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void Smg_Scan()
{
static u8 i; //动态扫描索引
LED_SMG_PORT = 0xFF; //关闭所有段选位,显示消隐
//先将P1的第三位取0(低三位与0进行与运算),然后再与i进行或运算,如果i为1,P1第三位(也就是ADDR2-ADDR0)为001,以此类推
P1 = (P1 & 0xF8) | i; //位选索引值赋值到P1口低3位
LED_SMG_PORT = SMG_BUFF[i]; //缓冲区中索引位置的数据送到数码管端口
if(i < 5)
i++;
else
i = 0;
}

/* 秒表计数函数,每隔 10ms 调用一次进行秒表计数累加 */
void Watch_Count()
{
if(Watch_Run) //当处于运行状态时递增计数值
{
Decimal++; //小数部分+1
if(Decimal >= 100) //小数部分计到100时进位到整数部分
{
Decimal = 0;
Integer++; //整数部分+1
if(Integer >= 10000) //整数部分计到10000时归零
{
Integer = 0;
}
}
Watch_Refresh = 1; //设置秒表计数刷新标志
}
}

smg.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef _SMG_H_
#define _SMG_H_

#define LED_SMG_PORT P0 //宏定义数码管端口

void Watch_Display();
void Watch_Action();
void Watch_Reset();
void Smg_Scan();
void Watch_Count();

extern bit Watch_Refresh;

#endif

key.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include "AllHead.h"

u8 Key_State[4] = {1, 1, 1, 1}; //按键当前状态

/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void Key_Driver()
{
u8 i = 0;
static u8 backup[4] = {1, 1, 1, 1}; //按键值备份,保存前一次的值
for(i = 0; i < 4; i++) //循环检测4个按键
{
if(backup[i] != Key_State[i]) //当前值与前次值不相等说明此时按键有动作
{
if(backup[i] != 0) //前次值不等于0,则当前值等于0,按键按下
{
if(1 == i) //Esc键复位秒表
{
Watch_Reset();
}
else if(2 == i) //回车键启停秒表
{
Watch_Action();
}
}
backup[i] = Key_State[i]; //更新前一次的备份值
}
}
}

/* 按键扫描函数,需在定时中断中调用 */
void Key_Scan()
{
u8 i = 0;
static u8 keybuf[4] = {0xFF, 0xFF, 0xFF, 0xFF}; //按键扫描缓冲区

//按键值移入缓冲区
keybuf[0] = (keybuf[0] << 1) | KeyIn1;
keybuf[1] = (keybuf[1] << 1) | KeyIn2;
keybuf[2] = (keybuf[2] << 1) | KeyIn3;
keybuf[3] = (keybuf[3] << 1) | KeyIn4;
//消抖后更新按键状态
for(i = 0; i < 4; i++)
{
if(0x00 == keybuf[i])
{
//连续8次扫描值为0,即16ms内都是按下状态时,可认为按键已稳定的按下
Key_State[i] = 0;
}
else if(0xFF == keybuf[i])
{
//连续8次扫描值为1,即16ms内都是弹起状态时,可认为按键已稳定的弹起
Key_State[i] = 1;
}
}
}

key.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef _KEY_H_
#define _KEY_H_

#define KEY_PORT P2 //宏定义按键端口

sbit KeyIn1 = P2^4;
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;

void Key_Driver();
void Key_Scan();

#endif

time.c

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

u8 T0RH = 0;
u8 T0RL = 0;

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

time.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Config_Time0(u16 ms);

#endif

main.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
#include "AllHead.h"

sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;

void main()
{
ENLED = 0; //使能选择数码管
ADDR3 = 1;
KEY_PORT = 0xFE; //P2.0置0,选择第4行按键作为独立按键(KeyOut4为0)
Config_Time0(2); //配置T0定时2ms
while(1)
{
if(Watch_Refresh) //如果秒表计数刷新标志为1
{
Watch_Refresh = 0; //秒表计数刷新标志置0
Watch_Display(); //调用秒表计数显示函数
}
Key_Driver(); //调用按键驱动函数
}
}

/* T0中断服务函数,完成数码管、按键扫描与秒表计数 */
void time0() interrupt 1
{
static u8 count = 0;

TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
Key_Scan(); //数码管扫描显示
Smg_Scan(); //按键扫描
//定时10ms进行一次秒表计数
count++;
if(count >= 5)
{
count = 0;
Watch_Count(); //调用秒表计数函数
}
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "key.h"
#include "time.h"
#include "smg.h"

#endif

实现按键长按功能

打开开关后,数码管显示数字 0,按向上的按键数字加 1,按向下的按键数字减 1,长按向上按键 1 秒后,数字会持续增加,长按向下按键 1 秒后,数字会持续减小。设定好数字后,按下回车按键,时间就会进行倒计时,当倒计时到 0 的时候,用蜂鸣器和板子上的 8 个 LED 小灯做炸弹效果, 蜂鸣器持续响,LED 小灯全亮

smg.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
42
43
44
#include "AllHead.h"

u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 SMG_BUFF[7] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管+独立LED显示缓冲区,初值0xFF

/* 将一个无符号长整型的数字显示到数码管上,num-待显示数字 */
void SMG_Show(u32 num)
{
signed char i; //由于i是--,最后会减为0,如果换成无符号的话再减就变成256了,所以这里要写出有符号的变量
u8 buf[6]; //数据转换的缓冲区
for(i = 0; i < 6; i++) //把长整型数转换为 6 位十进制的数组
{
buf[i] = num % 10; //取出最低位到数码管上
num /= 10; //去掉最低位
}
for(i = 5; i > 0; i--) //从最高位起,遇到 0 转换为空格,遇到非 0 则退出循环
{
if(0 == buf[i]) //如果该数码管显示的数为0则关闭该数码管
{
SMG_BUFF[i] = 0xFF;
}
else
break;
}
for(; i >= 0; i--) //剩余低位都如实转换为数码管显示字符

{
SMG_BUFF[i] = gsmg[buf[i]];
}
}

/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void Smg_Scan()
{
static u8 i; //动态扫描索引
LED_SMG_PORT = 0xFF; //关闭所有段选位,显示消隐
//先将P1的第三位取0(低三位与0进行与运算),然后再与i进行或运算,如果i为1,P1第三位(也就是ADDR2-ADDR0)为001,以此类推
P1 = (P1 & 0xF8) | i; //位选索引值赋值到P1口低3位
LED_SMG_PORT = SMG_BUFF[i]; //索引递增循环,遍历整个缓冲区,由于加了LED显示,点亮LED需要使能LEDS6,所以i小于6不是小于5
if(i < 6)
i++;
else
i = 0;
}

smg.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef _SMG_H_
#define _SMG_H_

#define LED_SMG_PORT P0 //宏定义数码管端口

void SMG_Show(u32 num);
void Smg_Scan();

extern u8 SMG_BUFF[7];

#endif

key.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
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
136
137
138
139
140
141
142
143
#include "AllHead.h"

u8 Key_State[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}};//按键当前状态
u8 code KeyCodeMap[4][4] = //矩阵按键编号到标准键盘键码的映射表
{
{ 0x31, 0x32, 0x33, 0x26 }, //数字键 1、数字键 2、数字键 3、向上键
{ 0x34, 0x35, 0x36, 0x25 }, //数字键 4、数字键 5、数字键 6、向左键
{ 0x37, 0x38, 0x39, 0x28 }, //数字键 7、数字键 8、数字键 9、向下键
{ 0x30, 0x1B, 0x0D, 0x27 } //数字键 0、ESC 键、 回车键、 向右键
};
//由于定义变量data可以省略,但是这个变量内存空间不够,所以得使用pdata
u32 pdata KeyDownTime[4][4] = {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}};//每个按键按下的持续时间,单位 ms
bit enBuzz = 0; //蜂鸣器使能标志
bit flag1s = 0; //1 秒定时标志
bit flagStart = 0; //倒计时启动标志
u16 CountDown = 0; //倒计时计数器

/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void Key_Action(u8 keycode) //按键动作函数,根据键码执行相应动作
{
if(0x26 == keycode) //向上键,倒计时设定值递增
{
if(CountDown < 9999) //最大计时 9999 秒
{
CountDown++;
SMG_Show(CountDown); //将数值显示在数码管上
}
}
else if(0x28 == keycode) //向下键,倒计时设定值递减
{
if(CountDown > 1) //最小计时 1 秒
{
CountDown--;
SMG_Show(CountDown); //将数值显示在数码管上
}
}
else if(0x0D == keycode) //回车键,启动倒计时
{
flagStart = 1; //启动倒计时
}
else if(0x1B == keycode) //Esc 键,取消倒计时
{
enBuzz = 0; //关闭蜂鸣器
SMG_BUFF[6] = 0xFF; //关闭独立 LED
flagStart = 0; //停止倒计时
CountDown = 0; //倒计时数归零
SMG_Show(CountDown); //将数值显示在数码管上
}
}
/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void Key_Driver()
{
u8 i, j;
static u8 backup[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //按键值备份,保存前一次的值
static u32 pdata TimeThr[4][4] = //快速输入执行的时间阈值(一开始默认为1秒,也就是按键按下超过1秒表示长按)
{
{1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000},
{1000, 1000, 1000, 1000}, {1000, 1000, 1000, 1000}
};
for(i = 0; i < 4; i++) //循环扫描 4*4 的矩阵按键
{
for(j = 0; j < 4; j++)
{
if(backup[i][j] != Key_State[i][j]) //当前值与前次值不相等说明此时按键有动作
{
if(backup[i][j] != 0) //前次值不等于0,则当前值等于0,按键按下
{
Key_Action(KeyCodeMap[i][j]); //调用按键动作函数
}
backup[i][j] = Key_State[i][j]; //更新前一次的备份值
}
if(KeyDownTime[i][j] > 0) //检测执行快速输入(按键按下的持续时间 > 0)
{
if(KeyDownTime[i][j] >= TimeThr[i][j]) //按键按下的持续时间达到阈值时执行一次动作
{
Key_Action(KeyCodeMap[i][j]); //调用按键动作函数
TimeThr[i][j] += 200; //时间阈值增加 200ms,以准备下次执行
}
}
else //按键弹起时复位阈值时间
TimeThr[i][j] = 1000; //恢复 1s 的初始阈值时间
}
}
}

/* 按键扫描函数,需在定时中断中调用 */
void Key_Scan()
{
u8 i = 0;
static u8 KeyOut = 0; //矩阵按键扫描输出索引
static u8 keybuf[4][4] = {{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
}; //矩阵按键扫描缓冲区
//将一行的 4 个按键值移入缓冲区
keybuf[KeyOut][0] = (keybuf[KeyOut][0] << 1) | KeyIn1;
keybuf[KeyOut][1] = (keybuf[KeyOut][1] << 1) | KeyIn2;
keybuf[KeyOut][2] = (keybuf[KeyOut][2] << 1) | KeyIn3;
keybuf[KeyOut][3] = (keybuf[KeyOut][3] << 1) | KeyIn4;
//消抖后更新按键状态
for (i = 0; i < 4; i++) //每行 4 个按键,所以循环 4 次
{
if (0x00 == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
Key_State[KeyOut][i] = 0;
KeyDownTime[KeyOut][i] += 4; //按下的持续时间累加
}
else if (0x0F == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
Key_State[KeyOut][i] = 1;
KeyDownTime[KeyOut][i] = 0; //按下的持续时间清零
}
}
//执行下一次的扫描输出
KeyOut++; //输出索引递增
if (KeyOut >= 4)
{
KeyOut = 0;
}
// KeyOut = KeyOut & 0x03; //索引值加到 4 即归零
switch(KeyOut) //根据索引,释放当前输出引脚,拉低下次的输出引脚
{
case 0:
KeyOut4 = 1;
KeyOut1 = 0;
break;
case 1:
KeyOut1 = 1;
KeyOut2 = 0;
break;
case 2:
KeyOut2 = 1;
KeyOut3 = 0;
break;
case 3:
KeyOut3 = 1;
KeyOut4 = 0;
break;
default:
break;
}
}

key.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef _KEY_H_
#define _KEY_H_

sbit KeyIn1 = P2^4;
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;
sbit KeyOut1 = P2^3;
sbit KeyOut2 = P2^2;
sbit KeyOut3 = P2^1;
sbit KeyOut4 = P2^0;

void Key_Action(u8 keycode);
void Key_Driver();
void Key_Scan();

extern u16 CountDown; //由于外部文件用到了该变量,所以前面要加上extern,外部才可以用
extern bit enBuzz;
extern bit flag1s;
extern bit flagStart;

#endif

time.c

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

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

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

time.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef _KEY_H_
#define _KEY_H_

sbit KeyIn1 = P2^4;
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;
sbit KeyOut1 = P2^3;
sbit KeyOut2 = P2^2;
sbit KeyOut3 = P2^1;
sbit KeyOut4 = P2^0;

void Key_Action(u8 keycode);
void Key_Driver();
void Key_Scan();

extern u16 CountDown; //由于外部文件用到了该变量,所以前面要加上extern,外部才可以用
extern bit enBuzz;
extern bit flag1s;
extern bit flagStart;

#endif

main.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include "AllHead.h"

sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;
sbit BUZZ = P1 ^ 6; //定义蜂鸣器管脚

void main()
{
ENLED = 0; //选择数码管和独立 LED
ADDR3 = 1;

Config_Time0(1); //配置T0定时1ms
SMG_Show(0); //上电显示 0
while(1)
{
Key_Driver(); //调用按键驱动函数
if(flagStart && flag1s) //倒计时启动且 1 秒定时到达时,处理倒计时
{
flag1s = 0;
if(CountDown > 0) //倒计时未到 0 时,计数器递减
{
CountDown--;
SMG_Show(CountDown); //刷新倒计时数显示
if(0 == CountDown) //减到 0 时,执行声光报警
{
enBuzz = 1; //启动蜂鸣器发声
SMG_BUFF[6] = 0x00; //点亮独立 LED
}
}
}
}
}

/* T0 中断服务函数,完成数码管、按键扫描与秒定时 */
void time0() interrupt 1
{
static u16 count = 0; //1 秒定时计数
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;

if(enBuzz) //蜂鸣器发声处理
BUZZ = ~BUZZ; //驱动蜂鸣器发声
else
BUZZ = 1; //关闭蜂鸣器
if(flagStart) //倒计时启动时处理 1 秒定时
{
count++;
if(count >= 1000)
{
count = 0;
flag1s = 1;
}
}
else //倒计时未启动时 1 秒定时器始终归零
flag1s = 0;
Key_Scan(); //数码管扫描显示
Smg_Scan(); //按键扫描
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "key.h"
#include "time.h"
#include "smg.h"

#endif

步进电机

步进电机是将电脉冲信号转变为角位移或线位移的开环控制元件。在非超载的情况下,电机的转速、停止的位置只取决于脉冲信号的频率和脉冲数,而不受负载变化的影响,即给电机加一个脉冲信号,电机则转过一个步距角。这一线性关系的存在,加上步进电机只有周期性的误差而无累计误差等特点

工作原理

通常步进电机的转子为永磁体,当电流流过定子绕组时,定子绕组产生一矢量磁场。输入一个电脉冲,电动机转动一个角度前进一步。它输出的角位移与输入的脉冲 数成正比、转速与脉冲频率成正比。改变绕组通电的顺序,电机就会反转。所以可以控制脉冲数量、频率及电动机各相绕组的通电顺序来控制步进电机的转动。 具体看下图:

步进电机工作原理:当定子的矢量磁场旋转一个角度。转子也随着该磁场转步距角。每输入一个电脉冲 ,电动机转动一个角度前进一步。它输出的角位移与输入的脉冲数成正比、转速与脉冲频率成正比。改变绕组通电的顺序,电机就会反转 。所以可以控制脉冲数量、频率及电动机各相绕维组的通电顺序来控制步进电机的转动

28BYJ-48 步进电机简介

28——步进电机的有效最大外径是 28 毫米

B——表示是步进电机

Y——表示是永磁式

J——表示是减速型

48——表示四相八拍

28BYJ48 步进电机自带减速器,为四相无线步进电机,直径为 28mm,实物如下所示:

28BYJ48 电机内部结构等效图如下所示:

步进电机一共有 5 根引线,其中红色的是公共端,连接到 5V 电源,接下来的橙、黄、粉、蓝就对应了 A、B、C、D 相;如果要导通 A 相绕组,就只需将橙色线接地即可,B 相则黄色接地,依此类推

绕组控制顺序表:(必须按顺序不能跳过哪步否则电机不会被磁力吸附)

橙、黄、粉、蓝 --- A、B、C、D
A --> AB --> B --> BC --> C --> CD --> D --> DA

28BYJ48 步进电机旋转驱动方式如下表:

28BYJ48 步进电机实际上是:减速齿轮+步进电机组成

原理图

板子需要把 跳线帽 跳到左边那列(J13~J16),可以使用 P1.0 到 P1.3 控制步 进电机了,如要再使用显示部分的话,就要再换回到右侧

问:如果大家既想让显示部分正常工作,又想让电机工作该怎么办呢?

跳线帽保持在右侧,用杜邦线把步进电机的控制引脚 (即左侧的排针)连接到其它的暂不使用的单片机 IO 上即可

单片机的 IO 口可以直接输出 0V 和 5V 的电压,但是电流驱动能力,也就是带载能力非常有限,所以我们在每相的控制线上都增加一个三极管来提高驱动能力。由图中可以看出,若要使 A 相导通,则必须是 Q2 导通,此时 A 相也就是橙色线就相当于接地了,于是 A 相绕组导通,此时单片机 P1 口低 4 位应输出 0b1110,即 0xE;如要 A、B 相同时导通,那么就是 Q2、Q3 导通,P1 口低 4 位应输出 0b1100,即 0xC,依此类推,可以得到下面的八拍节拍的 IO 控制代码数组:

1
unsigned char code BeatCode[8] = { 0x0E, 0x0C, 0x0D, 0x09, 0x0B, 0x03, 0x07, 0x06 }; 

28BYJ48 步进电机主要参数如下所示:

表中给出的参数是≥550,单位是 P.P.S,即每秒脉冲数,这里的意思就是说:电机保证 在你每秒给出 550 个步进脉冲的情况下,可以正常启动。那么换算成单节拍持续时间就是 1s/550=1.8ms,为了让电机能够启动,我们控制节拍刷新时间大于 1.8ms 就可以了

问:八拍模式时,步进电机转过一圈是需要 64 个节拍,而我们程序中是每个节拍持续 2ms,那么转一圈就应该是 128ms,即 1 秒钟转 7 圈多,可怎么看上去它好像是 7 秒多才转了一圈呢?

原因在于 “减速” 上,位于最中心的那个白色小齿轮才是步进电机的转子输出,64 个节拍只是让这个小齿轮转了一圈,然后它带动那个浅蓝色的大齿轮,这就 是一级减速,每 2 个齿轮都构成一级减速,一共就有了 4 级减速;电机参数表中的减速比是1:64,转子转 64 圈,最终输出轴才会 转一圈,也就是需要 64*64=4096 个节拍输出轴才转过一圈,2ms*4096=8192ms,8 秒多才转 一圈;4096 个节拍转动一圈,那么一个节拍转动的角度——步进角度就是 360/4096,看一下表中的步进角度参数 5.625/64,算一下就知道这两个值是相等的

问:厂家的参数为什么会有误差呢?

28BYJ-48 最初的设计目的是用来控制空调的扇叶的,扇叶的活动范围是不会超过180度的在这种应用场合下,厂商给出一个近似的整数减速比 1:64 已经足够精确了,这也是合情合理的;我们不一定是要用它来驱动空调扇叶,我们可以让它转动很多圈来干别的

程序

这里只用到了 P1 中的低4位,养成好习惯把整个 P1 先赋给一个变量,然后不影响到其他位的状态

中断转动任意角度

  • 运行后会发现角度并不精确,这是因为真实准确的减速比并不是这个值 1:64,而是 1:63.684,所以把程序里 4096 改成4076 会发现误差小很多了
  • StartMotor 函数中对 EA 的两次操作是因为:STC89C52 单片机是 8 位单片机(即按一个字节进行的),要操作 多个字节(不论是读还是写)就必须分多次进行了; beats 这个变量是 unsigned long 型,它要占用 4 个字节,那么对它的赋值最少也要分 4 次才能完成;所以执行前先关闭了中断,而等它执行完后,才又重新打开了中断。在它执行过程中单片机是不会响应中断的,即中断函数 time0 不会被执行,即使这时候定时器溢出了,中断发生了,也只能等待 EA 重新置 1 后,才能得到响应,中断函数 time0 才会被执行

注意需要把跳线帽跳到左边那列(J13~J16)

步进电机基础转动

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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned long u32;

#define MOTOR_PORT P1 //宏定义步进电机管脚

u32 beats = 0; //电机转动节拍总数

void Time0_Init(); //函数调用
void StartMotor(u32 angle);

void main()
{
Time0_Init(); //定时器初始化
StartMotor(360 * 2 + 180); //控制电机转动 2 圈半
while(1);
}

void Time0_Init()
{
EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0xF8; //定时 2ms
TL0 = 0xCD;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
}

/* 步进电机启动函数,angle-需转过的角度 */
void StartMotor(u32 angle)
{
EA = 0; //在计算前关闭中断,完成后再打开,以避免中断打断计算过程而造成错误
beats = (angle * 4076) / 360; //实测为 4076 拍转动一圈
EA = 1;
}

/* T0 中断服务函数,用于驱动步进电机旋转 */
void time0() interrupt 1
{
u8 temp; //临时变量
static u8 index = 0; //节拍输出索引
u8 code BeatCode[8] = {0x0E, 0x0C, 0x0D, 0x09, 0x0B, 0x03, 0x07, 0x06}; //步进电机节拍对应的 IO 控制代码
TH0 = 0xF8; //重新加载初值
TL0 = 0xCD;
if(beats != 0) //节拍数不为 0 则产生一个驱动节拍
{
temp = MOTOR_PORT; //用 temp 把步进电机端口当前值暂存
temp &= 0xF0; //用&操作清零低 4 位
temp |= BeatCode[index]; //用|操作把节拍代码写到低 4 位
MOTOR_PORT = temp; //把低 4 位的节拍代码和高 4 位的原值送回步进电机端口
index++; //节拍输出索引递增
index &= 0x07; //用&操作实现到 8 归零
beats--; //总节拍数-1
}
else //节拍数为 0 则关闭电机所有的相
MOTOR_PORT |= 0x0F;
}

按键控制步进电机

  • 1~9键:是转动圈数,上下键:正转和反转,左右键:+90度和-90度
  • 中断函数用一个静态bit变量实现二分频,即2ms定时,用于控制电机

Motor.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
42
43
44
45
46
47
48
49
50
#include "AllHead.h"

signed long beats = 0; //电机转动节拍总数

/* 步进电机启动函数,angle-需转过的角度 */
void StartMotor(signed long angle) //因为涉及正转反转,符号有正负,所以要用signed
{
EA = 0; //在计算前关闭中断,完成后再打开,以避免中断打断计算过程而造成错误
beats = (angle * 4076) / 360; //实测为 4076 拍转动一圈
EA = 1;
}

/* 步进电机停止函数 */
void StopMotor()
{
EA = 0;
beats = 0;
EA = 1;
}

/* 电机转动控制函数 */
void TurnMotor()
{
u8 temp; //临时变量
static u8 index = 0; //节拍输出索引
u8 code BeatCode[8] = {0x0E, 0x0C, 0x0D, 0x09, 0x0B, 0x03, 0x07, 0x06}; //步进电机节拍对应的 IO 控制代码
if(beats != 0) //节拍数不为 0 则产生一个驱动节拍
{
if(beats > 0) //节拍数大于 0 时正转
{
index++; //正转时节拍输出索引递增
index &= 0x07; //用&操作实现到 8 归零
beats--; //正转时节拍计数递减
}
else //节拍数小于 0 时反转
{
index--; //反转时节拍输出索引递减
index &= 0x07; //用&操作同样可以实现到-1 时归 7
beats++; //反转时节拍计数递增
}
temp = MOTOR_PORT; //用 temp 把步进电机端口当前值暂存
temp &= 0xF0; //用&操作清零低 4 位
temp |= BeatCode[index]; //用|操作把节拍代码写到低 4 位
MOTOR_PORT = temp; //把低 4 位的节拍代码和高 4 位的原值送回步进电机端口
}
else //节拍数为 0 则关闭电机所有的相
{
MOTOR_PORT |= 0x0F;
}
}

Motor.h

1
2
3
4
5
6
7
8
9
10
#ifndef _MOTOR_H_
#define _MOTOR_H_

#define MOTOR_PORT P1 //宏定义步进电机管脚

void StartMotor(signed long angle);
void StopMotor();
void TurnMotor();

#endif

key.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
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
#include "AllHead.h"

u8 code KeyCodeMap[4][4] = //矩阵按键编号到标准键盘键码的映射表
{
{ 0x31, 0x32, 0x33, 0x26 }, //数字键 1、数字键 2、数字键 3、向上键
{ 0x34, 0x35, 0x36, 0x25 }, //数字键 4、数字键 5、数字键 6、向左键
{ 0x37, 0x38, 0x39, 0x28 }, //数字键 7、数字键 8、数字键 9、向下键
{ 0x30, 0x1B, 0x0D, 0x27 }
}; //数字键 0、ESC 键、 回车键、 向右键

u8 Key_State[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //全部矩阵按键的当前状态

/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void Key_Driver()
{
u8 i, j;
static u8 backup[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //按键值备份,保存前一次的值
for(i = 0; i < 4; i++) //循环检测 4*4 的矩阵按键
{
for(j = 0; j < 4; j++)
{
if(backup[i][j] != Key_State[i][j]) //检测按键动作
{
if(backup[i][j] != 0) //按键按下时执行动作
{
Key_Action(KeyCodeMap[i][j]); //调用按键动作函数
}
backup[i][j] = Key_State[i][j]; //刷新前一次的备份
}
}
}
}

/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void Key_Action(u8 keycode)
{
static bit dirMotor = 0; //电机转动方向
if((keycode >= 0x30) && (keycode <= 0x39)) //控制电机转动 1-9 圈
{
if(0 == dirMotor)
StartMotor(360 * (keycode - 0x30)); //正转
else
StartMotor(-360 * (keycode - 0x30)); //反转
}
else if(0x26 == keycode) //向上键,控制转动方向为正转
{
dirMotor = 0;
}
else if(0x28 == keycode) //向下键,控制转动方向为反转
{
dirMotor = 1;
}
else if(0x25 == keycode) //向左键,固定正转 90 度
{
StartMotor(90);
}
else if(0x27 == keycode) //向右键,固定反转 90 度
{
StartMotor(-90);
}
else if(0x1B == keycode) //Esc 键,停止转动
{
StopMotor();
}
}

/* 按键扫描函数,需在定时中断中调用,推荐调用间隔 1ms */
void Key_Scan()
{
u8 i = 0;
static u8 KeyOut = 0; //矩阵按键扫描输出索引
static u8 keybuf[4][4] = {{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
}; //矩阵按键扫描缓冲区
//将一行的 4 个按键值移入缓冲区
keybuf[KeyOut][0] = (keybuf[KeyOut][0] << 1) | KeyIn1;
keybuf[KeyOut][1] = (keybuf[KeyOut][1] << 1) | KeyIn2;
keybuf[KeyOut][2] = (keybuf[KeyOut][2] << 1) | KeyIn3;
keybuf[KeyOut][3] = (keybuf[KeyOut][3] << 1) | KeyIn4;
//消抖后更新按键状态
for (i = 0; i < 4; i++) //每行 4 个按键,所以循环 4 次
{
if (0x00 == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
Key_State[KeyOut][i] = 0;
}
else if (0x0F == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
Key_State[KeyOut][i] = 1;
}
}
//执行下一次的扫描输出
KeyOut++; //输出索引递增
if (KeyOut >= 4)
{
KeyOut = 0;
}
// KeyOut = KeyOut & 0x03; //索引值加到 4 即归零
switch(KeyOut) //根据索引,释放当前输出引脚,拉低下次的输出引脚
{
case 0:
KeyOut4 = 1;
KeyOut1 = 0;
break;
case 1:
KeyOut1 = 1;
KeyOut2 = 0;
break;
case 2:
KeyOut2 = 1;
KeyOut3 = 0;
break;
case 3:
KeyOut3 = 1;
KeyOut4 = 0;
break;
default:
break;
}
}

key.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef _KEY_H_
#define _KEY_H_

sbit KeyIn1 = P2^4;
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;
sbit KeyOut1 = P2^3;
sbit KeyOut2 = P2^2;
sbit KeyOut3 = P2^1;
sbit KeyOut4 = P2^0;

void Key_Driver(); //函数声明
void Key_Action(u8 keycode);
void Key_Scan();

#endif

time.c

1
2
3
4
5
6
7
8
9
10
11
#include "AllHead.h"

void Time0_Init()
{
EA = 1; //使能总中断
TMOD |= 0x01; //设置 T0 为模式 1
TH0 = 0xFC; //定时 1ms
TL0 = 0x66;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
}

time.h

1
2
3
4
5
6
#ifndef _TIME_H_
#define _TIME_H_

void Time0_Init();

#endif

main.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
#include "AllHead.h"

void main()
{
Time0_Init(); //定时器初始化
while(1)
{
Key_Driver();
}
}

/* T0 中断服务函数,用于按键扫描与电机转动控制 */
void time0() interrupt 1
{
static bit div = 0; //标志
TH0 = 0xFC; //重新加载初值
TL0 = 0x66;
Key_Scan(); //执行按键扫描
//用一个静态 bit 变量实现二分频,即 2ms 定时,用于控制电机
div = ~div; //1ms改变一次值
if(1 == div) //每2ms调用电机控制函数
{
TurnMotor(); //电机转动控制函数
}
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名

#include <reg52.h>
#include "key.h"
#include "time.h"
#include "Motor.h"

#endif

蜂鸣器

蜂鸣器从结构区分分为压电式蜂鸣器电磁式蜂鸣器。压电式为压电陶瓷片发音,电流 比较小一些,电磁式蜂鸣器为线圈通电震动发音,体积比较小

按照驱动方式分为有源蜂鸣器无源蜂鸣器。这里的有源和无源不是指电源,而是振荡源。有源蜂鸣器内部带了振荡源,给了 BUZZ 引脚一个低电平,蜂鸣器就会直接响。而无源蜂鸣器内部是不带振荡源的,要让他响必须给 500Hz~4.5KHz 之间的脉冲频率信号来驱动它才会响

原理图

蜂鸣器电流依然相对较大,因此需要用三极管驱动,并且加了一个 100 欧的电阻作为限流电阻。此外还加了一个 D4 二极管,这个二极管叫做续流二极管

1
2
3
4
//重装载计算
reload = 65536 - (11059200/12)/(frequ*2); //由给定频率计算定时器重载值
T0RH = (unsigned char)(reload >> 8); //16位重载值分解为高低两个字节,因为51单片机是8位的需要分两次写入
T0RL = (unsigned char)reload;

程序

蜂鸣器演示

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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;

sbit BUZZ = P1 ^ 6; //蜂鸣器控制引脚

u8 T0RH = 0; //T0重载值的高字节
u8 T0RL = 0; //T0重载值的低字节

void Open_BUZZ(u16 frequ);
void Stop_BUZZ();

void main()
{
u16 i;
EA = 1; //使能全局中断
TMOD |= 0x01; //配置T0工作在模式1,但先不启动

while(1)
{
Open_BUZZ(4000); //以4KHz的频率启动蜂鸣器
for(i = 0; i < 40000; i++);
Stop_BUZZ(); //停止蜂鸣器
for(i = 0; i < 40000; i++);
Open_BUZZ(1000); //以1KHz的频率启动蜂鸣器
for(i = 0; i < 40000; i++);
Stop_BUZZ(); //停止蜂鸣器
for(i = 0; i < 40000; i++);
}
}

/* 蜂鸣器启动函数,frequ-工作频率 */
void Open_BUZZ(u16 frequ)
{
u16 reload; //计算所需的定时器重载值
reload = 65535 - (11059200 / 12) / (frequ * 2); //由给定频率计算定时器重载值
T0RH = (unsigned char)(reload >> 8); //16位重载值分解为高低两个字节
T0RL = (unsigned char)reload;
TH0 = 0xFF; //设定一个接近溢出的初值,以使定时器马上投入工作
TL0 = 0xFE;
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
}

/* 蜂鸣器停止函数 */
void Stop_BUZZ()
{
ET0 = 0; //禁用T0中断
TR0 = 0; //停止T0
}

/* T0中断服务函数,用于控制蜂鸣器发声 */
void time0() interrupt 1
{
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
BUZZ = ~BUZZ; //反转蜂鸣器控制电平
}

门铃

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
#include <reg52.h>

typedef unsigned int u16; //对系统默认数据类型进行重命名
typedef unsigned char u8;

sbit BUZZ = P1 ^ 6; //蜂鸣器控制引脚
u8 ding, dong, flag, stop;
u16 count; //定时标志

void Time0_Tnit(); //函数声明
void Mark_Init();

void main()
{
Time0_Tnit();
Mark_Init();
while(1);
}

void Time0_Tnit() //定时器0初始化
{
TMOD = 0X01; //定时器0 方式1
TH0 = 0Xff;
TL0 = 0X06; //定时250us
EA = 1;
ET0 = 1;
TR0 = 1; //打开定时器0
}
void Mark_Init() //各个标号初始化
{
ding = 0; //叮声音 计数标志
dong = 0; //咚声音 计数标志
count = 0; //定时0.5s标志
flag = 0;
stop = 0; //结束标志
}

/* 定时器中断服务函数 */
void time0() interrupt 1
{
count++;
TH0 = 0Xff; //重载初值
TL0 = 0X06;
if(count == 2000) //定时0.5s 叮响0.5秒,咚响0.5秒
{
count = 0;
//以下代码实现叮咚响切换
if(flag == 0)
{
flag = ~flag;
}
else
{
flag = 0;
stop = 1;
}
}
if(flag == 0)
{
//通过改变定时计数时间可以改变门铃的声音
ding++; //叮
if(ding == 1)
{
ding = 0;
BUZZ = ~BUZZ;
}
}
else
{
dong++;
if(dong == 2) //咚
{
dong = 0;
BUZZ = ~BUZZ;
}
}
}

PWM

PWM 又叫脉冲宽度调制,它利用微处理器的数字输出来对模拟电路进行控制的一种有效的技术,其实就是使用数字信号达到一个模拟信号的效果。脉冲宽度调制,就是改变脉冲宽度来实现不同的效果

这是一个周期是 10ms,即频率是 100Hz 的波形,但是每个周期内,高低电平脉冲宽度各不相同,这就是 PWM 的本质

占空比是指高电平的时间占整个周期的比例。比如第一部分波形的占空比是 40%,第二部分波形占空比是 60%,第三部分波形占空比是 80%,这就是 PWM 的解释

1
2
3
周期与频率的关系是互为倒数,周期单位是s,所以需要转换,如下面:
10ms = 0.01s --> 1/0.01s = 100Hz
100Hz = 1 / 100 = 0.01s = 10ms

程序

呼吸灯

  • 利用 2个定时器+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
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
136
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

sbit ADDR0 = P1 ^ 0;
sbit ADDR1 = P1 ^ 1;
sbit ADDR2 = P1 ^ 2;
sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;
sbit PWMOUT = P0 ^ 0; //定义用PWM控制的LED管脚(第一个LED灯)

u32 Period_Count = 0; //PWM 周期计数值
u8 HighRH = 0; //高电平重载值的高字节
u8 HighRL = 0; //高电平重载值的低字节
u8 LowRH = 0; //低电平重载值的高字节
u8 LowRL = 0; //低电平重载值的低字节
u8 T1RH = 0; //T1 重载值的高字节
u8 T1RL = 0; //T1 重载值的低字节

void Config_PWM(u16 freq, u16 duty); //函数声明
void Time1_Init(u16 ms);

void main()
{
ENLED = 0;
ADDR3 = 1; //使能独立 LED
ADDR2 = 1;
ADDR1 = 1;
ADDR0 = 0;

Config_PWM(100, 10); //配置并启动 PWM
Config_Time1(50); //用 T1 定时调整占空比
while(1);
}

/* 配置并启动 PWM,freq-频率,duty-占空比 */
void Config_PWM(u16 freq, u16 duty)
{
u16 high, low;
Period_Count = (11059200 / 12) / freq; //计算一个周期所需的计数值
high = (Period_Count * duty) / 100; //计算高电平所需的计数值
low = Period_Count - high; //计算低电平所需的计数值
high = 65536 - high + 12; //计算高电平的定时器重载值并补偿中断延时
low = 65536 - low + 12; //计算低电平的定时器重载值并补偿中断延时
HighRH = (unsigned char)(high >> 8); //高电平重载值拆分为高低字节
HighRL = (unsigned char)high;
LowRH = (unsigned char)(low >> 8); //低电平重载值拆分为高低字节
LowRL = (unsigned char)low;
EA = 1;
TMOD &= 0xF0; //清零 T0 的控制位
TMOD |= 0x01; //配置 T0 为模式 1
TH0 = HighRH; //加载 T0 重载值
TL0 = HighRL;
ET0 = 1; //使能 T0 中断
TR0 = 1; //启动 T0
PWMOUT = 1; //输出高电平
}

/* 配置并启动 T1,ms-定时时间 */
void Time1_Init(u16 ms)
{
u32 temp = 0; //临时变量
temp = 11059200 / 12; //定时器计数频率
temp = (temp * ms) / 1000; //计算所需的计数值
temp = 65536 - temp; //计算定时器重载值
temp += 12; //补偿中断响应延时造成的误差
T1RH = (unsigned char)(temp >> 8); //定时器重载值拆分为高低字节
T1RL = (unsigned char)temp;
EA = 1;
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x10; //配置 T1 为模式 1
TH1 = T1RH; //加载 T1 重载值
TL1 = T1RL;
ET1 = 1; //使能 T1 中断
TR1 = 1; //启动 T1
}

/* 占空比调整函数,频率不变只调整占空比 */
void Adjust_duty(u16 duty)
{
u16 high, low;
high = (Period_Count * duty) / 100; //计算高电平所需的计数值
low = Period_Count - high; //计算低电平所需的计数值
high = 65536 - high + 12; //计算高电平的定时器重载值并补偿中断延时
low = 65536 - low + 12; //计算低电平的定时器重载值并补偿中断延时
HighRH = (unsigned char)(high >> 8); //高电平重载值拆分为高低字节
HighRL = (unsigned char)high;
LowRH = (unsigned char)(low >> 8); //低电平重载值拆分为高低字节
LowRL = (unsigned char)low;
}

/* T0 中断服务函数,产生 PWM 输出 */
void time0() interrupt 1
{
if(1 == PWMOUT) //当前输出为高电平时,装载低电平值并输出低电平
{
TH0 = LowRH;
TL0 = LowRL;
PWMOUT = 0;
}
else //当前输出为低电平时,装载高电平值并输出高电平
{
TH0 = HighRH;
TL0 = HighRL;
PWMOUT = 1;
}
}

/* T1 中断服务函数,定时动态调整占空比 */
void time1() interrupt 3
{
static bit dir = 0;
static u8 index = 0;
u8 code table[13] = {5, 18, 30, 41, 51, 60, 68, 75, 81, 86, 90, 93, 95}; //占空比调整表
TH1 = T1RH; //重新加载 T1 重载值
TL1 = T1RL;
Adjust_duty(table[index]); //调整 PWM 的占空比
if(0 == dir)
{
index++;
if(index >= 12)
{
dir = 1;
}
}
else
{
index--;
if(0 == index)
{
dir = 0;
}
}
}

UART 串口通信

UART 即通用异步收发器,串行通信通常用于单片机和电脑之间以及单片机和单片机之间的通信

串口通信相关术语

通信的方式可以分为多种,按照数据传送方式可分为串行通信并行通信。按照通信的数据同步方式,可分为异同通信同步通信。按照数据的传输方向又可分为单工半双工全双工通信

1
2
3
4
5
6
全双工:通信双方可以在同一时刻互相传输数据 
半双工:通信双方可以互相传输数据,但必须分时复用一根数据线
单工:通信只能有一方发送到另一方,不能反向传输
异步:通信双方各自约定通信速率
同步:通信双方靠一根时钟线来约定通信速率
总线:连接各个设备的数据传输线路(类似于一条马路,把路边各住户连接起来,使住户可以相互交流)

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

GND 表示单片机系统电源的参考地,TXD 是串行发送引脚,RXD 是串行接收引脚。两个单片机之间要通信,首先电源基准得一样

(1)串行通信

串行通信是指使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度。其只需要少数几条线就可以在系统间交换信息,特别适用于计算机与计算机、计算机与外设之间的远距离通信。如下图所示:

串行通信的特点:传输线少,长距离传送时成本低,且可以利用电话网等现成的设备,但数据的传送控制比并行通信复杂。

(2)并行通信

并行通信通常是将数据字节的各位用多条数据线同时进行传送,通常是 8 位、16 位、32 位等数据一起传输。如下图所示:

并行通信的特点:控制简单、传输速度快;由于传输线较多,长距离传送时成本高且接收方的各位同时接收存在困难,抗干扰能力差。

(3)异步通信

异步通信是指通信的发送与接收设备使用各自的时钟控制数据的发送和接收过程。为使双方的收发协调,要求发送和接收设备的时钟尽可能一致

异步通信是以字符(构成的帧)为单位进行传输,字符与字符之间的间隙(时间间隔)是任意的,但每个字符中的各位是以固定的时间传送的,即字符之间不 一定有“位间隔”的整数倍的关系,但同一字符内的各位之间的距离均为“ 位间隔”的整数倍。如下图所示:

异步通信的特点:不要求收发双方时钟的严格一致,实现容易,设备开销较小,但每个字符要附加 2~3 位用于起止位,各帧之间还有间隔,因此传输效率不高。

(4)同步通信

同步通信时要建立发送方时钟对接收方时钟的直接控制,使双方达到完全同步。此时,传输数据的位之间的距离均为“位间隔”的整数倍,同时传送的字符间不留间隙,即保持位同步关系,也保持字符同步关系。发送方对接收方的同步可以通过两种方法实现。如下图所示:

(5)单工通信

单工是指数据传输仅能沿一个方向,不能实现反向传输。如下图所示:

(6)半双工通信

半双工是指数据传输可以沿两个方向,但需要分时进行。如下图所示:

(7)全双工通信

全双工是指数据可以同时进行双向传输。如下图所示:

(8)通信速率

比特率是 每秒钟传输二进制代码的位数,单位是:位/秒( bps)。如每秒钟传送 240 个字符,而每个字符格式包含 10 位(1 个起始位、1 个停止位、8 个数据位),这时的比特率为:

10×240个/秒=2400bps10 \text{位} ×240 \text{个/秒} = 2400 bps

波特率: 它表示每秒钟传输了多少个码元。通信中常用时间间隔相同的符号来表示一个二进制数字,这样的信号称为码元。用 0V 表示数字 0,5V 表示数字 1,那么 一个码元可以表示两种状态 0 和 1,所以一个码元等于一个二进制比特位,此时波特率的大小与比特率一致;如果在通信传输中,有 0V、 2V、4V 以及 6V 分别表示二进制数 00、 01、 10、 11,那么每个码元可以表示四种状态,即两个二进制比特位,所以码元数是二进制比特位数的一半,这个时候的波特率为比特率的一半。由于很多常见的通信中一个码元都是表示两种状态,所以我们常常直接以 波特率来表示比特率

波特率就是发送二进制数据位的速率,习惯上用 baud 表示,即我们发送一位二进制数据的持续时间 = 1/baud。在通信之前,单片机 1 和单片机 2 首先都要明确的约定好它们之间的通信波特率,必须保持一致,收发双方才能正常实现通信

问:约定好速度后,我们还要考虑第二个问题,数据什么时候是起始,什么时候是结束呢?

不管是提前接收还是延迟接收,数据都会接收错误。在 UART 通信的时候, 一个字节是 8 位, 规定 当没有通信信号发生时,通信线路保持高电平,当要 发送数据之前,先发一位 0 表示起始位,然后 发送 8 位数据位,数据位是 先低后高的顺序,数据位发完后 再发一位 1 表示停止位,加起来一共发送 10位;而接收方呢,原本一直保持的高电平, 一旦检测到了一位低电平,那就知道了要 开始准备接收数据了,接收到 8 位数据位后,然后 检测到高电平(停止位),再 准备下一个数据的接收

RS232 通信接口

●RS232 通信接口 :就是台式电脑那些 “9针"和"9孔”(公头/母头)串行接口,虽然RS232也有 “RXD”,“TXD”,“GND”,但是却不能直接和单片机连接,因为它们的电平不相同,不是所有的电路都是 5V 代表高电平而 0V 代表低电平的,对于RS232标准来说,它是 “反逻辑”,即 “低电平代表的是 1,而高电平代表的是 0”,所以需要用一个电平转换芯片 “MAX232” 作为中间人将它们两电平互相转化从而可以互相通信

CH340T芯片

这个芯片就可以实现把标准 RS232 串口电平转换成我们单片机能够识别和承受的 UART 0V/5V 电平。其实 RS232 串口和 UART 串口,它们的协议类型是一样的,只是电平标准不同而已,而 MAX232 这个芯片起到的就是中间人的作用,它把 UART 电平转换成 RS232 电平,也把 RS232 电平转换成 UART 电平,从而实现标准 RS232 接口和单片机 UART 之间的通信连接

IO 口模拟 UART 串口通信

把 P3.0 和 P3.1 当做 IO 口来进行模拟实际串口通信的过程 (了解即可)

串口调试助手的使用

串口调试助手的实质就是利用电脑上的 UART 通信接口,发送数据给我们的单片机,也可以把我们的单片机发送的数据接收到这个调试助手界面上

配置波特率的时候,我们用的是定时器 T0 的模式 2(8位自动装载模式)。模式 2 中,不再是 TH0 代表高 8 位,TL0 代表低 8 位了,而只有 TL0 在进行计数,当 TL0 溢出后,不仅仅会让 TF0 变 1,而且还会将 TH0 中的内容重新自动装到 TL0 中,好处是:可以把想要的定时器初值提前存在 TH0 中,当 TL0 溢出后,TH0 自动把初值就重新送入 TL0 了,全自动的,不需要程序中再给 TL0 重新赋值 了

IO 口模拟 UART 串口通信(串口发送数据则接收该数据+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
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
#include <reg52.h>

typedef unsigned char u8;
typedef unsigned int u16;

sbit PIN_RXD = P3 ^ 0; //接收引脚定义
sbit PIN_TXD = P3 ^ 1; //发送引脚定义

bit RxdOrTxd = 0; //指示当前状态为接收还是发送
bit RxdEnd = 0; //接收结束标志
bit TxdEnd = 0; //发送结束标志
u8 RxdBuf = 0; //接收缓冲器
u8 TxdBuf = 0; //发送缓冲器

void ConfigUART(u16 baud);
void StartTXD(u8 dat);
void StartRXD();

void main()
{
EA = 1; //开总中断
ConfigUART(9600); //配置波特率为9600

while (1)
{
while (PIN_RXD); //等待接收引脚出现低电平,即起始位
StartRXD(); //启动接收
while (!RxdEnd); //等待接收完成
StartTXD(RxdBuf + 1); //接收到的数据+1后,发送回去
while (!TxdEnd); //等待发送完成
}
}

/* 串口配置函数,baud-通信波特率 */
void ConfigUART(u16 baud)
{
TMOD &= 0xF0; //清零T0的控制位
TMOD |= 0x02; //配置T0为模式2
TH0 = 256 - (11059200 / 12) / baud; //计算T0重载值
}

/* 启动串行接收 */
void StartRXD()
{
TL0 = 256 - ((256 - TH0) >> 1); //接收启动时的T0定时为半个波特率周期
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
RxdEnd = 0; //清零接收结束标志
RxdOrTxd = 0; //设置当前状态为接收
}

/* 启动串行发送,dat-待发送字节数据 */
void StartTXD(u8 dat)
{
TxdBuf = dat; //待发送数据保存到发送缓冲器
TL0 = TH0; //T0计数初值为重载值
ET0 = 1; //使能T0中断
TR0 = 1; //启动T0
PIN_TXD = 0; //发送起始位
TxdEnd = 0; //清零发送结束标志
RxdOrTxd = 1; //设置当前状态为发送
}

/* T0中断服务函数,处理串行发送和接收 */
void Timer0() interrupt 1
{
static u8 cnt = 0; //位接收或发送计数

if (RxdOrTxd) //串行发送处理
{
cnt++;
if (cnt <= 8) //低位在先依次发送8bit数据位
{
PIN_TXD = TxdBuf & 0x01; //&0x01表示发送最低位
TxdBuf >>= 1; //将次低位右移到最低位
}
else if (cnt == 9) //发送停止位
{
PIN_TXD = 1;
}
else //发送结束
{
cnt = 0; //复位bit计数器
TR0 = 0; //关闭T0
TxdEnd = 1; //置发送结束标志
}
}
else //串行接收处理
{
if (cnt == 0) //处理起始位
{
if (!PIN_RXD) //起始位为0时,清零接收缓冲器,准备接收数据位
{
RxdBuf = 0;
cnt++;
}
else //起始位不为0时,中止接收
{
TR0 = 0; //关闭T0
}
}
else if (cnt <= 8) //处理8位数据位
{
RxdBuf >>= 1; //低位在先,所以将之前接收的位向右移
if (PIN_RXD) //接收脚为1时,缓冲器最高位置1,
{
//而为0时不处理即仍保持移位后的0
RxdBuf |= 0x80;
}
cnt++;
}
else //停止位处理
{
cnt = 0; //复位bit计数器
TR0 = 0; //关闭T0
if (PIN_RXD) //停止位为1时,方能认为数据有效
{
RxdEnd = 1; //置接收结束标志
}
}
}
}

USB 转串口通信

●USB 转串口通信,笔记本跟单片机通信需要在电路上添加一个 “USB 转串口芯片”,就可以成功实现 USB 通信协议和标准UART 串行通信协议的转换

图中左下方 J1 和 J2 是两个跳线的组合,我们需要用跳线帽把中间和下边的针短接在一起。右侧的 CH340T 这个电路,把电源、晶振接好后,6 脚和 7 脚的 DP 和 DM 分别接 USB 口的 2 个数据引脚上去,3 脚和 4 脚通过跳线接到了单片机的 TXD 和 RXD 上去

串口相关寄存器

串口控制寄存器 SCON

SM2多机通信控制位,主要用于方式 2 和方式 3。当 SM2=1 时可以利用收到的 RB8 来控制是否激活 RI(RB8=0 时不激活 RI,收到的信息丢弃;RB8=1 时收到的数据进入 SBUF,并激活 RI,进而在中断服务中将数据从 SBUF 读走)。当 SM2=0 时,不论收到的 RB8 为 0 和 1,均可以使收到的数据进入 SBUF,并激活 RI (即此时 RB8 不具有控制 RI 激活的功能)。通过控制 SM2,可以实现多机通信。

REN允许串行接收位。由软件置 REN=1,则启动串行口接收数据;若软件置REN=0,则禁止接收。

TB8:在方式 2 或方式 3 中,是发送数据的第 9 位,可以用软件规定其作用。 可以用作数据的奇偶校验位,或在多机通信中,作为地址帧/数据帧的标志位。 在方式 0 和方式 1 中,该位未用到。

RB8:在方式 2 或方式 3 中,是接收到数据的第 9 位,作为奇偶校验位或地址帧/数据帧的标志位。在方式 1 时,若 SM2=0,则 RB8 是接收到的停止位

TI发送中断标志位。在方式 0 时,当串行发送第 8 位数据结束时,或在其它方式,串行发送停止位的开始时,由内部硬件使 TI 置 1,向 CPU 发中断申请。 在中断服务程序中,必须用软件将其清 0,取消此中断申请。

RI接收中断标志位。在方式 0 时,当串行接收第 8 位数据结束时,或在其它方式,串行接收停止位的中间时,由内部硬件使 RI 置 1,向 CPU 发中断申请。 也必须在中断服务程序中,用软件将其清 0,取消此中断申请。

SM0 和 SM1 为工作方式选择位:

模式1: 1 位起始位,8 位数据位和 1 位停止位

波特率发生器:波特率发生器只能由 定时器 T1 定时器 T2 产生,定时器 T1必须使用模式 2,也就是自动重装载模式

电源控制寄存器 PCON

SMOD波特率倍增位。在串口方式 1、方式 2、方式 3 时,波特率与 SMOD 有 关,当 SMOD=1 时,波特率提高一倍。复位时,SMOD=0。

SBUF寄存器

串口通信的发送和接收电路在物理上有 2 个名字相同的 SBUF 寄存器,它们的地址也都 是 0x99,但是一个用来做 发送缓冲,一个用来做 接收缓冲,每次只操作 SBUF,单片机会自动根据对它执行的是“”还是“”操作来选择是接收 SBUF 还是 发送 SBUF

1
2
//定时器的重载值计算公式为:
TH1 = TL1 = 256 - (晶振值 / 12 / 2 / 16 / 波特率)

256:定时器模式2 , 8 位定时器的溢出值,也就是 TL1 的溢出值
晶振值11059200
12:一个机器周期(STC89C52中一个时钟周期等于12个时钟周期)
16:串口模块将一位信号采集16次,将其中7,8,9次取出来,如果这三次中两次对如果是高电平就认为这位数据是1.
波特率:要设定的波特率

1
2
3
4
//电源管理寄存器 PCON,设置它可以将波特率提高一倍
PCON |= 0x80
//此时公式是:
TH1 = TL1 = 256 - (晶振值 / 12 / 16 / 波特率)

串口初始化步骤

  1. 配置串口为模式 1 (SCON 寄存器)

  2. 配置定时器 T1 为模式 2,即自动重装模式(TMOD 寄存器)

  3. 根据波特率计算 TH1 和 TL1 的初值,如果有需要可以使用 PCON 进行波特率加倍

  4. 打开定时器控制寄存器 TR1,让定时器跑起来

注意

  1. 在使用 T1 做波特率发生器的时候,千万不要再使能 T1 的 中断

  2. 因为接收和发送触发的是同一个串口中断,所以在串口中断函数中就必须先判断是哪种中断,然后再作出相应的处理(UART中断号是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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;

void UART_Init(u16 baud);

void main()
{
UART_Init(9600); //配置波特率为 9600
while(1);
}

/* 串口配置函数,baud-通信波特率 */
void UART_Init(u16 baud)
{
SCON = 0x50; //配置串口为模式 1 0101 0000 --> 0x50
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x20; //配置 T1 为模式 2 0010 0000 --> 0x20
TH1 = 256 - (11059200 / 12 / 32) / baud; //计算 T1 重载值
TL1 = TH1; //初值等于重载值
EA = 1; //使能总中断
ES = 1; //使能串口中断
ET1 = 0; //禁止 T1 中断
TR1 = 1; //启动 T1
}

/* UART 中断服务函数 */
void UART() interrupt 4
{
u8 rec_data; //定义一个变量接收数据
if (RI) //接收到字节
{
RI = 0; //串行接收停止位的中间时,内部硬件会将RI置1,要清除接收中断标志位,需要清0,取消此中断申请
rec_data = SBUF; //存储接收到的数据
SBUF = rec_data + 1; //将接收到的 数据 + 1 放入到发送寄存器
}
if (TI) //字节发送完毕,发送完成 TI 会自动置 1
{
TI = 0; //串行发送停止位的开始时,内部硬件会将TI置 1,要清除发送完成标志位,需要清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
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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#define LED_SMG_PORT P0 //宏定义LED和数码管端口

sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;

u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 SMG_BUFF[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管缓存区,初值0xFF
u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节
u8 RxdByte = 0; //串口接收到的字节

void Time0_Init(u16 ms);
void UART_Init(u16 baud);
void Smg_Scan();

void main()
{
ENLED = 0; //选择数码管
ADDR3 = 1;
Time0_Init(1); //配置 T0 定时 1ms
UART_Init(9600); //配置波特率为 9600
while(1)
{
//将接收字节在数码管上以十六进制形式显示出来
SMG_BUFF[0] = gsmg[RxdByte & 0x0F]; //低四位
SMG_BUFF[1] = gsmg[RxdByte >> 4]; //高四位
}
}

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

/* 串口配置函数,baud-通信波特率 */
void UART_Init(u16 baud)
{
SCON = 0x50; //配置串口为模式 1 0101 0000 --> 0x50
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x20; //配置 T1 为模式 2 0010 0000 --> 0x20
TH1 = 256 - (11059200 / 12 / 32) / baud; //计算 T1 重载值
TL1 = TH1; //初值等于重载值
EA = 1; //使能总中断
ES = 1; //使能串口中断
ET1 = 0; //禁止 T1 中断
TR1 = 1; //启动 T1
}

/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void Smg_Scan()
{
static u8 i; //动态扫描索引
LED_SMG_PORT = 0xFF; //关闭所有段选位,显示消隐
//先将P1的第三位取0(低三位与0进行与运算),然后再与i进行或运算,如果i为1,P1第三位(也就是ADDR2-ADDR0)为001,以此类推
P1 = (P1 & 0xF8) | i; //位选索引值赋值到P1口低3位
LED_SMG_PORT = SMG_BUFF[i]; //缓冲区中索引位置的数据送到数码管端口
if(i < 5) //索引递增循环,遍历整个缓冲区
i++;
else
i = 0;
}

/* T0 中断服务函数,完成数码管扫描 */
void time0() interrupt 1
{
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
Smg_Scan(); //数码管动态扫描刷新函数
}

/* UART 中断服务函数 */
void UART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0; //串行接收停止位的中间时,内部硬件会将RI置1,要清除接收中断标志位,需要清0,取消此中断申请
RxdByte = SBUF; //接收到的数据保存到接收字节变量中
SBUF = RxdByte; //接收到的数据又直接发回,叫作-"echo",用以提示用户输入的信息是否已正确接收
}
if (TI) //字节发送完毕,发送完成 TI 会自动置 1
{
TI = 0; //串行发送停止位的开始时,内部硬件会将TI置 1,要清除发送完成标志位,需要清0,取消此中断申请
}
}

问:用文本格式直接发送一个“12”,串口调试助手返回十六进制显示的是 31、32 两个数据,而数码管显示的是 32,为什么?

对于 ASCII 码表来说,数字本身是字符而非数据,所以如果发送“12”的话,实际上是分别发送了“1”和“2”两个字符,单片机呢,先收到第一个字符“1”,在数码管上会显示出 31 这个对应数字,但是马上就又收到了“2”这个字符,数码管瞬间从 31 变成了 32, 而我们视觉上呢,是没有办法发现这种快速变化的,所以我们感觉数码管直接显示的是 32

接到任意字节数据后改变流水灯流水的方向,并且将数据显示在数码管上

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#define LED_SMG_PORT P0 //宏定义LED和数码管端口

sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;

u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 SMG_BUFF[7] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管缓存区,初值0xFF
u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节
u8 RxdByte = 0; //串口接收到的字节
u8 flag = 0; //1s定时标志
u8 dirflag = 0; //流水灯流动方向标志位(0代表左移,1代表右移)

void Config_Time0(u16 ms);
void UART_Init(u16 baud);
void Smg_Scan();
void LED_Fall(u8 dir);

void main()
{
ENLED = 0; //选择数码管
ADDR3 = 1;
Config_Time0(1); //配置 T0 定时 1ms
UART_Init(9600); //配置波特率为 9600
while(1)
{
if(0 == dirflag) //如果流动方向标志位为0
{
if(1 == flag) //1s定时标志
{
flag = 0; //标志位清0
LED_Fall(0); //左移
}
}
else if(1 == dirflag) //如果流动方向标志位为1
{
if(1 == flag) //1s定时标志
{
flag = 0; //标志位清0
LED_Fall(1); //右移
}
}
//将接收字节在数码管上以十六进制形式显示出来
SMG_BUFF[0] = gsmg[RxdByte & 0x0F]; //低四位
SMG_BUFF[1] = gsmg[RxdByte >> 4]; //高四位
}
}

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

/* 串口配置函数,baud-通信波特率 */
void UART_Init(u16 baud)
{
SCON = 0x50; //配置串口为模式 1 0101 0000 --> 0x50
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x20; //配置 T1 为模式 2 0010 0000 --> 0x20
TH1 = 256 - (11059200 / 12 / 32) / baud; //计算 T1 重载值
TL1 = TH1; //初值等于重载值
EA = 1; //使能总中断
ES = 1; //使能串口中断
ET1 = 0; //禁止 T1 中断
TR1 = 1; //启动 T1
}

/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void Smg_Scan()
{
static u8 i; //动态扫描索引
LED_SMG_PORT = 0xFF; //关闭所有段选位,显示消隐
//先将P1的第三位取0(低三位与0进行与运算),然后再与i进行或运算,如果i为1,P1第三位(也就是ADDR2-ADDR0)为001,以此类推
P1 = (P1 & 0xF8) | i; //位选索引值赋值到P1口低3位
LED_SMG_PORT = SMG_BUFF[i]; //缓冲区中索引位置的数据送到数码管端口
if(i < 6) //索引递增循环,遍历整个缓冲区
i++;
else
i = 0;
}

/* 控制LED流水灯流动方向函数 */
void LED_Fall(u8 dir)
{
if(0 == dir)
{
static u8 Led_state = 0x01; //LED状态
//注意这里要把LED的状态变量赋给Led_Buff[6],因为数码管和LED共用端口,所以这里不能直接赋给P0
SMG_BUFF[6] = ~Led_state; //取反赋给数组里的第七个元素(也就是使能LEDS6,才可以点亮LED)
Led_state <<= 1; //左移一位
if(0 == Led_state)
{
Led_state = 0x01; //左移到最高位后重新设置为第一个LED灯
}
}
else if(1 == dir)
{
static u8 Led_state = 0x80; //LED状态
//注意这里要把LED的状态变量赋给Led_Buff[6],因为数码管和LED共用端口,所以这里不能直接赋给P0
SMG_BUFF[6] = ~Led_state; //取反赋给数组里的第七个元素(也就是使能LEDS6,才可以点亮LED)
Led_state >>= 1; //右移一位
if(0 == Led_state)
{
Led_state = 0x80; //右移到最低位后重新设置为第八个LED灯
}
}
}

/* T0 中断服务函数,完成数码管扫描 */
void time0() interrupt 1
{
static u16 count = 0;
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
Smg_Scan(); //数码管动态扫描刷新函数
count++;
if(count >= 1000) //1s
{
count = 0;
flag = 1;
}
}

/* UART 中断服务函数 */
void UART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0; //串行接收停止位的中间时,内部硬件会将RI置1,要清除接收中断标志位,需要清0,取消此中断申请
RxdByte = SBUF; //接收到的数据保存到接收字节变量中
SBUF = RxdByte; //接收到的数据又直接发回,叫作-"echo",用以提示用户输入的信息是否已正确接收
dirflag = !dirflag; //每收到一个字节改变一次流水灯标志位
}
if (TI) //字节发送完毕,发送完成 TI 会自动置 1
{
TI = 0; //串行发送停止位的开始时,内部硬件会将TI置 1,要清除发送完成标志位,需要清0,取消此中断申请
}
}

接到任意字节数据后改变流水灯流水的方向,并且将数据显示在数码管上,当接收到大写 “B”,蜂鸣器响(要用文本模式发送才有效)

这个程序就是在上面代码的基础上加,先定义一个flagBuzz控制蜂鸣器状态的(0表示关),在串口中断那里,当接收到数据为" B "时,flagBuzz置1,在定时器0那一直扫描,当flagBuzz == 1 时,蜂鸣器取反(响),当flagBuzz == 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
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#define LED_SMG_PORT P0 //宏定义LED和数码管端口

sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;
sbit BUZZ = P1 ^ 6;

u8 gsmg[10] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90}; //数码管1-9
u8 SMG_BUFF[7] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管缓存区,初值0xFF
u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节
u8 RxdByte = 0; //串口接收到的字节
u8 flag = 0; //1s定时标志
u8 dirflag = 0; //流水灯流动方向标志位(0代表左移,1代表右移)
u8 flagBuzz = 0;//蜂鸣器控制标志

void Config_Time0(u16 ms);
void UART_Init(u16 baud);
void Smg_Scan();
void LED_Fall(u8 dir);

void main()
{
ENLED = 0; //选择数码管
ADDR3 = 1;
Config_Time0(1); //配置 T0 定时 1ms
UART_Init(9600); //配置波特率为 9600
while(1)
{
if(0 == dirflag) //如果流动方向标志位为0
{
if(1 == flag) //1s定时标志
{
flag = 0; //标志位清0
LED_Fall(0); //左移
}
}
else if(1 == dirflag) //如果流动方向标志位为1
{
if(1 == flag) //1s定时标志
{
flag = 0; //标志位清0
LED_Fall(1); //右移
}
}
//将接收字节在数码管上以十六进制形式显示出来
SMG_BUFF[0] = gsmg[RxdByte & 0x0F]; //低四位
SMG_BUFF[1] = gsmg[RxdByte >> 4]; //高四位
}
}

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

/* 串口配置函数,baud-通信波特率 */
void UART_Init(u16 baud)
{
SCON = 0x50; //配置串口为模式 1 0101 0000 --> 0x50
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x20; //配置 T1 为模式 2 0010 0000 --> 0x20
TH1 = 256 - (11059200 / 12 / 32) / baud; //计算 T1 重载值
TL1 = TH1; //初值等于重载值
EA = 1; //使能总中断
ES = 1; //使能串口中断
ET1 = 0; //禁止 T1 中断
TR1 = 1; //启动 T1
}

/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void Smg_Scan()
{
static u8 i; //动态扫描索引
LED_SMG_PORT = 0xFF; //关闭所有段选位,显示消隐
//先将P1的第三位取0(低三位与0进行与运算),然后再与i进行或运算,如果i为1,P1第三位(也就是ADDR2-ADDR0)为001,以此类推
P1 = (P1 & 0xF8) | i; //位选索引值赋值到P1口低3位
LED_SMG_PORT = SMG_BUFF[i]; //缓冲区中索引位置的数据送到数码管端口
if(i < 6) //索引递增循环,遍历整个缓冲区
i++;
else
i = 0;
}

/* 控制LED流水灯流动方向函数 */
void LED_Fall(u8 dir)
{
if(0 == dir)
{
static u8 Led_state = 0x01; //LED状态
//注意这里要把LED的状态变量赋给Led_Buff[6],因为数码管和LED共用端口,所以这里不能直接赋给P0
SMG_BUFF[6] = ~Led_state; //取反赋给数组里的第七个元素(也就是使能LEDS6,才可以点亮LED)
Led_state <<= 1; //左移一位
if(0 == Led_state)
{
Led_state = 0x01; //左移到最高位后重新设置为第一个LED灯
}
}
else if(1 == dir)
{
static u8 Led_state = 0x80; //LED状态
//注意这里要把LED的状态变量赋给Led_Buff[6],因为数码管和LED共用端口,所以这里不能直接赋给P0
SMG_BUFF[6] = ~Led_state; //取反赋给数组里的第七个元素(也就是使能LEDS6,才可以点亮LED)
Led_state >>= 1; //右移一位
if(0 == Led_state)
{
Led_state = 0x80; //右移到最低位后重新设置为第八个LED灯
}
}
}

/* T0 中断服务函数,完成数码管扫描 */
void time0() interrupt 1
{
static u16 count = 0;
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
Smg_Scan(); //数码管动态扫描刷新函数
count++;
if(count >= 1000) //1s
{
count = 0;
flag = 1;
}
if(1 == flagBuzz) //如果蜂鸣器控制标志为1
{
BUZZ = ~BUZZ; //蜂鸣器响
}
else
BUZZ = 1; //蜂鸣器关闭
}

/* UART 中断服务函数 */
void UART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0; //串行接收停止位的中间时,内部硬件会将RI置1,要清除接收中断标志位,需要清0,取消此中断申请
RxdByte = SBUF; //接收到的数据保存到接收字节变量中
SBUF = RxdByte; //接收到的数据又直接发回,叫作-"echo",用以提示用户输入的信息是否已正确接收
dirflag = !dirflag; //每收到一个字节改变一次流水灯标志位
if('B' == RxdByte) //如果接收到大写字母B蜂鸣器控制标志
{
flagBuzz = 1; //蜂鸣器控制标志置1
}
else
flagBuzz = 0; //否则蜂鸣器控制标志置0
}
if (TI) //字节发送完毕,发送完成 TI 会自动置 1
{
TI = 0; //串行发送停止位的开始时,内部硬件会将TI置 1,要清除发送完成标志位,需要清0,取消此中断申请
}
}

接收上位机下发的命令,根据命令值分别把不同数组的数据回发给上位机

程序用到了指针的自增运算,也就是+1 运算,还有sizeof()

这个程序还应用到一个小技巧,前边讲了串口发送中断标志位 TI 是硬件置位,软件清零的。如果我们想一次发送多个数据的时候,就需要把第一个字节写入 SBUF,然后再等待发送中断,在后续中断中再发送剩余的数据,这样我们的数据发送过程就被拆分到了两个地方——主循环内和中断服务函数内,无疑就使得程序结构变得零散了。所以我们可以改成在启动发送的时候,不是向 SBUF 中写入第一个待发的字节,而是直接让 TI = 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
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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名

bit cmdArrived = 0; //命令到达标志,即接收到上位机下发的命令

u8 cmdIndex = 0; //命令索引,即与上位机约定好的数组编号
u8 cntTxd = 0; //串口发送计数器
u8 *ptrTxd; //串口发送指针

u8 array1[1] = {1};
u8 array2[2] = {1, 2};
u8 array3[4] = {1, 2, 3, 4};
u8 array4[8] = {1, 2, 3, 4, 5, 6, 7, 8};

void UART_Init(u16 baud);

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

/* UART 中断服务函数 */
void UART() interrupt 4
{
if (RI) //接收到字节
{
RI = 0; //串行接收停止位的中间时,内部硬件会将RI置1,要清除接收中断标志位,需要清0,取消此中断申请
cmdIndex = SBUF; //接收到的数据保存到命令索引中
cmdArrived = 1; //设置命令到达标志
}
if (TI) //字节发送完毕,发送完成 TI 会自动置 1
{
TI = 0; //串行发送停止位的开始时,内部硬件会将TI置 1,要清除发送完成标志位,需要清0,取消此中断申请
if (cntTxd > 0) //有待发送数据时,继续发送后续字节
{
SBUF = *ptrTxd; //发出指针指向的数据
cntTxd--; //发送计数器递减
ptrTxd++; //发送指针递增
}
}
}

1602液晶

数据手册重要内容

1602 液晶可以显示 2 行,每行 16 个 字符,这个 2mA 仅仅是指液晶,而它的黄绿背光都是用 LED 做的,所以功耗 不会太小的, 一二十毫安 还是有的

1602 液晶引脚功能

编号 符号 引脚说明 编号 符号 引脚说明
1 VSS 电源地 9 D2 Data I/O
2 VDD 电源正极 10 D3 Data I/O
3 VL 液晶显示偏压信号 11 D4 Data I/O
4 RS 数据/命令选择端(H/L) 12 D5 Data I/O
5 R/W 读/写选择端(H/L) 13 D6 Data I/O
6 E 使能信号 14 D7 Data I/O
7 D0 Data I/O 15 BLA 背光源正极
8 D1 Data I/O 16 BLK 背光源负极

● 液晶的电源 1 脚 2 脚以及背光电源 15 脚 16 脚,正常接即可
3 脚叫做液晶显示偏压信号,调整显示的黑点和不显示的之间的对比度,调好就清晰多了
4 脚数据命令选择端 (这个引脚接到了 ADDR0 上,通过跳线帽和 P1.0 连接);高电平是数据,低电平是命令
5 脚读写选择端 (这个引脚接到了 ADDR1 上,通过跳线帽和 P1.1 连接);高电平是读,低电平是写
6 脚使能信号 (很重要!),液晶的读写命令和数据,都要靠它才能正常读写,这个引脚通过跳线帽接到了 ENLCD 上 (P1.5管脚)
7 到 14 引脚就是 8 个数据引脚了,通过这 8 个引脚读写数据和命令 (接到了P0口)

1602原理图

1602 液晶的读写时序

1602 内部 RAM 结构图

第一行的地址是 0x00H 到 0x27,第二行的地址从 0x40 到 0x67,其中第一行 0x00 到 0x0F 是与液晶上第一行 16 个字符显示位置相对应的,第二行 0x40 到 0x4F 是与第二行 16 个字符显示位置相对应的。而每行都多出来一部分,是为了显示移动字幕设置的

1602 液晶状态字

液晶有一个状态字字节,通过读取这个状态字的内容,可以知道 1602 液晶的一些内部情况

这个状态字节有 8 个位最高位表示了当前液晶是不是,如果这个位是 1 表示液 晶正“忙”禁止读写数据或者命令,如果是 0,则可以进行读写。而低 7 位就表示了当前数据地址指针的位置

1602 的基本操作时序,一共有 4 个(单片机读外部状态前,必须先保证自己是高电平)

首先把用到的总线接口做一个统一声明

1
2
3
4
#define LCD1602_DB P0 	  //根据原理图可以知道P0组是控制上面8个状态字
sbit LCD1602_RS = P1^0; //数据选择端
sbit LCD1602_RW = P1^1; //读写选择端
sbit LCD1602_E = P1^5; //使能信号
  1. 读状态:RS = L,R/W = H,E = H

因为P0口总线也是流水灯数码管等等共用的,读取后如果一直是高电平会影响其他外设,所以通常要把这个引脚拉低来释放总线,这里用了一个 do…while 循环语句来实现(判断while是否为1,为1则执行上面的语句,不为1则退出循环)

通过判断 sta 最高位的值来了解当前液晶是否处于状态,也可以得知当前数据的指针位置,如果当前读到的状态是不忙,那么程序可以进行读写操作,如果当前状态是,那么还得继续等待重新判断液晶的状态

1
2
3
4
5
6
7
8
LCD1602_DB = 0xFF;    
LCD1602_RS = 0;
LCD1602_RW = 1;
do{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}while (sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
  1. 读数据:RS = H,R/W = H,E = H (不常用)

  2. 写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲

这个指令一共有 4 条语句,其中前三条语句顺序无所谓,但是 E = 高脉冲这一句很关键。E = 高脉冲,意思就是:E 使能引脚先从低拉高,再从高拉低,形成一个高脉冲

  1. 写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲

写数据和写指令是类似的,就是把 RS 改成 H,把总线改成数据即可

注:这里用的1602液晶所使用的接口时序是摩托罗拉公司所创立的 6800时序 ,还有另外一种时序是 Intel 公司的 8080时序,也有部分液晶模块采用,只是相对来说比较少见

1602 液晶的使能引脚 E, 高电平的时候是 有效,低电平的时候是 无效,前面也提到了高电平时会影响 P0 口,因此正常情况下,如果我们没有使用液晶的话,那么程序开始写一句 LCD1602_E=0,就可以避免1602 干扰到其它外设。之前的程序没有加这句,是因为板子在这个引脚上加了一个 15K 的下拉电阻,这个下拉电阻就可以保证这个引脚上电后默认是 低电平(但是在实际开发过程中,就不必要这样了。如果这是个实际产品,能用软件去处理的,我们就不会用硬件去实现)

1602 液晶的指令

  1. 显示模式设置

写指令 0x38设置 16x2 显示,5x7 点阵,8 位数据接口。这条指令对这个1602液晶来说是固定的,必须写 0x38(仔细看会发现我们的液晶实际上内部点阵是 5x8 的)

  1. 显示开/关以及光标设置指令

这里有 2 条指令,第一条指令,一个字节中 8 位,其中高 5 位是固定的 0b00001,低 3位分别用 DCB 从高到低表示D=1 表示开显示, D=0 表示关显示; C=1 表示显示光标, C=0 表示不显示光标; B=1 表示光标闪烁, B=0 表示光标不闪烁;第二条指令,高 6 位是固定的 0b000001,低 2 位分别用 NS 从高到低表示,其中 N=1 表示读或者写一个字符后,指针自动加 1,光标自动加 1, N=0 表示读或者写一个字符 后指针自动减 1,光标自动减 1; S=1 表示写一个字符后,整屏显示左移(N=1)或右移(N=0),以达到光标不移动而屏幕移动的效果,而 S=0 表示写一 个字符后,整屏显示不移动

数据控制

  1. 清屏指令

写入 0x01 表示显示清屏,其中包含了数据指针清零,所有的显示清零。写入 0x02 则仅仅是数据指针清零,显示不清零

  1. RAM 地址设置指令

该指令码的 最高位为 1低 7 位为 RAM 的地址,RAM 地址与液晶上字符的关系如上映射图所示。通常,我们在读写数据之前都要先设置好地址,然后再进行数据的读写操作

通信时序解析

所谓“时序”从字面意义上来理解,一是 时间问题,二是 顺序问题

读操作时序

RS 引脚和 R/W 引脚,这两个引脚先进行变化,因为是读操作,所以 R/W 引脚首先要置为高电平,而不管它原来是什么。(读指令还是读数据,都是读操作,而且都有可能,所以 RS 引脚既有可能是置为高电平,也有可能是置为低电平),而 RS 和 R/W 变化了经过 Tsp1 这么长时间后,使能引脚 E 才能从低电平到高电平发生变化,经过了 tD 这么长时间后,LCD1602 输出 DB 的数据就是有效数据了(就可以来读取 DB 的数据了),读完了之后,我们要先把使能 E 拉低,经过一段时间后 RS、R/W 和 DB 才可以变化继续为下一次读写做准备了

写操作时序

写操作时序和读操作时序的差别,就是写操作时序中,DB 的改变是由单片机来完成的,因此要放到使能引脚 E 的变化之前进行操作

  1. 注意时间轴,如果没有标明(其实大部分也都是不标明的),那么从左往右的方向为时间正向轴,即时间在增长
  2. 上图框出并注明了看懂此图的一些常识:

(1) 时序图最左边一般是某一根引脚的标识,表示此行图线体现该引脚的变化,上图分别标明了RS、R/W、E、DB0~DB7四类引脚的时序变化

(2) 有线交叉状的部分,表示电平在变化,如上所标注

(3) 两条平行线分别对应高低电平,也正好吻合(2)中电平变化的说法

(4) 上图中,密封的菱形部分,注意要密封,表示数据有效,Valid Data这个词也显示了这点

  1. 需要十分注意的是,时序图里各个引脚的电平变化,基于的时间轴是一致的。一定要严格按照时间轴的增长方向来精确地观察时序图。要让器件严格的遵守时序图的变化。在类似于18B20这样的单总线器件对此要求尤为严格

  2. 以上几点,并不是LCD1602的时序图所特有的,绝大部分的时序图都遵循着这样的一般规则,所以要慢慢的习惯于这样的规则

时序图时间标签

tC:指的是使能引脚 E 从本次上升沿到下次上升沿的最短时间是 400ns

tPW:指的是使能引脚 E 高电平的持续时间最短是 150ns

tR tF:指的是使能引脚 E 的上升沿时间和下降沿时间,不能超过 25ns

tSP1:指的是 RS 和 R/W 引脚使能后至少保持 30ns,使能引脚 E 才可以变成高电平

tHD1:指的是使能引脚 E 变成低电平后,至少保持 10ns 之后,RS 和 R/W 才能进行变化

tD:指的是使能引脚 E 变成高电平后,最多 100ns 后,1602 就把数据送出来

tHD2:指的是读操作过程中,使能引脚 E 变成低电平后,至少保持 20ns,DB 数据总线才可以进行变化

tSP2:指的是 DB 数据总线准备好后,至少保持 40ns,使能引脚 E 才可以从低到高进行使能变化

tHD2:指的是写操作过程中,要引脚 E 变成低电平后,至少保持 10ns,DB 数据总线才可以变化

要懂得估计主控芯片的指令时间,可以在官方数据手册上查到MCU的一些级别参数。比如我们现在用STC51做为主控芯片,外部11.0592MHz晶振,指令周期就是一个时钟周期为(1/11.0592MHz)s,所以至少确定了它执行一条指令的时间是us级别的。我们看到,以上给的时间参数全部是ns级别的,所以即便我们在程序里不加延时程序,也应该可以很好的配合LCD1602的时序要求了

程序

在LCD1602上显示字符串

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
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
#define LCD1602_DB P0 //宏定义1602端口

sbit LCD1602_RS = P1 ^ 0;
sbit LCD1602_RW = P1 ^ 1;
sbit LCD1602_E = P1 ^ 5;

void Lcd1602_Init();
void Lcd_ShowStr(u8 x, u8 y, u8 *str);

void main()
{
Lcd1602_Init(); //LCD初始化
Lcd_ShowStr(5, 0, "Hello"); //在液晶上显示字符串
Lcd_ShowStr(0, 1, "You are the best");
while(1);
}

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0') //连续写入字符串数据,直到检测到结束符
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

左移字符串

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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#include <reg52.h>

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;
#define LCD1602_DB P0 //宏定义1602端口

sbit LCD1602_RS = P1 ^ 0;
sbit LCD1602_RW = P1 ^ 1;
sbit LCD1602_E = P1 ^ 5;

bit flag = 0; //500ms 定时标志
u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

//待显示的第一行字符串
u8 code str1[] = "Come on!";
//待显示的第二行字符串,需保持与第一行字符串等长,较短的行可用空格补齐
u8 code str2[] = "Amazing!";

void Time0_Init(u16 ms);
void Lcd1602_Init();
void Lcd_ShowStr(u8 x, u8 y, u8 *str, u8 len);

void main()
{
u8 i;
u8 index = 0; //移动索引
u8 pdata bufMove1[16 + sizeof(str1) + 16]; //移动显示缓冲区 1(液晶一共16个字节,在移动字符串前后各加上16个空格)
u8 pdata bufMove2[16 + sizeof(str2) + 16]; //移动显示缓冲区 2

Time0_Init(10); //配置 T0 定时 10ms
Lcd1602_Init(); //初始化液晶

//缓冲区开头一段填充为空格
for(i = 0; i < 16; i++)
{
bufMove1[i] = ' ';
bufMove2[i] = ' ';
}
//待显示字符串拷贝到缓冲区中间位置
for(i = 0; i < (sizeof(str1) - 1); i++) //i = 字符串长度,因为i从0算起,所以要 -1
{
bufMove1[16 + i] = str1[i];
bufMove2[16 + i] = str2[i];
}
//缓冲区结尾一段也填充为空格 (i初值为前面的16个空格加上字符串的长度,i要小于一整个缓冲区的长度 )
for(i = (16 + sizeof(str1) - 1); i < sizeof(bufMove1); i++)
{
bufMove1[i] = ' ';
bufMove2[i] = ' ';
}

while(1)
{
if(flag) //每 500ms 移动一次屏幕
{
flag = 0;
//从缓冲区抽出需显示的一段字符显示到液晶上
Lcd_ShowStr(0, 0, bufMove1 + index, 16);
Lcd_ShowStr(0, 1, bufMove2 + index, 16);
//移动索引递增,实现左移
index++;
if(index >= (16 + sizeof(str1) - 1))
{
//起始位置达到字符串尾部后即返回从头开始
index = 0;
}
}
}
}

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

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针,len-需显示的字符长度 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str, u8 len)
{
Lcd_SetCursor(x, y); //设置起始地址
while(len--) //连续写入 len 个字符数据
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

/* T0 中断服务函数,定时 500ms */
void time0() interrupt 1
{
static u8 count = 0;
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
count++;
if(count >= 50)
{
count = 0;
flag = 1;
}
}

计算器实例

计算器不考虑连加,连减等连续计算,不考虑小数情况。加减乘除分别用上下左右来替代,回车表示等于,ESC 表示归 0

main.c 文件实现所有应用层的操作函数,即计算器功能所需要信息显示、按键动作响应等,另外还包括主循环和定时中断的调度

key.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
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
#include "AllHead.h"

u8 code KeyCodeMap[4][4] = //矩阵按键编号到标准键盘键码的映射表
{
{ '1', '2', '3', 0x26 }, //数字键 1、数字键 2、数字键 3、向上键
{ '4', '5', '6', 0x25 }, //数字键 4、数字键 5、数字键 6、向左键
{ '7', '8', '9', 0x28 }, //数字键 7、数字键 8、数字键 9、向下键
{ '0', 0x1B, 0x0D, 0x27 } //数字键 0、ESC 键、 回车键、 向右键
};
u8 pdata Key_State[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}};//按键当前状态

/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void Key_Driver()
{
u8 i, j;
static u8 backup[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //按键值备份,保存前一次的值
for(i = 0; i < 4; i++) //循环扫描 4*4 的矩阵按键
{
for(j = 0; j < 4; j++)
{
if(backup[i][j] != Key_State[i][j]) //当前值与前次值不相等说明此时按键有动作
{
if(backup[i][j] != 0) //前次值不等于0,则当前值等于0,按键按下
{
Key_Action(KeyCodeMap[i][j]); //调用按键动作函数
}
backup[i][j] = Key_State[i][j]; //更新前一次的备份值
}
}
}
}

/* 按键扫描函数,需在定时中断中调用 */
void Key_Scan()
{
u8 i = 0;
static u8 KeyOut = 0; //矩阵按键扫描输出索引
static u8 keybuf[4][4] = {{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
}; //矩阵按键扫描缓冲区
//将一行的 4 个按键值移入缓冲区
keybuf[KeyOut][0] = (keybuf[KeyOut][0] << 1) | KeyIn1;
keybuf[KeyOut][1] = (keybuf[KeyOut][1] << 1) | KeyIn2;
keybuf[KeyOut][2] = (keybuf[KeyOut][2] << 1) | KeyIn3;
keybuf[KeyOut][3] = (keybuf[KeyOut][3] << 1) | KeyIn4;
//消抖后更新按键状态
for (i = 0; i < 4; i++) //每行 4 个按键,所以循环 4 次
{
if (0x00 == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
Key_State[KeyOut][i] = 0;
}
else if (0x0F == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
Key_State[KeyOut][i] = 1;
}
}
//执行下一次的扫描输出
KeyOut++; //输出索引递增
if (KeyOut >= 4)
{
KeyOut = 0;
}
// KeyOut = KeyOut & 0x03; //索引值加到 4 即归零
switch(KeyOut) //根据索引,释放当前输出引脚,拉低下次的输出引脚
{
case 0:
KeyOut4 = 1;
KeyOut1 = 0;
break;
case 1:
KeyOut1 = 1;
KeyOut2 = 0;
break;
case 2:
KeyOut2 = 1;
KeyOut3 = 0;
break;
case 3:
KeyOut3 = 1;
KeyOut4 = 0;
break;
default:
break;
}
}

key.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef _KEY_H_
#define _KEY_H_

sbit KeyIn1 = P2^4;
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;
sbit KeyOut1 = P2^3;
sbit KeyOut2 = P2^2;
sbit KeyOut3 = P2^1;
sbit KeyOut4 = P2^0;

extern void Key_Driver();
extern void Key_Scan();
extern void Key_Action(u8 keycode);

#endif

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针,len-需显示的字符长度 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 区域清除,清除从(x,y)坐标起始的 len 个字符位 */
void Lcd_AreaClear(u8 x, u8 y, u8 len)
{
Lcd_SetCursor(x, y); //设置起始地址
while(len--) //连续写入空格
{
Lcd_WriteDat(' ');
}
}

/* 整屏清除 */
void Lcd_FullClear()
{
Lcd_WriteCmd(0x01);
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd_AreaClear(u8 x, u8 y, u8 len);
extern void Lcd_FullClear();
extern void Lcd1602_Init();

#endif

time.c

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

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

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

time.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Time0_Init(u16 ms);

#endif

main.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
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
#include "AllHead.h"

u8 step = 0; //操作步骤
u8 oprt = 0; //运算类型
signed long num1 = 0; //操作数 1
signed long num2 = 0; //操作数 2
signed long result = 0; //运算结果

void main()
{
Time0_Init(1); //配置 T0 定时 1ms
Lcd1602_Init(); //初始化液晶
Lcd_ShowStr(15, 1, "0"); //初始显示一个数字 0

while(1)
{
Key_Driver(); //调用按键驱动
}
}

/* 长整型数转换为字符串,str-字符串指针,dat-待转换数,返回值-字符串长度 */
u8 LongToString(u8 *str, signed long dat)
{
signed char i = 0;
u8 len = 0;
u8 buf[12];
if(dat < 0) //如果为负数,首先取绝对值,并在指针上添加负号
{
dat = -dat;
*str++ = '-';
len++;
}
do //先转换为低位在前的十进制数组
{
buf[i++] = dat % 10;
dat /= 10;
}
while(dat > 0);
len += i; //i 最后的值就是有效字符的个数
while(i-- > 0) //将数组值转换为 ASCII 码反向拷贝到接收指针上
{
*str++ = buf[i] + '0';
}
*str = '\0'; //添加字符串结束符
return len; //返回字符串长度
}

/* 显示运算符,显示位置 y,运算符类型 type */
void ShowOprt(u8 y, u8 type)
{
switch(type)
{
case 0:
Lcd_ShowStr(0, y, "+");
break; //0 代表+
case 1:
Lcd_ShowStr(0, y, "-");
break; //1 代表-
case 2:
Lcd_ShowStr(0, y, "*");
break; //2 代表*
case 3:
Lcd_ShowStr(0, y, "/");
break; //3 代表/
default:
break;
}
}

/* 计算器复位,清零变量值,清除屏幕显示 */
void Reset()
{
num1 = 0;
num2 = 0;
step = 0;
Lcd_FullClear();
}

/* 数字键动作函数,n-按键输入的数值 */
void NumKey_Action(u8 num)
{
u8 len = 0;
u8 str[12];
if(step > 1) //如计算已完成,则重新开始新的计算
{
Reset();
}
if(0 == step) //输入第一操作数
{
num1 = num1 * 10 + num; //输入数值累加到原操作数上
len = LongToString(str, num1); //新数值转换为字符串
Lcd_ShowStr(16 - len, 1, str); //显示到液晶第二行上
}
else //输入第二操作数
{
num2 = num2 * 10 + num; //输入数值累加到原操作数上
len = LongToString(str, num2); //新数值转换为字符串
Lcd_ShowStr(16 - len, 1, str); //显示到液晶第二行上
}
}

/* 运算符按键动作函数,运算符类型 type */
void Oprt_KeyAction(u8 type)
{
u8 len = 0;
u8 str[12];
if(0 == step) //第二操作数尚未输入时响应,即不支持连续操作
{
len = LongToString(str, num1); //第一操作数转换为字符串
Lcd_AreaClear(0, 0, 16 - len); //清除第一行左边的字符位
Lcd_ShowStr(16 - len, 0, str); //字符串靠右显示在第一行
ShowOprt(1, type); //在第二行显示操作符
Lcd_AreaClear(1, 1, 14); //清除第二行中间的字符位
Lcd_ShowStr(15, 1, "0"); //在第二行最右端显示 0
oprt = type; //记录操作类型
step = 1;
}
}

/* 计算结果函数 */
void GetResult()
{
u8 len = 0;
u8 str[12];
if(1 == step) //第二操作数已输入时才执行计算
{
step = 2;
switch(oprt) //根据运算符类型计算结果,未考虑溢出问题
{
case 0:
result = num1 + num2;
break;
case 1:
result = num1 - num2;
break;
case 2:
result = num1 * num2;
break;
case 3:
result = num1 / num2;
break;
default:
break;
}
len = LongToString(str, num2); //原第二操作数和运算符显示到第一行
ShowOprt(0, oprt);
Lcd_AreaClear(1, 0, 16 - 1 - len);
Lcd_ShowStr(16 - len, 0, str);
len = LongToString(str, result); //计算结果和等号显示在第二行
Lcd_ShowStr(0, 1, "=");
Lcd_AreaClear(1, 1, 16 - 1 - len);
Lcd_ShowStr(16 - len, 1, str);
}
}

/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void Key_Action(u8 keycode) //按键动作函数,根据键码执行相应动作
{
if((keycode >= '0') && (keycode <= '9')) //输入字符
{
NumKey_Action(keycode - '0');
}
else if(0x26 == keycode) //向上键,+
{
Oprt_KeyAction(0);
}
else if(0x28 == keycode) //向下键,-
{
Oprt_KeyAction(1);
}
else if(0x25 == keycode) //向左键,*
{
Oprt_KeyAction(2);
}
else if(0x27 == keycode) //向右键,÷
{
Oprt_KeyAction(3);
}
else if(0x0D == keycode) //回车键,计算结果
{
GetResult();
}
else if(0x1B == keycode) //Esc 键,清除
{
Reset();
Lcd_ShowStr(15, 1, "0");
}
}

/* T0 中断服务函数,执行按键扫描 */
void time0() interrupt 1
{
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
Key_Scan();
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "key.h"
#include "time.h"
#include "Lcd1602.h"

#endif

串口控制蜂鸣器与LCD1602液晶

通过电脑串口调试助手下发三个不同的命令,第一条指令: buzz on 可以让蜂鸣器响;第二条指令:buzz off 可以让蜂鸣器不响;第三条指令:showstr , 这个命令空格后边,可以添加任何字符串,让后边的字符串在 1602 液晶上显示出来,同时不管发送什么命令,单片机收到后把命令原封不动的再通过串口发送给电脑

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 区域清除,清除从(x,y)坐标起始的 len 个字符位 */
void Lcd_AreaClear(u8 x, u8 y, u8 len)
{
Lcd_SetCursor(x, y); //设置起始地址
while(len--) //连续写入空格
{
Lcd_WriteDat(' ');
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd_AreaClear(u8 x, u8 y, u8 len);
extern void Lcd1602_Init();

#endif

UART.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
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
#include "AllHead.h"

bit flagFrame = 0; //帧接收完成标志,即接收到一帧新数据
bit flagTxd = 0; //单字节发送完成标志,用来替代 TXD 中断标志位
u8 cntRxd = 0; //接收字节计数器
u8 pdata bufRxd[64]; //接收字节缓冲区

/* 串口配置函数,baud-通信波特率 */
void UAER_Init(u16 baud)
{
SCON = 0x50; //配置串口为模式 1 0101 0000 --> 0x50
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x20; //配置 T1 为模式 2 0010 0000 --> 0x20
TH1 = 256 - (11059200 / 12 / 32) / baud; //计算 T1 重载值
TL1 = TH1; //初值等于重载值
EA = 1; //使能总中断
ES = 1; //使能串口中断
ET1 = 0; //禁止 T1 中断
TR1 = 1; //启动 T1
}

/* 串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定的发送长度 */
void Uart_Write(u8 *buf, u8 len)
{
while(len--) //循环发送所有字节
{
flagTxd = 0; //清零发送标志
SBUF = *buf++; //发送一个字节数据
while(!flagTxd); //等待该字节发送完成
}
}

/* 串口数据读取函数,buf-接收指针,len-指定的读取长度,返回值-实际读到的长度 */
u8 UartRead(u8 *buf, u8 len)
{
u8 i = 0;
if(len > cntRxd) //指定读取长度大于实际接收到的数据长度时
{
len = cntRxd; //读取长度设置为实际接收到的数据长度
}
for(i = 0; i < len; i++) //拷贝接收到的数据到接收指针上
{
*buf++ = bufRxd[i];
}
cntRxd = 0; //接收计数器清零
return len; //返回实际读取长度
}

/* 串口接收监控,由空闲时间判定帧结束,需在定时中断中调用,ms-定时间隔 */
void Uart_RxMonitor(u8 ms)
{
static u8 cntbkp = 0; //前一次接收到数据的长度
static u8 idletmr = 0; //空闲计时

if(cntRxd > 0) //接收计数器大于零时,监控总线空闲时间
{
if(cntbkp != cntRxd) //接收计数器改变,即刚接收到数据时,清零空闲计时(与前一次接收到的数据长度不一样代表接收到数据)
{
cntbkp = cntRxd; //把本次接收到数据赋给前一次接收到的数据
idletmr = 0;
}
else //接收计数器未改变,即总线空闲时,累积空闲时间
{
if(idletmr < 30) //空闲计时小于 30ms 时,持续累加
{
idletmr += ms;
if(idletmr >= 30) //空闲时间达到 30ms 时,即判定为一帧接收完毕
{
flagFrame = 1; //设置帧接收完成标志
}
}
}
}
else
cntbkp = 0;
}

/* 串口驱动函数,监测数据帧的接收,调度功能函数,需在主循环中调用 */
void Uart_Driver()
{
u8 len;
u8 pdata buf[40]; //数据缓冲区
if(flagFrame) //有命令到达时,读取处理该命令
{
flagFrame = 0;
len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中
Uart_Action(buf, len); //传递数据帧,调用动作执行函数
}
}

/* 串口中断服务函数 */
void UART() interrupt 4
{
if(RI) //接收到新字节
{
RI = 0; //清零接收中断标志位
if(cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时
{
//保存接收字节,并递增计数器
bufRxd[cntRxd++] = SBUF;
}
}
if(TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
flagTxd = 1; //设置字节发送完成标志
}
}

UART.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef _UART_H_
#define _UART_H_

extern void UAER_Init(u16 baud);
extern void Uart_Write(u8 *buf, u8 len);
u8 UartRead(u8 *buf, u8 len);
extern void Uart_RxMonitor(u8 ms);
extern void Uart_Driver();
extern void Uart_Action(u8 *buf, u8 len);

#endif

time.c

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

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

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

time.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Time0_Init(u16 ms);

#endif

main.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
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
#include "AllHead.h"

sbit BUZZ = P1 ^ 6; //蜂鸣器控制引脚
bit flagBuzzOn = 0; //蜂鸣器启动标志

void main()
{
Time0_Init(1); //配置 T0 定时 1ms
UAER_Init(9600); //配置波特率为 9600
Lcd1602_Init(); //初始化液晶

while(1)
{
Uart_Driver(); //调用串口驱动
}
}

/* 内存比较函数,比较两个指针所指向的内存数据是否相同,ptr1-待比较指针 1,ptr2-待比较指针 2,
len-待比较长度,返回值-两段内存数据完全相同时返回 1,不同返回 0 */
bit Cmp_Memory(u8 *ptr1, u8 *ptr2, u8 len)
{
while(len--)
{
if(*ptr1++ != *ptr2++) //遇到不相等数据时即刻返回 0
{
return 0;
}
}
return 1; //比较完全部长度数据都相等则返回 1
}

/* 串口动作函数,根据接收到的命令帧执行响应的动作 buf-接收到的命令帧指针,len-命令帧长度 */
void Uart_Action(u8 *buf, u8 len)
{
u8 i;
u8 code cmd0[] = "buzz on"; //开蜂鸣器命令
u8 code cmd1[] = "buzz off"; //关蜂鸣器命令
u8 code cmd2[] = "showstr "; //字符串显示命令
u8 code cmdLen[] = {sizeof(cmd0) - 1, sizeof(cmd1) - 1, sizeof(cmd2) - 1,}; //命令长度汇总表
u8 code *cmdPtr[] = {&cmd0[0], &cmd1[0], &cmd2[0],}; //命令指针汇总表

for(i = 0; i < sizeof(cmdLen); i++) //遍历命令列表,查找相同命令
{
if(len >= cmdLen[i]) //首先接收到的数据长度要不小于命令长度
{
if (Cmp_Memory(buf, cmdPtr[i], cmdLen[i])) //比较相同时退出循环
{
break;
}
}
}
switch(i) //循环退出时 i 的值即是当前命令的索引值
{
case 0:
flagBuzzOn = 1; //开启蜂鸣器
break;
case 1:
flagBuzzOn = 0; //关闭蜂鸣器
break;
case 2:
buf[len] = '\0'; //为接收到的字符串添加结束符
Lcd_ShowStr(0, 0, buf + cmdLen[2]); //显示命令后的字符串
i = len - cmdLen[2]; //计算有效字符个数
if(i < 16) //有效字符少于 16 时,清除液晶上的后续字符位
{
Lcd_AreaClear(i, 0, 16 - i);
}
break;
default: //未找到相符命令时,给上机发送“错误命令”的提示
Uart_Write("bad command.\r\n", sizeof("bad command.\r\n") - 1);
return;
}
buf[len++] = '\r'; //有效命令被执行后,在原命令帧之后添加
buf[len++] = '\n'; //回车换行符后返回给上位机,表示已执行
Uart_Write(buf, len);
}

/* T0 中断服务函数,执行串口接收监控和蜂鸣器驱动 */
void time0() interrupt 1
{
TH0 = T0RH; //重新加载重载值
TL0 = T0RL;
if(flagBuzzOn) //执行蜂鸣器鸣叫或关闭
{
BUZZ = ~BUZZ;
}
else
BUZZ = 1;
Uart_RxMonitor(1); //串口接收监控
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "time.h"
#include "Lcd1602.h"
#include "UART.h"

#endif

I2C 总线与 EEPROM

UART 通信属于异步通信,多用于板间通信,比如单片机和电脑,这个设备和另外一个设备之间的通信,I2C属于同步通信,多用于板内通信

I2C 时序

I2C 总线是由时钟总线 SCL 和数据总线 SDA 两条线构成,连接到总线上的所有器件的 SCL 都连到一起,所有 SDA 都连到一起,I2C 总线是 开漏引脚并联 的结构,因此我们外部要添加上拉电阻, 所有接入的器件保持高电平,这条线才是高电平,而任何一个器件输出一个低电平,那这条线就会保持低电平,因此可以做到任何一个器件都可以拉低电平, 也就是任何一个器件都可以作为主机;但绝大多数情况下我们都是用单片机来做主机, 而总线上挂的多个器件,每一个都像电话机一样有自己唯一的地址,在信息传输的过程中, 通过这唯一的地址就可以正常识别到属于自己的信息,在 KST-51 开发板上,就挂接了 2 个I2C 设备,一个是 24C02,一个是 PCF8591

I2C 时序流程图

I2C 分为起始信号、数据传输部分、停止信号。其中数据传输部分,可以一次通信过程传输很多个字节,字节数是不受限制的,而每个字节的数据最后也跟了一位,这一位叫做应答位,通常用 ACK 表示,有点类似于 UART 的停止位

I2C 通信流程解析

  1. 数据有效性规定

I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线(SCL)上的信号为低电平期间,数据线(SDA)上的高电平或低电平状态才允许变化。每次数据传输都以字节为单位,每次传输的字节数不受限制。如下图:

  1. 起始和停止信号

SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号SCL 线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号。起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。如下图:

起始条件:SCL高电平期间,SDA从高电平切换到低电平
终止条件:SCL高电平期间,SDA从低电平切换到高电平

  1. 应答响应

每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位,这个校验位是接收端通过控制 SDA(数据线)来实现的,以提醒发送端数据我这边已经接收完成,数据传送可以继续进行。这个校验位其实就是数据或地址传输过程中的响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号即特定的低电平脉冲, 发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号即特定的高电平脉冲,发送方接收到该信号后会产生一个停止信号,结束信号传输。应答响应时序图如下:

I2C 通信是高位在前,低位在后,每一个字节必须保证 8 位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)。这些信号中,起始信号是必需的,结束信号和应答信号都可以不要

发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

  1. 总线的寻址方式

I2C 总线寻址按照从机地址位数可分为两种,一种是 7 位,另一种是 10 位。采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如下:

D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“ 0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的 7 位寻址位有 4 位是固定位,3 位是可编程位,这时仅能寻址 8 个同样的器件,即可以有 8 个同样的器件接入到该 I2C 总线系统中。

  1. 数据传输

I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。在起始信号后必须传送一个从机的地址(7 位),第 8 位是数据的传送方向位(R/W)用“ 0 ”表示主机发送(写)数据(W),“ 1 ”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。

A2、A1、A0 都是接的 GND,也就是说都是 0,因此 24C02 的 7 位地址实际上是二进制的 0b1010000,也就是 0x50,AT24C02的固定地址 (8位) 为1010,可配置地址本开发板上为000,所以addr + W为0xA0(发送数据()),addr + R为0xA1(请求数据())

在总线的一次数据传送过程中,可以有以下几种组合方式:

a、主机向从机发送数据,数据传送方向在整个传送过程中不变

注意:有阴影部分表示数据由主机向从机传送无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。

b、主机在第一个字节后,立即从从机读数据

c、在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好相反

发送一个字节SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
接收一个字节SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。

EEPROM芯片

在实际的应用中,保存在单片机 RAM 中的数据,掉电后就丢失了,保存在单片机 FLASH 中的数据,又不能随意改变,也就是不能用它来记录变化的数值。一般都是使用 EEPROM 来保存数据, 特点就是掉电后不丢失。我们板子上使用的这个器件是 24C02,是一个容量大小是 2Kbits, 也就是 256 个字节EEPROM。一般情况下,EEPROM 拥有 30 万到 100 万次的寿命,也就是它可以反复写入 30-100 万次,而读取次数是无限的。24C02 是一个基于 I2C 通信协议的器件,但是要分清楚,I2C 是一个通信协议,它拥有严密的通信时序逻辑要求, 而 EEPROM 是一个器件,只是这个器件采样了 I2C 协议的接口与单片机相连而已,二者并没有必然的联系,EEPROM 可以用其它接口,I2C 也可以用在其它很多器件上

板子上的 EEPROM 器件型号是 24C02,设备地址高4位是固定的 0b1010,低3位由原理图可知是接地(即低电平),所以设备地址是 0b1010000(0x50),如果发送的这个地址确实存在,那么这个地址的器件应该回应一个 ACK(拉低 SDA 即输出“0”),如果不存在,就没“人”回应 ACK(SDA将保持高电平即“1”)

EEPROM 单字节读写操作时序

  1. EEPROM 写数据流程

● 第一步,首先是 I2C 的起始信号,接着跟上首字节,也就是 I2C 的器件地址,并且在读写方向上选择“写”操作

● 第二步,发送数据的存储地址。24C02 一共 256 个字节的存储空间,地址从 0x00~0xFF,我们想把数据存储在哪个位置,此刻写的就是哪个地址

● 第三步,发送要存储的数据第一个字节、第二个字节……注意在写数据的过程中,EEPROM 每个字节都会回应一个“应答位 0”,来告诉我们写 EEPROM 数据成功,如果没有回应答位,说明写入不成功。

在写数据的过程中,每成功写入一个字节,EEPROM 存储空间的地址就会自动加 1,当加到 0xFF 后,再写一个字节,地址会溢出又变成了 0x00

  1. EEPROM 读数据流程

● 第一步,首先是 I2C 的起始信号,接着跟上首字节,也就是 I2C 的器件地址,并且在读写方向上选择“”操作。选择写操作,是为了把所要读的数据的存储地址先写进去,告诉 EEPROM 我们要读取哪个地址的数据。

● 第二步,发送要读取的数据的地址,注意是地址而非存在 EEPROM 中的数据,通知 EEPROM 我要哪个分机的信息。

● 第三步,重新发送 I2C 起始信号和器件地址,并且在方向位选择“”操作。

这三步当中,每一个字节实际上都是在“”,所以每一个字节 EEPROM 都会回应一个“应答位 0”。

● 第四步,读取从器件发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个“应答位 ACK(0)”,如果不想读了,告诉 EEPROM,我不想要数据了,别再发数据了,那就发送一个“非应答位 NAK(1)”。

A、单片机是主机,24C02 是从机
B、无论是读是写,SCL 始终都是由主机控制的
C、写的时候应答信号由 从机 给出,表示从机是否正确接收了数据
D、读的时候应答信号则由 主机 给出,表示是否继续读下去

EEPROM 多字节读写操作时序

给 EEPROM 发送数据后,先保存在了 EEPROM的缓存里,EEPROM 必须要把缓存中的数据搬移到“非易失”的区域,才能达到掉电不丢失的效果。而往非易失区域写需要一定的时间,每种器件不完全一样,24C02 的 这个写入时间最高不超过 5ms (在往非易失区域写的过程,EEPROM 是不会再响应我们的访问的,不仅接收不到我们的数据,我们即使用 I2C 标准的寻址模式去寻址,EEPROM 都不会应答,就如同这个总线上没有这个器件一样。数据写入非易失区域完毕后,EEPROM 再次恢复正常,可以正常读写了)

EEPROM 的页写入

在向 EEPROM 连续写入多个字节的数据时,如果每写一个字节都要等待几 ms 的话,整体上的写入效率就太低了。所以就想了一个办法,把 EEPROM 分页管理24C01、24C02 这两个型号是 8 个字节一个页,而 24C04、24C08、24C1616 个字节一页。 开发板上用的型号是 24C02一共是 256 个字节,8 个字节一页,那么就一共有 32 页。 分配好页之后,如果我们在同一个页内连续写入几个字节后,最后再发送停止位的时序。 EEPROM 检测到这个停止位后,就会一次性把这一页的数据写到非易失区域,就不需要写一个字节检测一次了,并且页写入的时间也不会超过 5ms。如果我们写入的数据跨页了,那么写完了一页之后,我们要发送一个停止位,然后等待并且检测 EEPROM 的空闲模式,一直等到把上一页数据完全写到非易失区域后,再进行下一页的写入,这样就可以在很大程度上提高数据的写入效率

程序

访问 EEPROM 的地址

用 I2C 的协议来寻址 0x50,另外再寻址一个不存在的地址 0x62,寻址完毕后,把返回的 ACK 显示到我们的 1602 液晶上(这里0表示地址存在,1表示地址不存在)

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd1602_Init();

#endif

I2C.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "AllHead.h"

#define I2C_Delay() {_nop_();_nop_();_nop_();_nop_();} //宏定义延时函数(一个_nop_()的时间就是一个机器周期)

/* 产生总线起始信号 */
void I2C_Start()
{
I2C_SCL = 1; //首先确保 SDA、SCL 都是高电平
I2C_SDA = 1;
I2C_Delay();
I2C_SDA = 0; //当SCL为高电平时,SDA由高变为低
I2C_Delay();
I2C_SCL = 0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/* 产生总线停止信号 */
void I2C_Stop()
{
I2C_SCL = 0; //首先确保 SDA、SCL 都是低电平
I2C_SDA = 0;
I2C_Delay();
I2C_SCL = 1; //先拉高 SCL
I2C_Delay();
I2C_SDA = 1; //当SCL为高电平时,SDA由低变为高
I2C_Delay();
}

/* I2C 总线写操作,dat-待写入字节,返回值-从机应答位的值 */
bit I2C_Write(u8 dat)
{
bit ack; //用于暂存应答位的值
u8 i;
for(i = 0; i < 8; i++)
{
if((dat & 0x80) > 0) //比较最高位 (如1011 0100 & 1000 0000 --> 1000 0000 最高位等于1 大于0)
I2C_SDA = 1;
else
I2C_SDA = 0;
dat <<= 1; //左移一位(将次高位移到最高位)
I2C_Delay();
I2C_SCL = 1; //为1数据稳定等待下一次传输
I2C_Delay();
I2C_SCL = 0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
I2C_SDA = 1; //8 位数据发送完后,主机释放 SDA,以检测从机应答
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
ack = I2C_SDA; //读取此时的 SDA 值,即为从机的应答值
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线
return (~ack); //应答值取反以符合通常的逻辑:
} //0=不存在或忙或写入失败,1=存在且空闲或写入成功

/* I2C 寻址函数,即检查地址为 addr 的器件是否存在,返回值-从器件应答值 */
bit I2C_Addressing(u8 addr)
{
bit ack;
I2C_Start(); //产生起始位,即启动一次总线操作
ack = I2C_Write(addr << 1); //器件地址需左移一位,因寻址命令的最低位为读写位,用于表示之后的操作是读或写
I2C_Stop(); //不需进行后续读写,而直接停止本次总线操作
return ack;
}

I2C.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef _I2C_H_
#define _I2C_H_

sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;

void I2C_Start();
void I2C_Stop();
bit I2C_Write(u8 dat);
bit I2C_Addressing(u8 addr);

#endif

main.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
#include "AllHead.h"

void main()
{
bit ack;
u8 str[10];
Lcd1602_Init(); //初始化液晶

ack = I2C_Addressing(0x50); //查询地址为 0x50 的器件
str[0] = '5'; //将地址和应答值转换为字符串
str[1] = '0';
str[2] = ':';
str[3] = (unsigned char)ack + '0'; //转换为字符串
str[4] = '\0'; //结束符
Lcd_ShowStr(0, 0, str); //显示到液晶上

ack = I2C_Addressing(0x62); //查询地址为 0x62 的器件
str[0] = '6'; //将地址和应答值转换为字符串
str[1] = '2';
str[2] = ':';
str[3] = (unsigned char)ack + '0'; //转换为字符串
str[4] = '\0'; //结束符
Lcd_ShowStr(8, 0, str); //显示到液晶上

while(1);
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "Lcd1602.h"
#include "I2C.h"
#include "intrins.h"

#endif

读取 EEPROM 地址上的一个数据,将读出来的数据加 1,再写到 EEPROM 地址上

读取 EEPROM 的 0x02 这个地址上的一个数据,不管这个数据之前是多少,都将读出来的数据加 1,再写到 EEPROM 的 0x02 这个地址上

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd1602_Init();

#endif

I2C.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
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
#include "AllHead.h"

#define I2C_Delay() {_nop_();_nop_();_nop_();_nop_();} //宏定义延时函数(一个_nop_()的时间就是一个机器周期)

/* 产生总线起始信号 */
void I2C_Start()
{
I2C_SCL = 1; //首先确保 SDA、SCL 都是高电平
I2C_SDA = 1;
I2C_Delay();
I2C_SDA = 0; //当SCL为高电平时,SDA由高变为低
I2C_Delay();
I2C_SCL = 0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/* 产生总线停止信号 */
void I2C_Stop()
{
I2C_SCL = 0; //首先确保 SDA、SCL 都是低电平
I2C_SDA = 0;
I2C_Delay();
I2C_SCL = 1; //先拉高 SCL
I2C_Delay();
I2C_SDA = 1; //当SCL为高电平时,SDA由低变为高
I2C_Delay();
}

/* I2C 总线写操作,dat-待写入字节,返回值-从机应答位的值 */
bit I2C_Write(u8 dat)
{
bit ack; //用于暂存应答位的值
u8 i;
for(i = 0; i < 8; i++)
{
if((dat & 0x80) > 0) //比较最高位 (如1011 0100 & 1000 0000 --> 1000 0000 最高位等于1 大于0)
I2C_SDA = 1;
else
I2C_SDA = 0;
dat <<= 1; //左移一位(将次高位移到最高位)
I2C_Delay();
I2C_SCL = 1; //为1数据稳定等待下一次传输
I2C_Delay();
I2C_SCL = 0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
I2C_SDA = 1; //8 位数据发送完后,主机释放 SDA,以检测从机应答
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
ack = I2C_SDA; //读取此时的 SDA 值,即为从机的应答值
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线
return (~ack); //应答值取反以符合通常的逻辑:
} //0=不存在或忙或写入失败,1=存在且空闲或写入成功

/* I2C 总线读操作,并发送非应答信号,返回值-读到的字节 */
u8 I2C_ReadNACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0;//再拉低 SCL,以使从机发送出下一位 为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 1; //8 位数据发送完后,拉高 SDA,发送非应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成非应答位,并保持住总线

return receive;
}

/* I2C 总线读操作,并发送应答信号,返回值-读到的字节 */
u8 I2C_ReadACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0; //为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 0; //8 位数据发送完后,拉低 SDA,发送应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线

return receive;
}

I2C.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _I2C_H_
#define _I2C_H_

sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;

void I2C_Start();
void I2C_Stop();
bit I2C_Write(u8 dat);
u8 I2C_ReadNACK();
u8 I2C_ReadACK();

#endif

EEPROM.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
#include "AllHead.h"

/* 读取 EEPROM 中的一个字节,addr-字节地址 */
u8 EEPROM_ReadByte(u8 addr)
{
u8 receive;
I2C_Start();
I2C_Write(0xA0); //写命令(寻找从机) 最后一位为0表示主机向从机写数据
I2C_Write(addr); //写入存储地址
I2C_Start(); //发送重复启动信号
I2C_Write(0xA1); //进入接收模式 最后一位为1表示主机由从机读数据
receive = I2C_ReadNACK(); //读取字节数据
I2C_Stop(); //产生一个停止条件
return receive; //返回读取的数据
}

/* 向 EEPROM 中写入一个字节,addr-字节地址 */
void EEPROM_WriteByte(u8 addr, u8 dat)
{
I2C_Start();
I2C_Write(0xA0); //写命令,寻址器件,后续为写操作(前面4位固定,后面三位接gnd所以是0)
I2C_Write(addr); //写入存储地址
I2C_Write(dat); //写入一个字节数据
I2C_Stop(); //产生一个停止条件
}

EEPROM.h

1
2
3
4
5
6
7
#ifndef _EEPROM_H_
#define _EEPROM_H_

u8 EEPROM_ReadByte(u8 addr);
void EEPROM_WriteByte(u8 addr, u8 dat);

#endif

main.c

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

void main()
{
u8 dat;
u8 str[10];
Lcd1602_Init(); //初始化液晶

dat = EEPROM_ReadByte(0x02); //读取指定地址上的一个字节
str[0] = (dat / 100) + '0'; //转换为十进制字符串格式
str[1] = (dat / 10 % 10) + '0';
str[2] = (dat % 10) + '0';
str[3] = '\0';
Lcd_ShowStr(0, 0, str); //显示在液晶上
dat++; //将其数值+1
EEPROM_WriteByte(0x02, dat); //再写回到对应的地址上

while(1);
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "Lcd1602.h"
#include "I2C.h"
#include "intrins.h"
#include "EEPROM.h"

#endif

EEPROM页写入 – 读取 EEPROM 地址上的多个数据,将读出来的数据分别 + 1 3 5···,再写到 EEPROM 地址上

函数 MemToStr:可以把一段内存数据转换成十六进制字符串的形式。由于我们从 EEPROM 读出来的是正常的数据,而 1602 液晶接收的是 ASCII 码字符,因此我们要通过液晶把数据显示出来必须先通过一步转换。就是把每一个字节的数据高 4 位 和低 4 位分开,和 9 进行比较,如果小于等于 9,则直接加 '0' 转为 0~9 的 ASCII 码;如果大于 9,则先减掉 10 再加 ‘A’ 即可转为 A~F 的 ASCII 码

函数 EEPROM_Read:在读之前,要查询一下当前是否可以进行读写操作,EEPROM 正常响应才可以进行。进行后,读最后一个字节之前的,全部给出 ACK,而读完了最后一个字节, 我们要给出一个 NAK

函数 EEPROM_Write:每次写操作之前,都要进行查询判断当前 EEPROM 是否响应,正常响应后才可以写数据

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd1602_Init();

#endif

I2C.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
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
#include "AllHead.h"

#define I2C_Delay() {_nop_();_nop_();_nop_();_nop_();} //宏定义延时函数(一个_nop_()的时间就是一个机器周期)

/* 产生总线起始信号 */
void I2C_Start()
{
I2C_SCL = 1; //首先确保 SDA、SCL 都是高电平
I2C_SDA = 1;
I2C_Delay();
I2C_SDA = 0; //当SCL为高电平时,SDA由高变为低
I2C_Delay();
I2C_SCL = 0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/* 产生总线停止信号 */
void I2C_Stop()
{
I2C_SCL = 0; //首先确保 SDA、SCL 都是低电平
I2C_SDA = 0;
I2C_Delay();
I2C_SCL = 1; //先拉高 SCL
I2C_Delay();
I2C_SDA = 1; //当SCL为高电平时,SDA由低变为高
I2C_Delay();
}

/* I2C 总线写操作,dat-待写入字节,返回值-从机应答位的值 */
bit I2C_Write(u8 dat)
{
bit ack; //用于暂存应答位的值
u8 i;
for(i = 0; i < 8; i++)
{
if((dat & 0x80) > 0) //比较最高位 (如1011 0100 & 1000 0000 --> 1000 0000 最高位等于1 大于0)
I2C_SDA = 1;
else
I2C_SDA = 0;
dat <<= 1; //左移一位(将次高位移到最高位)
I2C_Delay();
I2C_SCL = 1; //为1数据稳定等待下一次传输
I2C_Delay();
I2C_SCL = 0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
I2C_SDA = 1; //8 位数据发送完后,主机释放 SDA,以检测从机应答
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
ack = I2C_SDA; //读取此时的 SDA 值,即为从机的应答值
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线
return (~ack); //应答值取反以符合通常的逻辑:
} //0=不存在或忙或写入失败,1=存在且空闲或写入成功

/* I2C 总线读操作,并发送非应答信号,返回值-读到的字节 */
u8 I2C_ReadNACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0;//再拉低 SCL,以使从机发送出下一位 为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 1; //8 位数据发送完后,拉高 SDA,发送非应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成非应答位,并保持住总线

return receive;
}

/* I2C 总线读操作,并发送应答信号,返回值-读到的字节 */
u8 I2C_ReadACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0; //为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 0; //8 位数据发送完后,拉低 SDA,发送应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线

return receive;
}

I2C.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _I2C_H_
#define _I2C_H_

sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;

void I2C_Start();
void I2C_Stop();
bit I2C_Write(u8 dat);
u8 I2C_ReadNACK();
u8 I2C_ReadACK();

#endif

EEPROM.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include "AllHead.h"

/* E2 读取函数,buf-数据接收指针,addr-E2 中的起始地址,len-读取长度 */
void EEPROM_Read(u8 *buf, u8 addr, u8 len)
{
do //用寻址操作查询当前是否可进行读写操作
{
I2C_Start(); //写命令(寻找从机) 最后一位为0表示主机向从机写数据
if(I2C_Write(0xA0)) //应答则跳出循环,非应答则进行下一次查询
{
//应答返回1符合if条件然后break跳出循环
break; //非应答返回0则一直在do while循环里面一直等到应答
}
I2C_Stop();
}
while(1);
I2C_Write(addr); //写入起始地址
I2C_Start(); //发送重复启动信号
I2C_Write(0xA1); //进入接收模式 最后一位为1表示主机由从机读数据
while(len > 1) //连续读取 len-1 个字节
{
*buf++ = I2C_ReadACK();//最后字节之前为读取操作+应答
len--;
}
*buf = I2C_ReadNACK(); //最后一个字节为读取操作+非应答
I2C_Stop(); //产生一个停止条件
}

/* E2 写入函数,buf-源数据指针,addr-E2 中的起始地址,len-写入长度 */
void EEPROM_Write(u8 *buf, u8 addr, u8 len)
{
while(len > 0)
{
//等待上次写入操作完成
do //用寻址操作查询当前是否可进行读写操作
{
I2C_Start(); //写命令(寻找从机) 最后一位为0表示主机向从机写数据
if(I2C_Write(0xA0)) //应答则跳出循环,非应答则进行下一次查询
{
//应答返回1符合if条件然后break跳出循环
break; //非应答返回0则一直在do while循环里面一直等到应答
}
I2C_Stop();
}
while(1);
//按页写模式连续写入字节
I2C_Write(addr); //写入起始地址
while(len > 0)
{
I2C_Write(*buf++); //写入一个字节数据
len--; //待写入长度计数递减
addr++; //E2 地址递增
if(0 == (addr & 0x07)) //检查地址是否到达页边界,24C02 每页 8 字节,
{
//所以检测低 3 位是否为零即可
break; //到达页边界时,跳出循环,结束本次写操作
}
}
I2C_Stop(); //产生一个停止条件
}
}

EEPROM.h

1
2
3
4
5
6
7
#ifndef _EEPROM_H_
#define _EEPROM_H_

void EEPROM_Read(u8 *buf, u8 addr, u8 len);
void EEPROM_Write(u8 *buf, u8 addr, u8 len);

#endif

main.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
42
43
44
45
#include "AllHead.h"

void MemToStr(u8 *str, u8 *src, u8 len);

void main()
{
u8 i;
u8 buf[5];
u8 str[20];

Lcd1602_Init(); //初始化液晶

EEPROM_Read(buf, 0x8E, sizeof(buf)); //从 EEPROM 中读取一段数据
MemToStr(str, buf, sizeof(buf)); //转换为十六进制字符串
Lcd_ShowStr(0, 0, str); //显示到液晶上
for(i = 0; i < sizeof(buf); i++) //数据依次+1,+2,+3...
{
buf[i] = buf[i] + 1 + i;
}
EEPROM_Write(buf, 0x8E, sizeof(buf)); //数据依次+1,+2,+3...
while(1);
}


/* 将一段内存数据转换为十六进制格式的字符串,str-字符串指针,src-源数据地址,len-数据长度 */
void MemToStr(u8 *str, u8 *src, u8 len)
{
u8 temp;
while(len--)
{
temp = *src >> 4; //先取高 4 位
if(temp <= 9) //转换为 0-9 或 A-F
*str++ = temp + '0'; //先把值传递给str指向的地址,然后地址++
else
*str++ = temp - 10 + 'A';
temp = *src & 0x0F; //再取低 4 位
if(temp <= 9) //转换为 0-9 或 A-F
*str++ = temp + '0';
else
*str++ = temp - 10 + 'A';
*str++ = ' '; //转换完一个字节添加一个空格
src++; //原数据地址++,转化为下个字节
}
*str++ = '\0'; //添加字符串结束符
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "Lcd1602.h"
#include "I2C.h"
#include "intrins.h"
#include "EEPROM.h"

#endif

I2C 和 EEPROM 的综合实验

上电后,1602 的第一行显示 EEPROM 从 0x20 地址开始的 16 个字符,第二行显示 EERPOM 从 0x40 开始的 16 个字符。可以通过 UART 串口通信来改变 EEPROM 内部的这个数据,并且同时也改变了 1602 显示的内容,下次上电的时候,直接会显示更新过的内容

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd1602_Init();

#endif

UART.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
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
#include "AllHead.h"

bit flagFrame = 0; //帧接收完成标志,即接收到一帧新数据
bit flagTxd = 0; //单字节发送完成标志,用来替代 TXD 中断标志位
u8 cntRxd = 0; //接收字节计数器
u8 pdata bufRxd[64]; //接收字节缓冲区

/* 串口配置函数,baud-通信波特率 */
void UAER_Init(u16 baud)
{
SCON = 0x50; //配置串口为模式 1 0101 0000 --> 0x50
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x20; //配置 T1 为模式 2 0010 0000 --> 0x20
TH1 = 256 - (11059200 / 12 / 32) / baud; //计算 T1 重载值
TL1 = TH1; //初值等于重载值
EA = 1; //使能总中断
ES = 1; //使能串口中断
ET1 = 0; //禁止 T1 中断
TR1 = 1; //启动 T1
}

/* 串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定的发送长度 */
void Uart_Write(u8 *buf, u8 len)
{
while(len--) //循环发送所有字节
{
flagTxd = 0; //清零发送标志
SBUF = *buf++; //发送一个字节数据
while(!flagTxd); //等待该字节发送完成
}
}

/* 串口数据读取函数,buf-接收指针,len-指定的读取长度,返回值-实际读到的长度 */
u8 UartRead(u8 *buf, u8 len)
{
u8 i = 0;
if(len > cntRxd) //指定读取长度大于实际接收到的数据长度时
{
len = cntRxd; //读取长度设置为实际接收到的数据长度
}
for(i = 0; i < len; i++) //拷贝接收到的数据到接收指针上
{
*buf++ = bufRxd[i];
}
cntRxd = 0; //接收计数器清零
return len; //返回实际读取长度
}

/* 串口接收监控,由空闲时间判定帧结束,需在定时中断中调用,ms-定时间隔 */
void Uart_RxMonitor(u8 ms)
{
static u8 cntbkp = 0; //前一次接收到数据的长度
static u8 idletmr = 0; //空闲计时

if(cntRxd > 0) //接收计数器大于零时,监控总线空闲时间
{
if(cntbkp != cntRxd) //接收计数器改变,即刚接收到数据时,清零空闲计时(与前一次接收到的数据长度不一样代表接收到数据)
{
cntbkp = cntRxd; //把本次接收到数据赋给前一次接收到的数据
idletmr = 0;
}
else //接收计数器未改变,即总线空闲时,累积空闲时间
{
if(idletmr < 30) //空闲计时小于 30ms 时,持续累加
{
idletmr += ms;
if(idletmr >= 30) //空闲时间达到 30ms 时,即判定为一帧接收完毕
{
flagFrame = 1; //设置帧接收完成标志
}
}
}
}
else
cntbkp = 0;
}

/* 串口驱动函数,监测数据帧的接收,调度功能函数,需在主循环中调用 */
void Uart_Driver()
{
u8 len;
u8 pdata buf[40]; //数据缓冲区
if(flagFrame) //有命令到达时,读取处理该命令
{
flagFrame = 0;
len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中
Uart_Action(buf, len); //传递数据帧,调用动作执行函数
}
}

/* 串口中断服务函数 */
void UART() interrupt 4
{
if(RI) //接收到新字节
{
RI = 0; //清零接收中断标志位
if(cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时
{
//保存接收字节,并递增计数器
bufRxd[cntRxd++] = SBUF;
}
}
if(TI) //字节发送完毕
{
TI = 0; //清零发送中断标志位
flagTxd = 1; //设置字节发送完成标志
}
}

UART.h

1
2
3
4
5
6
7
8
9
10
11
#ifndef _UART_H_
#define _UART_H_

extern void UAER_Init(u16 baud);
extern void Uart_Write(u8 *buf, u8 len);
u8 UartRead(u8 *buf, u8 len);
extern void Uart_RxMonitor(u8 ms);
extern void Uart_Driver();
extern void Uart_Action(u8 *buf, u8 len);

#endif

time.c

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

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

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

time.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Time0_Init(u16 ms);

#endif

I2C.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
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
#include "AllHead.h"

#define I2C_Delay() {_nop_();_nop_();_nop_();_nop_();} //宏定义延时函数(一个_nop_()的时间就是一个机器周期)

/* 产生总线起始信号 */
void I2C_Start()
{
I2C_SCL = 1; //首先确保 SDA、SCL 都是高电平
I2C_SDA = 1;
I2C_Delay();
I2C_SDA = 0; //当SCL为高电平时,SDA由高变为低
I2C_Delay();
I2C_SCL = 0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/* 产生总线停止信号 */
void I2C_Stop()
{
I2C_SCL = 0; //首先确保 SDA、SCL 都是低电平
I2C_SDA = 0;
I2C_Delay();
I2C_SCL = 1; //先拉高 SCL
I2C_Delay();
I2C_SDA = 1; //当SCL为高电平时,SDA由低变为高
I2C_Delay();
}

/* I2C 总线写操作,dat-待写入字节,返回值-从机应答位的值 */
bit I2C_Write(u8 dat)
{
bit ack; //用于暂存应答位的值
u8 i;
for(i = 0; i < 8; i++)
{
if((dat & 0x80) > 0) //比较最高位 (如1011 0100 & 1000 0000 --> 1000 0000 最高位等于1 大于0)
I2C_SDA = 1;
else
I2C_SDA = 0;
dat <<= 1; //左移一位(将次高位移到最高位)
I2C_Delay();
I2C_SCL = 1; //为1数据稳定等待下一次传输
I2C_Delay();
I2C_SCL = 0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
I2C_SDA = 1; //8 位数据发送完后,主机释放 SDA,以检测从机应答
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
ack = I2C_SDA; //读取此时的 SDA 值,即为从机的应答值
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线
return (~ack); //应答值取反以符合通常的逻辑:
} //0=不存在或忙或写入失败,1=存在且空闲或写入成功

/* I2C 总线读操作,并发送非应答信号,返回值-读到的字节 */
u8 I2C_ReadNACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0;//再拉低 SCL,以使从机发送出下一位 为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 1; //8 位数据发送完后,拉高 SDA,发送非应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成非应答位,并保持住总线

return receive;
}

/* I2C 总线读操作,并发送应答信号,返回值-读到的字节 */
u8 I2C_ReadACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0; //为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 0; //8 位数据发送完后,拉低 SDA,发送应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线

return receive;
}

I2C.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _I2C_H_
#define _I2C_H_

sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;

void I2C_Start();
void I2C_Stop();
bit I2C_Write(u8 dat);
u8 I2C_ReadNACK();
u8 I2C_ReadACK();

#endif

EEPROM.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include "AllHead.h"

/* E2 读取函数,buf-数据接收指针,addr-E2 中的起始地址,len-读取长度 */
void EEPROM_Read(u8 *buf, u8 addr, u8 len)
{
do //用寻址操作查询当前是否可进行读写操作
{
I2C_Start(); //写命令(寻找从机) 最后一位为0表示主机向从机写数据
if(I2C_Write(0xA0)) //应答则跳出循环,非应答则进行下一次查询
{
//应答返回1符合if条件然后break跳出循环
break; //非应答返回0则一直在do while循环里面一直等到应答
}
I2C_Stop();
}
while(1);
I2C_Write(addr); //写入起始地址
I2C_Start(); //发送重复启动信号
I2C_Write(0xA1); //进入接收模式 最后一位为1表示主机由从机读数据
while(len > 1) //连续读取 len-1 个字节
{
*buf++ = I2C_ReadACK();//最后字节之前为读取操作+应答
len--;
}
*buf = I2C_ReadNACK(); //最后一个字节为读取操作+非应答
I2C_Stop(); //产生一个停止条件
}

/* E2 写入函数,buf-源数据指针,addr-E2 中的起始地址,len-写入长度 */
void EEPROM_Write(u8 *buf, u8 addr, u8 len)
{
while(len > 0)
{
//等待上次写入操作完成
do //用寻址操作查询当前是否可进行读写操作
{
I2C_Start(); //写命令(寻找从机) 最后一位为0表示主机向从机写数据
if(I2C_Write(0xA0)) //应答则跳出循环,非应答则进行下一次查询
{
//应答返回1符合if条件然后break跳出循环
break; //非应答返回0则一直在do while循环里面一直等到应答
}
I2C_Stop();
}
while(1);
//按页写模式连续写入字节
I2C_Write(addr); //写入起始地址
while(len > 0)
{
I2C_Write(*buf++); //写入一个字节数据
len--; //待写入长度计数递减
addr++; //E2 地址递增
if(0 == (addr & 0x07)) //检查地址是否到达页边界,24C02 每页 8 字节,
{
//所以检测低 3 位是否为零即可
break; //到达页边界时,跳出循环,结束本次写操作
}
}
I2C_Stop(); //产生一个停止条件
}
}

EEPROM.h

1
2
3
4
5
6
7
#ifndef _EEPROM_H_
#define _EEPROM_H_

void EEPROM_Read(u8 *buf, u8 addr, u8 len);
void EEPROM_Write(u8 *buf, u8 addr, u8 len);

#endif

main.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
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
#include "AllHead.h"

void Init_ShowStr();
bit Cmp_Memory(u8 *ptr1, u8 *ptr2, u8 len);
void TrimString16(u8 *out, u8 *in);

void main()
{
Time0_Init(1); //配置 T0 定时 1ms
UAER_Init(9600); //配置波特率为 9600
Lcd1602_Init(); //初始化液晶
Init_ShowStr(); //初始显示内容
while(1)
{
Uart_Driver(); //调用串口驱动
}
}

/* 处理液晶屏初始显示内容 */
void Init_ShowStr()
{
u8 str[17];
str[16] = '\0'; //在最后添加字符串结束符,确保字符串可以结束
EEPROM_Read(str, 0x20, 16); //读取第一行字符串,其 EEPROM 起始地址为 0x20
Lcd_ShowStr(0, 0, str); //显示到液晶屏
EEPROM_Read(str, 0x40, 16); //读取第二行字符串,其 EEPROM 起始地址为 0x40
Lcd_ShowStr(0, 1, str); //显示到液晶屏
}

/* 内存比较函数,比较两个指针所指向的内存数据是否相同,ptr1-待比较指针 1,ptr2-待比较指针 2,
len-待比较长度,返回值-两段内存数据完全相同时返回 1,不同返回 0 */
bit Cmp_Memory(u8 *ptr1, u8 *ptr2, u8 len)
{
while(len--)
{
if(*ptr1++ != *ptr2++) //遇到不相等数据时即刻返回 0
{
return 0;
}
else
return 1; //比较完全部长度数据都相等则返回 1
}
}

/* 将一字符串整理成 16 字节的固定长度字符串,不足部分补空格
out-整理后的字符串输出指针,in-待整理字符串指针 */
void TrimString16(u8 *out, u8 *in)
{
u8 i = 0;
while(*in != '\0') //拷贝字符串直到输入字符串结束
{
*out++ = *in++;
i++;
if(i >= 16) //当拷贝长度已达到 16 字节时,强制跳出循环
break;
}
for(; i < 16; i++) //如不足 16 个字节则用空格补齐
{
*out++ = ' ';
}
*out = '\0'; //最后添加结束符
}

/* 串口动作函数,根据接收到的命令帧执行响应的动作,buf-接收到的命令帧指针,len-命令帧长度 */
void Uart_Action(u8 *buf, u8 len)
{
u8 i;
u8 str[17];
u8 code cmd0[] = "showstr1 "; //第一行字符显示命令
u8 code cmd1[] = "showstr2 "; //第二行字符显示命令
u8 code cmdLen[] = {sizeof(cmd0) - 1, sizeof(cmd1) - 1 }; //命令长度汇总表
u8 code *cmdPtr[] = { &cmd0[0], &cmd1[0] }; //命令指针汇总表

for(i = 0; i < sizeof(cmdLen); i++) //遍历命令列表,查找相同命令
{
if(len >= cmdLen[i]) //首先接收到的数据长度要不小于命令长度
{
if(Cmp_Memory(buf, cmdPtr[i], cmdLen[i])) //比较相同时退出循环
{
break;
}
}
}
switch(i) //根据比较结果执行相应命令
{
case 0:
buf[len] = '\0'; //为接收到的字符串添加结束符
TrimString16(str, buf + cmdLen[0]); //整理成 16 字节固定长度字符串
Lcd_ShowStr(0, 0, str); //显示字符串 1
EEPROM_Write(str, 0x20, sizeof(str)); //保存字符串 1,起始地址为 0x20
break;
case 1:
buf[len] = '\0'; //为接收到的字符串添加结束符
TrimString16(str, buf + cmdLen[1]); //整理成 16 字节固定长度字符串
Lcd_ShowStr(0, 1, str); //显示字符串 2
EEPROM_Write(str, 0x40, sizeof(str)); //保存字符串 2,起始地址为 0x40
break;
default: //未找到相符命令时,给上机发送“错误命令”的提示
Uart_Write("bad command.\r\n", sizeof("bad command.\r\n") - 1);
return;
}
buf[len++] = '\r'; //有效命令被执行后,在原命令帧之后添加
buf[len++] = '\n'; //回车换行符后返回给上位机,表示已执行
Uart_Write(buf, len);
}

void time() interrupt 1
{
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
Uart_RxMonitor(1); //串口接收监控
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "time.h"
#include "Lcd1602.h"
#include "UART.h"
#include "I2C.h"
#include "intrins.h"
#include "EEPROM.h"

#endif

实时时钟 DS1302

BCD 码

BCD 码亦称二进码十进制数二-十进制代码。用 4 位二进制数来表示 1 位十进制数中的 0~9 这 10 个数字。是一种二进制的数字编码形式,用二进制编码的十进制代码。十进制的一位数字,从 0 到 9,最大的数字就是 9,再加 1 就要进位,所以用 4 位二进制表示十进制,就是从 0b0000 到 0b1001,不存在 0b1010、 0b1011、0b1100、0b1101、0b1110、0b1111 这 6 个数字。BCD 码如果到了 0b1001,再加 1 的话,数字就变成 0b00010000 这样了,相当于用了 8 位的二进制数字表示了 2 位的十进制数字

从 DS1302 中读取出来的时钟数据均为 BCD 码格式,需转换为我们习惯的 10 进制

日期时间在时钟芯片中的存储格式就是 BCD 码,直接 取出表示十进制 1 位数字的 4 个二进制位然后再加上 0x30 就可组成一个ASCII码字节

需要注意:DS1302低位在前,高位在后

SPI 时序

SPI 是串行外围设备接口,是一种高速的、全双工、同步通信总线,常用于单片机和 EEPROM、FLASH、实时时钟、数字信号处理器等器件的通信

SPI 通信原理主要是主从方式通信,这种模式通常只有一个主机和一个或者多个从机,标准的 SPI 是 4 根线,分别是 SSEL(片选,也写作 SCS)、SCLK(时钟,也写作 SCK)、MOSI主机输出从机输入)和 MISO主机输入从机输出

SSEL:从设备片选使能信号。如果从设备是低电平使能的话,当拉低这个引脚后,从设备就会被选中,主机和这个被选中的从机进行通信

SCLK时钟信号,由主机产生,和 I2C 通信的 SCL 有点类似

MOSI:主机给从机发送指令或者数据的通道

MISO:主机读取从机的状态或者数据的通道

读写数据时序的四种模式

CPOL:时钟的极性。通信的整个过程分为空闲时刻通信时刻,如果 SCLK 在数据发送之前和之后的空闲状态是高电平,那么 CPOL=1,如果空闲状态 SCLK 是低电平,那么 CPOL=0

CPHA:时钟的相位。CPHA=1 表示数据的输出是在一个时钟周期的第一个沿上,那么数据的采样就是在第二个沿上了;CPHA=0 表示数据的采样是在一个时钟周期的第一个沿上,那么数据的输出就在第二个沿上

模式一

数据未发送时以及发送完毕后,SCK 都是高电平,因此 CPOL=1。 可以看出,在 SCK 第一个沿的时候,MOSI 和 MISO 会发生变化,同时 SCK 第二个沿的时候,数据是稳定的,此刻采样数据是合适的,也就是上升沿即一个时钟周期的后沿锁存读取数据,即 CPHA=1。最后最隐蔽的 SSEL 片选,这个引脚通常用来决定是哪个从机和主机进行通信

另外几种模式如图所示:

实时时钟芯片 DS1302

DS1302 是 一种涓流充电时钟芯片,内含有一个实时时钟/日历和 31 字节静态 RAM,通过简单的串行接口与单片机进行通信。实时时钟/日历电路提供秒、分、时、日、周、月、年的信息,每月的天数和闰年的天数可自动调整。时钟操作可通过 AM/PM 指示决定采用 24 或 12 小时格式。

DS1302 与单片机之间能简单地采用同步串行的方式进行通信,仅需用到三根通信线:①RES复位 ②I/O 数据线 ③SCLK 串行时钟。时钟/RAM 的读/写数据以一个字节或多达31 个字节的字符组方式通信。DS1302 工作时功耗很低保持数据和时钟信息时功率小于 1mW。

DS1302 由 DS1202 改进而来增加了以下的特性:双电源管脚用于主电源和备份电源供应Vcc1 为可编程涓流充电电源,附加七个字节存储器。它广泛应用于电话、传真、便携式仪器以及电池供电的仪器仪表等产品领域下面。

主要的性能指标:

实时时钟具有能计算 2100 年之前的秒、分、时、日、星期、月、年的能力,还有闰年调整的能力;
31 个 8 位暂存数据存储 RAM;
串行 I/O 口方式使得管脚数量最少;
宽范围工作电压 2.0 - 5.5V;
工作在 2.0V 时,电流小于 300nA;
读/写时钟或 RAM 数据时有两种传送方式单字节传送多字节传送字符组方式;
8 脚 DIP 封装或可选的 8 脚 SOIC 封装根据表面装配;
简单 3 线接口;
与 TTL 兼容 Vcc=5V;
可选工业级温度范围-40~+85

DS1302 芯片的管脚及功能

1、VCC2:主电源引脚
2、X1、X2:DS1302 外部晶振引脚,通常需外接 32.768K 晶振
3、GND:电源地
4、CE:使能引脚,也是复位引脚(新版本功能变)
5、I/O:串行数据引脚,数据输出或者输入都从这个引脚
6、SCLK:串行时钟引脚
7、VCC1:备用电源

DS1302 使用

操作 DS1302 的大致过程,就是将各种数据写入 DS1302 的寄存器,以设置它当前的时间的格式。然后使 DS1302 开始运作,DS1302 时钟会按照设置情况运转,再用单片机将其寄存器内的数据读出。再用液晶显示,就是我们常说的简易电子钟。所以总的来说 DS1302 的操作分 2 步(显示部分属于液晶显示的内容,不属于 DS1302 本身的内容)

DS1302 有一个控制寄存器、12 个日历、时钟寄存器和 31 个 RAM

控制寄存器

控制寄存器用于存放 DS1302 的控制命令字,DS1302 的 RST 引脚回到高电平后写入的第一个字节就为控制命令。它用于对 DS1302 读写过程进行控制,格式如下:

1、第 7 位永远都是 1,这一位如果是 0 的话,那写进去也是无效的
2、第 6 位,1 表示 RAM,寻址内部存储器地址;0 表示 CK,寻址内部寄存器
3、第 5 到第 1 位,为 RAM 或者寄存器的地址
4、最低位,高电平表示 RD,即下一步操作将要“”;低电平表示 W,即下一步操作将要“”。(与 AT24C02 寄存器类似,0读1写)

比如要读秒寄存器则命令为 1000 0001,反之写为 1000 0000

日历/时钟寄存器

DS1302 共有 12 个寄存器,其中有 7 个与日历、时钟相关,存放的数据为 BCD码形式。格式如下:

秒寄存器低四位为秒的个位,高的次三位为秒的十位。最高位CH 为DS1302 的运行标志,当 CH=0 时,DS1302 内部时钟运行,反之 CH=1 时停止。DS1302 内部是 BCD 码, 而秒的十位最大是 5,所以 3 个二进制位就够了

分寄存器:最高位未使用,剩下的 7 位中高 3 位是分钟的十位,低 4 位是分钟的个位

小时寄存器时寄存器最高位为 12/24 小时的格式选择位,该位为 1 时表示 12 小时格式,0 代表是 24 小时制。第六位固定是 0,当设置为 12 小时显示格式时,第 5 位的 1 (高电平)表示下午(PM)0 (低电平)表示上午(AM);而当设置为 24 小时格式时,高 3 位代表了小时的十位,低 4 位代表的是小时的个位

日寄存器:高 2 位固定是 0,bit5 和 bit4 是日期的十位,低 4 位是日期的个位

月寄存器:高 3 位固定是 0,bit4 是月的十位,低 4 位是月的个位

星期寄存器:高 5 位固定是 0,低 3 位代表了星期

年寄存器:高 4 位代表了年的十位,低 4 位代表了年的个位。这里的 00~ 99 指的是 2000 年~2099 年

写保护寄存器:当该寄存器最高位 WP 为 1 时,表示禁止给任何其它寄存器或者那 31 个字节的 RAM 写数据,DS1302只读不写,所以要在往 DS1302 写数据之前确保 WP 为 0

慢充电寄存器(涓细电流充电)寄存器:当 DS1302 掉电时,可以马上调用外部电源保护时间数据。该寄存器就是配置备用电源的充电选项的。其中高四位(4 个 TCS)只有在 1010 的情况下才能使用充电选项;低四位的情况与 DS1302 内部电路有关。

DS1302 通信时序

控制指令字输入后的下一个 SCLK 时钟的上升沿时,数据被写入 DS1302,数据输入从低位(位 0)开始。同样,在紧跟 8 位的控制指令字后的下一个 SCLK脉冲的下降沿读出 DS1302 的数据,读出数据时从低位 0 位到高位 7。其时序图如下所示:

上图就是 DS1302 的三个时序:复位时序,单字节写时序,单字节读时序

CE(RST):复位时序,即在 RST 引脚产生一个正脉冲,在整个读写器件,RST 要保持高电平,一次字节读写完毕之后,要注意把 RST 返回低电平准备下次读写周期;

单字节读时序:注意读之前还是要先对寄存器写命令,前八个为控制寄存器地址,从最低位开始写;可以看到,写数据是在 SCLK 的上升沿实现,而读数据在 SCLK 的下降沿实现。所以,在单字节读时序中,写命令的第八个上升沿结束后紧接着的第八个下降沿就将要寄存器的第一位数据读到数据线上了!这个就是 DS1302 操作中最特别的地方。当然读出来的数据也是最低位开始。

单字节写时序:两个字节的数据配合 16 个上升沿将数据写入即可。DS1302 的时序里, 单片机要预先写一个字节指令,指明要写入的寄存器的地址以及后续的操作是写操作,然后再写入一个字节的数据

程序注意事项
★要记得在操作 DS1302 之前关闭写保护(即确保 WP 为 0);
★注意用延时来降低单片机的速度以配合器件时序;
★DS1302 读出来的数据是 BCD 码形式,要转换成我们习惯的 10 进制;
★读取字节之前,将 IO 设置为输入口,读取完之后,要将其改回输出口;
★在写程序的时候,建议实现开辟数组(内存空间)来集中放置 DS1302 的一系列数据,方便以后扩展键盘输入。

DS1302 的 BURST 模式

DS1302 的突发模式。突发模式也分为 RAM 突发模式和时钟突发模式,只看和时钟相关的 clock burst mode

当写指令到DS1302 的时候,只要将要写的 5 位地址全部写1,即读操作用0xBF, 写操作用 0xBE,这样的指令送给 DS1302 之后,它就会自动识别出来是 burst 模式,马上把所有的 8 个字节同时锁存到另外的 8 个字节的寄存器缓冲区内,这样时钟继续走,而读数据是从另外一个缓冲区内读取的。同样的道理,如果用 burst 模式写数据,那么也是先写到这个缓冲区内,最终 DS1302 会把这个缓冲区内的数据一次性送到它的时钟寄存器内。 要注意的是,不管是读还是写,只要使用时钟的 burst 模式,则必须一次性读写 8 个寄存器,要把时钟的寄存器完全读出来或者完全写进去

原理图

程序

读取 DS1302 的当前时间,并显示在液晶屏上

先将 2023 年 10 月 14 日星期六 16 点 36 分 30 秒这个时间写到 DS1302 内部,让 DS1302 正常运行,然后再不停的读取 DS1302 的当前时间,并显示在液晶屏上

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd1602_Init();

#endif

DS1302.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
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
#include "AllHead.h"

//---DS1302写入和读取时分秒的地址命令---//
//---秒分时日月周年 最低位读写位;-------//
u8 Write_addr[7] = {0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c}; //数据可查看各类寄存器
u8 Read_addr[7] = {0x81, 0x83, 0x85, 0x87, 0x89, 0x8b, 0x8d};

//---DS1302时钟初始化2023年10月14日星期六16点36分30秒。---//
//---存储顺序是秒分时日月周年,存储格式是用BCD码---//
u8 DS1302_Time[7] = {0x30, 0x36, 0x16, 0x14, 0x10, 0x06, 0x23};

/*******************************************************************************
* 函 数 名 : DS1302_Write_Byte
* 函数功能 : DS1302写单字节
* 输 入 : addr:地址/命令
dat:数据
* 输 出 : 无
*******************************************************************************/
void DS1302_Write_Byte(u8 addr, u8 dat)
{
u8 i;

DS1302_CE = 0; //首先CE输出低电平
_nop_(); //延时1us
DS1302_SCK = 0; //SCLK也输出低电平
_nop_();
DS1302_CE = 1; //在读写过程,CE要保持高电平
_nop_();

for(i = 0; i < 8; i++) //循环8次,每次写1位,先写低位再写高位
{
DS1302_IO = addr & 0x01; //取出控制寄存器地址的最低位赋给DS1302的IO口
addr >>= 1; //将控制寄存器地址的次低位移到最低位
DS1302_SCK = 1; //SCLK由低到高产生一个上升沿,从而写入数据
_nop_(); //延时1us
DS1302_SCK = 0; //SCLK先拉低,以便下一次上升沿
_nop_();
}

for(i = 0; i < 8; i++)
{
DS1302_IO = dat & 0x01; //取出写入数据的最低位赋给DS1302的IO口
dat >>= 1; //将数据的次低位移到最低位
DS1302_SCK = 1;
_nop_();
DS1302_SCK = 0;
_nop_();
}
DS1302_CE = 0; //CE拉低
_nop_();
}

/*******************************************************************************
* 函 数 名 : DS1302_Read_Byte
* 函数功能 : DS1302读单字节
* 输 入 : addr:地址/命令
* 输 出 : 读取的数据
*******************************************************************************/
u8 DS1302_Read_Byte(u8 addr)
{
u8 i;
u8 dat; //将接收到的数据存放到这个变量

DS1302_CE = 0; //首先CE输出低电平
_nop_(); //延时1us
DS1302_SCK = 0; //SCLK也输出低电平
_nop_();
DS1302_CE = 1; //在读写过程,CE要保持高电平
_nop_();

for(i = 0; i < 8; i++) //循环8次,每次写1位,先写低位再写高位
{
DS1302_IO = addr & 0x01; //取出控制寄存器地址的最低位赋给DS1302的IO口
addr >>= 1; //将控制寄存器地址的次低位移到最低位
DS1302_SCK = 1; //SCLK由低到高产生一个上升沿,从而写入数据
_nop_(); //延时1us
DS1302_SCK = 0; //SCLK先拉低,以便下一次上升沿
_nop_();
}

for(i = 0; i < 8; i++) //循环8次,每次写1位,先写低位再写高位
{
dat >>= 1; //将数据的次低位移到最低位
if(DS1302_IO) //如果读取到的数据为1
dat |= 0x80; //dat相对应的值为1,否则不进入if语句,dat默认为0
DS1302_SCK = 1;
_nop_(); //延时1us
DS1302_SCK = 0; //SCLK由高到低产生一个下降沿,从而读入数据
_nop_();
}
DS1302_CE = 0; //CE拉低
_nop_();

return dat; //将读取到的数据返回出去
}

/*******************************************************************************
* 函 数 名 : DS1302_Init
* 函数功能 : DS1302初始化时间
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void DS1302_Init(void)
{
u8 i;
DS1302_Write_Byte(0x8E, 0x00); //关闭写保护(写保护寄存器的地址为0x8E,WP为其中的最高位,WP为0即为关闭写保护)
for(i = 0; i < 7; i++)
{
DS1302_Write_Byte(Write_addr[i], DS1302_Time[i]);
}
DS1302_Write_Byte(0x8E, 0x80); //打开写保护,以防止意外修改DS1302内部寄存器(WP为1即为打开写保护)
}

/*******************************************************************************
* 函 数 名 : DS1302_Read_Time
* 函数功能 : DS1302读取时间
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void DS1302_Read_Time(void)
{
u8 i;
for(i = 0; i < 7; i++)
{
DS1302_Time[i] = DS1302_Read_Byte(Read_addr[i]);
}
}

DS1302.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef _DS1302_H_
#define _DS1302_H_

sbit DS1302_CE = P1^7; //复位管脚
sbit DS1302_SCK = P3^5; //时钟管脚
sbit DS1302_IO = P3^4; //数据管脚

void DS1302_Write_Byte(u8 addr,u8 dat);
u8 DS1302_Read_Byte(u8 addr);
void DS1302_Init(void);
void DS1302_Read_Time(void);

extern u8 DS1302_Time[7]; //使用extern之后外部文件就可以调用

#endif

main.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
42
#include "AllHead.h"

void main()
{
u8 str[12]; //字符串转换缓冲区

DS1302_Init(); //初始化实时时钟
Lcd1602_Init(); //初始化液晶

while(1)
{
DS1302_Read_Time(); //读取 DS1302 当前时间
str[0] = '2'; //添加年份的高 2 位:20
str[1] = '0'; //因为BCD码用 4 位二进制数来表示 1 位十进制数
str[2] = (DS1302_Time[6] >> 4) + '0'; //“年”高位数字转换为 ASCII 码(将高位右移四位移到低位)
str[3] = (DS1302_Time[6] & 0x0F) + '0'; //“年”低位数字转换为 ASCII 码
str[4] = '-'; //添加日期分隔符
str[5] = (DS1302_Time[4] >> 4) + '0'; //“月”
str[6] = (DS1302_Time[4] & 0x0F) + '0';
str[7] = '-';
str[8] = (DS1302_Time[3] >> 4) + '0'; //“日”
str[9] = (DS1302_Time[3] & 0x0F) + '0';
str[10] = '\0'; //添加结束符号
Lcd_ShowStr(0, 0, str); //显示到液晶的第一行

str[0] = (DS1302_Time[5] & 0x0F) + '0'; //“星期”
str[1] = '\0';
Lcd_ShowStr(11, 0, "week");
Lcd_ShowStr(15, 0, str); //显示到液晶的第一行

str[0] = (DS1302_Time[2] >> 4) + '0'; //“时”
str[1] = (DS1302_Time[2] & 0x0F) + '0';
str[2] = ':';
str[3] = (DS1302_Time[1] >> 4) + '0'; //“分”
str[4] = (DS1302_Time[1] & 0x0F) + '0';
str[5] = ':';
str[6] = (DS1302_Time[0] >> 4) + '0'; //“秒”
str[7] = (DS1302_Time[0] & 0x0F) + '0';
str[8] = '\0';
Lcd_ShowStr(4, 1, str); //显示到液晶的第二行
}
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "Lcd1602.h"
#include "intrins.h"
#include "DS1302.h"

#endif

红外通信

红外光的基本原理

红外线是波长介于微波和可见光之间的电磁波,波长在 760 纳米到 1 毫米之间,是波形比红光长的非可见光。自然界中的一切物体,只要它的温度高于绝对零度(-273)就存在分子和原子的无规则运动,其表面就会不停的辐射红外线。红外发射管很常用,在我们的遥控器上都可以看到,它类似发光二极管,但是它发射出来的是红外光,是肉眼看不到的。红外发射管发射红外线的强度会随着电流的增大而增强

常见的红外发射管如图所示:

红外接收管内部是一个具有红外光敏感特征的 PN 节,属于光敏二极管,但是它只对红外光有反应。无红外光时,光敏管不导通,有红外光时,光敏管导通形成光电流,并且在一定范围内电流随着红外光的强度的增强而增大。典型的红外接收管如图所示:

红外遥控的原理

红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计算机系统中。

红外遥控通信系统一般由红外发射装置红外接收设备两大部分组成。

  1. 红外发射装置

红外发射装置,也就是通常我们说的红外遥控器是由键盘电路、红外编码电路、电源电路和红外发射电路组成。红外发射电路的主要元件为红外发光二极管。它实际上是一只特殊的发光二极管;由于其内部材料不同于普通发光二极管,因而在其两端施加一定电压时,它便发出的是红外线而不是可见光。目前大量的使用的红外发光二极管发出的红外线波长为 940nm 左右,外形与普通发光二极管相同。红外发光二极管有透明的,还有不透明的。红外遥控器和红外发光二极管如下图所示:

通常红外遥控为了提高抗干扰性能和降低电源消耗,红外遥控器常用载波的方式传送二进制编码,常用的载波频率为 38kHz,这是由发射端所使用的 455kHz晶振来决定的。在发射端要对晶振进行整数分频,分频系数一般取 12,所以455kHz÷12≈37.9kHz≈38kHz。也有一些遥控系统采用 36kHz、 40 kHz、 56 kHz等,一般由发射端晶振的振荡频率来决定。所以,通常的红外遥控器是将遥控信号(二进制脉冲码)调制在 38KHz 的载波上,经缓冲放大后送至红外发光二极管,转化为红外信号发射出去的

二进制脉冲码的形式有多种,其中最为常用的是 NEC Protocol 的 PWM码(脉冲宽度调制)和 Philips RC-5 Protocol 的PPM 码脉冲位置调制码,脉冲串之间的时间间隔来实现信号调制)。如果要开发红外接收设备,一定要知道红外遥控器的编码方式和载波频率

  1. 红外接收设备

红外接收设备是由红外接收电路、红外解码、电源和应用电路组成。红外遥控接收器的主要作用是将遥控发射器发来的红外光信好转换成电信号,再放大、限幅、检波、整形,形成遥控指令脉冲,输出至遥控微处理器。成品红外接收头的封装大致有两种:一种采用铁皮屏蔽;一种是塑料封装。均有三只引脚,即电源正( VDD)、电源负(GND)和数据输出(VOUT)。其外观实物图如下图所示:

正对接收头的凸起处看,从左至右,管脚依次是1:VOUT,2:GND,3:VDD。由于红外接收头在没有脉冲的时候为高电平,当收到脉冲的时候为低电平,所以可以通过外部中断的下降沿触发中断,在中断内通过计算高电平时间来判断接收到的数据是 0 还是 1

NEC协议红外遥控器

其特征如下:

1、8 位地址和 8 位指令长度;
2、地址和命令 2 次传输(确保可靠性)
3、PWM 脉冲位置调制,以发射红外载波的占空比代表“0”和“1”;
4、载波频率为 38Khz;
5、位时间为 1.125ms 或 2.25ms

NEC 码的位定义:一个脉冲对应 560us 的连续载波,一个逻辑 1 传输需要2.25ms(560us 脉冲+1680us 低电平),一个逻辑 0 的传输需要 1.125ms(560us脉冲+560us 低电平)。而红外接收头在收到脉冲的时候为低电平,在没有脉冲的时候为高电平,这样,我们在接收头端收到的信号为:逻辑 1 应该是 560us 低+1680us 高,逻辑 0 应该是 560us 低+560us 高。所以可以通过计算高电平时间判断接收到的数据是 0 还是 1。NEC 码位定义时序图如下图所示:

NEC 遥控指令的数据格式为:引导码、地址码、地址反码、控制码、控制反码(引导码、用户码、用户码(或者用户码反码)、按键键码和 键码反码)。引导码由一个 9ms 的低电平和一个 4.5ms 的高电平组成,地址码、地址反码、控制码、控制反码均是 8 位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可用于校验)。数据格式如下:

NEC 码还规定了连发码(由 9ms 低电平+2.5m 高电平+0.56ms 低电平+97.94ms 高电平组成),如果在一帧数据发送完毕之后,红外遥控器按键仍然没有放开,则发射连发码,可以通过统计连发码的次数来标记按键按下的长短或次数。

原理图

红外接收流程

程序

数码管把遥控器的用户码和键码显示出来

smg.c

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

u8 gsmg[16] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,
0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E
}; //数码管0-F
u8 SMG_BUFF[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; //数码管显示缓冲区,初值0xFF


/* 数码管动态扫描刷新函数,需在定时中断中调用 */
void Smg_Scan()
{
static u8 i; //动态扫描索引
LED_SMG_PORT = 0xFF; //关闭所有段选位,显示消隐
//先将P1的第三位取0(低三位与0进行与运算),然后再与i进行或运算,如果i为1,P1第三位(也就是ADDR2-ADDR0)为001,以此类推
P1 = (P1 & 0xF8) | i; //位选索引值赋值到P1口低3位
LED_SMG_PORT = SMG_BUFF[i]; //索引递增循环,遍历整个缓冲区,由于加了LED显示,点亮LED需要使能LEDS6,所以i小于6不是小于5
if(i < sizeof(SMG_BUFF) - 1)
i++;
else
i = 0;
}

smg.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef _SMG_H_
#define _SMG_H_

#define LED_SMG_PORT P0 //宏定义数码管端口

void Smg_Scan();

extern u8 gsmg[16];
extern u8 SMG_BUFF[6];

#endif

ired.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
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
#include "AllHead.h"

u8 gired_data[4];

/*******************************************************************************
* 函 数 名 : ired_init
* 函数功能 : 红外端口初始化函数,外部中断1配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void Ired_Init(void)
{
IRD = 1; //确保红外接收引脚被释放
TMOD &= 0x0F; //清零 T1 的控制位
TMOD |= 0x10; //配置 T1 为模式 1
IT1 = 1; //设置 INT1 为下降沿触发
EA = 1; //开启总中断
EX1 = 1; //使能 INT1 中断
}

void ired() interrupt 2 //外部中断1服务函数
{
u16 time_cnt = 0;
u8 i, j;
u8 ired_high_time = 0;

if(0 == IRD)
{
time_cnt = 1000;
while((!IRD) && (time_cnt)) //等待引导信号9ms低电平结束,若超过10ms强制退出,若高电平则跳出循环
{
Delay_10us(1); //延时约10us 10us*1000 = 10000us --> 10ms
time_cnt--;
if(0 == time_cnt)
{
return;
}
}
if(IRD) //引导信号9ms低电平已过,进入4.5ms高电平
{
time_cnt = 500;
while((IRD) && (time_cnt)) //等待引导信号4.5ms高电平结束,若超过5ms强制退出
{
Delay_10us(1); //延时约10us 10us*500 = 5000us --> 5ms
time_cnt--;
if(0 == time_cnt) return;
}
for(i = 0; i < 4; i++) //循环4次,读取4个字节数据(地址码、地址反码、控制码、控制反码)
{
for(j = 0; j < 8; j++) //循环8次读取每位数据即一个字节
{
time_cnt = 600;
while((!IRD) && (time_cnt)) //等待数据1或0前面的0.56ms结束,若超过6ms强制退出
{
Delay_10us(1); //延时约10us 10us*600 = 6000us --> 6ms
time_cnt--;
if(0 == time_cnt) return;
}
while(IRD) //等待数据1或0后面的高电平结束,若超过2ms强制退出
{
Delay_10us(10); //约0.1ms 100us*20 == 2000us --> 2ms
ired_high_time++;
if(ired_high_time > 20) return;
}
gired_data[i] >>= 1; //数据是先传低位再传高位,所以接收到的低位数据需要往右移一位
if(ired_high_time >= 8) //如果高电平时间大于0.8ms,数据则为1,否则为0
gired_data[i] |= 0x80; //如果高电平时间大于0.8ms则接收到的数据为1,或上0x80相当于取出最高位的数据
//接着向右移动一位
ired_high_time = 0; //重新清零,等待下一次计算时间
}
}
}
if(gired_data[2] != ~gired_data[3]) //校验控制码与反码,错误则返回
{
for(i = 0; i < 4; i++)
gired_data[i] = 0; //接收到的数据清0
return;
}
}
}

ired.h

1
2
3
4
5
6
7
8
9
10
#ifndef _IRED_H_
#define _IRED_H_

sbit IRD = P3^3;

void Ired_Init(void);

extern u8 gired_data[4];

#endif

time.h

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

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

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

time.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Time0_Init(u16 ms);

#endif

Delay.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "AllHead.h"

/* -------------------------------- begin ------------------------------ */
/**
* @函数名: Delay_10us
* @参数1 : 需要延时多少微秒
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */

void Delay_10us(u16 ten_us) //当传入ten_us==1时,大约延时10us
{
while(ten_us--);
}

Delay.h

1
2
3
4
5
6
#ifndef _DELAY_H_
#define _DELAY_H_

void Delay_10us(u16 ten_us);

#endif

main.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
#include "AllHead.h"

sbit ADDR3 = P1 ^ 3;
sbit ENLED = P1 ^ 4;

void main()
{
ENLED = 0; //使能选择数码管
ADDR3 = 1;
Time0_Init(1); //配置 T0 定时 1ms
Ired_Init(); //初始化红外功能
PT0 = 1; //配置 T0 中断为高优先级,启用本行可消除接收时的闪烁
while(1)
{
SMG_BUFF[5] = gsmg[gired_data[0] >> 4]; //用户码显示
SMG_BUFF[4] = gsmg[gired_data[0] & 0x0F];
SMG_BUFF[1] = gsmg[gired_data[2] >> 4]; //键码显示
SMG_BUFF[0] = gsmg[gired_data[2] & 0x0F];
}
}

/* T0 中断服务函数,执行数码管扫描显示 */
void time0() interrupt 1
{
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
Smg_Scan(); //数码管扫描显示
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "time.h"
#include "smg.h"
#include "intrins.h"
#include "ired.h"
#include "Delay.h"

#endif

问:当我们按下遥控器按键的时候,数码管显示的数字会闪烁,这是什么原因呢?

单片机的程序都是顺序执行的,一旦我们按下遥控器按键,单片机就会进入遥控器解码的中断程序内,而这个程序执行的时间又比较长,要几十个毫秒,而如果数码管动态刷新间隔超过10ms后就会感觉到闪烁,因此这个闪烁是由于程序执行红外解码时,延误了数码管动态刷新造成的,所以解决方法就是设置定时器0中断为高优先级,因为定时器中断执行时间也就几十个us不会对解码有很大的影响

温度传感器 DS18B20

DS18B20 是温度传感器,单片机可以通过 1-Wire 协议与 DS18B20 进行通信,最终将温度读出。1-Wire 总线的硬件接口很简单,只需要把 DS18B20 的数据引脚和单片机的一个 IO 口接上就可以了

DS18B20 内部结构如下图所示:

ROM 中的 64 位序列号是出厂前被光刻好的,它可以看作是该 DS18B20 的地址序列号。光刻 ROM 的作用是使每一个 DS18B20 都各不相同,这样就可以实现一根总线上挂接多个 DS18B20 的目的。

DS18B20 温度传感器的内部存储器包括一个高速的暂存器 RAM 和一个非易失性的可电擦除的 EEPROM,后者存放高温度和低温度触发器 TH、TL 和配置寄存器

配置寄存器是配置不同的位数来确定温度和数字的转化,配置寄存器结构如下:

低五位一直都是"1",TM测试模式位,用于设置 DS18B20 在工作模式还是在测试模式。在 DS18B20 出厂时该位被设置为 0,因此 DS18B20 在工作模式R1 和R0 用来设置 DS18B20 的精度(分辨率),可设置为 9,10,11 或 12 位,对应的分辨率温度是 0.5℃,0.25℃,0.125℃和 0.0625℃。R0 和 R1 配置如下图:

在初始状态下默认的精度是 12 位,即 R0=1、R1=1。高速暂存存储器由 9 个字节组成,其分配如下:

当温度转换命令(44H)发布后,经转换所得的温度值以二字节补码形式存放在高速暂存存储器的第 0 和第 1 个字节。存储的两个字节,高字节的前 5 位是符号位S,单片机可通过单线接口读到该数据,读取时低位在前,高位在后,数据格式如下:

如果测得的温度大于 0,这 5 位符号位S为‘ 0’,只要将测到的数值乘以 0.0625(默认精度是 12 位)即可得到实际温度;如果温度小于 0,这 5 位符号位S为‘ 1’,测到的数值需要取反加 1 再乘以 0.0625 即可得到实际温度。温度与数据对应关系如下:

比如要计算 +85 度,数据输出十六进制是 0X0550,因为要计算的温度大于0,所以高字节的高 5位为 0,十六进制0X0550 对应的二进制为 0000 0101 0101 0000该二进制对应的十进制为 1360,将这个值乘以 12 位精度 0.0625,所以可以得到+85 度。

又比如要计算 -0.5 度,数据输出十六进制是 FFF8,因为要计算的温度小于0,所以高字节的高 5位为 1,十六进制 FFF8 对应的二进制为 1111 1111 1111 1000,先对这个二进制数进行取反为 0000 0000 0000 0111,再进行加 1 ,则对应的十六进制为0x08十六进制 0x08 对应的十进制为 8,将这个值乘以 12 位精度 0.0625,算出来的值为 0.5,因为该温度小于 0 ,所以可以得到 -0.5 度。

读取温度数据

由于 DS18B20是单总线器件,所有的单总线器件都要求采用严格的信号时序,以保数据的完整性。DS18B20 时序包括如下几种:初始化时序、写(0 和 1)时序、 读(0和 1)时序。 DS18B20 发送所有的命令和数据都是字节的低位在前。这里我们简单介绍这几个信号的时序:

初始化时序

单总线上的所有通信都是以初始化序列开始主机输出低电平,保持低电平时间至少 480us(该时间的时间范围可以从 480 到 960 微秒),以产生复位脉冲。接着主机释放总线,外部的上拉电阻将单总线拉高,延时 15~60 us,并进入接收模式。接着 DS18B20 拉低总线 60~240 us,以产生低电平应答脉冲,若为低电平,还要做延时,其延时的时间从外部上拉电阻将单总线拉高算起最少要480 微妙。初始化时序图如下:

写时序

写时序包括写 0 时序和写 1 时序。所有写时序至少需要 60us,且在 2 次独立的写时序之间至少需要 1us 的恢复时间,两种写时序均起始于主机拉低总线。写 1 时序:主机输出低电平,延时 2us,然后释放总线,延时 60us。写 0时序:主机输出低电平,延时 60us,然后释放总线,延时 2us。写时序图如下:

读时序

单总线器件仅在主机发出读时序时,才向主机传输数据,所以,在主机发出读数据命令后,必须马上产生读时序,以便从机能够传输数据。所有读时序至少需要 60us,且在 2 次独立的读时序之间至少需要 1us 的恢复时间。每个读时序都由主机发起,至少拉低总线 1us。主机在读时序期间必须释放总线,并且在时序起始后的 15us 之内采样总线状态。读时序图如下:

典型的读时序过程为:主机输出低电平延时 2us,然后主机转入输入模式延时 2us,然后读取单总线当前的电平,然后延时 50us。

DS18B20 的典型温度读取过程为:复位→发 SKIP ROM 命令(0XCC)→ 发开始转换命令(0X44)(启动温度转换) → 延时 → 复位 → 发送 SKIP ROM 命令(0XCC)→ 发读存储器命令(0XBE)→ 连续读出两个字节数据(即温度) → 结束

原理图

程序

将读到的温度值显示在 1602 液晶上,并且保留一位小数位

DS18B20.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
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
#include "AllHead.h"

/* 复位总线,获取存在脉冲,以启动一次读写操作 */
bit Get_18B20Ack()
{
bit ack;

EA = 0; //禁止总中断
DS18B20_IO = 0; //产生 500us 复位脉冲
Delay_10us(50);
DS18B20_IO = 1;
Delay_10us(6); //延时 60us
ack = DS18B20_IO; //读取存在脉冲
while(!DS18B20_IO); //等待存在脉冲结束
EA = 1; //重新使能总中断
return ack;
}

/* 向 DS18B20 写入一个字节,dat-待写入字节 */
void DS18B20_Write(u8 dat)
{
u8 i;

EA = 0; //禁止总中断
for(i = 0; i < 8; i++)
{
DS18B20_IO = 0; //产生 2us 低电平脉冲
_nop_();
_nop_();
if((dat & 0x01) > 0) //dat最低位如果为大于0(等于1)
DS18B20_IO = 1;
else
DS18B20_IO = 0;
dat >>= 1; //右移一位
Delay_10us(6); //延时 60us
DS18B20_IO = 1; //拉高通信引脚
}
EA = 1; //重新使能总中断
}

/* 从 DS18B20 读取一个字节,返回值-读到的字节 */
u8 DS18B20_Read()
{
u8 i;
u8 dat = 0;

EA = 0; //禁止总中断

for(i = 0; i < 8; i++)
{
DS18B20_IO = 0; //产生 2us 低电平脉冲
_nop_();
_nop_();
DS18B20_IO = 1; //结束低电平脉冲,等待 18B20 输出数据
_nop_();
_nop_(); //延时 2us
dat >>= 1; //右移一位
if(DS18B20_IO) //如果通信引脚上的值为1
dat |= 0x80; //dat最高位赋为1
Delay_10us(5); //再延时 50us
}
EA = 1; //重新使能总中断
return dat;
}

/* 启动一次 18B20 温度转换,返回值-表示是否启动成功 */
bit DS18B20_Start()
{
bit ack;
ack = Get_18B20Ack(); //执行总线复位,并获取 18B20 应答
if(0 == ack) //如 18B20 正确应答,则启动一次转换
{
DS18B20_Write(0xCC); //跳过 ROM 操作
DS18B20_Write(0x44); //启动一次温度转换
}
return ~ack; //ack==0 表示操作成功,所以返回值对其取反
}

/* 读取 DS18B20 转换的温度值,返回值-表示是否读取成功 */
bit DS18B20_Read_Temp(int *temp)
{
bit ack;
u8 LSB, MSB; //16bit 温度值的低字节和高字节
ack = Get_18B20Ack(); //执行总线复位,并获取 18B20 应答
if(0 == ack) //如 18B20 正确应答,则读取温度值
{
DS18B20_Write(0xCC); //跳过 ROM 操作
DS18B20_Write(0xBE); //发送读命令
LSB = DS18B20_Read(); //读温度值的低字节
MSB = DS18B20_Read(); //读温度值的高字节
*temp = ((int)MSB << 8) + LSB; //合成为 16bit 整型数
}
return ~ack; //ack==0 表示操作应答,所以返回值为其取反值
}

DS18B20.h

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef _DS18B20_H_
#define _DS18B20_H_

sbit DS18B20_IO = P3^2; //DS18B20 通信引脚

bit Get_18B20Ack();
void DS18B20_Write(u8 dat);
u8 DS18B20_Read();
bit DS18B20_Start();
bit DS18B20_Read_Temp(int *temp);

#endif

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd1602_Init();

#endif

time.c

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

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

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

time.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Time0_Init(u16 ms);

#endif

Delay.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "AllHead.h"

/* -------------------------------- begin ------------------------------ */
/**
* @函数名: Delay_10us
* @参数1 : 需要延时多少微秒
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */

void Delay_10us(u16 ten_us) //当传入ten_us==1时,大约延时10us
{
while(ten_us--);
}

Delay.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Time0_Init(u16 ms);

#endif

main.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
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 "AllHead.h"

bit flag1s = 0; //1s 定时标志
u8 IntToString(u8 *str, int dat);

void main()
{
bit res; //接收应答
int temp; //读取到的当前温度值
int intT, decT; //温度值的整数和小数部分
u8 len;
u8 str[12];

Time0_Init(10); //T0 定时 10ms
DS18B20_Start(); //启动 DS18B20
Lcd1602_Init(); //初始化液晶

while(1)
{
if(flag1s) //每秒更新一次温度
{
flag1s = 0;
res = DS18B20_Read_Temp(&temp); //读取当前温度
if(res) //读取成功时,刷新当前温度显示
{
intT = temp >> 4; //分离出温度值整数部分
decT = temp & 0x0F; //分离出温度值小数部分
len = IntToString(str, intT); //整数部分转换为字符串
str[len++] = '.'; //添加小数点
decT = (decT * 10) / 16; //二进制的小数部分转换为 1 位十进制位
str[len++] = decT + '0'; //十进制小数位再转换为 ASCII 字符
while(len < 6) //用空格补齐到 6 个字符长度
{
str[len++] = ' ';
}
str[len] = '\0'; //添加字符串结束符
Lcd_ShowStr(0, 0, str); //显示到液晶屏上
}
else //读取失败时,提示错误信息
{
Lcd_ShowStr(0, 0, "error!");
}
DS18B20_Start(); //重新启动下一次转换
}
}
}

/* 整型数转换为字符串,str-字符串指针,dat-待转换数,返回值-字符串长度 */
u8 IntToString(u8 *str, int dat)
{
signed char i = 0;
u8 len = 0;
u8 buf[6];
if(dat < 0) //如果为负数,首先取绝对值,并在指针上添加负号
{
dat = -dat;
*str++ = '-';
len++;
}
do //先转换为低位在前的十进制数组
{
buf[i++] = dat % 10;
dat /= 10;
}
while(dat > 0);
len += i; //i 最后的值就是有效字符的个数
while(i-- > 0) //将数组值转换为 ASCII 码反向拷贝到接收指针上
{
*str++ = buf[i] + '0';
}
*str = '\0'; //添加字符串结束符
return len; //返回字符串长度
}

/* T0 中断服务函数,执行数码管扫描显示 */
void time0() interrupt 1
{
static u8 count = 0;
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
count++;
if(count >= 100) //定时 1s
{
count = 0;
flag1s = 1;
}
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "time.h"
#include "Lcd1602.h"
#include "intrins.h"
#include "Delay.h"
#include "DS18B20.h"

#endif

模数转换 A/D 与数模转换 D/A

A/D 和 D/A 的基本概念

A/D 是模拟量到数字量的转换,依靠的是模数转换器(ADC)。D/A 是数字量到模拟量的转换,依靠的是数模转换器(DAC)

模拟量:指变量在一定范围内连续变化的量,也就是在一定范围内可以取任意值,总之,任何两个数字之间都有无限个中间值,所以称之为连续变化的量,也就是模拟量

ADC 起到把连续的信号用离散的数字表达出来的作用

我们往杯子里倒水,水位会随着倒入的水量的多少而变化。现在就用这个米尺来测量我们杯子里的水位的高度。水位变化是连续的,而我们只能通过尺子上的刻度来读取水位的高度,获取我们想得到的水位的数字量信息。这个过程,就可以简单理解为我们电路中的 ADC 采样

A/D 的主要指标

AD 的种类很多,分为积分型、逐次逼近型、并行/串行比较型、Σ-Δ型等多种类型

  1. ADC 的位数

一个 n 位的 ADC 表示这个 ADC 共有 2 的 n 次方刻度8 位的 ADC,输出的是从 0 ~ 255 一共 256 个数字量,也就是 2 的 8 次方个数据刻度

  1. 基准源

基准源,也叫基准电压,是 ADC 的一个重要指标,要想把输入 ADC 的信号测量准确, 那么基准源首先要准,基准源的偏差会直接导致转换结果的偏差。假如我们的基准源应该是 5.10 V,但是实际上提供的却是 4.5 V, 这样误把 4.5 V 当成了 5.10 V 来处理的话,偏差会比较大

  1. 分辨率

分辨率是数字量变化一个最小刻度时,模拟信号的变化量,定义为满刻度量程与 2^n -1 的 比值。假定 5.10V 的电压系统,使用 8 位的 ADC 进行测量,那么相当于 0~255 一共 256 个 刻度把 5.10V 平均分成了 255 份,那么分辨率就是 5.10 / 255 = 0.02V

  1. INL(积分非线性度)和 DNL(差分非线性度)

分辨率是用来描述刻度划分的,而精度是用来描述准确程度

和 ADC 精度关系重大的两个指标是 INLDNL

INL 指的是 ADC 器件在所有的数值上对应的模拟值和真实值之间误差最大的那一个点的误差值,是 ADC 最重要的一个精度指标,单位是 LSB。LSB 是最低有效位的意思,那么它实际上对应的就是 ADC 的分辨率。一个基准为 5.10V 的 8 位 ADC, 它的分辨率就是 0.02V (5.10 / 255 = 0.02V),用它去测量一个电压信号,得到的结果是 100,就表示它测到的电压值100*0.02V=2V,假定它的 INL 是 1 LSB,就表示这个电压信号真实的准确值是在 1.98V~2.02V 之间的,按理想情况对应得到的数字应该是 99~101测量误差是一个最低有效位,即 1LSB

DNL 表示的是 ADC 相邻两个刻度之间最大的差异,单位也是 LSB。一把分辨率是 1 毫米的尺子,相邻的刻度之间并不都刚好是 1 毫米,而总是会存在或大或小的误差。同理,一个 ADC 的两个刻度线之间也不总是准确的等于分辨率,也是存在误差,这个误差就是 DNL。 一个基准为 5.10V 的 8 位 ADC,假定它的 DNL 是 0.5 LSB,那么当它的转换结果从 100 增加 到 101 时,理想情况下实际电压应该增加 0.02V,但 DNL 为 0.5LSB 的情况下实际电压的增加值是在 0.01~0.03V 之间。 DNL 并非一定小于 1LSB,很多时候它会等于或大于 1LSB,这就相当于是一定程度上的刻度紊乱,当实际电压保持不变时,ADC 得出的结果可能会在几个数值之间跳动,很大程度上就是由于这个原因

  1. 转换速率

转换速率,是指 ADC 每秒能进行采样转换的最大次数,单位是 sps(或 s/s、sa/s),它与 ADC 完成一次从模拟到数字的转换所需要的时间互为倒数关系。ADC 的种类比较多,其中积分型的 ADC 转换时间是毫秒级的,属于低速 ADC逐次逼近型 ADC 转换时间是微秒级的,属于中速 ADC并行/串行的 ADC 的转换时间可达到纳秒级,属于高速 ADC

PCF8591 的硬件接口

PCF8591 是一个单电源低功耗8 位 CMOS 数据采集器件,具有 4 路模拟输入,1 路模拟输出和一个串行 I2C 总线接口用来与单片机通信。与24C02 类似,3 个地址引脚 A0、A1、A2 用于编程硬件地址,允许最多 8 个器件连接到 I2C 总线而不需要额外的片选电路。器件的地址、控制以及数据都是通过 I2C 总线来传输

PCF8591 的原理图

引脚 1、2、3、44 路模拟输入,引脚 5、6、7I2C 总线的硬件地址8 脚数字地 GND9 脚和 10 脚I2C 总线的 SDA 和 SCL12 脚时钟选择引脚,如果接高电平表示用外部时钟输入,接低电平则用内部时钟,我们板子上用的是内部时钟,因此 12 脚直接接 GND,同时 11 脚悬空13 脚模拟地 AGND,在实际开发中,如果有比较复杂的模拟电路, AGND 部分在布局布线上要特别处理,而且和 GND 的连接也有多种方式,板子上没有复杂的模拟部分电路,所以把 AGND 和 GND 接到一起14 脚是基准源,15 脚是 DAC 的模拟输出,16 脚是供电电源 VCC

PCF8591 的 ADC 是逐次逼近型的,转换速率算是中速,但是它的速度瓶颈在 I2C 通信 上。由于 I 2C 通信速度较慢,所以最终的 PCF8591 的转换速度,直接取决于 I2C 的通信速率。 由于 I2C 速度的限制,所以 PCF8591 得算是个低速的 AD 和 DA 的集成,主要应用在一些转换速度要求不高,希望成本较低的场合

Vref 基准电压的提供有两种方法。一是采用简易的原则,直接接到 VCC 上去,但是由于 VCC 会受到整个线路的用电功耗情况影响,一来不是准确的 5V,实测大多在 4.8V 左右, 二来随着整个系统负载情况的变化会产生波动,所以只能用在简易的、对精度要求不高的场合。方法二是使用专门的基准电压器件,比如 TL431,它可以提供一个精度很高的 2.5V 的电压基准,如图所示:

图中 J17 是双排插针,可以根据自己的需求选择跳线帽短接还是使用杜邦线连接其它外部电路,二者都是可以的。在这个地方,直接把 J17 的 3 脚和 4 脚用跳线帽短路起来,那么 Vref 的基准源就是 2.5V 了。分别把 5 和 6、7 和 8、9 和 10、11 和 12 用跳线帽短接起来的话,那么 AIN0 实测的就是电位器的分压值AIN1 和 AIN2 测的是 GND 的值,AIN3 测的是+5V 的值。这里需要注意的是,AIN3 虽然测的是+5V 的值,但是对于 AD 来说,只要输入信号超过 Vref 基准源,它得到的始终都是最大值,即 255,也就是说它实际上无法测量超过其 Vref 的电压信号的。需要注意的是,所有输入信号的电压值都不能超过 VCC,即+5V,否则可能会损坏 ADC 芯片

PCF8591 的软件编程

PCF8591 的通信接口是 I2C,那么编程肯定是要符合这个协议的。单片机对 PCF8591 进行初始化,一共发送三个字节即可。第一个字节,和 EEPROM 类似,是器件地址字节,其中 7 位代表地址,1 位代表读写方向地址高 4 位固定是 0b1001,低三位是 A2,A1,A0, 这三位电路上都接了 GND,因此也就是 0b000,如图 所示

发送到 PCF8591第二个字节被存储在控制寄存器,用于控制 PCF8591 的功能。其中第 3 位和第 7 位是固定的 0,另外 6 位各自有各自的作用,如图所示

控制字节的第 6 位DA 使能位,这一位置 1 表示 DA 输出引脚使能,会产生模拟电压输出功能第 4 位和第 5 位可以实现把 PCF8591 的 4 路模拟输入配置成单端模式和差分模式,如图所示:

● 第 3、7 固定为 0
● 控制字节的 第 6 位是 DA 使能位 ,这一位置 1 表示 DA 输出引脚使能,会产生模拟电压输出功能
● 第 4 位和第 5 位 可以实现把 PCF8591 的 4 路模拟输入 配置成 单端模式和差分模式
● 第 2 位 自动增量控制位 ,自动增量的意思就是,比如我们一共有 4 个通道,当我们全部使用的时候,读完了通道 0,下一次再读,会自动进入通道 1 进行读取,不需要我们指定下一个通道,由于 A/D 每次读到的数据,都是上一次的转换结果,所以在使用自动增量功能的时候,要特别注意, 当前读到的是上一个通道的值
● 控制字节的 第 0 位和第 1 位 就是 通道0~3选择位 ( 00、01、10、11)

程序

AIN0、AIN1、AIN3 测到的电压值显示在液晶上,转动电位器会发现 AIN0 的值发生变化

简单显示 AIN0,AIN1,AIN3 电压在液晶(因为AIN2跟AIN1一样故没显示)

  • 显示结果是 AIN1接地故保持是 0V,AIN3 是基准源2.5V故也保持 2.5V,AIN0可以通过扭动板子电位器使输出 0~2.5V
  • 这里控制字节是 0x40|chn(也就是只使能了DA和选择通道号其余的暂时不操作)
  • val = (val*25) / 255; 255:因为这是8位的ADC,输出数字量是0~255
  • GetADCValue()函数里那两条读语句作用:当前的转换结果总是在下一个字节的 8 个 SCL 上才能读出,因此这里第一条语句的作用是产生一个整体的 SCL 时钟提供给 PCF8591 进行 A/D 转换, 第二次是读取当前的转换结果。如果我们只使用第二条语句的话,每次读到的都是上一次的转换结果

Lcd1602.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
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
#include "AllHead.h"

/* 等待液晶准备好 */
void Lcd_WaitReady() //读状态:RS = L,R/W = H,E = H
{
u8 sta = 0;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do //do while语句是先执行一次语句,再对循环条件进行判断
{
LCD1602_E = 1; //使能
sta = LCD1602_DB; //读取状态字
LCD1602_E = 0; //读完撤销使能,防止液晶输出数据干扰 P0 总线
}
while(sta & 0x80); //最高位等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}

/* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */
void Lcd_WriteCmd(u8 cmd) //写指令:RS = L,R/W = L,D0~D7 = 指令码,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */
void Lcd_WriteDat(u8 dat) //写数据:RS = H,R/W = L,D0~D7 = 数据,E = 高脉冲
{
Lcd_WaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}

/* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */
void Lcd_SetCursor(u8 x, u8 y)
{
u8 addr;
if(0 == y) //由输入的屏幕坐标计算显示RAM的地址
{
addr = 0x00 + x; //第一行字符地址从0x00起始
}
else
{
addr = 0x40 + x; //第二行字符地址从0x40起始
}
Lcd_WriteCmd(addr | 0x80); //设置RAM地址
}

/* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */
void Lcd_ShowStr(u8 x, u8 y, u8 *str)
{
Lcd_SetCursor(x, y); //设置起始地址
while(*str != '\0')
{
Lcd_WriteDat(*str++); //先取str指向的数据然后进入写数据函数,然后str自加1,优先级一样从右往左
}
}

/* 初始化 1602 液晶 */
void Lcd1602_Init()
{
Lcd_WriteCmd(0x38); //16*2显示,5*7点阵,8位数据接口
Lcd_WriteCmd(0x0C); //显示器开,光标关闭,闪烁关闭 0000 1100
Lcd_WriteCmd(0x06); //文字不动,地址自动+1 0000 0110
Lcd_WriteCmd(0x01); //清屏
}

Lcd1602.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef _LCD1602_H_
#define _LCD1602_H_

#define LCD1602_DB P0 //宏定义1602端口
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;

void Lcd_WaitReady();
void Lcd_WriteCmd(u8 cmd);
void Lcd_WriteDat(u8 dat);
void Lcd_SetCursor(u8 x, u8 y);
extern void Lcd_ShowStr(u8 x, u8 y, u8 *str);
extern void Lcd1602_Init();

#endif

I2C.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
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
#include "AllHead.h"

#define I2C_Delay() {_nop_();_nop_();_nop_();_nop_();} //宏定义延时函数(一个_nop_()的时间就是一个机器周期)

/* 产生总线起始信号 */
void I2C_Start()
{
I2C_SCL = 1; //首先确保 SDA、SCL 都是高电平
I2C_SDA = 1;
I2C_Delay();
I2C_SDA = 0; //当SCL为高电平时,SDA由高变为低
I2C_Delay();
I2C_SCL = 0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/* 产生总线停止信号 */
void I2C_Stop()
{
I2C_SCL = 0; //首先确保 SDA、SCL 都是低电平
I2C_SDA = 0;
I2C_Delay();
I2C_SCL = 1; //先拉高 SCL
I2C_Delay();
I2C_SDA = 1; //当SCL为高电平时,SDA由低变为高
I2C_Delay();
}

/* I2C 总线写操作,dat-待写入字节,返回值-从机应答位的值 */
bit I2C_Write(u8 dat)
{
bit ack; //用于暂存应答位的值
u8 i;
for(i = 0; i < 8; i++)
{
if((dat & 0x80) > 0) //比较最高位 (如1011 0100 & 1000 0000 --> 1000 0000 最高位等于1 大于0)
I2C_SDA = 1;
else
I2C_SDA = 0;
dat <<= 1; //左移一位(将次高位移到最高位)
I2C_Delay();
I2C_SCL = 1; //为1数据稳定等待下一次传输
I2C_Delay();
I2C_SCL = 0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
I2C_SDA = 1; //8 位数据发送完后,主机释放 SDA,以检测从机应答
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
ack = I2C_SDA; //读取此时的 SDA 值,即为从机的应答值
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线
return (~ack); //应答值取反以符合通常的逻辑:
} //0=不存在或忙或写入失败,1=存在且空闲或写入成功

/* I2C 总线读操作,并发送非应答信号,返回值-读到的字节 */
u8 I2C_ReadNACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0;//再拉低 SCL,以使从机发送出下一位 为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 1; //8 位数据发送完后,拉高 SDA,发送非应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成非应答位,并保持住总线

return receive;
}

/* I2C 总线读操作,并发送应答信号,返回值-读到的字节 */
u8 I2C_ReadACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0; //为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 0; //8 位数据发送完后,拉低 SDA,发送应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线

return receive;
}

I2C.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _I2C_H_
#define _I2C_H_

sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;

void I2C_Start();
void I2C_Stop();
bit I2C_Write(u8 dat);
u8 I2C_ReadNACK();
u8 I2C_ReadACK();

#endif

time.c

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

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

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

time.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Time0_Init(u16 ms);

#endif

main.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
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
#include "AllHead.h"

bit flag300ms = 1; //300ms 定时标志
u8 IntToString(u8 *str, int dat);

u8 ADC_Get_Value(u8 chn);
void ValueToString(u8 *str, u8 val);
void main()
{
u8 val;
u8 str[10];
Time0_Init(10); //T0 定时 10ms
Lcd1602_Init(); //初始化液晶
Lcd_ShowStr(0, 0, "AIN0 AIN1 AIN3"); //显示通道指示

while(1)
{
if(flag300ms) //每秒更新一次温度
{
flag300ms = 0;
//显示通道 0 的电压(电位器的分压值)
val = ADC_Get_Value(0); //获取 ADC 通道 0 的转换值
ValueToString(str, val); //转为字符串格式的电压值
Lcd_ShowStr(0, 1, str); //显示到液晶上
//显示通道 1 的电压(GND 的值) 通道1和2都是测量GND的电压值
val = ADC_Get_Value(1);
ValueToString(str, val);
Lcd_ShowStr(6, 1, str);
//显示通道 3 的电压(+5V 的值)
val = ADC_Get_Value(3);
ValueToString(str, val);
Lcd_ShowStr(12, 1, str);
}
}
}

/* 读取当前的 ADC 转换值,chn-ADC 通道号 0~3 */
u8 ADC_Get_Value(u8 chn)
{
u8 val;
I2C_Start();
if(!I2C_Write(0x90)) //寻址 PCF8591,如未应答,则停止操作并返回 0 1001 0000 --> 0x90
{
I2C_Stop();
return 0;
}
I2C_Write(0x40 | chn); //写入控制字节,选择转换通道 0100 0000 | chn (控制字节的第6位是DA使能位,这一位置1表示DA输出引脚使能,会产生模拟电压输出功能)
I2C_Start();
I2C_Write(0x91); //寻址 PCF8591,指定后续为读操作 1001 0001 --> 0x91 最后一位为 1 表示读操作
I2C_ReadACK(); //先空读一个字节,提供采样转换时间
val = I2C_ReadNACK(); //读取刚刚转换完的值
I2C_Stop();
return val;
}

/* ADC 转换值转为实际电压值的字符串形式,str-字符串指针,val-AD 转换值 */
void ValueToString(u8 *str, u8 val)
{
//电压值=转换结果*2.5V/255,式中的 25 隐含了一位十进制小数
val = (val * 25) / 255;
str[0] = (val / 10) + '0'; //整数位字符
str[1] = '.'; //小数点
str[2] = (val % 10) + '0'; //小数位字符
str[3] = 'V'; //电压单位
str[4] = '\0'; //结束符
}

/* T0 中断服务函数,执行数码管扫描显示 */
void time0() interrupt 1
{
static u8 count = 0;
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
count++;
if(count >= 30) //定时 1s
{
count = 0;
flag300ms = 1;
}
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "time.h"
#include "Lcd1602.h"
#include "I2C.h"
#include "intrins.h"

#endif

A/D 差分输入信号

控制字的第 4 位和第 5 位是用于控制 PCF8591 的模拟输入引脚是 单端输入还是差分输入

从严格意义上来讲,其实所有的信号都是差分信号,因为所有的电压只能是相对于另外一个电压而言。但是大多数系统,我们都是把系统的 GND 作为基准点。而对于 A/D 来说的差分输入,通常情况下是除了 GND 以外,另外两路幅度相同,极性相反的输入信号

差分输入的话,就不是单个输入,而是由 2 个输入端构成的一组输入。PCF8591 一共是 4 个模拟输入端,可以配置成 4 种模式,最典型的是 4 个输入端构造成的两路差分模式,如图所示:

当控制字的第 4 位和第 5 位都是 1 的时候,那么 4 路模拟被配置成 2 路差分模式输入 channel 0 和 channel 1。以 channel 0 为例,其中 AIN0 是正向输入端,AIN1 是反向输入端,它们之间的信号输入是幅度相同,极性相反的信号,通过减法器后,得到的是两个输入通道的差值,如图所示

通常情况下,差分输入的中线是基准电压的一半,我们的基准电压是 2.5V,假如 1.25V 作为中线,V+是 AIN0 的输入波形,V-是 AIN1 的输入波形,Signal Value 就是经过减法器后的波形。很多 A/D 都采用差分的方式输入,因为差分输入方式比单端输入来说,有更强的抗干扰能力

单端输入信号时,如果一线上发生干扰变化,比如幅度增大 5mv,GND 不变,测到的数据会有偏差;而差分信号输入时,当外界存在干扰信号时,只要布线合理,大都同时被耦合到两条线上,幅度增大 5mv 会同时增大 5mv,而接收端关心的只是两个信号的差值,所以外界的这种共模噪声可以被完全抵消掉。由于两根信号的极性相反,它们对外辐射的电磁场可以相互抵消,有效的抑制释放到外界的电磁能量

D/A 输出

D/A 是和 A/D 刚好反方向的,一个 8 位的 D/A,从 0~255,代表了 0~2.55V 的话,那么用单片机给第三个字节发送 100,D/A 引脚就会输出一个 1V 的电压,发送 200 就输出 一个 2V 的电压

D/A 输出实验

用单片机给第三个字节发送 100,D/A 引脚就会输出一个 1V 的电压,发送 200 就输出 一个 2V 的电压,并且通过上、下按键可以增大或减小输出幅度值,每次增加或减小 0.1V。如果有万用表的话,可以直接测试一下板子上 AOUT 点的输出电压,观察它的变化

key.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
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
#include "AllHead.h"

u8 code KeyCodeMap[4][4] = //矩阵按键编号到标准键盘键码的映射表
{
{ '1', '2', '3', 0x26 }, //数字键 1、数字键 2、数字键 3、向上键
{ '4', '5', '6', 0x25 }, //数字键 4、数字键 5、数字键 6、向左键
{ '7', '8', '9', 0x28 }, //数字键 7、数字键 8、数字键 9、向下键
{ '0', 0x1B, 0x0D, 0x27 } //数字键 0、ESC 键、 回车键、 向右键
};
u8 pdata Key_State[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}};//按键当前状态

/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void Key_Driver()
{
u8 i, j;
static u8 backup[4][4] = {{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}}; //按键值备份,保存前一次的值
for(i = 0; i < 4; i++) //循环扫描 4*4 的矩阵按键
{
for(j = 0; j < 4; j++)
{
if(backup[i][j] != Key_State[i][j]) //当前值与前次值不相等说明此时按键有动作
{
if(backup[i][j] != 0) //前次值不等于0,则当前值等于0,按键按下
{
Key_Action(KeyCodeMap[i][j]); //调用按键动作函数
}
backup[i][j] = Key_State[i][j]; //更新前一次的备份值
}
}
}
}

/* 按键扫描函数,需在定时中断中调用 */
void Key_Scan()
{
u8 i = 0;
static u8 KeyOut = 0; //矩阵按键扫描输出索引
static u8 keybuf[4][4] = {{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
}; //矩阵按键扫描缓冲区
//将一行的 4 个按键值移入缓冲区
keybuf[KeyOut][0] = (keybuf[KeyOut][0] << 1) | KeyIn1;
keybuf[KeyOut][1] = (keybuf[KeyOut][1] << 1) | KeyIn2;
keybuf[KeyOut][2] = (keybuf[KeyOut][2] << 1) | KeyIn3;
keybuf[KeyOut][3] = (keybuf[KeyOut][3] << 1) | KeyIn4;
//消抖后更新按键状态
for (i = 0; i < 4; i++) //每行 4 个按键,所以循环 4 次
{
if (0x00 == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
Key_State[KeyOut][i] = 0;
}
else if (0x0F == (keybuf[KeyOut][i] & 0x0F))
{
//连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
Key_State[KeyOut][i] = 1;
}
}
//执行下一次的扫描输出
KeyOut++; //输出索引递增
if (KeyOut >= 4)
{
KeyOut = 0;
}
// KeyOut = KeyOut & 0x03; //索引值加到 4 即归零
switch(KeyOut) //根据索引,释放当前输出引脚,拉低下次的输出引脚
{
case 0:
KeyOut4 = 1;
KeyOut1 = 0;
break;
case 1:
KeyOut1 = 1;
KeyOut2 = 0;
break;
case 2:
KeyOut2 = 1;
KeyOut3 = 0;
break;
case 3:
KeyOut3 = 1;
KeyOut4 = 0;
break;
default:
break;
}
}

key.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef _KEY_H_
#define _KEY_H_

sbit KeyIn1 = P2^4;
sbit KeyIn2 = P2^5;
sbit KeyIn3 = P2^6;
sbit KeyIn4 = P2^7;
sbit KeyOut1 = P2^3;
sbit KeyOut2 = P2^2;
sbit KeyOut3 = P2^1;
sbit KeyOut4 = P2^0;

extern void Key_Driver();
extern void Key_Scan();
extern void Key_Action(u8 keycode);

#endif

I2C.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
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
#include "AllHead.h"

#define I2C_Delay() {_nop_();_nop_();_nop_();_nop_();} //宏定义延时函数(一个_nop_()的时间就是一个机器周期)

/* 产生总线起始信号 */
void I2C_Start()
{
I2C_SCL = 1; //首先确保 SDA、SCL 都是高电平
I2C_SDA = 1;
I2C_Delay();
I2C_SDA = 0; //当SCL为高电平时,SDA由高变为低
I2C_Delay();
I2C_SCL = 0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/* 产生总线停止信号 */
void I2C_Stop()
{
I2C_SCL = 0; //首先确保 SDA、SCL 都是低电平
I2C_SDA = 0;
I2C_Delay();
I2C_SCL = 1; //先拉高 SCL
I2C_Delay();
I2C_SDA = 1; //当SCL为高电平时,SDA由低变为高
I2C_Delay();
}

/* I2C 总线写操作,dat-待写入字节,返回值-从机应答位的值 */
bit I2C_Write(u8 dat)
{
bit ack; //用于暂存应答位的值
u8 i;
for(i = 0; i < 8; i++)
{
if((dat & 0x80) > 0) //比较最高位 (如1011 0100 & 1000 0000 --> 1000 0000 最高位等于1 大于0)
I2C_SDA = 1;
else
I2C_SDA = 0;
dat <<= 1; //左移一位(将次高位移到最高位)
I2C_Delay();
I2C_SCL = 1; //为1数据稳定等待下一次传输
I2C_Delay();
I2C_SCL = 0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
I2C_SDA = 1; //8 位数据发送完后,主机释放 SDA,以检测从机应答
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
ack = I2C_SDA; //读取此时的 SDA 值,即为从机的应答值
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线
return (~ack); //应答值取反以符合通常的逻辑:
} //0=不存在或忙或写入失败,1=存在且空闲或写入成功

/* I2C 总线读操作,并发送非应答信号,返回值-读到的字节 */
u8 I2C_ReadNACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0;//再拉低 SCL,以使从机发送出下一位 为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 1; //8 位数据发送完后,拉高 SDA,发送非应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成非应答位,并保持住总线

return receive;
}

/* I2C 总线读操作,并发送应答信号,返回值-读到的字节 */
u8 I2C_ReadACK()
{
u8 receive = 0; //保存读取的数据
u8 i;
I2C_SDA = 1; //首先确保主机释放 SDA
for(i = 0; i < 8; i++)
{
I2C_SCL = 0;
I2C_Delay();
I2C_SCL = 1; //SCL为1时数据稳定,可以传输数据
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(1 == I2C_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
I2C_Delay(); //则SDA为0,不进入if语句,receive接收到0,则receive
I2C_SCL = 0; //为 0000 0010,之后将receive向左移动一位,则receive为
} //0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
I2C_SDA = 0; //8 位数据发送完后,拉低 SDA,发送应答信号
I2C_Delay();
I2C_SCL = 1; //拉高 SCL
I2C_Delay();
I2C_SCL = 0; //再拉低 SCL 完成应答位,并保持住总线

return receive;
}

I2C.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef _I2C_H_
#define _I2C_H_

sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;

void I2C_Start();
void I2C_Stop();
bit I2C_Write(u8 dat);
u8 I2C_ReadNACK();
u8 I2C_ReadACK();

#endif

time.c

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

u8 T0RH = 0; //T0 重载值的高字节
u8 T0RL = 0; //T0 重载值的低字节

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

time.h

1
2
3
4
5
6
7
8
9
#ifndef _TIME_H_
#define _TIME_H_

extern u8 T0RH; //由于外部文件用到该变量,所以前面要加 extern
extern u8 T0RL;

void Time0_Init(u16 ms);

#endif

main.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include "AllHead.h"

void Key_Action(u8 keycode);
void SetDACOut(unsigned char val);

void main()
{
Time0_Init(1); //T0 定时 1ms

while(1)
{
Key_Driver();
}
}

/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void Key_Action(u8 keycode)
{
static u8 volt = 0; //输出电压值,隐含了一位十进制小数位

if (0x26 == keycode) //向上键,增加 0.1V 电压值
{
if (volt < 25)
{
volt++;
SetDACOut(volt * 255 / 25); //转换为 AD 输出值
}
}
else if (keycode == 0x28) //向下键,减小 0.1V 电压值
{
if (volt > 0)
{
volt--;
SetDACOut(volt * 255 / 25); //转换为 AD 输出值
}
}
}

/* 设置 DAC 输出值,val-设定值 */
void SetDACOut(unsigned char val)
{
I2C_Start();
if (!I2C_Write(0x90)) //寻址 PCF8591,如未应答,则停止操作并返回
{
I2C_Stop();
return;
}
I2C_Write(0x40); //写入控制字节
I2C_Write(val); //写入 DA 值
I2C_Stop();
}

/* T0 中断服务函数,执行数码管扫描显示 */
void time0() interrupt 1
{
TH0 = T0RH; //加载T0重载值
TL0 = T0RL;
Key_Scan(); //按键扫描
}

AllHead.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef _ALLHEAD_H_
#define _ALLHEAD_H_

typedef unsigned char u8; //对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned long u32;

#include <reg52.h>
#include "key.h"
#include "time.h"
#include "I2C.h"
#include "intrins.h"

#endif