前言

这个板子是普中A2 STC89C52RC的板子,下面为开发板各功能模块

单片机最小系统(1)晶振电路(2)复位电路(3)电源电路(4)下载电路

GPIO概念:GPIO是通用输入输出端口的简称,可以通过软件来控制其输入和输出。

开发板上使用的 51 单片机型号是 STC89C52RC,此芯片共有 40 引脚,芯片引脚图如下图所示:

51单片机引脚分类:
(1)电源引脚:引脚图中的VCC、GND都属于电源引脚。

(2)晶振引脚:引脚图中的XTAL1、XTAL2都属于晶振引脚。

(3)复位引脚:引脚图中的RST/VPD属于复位引脚,不做其他功能使用。

(4)下载引脚:51单片机的串口功能引脚(TXD、RXD)可以作为下载引脚使用。

(5)GPIO引脚:引脚图中带有Px.x等字样的均属于GPIO引脚。从引脚图可以看出,GPIO占用了芯片大部分的引脚,共达32个,分为了4组,P0、P1、P2、P3,每组为8个IO,而且在P3组中每个IO都具备额外功能,只要通过相应的寄存器设置即可配置对应的附加功能,同一时刻,每个引脚只能使用该引脚的一个功能

LED

LED简介

LED(light-emitting diode),即 发光二极管,俗称LED小灯,是一种由磷化镓(GaP)等半导体材料制成的、能直接将电能转变成光能的发光显示件。当LED内部有一定电流通过时,它就会发光,不同LED能发出不同颜色的光,常见的有红色、黄色等。

LED原理图

LED程序

控制LED闪烁

因为是正极流进来,想要点亮LED灯所以要给P2.0低电平,单片机初始状态管脚默认为高电平,即对应值为1。所以如果想要点亮LED灯,只需要让他的管脚电平为低电平即可,即对应值为0

LED.c

1
2
3
4
5
6
7
8
9
10
11
#include "AllHead.h"
/*
功能:控制LED1灯闪烁
*/
void LED_flicker()
{
LED1 = 0; //LED1端口设置为低电平
delay_10us(50000);
LED1 = 1; //LED1端口设置为高电平
delay_10us(50000);
}

LED.h

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

#include "AllHead.h"

sbit LED1 = P2^0; //将p2^0管脚定义为LED1

/*=============函数声明区=============*/
void LED_flicker();

#endif

Delay.c

1
2
3
4
5
6
7
8
9
10
11
12
#include "AllHead.h"
/* -------------------------------- begin ------------------------------ */
/**
* @函数名: delay_us
* @参数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
10
11
#ifndef _DELAY_H
#define _DELAY_H

#include "AllHead.h"

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

void delay_10us(u16 ten_us);

#endif

main.c

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

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

AllHead.h

将所有的头文件放到AllHead.h,这样子就比较方便,直接在各个模块下调用这个头文件即可

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

#include <reg52.h>
#include "LED.h"
#include "Delay.h"

#endif

LED流水灯

~ 这个符号表示按位取反 (0变1 1变0)

LED.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "AllHead.h"
/*
功能:LED流水灯
*/
void LED_fall()
{
u8 i = 0;
for(i = 0;i < 8;i++)
{
LED_PORT = ~(0x01 << i); //0000 0001 左移i位 按位取反变成 1111 1110 (点亮第一个灯)以此类推循环8次
delay_10us(50000);
}
}

LED.h

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

#include "AllHead.h"

#define LED_PORT P2 //宏定义P2端口

/*=============函数声明区=============*/
void LED_fall();

#endif

Delay.c

1
2
3
4
5
6
7
8
9
10
11
12
#include "AllHead.h"
/* -------------------------------- begin ------------------------------ */
/**
* @函数名: delay_us
* @参数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
10
11
#ifndef _DELAY_H
#define _DELAY_H

#include "AllHead.h"

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

void delay_10us(u16 ten_us);

#endif

main.c

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

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

AllHead.h

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

#include <reg52.h>
#include "LED.h"
#include "Delay.h"

#endif

用库函数写LED灯左右来回流水

还有一种流水灯的做法是用库函数:#include “intrins.h”

crol_和_cror都是库函数,每次移动后不会自动补0而是把前面移出去的补到后面去

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
#include "AllHead.h"
/*
功能:用库函数写LED流水灯
_crol_函数(左移函数) :参数一:用来控制位; 参数二:用来控制每次移动的位数
_cror_函数(右移函数) :参数一:用来控制位; 参数二:用来控制每次移动的位数
*/
void LED_ku_fall()
{
u8 i = 0;
LED_PORT = ~0x01; //先点亮第一个LED灯(0000 0001 按位取反 --> 1111 1110) or 直接写0xfe
Delay1ms(500);
for(i = 0;i < 7;i++) //循环七次,因为灯一开始就点亮了
{
LED_PORT = _crol_(LED_PORT,1); //LED从左边开始每次移动一位
//(1111 1110 向左移动一位 --> 1111 1101)
Delay1ms(500); //延时500毫秒
}
for(i = 0;i < 7;i++) //循环七次,因为灯一开始就点亮了
{
LED_PORT = _cror_(LED_PORT,1); //LED从右边开始每次移动一位
//(0111 1111 向右移动一位 --> 1011 1111)
Delay1ms(500);
}
}

LED.h

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

#include "AllHead.h"

#define LED_PORT P2 //宏定义P2端口

/*=============函数声明区=============*/
void LED_ku_fall();

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

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

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

AllHead.h

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

#include <reg52.h>
#include "LED.h"
#include "Delay.h"
#include "intrins.h" //库函数

#endif

蜂鸣器

蜂鸣器简介

蜂鸣器:是一种一体化结构的电子讯响器,采用直流电压供电,分为 压电式蜂鸣器电磁式蜂鸣器(无源和有源), 51 开发板一般是 压电式 的蜂鸣器。

根据网上查的资料可以知道无源和有源的区别:

(1)有无震荡源

无源这里的“源”不是指电源,而是指震荡源。也就是说,有源蜂鸣器内部带震荡源,所以只要一通电就会叫。而无源内部不带震荡源,所以如果用直流信号无法令其鸣叫。

(2)价格不同

有源蜂鸣器往往比无源蜂鸣器,就是因为里面多个震荡电路。

怎么区分?

有绿色电路板的一种是无源蜂鸣器,没有电路板而用黑胶封闭的一种是有源蜂鸣器。

无源蜂鸣器通过脉冲频率才能发声。

蜂鸣器原理图

从图中可以看出,蜂鸣器控制管脚直接连接到 51 单片机的 P2.5 管脚上。图中并没有使用三极管进行电流放大,而是使用 ULN2003 芯片来驱动,当 P2.5 输入高电平BEEP 则输出低电平;当 P2.5 输入低电平BEEP 则输出高电平,类似一个非门

开发板上使用的是无源蜂鸣器,它需要一定频率的脉冲(高低电平)才会发声,因此需要让 P2.5 脚以一定频率不断输出高低电平信号才能控制蜂鸣器发出声音

蜂鸣器程序

蜂鸣器发出声音,一段时间后关闭

BEEP.c

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

void BEEP_Contral()
{
u16 i = 2000; //控制循环次数
while(1)
{
while(i--) //循环2000次(循环2000次之后跳出循环)
{
BEEP = !BEEP; //产生一定频率的脉冲信号
Delay1ms(1);
}
i = 0; //清零 赋0蜂鸣器就不会再响了
BEEP = 0; //关闭蜂鸣器 赋0和1都行
}
}

BEEP.h

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

#include "AllHead.h"

sbit BEEP = P2^5; //将P2.5管脚定义为BEEP

void BEEP_Contral();

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

1
2
3
4
5
6
7
void main( )
{
while(1)
{
BEEP_Contral();
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "BEEP.h"

#endif

修改变量 i 的值可以改变蜂鸣器发声时间

若要改变音调可以修改延时时间,但要注意频率不能太大或者太小,若要改变音量,可以修改 BEEP 输出高电平时间

数码管

数码管简介

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

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

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

数码表

共阴数码管码表

共阳数码管码表

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

原理图

数码管显示

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

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

74HC245芯片

74HC245 是一种三态输出、八路信号收发器,主要应用于大屏显示,以及其它的消费类电子产品中增加驱动。

(1)主要特性

①采用 CMOS 工艺
②宽电压工作范围:3.0V-5.0V
③双向三态输出
④八线双向收发器
⑤封装形式:SOP20、SOP20-2、TSSOP20、DIP20

(2)管脚功能定义

从上面的管脚功能定义说明及真值表可以知道该芯片使用方法很简单,给 OE 使能管脚低电平DIR 管脚为高电平,传输方向是 A->B, DIR 管脚为低电平,传输方向是 B->A,至于输出高电平还是输出低电平取决于输入端的状态,如果输入为低电平输出即为低输入为高电平输出即为高。如果 OE 使能管脚为高电平, 不论 DIR 管脚是高还是低,输出是高组态。 通常我们使用 74HC245 芯片用作驱动只会让其在一个方向输出,即 DIR 管脚高电平,传输方向是 A-->B

74HC138 芯片

74HC138D 是一种三通道输入、八通道输出译码器,主要应用于消费类电子产品

(1)主要特性

①采用CMOS 工艺
②低功耗
③工作电压:3.0V-5.0V
④封装形式:SOP16

(2)管脚功能定义

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

从上面的管脚功能定义说明及真值表可以知道该芯片使用方法很简单,给 E1、E2 使能管脚低电平,E3 管脚为高电平,至于哪个管脚输出有效电平(低电平),要看 A0,A1,A2 输入管脚的电平状态。如果 A0,A1,A2 都为低电平,则 Y0 输出有效电平(低电平),其他管脚均输出高电平。如果 A0 为高电平,A1, A2 都为低电平,则 Y1 输出有效电平(低电平),其他管脚均输出高电平。如果 E1、E2 使能管脚任意一个为高电平或者 E3 为低电平,不论输入是什么,输出都为高电平。

方法:A0、A1、A2 输入就相当于 3 位 2 进制数,A0 是低位,A1 是次高位,A2 是高位。而 Y0-Y7 具体哪一个输出有效电平,就看输入二进制对应的十进制数值。比如输入是 101(A2,A1,A0),其对应的十进制数是 5,所以Y5输出有效电平(低电平)。

数码管程序

静态数码管实验

实验现象:下载程序后“数码管模块”最左边数码管显示数字 0

main.c

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

#define SMG_A_DP_PORT P0 //宏定义数码管P0端口
u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
//定义数组存放0-F段码,gsmg中g代表全局变量

void main( )
{
SMG_A_DP_PORT = gsmg_code[0];
while(1)
{

}
}

动态数码管实验

从左到右显示数字1到7

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

u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
//定义数组存放0-F段码,gsmg中g代表全局变量

/********************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
*********************************************/
void smg_display()
{
u8 i = 0;
for(i = 0;i < 8;i++)
{
switch(i) //位选(选择在第几个位置显示)
{
case 0: LSC = 1;LSB = 1;LSA = 1;break; //Y7 LED8 板子从左边数第一个数码管
case 1: LSC = 1;LSB = 1;LSA = 0;break; //Y6 LED7 板子从左边数第二个数码管
case 2: LSC = 1;LSB = 0;LSA = 1;break; //Y5 LED6 板子从左边数第三个数码管
case 3: LSC = 1;LSB = 0;LSA = 0;break; //Y4 LED5 板子从左边数第四个数码管
case 4: LSC = 0;LSB = 1;LSA = 1;break; //Y3 LED4 板子从左边数第五个数码管
case 5: LSC = 0;LSB = 1;LSA = 0;break; //Y2 LED3 板子从左边数第六个数码管
case 6: LSC = 0;LSB = 0;LSA = 1;break; //Y1 LED2 板子从左边数第七个数码管
case 7: LSC = 0;LSB = 0;LSA = 0;break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = gsmg_code[i]; //传输段选数据
Delay1ms(1); //延时1毫秒左右,等待显示稳定
SMG_A_DP_PORT = 0x00; //消影
}
}

smg.h

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

#include "AllHead.h"

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

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display();

extern u8 gsmg_code[17];

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

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

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

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "smg.h"

#endif
动态数码管显示日期

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

u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
//定义数组存放0-F段码,gsmg中g代表全局变量

/********************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
*********************************************/
void smg_display(u8 location,u8 number)
{
switch(location) //位置
{
case 0: LSC = 1;LSB = 1;LSA = 1;break; //Y7 LED8 板子从左边数第一个数码管
case 1: LSC = 1;LSB = 1;LSA = 0;break; //Y6 LED7 板子从左边数第二个数码管
case 2: LSC = 1;LSB = 0;LSA = 1;break; //Y5 LED6 板子从左边数第三个数码管
case 3: LSC = 1;LSB = 0;LSA = 0;break; //Y4 LED5 板子从左边数第四个数码管
case 4: LSC = 0;LSB = 1;LSA = 1;break; //Y3 LED4 板子从左边数第五个数码管
case 5: LSC = 0;LSB = 1;LSA = 0;break; //Y2 LED3 板子从左边数第六个数码管
case 6: LSC = 0;LSB = 0;LSA = 1;break; //Y1 LED2 板子从左边数第七个数码管
case 7: LSC = 0;LSB = 0;LSA = 0;break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = gsmg_code[number]; //传输段选数据
Delay1ms(1); //延时1毫秒左右,等待显示稳定
SMG_A_DP_PORT = 0x00; //消影
}

smg.h

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

#include "AllHead.h"

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

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display(u8 location,u8 number);

extern u8 gsmg_code[17];

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

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

void main( )
{
while(1)
{
smg_display(0,2);
smg_display(1,0);
smg_display(2,2);
smg_display(3,3);
smg_display(4,0);
smg_display(5,7);
smg_display(6,1);
smg_display(7,3);
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "smg.h"

#endif

按键

独立按键介绍

按键是一种电子开关,使用时轻轻按开关按钮就可使开关接通,当松开手时, 开关断开

“1,2”和“3,4”管脚之间距离短,初始不导通,“1,3”和“2,4”管脚之间距离长,初始值导通。

当按键按下时,距离短的会变为导通,距离长的会变为不导通 ,所以就可以利用按键这一特性来控制其他的事物。

例如管脚1接单片机的一个引脚,管脚2接地。当按键被按下时,就会给这个引脚一个 低电平 。如果不按,单片机的这个引脚默认的是 高电平

但是按键一般都会抖动,所以要进行消抖: 硬件消抖和软件消抖

硬件消抖是通过 充放电延时时间 来进行消抖,但成本高,一个按键就需要(一个电阻与一个电源),所以 一般选择软件消抖,软件消抖时间一般为 10ms

按键管脚两端距离长的表示默认是导通状态,距离短的默认是断开状态, 如果按键按下,初始导通状态变为断开,初始断开状态变为导通。 通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,电压信号如下图所示

独立按键原理图

独立按键程序

独立按键控制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
#include "AllHead.h"

/******************************************************************************
* 函 数 名 : key_scan
* 函数功能 : 检测独立按键是否按下,按下则返回对应键值
* 输 入 : mode=0:单次扫描按键
mode=1:连续扫描按键
* 输 出 : KEY1_PRESS:K1 按下
KEY2_PRESS:K2 按下
KEY3_PRESS:K3 按下
KEY4_PRESS:K4 按下
KEY_UNPRESS:未有按键按下
********************************************************************************/
u8 key_scan(u8 mode)
{
static u8 key = 1; //定义一个静态变量
if(mode) key = 1; //如果mode=1则表示连续扫描按键 如果mode=0则忽略这句话
if(key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0 )) //任意按键按下(&&这个符号表示两个条件必须同时满足才能进入这个if语句)
{
Delay1ms(10); //消抖
key = 0;
if(KEY1 == 0) //如果按键仍然处于被按下状态则表明按键真的被按下了
return KEY1_PRESS;
else if(KEY2 == 0)
return KEY2_PRESS;
else if(KEY3 == 0)
return KEY3_PRESS;
else if(KEY4 == 0)
return KEY4_PRESS;
}
else if(KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1 ) //无按键按下
{
key = 1;
}
return KEY_UNPRESS;
}

Key.h

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

#include "AllHead.h"

//定义4个独立按键控制管脚
sbit KEY1 = P3^1;
sbit KEY2 = P3^0;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;

//使用宏定义独立按键按下的键值(如果按下则返回)
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4

//没按下
#define KEY_UNPRESS 0

u8 key_scan(u8 mode);

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "AllHead.h"
sbit LED1 = P2^0; //将p2^0管脚定义为LED1

void main( )
{
while(1)
{
key = key_scan(0); //0代表单次扫描按键
if(key == KEY1_PRESS) //如果按键K1按下
{
LED1 = !LED1; //LED1状态翻转
}
}
}

AllHead.h

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

#include <reg52.h>
#include "LED.h"
#include "Delay.h"
#include "key.h"

#endif

独立按键控制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
#include "AllHead.h"

/******************************************************************************
* 函 数 名 : key_scan
* 函数功能 : 检测独立按键是否按下,按下则返回对应键值
* 输 入 : mode=0:单次扫描按键
mode=1:连续扫描按键
* 输 出 : KEY1_PRESS:K1 按下
KEY2_PRESS:K2 按下
KEY3_PRESS:K3 按下
KEY4_PRESS:K4 按下
KEY_UNPRESS:未有按键按下
********************************************************************************/
u8 key_scan(u8 mode)
{
static u8 key = 1; //定义一个静态变量
if(mode) key = 1; //如果mode=1则表示连续扫描按键 如果mode=0则忽略这句话
if(key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0 )) //任意按键按下(&&这个符号表示两个条件必须同时满足才能进入这个if语句)
{
Delay1ms(10); //消抖
key = 0;
if(KEY1 == 0) //如果按键仍然处于被按下状态则表明按键真的被按下了
return KEY1_PRESS;
else if(KEY2 == 0)
return KEY2_PRESS;
else if(KEY3 == 0)
return KEY3_PRESS;
else if(KEY4 == 0)
return KEY4_PRESS;
}
else if(KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1 ) //无按键按下
{
key = 1;
}
return KEY_UNPRESS;
}

Key.h

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

#include "AllHead.h"

//定义4个独立按键控制管脚
sbit KEY1 = P3^1;
sbit KEY2 = P3^0;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;

//使用宏定义独立按键按下的键值(如果按下则返回)
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4

//没按下
#define KEY_UNPRESS 0

u8 key_scan(u8 mode);

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(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
#include "AllHead.h"

void main( )
{
u8 key = 0;
u8 LEDNum = ~0x01; //点亮第一个LED灯(因为下面程序直接LEDNum ++了,如果不先点亮第一个灯的话,上电之后按下按键1一开始是在第二个LED灯亮)
while(1)
{
key = key_scan(0); //0代表单次扫描按键
//按键1控制LED1到LED8方向移动
if(key == KEY1_PRESS) //如果按键K1按下
{
LEDNum++;
if(LEDNum >= 8) //如果移动大于或等于八位
LEDNum = 0; //将移动位置清0
LED_PORT = ~(0x01 << LEDNum); //左移LEDNum位再进行取反 0000 0001 --> 0000 0010(左移一位)
}
//按键2控制LED8到LED1方向移动
if(key == KEY2_PRESS) //如果按键K2按下
{
if(LEDNum == 0) //如果移动位置为0则表示LED1亮
LEDNum = 7; //让移动位置为7表示LED8亮
else
LEDNum--;
LED_PORT = ~(0x01 << LEDNum);
}
}
}

AllHead.h

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

#include <reg52.h>
#include "LED.h"
#include "Delay.h"
#include "key.h"

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

/******************************************************************************
* 函 数 名 : key_scan
* 函数功能 : 检测独立按键是否按下,按下则返回对应键值
* 输 入 : mode=0:单次扫描按键
mode=1:连续扫描按键
* 输 出 : KEY1_PRESS:K1 按下
KEY2_PRESS:K2 按下
KEY3_PRESS:K3 按下
KEY4_PRESS:K4 按下
KEY_UNPRESS:未有按键按下
********************************************************************************/
u8 key_scan(u8 mode)
{
static u8 key = 1; //定义一个静态变量
if(mode) key = 1; //如果mode=1则表示连续扫描按键 如果mode=0则忽略这句话
if(key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0 )) //任意按键按下(&&这个符号表示两个条件必须同时满足才能进入这个if语句)
{
Delay1ms(10); //消抖
key = 0;
if(KEY1 == 0) //如果按键仍然处于被按下状态则表明按键真的被按下了
return KEY1_PRESS;
else if(KEY2 == 0)
return KEY2_PRESS;
else if(KEY3 == 0)
return KEY3_PRESS;
else if(KEY4 == 0)
return KEY4_PRESS;
}
else if(KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1 ) //无按键按下
{
key = 1;
}
return KEY_UNPRESS;
}

Key.h

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

#include "AllHead.h"

//定义4个独立按键控制管脚
sbit KEY1 = P3^1;
sbit KEY2 = P3^0;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;

//使用宏定义独立按键按下的键值(如果按下则返回)
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4

//没按下
#define KEY_UNPRESS 0

u8 key_scan(u8 mode);

#endif

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

u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f, 0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
//定义数组存放0-F段码,gsmg中g代表全局变量

/********************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
*********************************************/
void smg_display(u8 location,u8 number)
{
switch(location) //位置
{
case 0: LSC = 1;LSB = 1;LSA = 1;break; //Y7 LED8 板子从左边数第一个数码管
case 1: LSC = 1;LSB = 1;LSA = 0;break; //Y6 LED7 板子从左边数第二个数码管
case 2: LSC = 1;LSB = 0;LSA = 1;break; //Y5 LED6 板子从左边数第三个数码管
case 3: LSC = 1;LSB = 0;LSA = 0;break; //Y4 LED5 板子从左边数第四个数码管
case 4: LSC = 0;LSB = 1;LSA = 1;break; //Y3 LED4 板子从左边数第五个数码管
case 5: LSC = 0;LSB = 1;LSA = 0;break; //Y2 LED3 板子从左边数第六个数码管
case 6: LSC = 0;LSB = 0;LSA = 1;break; //Y1 LED2 板子从左边数第七个数码管
case 7: LSC = 0;LSB = 0;LSA = 0;break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = gsmg_code[number]; //传输段选数据
Delay1ms(1); //延时1毫秒左右,等待显示稳定
SMG_A_DP_PORT = 0x00; //消影
}

smg.h

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

#include "AllHead.h"

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

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display(u8 location,u8 number);

extern u8 gsmg_code[17];

#endif

BEEP.c

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

void BEEP_Time(u8 ms) //参数传入数值多少毫秒
{
u8 i = 0;
for(i = 0;i < ms; i++)
{
BEEP = !BEEP; //产生一定频率的脉冲信号
Delay1ms(1);
}
}

BEEP.h

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

#include "AllHead.h"

sbit BEEP = P2^5; //将P2.5管脚定义为BEEP

void BEEP_Time(u8 ms);

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

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

u8 key = 0;
void main( )
{
smg_display(0,0); //上机后在一个数码管显示0
while(1)
{
key = key_scan(0); //0代表单次扫描按键
if(key)
{
BEEP_Time(500);
smg_display(0,key); //在第一个数码管显示按键的值
}
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "BEEP.h"
#include "smg.h"
#include "key.h"

#endif

矩阵按键

矩阵按键介绍

在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式 采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态。

矩阵按键原理图

矩阵按键的实现

行列描述法

先让一列为 低电平,其余几列全为高电平(此时我们确定了列数),然后立即轮流检测一次各行是否有低电平, 若检测到某一行为低电平(这时我们又确定了行数)则我们便可确认当前被按下的键是哪一行哪一列的,用同样方法轮流送各列一次低电平,再轮流检测一次各行是否变为 低电平 ,这样即可检测完所有的按键,当有键被按下时便可判断出按下的键是哪一个键。当然我们也可以将行线置低电平,扫描列是否有低电平。从而达到整个键盘的检测。

线翻转法

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

矩阵按键程序

行列扫描法_按下矩阵按键S1-S16 键,数码管显示 0-F

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

/***************************************************************************************
* 函 数 名 : key_matrix_ranks_scan
* 函数功能 : 使用行列式扫描方法,检测矩阵按键是否按下,按下则返回对应键值
* 输 入 : 无
* 输 出 : key_value:1-16,对应 S1-S16 键,
0:按键未按下
****************************************************************************************/
u8 key_matrix_ranks_scan(void) //行列式扫描函数
{
u8 key_value = 0;
KEY_MATRIX_PORT = 0xf7; //给第一列赋值0,其余全为1 1111 0111 --> 0xf7
if(KEY_MATRIX_PORT != 0xf7) //判断第一列按键是否按下(若按下则矩阵按键控制管脚不等于0xf7)
{
Delay1ms(10); //消抖
switch(KEY_MATRIX_PORT) //保存第一列按键按下后的键值
{
case 0x77: key_value = 1; break; //S1
case 0xb7: key_value = 5; break; //S5
case 0xd7: key_value = 9; break; //S9
case 0xe7: key_value = 13; break; //S13
}
}
while(KEY_MATRIX_PORT != 0xf7); //等待按键松开(松开矩阵按键控制管脚等于0xf7则退出循环)

KEY_MATRIX_PORT = 0xfb; //给第二列赋值0,其余全为1 1111 1011 --> 0xfb
if(KEY_MATRIX_PORT != 0xfb) //判断第二列按键是否按下(若按下则矩阵按键控制管脚不等于0xfb)
{
Delay1ms(10); //消抖
switch(KEY_MATRIX_PORT) //保存第二列按键按下后的键值
{
case 0x7b: key_value = 2; break; //S2
case 0xbb: key_value = 6; break; //S6
case 0xdb: key_value = 10; break; //S10
case 0xeb: key_value = 14; break; //S14
}
}
while(KEY_MATRIX_PORT != 0xfb); //等待按键松开(松开矩阵按键控制管脚等于0xfb则退出循环)

KEY_MATRIX_PORT = 0xfd; //给第三列赋值0,其余全为1 1111 1101 --> 0xfd
if(KEY_MATRIX_PORT != 0xfd) //判断第三列按键是否按下(若按下则矩阵按键控制管脚不等于0xfd)
{
Delay1ms(10); //消抖
switch(KEY_MATRIX_PORT) //保存第三列按键按下后的键值
{
case 0x7d: key_value = 3; break; //S3
case 0xbd: key_value = 7; break; //S7
case 0xdd: key_value = 11; break; //S11
case 0xed: key_value = 15; break; //S15
}
}
while(KEY_MATRIX_PORT != 0xfd); //等待按键松开(松开矩阵按键控制管脚等于0xfd则退出循环)

KEY_MATRIX_PORT = 0xfe; //给第四列赋值0,其余全为1 1111 1110 --> 0xfe
if(KEY_MATRIX_PORT != 0xfe) //判断第四列按键是否按下(若按下则矩阵按键控制管脚不等于0xfe)
{
Delay1ms(10); //消抖
switch(KEY_MATRIX_PORT) //保存第四列按键按下后的键值
{
case 0x7e: key_value = 4; break; //S4
case 0xbe: key_value = 8; break; //S8
case 0xde: key_value = 12; break; //S12
case 0xee: key_value = 16; break; //S16
}
}
while(KEY_MATRIX_PORT != 0xfe); //等待按键松开(松开矩阵按键控制管脚等于0xfe则退出循环)

return key_value;
}

Key.h

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

#include "AllHead.h"

#define KEY_MATRIX_PORT P1 //使用宏定义矩阵按键控制管脚

u8 key_matrix_ranks_scan(void);

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

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

void main( )
{
u8 key = 0;
u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
//定义数组存放0-F段码,gsmg中g代表全局变量
while(1)
{
key = key_matrix_ranks_scan(); //接收键值
if(key != 0) //要确保key不能为0
SMG_A_DP_PORT = gsmg_code[key - 1]; //数码管显示(得到的按键值减 1 换算成数组下标对应 0-F 段码)
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "key.h"

#endif
线翻转扫描法_按下矩阵按键S1-S16 键,数码管显示 0-F

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

/*******************************************************************************
* 函 数 名 : key_matrix_flip_scan
* 函数功能 : 使用线翻转扫描方法,检测矩阵按键是否按下,按下则返回对应键值
* 输 入 : 无
* 输 出 : key_value:1-16,对应 S1-S16 键,
0:按键未按下
********************************************************************************/
u8 key_matrix_flip_scan(void)
{
static u8 key_value = 0; //键值
KEY_MATRIX_PORT = 0x0f; //行为低电平,测试列 给所有行赋值0,列全为1 0000 1111 --> 0x0f
if(KEY_MATRIX_PORT != 0x0f) //判断按键是否按下(若按下则矩阵按键控制管脚不等于0x0f)
{
Delay1ms(10); //消抖
if(KEY_MATRIX_PORT != 0x0f)
{
//测试列
KEY_MATRIX_PORT = 0x0f; //行为低电平,测试列 给所有行赋值0,列全为1 0000 1111 --> 0x0f
switch(KEY_MATRIX_PORT) //保存行为0,按键按下后的列值
{
case 0x07: key_value = 1; break; //第一列 0000 0111 --> 0x07
case 0x0b: key_value = 2; break; //第二列 0000 1011 --> 0x0b
case 0x0d: key_value = 3; break; //第三列 0000 1101 --> 0x0d
case 0x0e: key_value = 4; break; //第四列 0000 1110 --> 0x0e
}
//测试行
KEY_MATRIX_PORT = 0xf0; //列为低电平,测试行 给所有行赋值1,列全为0 1111 0000 --> 0xf0
switch(KEY_MATRIX_PORT) //保存列为0,按键按下后的键值
{
case 0x70: key_value = key_value; break; //第一行 0111 0000 --> 0x70
case 0xb0: key_value = key_value + 4; break; //第二行 1011 0000 --> 0xb0
case 0xd0: key_value = key_value + 8; break; //第三行 1101 0000 --> 0xd0
case 0xe0: key_value = key_value + 12; break; //第四行 1110 0000 --> 0xe0
}
while(KEY_MATRIX_PORT != 0xf0); //等待按键松开(松开矩阵按键控制管脚等于0xf0则退出循环)
}
}
else
key_value = 0;
return key_value;
}

Key.h

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

#include "AllHead.h"

#define KEY_MATRIX_PORT P1 //使用宏定义矩阵按键控制管脚

u8 key_matrix_flip_scan(void);

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

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

void main( )
{
u8 key = 0;
u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; //定义数组存放0-F段码,gsmg中g代表全局变量
while(1)
{
key = key_matrix_flip_scan(); //接收键值
if(key != 0) //要确保key不能为0
SMG_A_DP_PORT = gsmg_code[key - 1]; //数码管显示(得到的按键值减 1 换算成数组下标对应 0-F 段码)
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "key.h"

#endif

LED点阵

LED点阵介绍

LED 点阵是由发光二极管排列组成的显示器件

8*8 点阵共由 64 个发光二极管组成,且每个发光二极管是放置在行线和列线的交叉点上,当对应的某一行置 1 电平,某一列置 0 电平,则相应的二极管就亮; 如要将第一个点点亮,则 1 脚接高电平 a 脚接低电平,则第一个点就亮了;如果 要将第一行点亮,则第 1 脚要接高电平,而(a、b、c、d、e、f、g、h )这些引脚接低电平,那么第一行就会点亮;如要将第一列点亮,则第 a 脚接低电平, 而(1、2、3、4、5、6、7、8)接高电平,那么第一列就会点亮。

LED点阵原理图

74HC595 芯片介绍

74HC595 是一个 8 位串行输入、并行输出的位移缓存器,其中并行输出为三态输出(即高电平、低电平和高阻抗)芯片管脚及功能说明如下:

芯片管脚功能

74HC595 是具有 8 位移位寄存器和一个存储器,三态输出功能。移位寄存器和存储器是单独的时钟。数据在 SCK 的上升沿输入,在 RCK 的上升沿进入到存储器中。如果两个时钟连在一起,则移位寄存器总是比存储器早一个脉冲。移位寄存器有一个串行输入(DS),和一个串行输出(Q7 非),和一个异步的低电平复位,存储寄存器有一个并行 8 位的,具有三态的总线输出,当 MR 为高电平,OE为低电平时,数据在 SHCP 上升沿进入移位寄存器,在 STCP 上升沿输出到并行端口

注意:74HC595是 先传输字节的高位后传输低位, 所以需要 将字节低位移动到高位传输,在传输数据时,要注意 移位寄存器时钟和存储寄存器时钟的先后顺序,将要写入的数据先传输到74HC595寄存器中,即在准备好每位数据时要将 SRCLK进行一个上升沿变化,此时即可将数据传输到寄存器内, 待 循环8次即一个字节传输到寄存器中时,就可以来一个存储时钟上升沿,此时就可以将74HC595寄存器中的数据全部一次传输到595端口输出。【要注意清除寄存器缓存的数据】

取字模软件

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

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

然后可以在这个 8x8 白色格子里面点击,点击后即会在对应位置出现一个黑点,表示在 LED 点阵对应位置的 LED 灯点亮,未点击位置(白色)表示 LED 点阵对应位置的 LED 灯熄灭。 比如在 8x8LED 点阵上显示数字 0,那么可以在图中 8x8 白色框内通过点 击对应位置描出一个数字 0 的外形,如下所示:

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

然后点击“取模方式”,选择 C51 格式选项,然后在点阵生成区自动会 生成数字字符对应的数据
(如果是使用汇编编程,那么汇编对应的汉字数据可选择 A51 )

到这里我们就将数字0的数据生成了,然后将生成的数据复制到我们程序定义的数组中

LED点阵实验

IO 扩展(串转并)实验-74HC595

实验现象:下载程序后,8*8LED 点阵以一行循环滚动显示
注意事项:LED 点阵旁的 J24 黄色跳线帽短接到 GND 一端

从上图中可以看出,74HC595 需要用到的控制管脚SER、RCLK、SRCLK直接连接到 51 单片机的 P3.4-P3.6 IO 口上,输出端则是直接连接到 LED 点阵模块的行端口上,即为 LED 发光二极管的阳极,LED点阵的列则为发光二极管的阴极。 要想控制 LED 点阵,可以将单片机管脚按照 74HC595 芯片的通信时序要求来传输数据,这样即可控制 LED 点阵的行数据。根据 LED 发光二极管导通原理,当阳极为高电平,阴极为低电平则点亮,否则熄灭。因此通过单片机 P0 口可控制点阵列,74HC595 可控制点阵行。

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

/*******************************************************************************
* 函 数 名 : hc595_write_data(u8 dat)
* 函数功能 : 向 74HC595 写入一个字节的数据
* 输 入 : dat:数据
* 输 出 : 无
********************************************************************************/
void hc595_write_data(u8 dat)
{
u8 i = 0;
for(i = 0; i < 8; i++) //循环8次即可将一个字节写入移位寄存器中(该寄存器先传输高位)
{
SER = dat >> 7; //优先传输一个字节中的高位 1000 0000 --> 0000 0001 (将高位移到最低位)
dat <<= 1; //将低位移动到高位(将次高位移到最高位)
SRCLK = 0;
delay_10us(1);
SRCLK = 1; //移位寄存器时钟上升沿将端口数据送入寄存器中 (上升沿:先低位后高位 先=0,后=1)
delay_10us(1);
}
rCLK = 0;
delay_10us(1);
rCLK = 1; //存储寄存器时钟上升沿将前面写入到寄存器的数据输出 (上升沿:先低位后高位 先=0,后=1)
}

Hc595.h

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

#include "AllHead.h"

//定义74HC595控制管脚
sbit SER = P3^4; //串行数据输入
sbit rCLK = P3^5; //存储寄存器时钟输入
sbit SRCLK = P3^6; //移位寄存器时钟输入

#define LEDDZ_COL_PORT P0 //点阵列控制端口

void hc595_write_data(u8 dat);

#endif

Delay.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
#include "AllHead.h"
/* -------------------------------- begin ------------------------------ */
/**
* @函数名: delay_us
* @参数1 : 需要延时多少微秒
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */
void delay_10us(u16 ten_us) //当传入ten_us==1时,大约延时10us
{
while(ten_us--);
}

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

Delay.h

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

#include "AllHead.h"

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

void delay_10us(u16 ten_us);
void Delay1ms(unsigned int xms);

#endif

main.c

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

void main( )
{
u8 i = 0;
u8 ghc595_buf[8] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
LEDDZ_COL_PORT = 0x00; //将LED点阵列全部设置为0,即LED阴极为低电平
while(1)
{
for(i = 0;i < 8;i++)
{
hc595_write_data(0x00); //消除前面寄存器缓存数据
hc595_write_data(ghc595_buf[i]); //写入新的数据
Delay1ms(500); //延时500ms
}
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "hc595.h"

#endif

点亮一个点

实验现象:下载程序后,8*8LED 点阵点亮左上角第一个点
注意事项:LED 点阵旁的 J24 黄色跳线帽短接到 GND 一端

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

/*******************************************************************************
* 函 数 名 : hc595_write_data(u8 dat)
* 函数功能 : 向 74HC595 写入一个字节的数据
* 输 入 : dat:数据
* 输 出 : 无
********************************************************************************/
void hc595_write_data(u8 dat)
{
u8 i = 0;
for(i = 0; i < 8; i++) //循环8次即可将一个字节写入移位寄存器中(该寄存器先传输高位)
{
SER = dat >> 7; //优先传输一个字节中的高位 1000 0000 --> 0000 0001 (将高位移到最低位)
dat <<= 1; //将低位移动到高位(将次高位移到最高位)
SRCLK = 0;
delay_10us(1);
SRCLK = 1; //移位寄存器时钟上升沿将端口数据送入寄存器中 (上升沿:先低位后高位 先=0,后=1)
delay_10us(1);
}
rCLK = 0;
delay_10us(1);
rCLK = 1; //存储寄存器时钟上升沿将前面写入到寄存器的数据输出 (上升沿:先低位后高位 先=0,后=1)
}

Hc595.h

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

#include "AllHead.h"

//定义74HC595控制管脚
sbit SER = P3^4; //串行数据输入
sbit rCLK = P3^5; //存储寄存器时钟输入
sbit SRCLK = P3^6; //移位寄存器时钟输入

#define LEDDZ_COL_PORT P0 //点阵列控制端口

void hc595_write_data(u8 dat);

#endif

Delay.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
#include "AllHead.h"
/* -------------------------------- begin ------------------------------ */
/**
* @函数名: delay_us
* @参数1 : 需要延时多少微秒
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */
void delay_10us(u16 ten_us) //当传入ten_us==1时,大约延时10us
{
while(ten_us--);
}

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

Delay.h

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

#include "AllHead.h"

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

void delay_10us(u16 ten_us);
void Delay1ms(unsigned int xms);

#endif

main.c

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

void main( )
{
LEDDZ_COL_PORT = 0x7f; ;//将LED点阵左边第一列设置为0,即LED阴极为低电平,其余列为1,即高电平
// 0111 1111 --> 0x7f
while(1)
{
hc595_write_data(0x80);//将LED点阵上边第一行设置为1,即LED阳极为高电平,其余行为0,即低电平
// 1000 0000 --> 0x80
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "hc595.h"

#endif

LED点阵显示图像“0”

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

/*******************************************************************************
* 函 数 名 : hc595_write_data(u8 dat)
* 函数功能 : 向 74HC595 写入一个字节的数据
* 输 入 : dat:数据
* 输 出 : 无
********************************************************************************/
void hc595_write_data(u8 dat)
{
u8 i = 0;
for(i = 0; i < 8; i++) //循环8次即可将一个字节写入移位寄存器中(该寄存器先传输高位)
{
SER = dat >> 7; //优先传输一个字节中的高位 1000 0000 --> 0000 0001 (将高位移到最低位)
dat <<= 1; //将低位移动到高位(将次高位移到最高位)
SRCLK = 0;
delay_10us(1);
SRCLK = 1; //移位寄存器时钟上升沿将端口数据送入寄存器中 (上升沿:先低位后高位 先=0,后=1)
delay_10us(1);
}
rCLK = 0;
delay_10us(1);
rCLK = 1; //存储寄存器时钟上升沿将前面写入到寄存器的数据输出 (上升沿:先低位后高位 先=0,后=1)
}

Hc595.h

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

#include "AllHead.h"

//定义74HC595控制管脚
sbit SER = P3^4; //串行数据输入
sbit rCLK = P3^5; //存储寄存器时钟输入
sbit SRCLK = P3^6; //移位寄存器时钟输入

#define LEDDZ_COL_PORT P0 //点阵列控制端口

void hc595_write_data(u8 dat);

#endif

Delay.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
#include "AllHead.h"
/* -------------------------------- begin ------------------------------ */
/**
* @函数名: delay_us
* @参数1 : 需要延时多少微秒
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */
void delay_10us(u16 ten_us) //当传入ten_us==1时,大约延时10us
{
while(ten_us--);
}

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

Delay.h

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

#include "AllHead.h"

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

void delay_10us(u16 ten_us);
void Delay1ms(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
#include "AllHead.h"

void main( )
{
u8 i = 0;
u8 gled_row[8] = {0x00,0x7C,0x82,0x82,0x82,0x7C,0x00,0x00}; //LED点阵显示数字0的行数据(使用取字模软件)
u8 gled_col[8] = {0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe}; //LED点阵显示数字0的列数据
//0111 1111 --> 0x7f 1011 1111 --> 0xbf 1101 1111 --> 0xdf 1110 1111 --> 0xef
while(1)
{
for(i = 0;i < 8;i++) //循环8次扫描8行、列
{
LEDDZ_COL_PORT = gled_col[i]; //传送列选数据
hc595_write_data(gled_row[i]); //传送行选数据
Delay1ms(1); //延时一段时间,等待显示稳定
hc595_write_data(0x00); //消影
}
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "hc595.h"

#endif

如果想要显示其他图案或者数字直接在取模软件生成行的数据复制就可以了

LED点阵显示动画

当我们了解了LED点阵屏显示图形后,就可以尝试用点阵屏来显示动画了,我们可以定义一个数组,把要显示的图形的数据存到数组里,然后逐个移动数据显示就变成了动画了

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

/*******************************************************************************
* 函 数 名 : hc595_write_data(u8 dat)
* 函数功能 : 向 74HC595 写入一个字节的数据
* 输 入 : dat:数据
* 输 出 : 无
********************************************************************************/
void hc595_write_data(u8 dat)
{
u8 i = 0;
for(i = 0; i < 8; i++) //循环8次即可将一个字节写入移位寄存器中(该寄存器先传输高位)
{
SER = dat >> 7; //优先传输一个字节中的高位 1000 0000 --> 0000 0001 (将高位移到最低位)
dat <<= 1; //将低位移动到高位(将次高位移到最高位)
SRCLK = 0;
delay_10us(1);
SRCLK = 1; //移位寄存器时钟上升沿将端口数据送入寄存器中 (上升沿:先低位后高位 先=0,后=1)
delay_10us(1);
}
rCLK = 0;
delay_10us(1);
rCLK = 1; //存储寄存器时钟上升沿将前面写入到寄存器的数据输出 (上升沿:先低位后高位 先=0,后=1)
}

//LED点阵显示函数 参数一:LED点阵列数 参数二:传输数据
void MatrixLED_ShowCol(u8 col,u8 dat)
{
hc595_write_data(dat);
LEDDZ_COL_PORT = ~(0x80 >> col); //LED点阵第一列向右移动 1000 0000 按位取反--> 0111 1111
Delay1ms(1);
LEDDZ_COL_PORT = 0xff; //消影
}

Hc595.h

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

#include "AllHead.h"

//定义74HC595控制管脚
sbit SER = P3^4; //串行数据输入
sbit rCLK = P3^5; //存储寄存器时钟输入
sbit SRCLK = P3^6; //移位寄存器时钟输入

#define LEDDZ_COL_PORT P0 //点阵列控制端口

void hc595_write_data(u8 dat);
void MatrixLED_ShowCol(u8 col,u8 dat);

#endif

Delay.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
#include "AllHead.h"
/* -------------------------------- begin ------------------------------ */
/**
* @函数名: delay_us
* @参数1 : 需要延时多少微秒
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */
void delay_10us(u16 ten_us) //当传入ten_us==1时,大约延时10us
{
while(ten_us--);
}

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

Delay.h

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

#include "AllHead.h"

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

void delay_10us(u16 ten_us);
void Delay1ms(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
#include "AllHead.h"

void main( )
{
u8 i = 0;
u8 offset = 0; //偏移量Offset
u8 count = 0;
u8 gled_row[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x7F,0x08,0x08,0x08,0x7F,0x00,0x1C,
0x2A,0x2A,0x2A,0x18,0x00,0x00,0x7E,0x01,
0x02,0x00,0x7E,0x01,0x02,0x00,0x1E,0x21,
0x21,0x1E,0x00,0x00,0x7D,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,}; //LED点阵显示动画的行数据(使用取字模软件)
while(1)
{
for(i = 0;i < 8;i++)
{
MatrixLED_ShowCol(i,gled_row[i + offset]); //gled_row[i + offset] 为移动数据
}
count++;
if(count > 10) //计次延时,不要用Delay,最好用定时器
{
count = 0;
offset++;
if(offset > 40) //当超过数组的范围时偏移量清零
offset = 0;
}
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "hc595.h"

#endif

注意:做 LED 点阵实验时,一定要将 LED 点阵旁的 J24 黄色跳线帽短接到 GND 一端。如下所示:

16*16点阵

4片74HC595芯片,数据传入是 第一个字节最终传到第4片,第二个字节最终传到第3片,第三个字节最终传到第2片,第四个字节最终传到第1片上。(因为第一片8位满了后再传入的就会把前面的挤出去)

直流电机

直流电机介绍

直流电机是指 能将直流电能转换成机械能(直流电动机)或将机械能转换成直流电能(直流发电机的旋转电机)。它是能实现直流电能和机械能互相转换的电机。当它作电动机运行时是直流电动机,将电能转换为机械能;作发电机运行时是直流发电机,将机械能转换为电能。

直流电机的结构应由 定子和转子 两大部分组成。直流电机运行时静止不动的部分称为定子;运行时转动的部分称为转子。 直流电机没有正负之分 ,在两端加上直流电就能工作。需要知道直流电机的额定电压和额定功率,不能使之长时间超负荷运作。在交换接线后,可以形成正反转。

其内部相当于非门电路,即输入高输出为低,输入为低输出是高

外观实物图如下:

其内部结构如下图所示:

ULN2003 芯片介绍

该芯片是一个单片高电压、高电流的达林顿晶体管阵列集成电路。不仅可以用来驱动直流电机,还可用来驱动五线四相步进电机,比如 28BYJ-48 步进电机。

ULN2003 是一个单片高电压、高电流的达林顿晶体管阵列集成电路。它是由 7 对 NPN 达林顿管组成的,它的高电压输出特性和阴极箝位二极管可以转换感应负载。单个达林顿对的集电极电流是 500mA。达林顿管并联可以承受更大的电流。 此电路主要应用于继电器驱动器,字锤驱动器,灯驱动器,显示驱动器(LED 气体放电),线路驱动器和逻辑缓冲器。ULN2003 的每对达林顿管都有一个 2.7k 串联电阻,可以直接和 TTL 或 5V CMOS 装置。

逻辑框图

从上图可以很容易理解该芯片的使用方法,其内部相当于非门电路,即输入高输出为低,输入为低输出是高,这里要注意:因为 ULN2003 的输出是集电极开路,ULN2003 要输出高电平,必须在输出口外接上拉电阻。这也就能解释在后面连接直流电机时为什么不能直接将 ULN2003 的 2 个输出口接电机线,而必须一根线接电源,另一个才接 ULN2003 输出口。

若使用该芯片驱动直流电机,只可实现单方向控制,电机一端接电源正极, 另一端接芯片的输出口。若想控制五线四相步进电机,则可将四路输出接到步进电机的四相上,电机另一条线接电正极。

直流电机实验

(1)硬件设计

实验现象:下载程序后,直流电机旋转 5秒 后停止

本实验使用到硬件资源如下:
(1)步进电机驱动模块
(2)直流电机

开发板上的步进电机驱动模块电路如下图所示:

从上图可知,ULN2003 的输入口与单片机的 P1.0-P1.3 连接,对应输出则是 OUT1-OUT4,而 J47 则是提供给外部连接电机的接口,可以支持直流电机、五线四相步进电机 28BYJ-48 连接。本实验使用的是直流电机,电机的一根线连接在 VCC 上,另一根连接在 OUT1 上,因此可通过单片机 P1.0输出高电平控制电机旋转输出低电源控制电机停止

注意:单片机 P1.0 输出低电平时,ULN2003 的 OUT1 并不会输出高电平导致停止,而是因为集电极开路,导致电机无电流流入致使停止。

注意:直流电机的两根线要连接在 J47 端子的 O1 和 5V 上。如下所示:

(2)软件设计

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

main.c

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

sbit DC_Motor = P1^0; //定义直流电机控制管脚

#define DC_MOTOR_RUN_TIME 5000 //定义直流电机运行时间为5000ms

void main( )
{
DC_Motor = 1; //开启直流电机
Delay1ms(DC_MOTOR_RUN_TIME);
DC_Motor = 0; //关闭直流电机
while(1)
{

}
}

AllHead.h

1
2
3
4
5
6
7
#ifndef _ALLHEAD_H
#define _ALLHEAD_H

#include <reg52.h>
#include "Delay.h"

#endif

步进电机

步进电机简介

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

下图为混合式步进电机组成图

步进电机工作原理

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

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

步进电机驱动原理

(1)双极性步进电机驱动原理

1
2
3
4
5
6
7
8
A 相通电,B 相不通电 
A、B 相全部通电,且电流相同,产生相同磁性
B 相通电,A 断电
B 相通电,A 相通电,且电流相等,产生相同磁性
A 相通电,B 断电
A、B 相全部通电,且电流相同,产生相同磁性
B 相通电,A 断电
B 相通电,A 相通电,且电流相等,产生相同磁性

其中 1~4 步与 5~8 步的电流方向相反(电流相反,电磁的极性就相反)这样就产生了顺时针旋转,同理逆时针是将通电顺序反过来即可

(2)单极性步进电机驱动原理

单极性与双极性步进电机驱动类似,都可以分为整步与半步的驱动方式,不同的是,双极性的步进电机可以通过改变电流的方向来改变每相的磁场方向,但是单极性的就不可以了,它有一个公共端,这就直接决定了电流方向

图中的通电顺序为:A->AB->B->BC->C->CD->D->DA, 转子每次只走半步 45 度,所以这也被称为半步驱动,与整步相比半步的旋转方式旋转起来更加的顺滑。

(3)细分驱动原理

对于细分驱动的原理,不分单双极性步进电机,下图以单极性为例:

在上图中均为双相激励;其中图(a)为 A 相电流很大,B 相的电流极其微弱,接近 0;图 © 为 A 相和 B 相的电流相同,电流决定磁场,所以说 A 相 和 B 相的磁场也是相同的,(a) 和(c)可以是极限特殊的情况,再看图(b) 和图(d)这两个是由于 A 相和 B 相的电流不同产生位置情况;由此可以得出改变定子的电流比例就可以使得转子在任意角度停住。细分的原理就是:通过改变定子的电流比例,改变转子在一个整步中的不同位置,可以将一个整步分成多个小步来运行 。

在上图中就是一个整步分成了 4 步来跑,从(a)~(d)是 A 相的电流逐渐减小,B 相电流逐渐增大的过程,如果驱动器的细分能力很强,可以将其分成 32 细分、64 细分等;这不仅提高了步进电机旋转的顺畅度而且提高了每步的精度。细分驱动具有转动顺畅、精度高、转矩大的特点,但控制复杂,一般需要专用芯片来实现

步进电机技术指标

(1)静态技术指标

相数:产生不同对极 N、S 磁场的激磁线圈对数,也可以理解为步进电机中线圈的组数,其中两相步进电机步距角为 1.8°,三相的步进电机步距角为 1.5°,相数越多的步进电机,其步距角就越小。

拍数:完成一个磁场周期性变化所需脉冲数或导电状态用 n 表示,或指电机转过一个齿距角所需脉冲数,以四相电机为例,有四相四拍运行方式即 AB-BC-CD-DA-AB,四相八拍运行方式即 A-AB-B-BC-C-CD-D-DA-A。

步距角:一个脉冲信号所对应的电机转动的角度,可以简单理解为一个脉冲信号驱动的角度,电机上都有写,一般 42 步进电机的步距角为 1.8°

定位转矩:电机在不通电状态下,电机转子自身的锁定力矩(由磁场齿形 的谐波以及机械误差造成的)

静转矩:电机在额定静态电压作用下,电机不作旋转运动时,电机转轴的锁定力矩。此力矩是衡量电机体积的标准,与驱动电压及驱动电源等无关

(2)动态技术指标

步距角精度:步进电机转动一个步距角度的理论值与实际值的误差。用百分比表示:误差/步距角 *100%。

失步:电机运转时运转的步数,不等于理论上的步数。也可以叫做丢步, 一般都是因负载太大或者是频率过快。

失调角:转子齿轴线偏移定子齿轴线的角度,电机运转必存在失调角,由失调角产生的误差,采用细分驱动是不能解决的。

最大空载起动频率:在不加负载的情况下,能够直接起动的最大频率。

最大空载的运行频率:电机不带负载的最高转速频率。

运行转矩特性:电机的动态力矩取决于电机运行时的平均电流(而非静态电流),平均电流越大,电机输出力矩越大,即电机的频率特性越硬

电机正反转控制:通过改变通电顺序而改变电机的正反转

28BYJ-48 步进电机简介

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

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

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

28BYJ48步进电机本来为低电平驱动,但是使用的这个开发板是用ULN2003芯片控制步进电机管脚,相当于非门,所以得高电平才能驱动28BYJ48步进电机

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

在上图中 28BYJ48 步进电机主要参数中可以看到有一个减速比:1:64,步进角为 5.625/64 度,如果需要转动一圈,那么需要 360/5.625*644096 个脉冲信号

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

步进电机原理图

按键控制步进电机驱动实验

实验名称:步进电机实验
实验现象:下载程序后,当按下 KEY1 键可调节电电机旋转方向;当按下 KEY2 键,电机加速;当按下 KEY3 键,电机减速

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

/*******************************************************************************
* 函 数 名 : step_motor_28BYJ48_send_pulse
* 函数功能 : 输出一个数据给 ULN2003 从而实现向步进电机发送一个脉冲
* 输 入 : step:指定步进序号,可选值 0~7
dir:方向选择,1:逆时针, 0:顺时针
* 输 出 : 无
*******************************************************************************/
void step_motor_28BYJ48_send_pluse(u8 step, u8 dir)
{
u8 temp = step;
if(dir == 0) //如果为顺时针旋转
temp = 7 - step; //调换节拍信号
switch(temp) //8 个节拍控制:D->DC->C->CB->B->BA->A->DA(高电平驱动)
{ //步进电机为低电平驱动,但是这个开发板用ULN2003芯片控制步进电机管脚,相当于非门,所以得高电平才能驱动步进电机

case 0: IN4_A = 0;IN3_B = 0;IN2_C = 0;IN1_D = 1; break; //D
case 1: IN4_A = 0;IN3_B = 0;IN2_C = 1;IN1_D = 1; break; //DC
case 2: IN4_A = 0;IN3_B = 0;IN2_C = 1;IN1_D = 0; break; //C
case 3: IN4_A = 0;IN3_B = 1;IN2_C = 1;IN1_D = 0; break; //CB
case 4: IN4_A = 0;IN3_B = 1;IN2_C = 0;IN1_D = 0; break; //B
case 5: IN4_A = 1;IN3_B = 1;IN2_C = 0;IN1_D = 0; break; //BA
case 6: IN4_A = 1;IN3_B = 0;IN2_C = 0;IN1_D = 0; break; //A
case 7: IN4_A = 1;IN3_B = 0;IN2_C = 0;IN1_D = 1; break; //DA
}
}

step_motor.h

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

#include "AllHead.h"

//定义 ULN2003 控制步进电机管脚
sbit IN1_D = P1^0;
sbit IN2_C = P1^1;
sbit IN3_B = P1^2;
sbit IN4_A = P1^3;

// 定义步进电机速度(速度范围为1到5),值越小,速度越快(因为时间越短,速度越快)
// 最小不能小于1
#define STEPMOTOR_MAXSPEED 1 //最大速度所用时间
#define STEPMOTOR_MINSPEED 5 //最小速度所用时间

void step_motor_28BYJ48_send_pluse(u8 step, u8 dir);

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

/******************************************************************************
* 函 数 名 : key_scan
* 函数功能 : 检测独立按键是否按下,按下则返回对应键值
* 输 入 : mode=0:单次扫描按键
mode=1:连续扫描按键
* 输 出 : KEY1_PRESS:K1 按下
KEY2_PRESS:K2 按下
KEY3_PRESS:K3 按下
KEY4_PRESS:K4 按下
KEY_UNPRESS:未有按键按下
********************************************************************************/
u8 key_scan(u8 mode)
{
static u8 key = 1; //定义一个静态变量
if(mode) key = 1; //如果mode=1则表示连续扫描按键 如果mode=0则忽略这句话
if(key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0 )) //任意按键按下
{
Delay1ms(10); //消抖
key = 0;
if(KEY1 == 0) //如果按键仍然处于被按下状态则表明按键真的被按下了
return KEY1_PRESS;
else if(KEY2 == 0)
return KEY2_PRESS;
else if(KEY3 == 0)
return KEY3_PRESS;
else if(KEY4 == 0)
return KEY4_PRESS;
}
else if(KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1 ) //无按键按下
{
key = 1;
}
return KEY_UNPRESS;
}

Key.h

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

#include "AllHead.h"

//定义4个独立按键控制管脚
sbit KEY1 = P3^1;
sbit KEY2 = P3^0;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;

//使用宏定义独立按键按下的键值(如果按下则返回)
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4

//没按下
#define KEY_UNPRESS 0

u8 key_scan(u8 mode);

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(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
#include "AllHead.h"

void main( )
{
u8 key = 0;
u8 step = 0;
u8 dir = 0; //默认顺时针方向
u8 speed = STEPMOTOR_MAXSPEED; //默认最大速度旋转
while(1)
{
key = key_scan(0);
if(key == KEY1_PRESS) //按键1换向
dir = !dir;
else if(key == KEY2_PRESS) //按键2加速
{
if(speed > STEPMOTOR_MAXSPEED) //如果该速度所用时间大于最大速度所用时间
speed -= 1; //让速度所用时间-1,使其加速
}
else if(key == KEY3_PRESS) //按键3减速
{
if(speed < STEPMOTOR_MINSPEED) //如果该速度所用时间小于最大速度所用时间
speed += 1; //让速度所用时间+1,使其减速
}
step_motor_28BYJ48_send_pluse(step++,dir);
if(step == 8)
step = 0;
Delay1ms(speed);
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "key.h"
#include "step_motor.h"

#endif

注意:将步进电机红色线对接“步进电机模块” 输出端子 J47 的 5V 上,其它相序依次接入。

中断

中断概念

中断是为使单片机具有对外部或内部随机发生的事件实时处理而设置的, 中断功能的存在,很大程度上提高了单片机处理外部或内部事件的能力。对于单片机来讲,中断是指 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 溢出中断请求标志位。

中断优先级

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

中断号

中断响应条件

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

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

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

外部中断配置

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

主程序中需要有以下代码

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

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

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

程序–外部中断K3控制LED亮灭实验

EXTI.c

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

/* -------------------------------- begin ------------------------------ */
/**
* @函数名: EXTI0_Init
* @功能 : 外部中断0初始化
* @返回值: 无
**/
/* -------------------------------- end -------------------------------- */
void EXTI0_Init(void)
{
IT0 = 1; //跳变沿触发方式(下降沿)
EX0 = 1; //打开 INT0 的中断允许
EA = 1; //打开总中断
}

EXTI.h

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

#include "AllHead.h"

void EXTI0_Init(void);

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(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
#include "AllHead.h"

void main( )
{
EXTI0_Init(); //进入外部中断函数
while(1)
{

}
}

/* -------------------------------- begin ------------------------------ */
/**
* @功能 : 外部中断0服务函数
**/
/* -------------------------------- end -------------------------------- */
void EXTI0() interrupt 0 //外部中断0中断函数
{
Delay1ms(10); //消抖
if(KEY3 == 0) //再次判断 K3 键是否按下
LED1 = !LED1; //LED1状态翻转
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "EXTI.h"

#endif

注意:由于红外接收传感器与 K3 共用 P3.2 口,因因此在做外部中断0实验时, 将红外接收传感器从开发板取下,防止干扰

定时器

CPU 时序

①振荡周期:为单片机提供定时信号的振荡源的周期(晶振周期或外加振荡周期)。

②状态周期:2 个振荡周期为 1 个状态周期,用 S 表示。振荡周期又称 S 周期或时钟周期。

③机器周期: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 计数器的计数值。

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

上图中的 T0 和 T1 引脚对应的是单片机 P3.4 和 P3.5 管脚

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

TMOD 是定时/计数器的工作方式寄存器,确定工作方式和功能
TCON控制寄存器,控制 T0、T1启动停止及设置溢出标志

定时计数器寄存器

工作方式寄存器 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:工作方式设置位。定时/计数器有四种工作方式。

控制寄存器 TCON

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

其格式如下:

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

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

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

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

定时/计数器的工作方式

(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 计数器

原理跟方式 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

定时器配置

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

①对 TMOD 赋值,以确定 T0 和 T1 的工作方式,如果使用定时器 0 即对 T0 配 置,如果使用定时器 1 即对 T1 配置。
②根据所要定时的时间计算初值,并将其写入 TH0、TL0 或 TH1、TL1。
③如果使用中断,则对 EA 赋值,开放定时器中断。
④使 TR0 或 TR1 置位,启动定时/计数器定时或计数

计算定时/计数器初值

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

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

溢出是65536,则用

655361000=64536=FC18H65536-1000=64536=FC18H

所以初值为 THx=0XFC,TLx=0X18 或者使用小工具进行换算不用手动算。

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

小工具

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

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

以定时器 0 为例介绍配置定时器工作方式 1、设定 1ms 初值,开启定时器计数功能以及总中断,如下

1
2
3
4
5
6
7
8
9
void time0_init(void)
{
TMOD|=0X01;//选择为定时器0模式,工作方式1(为了不干扰T1定时器所以用|);如果是定时器1则把0x01改成0x10
TH0=0XFC; //给定时器赋初值,定时 1ms;如果是定时器1则把TH0改成TH1
TL0=0X18; //如果是定时器1则把TL0改成TL1
ET0=1; //打开定时器 0 中断允许
EA=1; //打开总中断
TR0=1; //打开定时器
}

程序–定时器0控制LED1秒闪烁

time.c

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

/* -------------------------------- begin ------------------------------ */
/**
* @函数名: Time0_Init
* @功能 : 定时器0中断配置函数,通过设置 TH 和 TL 即可确定定时时间(使用工具可确定TH 与TL的值)
* @返回值: 无 晶振频率为11.0592
**/
/* -------------------------------- end -------------------------------- */
void Time0_Init(void)
{
TMOD |= 0x01; //选择为定时器0模式,工作方式1(为了不干扰T1定时器所以用|);如果是定时器1则把0x01改成0x10
TH0 = 0xFC; //给定时器赋初值,定时1ms;如果是定时器1则把TH0改成TH1
TL0 = 0X66; //如果是定时器1则把TL0改成TL1
ET0 = 1; //打开定时器0中断允许
EA = 1; //打开总中断
TR0 = 1; //打开定时器
}

time.h

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

#include "AllHead.h"

void Time0_Init(void);

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

void main( )
{
Time0_Init(); //定时器0中断配置
while(1)
{

}
}

/* -------------------------------- begin ------------------------------ */
/**
* @功能 : 定时器0中断服务函数
**/
/* -------------------------------- end -------------------------------- */

void Time0() interrupt 1 //定时器0中断号为1
{
static u16 count = 0; //定义静态变量count
TH0 = 0xFC; //给定时器赋初值,定时1ms
TL0 = 0X66;
count++;
if(count == 1000) //1000ms=1s
{
count = 0; //溢出然后重新赋0
LED1 = !LED1;
}
}

AllHead.h

1
2
3
4
5
6
7
#ifndef _ALLHEAD_H
#define _ALLHEAD_H

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

#endif

串口通信

串口通信相关术语

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

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

(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,那么每个码元可以表示四种状态,即两个二进制比特位,所以码元数是二进制比特位数的一半,这个时候的波特率为比特率的一半。由于很多常见的通信中一个码元都是表示两种状态,所以我们常常直接以 波特率来表示比特率

单片机串口介绍

串口通信简介

串口通信(Serial Communication),是指外设和计算机间通过数据信号线、 地线等按位进行传输数据的一种通信方式,属于串行通信方式。串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。

(1)接口标准

串口通信的接口标准有很多,有 RS-232C、 RS-232、 RS-422A、 RS-485 等。常用的是 RS-232RS-485。RS-232 其实是 RS-232C 的改进,原理是一样的。

RS-232C 接口规定使用 25 针连接器,简称 DB25,连接器的尺寸及每个插 针的排列位置都有明确的定义,如下图所示:

RS-232C 还有一种 9 针的非标准连接器接口,简称 DB9。串口通信使用的大多都是 DB9 接口。DB25 和 DB9 接头有公头和母头之分,其中带针状的接头是公头,而带孔状的接头是母头。9 针串口线的外观图如下图所示:

9 针串口和 25 针串口常用管脚的功能说明如下图所示:

在串口通信中,通常我们只使用 2、3、5 三个管脚,即 TXD、RXD、SGND

RS-232C 是用正负电压来表示逻辑状态;与晶体管-晶体管逻辑集成电路(TTL)以高低电平表示逻辑状态的规定正好相反。而我们 51 单片机使用的就是 TTL 电平,所以要实现 51 单片机与计算机的串口通信,需要进行 TTL 与 RS-232C 电平转换,通常使用的电平转换芯片是 MAX232

在串口通信中通常 PC 机的 DB9 为公头,单片机上使用的串口 DB9 为母头, 通过一根直通串口线进行相连。

串口通信中还需要注意的是,串口数据收发线要交叉连接,计算机的 TXD 要对应单片机的 RXD,计算机的 RXD 要对应单片机的 TXD,并且共 GND,如下图:

(2)通信协议

RS232 的通信协议比较简单,通常遵循 96-N-8-1 格式

“96”表示的是通信波特率为 9600

串口通信中通常使用的是 异步串口通信,即没有时钟线,所以两个设备要通信,必须要保持一致的波特率,当然,波特率常用值还有 4800、 115200 等。

“N”表示的是无校验位

由于串口通信相对更容易受到外部干扰导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有 奇校验(odd)、偶校验(even)、0 校验(space)、1 校验(mark)以及无校验(noparity)

“8”表示的是数据位数为 8 位

当然数据位数还可以为 5、6、7 位长度

“1”表示的是 1 位停止位

串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由 一个逻辑 0的数据位表示,而数据包的停止信号可由0.5、1、1.5 或 2``` 个逻辑 1 的数据位表示,只要双方约定一致即可。

(3)串口内部结构

上图中右边的 TXDRXD 为单片机 IO 口,TXD 对应的是 P3.1 管脚,RXD 对 应的是 P3.0 管脚。SBUF 是存放数据的,读取也是从这里读。

SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器读操作时,读出的是接收寄存器

串口相关寄存器

串口控制寄存器 SCON

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

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,取消此中断申请。

电源控制寄存器 PCON

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

串口工作方式

(1)方式 0

方式 0 时,串行口为同步移位寄存器的输入输出方式。主要用于扩展并行输 入或输出口。数据由 RXD(P3.0)引脚输入或输出,同步移位脉冲由 TXD(P3.1) 引脚输出。发送和接收均为 8 位数据,低位在先,高位在后。波特率固定为 fosc/12

对应的输入输出时序图如下所示:

①方式 0 输出

②方式 0 输入

(2)方式 1

方式 1 是 10 位数据的异步通信口。TXD 为数据发送引脚,RXD 为数据接收引脚,传送一帧数据的格式如下所示。其中 1 位起始位,8 位数据位,1 位停止位。

对应的输入输出时序图如下所示:

①方式 1 输出

②方式 1 输入

用软件置 REN 为 1 时,接收器以所选择波特率的 16 倍速率采样 RXD 引脚电平,检测到 RXD 引脚输入电平发生负跳变时,则说明起始位有效,将其移入输入移位寄存器,并开始接收这一帧信息的其余位。接收过程中,数据从输入移位寄存器右边移入,起始位移至输入移位寄存器最左边时,控制电路进行最后一次移 位。当 RI=0,且 SM2=0(或接收到的停止位为 1)时,将接收到的 9 位数据的前 8 位数据装入接收 SBUF,第 9 位(停止位)进入 RB8,并置 RI=1,向 CPU 请求中断。

(3)方式2和方式3

方式 2 或方式 3 时为 11 位数据的异步通信口。TXD 为数据发送引脚,RXD 为 数据接收引脚。其数据格式如下所示:

对应的输入输出时序图如下所示:

①方式 2、方式 3 输出

发送开始时,先把起始位 0 输出到 TXD 引脚,然后发送移位寄存器的输出位 (D0)到 TXD 引脚。每一个移位脉冲都使输出移位寄存器的各位右移一位,并由 TXD 引脚输出。第一次移位时,停止位“1”移入输出移位寄存器的第 9 位上, 以后每次移位,左边都移入 0。当停止位移至输出位时,左边其余位全为 0,检测电路检测到这一条件时,使控制电路进行最后一次移位,并置 TI=1,向 CPU 请求中断。

②方式 2、方式 3 输入

接收时,数据从右边移入输入移位寄存器,在起始位 0 移到最左边时,控制电路进行最后一次移位。当 RI=0,且 SM2=0(或接收到的第 9 位数据为 1)时, 接收到的数据装入接收缓冲器 SBUF 和 RB8(接收数据的第 9 位),置 RI=1,向 CPU 请求中断。如果条件不满足,则数据丢失,且不置位 RI,继续搜索 RXD 引脚 的负跳变。

串口的使用方法

计算波特率

方式0的波特率=fosc/12\text{方式0的波特率} = fosc/12

方式2的波特率=(2SMOD/64)×fosc\text{方式2的波特率} =(2^{SMOD}/64)\times fosc

方式1的波特率=(2SMOD/32)×(T1 溢出率)\text{方式1的波特率} =(2^{SMOD}/32)\times\text{(T1 溢出率)}

方式3的波特率=(2SMOD/32)×(T1 溢出率)\text{方式3的波特率} =(2^{SMOD}/32)\times\text{(T1 溢出率)}

其中 T1 溢出率 =

fosc/(12×(256(TH1)))fosc /(12×(256 - (TH1)))

也可以用小工具自动生成波特率。在做串口通信实验时, 一定要确认外部晶振是否是 11.0592M。因为当使用12M 晶振时,波特率误差有 6.98%,会导致通信过程中出现乱码等错误信息

串口初始化步骤

如何使用串口,大家可以按照以下几个步骤配置

①确定 T1 的工作方式(TMOD 寄存器);
②确定串口工作方式(SCON 寄存器);
③计算 T1 的初值(设定波特率),装载 TH1、TL1;
④启动 T1(TCON 中的 TR1 位);
⑤如果使用中断,需开启串口中断控制位(IE 寄存器)。

例如:设置串口为工作方式 1、波特率为 9600、波特率加倍、使用中断。其配置程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*******************************************************************************
* 函 数 名 : uart_init
* 函数功能 : 串口通信中断配置函数,通过设置 TH 和 TL 即可确定定时时间
* 输 入 : baud:波特率对应的 TH、TL 装载值
* 输 出 : 无
*******************************************************************************/
void uart_init(u8 baud)
{
TMOD|= 0X20; //设置计数器工作方式 2
SCON = 0X50; //设置为工作方式 1
PCON = 0X80; //波特率加倍
TH1 = baud; //计数器初始值设置
TL1 = baud;
ES = 1; //打开接收中断
EA = 1; //打开总中断
TR1 = 1; //打开计数器
}
//在主函数中调用该函数并传入0XFA 值即可
uart_init(0XFA);//波特率为 9600

串口通信原理图

串口通信实验

注意事项:使用黄色跳线帽将 CH340 旁的 P5 端子的 UTX 和 P30 短接,URX 和 P31 短接,出厂默认已短接好

串口助手发送数据给单片机,单片机原封不动转发给串口助手

Uart.c

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

/*******************************************************************************
* 函 数 名 : uart_init
* 函数功能 : 串口通信中断配置函数,通过设置 TH 和 TL 即可确定定时时间
* 输 入 : baud:波特率对应的 TH、TL 装载值
* 输 出 : 无
*******************************************************************************/
void UART_Init(u8 baud)
{
TMOD |= 0X20; //设置计数器工作方式 2 0010 0000 --> 0x20
SCON = 0X50; //设置为工作方式 1 0101 0000 --> 0x50
PCON = 0X80; //波特率加倍 1000 0000 --> 0x80
TH1 = baud; //计数器初始值设置(使用小工具)
TH1 = baud;
ES = 1; //打开接收中断
EA = 1; //打开总中断
TR1 = 1; //打开计数器
}

Uart.h

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

#include "AllHead.h"

void UART_Init(u8 baud);

#endif

main.c

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

void main( )
{
UART_Init(0xFA); //波特率为 9600(使用串口小工具计算)
while(1)
{

}
}

void uart() interrupt 4 //串口通信中断服务函数
{
u8 rec_data;
RI = 0; //串行接收停止位的中间时,内部硬件会将RI置1,要清除接收中断标志位,需要清0,取消此中断申请
rec_data = SBUF; //存储接收到的数据
SBUF = rec_data; //将接收到的数据放入到发送寄存器
while(!TI); //等待发送数据完成(发送完成 TI 会自动置 1,!1为 0 则跳出循环)
TI = 0; //串行发送停止位的开始时,内部硬件会将TI置 1,要清除发送完成标志位,需要清0,取消此中断申请
}

AllHead.h

1
2
3
4
5
6
7
#ifndef _ALLHEAD_H
#define _ALLHEAD_H

#include <reg52.h>
#include "uart.h"

#endif

串口控制led灯

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

/*******************************************************************************
* 函 数 名 : uart_init
* 函数功能 : 串口通信中断配置函数,通过设置 TH 和 TL 即可确定定时时间
* 输 入 : baud:波特率对应的 TH、TL 装载值
* 输 出 : 无
*******************************************************************************/
void UART_Init(u8 baud)
{
TMOD |= 0X20; //设置计数器工作方式 2 0010 0000 --> 0x20
SCON = 0X50; //设置为工作方式 1 0101 0000 --> 0x50
PCON = 0X80; //波特率加倍 1000 0000 --> 0x80
TH1 = baud; //计数器初始值设置(使用小工具)
TH1 = baud;
ES = 1; //打开接收中断
EA = 1; //打开总中断
TR1 = 1; //打开计数器
}

void UART_SendByte(u8 Byte) //写数据到SBUF
{
SBUF = Byte; //将数据写入串口数据缓存寄存器
while(!TI); //等待发送数据完成 TI=1即为发送完成 (发送完成 TI 会自动置1,!1为 0 则跳出循环)
TI = 0; //串行发送停止位的开始时,内部硬件会将TI置1,要清除发送完成标志位,需要清0,取消此中断申请
}

Uart.h

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

#include "AllHead.h"

void UART_Init(u8 baud);
void UART_SendByte(u8 Byte);

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

#define LED_PORT P2 //宏定义P2端口
void main( )
{
UART_Init(0xFA); //波特率为 9600(使用串口小工具计算)
while(1)
{

}
}

void uart() interrupt 4 //串口通信中断服务函数 (接收数据需要使用中断)
{
if(RI == 1) //如果接收标志位为1,接收到了数据
{
//当接收到数据的时候,让控制led的P2端口等于串口数据缓冲寄存器中的值,从而实现串口控制led灯
LED_PORT = ~SBUF; //因为低电平 0 让灯亮,所以要取反
UART_SendByte(SBUF); //将收到的数据发回串口
RI = 0; //串行接收停止位的中间时,内部硬件会将RI置1,要清除接收中断标志位,需要清0,取消此中断申请
}
}

AllHead.h

1
2
3
4
5
6
7
#ifndef _ALLHEAD_H
#define _ALLHEAD_H

#include <reg52.h>
#include "uart.h"

#endif

PC端发送数据

现象

I2C-EEPROM

存储器介绍

I2C介绍

I2C是微电子通信控制领域广泛采用的一种总线标准。它是同步通信的一种特殊形式,具有接口线少,控制方式简单, 器件封装形式小,通信速率较高等优点。

I2C 总线只有两根双向信号线。一根是数据线 SDA,另一根是时钟线 SCL。由于其管脚少,硬件实现简单,可扩展性强等特点,因此被广泛的使用在各大集成芯片内。

I2C 通信设备常用的连接方式如下图所示:

它的物理层有如下特点:

(1)它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在 一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。

(2)一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA),一 条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。

(3)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。

(4)总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。

(5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。

(6)具有三种传输模式:标准模式传输速率为 100kbit/s,快速模式为 400kbit/s,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。

(7)连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。

I2C 总线常用的一些术语

主机:启动数据传送并产生时钟信号的设备;

从机:被主机寻址的器件;

多主机:同时有多于一个主机尝试控制总线但不破坏传输;

主模式:用 I2CNDAT 支持自动字节计数的模式; 位 I2CRM,I2CSTT,I2CSTP 控制数据的接收和发送;

从模式:发送和接收操作都是由 I2C 模块自动控制的;

仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并使传输不被破坏的过程;

同步:两个或多个器件同步时钟信号的过程;

发送器:发送数据到总线的器件;

接收器:从总线接收数据的器件。

I2C 协议

I2C 的协议定义了通信的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。

(1)数据有效性规定

I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。每次数据传输都以字节为单位,每次传输的字节数不受限制。如下图:

(2)起始和停止信号

SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号SCL 线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号。起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。如下图:

起始条件:SCL高电平期间,SDA从高电平切换到低电平
终止条件:SCL高电平期间,SDA从低电平切换到高电平

(3)应答响应

每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位,这个校验位是接收端通过控制 SDA(数据线)来实现的,以提醒发送端数据我这边已经接收完成,数据传送可以继续进行。这个校验位其实就是数据或地址传输过程中的响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号即特定的低电平脉冲, 发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号即特定的高电平脉冲,发送方接收到该信号后会产生一个停止信号,结束信号传输。应答响应时序图如下:

每一个字节必须保证 8 位长度。数据传送时,先传送最高位(MSB),每 一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)。这些信号中,起始信号是必需的,结束信号和应答信号都可以不要

发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

(4)总线的寻址方式

I2C 总线寻址按照从机地址位数可分为两种,一种是 7 位,另一种是 10 位。采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如下:

D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“ 0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的 7 位寻址位有 4 位是固定位,3 位是可编程位,这时仅能寻址 8 个同样的器件,即可以有 8 个同样的器件接入到该 I2C 总线系统中。

(5)数据传输

I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。在起始信号后必须传送一个从机的地址(7 位),第 8 位是数据的传送方向位(R/W),用“ 0”表示主机发送(写)数据(W),“ 1”表示主机接收数据(R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。

AT24C02的固定地址为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)。

AT24C02 介绍(EEPROM)

AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息
存储介质:E2PROM
通讯接口:I2C
总线容量:256字节

AT24C01/02/04/08/16…是一个 1K/2K/4K/8K/16K 位串行 CMOS,内部含有128/256/512/1024/2048 个 8 位字节,AT24C01 有一个 8 字节页写缓冲器AT24C02/04/08/16 有一个 16 字节页写缓冲器。该器件通过 I2C 总线接口进行操作,它有一个专门的写保护功能。我们开发板上使用的是 AT24C02(EEPROM) 芯片,此芯片具有 I2C 通信接口,芯片内保存的数据在掉电情况下都不丢失, 所以通常用于存放一些比较重要的数据等。AT24C02 芯片管脚及外观图如下图所示:

芯片管脚说明如下图所示:

AT24C02 器件地址为7 位高 4 位固定为 1010,低 3 位由 A0/A1/A2 信号线的电平决定。 因为传输地址或数据是以字节为单位传送的,当传送地址时, 器件地址占 7 位,还有最后一位(最低位 R/W)用来选择读写方向,它与地址无关。其格式如下:

开发板已经将芯片的 A0/A1/A2 连接到 GND,所以器件地址为 1010000,即 0x50(未计算最低位)。如果要对芯片进行写操作时,R/W 即为 0, 写器件地址即为 0XA0;如果要对芯片进行读操作时,R/W 即为 1,此时读器件地址为 0XA1

I2C 总线时序如下图所示:

EEPROM原理图

I2C-EEPROM实验

下载程序后,数码管右 4 位显示 0,按 K1 键将将数据写入到 EEPROM 内保存,按 K2 键读取 EEPROM 内保存的数据,按 K3 键显示数据加 1,按 K4 键显示数据清零,最大能写入的数据是 255

第一种写法

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

/******************************************************************************
* 函 数 名 : key_scan
* 函数功能 : 检测独立按键是否按下,按下则返回对应键值
* 输 入 : mode=0:单次扫描按键
mode=1:连续扫描按键
* 输 出 : KEY1_PRESS:K1 按下
KEY2_PRESS:K2 按下
KEY3_PRESS:K3 按下
KEY4_PRESS:K4 按下
KEY_UNPRESS:未有按键按下
********************************************************************************/
u8 key_scan(u8 mode)
{
static u8 key = 1; //定义一个静态变量
if(mode) key = 1; //如果 mode=1则表示连续扫描按键 如果 mode=0则忽略这句话
if(key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0 )) //任意按键按下
{
Delay1ms(10); //消抖
key = 0;
if(KEY1 == 0) //如果按键仍然处于被按下状态则表明按键真的被按下了
return KEY1_PRESS;
else if(KEY2 == 0)
return KEY2_PRESS;
else if(KEY3 == 0)
return KEY3_PRESS;
else if(KEY4 == 0)
return KEY4_PRESS;
}
else if(KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1 ) //无按键按下
{
key = 1;
}
return KEY_UNPRESS;
}

Key.h

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

#include "AllHead.h"

//定义4个独立按键控制管脚
sbit KEY1 = P3^1;
sbit KEY2 = P3^0;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;

//使用宏定义独立按键按下的键值(如果按下则返回)
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4

//没按下
#define KEY_UNPRESS 0

u8 key_scan(u8 mode);

#endif

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

u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,
0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; //定义数组存放0-F段码,gsmg中g代表全局变量

/********************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
*********************************************/
void smg_display(u8 location,u8 dat[])
{
u8 i = 0;
u8 pos_temp = location - 1;
for(i = pos_temp;i < 8;i++)
{
switch(i)
{
case 0: LSC = 1;LSB = 1;LSA = 1;break; //Y7 LED8 板子从左边数第一个数码管
case 1: LSC = 1;LSB = 1;LSA = 0;break; //Y6 LED7 板子从左边数第二个数码管
case 2: LSC = 1;LSB = 0;LSA = 1;break; //Y5 LED6 板子从左边数第三个数码管
case 3: LSC = 1;LSB = 0;LSA = 0;break; //Y4 LED5 板子从左边数第四个数码管
case 4: LSC = 0;LSB = 1;LSA = 1;break; //Y3 LED4 板子从左边数第五个数码管
case 5: LSC = 0;LSB = 1;LSA = 0;break; //Y2 LED3 板子从左边数第六个数码管
case 6: LSC = 0;LSB = 0;LSA = 1;break; //Y1 LED2 板子从左边数第七个数码管
case 7: LSC = 0;LSB = 0;LSA = 0;break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = gsmg_code[dat[i-pos_temp]]; //传输段选数据
Delay1ms(1); //延时1毫秒左右,等待显示稳定
SMG_A_DP_PORT = 0x00; //消影
}
}

smg.h

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

#include "AllHead.h"

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

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display(u8 location,u8 dat[]);

extern u8 gsmg_code[17];

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

IIC.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"

/*******************************************************************************
* 函 数 名 : iic_start
* 函数功能 : 产生 IIC 起始信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_start(void)
{
IIC_SCL = 1;
IIC_SDA = 1;
delay_10us(1);
IIC_SDA = 0; //当SCL为高电平时,SDA由高变为低
delay_10us(1);
IIC_SCL = 0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/*******************************************************************************
* 函 数 名 : iic_stop
* 函数功能 : 产生 IIC 停止信号
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_stop(void)
{
IIC_SCL = 1;
IIC_SDA = 0;
delay_10us(1);
IIC_SDA = 1; //当SCL为高电平时,SDA由低变为高
delay_10us(1);
}

/*******************************************************************************
* 函 数 名 : iic_ack
* 函数功能 : 产生 ACK 应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_ack(void)
{
IIC_SCL = 0;
IIC_SDA = 0; //SDA为低电平
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SCL = 0;
}

/*******************************************************************************
* 函 数 名 : iic_nack
* 函数功能 : 产生 NACK 非应答
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void iic_nack(void)
{
IIC_SCL = 0;
IIC_SDA = 1; //SDA为高电平
delay_10us(1);
IIC_SCL = 1;
delay_10us(1);
IIC_SCL = 0;
}

/*******************************************************************************
* 函 数 名 : iic_wait_ack
* 函数功能 : 等待应答信号到来
* 输 入 : 无
* 输 出 : 1,接收应答失败
0,接收应答成功
*******************************************************************************/
u8 iic_wait_ack(void)
{
u8 time_temp = 0; //计时变量
IIC_SCL = 1;
delay_10us(1);
while(IIC_SDA) //等待SDA为低电平(SDA为1进入循环,为0则跳出循环)
{
time_temp++;
if(time_temp > 100) //超时则强制结束IIC通信
{
iic_stop(); //强制结束IIC通信
return 1; //返回1(接收应答失败)
}
}
IIC_SCL = 0;
return 0; //返回0(接收应答成功)
}

/*******************************************************************************
* 函 数 名 : iic_write_byte
* 函数功能 : IIC 发送一个字节
* 输 入 : dat:发送一个字节
* 输 出 : 无
*******************************************************************************/
void iic_write_byte(u8 dat)
{
u8 i = 0;
IIC_SCL = 0; //为0数据才可以改变
for(i = 0;i < 8; i++)
{
if((dat & 0x80) > 0) //比较最高位 (如1011 0100 & 1000 0000 --> 1000 0000 最高位等于1 大于0)
IIC_SDA = 1;
else
IIC_SDA = 0;
dat <<= 1; //左移一位(将次高位移到最高位)
delay_10us(1);
IIC_SCL = 1; //为1数据稳定等待下一次传输
delay_10us(1);
IIC_SCL = 0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
delay_10us(1);
}
}

/*******************************************************************************
* 函 数 名 : iic_read_byte
* 函数功能 : IIC 读一个字节
* 输 入 : ack=1 时,发送 ACK,ack=0,发送 nACK
* 输 出 : 应答或非应答
*******************************************************************************/
u8 iic_read_byte(u8 ack)
{
u8 i = 0;
u8 receive = 0; //保存读取的数据
for(i = 0; i < 8; i++) //循环8次将一个字节读出,先读高位再传低位
{
IIC_SCL = 0;
delay_10us(1);
IIC_SCL = 1; //SCL为1时数据稳定,可以传输数据
delay_10us(1);
receive <<= 1; //将接收到的数据向左移动一位 (假设要传输 1001 1000,则先接收到高位1,则SDA为1,
if(IIC_SDA) //如果SDA为1 receive加1,则receive为 0000 0001,之后将receive
receive++; //则接收到的数据加1 向左移动一位,则receive为 0000 0010,接着传输第二位0,
delay_10us(1); // 则SDA为0,不进入if语句,receive接收到0,则receive
// 为 0000 0010,之后将receive向左移动一位,则receive为
} // 0000 0100 以此类推接收数据,直到数据传输完成则跳出循环)
if(!ack)
iic_nack(); //非应答
else
iic_ack(); //应答
return receive; //返回出去
}

IIC.h

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

#include "AllHead.h"

sbit IIC_SCL = P2^1;
sbit IIC_SDA = P2^0;

void iic_start(void);
void iic_stop(void);
void iic_ack(void);
void iic_nack(void);
u8 iic_wait_ack(void);
void iic_write_byte(u8 dat);
u8 iic_read_byte(u8 ack);

#endif

EEPROM.c(对应IIC第一种第一种写法)

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

/*******************************************************************************
* 函 数 名 : EEPROM_write_one_byte
* 函数功能 : 在 EEPROM 指定地址写入一个数据
* 输 入 : addr:写入数据的目的地址
dat:要写入的数据
* 输 出 : 无
*******************************************************************************/
void EEPROM_write_one_byte(u8 addr,u8 dat)
{
iic_start();
iic_write_byte(0xA0); //发送写命令(前面4位固定,后面三位接gnd所以是0)
iic_wait_ack();
iic_write_byte(addr); //发送写地址
iic_wait_ack();
iic_write_byte(dat); //发送字节
iic_wait_ack();
iic_stop(); //产生一个停止条件
Delay1ms(10);
}

/*******************************************************************************
* 函 数 名 : EEPROM_read_one_byte
* 函数功能 : 在 EEPROM 指定地址读出一个数据
* 输 入 : addr:开始读数的地址
* 输 出 : 读到的数据
*******************************************************************************/
u8 EEPROM_read_one_byte(u8 addr)
{
u8 receive = 0;
iic_start();
iic_write_byte(0xA0); //发送写命令(寻找从机) 最后一位为0表示主机向从机写数据
iic_wait_ack();
iic_write_byte(addr); //在EEPROM指定一个地址(将要接收的数据存放到这个地址)
iic_wait_ack();
iic_start(); //再次启动总线
iic_write_byte(0xA1); //进入接收模式 最后一位为1表示主机由从机读数据
iic_wait_ack();
receive = iic_read_byte(addr); //读取字节
iic_stop(); //产生一个停止条件
return receive; //返回读取的数据
}

EEPROM.h

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

#include "AllHead.h"

void EEPROM_write_one_byte(u8 addr,u8 dat);
u8 EEPROM_read_one_byte(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
27
28
29
30
31
32
33
34
35
36
#include "AllHead.h"

#define EEPROM_ADDRESS 0 //定义数据存入 EEPROM 的起始地址

void main( )
{
u8 key = 0;
u8 save_value = 0;
u8 save_buf[3];
while(1)
{
key = key_scan(0);
if(key == KEY1_PRESS)
{
EEPROM_write_one_byte(EEPROM_ADDRESS,save_value); //按K1键将数据写入到EEPROM内保存
}
else if(key == KEY2_PRESS)
{
save_value = EEPROM_read_one_byte(EEPROM_ADDRESS);//按K2键读取EEPROM内保存的数据
}
else if(key == KEY3_PRESS)
{
save_value++; //按K3键显示数据加1
if(save_value == 255)
save_value = 255;
}
else if(key == KEY4_PRESS)
{
save_value = 0; //按 K4 键显示数据清零,最大能写入的数据是255
}
save_buf[0] = save_value / 100; //百位
save_buf[1] = save_value % 100 / 10; //十位
save_buf[2] = save_value % 100 % 10; //个位
smg_display(6,save_buf);
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "smg.h"
#include "key.h"
#include "EXTI.h"
#include "iic.h"
#include "EEPROM.h"

#endif

第二种写法,区别在IIC那里的写法不同,第二种写法开发板会比较灵敏一点

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

/******************************************************************************
* 函 数 名 : key_scan
* 函数功能 : 检测独立按键是否按下,按下则返回对应键值
* 输 入 : mode=0:单次扫描按键
mode=1:连续扫描按键
* 输 出 : KEY1_PRESS:K1 按下
KEY2_PRESS:K2 按下
KEY3_PRESS:K3 按下
KEY4_PRESS:K4 按下
KEY_UNPRESS:未有按键按下
********************************************************************************/
u8 key_scan(u8 mode)
{
static u8 key = 1; //定义一个静态变量
if(mode) key = 1; //如果mode=1则表示连续扫描按键 如果mode=0则忽略这句话
if(key == 1 && (KEY1 == 0 || KEY2 == 0 || KEY3 == 0 || KEY4 == 0 )) //任意按键按下
{
Delay1ms(10); //消抖
key = 0;
if(KEY1 == 0) //如果按键仍然处于被按下状态则表明按键真的被按下了
return KEY1_PRESS;
else if(KEY2 == 0)
return KEY2_PRESS;
else if(KEY3 == 0)
return KEY3_PRESS;
else if(KEY4 == 0)
return KEY4_PRESS;
}
else if(KEY1 == 1 && KEY2 == 1 && KEY3 == 1 && KEY4 == 1 ) //无按键按下
{
key = 1;
}
return KEY_UNPRESS;
}

Key.h

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

#include "AllHead.h"

//定义4个独立按键控制管脚
sbit KEY1 = P3^1;
sbit KEY2 = P3^0;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;

//使用宏定义独立按键按下的键值(如果按下则返回)
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4

#define KEY_MATRIX_PORT P1 //使用宏定义矩阵按键控制管脚

//没按下
#define KEY_UNPRESS 0

u8 key_scan(u8 mode);

#endif

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

u8 gsmg_code[17] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,
0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; //定义数组存放0-F段码,gsmg中g代表全局变量

/********************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
*********************************************/
void smg_display(u8 location,u8 dat[])
{
u8 i = 0;
u8 pos_temp = location - 1;
for(i = pos_temp;i < 8;i++)
{
switch(i)
{
case 0: LSC = 1;LSB = 1;LSA = 1;break; //Y7 LED8 板子从左边数第一个数码管
case 1: LSC = 1;LSB = 1;LSA = 0;break; //Y6 LED7 板子从左边数第二个数码管
case 2: LSC = 1;LSB = 0;LSA = 1;break; //Y5 LED6 板子从左边数第三个数码管
case 3: LSC = 1;LSB = 0;LSA = 0;break; //Y4 LED5 板子从左边数第四个数码管
case 4: LSC = 0;LSB = 1;LSA = 1;break; //Y3 LED4 板子从左边数第五个数码管
case 5: LSC = 0;LSB = 1;LSA = 0;break; //Y2 LED3 板子从左边数第六个数码管
case 6: LSC = 0;LSB = 0;LSA = 1;break; //Y1 LED2 板子从左边数第七个数码管
case 7: LSC = 0;LSB = 0;LSA = 0;break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = gsmg_code[dat[i-pos_temp]]; //传输段选数据
Delay1ms(1); //延时1毫秒左右,等待显示稳定
SMG_A_DP_PORT = 0x00; //消影
}
}

smg.h

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

#include "AllHead.h"

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

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display(u8 location,u8 dat[]);

extern u8 gsmg_code[17];

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(unsigned int xms);

#endif

IIC.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"

/**
* @brief I2C开始 产生 IIC 起始信号
* @param 无
* @retval 无
*/
void I2C_Start(void)
{
I2C_SDA=1; //先确保SDA为1
I2C_SCL=1; //再把SCL拉高
I2C_SDA=0; //当SCL为高电平时,SDA由高变为低
I2C_SCL=0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/**
* @brief I2C停止 产生IIC停止信号
* @param 无
* @retval 无
*/
void I2C_Stop(void)
{
I2C_SDA=0; //先把SDA拉低
I2C_SCL=1; //再把SCL拉高
I2C_SDA=1; //当SCL为高电平时,SDA由低变为高
}

/**
* @brief I2C发送一个字节
* @param Byte 要发送的字节
* @retval 无
*/
void I2C_SendByte(u8 Byte)
{
u8 i;
for(i=0;i<8;i++)
{ //先传高位再传低位
I2C_SDA=Byte&(0x80>>i); //取出最高位把值赋给SDA,接着再右移一位再取出来赋给SDA,以此类推循环八次
I2C_SCL=1; //为1数据稳定等待下一次传输
I2C_SCL=0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
}

/**
* @brief I2C接收一个字节
* @param 无
* @retval 接收到的一个字节数据
*/
u8 I2C_ReceiveByte(void)
{
u8 i,Byte=0x00;
I2C_SDA=1;
for(i=0;i<8;i++)
{
I2C_SCL=1; //SCL为1时数据稳定,可以传输数据
if(I2C_SDA) //如果SDA为1
{
Byte|=(0x80>>i); //进入该循环表示Byte接收到1,如果SDA等于0,则默认Byte接收到0,不进入循环
}
I2C_SCL=0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
return Byte;
}

/**
* @brief I2C发送应答
* @param AckBit 应答位,0为应答,1为非应答
* @retval 无
*/
void I2C_SendAck(u8 AckBit)
{
I2C_SDA=AckBit; //应答位赋给SDA
I2C_SCL=1; //把SCL拉高
I2C_SCL=0; //再把SCL拉低
}

/**
* @brief I2C接收应答位
* @param 无
* @retval 接收到的应答位,0为应答,1为非应答
*/
u8 I2C_ReceiveAck(void)
{
u8 AckBit;
I2C_SDA=1;
I2C_SCL=1;
AckBit=I2C_SDA; //SDA为0接收应答,为0为非应答
I2C_SCL=0;
return AckBit;
}

IIC.h

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

#include "AllHead.h"

sbit I2C_SCL = P2^1;
sbit I2C_SDA = P2^0;

void I2C_Start(void);
void I2C_Stop(void);
void I2C_SendByte(u8 Byte);
u8 I2C_ReceiveByte(void);
void I2C_SendAck(u8 AckBit);
u8 I2C_ReceiveAck(void);

#endif

EEPROM.c(对应IIC第二种写法)

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

/**
* @brief AT24C02写入一个字节
* @param addr 要写入字节的地址
* @param Data 要写入的数据
* @retval 无
*/
void EEPROM_WriteByte(u8 addr,u8 Data)
{
I2C_Start();
I2C_SendByte(0xA0); //发送写命令(前面4位固定,后面三位接gnd所以是0)
I2C_ReceiveAck();
I2C_SendByte(addr); //发送写地址
I2C_ReceiveAck();
I2C_SendByte(Data); //发送字节
I2C_ReceiveAck();
I2C_Stop(); //产生一个停止条件
}

/**
* @brief AT24C02读取一个字节
* @param addr 要读出字节的地址
* @retval 读出的数据
*/
u8 EEPROM_ReadByte(u8 addr)
{
u8 Data;
I2C_Start();
I2C_SendByte(0xA0); //发送写命令(寻找从机) 最后一位为0表示主机向从机写数据
I2C_ReceiveAck();
I2C_SendByte(addr); //在EEPROM指定一个地址(将要接收的数据存放到这个地址)
I2C_ReceiveAck();
I2C_Start(); //再次启动总线
I2C_SendByte(0xA1); //进入接收模式 最后一位为1表示主机由从机读数据
I2C_ReceiveAck();
Data=I2C_ReceiveByte(); //读取字节
I2C_SendAck(1);
I2C_Stop(); //产生一个停止条件
return Data; //返回读取的数据
}

EEPROM.h

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

#include "AllHead.h"

void EEPROM_WriteByte(u8 addr,u8 Data);
u8 EEPROM_ReadByte(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
27
28
29
30
31
32
33
34
35
36
#include "AllHead.h"

#define EEPROM_ADDRESS 0 //定义数据存入 EEPROM 的起始地址

void main( )
{
u8 key = 0;
u8 save_value = 0;
u8 save_buf[3];
while(1)
{
key = key_scan(0);
if(key == KEY1_PRESS)
{
EEPROM_WriteByte(EEPROM_ADDRESS,save_value); //按K1键将数据写入到EEPROM内保存
}
else if(key == KEY2_PRESS)
{
save_value = EEPROM_ReadByte(EEPROM_ADDRESS);//按K2键读取EEPROM内保存的数据
}
else if(key == KEY3_PRESS)
{
save_value++; //按K3键显示数据加1
if(save_value == 255)
save_value = 255;
}
else if(key == KEY4_PRESS)
{
save_value = 0; //按 K4 键显示数据清零,最大能写入的数据是255
}
save_buf[0] = save_value / 100; //百位
save_buf[1] = save_value % 100 / 10; //十位
save_buf[2] = save_value % 100 % 10; //个位
smg_display(6,save_buf);
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "smg.h"
#include "key.h"
#include "EXTI.h"
#include "iic.h"
#include "EEPROM.h"

#endif

按键1控制秒表启动与暂停,按键2使秒表清0,按键3将秒表数据存储到EEPROM,按键4读取EEPROM中存储的数据

这个是一种新的思路,把按键和数码管的驱动函数放到定时器中断服务函数这里来,让定时器可以实现多个模块进行计时,实现更高效率的计时

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

u8 key_ketNumber = 0;

/**
* @brief 获取按键键码
* @param 无
* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
*/
u8 key(void)
{
u8 temp;
temp = key_ketNumber;
key_ketNumber = 0;
return temp;
}

/**
* @brief 获取当前按键的状态,无消抖及松手检测
* @param 无
* @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
*/
u8 Key_GetState(void)
{
u8 KeyNumber = 0;
if(KEY1 == 0) //如果按键1按下
KeyNumber = KEY1_PRESS; // 按键值1
if(KEY2 == 0) //如果按键2按下
KeyNumber = KEY2_PRESS; // 按键值2
if(KEY3 == 0) //如果按键3按下
KeyNumber = KEY3_PRESS; // 按键值3
if(KEY4 == 0) //如果按键4按下
KeyNumber = KEY4_PRESS; // 按键值4
return KeyNumber; //返回按键值
}

/**
* @brief 按键驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Key_loop(void)
{
static u8 NowState,LastState;
LastState = NowState; //按键状态更新
NowState = Key_GetState(); //获取当前按键状态
//如果上个时间点按键按下,这个时间点未按下,则是松手瞬间,以此避免消抖和松手检测
if(LastState == KEY1_PRESS && NowState == KEY_UNPRESS)
key_ketNumber = KEY1_PRESS;
if(LastState == KEY2_PRESS && NowState == KEY_UNPRESS)
key_ketNumber = KEY2_PRESS;
if(LastState == KEY3_PRESS && NowState == KEY_UNPRESS)
key_ketNumber = KEY3_PRESS;
if(LastState == KEY4_PRESS && NowState == KEY_UNPRESS)
key_ketNumber = KEY4_PRESS;
}

Key.h

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

#include "AllHead.h"

//定义4个独立按键控制管脚
sbit KEY1 = P3^1;
sbit KEY2 = P3^0;
sbit KEY3 = P3^2;
sbit KEY4 = P3^3;

//使用宏定义独立按键按下的键值(如果按下则返回)
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4

//没按下
#define KEY_UNPRESS 0

u8 Key_GetState(void);
u8 key(void);
void Key_loop(void);

#endif

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

//数码管显示缓存区
u8 smg_Buf[9] = {0,10,10,10,10,10,10,10,10};

//数码管段码表
u8 gsmg_code[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};
//定义数组存放0-10段码,gsmg中g代表全局变量

/**
* @brief 设置显示缓存区
* @param Location 要设置的位置,范围:1~8
* @param Number 要设置的数字,范围:段码表索引范围
* @retval 无
*/
void smg_SetBuf(u8 location,u8 number)
{
smg_Buf[location] = number; //将某个位置的数码管改成某个数字
}

/********************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
*********************************************/
void smg_display(u8 location,u8 number)
{
SMG_A_DP_PORT = 0x00; //消影
switch(location)
{
case 1: LSC = 1;LSB = 1;LSA = 1;break; //Y7 LED8 板子从左边数第一个数码管
case 2: LSC = 1;LSB = 1;LSA = 0;break; //Y6 LED7 板子从左边数第二个数码管
case 3: LSC = 1;LSB = 0;LSA = 1;break; //Y5 LED6 板子从左边数第三个数码管
case 4: LSC = 1;LSB = 0;LSA = 0;break; //Y4 LED5 板子从左边数第四个数码管
case 5: LSC = 0;LSB = 1;LSA = 1;break; //Y3 LED4 板子从左边数第五个数码管
case 6: LSC = 0;LSB = 1;LSA = 0;break; //Y2 LED3 板子从左边数第六个数码管
case 7: LSC = 0;LSB = 0;LSA = 1;break; //Y1 LED2 板子从左边数第七个数码管
case 8: LSC = 0;LSB = 0;LSA = 0;break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = gsmg_code[number]; //传输段选数据
}

/**
* @brief 数码管驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void smg_loop(void)
{
static u8 i = 1;
smg_display(i,smg_Buf[i]);
i++;
if(i > 8)
i = 1;
}

smg.h

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

#include "AllHead.h"

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

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display(u8 location,u8 number);
void smg_loop(void);
void smg_SetBuf(u8 location,u8 number);

#endif

EXTI.c

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

/* -------------------------------- begin ------------------------------ */
/**
* @函数名: Time0_Init
* @功能 : 定时器0中断配置函数,通过设置 TH 和 TL 即可确定定时时间(使用工具可确定TH 与TL的值)
* @返回值: 无 晶振频率为11.0592
**/
/* -------------------------------- end -------------------------------- */
void Time0_Init(void)
{
TMOD |= 0x01; //选择为定时器0模式,工作方式1(为了不干扰T1定时器所以用|);如果是定时器1则把0x01改成0x10
TH0 = 0xFC; //给定时器赋初值,定时1ms;如果是定时器1则把TH0改成TH1
TL0 = 0X66; //如果是定时器1则把TL0改成TL1
ET0 = 1; //打开定时器0中断允许
EA = 1; //打开总中断
TR0 = 1; //打开定时器
}

EXTI.h

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

#include "AllHead.h"

void Time0_Init(void);

#endif

IIC.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"

/**
* @brief I2C开始 产生 IIC 起始信号
* @param 无
* @retval 无
*/
void I2C_Start(void)
{
I2C_SDA=1; //先确保SDA为1
I2C_SCL=1; //再把SCL拉高
I2C_SDA=0; //当SCL为高电平时,SDA由高变为低
I2C_SCL=0; //钳住I2C总线,准备发送或接收数据(SCL为低电平时数据可以改变)
}

/**
* @brief I2C停止 产生IIC停止信号
* @param 无
* @retval 无
*/
void I2C_Stop(void)
{
I2C_SDA=0; //先把SDA拉低
I2C_SCL=1; //再把SCL拉高
I2C_SDA=1; //当SCL为高电平时,SDA由低变为高
}

/**
* @brief I2C发送一个字节
* @param Byte 要发送的字节
* @retval 无
*/
void I2C_SendByte(u8 Byte)
{
u8 i;
for(i=0;i<8;i++)
{ //先传高位再传低位
I2C_SDA=Byte&(0x80>>i); //取出最高位把值赋给SDA,接着再右移一位再取出来赋给SDA,以此类推循环八次
I2C_SCL=1; //为1数据稳定等待下一次传输
I2C_SCL=0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
}

/**
* @brief I2C接收一个字节
* @param 无
* @retval 接收到的一个字节数据
*/
u8 I2C_ReceiveByte(void)
{
u8 i,Byte=0x00;
I2C_SDA=1;
for(i=0;i<8;i++)
{
I2C_SCL=1; //SCL为1时数据稳定,可以传输数据
if(I2C_SDA) //如果SDA为1
{
Byte|=(0x80>>i); //进入该循环表示Byte接收到1,如果SDA等于0,则默认Byte接收到0,不进入循环
}
I2C_SCL=0; //数据传输完毕,让SCL为0,使下一次数据可以改变并进行传输
}
return Byte;
}

/**
* @brief I2C发送应答
* @param AckBit 应答位,0为应答,1为非应答
* @retval 无
*/
void I2C_SendAck(u8 AckBit)
{
I2C_SDA=AckBit; //应答位赋给SDA
I2C_SCL=1; //把SCL拉高
I2C_SCL=0; //再把SCL拉低
}

/**
* @brief I2C接收应答位
* @param 无
* @retval 接收到的应答位,0为应答,1为非应答
*/
u8 I2C_ReceiveAck(void)
{
u8 AckBit;
I2C_SDA=1;
I2C_SCL=1;
AckBit=I2C_SDA; //SDA为0接收应答,为0为非应答
I2C_SCL=0;
return AckBit;
}

IIC.h

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

#include "AllHead.h"

sbit I2C_SCL = P2^1;
sbit I2C_SDA = P2^0;

void I2C_Start(void);
void I2C_Stop(void);
void I2C_SendByte(u8 Byte);
u8 I2C_ReceiveByte(void);
void I2C_SendAck(u8 AckBit);
u8 I2C_ReceiveAck(void);

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

/**
* @brief AT24C02写入一个字节
* @param WordAddress 要写入字节的地址
* @param Data 要写入的数据
* @retval 无
*/
void EEPROM_WriteByte(u8 addr,u8 Data)
{
I2C_Start();
I2C_SendByte(0xA0); //发送写命令(前面4位固定,后面三位接gnd所以是0)
I2C_ReceiveAck();
I2C_SendByte(addr); //发送写地址
I2C_ReceiveAck();
I2C_SendByte(Data); //发送字节
I2C_ReceiveAck();
I2C_Stop(); //产生一个停止条件
}

/**
* @brief AT24C02读取一个字节
* @param WordAddress 要读出字节的地址
* @retval 读出的数据
*/
u8 EEPROM_ReadByte(u8 addr)
{
u8 Data;
I2C_Start();
I2C_SendByte(0xA0); //发送写命令(寻找从机) 最后一位为0表示主机向从机写数据
I2C_ReceiveAck();
I2C_SendByte(addr); //在EEPROM指定一个地址(将要接收的数据存放到这个地址)
I2C_ReceiveAck();
I2C_Start(); //再次启动总线
I2C_SendByte(0xA1); //进入接收模式 最后一位为1表示主机由从机读数据
I2C_ReceiveAck();
Data=I2C_ReceiveByte(); //读取字节
I2C_SendAck(1);
I2C_Stop(); //产生一个停止条件
return Data; //返回读取的数据
}

EEPROM.h

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

#include "AllHead.h"

void EEPROM_WriteByte(u8 addr,u8 Data);
u8 EEPROM_ReadByte(u8 addr);

#endif

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

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

Delay.h

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

#include "AllHead.h"

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

void Delay1ms(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
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
#include "AllHead.h"

u8 key_Number = 0;
u8 Min,Sec,MinSec; //定义 分钟,秒钟,MinSce为秒钟后面计数的,每100个MinSec秒钟就加1
u8 RunFlag; //标志位

void main( )
{
Time0_Init();
while(1)
{
key_Number = key();
if(key_Number == KEY1_PRESS)
{
RunFlag = !RunFlag; //标志位翻转(控制秒表启动与暂停)
}
if(key_Number == KEY2_PRESS) //按键2按下秒表清0
{
Min = 0; //分秒清0
Sec = 0;
MinSec = 0;
}
if(key_Number == KEY3_PRESS)
{
EEPROM_WriteByte(0,Min); //将分秒写入EEPROM
Delay1ms(5);
EEPROM_WriteByte(1,Sec);
Delay1ms(5);
EEPROM_WriteByte(2,MinSec);
Delay1ms(5);
}
if(key_Number == KEY4_PRESS)
{
Min = EEPROM_ReadByte(0); //读取EEPROM中的数据
Sec = EEPROM_ReadByte(1);
MinSec = EEPROM_ReadByte(2);
}
smg_SetBuf(1,Min/10); //显示分第一位
smg_SetBuf(2,Min%10); //显示分第二位
smg_SetBuf(3,11); //显示横杠
smg_SetBuf(4,Sec/10); //显示秒第一位
smg_SetBuf(5,Sec%10); //显示秒第一位
smg_SetBuf(6,11); //显示横杠
smg_SetBuf(7,MinSec/10);
smg_SetBuf(8,MinSec%10);
}
}

/**
* @brief 秒表驱动函数,在中断中调用
* @param 无
* @retval 无
*/
void Sec_loop(void)
{
if(RunFlag) //如果中断标志位为1,则启动计时
{
MinSec++;
if(MinSec >= 100)
{
MinSec = 0;
Sec++;
if(Sec >= 60) //如果秒钟加到 60
{
Sec = 0; //秒钟清 0
Min++; //分钟加 1
if(Min >= 60) //如果分钟加到 60
{
Min = 0; //分钟清 0
}
}
}
}
}

/* -------------------------------- begin ------------------------------ */
/**
* @功能 : 定时器0中断服务函数
**/
/* -------------------------------- end -------------------------------- */
void Time0() interrupt 1 //定时器0中断号为1
{
static u16 count1 = 0,count2 = 0,count3 = 0; //定义静态变量count
TH0 = 0xFC; //给定时器赋初值,定时1ms
TL0 = 0X66;
count1++;
if(count1 == 20) //20ms
{
count1 = 0; //溢出然后重新赋0
Key_loop(); //每 20 毫秒进入按键驱动函数,相当于每 20 毫秒扫描一次按键,用这个方法就不需要进行消抖操作
}
count2++;
if(count2 == 2) //2ms
{
count2 = 0; //溢出然后重新赋0
smg_loop(); //每 2 毫秒进入数码管驱动函数,相当于每 2 毫秒扫描一次数码管
}
count3++;
if(count3 == 10) //10 ms
{
count3 = 0; //溢出然后重新赋0
Sec_loop(); //每 10 毫秒进入秒表驱动函数
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "smg.h"
#include "key.h"
#include "EXTI.h"
#include "iic.h"
#include "EEPROM.h"

#endif

DS18B20-温度传感器

DS18B20 介绍

DS18B20 是一种“ 一线总线(单总线)”接口的温度传感器。它是一种新型的体积小、适用电压宽、与微处理器接口简单的数字化温度传感器。

DS18B20 温度传感器的特点:

1、适应电压范围更宽,电压范围:3.0~5.5V,在寄生电源方式下可由数据线供电。
2、独特的单线接口方式,DS18B20 在与微处理器连接时仅需要一条口线即可实现微处理器与 DS18B20 的双向通讯。
3、DS18B20 支持多点组网功能,多个 DS18B20 可以并联在唯一的三线上,实现组网多点测温。
4、DS18B20 在使用中不需要任何外围元件,全部传感元件及转换电路集成在形如一只三极管的集成电路内。
5、温度范围 -55℃~+125℃,在-10~+85℃时精度为±0.5℃
6、可编程的分辨率为 9~12 位,对应的可分辨温度分别为 0.5℃、0.25℃、0.125℃ 和 0.0625℃,可实现高精度测温
7、在 9 位分辨率时最多在 93.75ms 内把温度转换为数字,12 位分辨率时最多在 750ms 内把温度值转换为数字,速度更快。
8、测量结果直接输出数字温度信号,以"一根总线"串行传送给 CPU,同时可传送 CRC 校验码,具有极强的抗干扰纠错能力。
9、负压特性:电源极性接反时,芯片不会因发热而烧毁,但不能正常工作

DS18B20 外观实物如下图所示:

从 DS18B20 外观图可以看到,当我们正对传感器切面(传感器型号字符那一面)时,传感器的管脚顺序是从左到右排列管脚 1 为 GND,管脚 2 为数据DQ,管脚 3 为 VDD如果把传感器插反,那么电源将短路,传感器就会发烫,很容易损坏,所以一定要注意传感器方向,通常在开发板上都会标出传感器的凸起出,所以只需要把传感器凸起的方向对着开发板凸起方向插入即可。

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)→ 连续读出两个字节数据(即温度) → 结束

DS18B20 模块电路原理图

传感器接口的单总线管脚接至单片机 P3.7 IO 口上

DS18B20 实验

插上 DS18B20 温度传感器,数码管显示检测的温度值

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
/*******************************************************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
* 输 入 : dat:要显示的数据
location:从左开始第几个位置开始显示,范围1-8
* 输 出 : 无
*******************************************************************************/
void smg_display(u8 dat[], u8 location)
{
u8 i = 0;
u8 location_temp = location - 1;
for(i = location_temp; i < 8; i++)
{
switch(i)
{
case 0:
LSC = 1;
LSB = 1;
LSA = 1;
break; //Y7 LED8 板子从左边数第一个数码管
case 1:
LSC = 1;
LSB = 1;
LSA = 0;
break; //Y6 LED7 板子从左边数第二个数码管
case 2:
LSC = 1;
LSB = 0;
LSA = 1;
break; //Y5 LED6 板子从左边数第三个数码管
case 3:
LSC = 1;
LSB = 0;
LSA = 0;
break; //Y4 LED5 板子从左边数第四个数码管
case 4:
LSC = 0;
LSB = 1;
LSA = 1;
break; //Y3 LED4 板子从左边数第五个数码管
case 5:
LSC = 0;
LSB = 1;
LSA = 0;
break; //Y2 LED3 板子从左边数第六个数码管
case 6:
LSC = 0;
LSB = 0;
LSA = 1;
break; //Y1 LED2 板子从左边数第七个数码管
case 7:
LSC = 0;
LSB = 0;
LSA = 0;
break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = dat[i - location_temp]; //传送段选数据
delay_10us(100);//延时一段时间,等待显示稳定
SMG_A_DP_PORT = 0x00; //消音
}
}

smg.h

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

#include "AllHead.h"
#define SMG_A_DP_PORT P0 //宏定义数码管P0端口

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display(u8 dat[], u8 location);

extern u8 gsmg_code[17]; //允许外部进行调用就得用extern

#endif

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

/*******************************************************************************
* 函 数 名 : ds18b20_reset
* 函数功能 : 复位 DS18B20
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ds18b20_reset(void)
{
DS18B20_PORT = 0; //拉低 DQ
delay_10us(75); //延时750微秒
DS18B20_PORT = 1; //拉高DQ DQ = 1
delay_10us(2); //延时20微秒
}

/*******************************************************************************
* 函 数 名 : ds18b20_check
* 函数功能 : 检测 DS18B20 是否存在
* 输 入 : 无
* 输 出 : 1:未检测到 DS18B20 的存在,0:存在
*******************************************************************************/
u8 ds18b20_check(void)
{
u8 time_temp = 0; //设置时间变量原因是如果DQ一直为高电平的话不能一直在这里死循环,如果等待一会DQ还是高电平的话则强制退出
while(DS18B20_PORT && time_temp < 20) //等待DQ为低电平(如果DQ为1且时间变量小于20则进入循环,如果为低电平0则跳出循环)
{
time_temp++;
delay_10us(1);
}
if(time_temp >= 20) return 1; //如果超时则强制返回 1,证明DS18B20不存在
else time_temp = 0;

while((!DS18B20_PORT) && time_temp < 20) //等待DQ为高电平(如果DQ为0且时间变量小于20则进入循环,如果为高电平1则跳出循环)
{
time_temp++;
delay_10us(1);
}
if(time_temp >= 20) return 1; //如果超时则强制返回 1,证明DS18B20不存在
else
return 0; //证明DS18B20存在
}

/*******************************************************************************
* 函 数 名 : ds18b20_init
* 函数功能 : 初始化 DS18B20 的 IO 口 DQ, 同时检测 DS 的存在
* 输 入 : 无
* 输 出 : 1:不存在,0:存在
*******************************************************************************/
u8 ds18b20_init(void)
{
ds18b20_reset();
return ds18b20_check();
}

/*******************************************************************************
* 函 数 名 : ds18b20_write_byte
* 函数功能 : 写一个字节到DS18B20
* 输 入 : dat:要写入的字节
* 输 出 : 无
*******************************************************************************/
void ds18b20_write_byte(u8 dat)
{
u8 i = 0;
u8 temp = 0;
for(i = 0; i < 8; i++) //循环8次,每次写一位,且先写低位再写高位
{
//例如data为 0101 1011
temp = dat & 0x01; //选择低位准备写入(则temp为0000 0001 )
dat >>= 1; //将次低位移到最低位,例如一开始dat为0101 1011 向右移动一位则变成 0010 1101
if(temp) //如果写 1 时序
{
DS18B20_PORT = 0;//主机拉低总线
_nop_();
_nop_(); //延时两毫秒(该函数为头文件intrins.h里的函数,一个_nop_()表示延时 1 毫秒)
DS18B20_PORT = 1;//释放总线
delay_10us(6); //延时 60 us
}
else //写入 0 时序
{
DS18B20_PORT = 0;//主机拉低总线
delay_10us(6); //延时 60 us
DS18B20_PORT = 1;//释放总线
_nop_();
_nop_(); //延时两毫秒(该函数为头文件intrins.h里的函数,一个_nop_()表示延时 1 微秒)
}
}
}

/*******************************************************************************
* 函 数 名 : ds18b20_read_bit
* 函数功能 : 从DS18B20读取一个位
* 输 入 : 无
* 输 出 : 1/0
*******************************************************************************/
u8 ds18b20_read_bit(void)
{
u8 dat = 0;
DS18B20_PORT = 0;
_nop_();
_nop_();
DS18B20_PORT = 1;
_nop_();
_nop_(); //该段时间不能过长,必须在15us内读取数据
if(DS18B20_PORT)
dat = 1; //如果总线上为1则数据dat为1,否则为0
else
dat = 0;
delay_10us(5);
return dat;
}

/*******************************************************************************
* 函 数 名 : ds18b20_read_byte
* 函数功能 : 从DS18B20读取一个字节
* 输 入 : 无
* 输 出 : 一个字节数据
*******************************************************************************/
u8 ds18b20_read_byte(void)
{
u8 i = 0;
u8 dat = 0;
u8 temp = 0;
for(i = 0; i < 8; i++) //循环8次,每次读取一位,且先读低位再读高位
{
temp = ds18b20_read_bit(); //将接收到的位赋给temp 例如temp接收到1
dat = (temp << 7) | (dat >> 1); //dat向右移动一位,将最高位移到次高位,temp向左移动7位将最低位移到最高位
// 0000 0001 << 7 --> 1000 0000 ,接着两个进行或运算的结果赋给dat,此目的是为了保留dat的值
//接着将dat向右移动1位到次高位与重新接收到的temp向左移动7位来进行或运算,以此类推进行八次循环
}
return dat;
}

/*******************************************************************************
* 函 数 名 : ds18b20_start
* 函数功能 : 开始温度转换
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ds18b20_start(void)
{
ds18b20_reset(); //复位
ds18b20_check(); //检查DS18B20是否存在
ds18b20_write_byte(0xCC); //发SKIP ROM 命令
ds18b20_write_byte(0x44); //发开始转换命令
}

/*******************************************************************************
* 函 数 名 : ds18b20_read_temperture(温度读取函数)
* 函数功能 : 从ds18b20得到温度值
* 输 入 : 无
* 输 出 : 温度数据
*******************************************************************************/
u8 ds18b20_read_temperture(void)
{
u8 dath = 0;
u8 datl = 0;
u16 value = 0;
float temp;

ds18b20_start();
ds18b20_reset(); //复位
ds18b20_check(); //检查DS18B20是否存在
ds18b20_write_byte(0xCC); //发SKIP ROM 命令
ds18b20_write_byte(0xBE); //读存储器

datl = ds18b20_read_byte(); //低八位字节数据
dath = ds18b20_read_byte(); //高八位字节数据
value = (dath << 8) + datl;//合并为16位的数据

if(value && 0xF800 == 0xf800) //判断符号位,负温度(如果该16位数据中前五位为 1 则表示温度小于0 )
{
value = (~value) + 1; //读取到的数据取反再加1
temp = value * (-0.0625); //乘以精度
}
else //如果该16位数据中前五位为 0 则表示温度大于0
temp = value * 0.0625; //读取到的数据直接乘以精度得出温度
return temp;
}

ds18b20.h

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

#include "AllHead.h"

sbit DS18B20_PORT = P3^7; //定义ds18b20 DQ的管脚

void ds18b20_reset(void);
u8 ds18b20_check(void);
u8 ds18b20_init(void);
void ds18b20_write_byte(u8 dat);
u8 ds18b20_read_bit(void);
void ds18b20_write_byte(u8 dat);
void ds18b20_start(void);
u8 ds18b20_read_temperture(void);

#endif

Delay.c

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

/* -------------------------------- begin ------------------------------ */
/**
* @函数名: delay_us
* @参数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
10
11
#ifndef _DELAY_H
#define _DELAY_H

#include "AllHead.h"

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

void delay_10us(u16 ten_us);

#endif

main.c

主函数代码很简单,在 while循环中间隔一定时间读取温度数据,并将温度值保留小数点后一位,然后将读取的温度数据转换为数码管可显示的段码,最后调用数码管显示函数

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

void main()
{
u8 i = 0;
int temp_value;
u8 temp_buf[5];

ds18b20_init(); //初始化DS18B20

while(1)
{
i++;
if(i % 50 == 0) //间隔一段时间读取温度值,间隔时间要大于温度传感器转换温度时间
temp_value = ds18b20_read_temperture() * 10; //保留温度值小数后一位(乘以10保留最后一位小数) 如温度123.5乘以10为1235
if(temp_value < 0) //负温度
{
temp_value = -temp_value; //显示负号
temp_buf[0] = 0x40; // 0x40为数码管那个负号
}
else
{
temp_buf[0] = 0x00; //不显示
}
temp_buf[1] = gsmg_code[temp_value / 1000]; //百位 因为温度乘以10,所以该数值有四位数,要取出第一位得 /1000
temp_buf[2] = gsmg_code[temp_value % 1000 / 100]; //十位
temp_buf[3] = gsmg_code[temp_value % 1000 % 100 / 10] | 0x80; //个位+小数点
temp_buf[4] = gsmg_code[temp_value % 1000 % 100 % 10]; //小数点后一位
smg_display(temp_buf, 4); //在第四个数码管开始显示数据
}
}

AllHead.h

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

#include <reg52.h>
#include "intrins.h" //库函数
#include "smg.h"
#include "ds18b20.h"

#endif

注意:一定要注意温度传感器的方向,在接口处我们已经用丝印画了一个凸起,所以只需要将温度传感器对应插入即可

DS1302 时钟

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;
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 时停止。

小时寄存器时寄存器最高位为 12/24 小时的格式选择位,该位为 1 时表示 12 小时格式。当设置为 12 小时显示格式时,第 5 位的高电平表示下午(PM);而当设置为 24 小时格式时,第 5 位为具体的时间数据。

写保护寄存器:当该寄存器最高位 WP 为 1 时,DS1302只读不写,所以要在往 DS1302 写数据之前确保 WP 为 0

慢充电寄存器(涓细电流充电)寄存器:当 DS1302 掉电时,可以马上调用外部电源保护时间数据。该寄存器就是配置备用电源的充电选项的。其中高四位(4 个 TCS)只有在 1010 的情况下才能使用充电选项;低四位的情况与 DS1302 内部电路有关。

什么是BCD 码

BCD 码是通过 4 位二进制码来表示 1 位十进制中的 0~9 这 10 个数码

如下所示:

从 DS1302 中读取出来的时钟数据均为 BCD 码格式,需转换为我们习惯的 10 进制

DS1302 的读写时序

控制指令字输入后的下一个 SCLK 时钟的上升沿时,数据被写入 DS1302,数据输入从低位(位 0)开始。同样,在紧跟 8 位的控制指令字后的下一个 SCLK脉冲的下降沿读出 DS1302 的数据,读出数据时从低位 0 位到高位 7。其时序图如下所示:

上图就是 DS1302 的三个时序:复位时序,单字节写时序,单字节读时序

CE(RST):复位时序,即在 RST 引脚产生一个正脉冲,在整个读写器件,RST 要保持高电平,一次字节读写完毕之后,要注意把 RST 返回低电平准备下次读写周期;

单字节读时序:注意读之前还是要先对寄存器写命令,前八个为控制寄存器地址,从最低位开始写;可以看到,写数据是在 SCLK 的上升沿实现,而读数据在 SCLK 的下降沿实现。所以,在单字节读时序中,写命令的第八个上升沿结束后紧接着的第八个下降沿就将要寄存器的第一位数据读到数据线上了!这个就是 DS1302 操作中最特别的地方。当然读出来的数据也是最低位开始。

单字节写时序:两个字节的数据配合 16 个上升沿将数据写入即可。

程序注意事项
★要记得在操作 DS1302 之前关闭写保护(即确保 WP 为 0);
★注意用延时来降低单片机的速度以配合器件时序;
★DS1302 读出来的数据是 BCD 码形式,要转换成我们习惯的 10 进制;
★读取字节之前,将 IO 设置为输入口,读取完之后,要将其改回输出口;
★在写程序的时候,建议实现开辟数组(内存空间)来集中放置 DS1302 的一系列数据,方便以后扩展键盘输入。

DS1302时钟原理图

DS1302 芯片的控制管脚接至单片机 P3.4-P3.6 上,在芯片的X1、X2 管脚处外接了一个32.768KHZ 晶振,为时钟运行提供一个稳定的时钟频率,C2 和 C3 为旁路电容,目的是消除晶振起振时产生的电感干扰。如果需要备用电源可将外部备用电源接入第 8 脚 VCC1。

DS1302时钟实验程序

数码管上显示电子时钟时分秒,格式为“XX-XX-XX”

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
/*******************************************************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
* 输 入 : dat:要显示的数据
location:从左开始第几个位置开始显示,范围1-8
* 输 出 : 无
*******************************************************************************/
void smg_display(u8 dat[], u8 location)
{
u8 i = 0;
u8 location_temp = location - 1;
for(i = location_temp; i < 8;i++)
{
switch(i)
{
case 0: LSC = 1;LSB = 1;LSA = 1;break; //Y7 LED8 板子从左边数第一个数码管
case 1: LSC = 1;LSB = 1;LSA = 0;break; //Y6 LED7 板子从左边数第二个数码管
case 2: LSC = 1;LSB = 0;LSA = 1;break; //Y5 LED6 板子从左边数第三个数码管
case 3: LSC = 1;LSB = 0;LSA = 0;break; //Y4 LED5 板子从左边数第四个数码管
case 4: LSC = 0;LSB = 1;LSA = 1;break; //Y3 LED4 板子从左边数第五个数码管
case 5: LSC = 0;LSB = 1;LSA = 0;break; //Y2 LED3 板子从左边数第六个数码管
case 6: LSC = 0;LSB = 0;LSA = 1;break; //Y1 LED2 板子从左边数第七个数码管
case 7: LSC = 0;LSB = 0;LSA = 0;break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = dat[i - location_temp]; //传送段选数据
delay_10us(100);//延时一段时间,等待显示稳定
SMG_A_DP_PORT=0x00;//消音
}
}

smg.h

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

#include "AllHead.h"
#define SMG_A_DP_PORT P0 //宏定义数码管P0端口

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display(u8 dat[], u8 location);

extern u8 gsmg_code[17]; //允许外部进行调用就得用extern

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

//---DS1302写入和读取时分秒的地址命令---//
//---秒分时日月周年 最低位读写位;-------//
u8 gWRITE_addr[7] = {0x80,0x82,0x84,0x86,0x88,0x8a,0x8c};//数据可查看各类寄存器
u8 gRREAD_addr[7] = {0x81,0x83,0x85,0x87,0x89,0x8b,0x8d};

//---DS1302时钟初始化2023年8月8日星期二18点34分01秒。---//
//---存储顺序是秒分时日月周年,存储格式是用BCD码---//
u8 gDS1302_TIME[7] = {0x01,0x34,0x18,0x08,0x08,0x02,0x23};

/*******************************************************************************
* 函 数 名 : ds1302_write_byte
* 函数功能 : DS1302写单字节
* 输 入 : addr:地址/命令
dat:数据
* 输 出 : 无
*******************************************************************************/
void ds1302_write_byte(u8 addr,u8 dat)
{
u8 i = 0;
DS1302_CE = 0; //首先CE输出低电平
_nop_(); //延时1us
DS1302_SCLK = 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_SCLK = 1; //SCLK由低到高产生一个上升沿,从而写入数据
_nop_();
DS1302_SCLK = 0; //SCLK先拉低,以便下一次上升沿
_nop_();
}
for(i = 0;i < 8;i++) //循环8次,每次写1位,先写低位再写高位
{
DS1302_IO = dat & 0x01; //取出写入数据的最低位赋给DS1302的IO口
dat >>= 1; //将数据的次低位移到最低位
DS1302_SCLK = 1; //SCLK由低到高产生一个上升沿,从而写入数据
_nop_(); //延时1us
DS1302_SCLK = 0; //SCLK先拉低,以便下一次上升沿
_nop_();
}
DS1302_CE = 0; //RST拉低
_nop_();
}

/*******************************************************************************
* 函 数 名 : ds1302_read_byte
* 函数功能 : DS1302读单字节
* 输 入 : addr:地址/命令
* 输 出 : 读取的数据
*******************************************************************************/
u8 ds1302_read_byte(u8 addr)
{
u8 i = 0;
u8 temp = 0; //接收数据中一个位的变量
u8 dat = 0; //将接收到的数据存放到这个变量

DS1302_CE = 0; //首先CE输出低电平
_nop_(); //延时1us
DS1302_SCLK = 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_SCLK = 1; //SCLK由低到高产生一个上升沿,从而写入数据
_nop_();
DS1302_SCLK = 0; //SCLK先拉低,以便下一次上升沿
_nop_();
}
for(i = 0;i < 8;i++) //循环8次,每次读1位,先读低位再读高位
{
temp = DS1302_IO; //将在IO口接收到的数据中的一位存放到temp变量中
dat = (temp << 7) | (dat >> 1); //先将dat右移1位,然后temp左移7位,最后或运算(或运算中或符号后面的数据优先级比较高)
DS1302_SCLK = 1;
_nop_();
DS1302_SCLK = 0; //SCLK由高到低产生一个下降沿,从而读入数据
_nop_();
}
DS1302_CE = 0; //CE拉低
_nop_();
DS1302_SCLK = 1; //对于实物中,P3.4口没有外接上拉电阻的,此处代码需要添加,使数据口有一个上升沿脉冲
_nop_();
DS1302_IO = 0;
_nop_();
DS1302_IO = 1; //读取字节之前,将 IO 设置为输入口,读取完之后,要将其改回输出口
_nop_();
return dat; //将读取到的数据返回出去
}

/*******************************************************************************
* 函 数 名 : ds1302_init
* 函数功能 : DS1302初始化时间
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ds1302_init(void)
{
u8 i = 0;
ds1302_write_byte(0x8E,0X00); //关闭写保护(写保护寄存器的地址为0x8E,WP为其中的最高位,WP为0即为关闭写保护)
for(i = 0;i < 7;i++)
{
ds1302_write_byte(gWRITE_addr[i],gDS1302_TIME[i]);
}
ds1302_write_byte(0x8E,0X80); //打开写保护,以防止意外修改DS1302内部寄存器(WP为1即为打开写保护)
}

/*******************************************************************************
* 函 数 名 : ds1302_read_time
* 函数功能 : DS1302读取时间
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ds1302_read_time(void)
{
u8 i = 0;
for(i = 0;i < 7;i++)
{
gDS1302_TIME[i] = ds1302_read_byte(gRREAD_addr[i]);
}
}

ds1302.h

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

#include "AllHead.h"

sbit DS1302_CE = P3^5; //复位管脚
sbit DS1302_IO = P3^4; //数据管脚
sbit DS1302_SCLK = P3^6; //时钟管脚

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

void main( )
{
u8 time_buf[8]; //将读取的数据存储到一个缓存数组中,方便数据的处理与显示
ds1302_init(); //初始化DS1302
while(1)
{
ds1302_read_time(); //读取时间
//时
time_buf[0] = gsmg_code[gDS1302_TIME[2] / 16]; //取出小时的第一位,由于是BCD码格式,类似于十六进制的高四位和低四位,所以得 / 16
time_buf[1] = gsmg_code[gDS1302_TIME[2] % 16]; //取出小时的第二位
time_buf[2] = 0x40; //在数码管中表示横杠
//分
time_buf[3] = gsmg_code[gDS1302_TIME[1] / 16]; //取出分的第一位
time_buf[4] = gsmg_code[gDS1302_TIME[1] % 16]; //取出分的第二位
time_buf[5] = 0x40;
//秒
time_buf[6] = gsmg_code[gDS1302_TIME[0] / 16]; //取出秒的第一位
time_buf[7] = gsmg_code[gDS1302_TIME[0] % 16]; //取出秒的第二位
smg_display(time_buf,1);
}
}

主函数代码先初始化 DS1302 并设定好初始时间,进入 while 循环,读取 DS1302 时钟数据存储至全局变量数组gDS1302_TIME 中,最后将读取的数据转换为数码管可显示的段码数据并调用数码管显示函数显示时间。

在处理 DS1302 读取的数据时,取高低位是使用除16和取余16,并非之前的除 10 和取余 10。这是因为写入进 DS1302 时是 BCD 码,读取出的数据也是 BCD 码,而 BCD 码即是 4 位表示一个十进制数,类似于一个字节的十六进制数据的高 4 位和低 4 位一样,所以这里是除 16 和取余 16

AllHead.h

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

#include <reg52.h>
#include "intrins.h" //库函数
#include "smg.h"
#include "ds1302.h"

#endif

红外遥控

红外遥控介绍

(1) 红外线简介

人的眼睛能看到的可见光按波长从长到短排列,依次为红、橙、黄、绿、青、蓝、紫。其中红光的波长范围为 0.62~0.76μm;紫光的波长范围为 0.38~0.46μm。比紫光波长还短的光叫紫外线,比红光波长还长的光叫红外线。红外线遥控就是利用波长为 0.76~1.5μm 之间的近红外线来传送控制信号的。

(2) 红外遥控的原理

红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计算机系统中。

红外遥控通信系统一般由红外发射装置红外接收设备两大部分组成。

1) 红外发射装置

红外发射装置,也就是通常我们说的红外遥控器是由键盘电路、红外编码电路、电源电路和红外发射电路组成。红外发射电路的主要元件为红外发光二极管。它实际上是一只特殊的发光二极管;由于其内部材料不同于普通发光二极管,因而在其两端施加一定电压时,它便发出的是红外线而不是可见光。目前大量的使用的红外发光二极管发出的红外线波长为 940nm 左右,外形与普通发光二极管相同。红外发光二极管有透明的,还有不透明的。红外遥控器和红外发光二极管如下图所示:

通常红外遥控为了提高抗干扰性能和降低电源消耗,红外遥控器常用载波的方式传送二进制编码,常用的载波频率为 38kHz,这是由发射端所使用的 455kHz晶振来决定的。在发射端要对晶振进行整数分频,分频系数一般取 12,所以455kHz÷12≈37.9kHz≈38kHz。也有一些遥控系统采用 36kHz、 40 kHz、 56 kHz等,一般由发射端晶振的振荡频率来决定。所以,通常的红外遥控器是将遥控信号(二进制脉冲码)调制在 38KHz 的载波上,经缓冲放大后送至红外发光二极管,转化为红外信号发射出去的

二进制脉冲码的形式有多种,其中最为常用的是 NEC Protocol 的 PWM码(脉冲宽度调制)和 Philips RC-5 Protocol 的PPM 码脉冲位置调制码,脉冲串之间的时间间隔来实现信号调制)。如果要开发红外接收设备,一定要知道红外遥控器的编码方式和载波频率。开发板配套的红外遥控器使用的是 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 高电平组成),如果在一帧数据发送完毕之后,红外遥控器按键仍然没有放开,则发射连发码,可以通过统计连发码的次数来标记按键按下的长短或次数。

2) 红外接收设备

红外接收设备是由红外接收电路、红外解码、电源和应用电路组成。红外遥控接收器的主要作用是将遥控发射器发来的红外光信好转换成电信号,再放大、限幅、检波、整形,形成遥控指令脉冲,输出至遥控微处理器。成品红外接收头的封装大致有两种:一种采用铁皮屏蔽;一种是塑料封装。均有三只引脚,即电源正( VDD)、电源负(GND)和数据输出(VOUT)。其外观实物图如下图所示:

正对接收头的凸起处看,从左至右,管脚依次是1:VOUT,2:GND,3:VDD。由于红外接收头在没有脉冲的时候为高电平,当收到脉冲的时候为低电平,所以可以通过外部中断的下降沿触发中断,在中断内通过计算高电平时间来判断接收到的数据是 0 还是 1

红外接收模块电路

从上图中可知,红外接收头的输出管脚接至单片机 P3.2 管脚上,为了保证红外接收头输出管脚默认为高电平,需外接一个 10K 上拉电阻。

红外遥控实验

数码管上显示红外解码遥控器键值

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
/*******************************************************************************
* 函 数 名 : smg_display
* 函数功能 : 动态数码管显示
* 输 入 : dat:要显示的数据
location:从左开始第几个位置开始显示,范围1-8
* 输 出 : 无
*******************************************************************************/
void smg_display(u8 dat[], u8 location)
{
u8 i = 0;
u8 location_temp = location - 1;
for(i = location_temp; i < 8; i++)
{
switch(i)
{
case 0:
LSC = 1;
LSB = 1;
LSA = 1;
break; //Y7 LED8 板子从左边数第一个数码管
case 1:
LSC = 1;
LSB = 1;
LSA = 0;
break; //Y6 LED7 板子从左边数第二个数码管
case 2:
LSC = 1;
LSB = 0;
LSA = 1;
break; //Y5 LED6 板子从左边数第三个数码管
case 3:
LSC = 1;
LSB = 0;
LSA = 0;
break; //Y4 LED5 板子从左边数第四个数码管
case 4:
LSC = 0;
LSB = 1;
LSA = 1;
break; //Y3 LED4 板子从左边数第五个数码管
case 5:
LSC = 0;
LSB = 1;
LSA = 0;
break; //Y2 LED3 板子从左边数第六个数码管
case 6:
LSC = 0;
LSB = 0;
LSA = 1;
break; //Y1 LED2 板子从左边数第七个数码管
case 7:
LSC = 0;
LSB = 0;
LSA = 0;
break; //Y0 LED1 板子从左边数第八个数码管
}
SMG_A_DP_PORT = dat[i - location_temp]; //传送段选数据
delay_10us(100);//延时一段时间,等待显示稳定
SMG_A_DP_PORT = 0x00; //消影
}
}

smg.h

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

#include "AllHead.h"
#define SMG_A_DP_PORT P0 //宏定义数码管P0端口

sbit LSA = P2^2; //将P2.2管脚定义为LSA
sbit LSB = P2^3; //将P2.3管脚定义为LSB
sbit LSC = P2^4; //将P2.4管脚定义为LSC

void smg_display(u8 dat[], u8 location);

extern u8 gsmg_code[17]; //允许外部进行调用就得用extern

#endif

ired.c

使用外部中断 0 来解码红外遥控数据,所以需初始化配置外部中断0。

初始化外部中断后,中断就已经开启了,当 P32 引脚来一个下降沿,就会触发一次中断,在中断内我们可以计算高电平时间,通过高电平时间判断是否进入引导码及数据 0 和 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
#include "AllHead.h"

u8 gired_data[4];

/*******************************************************************************
* 函 数 名 : ired_init
* 函数功能 : 红外端口初始化函数,外部中断0配置
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ired_init(void)
{
IT0 = 1; //下降沿触发
EX0 = 1; //打开中断0允许
EA = 1; //打开总中断
IRED = 1; //初始化端口
}

void ired() interrupt 0 //外部中断0服务函数
{
u16 time_cnt = 0;
u8 i = 0, j = 0;
u8 ired_high_time = 0;

if(IRED == 0)
{
time_cnt = 1000;
while((!IRED) && (time_cnt)) //等待引导信号9ms低电平结束,若超过10ms强制退出,若高电平则跳出循环
{
delay_10us(1); //延时约10us 10us*1000 = 10000us --> 10ms
time_cnt--;
if(time_cnt == 0) return;
}
if(IRED) //引导信号9ms低电平已过,进入4.5ms高电平
{
time_cnt = 500;
while((IRED) && (time_cnt)) //等待引导信号4.5ms高电平结束,若超过5ms强制退出
{
delay_10us(1); //延时约10us 10us*500 = 5000us --> 5ms
time_cnt--;
if(time_cnt == 0) return;
}
for(i = 0; i < 4; i++) //循环4次,读取4个字节数据(地址码、地址反码、控制码、控制反码)
{
for(j = 0; j < 8; j++) //循环8次读取每位数据即一个字节
{
time_cnt = 600;
while((!IRED) && (time_cnt)) //等待数据1或0前面的0.56ms结束,若超过6ms强制退出
{
delay_10us(1); //延时约10us 10us*600 = 6000us --> 6ms
time_cnt--;
if(time_cnt == 0) return;
}
while(IRED) //等待数据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
11
12
13
14
15
#ifndef _IRED_H_
#define _IRED_H_

#include "AllHead.h"

//管脚定义
sbit IRED = P3^2; //管脚定义

//声明变量
void ired_init(void);

//函数声明
extern u8 gired_data[4];

#endif

Delay.c

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

/* -------------------------------- begin ------------------------------ */
/**
* @函数名: delay_us
* @参数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
10
11
#ifndef _DELAY_H
#define _DELAY_H

#include "AllHead.h"

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

void delay_10us(u16 ten_us);

#endif

main.c

主函数代码首先初始化红外,进入while 循环将红外解码数据转换为数码管段码数据,然后在数码管上显示。

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

void main( )
{
u8 ired_buf[3];
ired_init(); //红外初始化
while(1)
{
ired_buf[0] = gsmg_code[gired_data[2] / 16 ]; //将控制码高4位转换为数码管段码
ired_buf[1] = gsmg_code[gired_data[2] % 16 ]; //将控制码低4位转换为数码管段码
ired_buf[2] = 0x76; //显示H的段码
smg_display(ired_buf, 6);
}
}

AllHead.h

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

#include <reg52.h>
#include "Delay.h"
#include "smg.h"
#include "ired.h"

#endif

注意:红外接收头的凸起位置需对到 PCB 板丝印凸出位置,否则功能无法正常实现。