控制LED模块的亮灭
根据原理图可知控制LED模块亮灭的管脚为P2端口
P2端口的8个引脚分别对应八个LED灯
引脚输出0时,对应的LED灯亮起来,引脚输出1时,对应的LED灯熄灭
例如我们要点亮P2.0对应的LED灯,那么我们可以让P2 = 1111 1110
但是不能直接写二进制,要写成十六进制的,并加上0x或0X前缀表示这是一个十六进制的数
那么点亮P2.0对应的LED就可以写为:P2 = 0xfe;
如果添加的是<REGX52.H>头文件,那么我们可以单独控制一个引脚,例如我要点亮P2.0引脚对应的LED,可以这样写:P2_0 = 0;
点亮一个LED
示例:
// 点亮一个LED灯
#include <REGX52.h>
void main()
{
P2 = 0xfe;
// P2_0 = 0; // 这样也可以点亮一个LED灯
}
延时函数
想要让LED灯进行亮灭操作,我们加一个延时操作就行了
示例
// 控制程序暂停的时间,单位毫秒
void delay(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
// 每循环一次就是一毫秒
i=2;
j=239;
do
{
while(--j);
}while(--i);
ms--;
}
}
LED闪烁
#include <REGX52.H>
void delay(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
// 每循环一次就是一毫秒
i=2;
j=239;
do
{
while(--j);
}while(--i);
ms--;
}
}
void main()
{
while(1)
{
P2 = 0xfe;
delay(50);
P2 = 0xff;
delay(50);
}
}
LED流水灯
示例
#include <REGX52.H>
// 延时函数
void delay(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
// 每循环一次就是一毫秒
i=2;
j=239;
do
{
while(--j);
}while(--i);
ms--;
}
}
int i;
// 把八个小灯点亮的顺序放到一个数组中
int s[] = {0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};
void main()
{
while(1)
{
for(i = 0;i<8;i++)
{
// 根据i取出数组中数,来点亮不同的LED
P2 = s[i];
delay(50);
}
}
}
控制独立按钮
根据原理图可知,P3端口的前四个引脚分别对应四个独立按钮
当按钮被按下时,按钮等于0,松开按钮等于1
按键的抖动
对于机械开关,当机械触点断开、闭合时,由于机械点的弹性作用,一个开关在闭合时不会马上稳定的接通,在断开时也不会一下子断开,都会有一连串的抖动
抖动的时间大概在5~10毫秒,所以操作按钮时要加上一定的延时,让按键稳定下来
例如:
#include <REGX52.H>
// 延时函数
void delay(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
// 每循环一次就是一毫秒
i=2;
j=239;
do
{
while(--j);
}while(--i);
ms--;
}
}
void main()
{
while(1)
{
// 当独立按钮第一个按钮被按下时,P3_1等于0
if(P3_1 == 0)
{
// 按键抖动的时间,不作任何事
delay(20);
// 等按键稳定下来,再执行其他操作
...
}
}
}
按键按下点亮LED
示例
#include <REGX52.H>
// 延时函数
void delay(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
// 每循环一次就是一毫秒
i=2;
j=239;
do
{
while(--j);
}while(--i);
ms--;
}
}
void main()
{
while(1)
{
// 当独立按钮第一个按钮被按下时,P3_1等于0
if(P3_1 == 0)
{
// 按键闭合抖动的时间,不作任何事,等他稳定下来
delay(20);
// 当P3_1还等于0时,说明按键还没有松开,进入死循环,直到按钮松开,结束循环
while(P3_1 == 0);
// 按键断开抖动的时间
delay(20);
// 点亮一个LED灯
P2_0 = 0;
}
}
}
独立按钮控制LED显示二进制
效果:通过LED用二进制的方式计数,按一下按键表示加一,亮着的灯表示1,熄灭的灯表示0
示例
#include <REGX52.H>
void delay(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
i=2;
j=239;
do
{
while(--j);
}while(--i);
ms--;
}
}
// unsigned char类型的存储范围为0~255,刚好是8位二进制的范围,所以用这个类型来存储8位二进制的数
unsigned char led_num = 0;
void main()
{
while(1)
{
if(P3_1==0)
{
delay(20);
while(P3_1==0);
delay(20);
// 按键每按一下LED灯要展示的二进制数就加一
led_num++;
// 因为要展示的效果是亮为1,灭为0,与实际点亮LED灯输入相反,所以进行取反
P2 = ~led_num;
}
}
}
独立按键控制LED移位
实验现象:按一下第一个独立按键,小灯向前移一位,按第二个独立按键小灯向后移一位
示例
#include <REGX52.H>
// 延时函数
void delay(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
i=2;
j=239;
do
{
while(--j);
}while(--i);
ms--;
}
}
void main()
{
// 要移位的距离
int led_num = 0;
// 初始化,一开始就点亮第一个LED灯
P2=~0x01;
while(1)
{
if(P3_1==0)
{
delay(20);
while(P3_1==0);
delay(20);
// 当移位的距离达到7时,说明移到最末端了,再移位要从第一个位置开始
if(led_num==7)
{
led_num = -1;
}
// 要移位的距离自增
led_num++;
P2 = ~(0x01<<led_num);
}
if(P3_0==0)
{
delay(20);
while(P3_0==0);
delay(20);
// 与上面同理,不过方向相反
if(led_num==0)
{
led_num=8;
}
led_num--;
P2=~(0x01<<led_num);
}
}
}
控制数码管
数码管介绍
LED数码管:数码管是一种简单、廉价的显示器,是由多个发光二极管封装在一起组成“8”字型的器件
单个数码管
有两种链接方式,共阴极(上面的原理图)和共阳极(下面的原理图)
假设想在数码管上显示数字6,那么我们要点亮的LED有:A、C、D、E、F、G,其中DP表示小数点,需要展示小数时再点亮
共阴极的输出电平为:1011 1110
共阳极的输出电平为:0100 0001
多个数码管
控制A、B、C、D等等这些单个LED的引脚称为位选
而控制一个数码管亮灭的引脚称为段选
这样连接的坏处是不能同时在四个数码管上展示不一样的数字,因为四个位选端都连到一起了,只能展示一样的内容。好处是节省了IO口,不用接这么多引脚
拿上面的共阴极原理图举例,我们想让第二个数码管显示数字1,其他熄灭时,只需要让9输出低电平,其他输出高电平,就是:1011
在让位选端输出:0110 0000
如果我们想在同一时间展示不同数字,可以利用人眼视觉暂留和LED余晖,一次性只点亮一个数码管,然后熄灭再点亮下一个要展示的数码管,再熄灭再点亮下一个数码管,就这样不停的来回,造成几个数码管同时显示不同数字的视觉错觉
数码管原理图
COM就是公共端(数码管定义图中的DIG)
共阴极的连接方式(亮:0;灭1),位选端就要使用阳码(亮:1;灭:0)
译码器原理图
38译码器,三根线控制八个引脚
译码器就是控制上面数码管亮灭的,例如我要LED3数码管显示数字,那么我们就要控制译码器输出:1111 1011
如何控制译码器的输出:只需要使用P2口的三个引脚就够了,P2.2、P2.3、P2.4
如上原理图,C、B、A输出0或1组成的二进制数,在转换成十进制。例如001转十进制为1,那么Y1就输出0,其都输出1,也就是选中了LED2。这样三个io口就可以输出8个不同信息
C、B、A | Y | LED |
---|---|---|
000 | 0 | LED1 |
001 | 1 | LED2 |
010 | 2 | LED3 |
011 | 3 | LED4 |
100 | 4 | LED5 |
101 | 5 | LED6 |
110 | 6 | LED7 |
111 | 7 | LED8 |
静态数码管
现象:在第二个数码管位置展示数字1
示例:
#include <REGX52.H>
void main()
{
// 通过译码器点亮第二个数码管
P2_4=0;
P2_3=0;
P2_2=1;
// 输出数字1,0000 0110
P0=0x06;
while(1);
}
把点亮数码管和展示的数字封装成函数
#include <REGX52.H>
// 数码管输出0~9
unsigned char nixie_table[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
// 第一个参数是要点亮第几个数码管,第二个参数是要输出的数字
void nixie(unsigned char location, num)
{
switch(location)
{
// 点亮第一个数码管
case 1:P2_4=0;P2_3=0;P2_2=0;break;
// 点亮第二个数码管
case 2:P2_4=0;P2_3=0;P2_2=1;break;
case 3:P2_4=0;P2_3=1;P2_2=0;break;
case 4:P2_4=0;P2_3=1;P2_2=1;break;
case 5:P2_4=1;P2_3=0;P2_2=0;break;
case 6:P2_4=1;P2_3=0;P2_2=1;break;
case 7:P2_4=1;P2_3=1;P2_2=0;break;
case 8:P2_4=1;P2_3=1;P2_2=1;break;
}
// 根据传入的num从数组中取出对应的十六进制数,然后展示在数码管上
P0=nixie_table[num];
}
void main()
{
nixie(3,1);
while(1);
}
消影
-
在上面知道了数码管动态显示的原理,就是不停的扫描。先通过38译码器进行段选,在通过P2引脚进行位选,再又通过译码器进行段选,再通过P2引脚进行位选,不停重复就实现同时显示多个数码管
-
也就是一个段选匹配一个位选。假设把段选当作0,位选当作1,在显示多个数码管的时候如下(01,01,01,...)
-
但是这样做会让后一个段选完成后,匹配上前一个的位选,这样就导致显示错乱,变成了类似(0,10,10,1,...)这样
-
所以要再位选完成后延时一会(例如1毫秒),在把位选清零(熄灭LED),就不会出现错乱的现象了
动态数码管
需要进行消影
例如下面代码直接烧录到单片机中运行会出现错乱的现象(可以自己观察一下),需要对子函数进行修改
#include <REGX52.H>
unsigned char nixie_table[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void nixie(unsigned char location, num)
{
switch(location)
{
case 1:P2_4=0;P2_3=0;P2_2=0;break;
case 2:P2_4=0;P2_3=0;P2_2=1;break;
case 3:P2_4=0;P2_3=1;P2_2=0;break;
case 4:P2_4=0;P2_3=1;P2_2=1;break;
case 5:P2_4=1;P2_3=0;P2_2=0;break;
case 6:P2_4=1;P2_3=0;P2_2=1;break;
case 7:P2_4=1;P2_3=1;P2_2=0;break;
case 8:P2_4=1;P2_3=1;P2_2=1;break;
}
P0=nixie_table[num];
}
void main()
{
while(1)
{
nixie(1,1);
nixie(2,2);
nixie(3,3);
}
}
修改后的子函数代码
void nixie(unsigned char location, num)
{
switch(location)
{
case 1:P2_4=0;P2_3=0;P2_2=0;break;
case 2:P2_4=0;P2_3=0;P2_2=1;break;
case 3:P2_4=0;P2_3=1;P2_2=0;break;
case 4:P2_4=0;P2_3=1;P2_2=1;break;
case 5:P2_4=1;P2_3=0;P2_2=0;break;
case 6:P2_4=1;P2_3=0;P2_2=1;break;
case 7:P2_4=1;P2_3=1;P2_2=0;break;
case 8:P2_4=1;P2_3=1;P2_2=1;break;
}
P0=nixie_table[num];
// 先延迟一毫秒
delay(1);
// 再让P0口归零,也就是熄灭所有LED
P0=0x00;
}
在数码管上同时显示123
让数码管不停的扫描
#include <REGX52.H>
unsigned char nixie_table[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
// 延时函数
void delay(unsigned int ms)
{
unsigned char i, j;
while(ms)
{
// 每循环一次就是一毫秒
i=2;
j=239;
do
{
while(--j);
}while(--i);
ms--;
}
}
void nixie(unsigned char location, num)
{
switch(location)
{
// 我为了看起来舒服一点把顺序
case 8:P2_4=0;P2_3=0;P2_2=0;break;
case 7:P2_4=0;P2_3=0;P2_2=1;break;
case 6:P2_4=0;P2_3=1;P2_2=0;break;
case 5:P2_4=0;P2_3=1;P2_2=1;break;
case 4:P2_4=1;P2_3=0;P2_2=0;break;
case 3:P2_4=1;P2_3=0;P2_2=1;break;
case 2:P2_4=1;P2_3=1;P2_2=0;break;
case 1:P2_4=1;P2_3=1;P2_2=1;break;
}
P0=nixie_table[num];
// 消影
delay(1);
P0=0x00;
}
void main()
{
while(1)
{
nixie(1,1);
nixie(2,2);
nixie(3,3);
}
}
模块开发
把各个模块的代码放到不同的.c文件中,在.h文件中提供可调用的函数声明,其他.c文件想使用其中代码时,只需要#include即可。这样大大提高了代码的可阅读性、和维护性和可移植性
预编译
预编译 | 意义 |
---|---|
#include | 加载头文件 |
#define | 宏定义 |
#ifnedf | 如果没有定义过则加载下面的代码(防止头文件重复包含) |
#endif | 与#ifndef、#if匹配,组成类似 ”花括号“ 的东西 |
此外还有:#ifdef、#if、#else、#elif、#undef
模块化示例
例如:我要将上一个写的示例(动态数码管)进行模块化开发
先模块化延时函数
创建:delay的头文件
在头文件中进行预处理,并声明函数
让头文件显示在工程目录中
添加源文件:delay.c
在源文件中写函数的定义
在把nixie函数也放入进行模块化编程,步骤同上
头文件
源文件
最后再main函数中实现操作
LCD1602调试工具
使用LCD1602作为调试窗口,提供类似printf函数的功能,可以实时的观察单片机内部数据的变换情况,便于调试和演示
驱动文件及使用方法
在这里面下载.c和.h文件,里面有事先写好的LCD1602驱动文件,等后面学会如何操作LCD1602后可以自己写一个驱动文件
链接:https://pan.baidu.com/s/1DE9PlJ9aCSvuMEHFRh1E9g?pwd=1234
提取码:1234
下载好后把两个文件放到工程目录中(跟main文件同级)
现在先暂时只需要知道怎么使用事先提供的驱动来操作LCD1602显示屏即可
函数 | 作用 |
---|---|
LCD_Init(); | 初始化 |
LCD_ShowChar(line, column, char); | 显示一个字符 |
LCD_ShowString(line, column, string); | 显示字符串 |
LCD_ShowNum(line, column, num, len); | 显示无十进制数 |
LCD_ShowSingendNum(line, column, num, len); | 显示有符号的十进制数 |
LCD_ShowHexNum(line, column, hex_num, len); | 显示十六进制数 |
LCD_ShowBinNum(line, column, bin_num, len); | 显示二进制数 |
参数解释:
-
line:行,从第几行开始显示
-
column:列,从第几列开始显示
-
char:字符类型
-
string:字符串类型
-
num、hex_num、bin_num:都表示数字,可以传十进制数也可以传十六进制数,但是不可以传二进制数(这样写只是为了强调要显示什么进制的数)
-
len:数字显示的长度(例如数字传了89,长度传了4,那么在屏幕上的显示是0089;如果长度传的1,则屏幕上只显示9)
示例
#include <REGX52.H>
#include "LCD1602.h"
void main()
{
LCD_Init();
// 在第一行第一个位置显示 A
LCD_ShowChar(1,1,'A');
// 在第一行第三个位置显示 hello
LCD_ShowString(1,3,"hello");
// 在第一行第九个位置显示无符号数字 123
LCD_ShowNum(1,9,123,3);
// 在第一行第十三个位置显示有符合数字 -66(长度不包含符合)
LCD_ShowSignedNum(1,13,-66,2);
// 在第二行第一个位置显示23的十六进制数
LCD_ShowHexNum(2,1,23,2);
// 在第二行第四个位置显示11的二进制数
LCD_ShowBinNum(2,4,11,8);
while(1){}
}
效果如图
矩阵键盘
在键盘中按键数量较多时,为了减少io口的占用,通常将按键排列成矩阵形式
采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态
扫描的概念
数码管扫描(输出扫描)
显示第一位--->显示第二位--->显示第三位--->......,然后快速循环这个过程,最终实现所有数码管同时显示的效果
矩阵键盘扫描(输入扫描)
读取第一行(列)--->读取第二行(列)--->读取第三行(列)--->......,然后快速循环这个过程,最终实现所有按键同时检测的效果
这样可以大大的节省io口
例如我们的1080p显示器,就拥有1920乘1080,2073600个像素点,显示彩色画面需要用到三基色:红、绿、蓝,也就是需要2073600乘3,6220800个LED在显示屏上。让一个端口对应一个LED显然不太现实,所以就要 用到扫描的概念;一个端口控制一行或者一列,那么1080p显示器就只需要1080+1920=3000,再乘3显示彩色画面,也就是只需要9000个端口就可以了。扫描的过程就由我们的显卡来完成
原理图
逐行扫描
P1.7、P1.6、P1.5、P1.4分别连接四个按键,当开始扫描第一行(s1、s2、s3、s4)时,令P1.7=0,其他三个等于1,也就是0111 ;扫描第二行输出1011,以此类推
在扫描行的时候就是判断P1.3、P1.2、P1.1、P1.0四个端口是否有输入0的,有输入0的说明对应的按键被按下了
结合上面可知当我们按下了第一行的第一个按键(s1)时,则输出0111 0111
逐列扫描
与逐行相反,先从P1.3、P1.2、P1.1、P1.0开始一列一列扫描,再判断P1.7~P1.4是否有输入0的,有则说明按下了对应的按键
注意:该电路板因为引脚冲突的问题,使用逐行扫描的方式时蜂鸣器也会响,所以最好使用逐列扫描
显示按键序号
实验现象:矩阵按键共有16个按键,从从左到右从上到下,依次排序为1~16,每按一下键位就在LCD屏幕上显示对应序号
使用模块化编程。delay.h\c
和LCD1602.h\c
文件在前面有说明,这里添加文件matrix_key.h
和matrix_key.c
文件用来存放矩阵按键相关的函数
结构如图
方法一:
- 使用行列式扫描方法,检测矩阵按键是否按下,按下则返回对应键值
- 输 出 : key_value:1-16,对应S1-S16键
matrix_key.h
#ifndef __matrix_key_h__
#define __matrix_key_h__
unsigned char matrix_key_num();
#endif
matrix_key.c
#include <REGX52.H>
#include "delay.h"
unsigned char matrix_key_num()
{
// 显示在LCD上的数
static unsigned char key_value=0;
// 先初始化P1,准备开始扫描
P1=0xff;
// 开始扫描第一列,根据原理图可知,P1.3是第一列。
P1_3=0;
// 开始判断第一列第一个按键是否被按下
if(P1_7==0){delay(20);while(P1_7==0);delay(20);key_value = 1;}
// 开始判断第一列第二个按键是否被按下,后面以此类推
if(P1_6==0){delay(20);while(P1_6==0);delay(20);key_value = 5;}
if(P1_5==0){delay(20);while(P1_5==0);delay(20);key_value = 9;}
if(P1_4==0){delay(20);while(P1_4==0);delay(20);key_value = 13;}
P1=0xff;
P1_2=0;
if(P1_7==0){delay(20);while(P1_7==0);delay(20);key_value = 2;}
if(P1_6==0){delay(20);while(P1_6==0);delay(20);key_value = 6;}
if(P1_5==0){delay(20);while(P1_5==0);delay(20);key_value = 10;}
if(P1_4==0){delay(20);while(P1_4==0);delay(20);key_value = 14;}
P1=0xff;
P1_1=0;
if(P1_7==0){delay(20);while(P1_7==0);delay(20);key_value = 3;}
if(P1_6==0){delay(20);while(P1_6==0);delay(20);key_value = 7;}
if(P1_5==0){delay(20);while(P1_5==0);delay(20);key_value = 11;}
if(P1_4==0){delay(20);while(P1_4==0);delay(20);key_value = 15;}
P1=0xff;
P1_0=0;
if(P1_7==0){delay(20);while(P1_7==0);delay(20);key_value = 4;}
if(P1_6==0){delay(20);while(P1_6==0);delay(20);key_value = 8;}
if(P1_5==0){delay(20);while(P1_5==0);delay(20);key_value = 12;}
if(P1_4==0){delay(20);while(P1_4==0);delay(20);key_value = 16;}
return key_value;
}
方法二:
- 使用线翻转扫描方法,检测矩阵按键是否按下,按下则返回对应键值
- 输 出 : key_value:1-16,对应S1-S16键
matrix_key.h
#ifndef __matrix_key_h__
#define __matrix_key_h__
unsigned char matrix_key_num();
#endif
matrix_key.c
#include <REGX52.H>
#include "delay.h"
unsigned char matrix_key_num()
{
static unsigned char key_value=0;
P1=0x0f;//给所有行赋值0,列全为1
if(P1!=0x0f)//判断按键是否按下
{
delay(20);//消抖
if(P1!=0x0f)
{
//测试列被按下的位置
P1=0x0f;
switch(P1)//保存行为0,按键按下后的列值
{
case 0x07: key_value=1;break;
case 0x0b: key_value=2;break;
case 0x0d: key_value=3;break;
case 0x0e: key_value=4;break;
}
//测试行被按下位置
P1=0xf0;
switch(P1)//保存列为0,按键按下后的键值
{
case 0x70: key_value=key_value;break;
case 0xb0: key_value=key_value+4;break;
case 0xd0: key_value=key_value+8;break;
case 0xe0: key_value=key_value+12;break;
}
while(P1!=0xf0);//等待按键松开
}
}
else
key_value=0;
return key_value;
}
电子密码锁
实验现象:输入六位密码,密码正确时显示在LCD上显示yes,错误显示no。其中,按键s1s9分别代表数字19,按键s10代表数字0;按键s11代表确定密码,开始判断密码是否正确;按键s12表示退格,重新输入上一次输入的密码;s13~s16不作回应
注意:该实验的头文件:delay.h
、LCD1602.h
、matrix_key.h
都基于上一个实验,就main.c文件改变
代码示例
#include <REGX52.H>
#include <string.h> // 包含memset方法的头文件
#include "LCD1602.h"
#include "matrix_key.h"
#include "delay.h"
// 矩阵按键的键码
unsigned char key;
// 用户输入的密码存放到数组中
unsigned char user_password[6];
// 记录用户输入的密码个数,最多六位
unsigned int count = 0;
void main()
{
// 初始化LCD及密码
LCD_Init();
LCD_ShowString(1,1,"password:");
LCD_ShowString(2,1,"------");
while(1)
{
// 扫描键盘是否有按键按下
key = matrix_key_num();
if(key)
{
// 当按键键位小于或等于10时,说明输入的是密码(等于10时认为输入0)
if(key <= 10)
{
// count限制输入的密码个数,最多六位密码
if(count<6)
{
// 将用户的密码存放到数组中
user_password[count] = key%10;
// 根据用户输入的次数,把用户输入的密码显示在LCD对应的位置上
LCD_ShowNum(2,count+1,user_password[count],1);
count++;
}
}
// 当键码等于s11时,表示确定密码
if(key == 11)
{
// 验证用户输入的密码是否等于我们设定的密码
if(user_password[0] == 1 &&
user_password[1] == 2 &&
user_password[2] == 3 &&
user_password[3] == 4 &&
user_password[4] == 5 &&
user_password[5] == 6)
{
// 判断为真说明验证通过,打印yes,延迟1.5秒后初始化密码、显示、输入次数
LCD_ShowString(2,1,"YES ");
delay(1500);
count = 0;
// memset方法是把数组元素全部置空;sizeof统计数组长度
memset(user_password, 0, sizeof(user_password));
LCD_ShowString(2,1,"------");
}
else
{
// 判断为真说明验证通过,打印no,延迟1.5秒后初始化密码、显示、输入次数
LCD_ShowString(2,1,"NO ");
delay(1500);
count = 0;
// 将数组置空
memset(user_password, 0, sizeof(user_password));
LCD_ShowString(2,1,"------");
}
}
// 当键码等于s12时,取消上一次的输入
if(key == 12)
{
// 把LCD上显示的最后一位密码覆盖为0,且密码输入次数自减
LCD_ShowChar(2,count,'-');
if(count!=0)
{
count--;
}
}
}
}
}
定时器
定时器介绍
51单片机的定时器属于单片机芯片的内部资源,其电路的连接和运转均在单片机内部完成
作用:
- 用于计时系统,可实现软件记时,或者使程序每隔一固定时间完成一项操作
- 代替长时间的delay,提高CPU的运行效率和处理速度(使用delay会一直占用CPU,干不了其他事)
定时器个数
- 共有三个定时器(T0、T1、T2),T0、和T1、与传统51单片机兼容,但是T2是此型号(STC89C52)单片机增加的资源
- 注意:定时器的资源和单片机的型号是关联的,不同型号可能会有不同的操作方式,但是一般来说T0和T1的操作方法都是一样的
工作原理
内部在单片机内部就像一个小闹钟一样,根据时钟的输出信号,每隔一段时间,计数单元的数值就加一,当计数单元增加到设定的次数时,计数单位就会向中断系统发出中断申请,产生提示,使程序跳转到中断服务函数中执行
定时器工作模式
STC89C52的T0和T1均有四种工作模式:
- 模式0:13位定时器/计数器
- 模式1:16位定时器/计数器(常用)
- 模式2:8位自动重装模式
- 模式3:两个8位计数器
在下面详细介绍模式1
模式1:16位定时器/计数器
原理图
定时器可以分为三个部分:时钟、计数单元、中断
GATE、TR0、TF0、TL0、TH0等等都是寄存器,在代码中配置0或1来控制硬件的运行方式
时钟部分
时钟有两个来源:
-
SYSclk:系统时钟,即晶振周期,本开发板上的晶振为12MHz
-
T0Pin:外部引脚(单片机上40个引脚的其中之一,可以看单片机原理图)提供脉冲
使用系统时钟时会进行分频
连接 MCU in 12T mode
这个电路时进行12分频,输出的频率就是1MHz,一个周期就是1微秒,也就是每隔1微秒后面的计数器就会加一
连接MCU in 6T mode
进行6分频,每隔2微秒计数器加一
C/T:
-
根据我们的实际使用情况在代码中配置寄存器,连接对应的电路
-
字母上面有横杠的表示低电平(0),没有横杠的表示高电平(1)
-
也就是如果配置为1,就连在下面的电路上使用外部引脚的脉冲;如果配置为0,就连上面的电路使用系统时钟
计数单元
-
TL0和TH0:TL0和TH0是一个十六位的计数器,TH表示高8位,TL表示低8位(后面的0表示定时器的名字:T0定时器)。这个计数器最大只能存65535这么大的数。
-
工作原理:左边的时钟会给计数器提供脉冲,每来一个脉冲就计数器加一,当加到最大值65535时就会产生溢出,并且计数器归零
-
非门:取反
-
或门:有1就输出1,全为0才输出0
-
与门:有0就输出0,全为1才输出1
-
GATE:门控端
根据原理图可知
- 当GATE配置为0时,经过 非门 变为1,那么不管INT0的引脚输入什么都会经过 或门 后输出1,只有当TR0置1才能打开定时器/计数器。由TR0全权控制是否打开定时器/计数器
- 当GATE配置为1时,经过 非门 变为0,那么只有当INT0的引脚输出1,且TR0置1才能打开定时器/计数器,由两个地方控制定时器/计数器的开关
中断部分
计数器溢出后TF0置1,然后向中断系统申请中断
Interrupt:表示中断系统的电路,如下图
中断系统电路
- 传统51单片机的中断系统
- STC89C51RC系列单片机的中断系统
模式2:8位自动重装
原理图
在模式2中定时器只有八位参与计数,当定时器低八位(如TL1)计数溢出时,单片机自动把存在TH1中的值装进TL1,继续进行定时计数,这就完成了八位自动重装。与模式1相比,不需要在中断程序中对TL1再赋值,只需在初始化时,对TL1和TH1赋相同的值就行了。一般在单片机串行通信编程时才用到模式2.
中断系统简介
中断系统是为使CPU具有对外界紧急事件的事实处理能力而设置的
什么叫中断
概念
当CPU正在处理某件事情的时候,外界发生了紧急事件的请求,CPU会暂停当前的工作,转而去处理这个紧急事件,处理完了以后再回到原来被中断的地方,继续原理的工作,这样的过程叫中断
中断系统简介
中断流程图
中断源和中断优先级
请示CPU中断的请求源称为中断源,微型机的中断系统一般允许多个中断源,当几个中断源同时向CPU请示的时候,就存在CPU优先响应那个中断源的问题。所以规定每一个中断源都有一个优先级别,CPU总是先响应优先级最高的中断请求
中断嵌套
当CPU正在处理一个中断源的请求时(执行相应的中断服务程序),又来了一个中断源,且级别比当前处理的中断源级别还高,那么CPU就会暂停当前的中断服务程序,转而去处理优先级别更高的中断请求,等处理完后再回到优先级低的中断服务程序,这样的过程称为中断嵌套
STC89C52的中断资源
-
中断源个数:8个(外部中断0、定时器0中断、外部中断1、定时器1中断、串口中断、外部中断2、外部中断3)
-
中断优先级个数:4个
-
中断号:对应上面的中断源,从0~7(中断号0代表外部中断0;中断号1代表定时器0中断;后面的以此类推)
写一个函数,并在形参列表的后面加上Interrupt,再加上中断号,示例如下:
-
注意:不同单片机的型号的中断资源是不同的,例如中断源和中断优先级的个数不同等等
寄存器简介
- 寄存器是连接软硬件的媒介
- 在单片机中寄存器就是一段特殊的RAM存储器,一方面,寄存器可以存储和读取数据,另一方面,每一个寄存器背后都连接了一根导线,控制着电路的连接方式
- 寄存器相当于一个复杂机器的”操作按钮“
定时器/计数器相关寄存器
TCON:定时器/计数器控制寄存器
可位寻址(可以单独操作其中的一位)
TCON位定时器/计数器T0、T1的控制寄存器,同时也配置T0、T1溢出中断源和外部请求中断源等
符号 | 寄存器地址 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|
TCON | 88H | TF1 | TR1 | TF0 | TR0 | IE1 | IT1 | IE0 | IT0 |
结合定时器/计数器原理图来看
TMOD:定时器/计数器工作模式寄存器
注意:不可位寻址,在代码中配置的时候要写十六进制
例如:我要使用定时器0的16位定时器/计数器,配置的二进制为0000 0001,再转为十六进制即可
中断寄存器
结合上面中断部分的电路原理图图来看
暂时了解:IE、IP及IPH
IE:控制是否允许中断
例如我们需要使用定时器T0来触发中断时时,只需要把ET0和EA置1即可
IP和IPH:控制中断优先级
传统51单片机只具有两个中断优先级,STC89C51RC/RD系列单片机通过新增加的特殊功能寄存器(IPH/XICON)中的相应位,可将中断优先级设置为4个优先级。
如果只配置IP寄存器,那么中断优先级只有两级,与传统51单片机两个优先级完全兼容;想要使用两个以上的中断优先级就需要配置IP、IPH、XICON三个寄存器
IP
根据原理图可知,定时器0使用低优先级中断把PT0置0,高优先级把PT0置1
IPH和辅助中断寄存器XICON
使用定时器控制流水灯模式
寄存器配置要求
使用系统时钟,进行十二分频(在12MHz的情况下每一微秒加一),定时器T0,模式1(16位计数器),低优先级中断
根据上述要求在代码中进行寄存器的配置
- 配置定时器0的模式1:
需要对TMOD寄存器进行配置,且不可位寻址。把TMOD.1和TMOD.0分别配置为0、1即可,其他暂时配置0。因此
TMOD=0x01; // 0000 0001
- 允许计数器T0计数:
因为在TMOD中配置了GATE为0,所以就由TR0控制计数器是否允许计数,且TCON寄存器可位寻址,因此
TR0=1;
- 将溢出标志位清零:
TCON可位寻址,因此
TF0=0;
- 允许控制中断:
在IE寄存器可位寻址,
ET0=1;
:允许定时器T0中断;EA=1;
:开放总中断
- 低优先级:
只设置一个中断,所以只配置IP即可,且可位寻址,因此
PT0=0;
- 设置计数器TL、TH的初始值
16位计数器的最大值为65535,也就是可以定时65535us。假设我想定时一秒,但是计数器的最大值也才65535微秒,所以可以让计数器从64536开始计数,溢出的时间刚好为1000微秒,也就是一毫秒,把这个过程循环1000次就实现了定时一秒的效果
在代码中给计数器赋初值为64536,十六进制数为:0xfc18,那么计数器中的高位
TH0=0xfc;
,低位TL0=0x18;
优化上面TMOD的赋值:
在上面直接让
TMOD=0x01;
,在只使用一个定时器T0的时候不会有问题,但是如果同时使用定时器T1和定时器T0就会导致TMOD的状态会被后一个定时器刷新,因为TMOD不可位寻址,只能以字节为单位进行赋值。例如配置定时器T0 的时候TMOD=0x01
,再配置一个定时器T1的时候TMOD=0x10
,这样定时器T1配置的TMOD就刷新了定时器T0的配置
使用与或式赋值法可以解决上面的问题
TMOD=TMOD&0xf0; // 把TMOD的第四位清零,高四位保持不变
TMOD=TMOD|0x01; // 把TMOD的最低位置1,高四位保持不变
通过上面两行代码,可以只改变低四位而不影响高四位,想要改变其他位也是同理
代码示例:
初始化定时器的这个函数可以单独封装到一个文件中,方便以后使用,文件名:timer0_mode1_init
#include <REGX52.H>
// 延时函数,前面有讲
#include "delay.h"
// 初始化定时器T0的模式1
void timer0_mode1_init()
{
// 配置定时器
TMOD &= 0xf0; // 把TMOD的第四位清零,高四位保持不变
TMOD |= 0x01; // 把TMOD的最低位置1,高四位保持不变
// 将溢出标志清零
TF0 = 0;
// 允许中断控制
TR0 = 1;
// 初始化计数器:64536
TH0 = 0xfc;
TL0 = 0x18;
// 允许定时器T0中断
ET0 = 1;
// 打开总中断
EA = 1;
// 设置优先级:低优先级
PT0 = 0;
}
// 记录流水灯移动的方向,等于0向右移,等于1向左移
int direction;
void main()
{
timer0_mode1_init();
// 默认点亮第一个LED灯
P2=0xfe;
while(1)
{
// 独立按键第一个按键按下后流水灯向右移
if(P3_1==0)
{
delay(20);
while(P3_1==0);
delay(20);
direction = 0;
}
// 独立按键第二个按键按下后流水灯向左移
if(P3_0==0)
{
delay(20);
while(P3_0==0);
delay(20);
direction = 1;
}
}
}
// 流水灯函数
void flow_light(int direction)
{
// 流水灯向右移
if(direction == 0)
{
if(P2==0x7f)
{
P2=0xfe;
}
else
{
P2=~P2;
P2<<=1;
P2=~P2;
}
}
// 流水灯向左移
if(direction == 1)
{
if(P2==0xfe)
{
P2=0x7f;
}
else
{
P2=~P2;
P2>>=1;
P2=~P2;
}
}
}
// 中断函数,Interrupt 1就是中断号
void Timer0() interrupt 1
{
// 记录中断次数
static unsigned int T0_count;
// 每触发一次中断都要重新配置计数器的初始值
TH0 = 0xfc;
TL0 = 0x18;
// 中断次数自增
T0_count++;
// 当触发了1000次中断说明时间过去了一秒
if(T0_count == 1000)
{
T0_count=0;
// 调用流水灯函数,并把移动方向告诉他
flow_light(direction);
}
}
使用定时器实现时钟的功能
代码示例
#include <REGX52.H>
#include "delay.h"
// 在上一个案例中把初始化时钟的函数封装到文件里
#include "timer0_mode1_init.h"
#include "LCD1602.h"
unsigned int secondes, minutes, hour;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"CLOCK");
LCD_ShowString(2,1," : :");
timer0_mode1_init();
while(1)
{
// 按一下独立按键的第一个按钮暂停时钟,再按一下时钟开始计数,以此往复
if(P3_1==0)
{
delay(20);
while(P3_1==0);
delay(20);
// 对总中断的状态取反
EA=~EA;
}
// 秒钟走到60后归零,分针进一
if(secondes == 60)
{
secondes=0;
minutes++;
}
// 分钟走到60后归零,时针进一
if(minutes==60)
{
minutes=0;
hour++;
}
// 时钟走到24归零
if(hour==24)
{
hour=0;
}
// 显示时间,时、分、秒
LCD_ShowNum(2,1,hour,2);
LCD_ShowNum(2,4,minutes,2);
LCD_ShowNum(2,7,secondes,2);
}
}
// 中断函数,Interrupt 1就是中断号
void Timer0() interrupt 1
{
// 记录中断次数
static unsigned int T0_count;
// 每触发一次中断都要重新配置计数器的初始值
TH0 = 0xfc;
TL0 = 0x18;
// 中断次数自增
T0_count++;
// 当触发了1000次中断说明时间过去了一秒
if(T0_count == 1000)
{
T0_count=0;
// 秒针每一秒加一
secondes++;
}
}
串口通信
串口介绍
- 串口是一种十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可以实现两个设备的互相通信
- 串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大扩展了单片机的应用范围,增强了单片机系统的硬件实力
- 51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信
简单的双向串口通信电路
电平标准
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种
- TTL电平(单片机io口):+5V表示1;0V表示0
- RS232电平:-3 ~ -15V表示1;+3 ~ +15V表示0
- RS485电平:两线压差+2 ~ +6V表示1;-2 ~ -6V表示0;(差分信号)
常见通信接口
名称 | 引脚定义 | 通信方式 | 特点 |
---|---|---|---|
UART | TXD、RXD | 全双工、异步 | 点对点通信 |
I²C | SCL、SDA | 半双工、同步 | 可挂载多个设备 |
SPI | SCLK、MOSI、MISO、CS | 全双工、同步 | 可挂载多个设备 |
1-Wire | DQ | 半双工、异步 | 可挂载多个设备 |
此外还有:CAN、USB等
相关术语
- 全双工:通信双方可以在同一时刻相互传输数据
- 半双工:只有一根通信线,双方可以相互传输数据,但是在发的时候不能收,收的时候不能发
- 单工:只能单向的一方传数据给另一方
- 异步: 通信双方约定使用相同的波特率,使用各自的时钟控制数据传输
- 同步: 通信双方在同一时钟控制下同步传输数据
- 总线:连接各个设备的数据传输线路
波特率:串口通信的速率
表示每秒传输二进制数据的位数,单位是bps,位/秒。
异步通信:双方使用相同的波特率来发送和接收数据,例如:9600、19200、57600和115200等
51单片机的UART
UART是单片机内部资源,与定时器一样
UART的引脚TXD和RXD分别与P3.1和P3.0是复用的关系
STC89C51的共有1个UART
STC89C51的UART有四种工作模式
- 模式0:同步移位寄存器
- 模式1:8位UART,波特率可变(常用)
- 模式2:9位UART,波特率固定
- 模式2:9位UART,波特率可定
串口通信的数据格式参数
-
起始位: 低电平,标志着一帧数据的开始
-
数据位: 数据内容,可选择为5、6、7、8位
-
校验位:用于数据验证
可分为奇校验和偶校验。奇校验时数据位和校验位中1的总数应为奇数,同理,偶校验时数据位和校验位中1的总数应为偶数;
-
停止位: 高电平,标志着一帧数据的结束
时序图
简易的串口模式图
SBUF在代码中发送数据:SBUF=0xff;
串口寄存器
UART串口寄存器
IE、IPH、IP寄存器在 “中断寄存器” 部分有介绍过
SCON:串行控制寄存器
-
SM0/FE
当PCON寄存器中的SMOD0置1时,该位用于帧错误检测
当PCON寄存器中的SMOD0置0时,改位与SM1一起指定串行通信的工作方式
-
SM1
与SM0/FE组合确定串口的工作方式:
-
SM2
允许方式2或方式3多机通信控制位。使用方式1把SM2置0即可
-
REN
允许/禁止串行接收数据。REN=1允许串行口接收数据;REN=0禁止串口接收数据
-
TB8
在方式2或方式3中,TB8为要发送的第9位数据,由软件置位。例如:可用于数据校验或多机通信中表示地址帧/数据帧的标志位
-
RB8
在方式2或方式3中,RB8为接收到的第9位数据。若SM2=0,则RB8是接收到的停止位
-
TI
发送中断请求标志位。当数据发送完毕后由内部硬件置1,即TI=1,不能由硬件自动置位,必须使用软件手动清0(TI=1)
-
RI
接收中断请求标志位。当数据接收完毕后由内部硬件置1,即RI=1,不能由硬件自动置位,必须使用软件手动清0(RI=0)
PCON:电源控制寄存器
-
SMOD
波特率选择位。当SMOD软件置1时,则通信方式1、2、3的波特率加倍;SMOD置0则波特率不加倍
-
SMOD0
帧错误检测有效控制位。当SMOD0=1时,SCON寄存器中的SM0/FE位用于FE(帧错误检测)功能;当SMOD0=0时,SCON寄存器中的SM0/FE位用于SM0功能,和SM1一起指定串口的通信方式
单片机发送数据到电脑
配置寄存器及波特率公式
寄存器配置要求
定时器选择T1、工作模式2(8位重装模式),串口通信选择方式1(8位UART,波特率可变),开发板的晶振为11.0592,波特率设置为9600,波特率加倍
计算波特率的公式如下
注意:晶振频率也可以用fosc表示
溢出一次的时间=(计数器可容纳的最大值 + 1 - 初始值)*(分频系数 / 晶振频率)
溢出率=1 / 溢出一次的时间
波特率=( 2^SMOD / 32) * 溢出率
配置寄存器
-
定时器T1,工作模式2:令TMOD=0010 0000;
TMOD &= 0x0f; // 低四位保持不变,高四位全部置0 TMOD |= 0x20; // 低四位保持不变,高四位的后两位变为10
-
波特率加倍,不做帧错误检测
使PCON寄存器的SMOD=1,SMOD0=0,即PCON的最高两位为10
PCON |= 0x80; // PCON的第7位置1,其他不变 PCON &= 0xbf; // PCON的第6位置0,其他不变
-
串口通信选择方式1
SM0置0,SM1置1,组合成工作方式1,禁止接收数据,所以REN置0,其他也置0
则
SCON = 0x40;
-
波特率设置为9600,求计数器的初始值
根据上面的波特率计算公式可知
9600=(2^1/32)*溢出率,溢出率等于153600
153600=1/[(255+1 - 初始值) * (12 / 11.0592MHz)],初始值为250
TL1 = 0xfa; // 定时器的初始值 TH1 = 0xfa; // 定时器的重载值
-
定时器1开始记时但是禁止触发中断
EA=1; // 总中断开启 ET1 = 0; //禁止定时器中断 TR1 = 1; //定时器1开始计时 PS=0; // 低优先级 ES=1; // 开启串口中断
示例
将UART_mode1_init和UART_send_byte封装到UART文件中
#include <REGX52.H>
#include "delay.h"
// 初始化9600波特率,晶振11.0952的串口,工作方式1
void UART_mode1_init() //9600bps@11.0592MHz
{
PCON |= 0x80; // PCON的第7位置1,其他不变
PCON &= 0xbf; // PCON的第6位置0,其他不变
SCON = 0x40; //8位数据,可变波特率
TMOD &= 0x0f; // 低四位保持不变,高四位全部置0
TMOD |= 0x20; // 低四位保持不变,高四位的后两位变为10
TL1 = 0xfa; // 定时器的初始值
TH1 = 0xfa; // 定时器的重载值
EA=1; // 总中断开启
ET1 = 0; //禁止定时器中断
TR1 = 1; //定时器1开始计时
PS=0; // 低优先级
ES=1; // 开启串口中断
}
// 发送一个字节
void UART_send_byte(unsigned char byte)
{
// 将要发送的数据放到SBUF存储器中,单片机会自动发送数据
SBUF=byte;
// 发送数据时TI=1,当数据发送完成后TI=0
while(TI==0);
// 需要软件手动清零
TI=0;
}
void main()
{
// 要发送的数据
static unsigned char byte;
// 初始化
UART_mode1_init();
while(1)
{
// 发送数据
UART_send_byte(byte);
delay(1000);
byte++;
}
}
STC-ISP串口工具及发送和接收数据
STC-ISP发送和接收数据
STC-ISP串口工具:计算波特率
单片机接收数据
示例
#include <REGX52.H>
#include "delay.h"
// 把上面的初始化函数及函数封装
#include "UART.h"
void main()
{
// 初始化
UART_mode1_init();
while(1)
{
}
}
// 串口中断
void UART_rountine() interrupt 4
{
if(RI)
{
// 把接收到的值取反后赋值给P2
P2=~SBUF;
// 把接收的数据在发送回去
UART_send_byte(SBUF);
// 软件清零
RI=0;
}
}
LED点阵屏
LED点阵屏介绍
简介
LED点阵屏由若干个独立的LED组成,以矩阵的形式排列
分类:
- 按颜色:单色、双色、全彩
- 按像素:8*8、16*16等(一般来说都是8的倍数),大规模的LED点阵通常由很多个小点阵拼接
显示原理
- LED点阵屏的结构类似于数码管,只不过的数码管把每一列的LED以“8”字型排列
- LED点阵屏与数码管一样,有共阴极和共阳极两种连接法,不同的接法对应的电路结构不同
- LED点阵屏需进行逐行或逐列扫描,才能是所有LED同时显示,与数码管现实原理一样
8*8LED点阵原理图
P0端口控制LED点阵的列(段选),Dp控制LED点阵的行(位选),不可位选址
P07置0,Dp给0x80,则第一列点亮第一个LED灯
控制Dp输出的是74HC595(串转并),串行输出转并行输出,用于扩展io口
74HC595(串转并)
74HC595是串行输入并行输出的移位寄存器,可用三根线输入串行数据,八根线输出并行数据,多片级联后,可输出16位、24位、32位等,常用于io口扩展
-
OE:OE为低电平时74HC595才会运行。在开发板上,使用跳线帽把OE引脚和GND链接,74HC595才可以运行
-
串转并的具体过程
-
SER:串行数据输入
存储一位的数据
用于接收串行输入数据。通过将数据按位串行输入到 SER 引脚,可以将数据加载到 74HC595 的移位寄存器中
-
SRCLK:上升沿移位
第一次将SRCLK置1,会SER中的位(0或1)放到移位寄存器的第0位置,再把SRCLK置0,为下一次移位做准备;
第二次将SRCLK置1,会再把SER中的位放到移位寄存器的第0位置,而之前在第0位置的数据就会被移位到第1位置,再把SRCLK置0,为下一次移位做准备;
第二次将SRCLK置1,会再把SER中的位放到移位寄存器的第0位置,而之前在第0位置的数据就会被移位到第1位置,在第1位置的数据会被移到第2位置,再把SRCLK置0,为下一次移位做准备;
以此类推,直到把移位寄存器的8个位放满
注意:高位先进
-
RCLK:上升沿锁存
RCLK置1,把移位寄存器中的所有数据原封不动的放入到并行输出寄存器中,再通过并行输出寄存器的并行输出引脚(QA~QH)并行输出,以供外部电路使用(扩展io等)
-
QH': 通过级联多个芯片来扩展输出端口
如果级联两个74HC595芯片,可以通过第一个芯片的 QH' 引脚将数据传递到第二个芯片的 SER 引脚,以扩展输出端口。这种级联的方法可以扩展更多的输出端口,以满足更多的应用需求。
LED点阵静态显示
示例
#include <REGX52.H>
#include "delay.h"
sbit RCK=P3^5; // RCLK
sbit SER=P3^4; // SER
sbit SCK=P3^6; // SRCLK
// 使用74HC595控制8位数据的输出
void _74HC595_write_byte(unsigned byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
/*
用 SER=byte&0x80 举例
0x80的最高位为1,其他位全为0
所以 byte&0x80 后,除了byte的最高位不确定以外,其他全部位皆为0
如果byte的最高位为1,那么 byte&0x80=0x80,否则 byte&0x80=0x00,也就是0
又因为SER只接受一位,有点类似布尔值,非0即1,只要SER等号后面不是一个非零数,SER就等于1,否则SER等于0
所以byte的最高位为1,那SER就等于1;byte的最高位为0,那SER就等于0。这样就把byte的最高位取出来了
byte后面的七位数据原理同上,只需要对0x80向右移位即可,想取出第6位就右移1,第五位就右移2,这就是为什么0x80右移i的原因
*/
SER=byte&0x80>>i;
// 上升沿移位置1,把SER的数据移入到移位寄存器中
SCK=1;
// 上升沿移位置0,为下一次移位做准备
SCK=0;
}
// byte的八位数据全部移入移位寄存器了,锁存置1,把移位寄存器中的数据放到并行输出寄存器,进行串行输出
RCK=1;
// 锁存置0
RCK=0;
}
// 根据Column(0~7)的值显示对应列的LED
void LED_lattice_column_show(unsigned char column, byte)
{
// byte控制这一行的LED那个亮那个灭(0是灭,1是亮)
_74HC595_write_byte(byte);
// 0x80>>column跟上面函数的0x80>>i是差不多的,可以单独控制8位数据中的其中一位 置1而其他位 置0
// P0端口1是灭,0是亮,所以要取反
P0=~(0x80>>column);
// 进行消影
delay(1);
P0=0xff;
}
void main()
{
// 初始化上升沿移位和锁存为0
SCK=0;
RCK=0;
while(1)
{
// 显示一个爱心
LED_lattice_column_show(0,0x70);
LED_lattice_column_show(1,0x88);
LED_lattice_column_show(2,0x84);
LED_lattice_column_show(3,0x42);
LED_lattice_column_show(4,0x42);
LED_lattice_column_show(5,0x84);
LED_lattice_column_show(6,0x88);
LED_lattice_column_show(7,0x70);
}
}
LED点阵显示逐帧动画
code关键字:
- 把该变量下的数据存放到ROM(硬盘)中
- 不加关键字code的变量数据在运行时会存放到RAM(内存)中,方便读写
- 注意:在ROM中的数据只可读不可写
#include <REGX52.H>
// 把上面代码中的_74HC595_write_byte和LED_lattice_column_show函数进行了封装
#include "LED_lattice.h"
// 8*8的4个动画帧
unsigned char code animation[]=
{
// 第一帧
0x70,0x88,0x84,0x42,0x42,0x84,0x88,0x70,
// 第二帧
0x30,0x48,0x48,0x24,0x24,0x48,0x48,0x30,
0x00,0x10,0x28,0x14,0x14,0x28,0x10,0x00,
0x30,0x48,0x48,0x24,0x24,0x48,0x48,0x30
};
// 逐帧动画
void love_throb()
{
// offset:
unsigned char i, offset, count;
for(i=0;i<8;i++)
{
LED_lattice_column_show(i, animation[i+offset]);
}
count++;
if(count==15)
{
count=0;
offset+=8;
if(offset==32)
{
offset=0;
}
}
}
void main()
{
// SCK=0;
// RCK=0;
// 函数中就这两句代码
LED_lattice_init();
while(1)
{
love_throb();
}
}
LED点阵显示流动动画
#include <REGX52.H>
#include "LED_lattice.h"
unsigned char code animation[]=
{
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x10,0x38,0x54,0x92,
0x10,0x10,0x10,0x70,
0x98,0x94,0x42,0x42,
0x84,0x8C,0x72,0x42,
0x84,0x88,0x70,0x10,
0x10,0x10,0x38,0x54,
0x54,0x54,0x6C,0x44,
0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,
0x00,0x00,
};
void arrow_love()
{
unsigned char i, offset, count;
for(i=0;i<8;i++)
{
LED_lattice_column_show(i, animation[i+offset]);
}
count++;
if(count==10)
{
count=0;
offset++;
if(offset==38)
{
offset=0;
}
}
}
void main()
{
LED_lattice_init();
while(1)
{
arrow_love();
}
}
文字取模软件使用
软件链接
链接:https://pan.baidu.com/s/1uqhiKl98G0Xn3LbV0OWZbQ?pwd=1234
提取码:1234
根据扫描方式设置
重点设置这两个(我这里代码使用的是纵向)
创建图像
放大格点
生成代码
DS1302实时时钟
DS1302中文手册
链接:https://pan.baidu.com/s/1qVE1K2UJ6ZwYKj1GDid-uA?pwd=1234
提取码:1234
SD1390介绍
-
由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片,他可以对年、月、日、周、时、分、秒进行记时,且具有闰年补偿等多种方式
-
RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片
引脚定义及电路
内部结构
本开发板的DS1302原理图
寄存器和命令字
寄存器
- CH:时钟静止,CH置1后整个时钟就停止记时
- 时寄存器:最高位置1表示12小时制。置0表示24小时制
- WP:置1开启写保护,不许写入数据。置0关闭写保护,允许写入数据
命令字
命令字控制DS1302的读写及操作数据
- 最高位(第7位)固定为1,为0则不允许对DS1602进行读写
- 第6位为0(CK)时,是对时钟/日历数据进行操作;为1(RAM)时对RAM数据进行操作
- 第5到第1位表示了输入输出时操作的寄存器。例如五个0就是操作时间秒的
- 第0位为0时(WR)表示要对这个地方进行写操作;为1(RD)时表示要对这个地方进行读操作
例如对秒寄存器进行写操作,那么命令字的二进制为1000 0000,十六进制为0x80
时序图
解析
CE:操作数据时把CE置1,读写完毕后置0
SCLK:置1的时候就对数据进行操作,置0准备下一次操作数据。在上升沿是写数据,下降沿是读数据。
I/O:在内部结构可以知道跟io口交互的是移位寄存器,所以数据是一位一位的输入或输出的。SCLK给上升沿就写入,给下降沿就读出
注意:
在写数据时把SCLK置0再置1就完成了对一个位的写入,循环十六次就可以把一个字节的数据写到对应的寄存器中
但是在读数据时有点不一样。有个地方要连续置两次1,保证数据传输的正确。具体如下图
两个图表达的意思是一样的,命令字发送完毕后不能马上把SCLK置0,要再置一次1,然后置0,读取数据的最低位,重复8次SCLK=1,SCLK=0。就读出了整个字节
对寄存器进行读写
写数据
void DS1302_write_data(unsigned char command, byte)
{
unsigned char i;
DS1302_CE=1;
// 传入命令字,从低位开始一位一位的传
for(i=0;i<8;i++)
{
// command&0x01可以取出command的最低位
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=0;
// delay(1); // 注意:如果单片机速率较高的话需要加一个延时
DS1302_SCLK=1;
}
// 写入数据,从低位开始一位一位的传
for(i=0;i<8;i++)
{
// byte&0x01可以取出byte的最低位
DS1302_IO=byte&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
DS1302_CE=0;
}
读数据
unsigned char DS1302_read_data(unsigned char command)
{
// byte:读出来的数据。局部函数需要给初始值
unsigned char i, byte=0x00;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
for(i=0;i<8;i++)
{
// 在命令字发送结束后SCLK不能马上置0,要再置1来接收读出来的数据
DS1302_SCLK=1;
DS1302_SCLK=0;
// 读数据时先读低位,跟写数据和发送命令字一样
// 如果IO口输出0,说明读出的数据该位为0;如果IO口输出1,说明读出的数据该位为1
// byte初始值为0x00,所以只有在IO输出1的时候才需要把对应位改为1,IO输出0时不需要改变对应的位
if(DS1302_IO)
{
// byte|=0x01 把最低位置1,其他位不变
byte|=(0x01<<i);
}
}
DS1302_CE=0;
// 把读出来的字节返回
return byte;
}
测试代码
#include <REGX52.H>
#include "LCD1602.h"
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
// 初始化DS1302
void DS1302_init()
{
// 开发板上电后寄存器会自动置1
DS1302_CE=0;
DS1302_SCLK=0;
}
void DS1302_write_data(unsigned char command, byte)
{
unsigned char i;
DS1302_CE=1;
// 传入命令字,从低位开始一位一位的传
for(i=0;i<8;i++)
{
// command&0x01可以取出command的最低位
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=0;
// delay(1); // 注意:如果单片机速率较高的话需要加一个延时
DS1302_SCLK=1;
}
// 写入数据,从低位开始一位一位的传
for(i=0;i<8;i++)
{
// byte&0x01可以取出byte的最低位
DS1302_IO=byte&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
DS1302_CE=0;
}
unsigned char DS1302_read_data(unsigned char command)
{
// byte:读出来的数据。局部函数需要给初始值
unsigned char i, byte=0x00;
DS1302_CE=1;
// 发送命令字
for(i=0;i<8;i++)
{
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
// 读数据
for(i=0;i<8;i++)
{
// 在命令字发送结束后SCLK不能马上置0,要再置1来接收读出来的数据
DS1302_SCLK=1;
DS1302_SCLK=0;
// 读数据时先读低位,跟写数据和发送命令字一样
// 如果IO口输出0,说明读出的数据该位为0;如果IO口输出1,说明读出的数据该位为1
// byte|=0x01 把最低位置1,其他位不变(跟上个代码的 &= 置0是一样的)
if(DS1302_IO){byte|=(0x01<<i);}
}
DS1302_CE=0;
DS1302_IO=0;
return byte;
}
void main()
{
unsigned char byte;
// 初始化
LCD_Init();
DS1302_init();
// 关闭写保护
DS1302_write_data(0x8e,0x00);
// 在秒寄存器中写入数据 0x03
DS1302_write_data(0x80,0x03);
// 把秒寄存器中的数据读出来,并用byte变量接收返回值
byte=DS1302_read_data(0x81);
LCD_ShowNum(2,1,byte,3);
while(1)
{
}
}
优化代码
当我们把下面两行代码放到main函数的while循环中时,会发现LCD1602显示屏显示混乱
// 把秒寄存器中的数据读出来,并用byte变量接收返回值
byte=DS1302_read_data(0x81);
LCD_ShowNum(2,1,byte,3);
要解决上述问题,只需要在读数据的函数中,在return前加上一行代码:DS1302_IO=0;
unsigned char DS1302_read_data(unsigned char command)
{
// byte:读出来的数据。局部函数需要给初始值
unsigned char i, byte=0x00;
DS1302_CE=1;
// 发送命令字
for(i=0;i<8;i++)
{
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
// 读数据
for(i=0;i<8;i++)
{
// 在命令字发送结束后SCLK不能马上置0,要再置1来接收读出来的数据
DS1302_SCLK=1;
DS1302_SCLK=0;
// 读数据时先读低位,跟写数据和发送命令字一样
// 如果IO口输出0,说明读出的数据该位为0;如果IO口输出1,说明读出的数据该位为1
// byte|=0x01 把最低位置1,其他位不变(跟上个代码的 &= 置0是一样的)
if(DS1302_IO){byte|=(0x01<<i);}
}
DS1302_CE=0;
DS1302_IO=0; // 这是新加的代码
return byte;
}
经过上述代码我们已经可以正常的读写寄存器中的数据了,但是又出现了新的问题
显示屏上从3开始记时,记时记到9后直接变成了16
出现这个情况是因为DS1302内部的寄存器不是以正常的二进制来进行存储的,而是使用BCD码进行存储的
BCD码
BCD码(Binary Coded Decimal),用4位二进制数表示1位十进制数
- 8位二进制数的高4位表示十进制数的十位数,低4位表示十进制数的个位数
- 例如:1000 0011,用BCD码表示13;1000 0101,表示85;0001 1010不合法,因为1010转十进制后等于10,不能作为个位数
- 在十六进制中就是:0x13表示13;0x85表示85;0x1A不合法
BCD码转十进制:DEC = BCD / 16 * 10 + BCD % 16;(只能转换的2位BCD)
十进制转BCD码:BCD = DEC / 10 * 16 + DEC % 10;(只能转换的2位BCD)
DEC表示一个十进制数
所以要把LCD_ShowNum(2,1,byte,3);
改成下面这样才能正确显示
byte=DS1302_read_data(0x81);
LCD_ShowNum(2,1,byte/16*10+byte%16,3);
封装DS1302有关代码,并优化
源文件
#include <REGX52.H>
// 定义宏常量
#define DS1302_seconds 0x80 // 秒 写数据地址
#define DS1302_minutes 0x82 // 分 写数据地址
#define DS1302_hour 0x84 // 时 写数据地址
#define DS1302_date 0x86 // 日 写数据地址
#define DS1302_month 0x88 // 月 写数据地址
#define DS1302_year 0x8c // 年 写数据地址
#define DS1302_day 0x8a // 周 写数据地址
#define DS1302_WP 0x8e // 写保护地址
/*
这里本来应该还有读数据的地址,但是读数据的地址只比写数据的地址多1
例如:秒寄存器的读写地址:0x80和0x81
只需要在我们读数据时把写数据的地址加一或者把最低位置1
这样就不用定义这么多常量
*/
// 定义引脚,方便使用
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
// 定义时间单位的数组
unsigned char time_units[] = {
DS1302_seconds, // 秒
DS1302_minutes, // 分
DS1302_hour, // 时
DS1302_date, // 日
DS1302_month, // 月
DS1302_year, // 年
DS1302_day // 周
};
// 存放当前时间的数组,与上面时间单位数组一一对应。也可以设置初始值
unsigned char current_time[] = {55,59,12,29,11,23,3};
/*
@函数作用:初始化DS1302
@参数:无
@返回值:无
*/
void DS1302_init()
{
// 开发板上电后寄存器会自动置1
DS1302_CE=0;
DS1302_SCLK=0;
}
/*
@函数作用:写数据
@参数:command 命令字
@参数:byte 要写入的数据,字节
@返回值:无
*/
void DS1302_write_data(unsigned char command, byte)
{
unsigned char i;
DS1302_CE=1;
// 传入命令字,从低位开始一位一位的传
for(i=0;i<8;i++)
{
// command&0x01可以取出command的最低位
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=0;
// delay(1); // 注意:如果单片机速率较高的话需要加一个延时
DS1302_SCLK=1;
}
// 写入数据,从低位开始一位一位的传
for(i=0;i<8;i++)
{
// byte&0x01可以取出byte的最低位
DS1302_IO=byte&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
DS1302_CE=0;
}
/*
@函数作用:读数据
@参数:command 命令字
@返回值:从寄存器读出来的数据
*/
unsigned char DS1302_read_data(unsigned char command)
{
// byte:读出来的数据。局部函数需要给初始值
unsigned char i, byte=0x00;
command|=0x01; // 把最低位置1,具体为什么在定义宏常量的地方有解释
DS1302_CE=1;
// 发送命令字
for(i=0;i<8;i++)
{
DS1302_IO=command&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
// 读数据
for(i=0;i<8;i++)
{
// 在命令字发送结束后SCLK不能马上置0,要再置1来接收读出来的数据
DS1302_SCLK=1;
DS1302_SCLK=0;
// 读数据时先读低位,跟写数据和发送命令字一样
// 如果IO口输出0,说明读出的数据该位为0;如果IO口输出1,说明读出的数据该位为1
// byte|=0x01 把最低位置1,其他位不变(跟上个代码的 &= 置0是一样的)
if(DS1302_IO){byte|=(0x01<<i);}
}
DS1302_CE=0;
// 保证读数据的正确
DS1302_IO=0;
return byte;
}
/*
@函数作用:BCD码转为十进制数
@参数:BCD,BCD码
@返回值:十进制数
*/
unsigned char BCD_to_DEC(unsigned char BCD)
{
unsigned char DEC;
DEC=BCD/16*10+BCD%16;
return DEC;
}
/*
@函数作用:十进制数转为BCD码
@参数:十进制数
@返回值:BCD码
*/
unsigned char DEC_to_BCD(unsigned char DEC)
{
unsigned char BCD;
BCD=DEC/10*16+DEC%10;
return BCD;
}
/*
@函数作用:把当前时间数组中的时间写入寄存器中
@参数:无
@返回值:无
*/
void DS1302_set_time()
{
unsigned char i;
// 关闭写保护
DS1302_write_data(DS1302_WP, 0x00);
for(i=0;i<7;i++)
{
// 把当前时间数组中的十进制数据通过DEC_to_BCD转成BCD码
// 再通过DS1302_write_data函数写入对应的寄存器中
DS1302_write_data(time_units[i], DEC_to_BCD(current_time[i]));
}
// 打开写保护
DS1302_write_data(DS1302_WP, 0x80);
}
// 返回的是数组首地址,用指针接收即可
/*
@函数作用:把寄存器中的时间读到当前时间数组中并返回该数组
@参数:无
@返回值:指针,当前时间数组的数组首地址
*/
unsigned char *DS1302_read_time()
{
unsigned i, temp;
for(i=0;i<7;i++)
{
// 通过DS1302_read_data函数接收对应寄存器中的BCD码数据
temp=DS1302_read_data(time_units[i]);
// 把BCD码转为十进制数后再存入数组中
current_time[i]=BCD_to_DEC(temp);
}
// 把当前时间数组返回(数组名表示数组的首地址)
return current_time;
}
头文件
#ifndef __DS1302_h__
#define __DS1302_h__
// 初始化
void DS1302_init();
// 写单个时间数据
void DS1302_write_data(unsigned char command, byte);
// 读单个时间数据
unsigned char DS1302_read_data(unsigned char command);
// 把时间读到当前时间数组中并返回数组
unsigned char *DS1302_read_time();
// 把数组中的时间写入寄存器中
void DS1302_set_time();
#endif
实时时钟
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
void main()
{
// 接收返回的数组首地址
unsigned char *time;
// 初始化LCD和DS1302
LCD_Init();
DS1302_init();
// 把时间读到寄存器中
DS1302_set_time();
LCD_ShowString(1,1,"20 - -");
LCD_ShowString(2,1," : : week:");
while(1)
{
time=DS1302_read_time();
// 根据自己的排版从数组中取出相应的数据展示即可
LCD_ShowNum(1,3,time[5],2);
LCD_ShowNum(1,6,time[4],2);
LCD_ShowNum(1,9,time[3],2);
LCD_ShowNum(2,1,time[2],2);
LCD_ShowNum(2,4,time[1],2);
LCD_ShowNum(2,7,time[0],2);
LCD_ShowNum(2,15,time[6],1);
}
}
可调节时间的实时时钟
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "delay.h"
#include "timer0_mode1_init.h"
// count:要修改的时间
// time:接收返回的数组首地址
// time_filcker:控制时间闪烁的标志位
// key_num:独立按键的建码
// mode:是否处于修改时间的标志位
unsigned char count, *time, time_filcker, key_num, mode;
// 获取独立按键的建码
unsigned char key()
{
if(P3_1==0){delay(20);while(P3_1==0);delay(20);return 1;}
if(P3_0==0){delay(20);while(P3_0==0);delay(20);return 2;}
if(P3_2==0){delay(20);while(P3_2==0);delay(20);return 3;}
if(P3_3==0){delay(20);while(P3_3==0);delay(20);return 4;}
return 0;
}
// 校验当前时间数组中的时间是否符合标准
void if_data()
{
// time数组中的数据为unsigned char,该类型的范围为0~255
if(time[0]==255){time[0]=59;} // 秒的范围在0~59
if(time[1]==255){time[1]=59;} // 分的范围在0~59
if(time[2]==255){time[2]=23;} // 时的范围在0~23
if(time[4]==0){time[4]=12;} // 月的范围在1~12
if(time[5]==255){time[5]=99;} // 年的范围在0~99
if(time[6]==0){time[6]=7;} // 周的范围在1~7
if(time[3]==0) // 天的范围要根据月份和年份来确定
{
if
(
time[4]==1 || time[4]==3 ||
time[4]==5 || time[4]==7 ||
time[4]==8 || time[4]==10 || time[4]==12
) // 当月份为1,3,5,7,8,10,12时,天的范围在1~31
{
time[3]=31;
}
else if(time[4]==2) // 当月份为2时,要根据年份确定天的范围
{
if(time[5]%4==0) // 闰年,天的范围在1~29
{
time[3]=29;
}
else // 平年,天的范围在1~28
{
time[3]=28;
}
}
else // 其他情况,天的范围在1~30
{
time[3]=30;
}
}
if(time[0]==60){time[0]=0;} // 秒的范围在0~59
if(time[1]==60){time[1]=0;} // 分的范围在0~59
if(time[2]==24){time[2]=0;} // 时的范围在0~23
if(time[4]==13){time[4]=1;} // 月的范围在1~12
if(time[5]==100){time[5]=0;} // 年的范围在0~99
if(time[6]==8){time[6]=1;} // 周的范围在1~7
// 天的范围要根据月份和年份来确定
if
(
time[4]==1 || time[4]==3 ||
time[4]==5 || time[4]==7 ||
time[4]==8 || time[4]==10 || time[4]==12
)
{
if(time[3]==32) // 当月份为1,3,5,7,8,10,12时,天的范围在1~31
{
time[3]=1;
}
}
else if(time[4]==2) // 当月份为2时,要根据年份确定天的范围
{
if(time[5]%4==0) // 闰年,天的范围在1~29
{
if(time[3]==30)
{
time[3]=1;
}
}
else // 平年,天的范围在1~28
{
if(time[3]==29)
{
time[3]=1;
}
}
}
else // 其他情况,天的范围在1~30
{
if(time[3]==31)
{
time[3]=1;
}
}
}
// 更新时间函数
void update_time(unsigned char key_num)
{
// 当独立按键按下2时对下一个时间进行调整
if(key_num==2)
{
count++;
if(count==7)
{
count=0;
}
}
// 独立按键按下3对当前要修改的时间加1
if(key_num==3)
{
time[count]+=1;
// 调用校验时间是否符合规范的函数
if_data();
// 重新把数组中的时间读到寄存器中
DS1302_set_time();
}
// 独立按键按下4对当前要修改的时间减1
if(key_num==4)
{
time[count]-=1;
// 调用校验时间是否符合规范的函数
if_data();
// 重新把数组中的时间读到寄存器中
DS1302_set_time();
}
}
void main()
{
// 初始化LCD和DS1302和定时器
LCD_Init();
DS1302_init();
timer0_mode1_init();
// 把时间读到寄存器中
DS1302_set_time();
LCD_ShowString(1,1,"20 - -");
LCD_ShowString(2,1," : : week:");
while(1)
{
// 接收当前时间的数组,从0~7分别为:秒、分、时、天、月、年、周
time=DS1302_read_time();
/*
mode:当mode为1时说明进入修改时间的状态,为0则是显示时间的状态
time_filcker:闪烁标志位,为1要修改的地方不显示,为0则显示,每0.5秒转换一次状态达到闪烁的效果
count:对应要修改的具体时间,count为0说明当前在修改秒钟,为1在修改分钟,为2在修改时钟
*/
// 在修改时间状态,根据count选择修改的时间位进行闪烁
if(mode==1 && time_filcker==1 && count==5){LCD_ShowString(1,3," ");}
else{LCD_ShowNum(1,3,time[5],2);}
if(mode==1 && time_filcker==1 && count==4){LCD_ShowString(1,6," ");}
else{LCD_ShowNum(1,6,time[4],2);}
if(mode==1 && time_filcker==1 && count==3){LCD_ShowString(1,9," ");}
else{LCD_ShowNum(1,9,time[3],2);}
if(mode==1 && time_filcker==1 && count==2){LCD_ShowString(2,1," ");}
else{LCD_ShowNum(2,1,time[2],2);}
if(mode==1 && time_filcker==1 && count==1){LCD_ShowString(2,4," ");}
else{LCD_ShowNum(2,4,time[1],2);}
if(mode==1 && time_filcker==1 && count==0){LCD_ShowString(2,7," ");}
else{LCD_ShowNum(2,7,time[0],2);}
if(mode==1 && time_filcker==1 && count==6){LCD_ShowString(2,15," ");}
else{LCD_ShowNum(2,15,time[6],1);}
// 获取独立按键的建码
key_num=key();
// 当按键按下1对mode进行逻辑取反
if(key_num==1){mode=!mode;}
// 当mode为1时进入修改时间的函数
if(mode){update_time(key_num);}
}
}
// 中断函数使用模板
void Timer0() interrupt 1
{
// 记录中断次数
static unsigned int T0_count;
// 每触发一次中断都要重新配置计数器的初始值
TH0 = 0xfc;
TL0 = 0x18;
// 中断次数自增
T0_count++;
// 每500ms触发一次中断
if(T0_count >= 500)
{
T0_count=0;
// 每500ms对time_filcker取反,实现闪烁功能
time_filcker=!time_filcker;
}
}
蜂鸣器
蜂鸣器介绍
-
蜂鸣器是一种将电信号转换为声音信号的器件,常用来产生设备的按键音、报警音等提示信号
-
蜂鸣器按驱动方式分为有源蜂鸣器和无源蜂鸣器
- 有源蜂鸣器:内部自带震荡源,将正负极接上直流电压即可持续发声,频率固定
- 无源蜂鸣器:内部不带震荡源,需要控制器提供震荡脉冲才可发声,调整提供震荡脉冲的频率,可发出不同频率的声音
-
蜂鸣器分正负极
驱动电路
三极管驱动
摘抄自:有源蜂鸣器与无源蜂鸣器的驱动方式详解(精华版)_无源蜂鸣器驱动电路图-CSDN博客
蜂鸣器驱动电路一般都包含以下几个部分:一个三极管、一个蜂鸣器、一个续流二极管和一个电源滤波电容。
-
蜂鸣器
发声元件,在其两端施加直流电压(有源蜂鸣器)或者方波(无源蜂鸣器)就可以发声,其主要参数是外形尺寸、发声方向、工作电压、工作频率、工作电流、驱动方式(直流/方波)等。这些都可以根据需要来选择。
-
续流二极管
蜂鸣器本质上是一个感性元件,其电流不能瞬变,因此必须有一个续流二极管提供续流。否则,在蜂鸣器两端会产生几十伏的尖峰电压,可能损坏驱动三极管,并干扰整个电路系统的其它部分。
-
滤波电容
滤波电容C1的作用是滤波,滤除蜂鸣器电流对其它部分的影响,也可改善电源的交流阻抗,如果可能,最好是再并联一个220uF的电解电容。
-
三极管
三极管Q1起开关作用,其基极的高电平使三极管饱和导通,使蜂鸣器发声;而基极低电平则使三极管关闭,蜂鸣器停止发声。
集成电路驱动(IO口驱动)
原理图
-
该开发板使用的是集成电路,给P25端口1或0就可以控制蜂鸣器有无电流
-
蜂鸣器是无源蜂鸣器,通过P25产生一个震动频率,控制蜂鸣器发出不同的声音
-
而利用I/O定时翻转电平来产生驱动波形的方式会比较麻烦一点,必须利用定时器来做定时,通过定时翻转电平产生符合蜂鸣器要求的频率的波形,这个波形就可以用来驱动蜂鸣器了。
-
比如为2500Hz的蜂鸣器的驱动,可以知道周期为400μs,这样只需要驱动蜂鸣器的I/O口每200μs翻转一次电平就可以产生一个频率为2500Hz,占空比为1/2duty的方波,再通过三极管放大就可以驱动这个蜂鸣器了。
频率和周期的概念
- 频率:是单位时间内完成周期性变化的次数,是描述周期运动频繁程度的量,常用符号f或ν表示,单位为秒分之一,Hz是频率的基本单位,通常是以1秒完成的动作次数。比如你1秒能吃3个馒头那就记作你吃馒头的频率是3Hz(3赫兹),比如你的眼皮1秒能跳动10次就记作你眼皮跳动10Hz(10赫兹)
- 周期: 就是1秒时间内每一次变化或动作所消耗的时间 ,基本单位为秒(s),与频率的单位:Hz对应
- 频率和周期的关系:
频率=1/周期
周期=1/频率
- 单位换算:Hz 对应s;KHz对应ms;MHz对应us。1Hz=1000KHz=1000000MHz;1s=1000ms=1000us
乐理简介
钢琴键与音符对照
-
每一组共有12个键,7个白键,5个黑键
-
越往右音调就越高
-
每个相邻的两个键是半音的关系。相隔一个键的两个键是全音的关系
例如小字1组中的c1白键跟他后面的黑键就是半音关系。e1和f1也是半音的关系
c1和d1是全音的关系,e1和f1后面的黑键是全音的关系
-
c1与c2之间相隔8度,c1与c键也是相隔8度。同理,e1与e2键、e与E键都是相隔8度。只不过e1到e2是升8度;而e到E是降8度
音符
一般以四份音符作为时间基准
二分音符弹奏的时间(按下某个键的时间)是四分音符的两倍;全音符的弹奏时间是四分音符的四倍,八分音符的弹奏是四分音符的二分之一;十六分音符的弹奏时间是四分音符的四分之一
简单的识谱
简谱数字对应琴键
-
数字1对应中央c键,也就是小字组中的c1
2、3、4、5、6、7分别对应d1、e1、f1、g1、a1、b1
-
数字上面每加一个点就升8度;数字下面每加一个点是降8度
例如数字1的上面加一个点就对应c2键,加两个点就对应c3键
数字1下面加一个点就对应c键,加两个点就对应C键
-
数字的左边加上一个#的意思是升高半音,左边加上d就是降低半音
c1就是按下c1右边的黑键,dc1就是按下d键
e1就是按下f1
-
按键时间:
- 一个数字都表示这是一个四分音符弹奏的时间,也就是一拍
- 一个数字后面跟一个增时线 “-” 符号说明是一个二分音符弹奏时间,跟三个这个符合是一个全音符弹奏的时间。也就是一个 “-” 就是一个四分音符弹奏的时间,但是是连续的按不是一个符号按一下
- 如果在数字下面加一个减时线,也就是数字下面加一条线就是八分音符弹奏时间,加两条线就是十六分音符的弹奏时间,后面以此类推
- 数字后面跟一个附点 “ .” 符号就是增加当前音符的二分之一的时间。假设当前曲子的四分音符表示按下按键500ms,那么 “1.” 就表示按下c1键500+250=750ms的时间
-
数字 0 表示休止符,不能发声
-
两个数字之间的顶上用一根线连起来的叫延音线,表示这两个音是按着不放的,不是按一下松开又按一下,是一直按着的
-
简谱头信息
用不同频率发出不同的音调
中音1表示中央c键,也就是c1键
中音1#表示c1键升半音,也就是c1右边的黑键
相邻的钢琴按键的频率关系
频率从a字键开始,假设a键的频率为440,那么a1键的频率就是880,A键的频率就是220
- 也就是a字键每升8度频率就乘二,每降8度就除以二
- 由上可以推算出每个键之间的频率关系:一个键的频率乘以2的12分之一次方就是相邻的右边的键的频率;除以2的12分之一次方就是相邻的左边的键的频率
根据频率求定时器的初值
前置条件:
- 晶振:11.0592
- 分频次数:12
- 定时器模式:定时器0,16位工作模式
用低音1举例:262Hz
通过频率可知:周期=1/262=3816.79389312977us
IO口要翻转电平产生符合蜂鸣器要求的频率的波形,所以每1908.39694656489us(周期/2)就要翻转一次电平,对这个值取整:1908us
定时器的初值=(计数器最大值 + 1) - 定时时间*(分频次数/晶振)
初值=(65535+1)-1908*(12/11.0592)=63466
频率对应初值的表如下:
C大调
音符 | C大调频率(Hz) | X*12^(1/12) | 周期(us) | 周期/2(us) | 取整 | 初值 |
---|---|---|---|---|---|---|
c | 262 | 261.626 | 3816.79389312977 | 1908.39694656489 | 1908 | 63466 |
c# | 277 | 277.183 | 3610.1083032491 | 1805.05415162455 | 1805 | 63577 |
d | 294 | 293.665 | 3401.36054421769 | 1700.68027210884 | 1701 | 63690 |
d# | 311 | 311.127 | 3215.43408360129 | 1607.71704180064 | 1608 | 63791 |
e | 330 | 329.628 | 3030.30303030303 | 1515.15151515152 | 1515 | 63892 |
f | 349 | 349.228 | 2865.32951289398 | 1432.66475644699 | 1433 | 63981 |
f# | 370 | 369.994 | 2702.7027027027 | 1351.35135135135 | 1351 | 64070 |
g | 392 | 391.995 | 2551.02040816327 | 1275.51020408163 | 1276 | 64151 |
g# | 415 | 415.305 | 2409.63855421687 | 1204.81927710843 | 1205 | 64228 |
a | 440 | 440 | 2272.72727272727 | 1136.36363636364 | 1136 | 64303 |
a# | 466 | 466.164 | 2145.92274678112 | 1072.96137339056 | 1073 | 64372 |
b | 494 | 493.883 | 2024.29149797571 | 1012.14574898785 | 1012 | 64438 |
c1 | 523 | 523.251 | 1912.04588910134 | 956.022944550669 | 956 | 64499 |
c1# | 554 | 554.365 | 1805.05415162455 | 902.527075812274 | 903 | 64556 |
d1 | 587 | 587.330 | 1703.57751277683 | 851.788756388416 | 852 | 64612 |
d1# | 622 | 622.254 | 1607.71704180064 | 803.858520900322 | 804 | 64664 |
e1 | 659 | 659.255 | 1517.45068285281 | 758.725341426404 | 759 | 64712 |
f1 | 698 | 698.456 | 1432.66475644699 | 716.332378223496 | 716 | 64759 |
f1# | 740 | 739.989 | 1351.35135135135 | 675.675675675676 | 676 | 64802 |
g1 | 784 | 783.991 | 1275.51020408163 | 637.755102040816 | 638 | 64844 |
g1# | 831 | 830.609 | 1203.36943441637 | 601.684717208183 | 602 | 64883 |
a1 | 880 | 880 | 1136.36363636364 | 568.181818181818 | 568 | 64920 |
a1# | 932 | 932.328 | 1072.96137339056 | 536.480686695279 | 536 | 64954 |
b1 | 988 | 987.767 | 1012.14574898785 | 506.072874493927 | 506 | 64987 |
c2 | 1046 | 1046.502 | 956.022944550669 | 478.011472275335 | 478 | 65017 |
c2# | 1109 | 1108.731 | 901.713255184851 | 450.856627592426 | 451 | 65047 |
d2 | 1175 | 1174.659 | 851.063829787234 | 425.531914893617 | 426 | 65074 |
d2# | 1245 | 1244.508 | 803.212851405623 | 401.606425702811 | 402 | 65100 |
e2 | 1318 | 1318.510 | 758.725341426404 | 379.362670713202 | 379 | 65125 |
f2 | 1397 | 1396.913 | 715.819613457409 | 357.909806728704 | 358 | 65148 |
f2# | 1480 | 1479.978 | 675.675675675676 | 337.837837837838 | 338 | 65169 |
g2 | 1568 | 1567.982 | 637.755102040816 | 318.877551020408 | 319 | 65190 |
g2# | 1661 | 1661.219 | 602.046959662854 | 301.023479831427 | 301 | 65209 |
a2 | 1760 | 1760 | 568.181818181818 | 284.090909090909 | 284 | 65228 |
a2# | 1865 | 1864.655 | 536.193029490617 | 268.096514745308 | 268 | 65245 |
b2 | 1976 | 1975.533 | 506.072874493927 | 253.036437246964 | 253 | 65261 |
B大调
B大调频率(Hz) | 周期/2(us) | 取整 | 初值 |
---|---|---|---|
131 | 3816.79389312977 | 3817 | 61394 |
139 | 3597.12230215827 | 3597 | 61633 |
147 | 3401.36054421769 | 3401 | 61846 |
156 | 3205.12820512821 | 3205 | 62058 |
165 | 3030.30303030303 | 3030 | 62248 |
175 | 2857.14285714286 | 2857 | 62436 |
185 | 2702.7027027027 | 2703 | 62603 |
196 | 2551.02040816327 | 2551 | 62768 |
208 | 2403.84615384615 | 2404 | 62927 |
220 | 2272.72727272727 | 2273 | 63070 |
233 | 2145.92274678112 | 2146 | 63207 |
247 | 2024.29149797571 | 2024 | 63340 |
262 | 1908.39694656489 | 1908 | 63466 |
277 | 1805.05415162455 | 1805 | 63577 |
294 | 1700.68027210884 | 1701 | 63690 |
311 | 1607.71704180064 | 1608 | 63791 |
330 | 1515.15151515152 | 1515 | 63892 |
349 | 1432.66475644699 | 1433 | 63981 |
370 | 1351.35135135135 | 1351 | 64070 |
392 | 1275.51020408163 | 1276 | 64151 |
415 | 1204.81927710843 | 1205 | 64228 |
440 | 1136.36363636364 | 1136 | 64303 |
466 | 1072.96137339056 | 1073 | 64372 |
494 | 1012.14574898785 | 1012 | 64438 |
523 | 956.022944550669 | 956 | 64499 |
554 | 902.527075812274 | 903 | 64556 |
587 | 851.788756388416 | 852 | 64612 |
622 | 803.858520900322 | 804 | 64664 |
659 | 758.725341426404 | 759 | 64712 |
698 | 716.332378223496 | 716 | 64759 |
740 | 675.675675675676 | 676 | 64802 |
784 | 637.755102040816 | 638 | 64844 |
831 | 601.684717208183 | 602 | 64883 |
880 | 568.181818181818 | 568 | 64920 |
932 | 536.480686695279 | 536 | 64954 |
988 | 506.072874493927 | 506 | 64987 |
D大调
D大调频率(Hz) | 初值 |
---|---|
523 | 64499 |
554 | 64556 |
587 | 64612 |
622 | 64664 |
659 | 64712 |
698 | 64759 |
740 | 64802 |
784 | 64844 |
831 | 64883 |
880 | 64920 |
932 | 64954 |
988 | 64987 |
1047 | 65017 |
1109 | 65047 |
1175 | 65074 |
1245 | 65100 |
1319 | 65125 |
1397 | 65148 |
1480 | 65169 |
1568 | 65190 |
1661 | 65209 |
1760 | 65228 |
1865 | 65245 |
1976 | 65261 |
2093 | 65277 |
2217 | 65291 |
2349 | 65305 |
2489 | 65318 |
2637 | 65330 |
2794 | 65342 |
2960 | 65353 |
3136 | 65363 |
3322 | 65372 |
3520 | 65382 |
3729 | 65391 |
3951 | 65398 |
使用蜂鸣器播放音乐
不同调对应的初值
16位工作模式
// B大调的低、中、高音频率对应的定时器初值(频率有点低,会有杂音)
unsigned int code freq_table_b[] = {
0, // 休止符
61394,61633,61846,62058,62248,62436,62603,62768,62927,63070,63207,63340,
63466,63584,63697,63803,63902,63995,64082,64162,64238,64311,64379,64444,
64506,64564,64618,64669,64718,64763,64807,64847,64886,64923,64958,64990,
};
// C大调的低、中、高音频率对应的定时器初值(推荐使用)
unsigned int code freq_table_c[] = {
0, // 休止符
63466,63577,63690,63791,63892,63981,64070,64151,64228,64303,64372,64438,
64499,64556,64612,64664,64712,64759,64802,64844,64883,64920,64954,64987,
65017,65047,65074,65100,65125,65148,65169,65190,65209,65228,65245,65261,
};
// D大调的低、中、高音频率对应的定时器初值(声音太尖锐)
unsigned int code freq_table_d[] = {
0, // 休止符
64499,64556,64612,64664,64712,64759,64802,64844,64883,64920,64954,64987,
65017,65047,65074,65100,65125,65148,65169,65190,65209,65228,65245,65261,
65277,65291,65305,65318,65330,65342,65353,65363,65372,65382,65391,65398,
};
歌曲
// 休止符
#define cease 0
// 低音的12个键
#define a1 1
#define a1_ 2
#define a2 3
#define a2_ 4
#define a3 5
#define a4 6
#define a4_ 7
#define a5 8
#define a5_ 9
#define a6 10
#define a6_ 11
#define a7 12
// 中音的12个键
#define b1 13
#define b1_ 14
#define b2 15
#define b2_ 16
#define b3 17
#define b4 18
#define b4_ 19
#define b5 20
#define b5_ 21
#define b6 22
#define b6_ 23
#define b7 24
// 高音的12个键
#define c1 25
#define c1_ 26
#define c2 27
#define c2_ 28
#define c3 29
#define c4 30
#define c4_ 31
#define c5 32
#define c5_ 33
#define c6 34
#define c6_ 35
#define c7 36
// 歌曲:演员
unsigned int code music_yanyuan[]={
/*
三个为一组,分别代表:音符,时值,不同音符之间是否有延音
*/
//1
a3,2,0,
a5,2,0,
b4,2,0,
b3,4,0,
a5,2,0,
b3,2,0,
b2,2,0,
cease,2,0,
a5,2,0,
b2,2,0,
b1,4,0,
a7,2,0,
b1,2,0,
b2,2,0,
a3,2,0,
a5,2,0,
b4,2,0,
b3,4,0,
a5,2,0,
b3,2,0,
b2,2,0,
cease,2,0,
a5,2,0,
b2,2,0,
b1,4,0,
a7,2,0,
b1,2,0,
b2,2,0,
cease,2,0,
a6,2,0,
b5,2,0,
b4,4,0,
a6,2,0,
b4,2,0,
b3,2,0,
//2
cease,2,0,
a5,2,0,
b3,2,0,
b2,4,0,
a5,2,0,
b2,2,0,
b1,2,0,
a3,2,0,
a5,2,0,
b4,2,0,
b3,4,0,
a5,2,0,
b3,2,0,
b2,2,0,
b2,4+4,0,
cease,2,0,
a5,2,0,
b4,2,0,
b3,2+4+4,0,//->
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b1,2,0,
b1,2,0,
a6,2,1,
a7,2+2,0,//->
//3
a6,2+4+4+4,0,
cease,2,0,
b3,2,0,
b3,2,0,
b3,2,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
b3,1,0,
b3,1,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
b1,2,0,
b2,2,0,
b1,2,0,
b2,2,0,
b1,2,0,
//4
b2,2+1,0,
b3,1+4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
a5,2,0,
b4,2,0,
b3,2+4+4,0,//->
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b2,2,0,
b3,1,0,
b5,1+2,0,
b1,2,0,
b1,2,0,
a6,2,0,
a7,2+1,0,
a6,1+4+4+4,0,
//5
cease,4,0,
b3,2,0,
b3,2,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
b3,1,0,
b3,1,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
a5,1,0,
a5,1,0,
b3,2,0,
b2,2,0,
b3,2,0,
b2,2+2,0,//->
b1,4+2,0,
cease,4,0,
cease,4,0,
//6
cease,4+2,0,
c5,1,0,
c5,1,0,
c5,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
c5,2+1,0,
b6,1+2,0,
b6,2,0,
b6,2,0,
b7,2,0,
c1,2,0,
b7,2+4+2,0,//->
c5,2,0,
c5,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
c5,2,0,
b7,2,0,
b7,2,0,
b7,2,0,
b7,2,0,
c1,2,0,
c2,2,0,
c1,2+4+2,0,//->
//7
c3,1,0,
c3,1,0,
c4,2,0,
c3,2,0,
c4,2,0,
c3,2,0,
c4,2+1,0,
b6,1+2,0,
b6,2,0,
b6,2,0,
b4,2,0,
c1,2,0,
b7,2+4+2,0,//->
c1,1,0,
c1,1,0,
c2,2,0,
c1,2,0,
c2,2,0,
c1,2,0,
c2,2,0,
c1,2,0,
c2,2,0,
c1,2,0,
c2,2,0,
c1,2,0,
c2,2,0,
c4,2+2,0,//->
//8
c3,4,0,
c5,1,0,
c5,1,0,
c5,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
c5,2,0,
b6,2,0,
b6,2,0,
b6,2,0,
b6,2,0,
b7,2,0,
c1,2,0,
b7,2+4+2,0,//->
c5,1,0,
c5,1,0,
c5,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
//9
c5,2,0,
b7,2,0,
b7,2,0,
b7,2,0,
b7,2,0,
b5,2,0,
c2,2,0,
c1,2+4+2,0,//->
c3,1,0,
c3,1,0,
c4,2,0,
c3,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
b6,2,0,
b6,2,0,
b6,2,0,
b6,2,0,
b4,2,0,
c1,2,0,
b7,2+4+4,0,//->
b7,2,0,
b5,2,0,
c2,2,0,
c1,2+4+4+4+4,0,
//10
cease,4,0,
cease,4,0,
cease,2,0,
a5,2,0,
b4,2,0,
b3,2+4+4,0,//->
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b2,2,0,
b3,1,0,
b5,1+2,0,
b1,2,0,
b1,2,0,
a6,1,1,
a7,1,0,
a7,2+1,1,
a6,1+4+4+4,0,
//11
cease,4,0,
cease,2,0,
b3,2,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
b3,2,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,
b2,4,0,
a5,2,0,
b3,2,0,
b2,2,0,
b3,2,0,
b2,2+2,0,//->
b1,4+2,0,
cease,4,0,
cease,4,0,
// 终止标准,当music_select等于这个值时说明音乐已经播放完了
0xff
};
// 休止符
#define cease 0
// 低音的12个键
#define a1 1
#define a1_ 2
#define a2 3
#define a2_ 4
#define a3 5
#define a4 6
#define a4_ 7
#define a5 8
#define a5_ 9
#define a6 10
#define a6_ 11
#define a7 12
// 中音的12个键
#define b1 13
#define b1_ 14
#define b2 15
#define b2_ 16
#define b3 17
#define b4 18
#define b4_ 19
#define b5 20
#define b5_ 21
#define b6 22
#define b6_ 23
#define b7 24
// 高音的12个键
#define c1 25
#define c1_ 26
#define c2 27
#define c2_ 28
#define c3 29
#define c4 30
#define c4_ 31
#define c5 32
#define c5_ 33
#define c6 34
#define c6_ 35
#define c7 36
// 歌曲:夜空中最亮的星
unsigned int code music_xin[] = {
// 1
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
//2
b3,2,0,
b2,2,0,
b3,2,0,
b2,2,0,
b3,2,0,
b5,2,0,
b5,4+4,0,//->
//3
cease,4,0,
cease,4,0,
cease,2+1,0,
a5,1,0,
b1,4+2,0,
b1,1,1,
b2,1+2,0,
b1,2+4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b1,2+4,0,
cease,2,0,
a5,2,0,
//4
b1,2,0,
b2,2,0,
b3,2,0,
b1,2+2,0,
a5,2+2,0,
b2,2+4+2,0,//->
b3,2+4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
a5,2,0,
b3,2,0,
b2,2,0,
b3,2,0,
b2,2,0,
b3,2,0,
b5,2,0,
b5,4+4,0,//->
//5
cease,4,0,
cease,4,0,
cease,2,0,
a5,2,0,
b1,4+2,0,
b1,1,1,
b2,1+2,0,
b1,2+4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2+1,0,
b1,1+4,0,
cease,2,0,
a5,2,0,
//6
b1,2,0,
b2,2,0,
b3,2,0,
b1,2+2,0,
a5,2+2,0,
b5,2+2,0,//->
b3,2+4+2,1,
b4_,2+2,1,
b5,2+4+2,0,//->
a5,2,0,
b3,2,0,
b2,2+2,0,
b1,2+2,0,//->
b1,2+2,0,
b1,2+2,0,
b1,2,0,
a6,2,0,
a5,2,0,
//7
b5,2,0,
b6,2+2,0,
b6,2+4,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b5,2+2,0,
b1,2+2,0,
b2,2+4+4+2,1,//->
b1,2+2,1,
a7,2,0,
cease,4,0,
b1,2,0,
b1,2,0,
b1,2,0,
b1,2,0,
a6,2,0,
a5,2,0,
//8
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
a5,2,0,
b5,2,0,
b3,2,1,
b2,2,0,
b2,2+4+4,0,//->
b3,2,0,
b2,2+2,0,
b1,2+4,0,
b1,4,0,
b1,2,0,
b1,2,0,
a6,2,0,
a5,2,0,
//9
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
a5,2,0,
b5,2,0,
b1,2+2,0,
b2,2+4+4,0,//->
cease,2,0,
b2,2,1,
b3,2,1,
b1,2+4,0,
b1,2,0,
b1,2,0,
b1,4+2,0,
a6,2,0,
//10
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b5,2+2,0,
b3,2+2,0,
b2,2+4+4+4+4,0,//->
cease,4,0,
b1,4,0,
b2,4,0,
b3,4,0,
b1,4,0,
b2,4,0,
b3,4,0,
b5,4,0,
//11
cease,4,0,
b1,4,0,
b2,4,0,
b3,4,0,
b1,4,0,
b2,4,0,
b3,4,0,
b5,4,0,
b3,2,0,
b2,2,0,
b3,2,0,
b2,2,0,
b3,2,0,
b5,2,0,
b5,4+4,0,//->
cease,4,0,
cease,4,0,
cease,2+1,0,
a5,1,0,
b1,4+2,0,
b1,1,1,
b2,1+2,0,
b1,2+4,0,
//12
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b1,2+4,0,
cease,2,0,
a5,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b1,2+2,0,
a5,2+2,0,
b2,2+4+2,0,//->
b3,2+4+4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
a5,2,0,
//13
b3,2,0,
b2,2,0,
b3,2,0,
b2,2,0,
b3,2,0,
b5,2,0,
b5,4+4,0,//->
cease,4,0,
cease,4,0,
cease,2,0,
a5,2,0,
b1,4+2,0,
b1,1,1,
b2,1+2,0,
b1,2+4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
//14
b1,2,0,
b2,2,0,
b2,2,0,
b3,2,0,
b1,4,0,
cease,2,0,
a5,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b1,2+2,0,
b5,2+2,0,
b3,2+4+4+2,0,//->
b4_,2,1,
b4,2,0,
b5,2+4+2,0,//->
a5,2,0,
b3,2,0,
b2,2+2,0,
b1,2+2,0,//->
//15
b1,2+2,0,
b1,2+2,0,
b1,2,0,
a6,2,0,
a5,2,0,
b5,2,0,
b6,2+2,0,
b6,2+4,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b5,2,0,
b5,2,0,
b1,2+2,0,
b2,2+4+4+2,1,//->
b1,2+2,1,
a7,2,0,
//16
cease,4,0,
b1,2,0,
b1,2,0,
b1,2,0,
b1,2,0,
a6,2,0,
a5,2,0,
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
a5,2,0,
b5,2,0,
b3,2,1,
b2,2,0,
b2,2+4+4,0,//->
b3,2,0,
b2,2+2,0,
b1,2+4,0,//->
//17
b1,4,0,
b1,2,0,
b1,2,0,
a6,2,0,
a5,2,0,
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b5,2,0,
b5,2,0,
b1,2+2,0,
b2,2+4+4,0,//->
cease,2,0,
b2,2,1,
b3,2,1,
b1,2+4,0,//->
//18
b1,2,0,
b1,2,0,
b1,4+2,0,
a6,2,0,
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2+2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b5,2+2,0,
b3,2+2,0,
b2,2+4+4+4+4,0,//->
b7,4,0,
c1,4,0,
b3,4,0,
b5,4,0,
//19
c5,4+4,0,
c4,4+4,0,
c3,4+4+4,0,
c4,2,0,
c3,2,0,
c2,4+4+4+4,0,
c1,4,0,
b7,4,0,
c1,4,0,
b3,4,0,
c6,4+4+4,0,
c5,4,0,
c4,4,0,
//20
c5,4+4+4+4,0,
////
c5,4+2,0,
a5,2,0,
b3,2,0,
b2,2+2,0,
b1,2+2,0,//->
b1,2+2,0,
b1,2+2,0,
b1,2,0,
a6,2,0,
a5,2,0,
b5,2,0,
b6,2+2,0,
b6,2+4,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b5,2+2,0,
b1,2+2,0,
b2,2+4+4+2,1,//->
b1,2+2,1,
a7,2,0,
cease,4,0,
b1,2,0,
b1,2,0,
b1,2,0,
b1,2,0,
a6,2,0,
a5,2,0,
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
a5,2,0,
b5,2,0,
b3,2,1,
b2,2,0,
b2,2+4+4,0,//->
b3,2,0,
b2,2+2,0,
b1,2+4,0,//->
b1,4,0,
b1,2,0,
b1,2,0,
a6,2,0,
a5,2,0,
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b5,2,0,
b5,2,0,
b1,2+2,0,
b2,2+4+4,0,//->
cease,2,0,
b2,2,1,
b3,2,1,
b1,2+4,0,//->
b1,2,0,
b1,2,0,
b1,4+2,0,
a6,2,0,
b5,2,0,
b6,2+2,0,
b6,2+2,0,
b1,2+2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b5,2+2,0,
b3,2+2,0,
b2,2+4+4+4+4,0,//->
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
b3,2,0,
b2,2,0,
b3,2,0,
b2,2,0,
b3,2,0,
b5,2,0,
b5,4+4,0,
cease,4,0,
cease,4,0,
cease,2+1,0,
a5,1,0,
b1,4+2,0,
b1,1,1,
b2,1+2,0,
b1,2+4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b1,2+4,0,
cease,2,0,
a5,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b1,2+2,0,
a5,2,0,
cease,4,0,
b2,4+2,0,
b3,2+4+4,0,
0xff
};
示例代码
#include <REGX52.H>
#include "timer0_mode1_init.h"
#include "delay.h"
// 休止符
#define cease 0
// 低音的12个键
#define a1 1
#define a1_ 2
#define a2 3
#define a2_ 4
#define a3 5
#define a4 6
#define a4_ 7
#define a5 8
#define a5_ 9
#define a6 10
#define a6_ 11
#define a7 12
// 中音的12个键
#define b1 13
#define b1_ 14
#define b2 15
#define b2_ 16
#define b3 17
#define b4 18
#define b4_ 19
#define b5 20
#define b5_ 21
#define b6 22
#define b6_ 23
#define b7 24
// 高音的12个键
#define c1 25
#define c1_ 26
#define c2 27
#define c2_ 28
#define c3 29
#define c4 30
#define c4_ 31
#define c5 32
#define c5_ 33
#define c6 34
#define c6_ 35
#define c7 36
// 控制蜂鸣器发声的IO口
sbit buzzer=P2^5;
// 当前歌曲四分音符的时值,根据实际情况调整
unsigned int quarter_time=600;
// freq_select:选择对应的频率
// music_select:曲子对应的音符、时值、不同音符之间是否有延音
unsigned int freq_select, music_select;
// B大调的低、中、高音频率对应的定时器初值(频率有点低,会有杂音)
unsigned int code freq_table_b[] = {
0, // 休止符
61394,61633,61846,62058,62248,62436,62603,62768,62927,63070,63207,63340,
63466,63584,63697,63803,63902,63995,64082,64162,64238,64311,64379,64444,
64506,64564,64618,64669,64718,64763,64807,64847,64886,64923,64958,64990,
};
// C大调的低、中、高音频率对应的定时器初值(推荐使用)
unsigned int code freq_table_c[] = {
0, // 休止符
63466,63577,63690,63791,63892,63981,64070,64151,64228,64303,64372,64438,
64499,64556,64612,64664,64712,64759,64802,64844,64883,64920,64954,64987,
65017,65047,65074,65100,65125,65148,65169,65190,65209,65228,65245,65261,
};
// D大调的低、中、高音频率对应的定时器初值(声音太尖锐)
unsigned int code freq_table_d[] = {
0, // 休止符
64499,64556,64612,64664,64712,64759,64802,64844,64883,64920,64954,64987,
65017,65047,65074,65100,65125,65148,65169,65190,65209,65228,65245,65261,
65277,65291,65305,65318,65330,65342,65353,65363,65372,65382,65391,65398,
};
// 歌曲:演员
unsigned int code music_yanyuan[]={
/*
三个为一组,分别代表:音符,时值,不同音符之间是否有延音
*/
//1
a3,2,0,
a5,2,0,
b4,2,0,
b3,4,0,
a5,2,0,
b3,2,0,
b2,2,0,
cease,2,0,
a5,2,0,
b2,2,0,
b1,4,0,
a7,2,0,
b1,2,0,
b2,2,0,
a3,2,0,
a5,2,0,
b4,2,0,
b3,4,0,
a5,2,0,
b3,2,0,
b2,2,0,
cease,2,0,
a5,2,0,
b2,2,0,
b1,4,0,
a7,2,0,
b1,2,0,
b2,2,0,
cease,2,0,
a6,2,0,
b5,2,0,
b4,4,0,
a6,2,0,
b4,2,0,
b3,2,0,
//2
cease,2,0,
a5,2,0,
b3,2,0,
b2,4,0,
a5,2,0,
b2,2,0,
b1,2,0,
a3,2,0,
a5,2,0,
b4,2,0,
b3,4,0,
a5,2,0,
b3,2,0,
b2,2,0,
b2,4+4,0,
cease,2,0,
a5,2,0,
b4,2,0,
b3,2+4+4,0,//->
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b2,2,0,
b3,2,0,
b5,2+2,0,
b1,2,0,
b1,2,0,
a6,2,1,
a7,2+2,0,//->
//3
a6,2+4+4+4,0,
cease,2,0,
b3,2,0,
b3,2,0,
b3,2,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
b3,1,0,
b3,1,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
b1,2,0,
b2,2,0,
b1,2,0,
b2,2,0,
b1,2,0,
//4
b2,2+1,0,
b3,1+4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,4,0,
cease,2,0,
a5,2,0,
b4,2,0,
b3,2+4+4,0,//->
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b2,2,0,
b3,1,0,
b5,1+2,0,
b1,2,0,
b1,2,0,
a6,2,0,
a7,2+1,0,
a6,1+4+4+4,0,
//5
cease,4,0,
b3,2,0,
b3,2,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
b3,1,0,
b3,1,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
a5,1,0,
a5,1,0,
b3,2,0,
b2,2,0,
b3,2,0,
b2,2+2,0,//->
b1,4+2,0,
cease,4,0,
cease,4,0,
//6
cease,4+2,0,
c5,1,0,
c5,1,0,
c5,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
c5,2+1,0,
b6,1+2,0,
b6,2,0,
b6,2,0,
b7,2,0,
c1,2,0,
b7,2+4+2,0,//->
c5,2,0,
c5,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
c5,2,0,
b7,2,0,
b7,2,0,
b7,2,0,
b7,2,0,
c1,2,0,
c2,2,0,
c1,2+4+2,0,//->
//7
c3,1,0,
c3,1,0,
c4,2,0,
c3,2,0,
c4,2,0,
c3,2,0,
c4,2+1,0,
b6,1+2,0,
b6,2,0,
b6,2,0,
b4,2,0,
c1,2,0,
b7,2+4+2,0,//->
c1,1,0,
c1,1,0,
c2,2,0,
c1,2,0,
c2,2,0,
c1,2,0,
c2,2,0,
c1,2,0,
c2,2,0,
c1,2,0,
c2,2,0,
c1,2,0,
c2,2,0,
c4,2+2,0,//->
//8
c3,4,0,
c5,1,0,
c5,1,0,
c5,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
c5,2,0,
b6,2,0,
b6,2,0,
b6,2,0,
b6,2,0,
b7,2,0,
c1,2,0,
b7,2+4+2,0,//->
c5,1,0,
c5,1,0,
c5,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
//9
c5,2,0,
b7,2,0,
b7,2,0,
b7,2,0,
b7,2,0,
b5,2,0,
c2,2,0,
c1,2+4+2,0,//->
c3,1,0,
c3,1,0,
c4,2,0,
c3,2,0,
c4,2,0,
c3,2,0,
c4,2,0,
b6,2,0,
b6,2,0,
b6,2,0,
b6,2,0,
b4,2,0,
c1,2,0,
b7,2+4+4,0,//->
b7,2,0,
b5,2,0,
c2,2,0,
c1,2+4+4+4+4,0,
//10
cease,4,0,
cease,4,0,
cease,2,0,
a5,2,0,
b4,2,0,
b3,2+4+4,0,//->
cease,4,0,
cease,4,0,
cease,2,0,
b1,2,0,
b2,2,0,
b3,1,0,
b5,1+2,0,
b1,2,0,
b1,2,0,
a6,1,1,
a7,1,0,
a7,2+1,1,
a6,1+4+4+4,0,
//11
cease,4,0,
cease,2,0,
b3,2,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,//->
b2,4,0,
b3,2,0,
b4,2,0,
b3,2,0,
b4,2,0,
a6,2+2,0,
b2,4,0,
a5,2,0,
b3,2,0,
b2,2,0,
b3,2,0,
b2,2+2,0,//->
b1,4+2,0,
cease,4,0,
cease,4,0,
// 终止标准,当music_select等于这个值时说明音乐已经播放完了
0xff
};
void main()
{
// 初始化定时器T0,16位计数器
timer0_mode1_init();
while(1)
{
if(music_yanyuan[music_select]!=0xff)
{
// 取出音符
freq_select=music_yanyuan[music_select];
music_select++;
// 取出时值数,并乘以8分音符的时值
delay(quarter_time/4*music_yanyuan[music_select]);
music_select++;
// 判断不同音符之间是否有延音
if(!music_yanyuan[music_select])
{
// 如果没有那么关闭定时器一段时间,
TR0=0;
delay(5);
TR0=1;
}
music_select++;
}
else
{
// 关闭定时器并无限循环
TR0=0;
while(1);
}
}
}
void Timer0() interrupt 1
{
// 当处于休止符期间时不翻转IO口
if(freq_table_c[freq_select])
{
TH0=freq_table_c[freq_select]/256;
TL0=freq_table_c[freq_select]%256;
buzzer=!buzzer;
}
}
AT24C02存储数据(掉电不丢失)(i²C总线)
存储器介绍
- RAM:可以理解为内存,掉电会丢失数据,容量较小,但是传输速度快
SRAM:内部结构是锁存器,D触发器,使用电路存储数据。读写速度最快的存储器,但是容量少且成本较高。常用于CPU,高速缓存
DRAM:使用电容存储,里面有电时表示高电平(1),没有电时表示低电平(0)。但是电容存在漏电现象,所以需要配一个扫描电路来不停的给他补电,刷新一下。因此叫动态RAM,常用与手机内存、电脑内存
- ROM:硬盘,掉电不丢失数据,容量大,但是传输速度较慢
- Mask ROM:提前把要存储的数据告诉生产厂家,直接生产对应的数据,生产完成后不可修改,只可读
- PROM:只可以烧录一次数据,一旦烧录完成就不可在修改了,只可读
- EPROM:数据烧录完成后可以清除再烧录,不过要使用紫外线照射30分钟才能清除
- E²PROM(EEPROM):不用照射紫外线,使用电也可以清除数据,清除速度也挺快,但是使用并不广泛,因为后面出现的存储器太厉害了
上面四个是一代一代进化过来的,直到现在的EEPROM
- Flash:U盘、内存卡、固态硬盘、手机存储硬盘等等
存储器简化模型
网格直接交汇的地方是不连接的,只有存储数据后才连接起来
假设地址总线的高位在上,数据总线的高位在左
在地址为0x80的地方存一个数据:0x11
Mask ROM:先选中地址总线的第一根线,再把数据总线的第三根线和第八根线分别使用二极管连接在地址总线上,如上图所示,所以这就是为什么只能由厂家在生产时来写入数据的原因
PROM:选中地址总线的第一根线,把数据总线的第三和第八根线与地址总线连接的二极管分别击穿。这也是为什么PROM只能写入一次数据的原因
AT24C02
AT24C02介绍
AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息
- 存储介质:E²PROM(EEPROM)
- 通讯接口:i²C总线
- 容量:256字节
AT24C02引脚及应用电路
WP引脚直接接在GND上了,所以就没有写保护
本开发板的电路,基本一致
AT24C02内部结构
i²C总线
i²C总线介绍
- i²C总线(Inter IC BUS)是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
- 同步、半双工,带数据应答
- 通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说,使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用者来说,使用通用的通信协议可以避免学习各种各样的自定义协议,降低了学习和应用的难度
i²C电路规范
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
- 开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题
i²C时序结构
主机:单片机
从机:AT24C02
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平
发送一个字节
在SCL低电平期间,主机把数据位按从高位到低位的顺序依次放到SDA的线上,然后将SCL切换到高电平,从机开始开始根据SDA的电平读数据,当SDA为高电平时读出来的数据为1,当SDA为低电平的的时候读出来的数据为0,读完一个位再把SCL切换到低电平
在SCL高电平期间SDA不允许有数据变化,依次循环上面的过程8次,即可发送一个字节
接收一个字节
在SCL低电平期间,从机把数据位按从高位到低位的顺序依次放到SDA的线上,然后将SCL切换到高电平,主机开始开始根据SDA的电平读数据,当SDA为高电平时读出来的数据为1,当SDA为低电平的的时候读出来的数据为0,读完一个位再把SCL切换到低电平
在SCL高电平期间SDA不允许有数据变化,依次循环上面的过程8次,即可接受一个字节(注意:主机在接受数据前要释放SDA)
- 发送应答:在接受完一个字节后,主机下一个时钟发送一位数据,判断主机是否应答,数据0表示应答,数据1表示没有应答
- 接收应答:在发送完一个字节后,主机下一个时钟再接收一位数据,判断从机是否应答,数据0表示应答,数据1表示没有应答(主机在接受之前,需要释放SDA)
- 因为上拉电阻的存在,当主机释放SDA后,SDA默认为高电平,所以用数据1表示没有应答
- 可以把这个当作第九位数据,类似校验位
i²C数据帧
结合上面的时序图看
发送一个数据帧
接收一个数据帧
复合格式(先发送再接收)
就是发送和接收两步合成一步,中间只是少了一个结束条件
AT24C02数据帧
-
字节写:
- 第一个绿方框(SLAVE ADDRESS+W)中放的是AT24C02的地址加000再加0,也就是这里的值是固定的:0xA0。
- 第二个绿方框(WORO ADDRESS)放的是我们要写入的数据的地址,范围在0~255,也就是说可以写入255个字节
- 第三个绿方框(DATA)就是我们要写入的数据
-
随机读:
- 第一个绿方框(SLAVE ADDRESS+R)中放的是AT24C02的地址加000再加1,也就是这里的值是固定的:0xA1。
- 第二个绿方框(WORO ADDRESS)放的是我们要读的数据的地址,范围在0~255
-
注意:在写完数据帧后要延迟5ms,原因在下面有说明
分别封装AT24C02和i²C相关代码
注意:对一个位连续操作时要考虑这个芯片能够承受的最快时间。比如把SCL置1后马上又置0,因为我当前使用的单片机晶振为11.0592,时钟周期1us左右,在大部分情况下都不需要加延时函数,只有写数据时的周期要5ms,需要加延时函数
如下图
i2c相关代码
i2c.h
#ifndef __i2c_h__ #define __i2c_h__ // 开始条件 void i2c_start(); // 终止条件 void i2c_stop(); // 主机发送一个字节 void i2c_send_byte(unsigned char byte); // 主机接收一个字节 unsigned char i2c_reception_byte(); // 主机发送应答 void i2c_send_Ack(unsigned char Ack); // 主机接收应答 unsigned char i2c_reception_Ack(); #endif
i2c.c
#include <REGX52.H> sbit i2c_SCL=P2^1; sbit i2c_SDA=P2^0; /* @函数作用:i2c开始 @参数:无 @返回值:无 */ void i2c_start() { // 初始化SCL和SDA i2c_SDA=1; i2c_SCL=1; // SCL高电平期间,SDA从高电平切换到低电平 i2c_SDA=0; // SCL拉低,准备开始接收或发送数据了 i2c_SCL=0; } /* @函数作用:i2c结束 @参数:无 @返回值:无 */ void i2c_stop() { // SCL高电平期间,SDA从低电平切换到高电平 i2c_SDA=0; i2c_SCL=1; i2c_SDA=1; } /* @函数作用:发送一个字节 @参数:byte 一个字节 @返回值:无 */ void i2c_send_byte(unsigned char byte) { unsigned char i; for(i=0;i<8;i++) { // 把数据放到SDA上,从高位开始传,将byte第i个位赋值个SDA i2c_SDA=byte&(0x80>>i); // 拉高SCL开始发送数据位 i2c_SCL=1; // 数据接收完毕 i2c_SCL=0; } } /* @函数作用:接收一个字节 @参数:无 @返回值:unsigned char类型,从机发送给主机的数据 */ unsigned char i2c_reception_byte() { unsigned char i, byte=0x00; // 主机释放SDA i2c_SDA=1; for(i=0;i<8;i++) { // 拉高SCL开始接收数据位 i2c_SCL=1; // 将SDA的状态传给byte的第i个位 if(i2c_SDA){byte|=(0x80>>i);} i2c_SCL=0; } // 返回接收的数据 return byte; } /* @函数作用:主机发送应答 @参数:Ack 主机发送给从机的应答位 @返回值:无 */ void i2c_send_Ack(unsigned char Ack) { // 从机接收主机的应答 i2c_SDA=Ack; i2c_SCL=1; i2c_SCL=0; } /* @函数作用:接收应答 @参数:无 @返回值:从机发送给主机的应答位 */ unsigned char i2c_reception_Ack() { unsigned char Ack; // 主机释放SDA i2c_SDA=1; i2c_SCL=1; // 从机发送的应答 Ack=i2c_SDA; i2c_SCL=0; return Ack; }
AT24C02相关代码
AT24C02.h
#ifndef __AT24C02_h__ #define __AT24C02_h__ // 在地址address的地方写入一个字节 void AT24C02_write_byte(unsigned char address, unsigned Data); // 把地址address中的数据读出来 unsigned char At24C02_read_byte(unsigned char address); #endif
AT24C02.c
#include "i2c.h" // AT24C02的写地址,把最低位置1就是读地址 #define slave_addr 0xa0 /* @函数作用:字节写 @参数:address 要在这个地址上写数据,范围为:0~255 @参数:Data 要写入的数据,一个字节 @返回值:无 */ void AT24C02_write_byte(unsigned char address, Data) { // 起始条件 i2c_start(); // 传入AT24C02的地址,写状态 i2c_send_byte(slave_addr); // 接收应答 i2c_reception_Ack(); // 传入要写数据的地址 i2c_send_byte(address); // 接收应答 i2c_reception_Ack(); // 要写入的数据 i2c_send_byte(Data); // 接收应答 i2c_reception_Ack(); // 停止条件 i2c_stop(); } /* @函数作用:随机读 @参数:address 要读这个地址上数据,范围为:0~255 @返回值:从address地址读出来的数据,一个字节 */ unsigned char At24C02_read_byte(unsigned char address) { // 接收的地址 unsigned Data; // 起始条件 i2c_start(); // 传入AT24C02的地址,写状态 i2c_send_byte(slave_addr); // 接收应答 i2c_reception_Ack(); // 传入要读取的数据的地址 i2c_send_byte(address); // 接收应答 i2c_reception_Ack(); // 起始条件 i2c_start(); // 传入AT24C02的地址,读状态状态(把最低位置1) i2c_send_byte(slave_addr|0x01); // 接收应答 i2c_reception_Ack(); // 读取数据 Data=i2c_reception_byte(); // 发送应答 i2c_send_Ack(1); // 停止条件 i2c_stop(); return Data; }
示例代码
把数据存储在AT24C02中
#include <REGX52.H>
#include "one_key.h" // 扫描四个独立按键,返回对应的键码值
#include "LCD1602.h"
#include "AT24C02.h"
#include "delay.h"
// key_num:独立按键的键码;TH:int类型的高8位;TL:int类型的低8位
unsigned char key_num, TH, TL;
// 记录当前数字
unsigned int num;
void main()
{
LCD_Init();
while(1)
{
// 扫描独立按键
key_num=key();
switch (key_num)
{
// num自增
case 1:
num++;
LCD_ShowNum(1,1,num,5);
break;
// num自减
case 2:
num--;
LCD_ShowNum(1,1,num,5);
break;
// 将int类型的num的高8位和低8位分别存放到地址0、1中
case 3:
TH=num/256;
TL=num%256;
AT24C02_write_byte(0, TH);
delay(5);
AT24C02_write_byte(1, TL);
delay(5);
LCD_ShowString(1,1,"WRITE OK");
delay(1000);
LCD_ShowString(1,1," ");
LCD_ShowNum(1,1,num,5);
break;
// 将地址0、1中的数据分别读出来并显示在LCD上
case 4:
TH=At24C02_read_byte(0);
TL=At24C02_read_byte(1);
num=TH*256+TL;
LCD_ShowString(1,1,"READ OK");
delay(1000);
LCD_ShowString(1,1," ");
LCD_ShowNum(1,1,num,5);
break;
}
}
}
秒表(定时器扫描按键和数码管,并将数据记录在AT24C02中)
定时器扫描独立按键的代码
timer_one_key.c
#include <REGX52.H>
// key_temp:按键按下又松开的值
// now_key_num:这次运行函数扫描到的按键值
// last_key_num:上次运行函数扫描到的按键值
unsigned char key_temp, now_key_num=0, last_key_num=0;
// 获取独立按键的按键值
unsigned char key_scan()
{
if(P3_1==0){return 1;}
if(P3_0==0){return 2;}
if(P3_2==0){return 3;}
if(P3_3==0){return 4;}
return 0;
}
/*
@函数作用:在定时器中调用该函数,每调用一次就扫描一个数码管
@参数:无
@返回值:无
*/
void key_loop()
{
// 把last_key_num赋值为上次函数运行时的按键值
last_key_num=now_key_num;
// now_key_num赋值为当前的按键值
now_key_num=key_scan();
// 当last_key_num不为0且now_key_num为0说明该按键被按下加松开
if(last_key_num != 0 && now_key_num == 0)
{
key_temp = last_key_num;
}
}
unsigned char key()
{
// key_temp的值返回前将他置0
unsigned char temp;
temp=key_temp;
key_temp=0;
return temp;
}
timer_one_key.h
#ifndef __timer_one_key_h__
#define __timer_one_key_h__
/*
利用定时器扫描独立按键
*/
// 返回独立按键的键码
unsigned char key();
// 扫描一次独立按键,在定时器中每隔20ms运行一次该函数
void key_loop();
// 使用模板
//void Timer0() interrupt 1
//{
// // 记录中断次数
// static unsigned int T0_count;
// // 每触发一次中断都要重新配置计数器的初始值
// TH0 = 0xfc;
// TL0 = 0x66;
// // 中断次数自增
// T0_count++;
// // 每20ms扫描一次独立按键
// if(T0_count == 20)
// {
// T0_count =0;
// key_loop();
// }
//}
#endif
定时器扫描数码管的代码
timer_nixie.c
#include <REGX52.H>
// 从左到右分别显示8个数码管,初始化什么都不显示
unsigned char nixie_show[] = {10,10,10,10,10,10,10,10};
// 索引0~9:显示数字0~9。索引10表示什么都不显示。索引11:显示一个横杠
unsigned char nixie_table[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x00,0x40};
/*
@函数作用:显示字符或数字到数码管上
@参数:location 要显示在第几个数码管上,从左到右数
@参数:num 要显示的字符索引,对应nixie_table,范围:0~11
@返回值:无
*/
void nixie(unsigned char location, num)
{
// nixie_show数组索引从0开始的,所以要减一
nixie_show[location-1]=num;
}
/*
@函数作用:扫描数码管
@参数:location 要显示在第几个数码管上,从左到右数
@参数:num 要显示的字符索引,对应nixie_table,范围:0~11
@返回值:无
*/
void nixie_scan(unsigned char location, num)
{
// 每2ms运行一下该函数也相当于延时了,只需要在函数的开始把数码管熄灭,也可以实现段选和位选相匹配
P0=0x00;
// 经过74HC138译码器选择点亮第几个数码管
switch(location)
{
case 8:P2_4=0;P2_3=0;P2_2=0;break;
case 7:P2_4=0;P2_3=0;P2_2=1;break;
case 6:P2_4=0;P2_3=1;P2_2=0;break;
case 5:P2_4=0;P2_3=1;P2_2=1;break;
case 4:P2_4=1;P2_3=0;P2_2=0;break;
case 3:P2_4=1;P2_3=0;P2_2=1;break;
case 2:P2_4=1;P2_3=1;P2_2=0;break;
case 1:P2_4=1;P2_3=1;P2_2=1;break;
}
// 显示对应索引的字符
P0=nixie_table[num];
}
/*
@函数作用:写在定时器中,每一段时间执行一次扫描数码管的操作
@参数:无
@返回值:无
*/
void nixie_loop()
{
static unsigned char i = 0;
// 调用扫描数码管的函数
nixie_scan(i+1, nixie_show[i]);
i++;
if(i==8){i=0;};
}
timer_nixie.h
#ifndef __timer_nixie_h__
#define __timer_nixie_h__
// 在对应的位置上显示字符
void nixie(unsigned char location, num);
// 写在定时器中
void nixie_loop();
// 使用模板
//void Timer0() interrupt 1
//{
// // 记录中断次数
// static unsigned int T0_count;
// // 每触发一次中断都要重新配置计数器的初始值
// TH0 = 0xfc;
// TL0 = 0x66;
// // 中断次数自增
// T0_count++;
// // 每2ms扫描一次数码管
// if(T0_count == 2)
// {
// T0_count =0;
// nixie_loop();
// }
//}
#endif
main.c代码
#include <REGX52.H>
#include "timer_one_key.h"
#include "AT24C02.h"
#include "delay.h"
#include "timer0_mode1_init.h"
#include "timer_nixie.h"
// 独立按键键码、分、秒、秒后面的两位
unsigned char key_num, min, sec, ms;
// 开始标志位
unsigned char run_flag;
void main()
{
// 初始化定时器
timer0_mode1_init();
while(1)
{
// 获取独立按键键码
key_num=key();
switch (key_num)
{
// 当按键按下1对开始标志位取反。为1时开始记时,为0时暂停记时
case 1:
run_flag = !run_flag;
break;
// 当按键按下2计时器清零
case 2:
min=0;
sec=0;
ms=0;
break;
// 按键按下3把当前记的时存放到AT24C02中
case 3:
AT24C02_write_byte(1,min);
delay(5);
AT24C02_write_byte(2,sec);
delay(5);
AT24C02_write_byte(3,ms);
delay(5);
break;
// 按键按下4把把AT24C02中的数据取出来
case 4:
min=At24C02_read_byte(1);
sec=At24C02_read_byte(2);
ms=At24C02_read_byte(3);
}
// 显示时间
nixie(1,min/10);
nixie(2,min%10);
nixie(3,11);
nixie(4,sec/10);
nixie(5,sec%10);
nixie(6,11);
nixie(7,ms/10);
nixie(8,ms%10);
}
}
void time_loop()
{
// 当开始标志位为1时开始记时
if(run_flag==1)
{
ms++;
if(ms==100)
{
ms=0;
sec++;
}
if(sec==60)
{
sec=0;
min++;
}
if(min==60)
{
min=0;
}
}
}
void Timer0() interrupt 1
{
// 记录中断次数
static unsigned int T0_count1, T0_count2, T0_count3;
// 每触发一次中断都要重新配置计数器的初始值
TH0 = 0xfc;
TL0 = 0x66;
// 中断次数自增
T0_count1++;
T0_count2++;
T0_count3++;
if(T0_count1 == 20)
{
T0_count1 =0;
key_loop();
}
if(T0_count2 == 2)
{
T0_count2 =0;
nixie_loop();
}
// 每10ms秒表的最低位加一
if(T0_count3 == 10)
{
T0_count3=0;
time_loop();
}
}
DS18B20数字温度传感器(单总线)
DS18B20
DS18B20介绍
DS18B20是一种常见的数字温度传感器,其控制命令和数据都是以数字信号的方式输入输出,相比较于模拟温度传感器,具有功能强大、硬件简单、易扩展、抗干扰强等特点
- 测温范围:-55度到+125度
- 通信接口:1-Wire(单总线)
- 其他特征:可形成总线结构、内置温度报警功能、可寄生供电
引脚及应用电路
本开发板原理图
内部结构框图
存储器结构
左边的SCRATCHPAD在上面的内部结构框图中有画出来,暂存器,与总线进行数据交互
右边的EEPROM对应内部结构框图中的内部设备,从上到下分别是:存放温度阈值上限的存储器、存放温度阈值下限的存储器、存放温度测量精度的存储器
SCRATCHPAD(暂存器),一共可以存储九个字节
-
Byte0和Byte1:共同组成温度的数据,默认值为85摄氏度
Byte0:最低有效字节
Byte1:最高有效字节
-
Byte2和Byte3:温度阈值的上下限
这两个与EEPROM的TH和TL是相对应的,我们不能直接把数据写到EEPROM中,要先把数据放到暂存器中,再通过一条指令把数据赋值到EEPROM中,对数据进行永久存储
也可以通过另一条指令把EEPROM中的数据读到暂存器中
-
Byte4:配置寄存器,与Byte2和Byte3是同样的,只是对数据进行暂存
在初始转态下默认的精度是12位,即R0=1、R1=1
-
Byte5~Byte7:保留位,以后器件升级了可能会用到
-
Byte8:校验位。芯片会对前面八个字节进行一些特殊的运算,算出一个校验位跟在后面。我们把前面的8个字节读出来了之后经过同样的运算,看是不是与校验位相同,来保证数据的正确
单总线(1-Wire BUS)
单总线(1-Wire BUS)介绍
单总线(1-Wire BUS)是由Dallas公司开发的一种通用数据总线
- 一根通信线:DQ
- 异步、半双工
- 单总线只需要一根通信线即可实现数据的双向传输,当采用寄生供电时,还可以省去设备的VDD线路,此时,供电加通信只需要DQ和GND两根线
单总线电路规范
- 设备的DQ均要配置成开漏输出模式
- DQ添加一个上拉电阻,阻值一般为4.7KΩ左右
- 若此总线的从机采取寄生供电,则主机还应配一个强上拉输出电路
单总线时序结构
-
初始化:
主机将总线拉低至少480us,然后释放总线,等待15~60us后。
如果从机存在,则会拉低总线60~240us以响应主机,之后从机释放总线
-
发送一位数据(写入):
-
主机将总线拉低60~120us,然后释放总线,表示发送0
-
主机将总线拉低1~15us,然后释放总线,表示发送1
-
从机(DS18B20)会在主机将总线拉低电平30us后读取总线的电平,此时的电平就是要写入的数据
-
写入一位数据的时间应大于60us
-
-
接受一位数据(读取):
- 主机将总线拉低1~15us,然后释放总线,表示准备好了,可以开始接收数据了
- 在拉低电平后的15us内读取总线的电平(尽量贴近15us的末尾)。读取的低电平则接收的数据为0;读取高电平则接收的数据为1;
- 整个时间片应大于60us
-
发送和接受一个字节:
- 发送一个字节:连续调用8次发送一位的时序
- 接受一个字节:连续调用8次接受一位的时序
- 注意:低位在前(i²c总线是高位在前)
DS18B20流程操作
- 初始化:从机复位,主机判断从机是否响应
- ROM操作:ROM指令+本指令需要的读写操作
- 功能操作:功能指令+本指令需要的读写操作
结合上面的DS18B20的内部结构框图和存储器结构来看。
ROM指令相当于操作:64-BIT ROM AND 1-Wire PORT,用于总线寻址
ROM指令
ROM指令 | 翻译 | 对应的字节 |
---|---|---|
SEARCH ROM | 搜寻ROM | 0xF0 |
READ ROM | 读ROM | 0x33 |
MATCH ROM | 匹配ROM(指令后面紧跟一个设备的地址,对该设备进行单独通信) | 0x55 |
SKIP ROM | 跳过ROM(当只有一个设备的时候使用) | 0xCC |
ALARM SEARCH | 报警搜索(寻找温度超过阈值的设备) | 0xEC |
功能指令也叫RAM指令,用于操作SCRATCHPAD中的数据和EEPROM中的数据
功能指令
功能指令 | 翻译 | 对应的字节 |
---|---|---|
CONVERT T | 温度变换(更新温度数据到暂存器中) | 0x44 |
WRITE SCRATCHPAD | 写暂存器(把紧跟着的三个数据写入暂存器中的Byte2~Byte4中) | 0x4E |
READ SCRATCHPAD | 读暂存器(把暂存器中的数据一个字节一个字节的读出来,从Byte0开始) | 0xBE |
COPY SCRATCHPAD | 复制暂存器(把暂存器中的数据写入到EEPROM中) | 0x48 |
RECALL E2 | 把EEPROM中的数据写到暂存器中 | 0xB8 |
READ POWER SUPPLY | 读取设备的供电模式(是否为寄生供电) | 0xB4 |
DS18B20数据帧
更新温度数据:初始化--->跳过ROM指令(只有一个设备,所以直接跳过ROM)--->温度变换指令
读取温度数据:初始化--->跳过ROM--->读暂存器指令--->连续的读操作(根据要获取的数据读相应的次数,例如只要温度的数据,只需要连续读两次就够了,也就是把Byte0和Byte1中的数据读出来)
温度存储格式及计算方法
当温度转换命令(0x44)发布后,经转换所得的温度以二进制补码形式存放在暂存器的Byte0和Byte1中。存储的两个字节,高字节的前5位是符号位s,单片机可通过单线接口读到该数据,读取时低位在前,高位在后,数据格式如下。
如果测得的温度大于0,高5位为0,只要将测到的数值乘以0.0625(默认精度是12位)即可得到实际温度。
如果测得的温度小于0,高5位为1,测得的数值要按位取反,加一,再乘以0.0625
封装单总线和DS18B20相关代码
利用STC-ISP生成微秒级别的延时
调用一次函数的时间为5us
如果不想调用函数,直接延时,就把原本要延时的时间加上5us的函数调用时间,再把函数中的代码赋值到程序中。
例如我要延时5us,但是不想通过函数的方式,那么只需要在STC-ISP中的定时长度中输入10us,再把函数中的代码复制
单总线相关代码
注意:单总线通信时,要求通信的双方都遵循同样的时间来收发数据。但是如果使用了定时器来触发中断,会导致主机在通信时被中断去执行其他任务,从而导致数据传输时出现错误。
所以在双方通信期间要关闭所有中断
one_wire.c
#include <REGX52.H>
// 单总线控制引脚
sbit one_wire_DQ = P3^7;
/*
@函数作用:初始化单总线
@参数:无
@返回值:0或1。当从机响应主机时返回0,没有响应则返回1
*/
unsigned char one_wire_init()
{
/*
主机将总线拉低至少480us,然后释放总线,等待15~60us后。
如果从机存在,则会拉低总线60~240us以响应主机,之后从机释放总线
*/
unsigned char i, Ack;
// 通信期间关闭中断
EA=0;
one_wire_DQ=1;
one_wire_DQ=0;
i = 230;while (--i); // 延时500us
one_wire_DQ=1;
i = 32;while (--i); // 延时70us
Ack=one_wire_DQ;
i = 110;while (--i); // 延时240us
// 通信完了再开启中断
EA=1;
return Ack;
}
/*
@函数作用:发送一位数据
@参数:一位数据,0或1
@返回值:无
*/
void one_wire_write_bit(unsigned char Bit)
{
/*
主机将总线拉低60~120us,然后释放总线,表示发送0
主机将总线拉低1~15us,然后释放总线,表示发送1
*/
unsigned char i;
EA=0;
one_wire_DQ=0;
i = 4;while (--i); // 延时10us
// 当Bit为0时,10us后总线还是低电平状态,状态不变,表示发送0
// 当Bit为1时,10us后总线就被拉高,表示发送1
one_wire_DQ=Bit;
i = 23;while (--i); // 延时50us
one_wire_DQ=1;
EA=1;
// 整个时间片在60us左右
}
/*
@函数作用:接收一位数据
@参数:无
@返回值:一位数据,0或1
*/
unsigned char one_wire_read_bit()
{
/*
主机将总线拉低1~15us,然后释放总线,表示准备好了,可以开始接收数据了
在拉低电平后的15us内读取总线的电平(尽量贴近15us的末尾)。
读取的低电平则接收的数据为0;读取高电平则接收的数据为1;
*/
unsigned char i, Bit;
EA=0;
one_wire_DQ=0;
i = 2;while (--i); // 延时5us
one_wire_DQ=1; // 主机释放总线
i = 3;while (--i); // 延时7us
Bit=one_wire_DQ; // 读取总线电平
i = 22;while (--i); // 延时48us
EA=1;
return Bit;
}
/*
@函数作用:发送一个字节
@参数:要发送的字节
@返回值:无
*/
void one_wire_write_byte(unsigned char byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
one_wire_write_bit(byte&(0x01<<i));
}
}
/*
@函数作用:接收一个字节
@参数:无
@返回值:接收到的一个字节
*/
unsigned char one_wire_read_byte()
{
unsigned char i, byte=0x00;
for(i=0;i<8;i++)
{
if(one_wire_read_bit()){byte|=(0x01<<i);}
}
return byte;
}
one_wire.h
#ifndef __one_wire_h__
#define __one_wire_h__
// 初始化单总线
unsigned char one_wire_init();
// 发送一位数据
void one_wire_write_bit(unsigned char Bit);
// 接收一位数据
unsigned char one_wire_read_bit();
// 发送一个字节
void one_wire_write_byte(unsigned char byte);
// 接收一个字节
unsigned char one_wire_read_byte();
#endif
DS18B20相关代码
DS18B20.c
#include <REGX52.H>
#include "one_wire.h"
// ROM指令
#define SEARCH_ROM 0xF0 // 搜寻ROM
#define READ_ROM 0x33 // 读ROM
#define MATCH_ROM 0x55 // 匹配ROM
#define SKIP_ROM 0xCC // 跳过ROM
#define ALARM_SEARCH 0xEC // 报警搜索
// 功能指令(RAM指令)
#define CONVERT_T 0x44 //温度变换
#define WRITE_SCRATCHPAD 0x4E //写暂存器
#define READ_SCRATCHPAD 0xBE //读暂存器
#define COPY_SCRATCHPAD 0x48 //复制暂存器
#define RECALL_E2 0xB8 //把EEPROM中的数据写到暂存器中
#define READ_POWER_SUPPLY 0xB4 //读取设备的供电模式
/*
@函数作用:温度变换(温度更新)
@参数:无
@返回值:无
*/
void DS18B20_update_temperture()
{
// 初始化
one_wire_init();
// 跳过ROM
one_wire_write_byte(SKIP_ROM);
// 变换温度
one_wire_write_byte(CONVERT_T);
}
/*
@函数作用:读取暂存器中的温度
@参数:无
@返回值:温度数值,float类型
*/
float DS18B20_read_temperture()
{
// 温度的低8位和高8位
unsigned char LSB, MSB;
// 温度的补码
unsigned int value;
// 小数表示温度
float temperture;
// 初始化
one_wire_init();
// 跳过ROM
one_wire_write_byte(SKIP_ROM);
// 读暂存器,从低字节开始读
one_wire_write_byte(READ_SCRATCHPAD);
LSB=one_wire_read_byte(); // 温度的低8位
MSB=one_wire_read_byte(); // 温度的高8位
value=(MSB<<8)+LSB;
// 高5位中有一个为1,说明温度是负数
if(value&0x8000)
{
// 对补码取反再加一可得到原码
value=(~value)+1;
// 再乘以精度
temperture=value*(-0.0625);
}
else
{
// 温度为正数,直接乘以精度
temperture=value*0.0625;
}
return temperture;
}
DS18B20.h
#ifndef __DS18B20_h__
#define __DS18B20_h__
// 温度变换
void DS18B20_update_temperture();
// 读取暂存器中的温度
float DS18B20_read_temperture();
#endif
示例代码
DS18B20实时检测温度
#include <REGX52.H>
#include "DS18B20.h"
#include "LCD1602.h"
#include "delay.h"
void main()
{
// 从暂存器中获取的有符号温度
float temperture;
// 获取温度的整数部分和小数部分
int int_data, float_data;
LCD_Init();
LCD_ShowString(1,1,"TEMPERTURE:");
while(1)
{
// 更新温度
DS18B20_update_temperture();
// 每1秒更新一下温度
delay(1000);
// 读取温度数据
temperture=DS18B20_read_temperture();
// 把浮点型强转成整型,获取浮点数的整数部分
int_data=temperture;
// 浮点数减去整数部分再乘以一千,取出小数的前四位作为整数
float_data=(temperture-int_data)*10000;
// 因为温度有正负数,所以要用有符号十进制数显示
LCD_ShowSignedNum(2,1,int_data,3);
LCD_ShowSignedNum(2,5,float_data,4);
// 用小数点把小数部分的符号覆盖掉
LCD_ShowString(2,5,".");
}
}
DS18B20温度报警
#include <REGX52.H>
#include "DS18B20.h"
#include "LCD1602.h"
#include "delay.h"
#include "AT24C02.h"
#include "timer_one_key.h"
#include "timer0_mode1.h"
// 蜂鸣器IO口
sbit buzzer = P2^5;
// 从暂存器中获取的有符号温度
static float temperture;
// 获取温度的整数部分和小数部分
int int_data, float_data;
// 温度的上限阈值和下限阈值
char T_upper_limit, T_lower_limit;
// 独立按键
unsigned char key_num;
// 蜂鸣器报警标志位
unsigned char buzzer_ring;
// 独立按键事件
void key_event()
{
/*
DS18B20的温度检测范围在+125~-55°C之间
T_upper_limit和T_lower_limit不能超过这个范围
且T_upper_limit要大于T_lower_limit
*/
switch(key_num)
{
// 按键为1和2时阈值上限进行自增和自减,并把数据存放到AT24C02中
case 1:
if(T_upper_limit<125)
{
T_upper_limit++;
AT24C02_write_byte(0,T_upper_limit);
delay(5);
}
break;
case 2:
if(T_upper_limit>T_lower_limit)
{
T_upper_limit--;
AT24C02_write_byte(0,T_upper_limit);
delay(5);
}
break;
// 按键为3和4时阈值下限进行自增和自减,并把数据存放到AT24C02中
case 3:
if(T_lower_limit<T_upper_limit)
{
T_lower_limit++;
AT24C02_write_byte(1,T_lower_limit);
delay(5);
}
break;
case 4:
if(T_lower_limit>-55)
{
T_lower_limit--;
AT24C02_write_byte(1,T_lower_limit);
delay(5);
}
break;
}
// 最后更新温度阈值的显示数据
LCD_ShowSignedNum(2,4,T_upper_limit,3);
LCD_ShowSignedNum(2,13,T_lower_limit,3);
}
// 温度超过阈值时显示警告信息
void show_waraing()
{
// 当温度超过设置的阈值时显示警告信息
// 并且蜂鸣器报警标志位置1,蜂鸣器鸣响
if(temperture>T_upper_limit)
{
LCD_ShowString(1,13,"High");
buzzer_ring=1;
}
else if(temperture<T_lower_limit)
{
LCD_ShowString(1,13,"Low ");
buzzer_ring=1;
}
else
{
LCD_ShowString(1,13," ");
buzzer_ring=0;
}
}
// 显示温度
void show_T()
{
// 更新温度
DS18B20_update_temperture();
// 每1秒更新一下温度
delay(1000);
// 读取温度数据
temperture=DS18B20_read_temperture();
// 把浮点型强转成整型,获取浮点数的整数部分
int_data=temperture;
// 浮点数减去整数部分再乘以一千,取出小数的前四位作为整数
float_data=(temperture-int_data)*10000;
// 因为温度有正负数,所以要用有符号十进制数显示
LCD_ShowSignedNum(1,3,int_data,3);
LCD_ShowSignedNum(1,7,float_data,4);
// 用小数点把小数部分的符号覆盖掉
LCD_ShowString(1,7,".");
}
void main()
{
// 初始化LCD和定时器
LCD_Init();
timer0_mode1_init();
// 显示提示信息
LCD_ShowString(1,1,"T:");
LCD_ShowString(2,1,"TH:");
LCD_ShowString(2,10,"TL:");
// 从AT24C02中读取我们设置的温度阈值
T_upper_limit=At24C02_read_byte(0);
T_lower_limit=At24C02_read_byte(1);
// 如果是第一次读取,数据可能不合法,我们设置初始值
if(T_upper_limit>125 || T_lower_limit<-55 || T_lower_limit>T_upper_limit)
{
T_upper_limit=20;
T_lower_limit=10;
}
// 显示阈值数据
LCD_ShowSignedNum(2,4,T_upper_limit,3);
LCD_ShowSignedNum(2,13,T_lower_limit,3);
while(1)
{
// 显示温度
show_T();
// 获取按键键码
key_num=key();
if(key_num)
{
key_event();
}
// 温度超过阈值进行警告
show_waraing();
}
}
void Timer0() interrupt 1
{
// 记录中断次数
static unsigned int T0_count1, T0_count2;
// 每触发一次中断都要重新配置计数器的初始值
TH0 = 0xfc;
TL0 = 0x66;
// 中断次数自增
T0_count1++;
T0_count2++;
// 每20ms扫描一次独立按键
if(T0_count1 == 20)
{
T0_count1 =0;
key_loop();
}
// 蜂鸣器每3ms翻转一次,如果蜂鸣器警告标志位置1的话
if(T0_count2 == 3)
{
T0_count2=0;
if(buzzer_ring)
{
buzzer=!buzzer;
}
}
}
LCD1602液晶显示屏
LCD1602介绍
LCD1602(Liquid Crystal Display)液晶显示屏是一种字符型液晶显示模块,可以显示ASCII码的标准字符和其他的一些内置特殊字符,还可以有8个自定义字符
显示容量:16*2个字符,每个字符为5*8的点阵
LCD1602原理图及引脚定义
- GND:接地
- VCC:电源正极
- VO:对比度调节电压。调整屏幕字符显示的深浅
- RS:数据/指令选择位。1为数据,0为指令
- RW:读/写选择位。1为读,0为写
- E:使能。1为传输的数据显示在屏幕上的,下降沿执行命令
- DB0~DB7:数据并行输入/输出
- BG VCC:背光灯正极
- BG GND:背光灯负极
LCD1602工作原理和内部结构
-
CGRAM+CGROM(字模库)
输入对应的字符地址就可以显示对应的字符
-
CGRAM:可以自定义的字符,一共8个字符
-
CGROM:不可自定义,厂商生产出来自带的字符
-
-
DDRAM:
共80个字节,用来寄存显示字符的。其地址和屏幕的对应关系如下
从上图可知,不是所有的地址都可以直接用来显示字符数据,只有第一行中的0x000x0f、第二行中的0x400x4f才能显示在屏幕上,其他地址只能用于存储。
在0x00的地址中写入数据0x41,就会先去CGROM中寻找0x41对应的字符:a,然后把 “ a ”显示在屏幕的第一行第一列
-
AC(光标位置):在DDRAM中写数据时确定位置
工作流程:
-
先根据要显示的字符,对应CGROM字模库中的字符找到对应的字节,例如显示 ‘a’,对应的字节就是0110 0001,也就是0x61(CGROM中的表示字符的字节与ASCII码中表示字符的字节基本是一致的。因为c语言使用的ASCII码,所以想显示字符‘a’,可以直接传入字符‘a’,c语言编译器会把字符‘a’转换为ASCLL码中对应的字节:0x61,刚好与LCD1602的CGROM中字符‘a’对应的字节是一致的)
-
再设置光标的位置,例如我想在LCD的第一行第二列显示字符,就要在DDRAM中找到对应的地址(DDRAM与LCD的屏幕是一一对应的,在DDRAM的第一行第二列中写入字节,LCD的第一行第二列就会显示该字节对应的字符),也就是0x01,再把该地址的最高位置1,表示这是一个设置光标位置的指令(指令在下面有说明),再把该指令通过写指令的时序传给LCD1602,就成功设置了光标的位置
-
最后再通过写数据的时序把0x61传给LCD1602,就成功在屏幕的第一行第二列显示了一个字符‘a’
-
之前我们利用LCD显示数字、二进制数字、字符串等等,其本质都是显示字符。例如显示十进制数,就是把数字中的每一位都都转换成字符,再通过显示字符的方式一个个显示,其他都是同理
标准术语解释
LCD1602是一种常见的字符型液晶显示屏,它由16列、2行的字符组成,每个字符由5x8个像素点组成。它的工作原理涉及到DDRAM寄存器和CGROM字模库。
- DDRAM寄存器(Display Data RAM):DDRAM寄存器是LCD1602中用来存储要显示的字符数据的地方。LCD1602的控制器可以通过设置DDRAM寄存器中的值来决定显示的字符内容。每个字符在DDRAM中都有一个对应的地址,通过设置不同的地址,可以在显示屏上显示不同的字符。
- CGROM字模库(Character Generator ROM):CGROM是一个只读存储器,用来存储LCD1602中的字符字模。每个字符在CGROM中都有一个对应的字模,包括了该字符的像素点阵列。当控制器需要显示某个特定的字符时,它会从CGROM中读取该字符的字模数据,并将其加载到DDRAM中,然后LCD1602根据DDRAM中的数据来显示相应的字符。
具体的工作流程如下:
- 控制器接收到要显示的字符数据,并确定该字符在CGROM中的地址。
- 控制器根据字符的地址从CGROM中读取对应的字模数据。
- 控制器将字模数据加载到DDRAM中的相应位置,以便LCD1602显示该字符。
总之,LCD1602的工作原理涉及到DDRAM寄存器和CGROM字模库之间的联系。控制器通过设置DDRAM寄存器中的值来决定显示的字符内容,并通过读取CGROM中的字模数据来显示相应的字符。
LCD1602时序结构
写数据/指令
- 如果是写数据,就把RS置1,如果是写指令就把RS置0
- 将RW置0,表示写数据
- 把一个字节全部放到DB0~DB7上,例如我想显示 “ a ” ,就把0x41放上去
- 再把E置1,这个数据是有效数据,等数据接收完毕再把E置0
读数据就是把RW置1,其他步骤是一样的
LCD1602指令集
*:表示这个位不管是1还是0都无所谓,一律看做0就行
常用的指令:
-
0x38:八位数据接口,两行显示,5*7的点阵
-
0x0c:显示开,光标关,闪烁关
-
0x06:数据读写操作后,光标自动加一,画面不动
-
0x01:清屏
-
0x80 | AC:设置光标位置
我们可以观察到,DDRAM的80个地址中,没有一个最高位为1的地址
而指令集中也可以看到,当我们把指令的最高位置1时,就是在设定下一个要存入数据的DDRAM地址
所以要在DDRAM中存入数据不是直接传入DDRAM中的一个地址,而是要把该地址的最高位置1,并当作指令传给LCD1602,再把RS置1开始写数据才能把数据存到DDRAM对应的地址中
因此0x80|AC就是把AC的最高位置1(AC表示DDRAM中的一个地址)
LCD1602代码封装
源文件
LCD1602.c
#include <REGX52.H>
// IO口命名
#define LCD1602_data P0
sbit LCD1602_RS=P2^6;
sbit LCD1602_RW=P2^5;
sbit LCD1602_E=P2^7;
/*
@函数作用:延时1ms
@参数:无
@返回值:无
*/
void Delay1ms() //@11.0592MHz
{
unsigned char data i, j;
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
/*
@函数作用:写指令
@参数:order,一个字节,要执行的指令
@返回值:无
*/
void LCD1602_write_order(unsigned char order)
{
/*
- 如果是写数据,就把RS置1,如果是写指令就把RS置0
- 将RW置0,表示写数据
*/
LCD1602_RS=0;
LCD1602_RW=0;
// 把指令放到IO口上
LCD1602_data=order;
// 再把E置1,这个数据是有效数据,等数据接收完毕再把E置0
LCD1602_E=1;
Delay1ms(); // 这里延时是因为写入数据时需要执行时间
LCD1602_E=0;
Delay1ms();
}
/*
@函数作用:写入一个字节的数据
@参数:Data 要写入的数据
@返回值:无
*/
void LCD1602_write_data(unsigned char Data)
{
// 与写入指令步骤一致,将RS=0改为RS=1即可
LCD1602_RS=1;
LCD1602_RW=0;
LCD1602_data=Data;
LCD1602_E=1;
Delay1ms();
LCD1602_E=0;
Delay1ms();
}
/*
@函数作用:设置光标位置
@参数:line 光标要设置在的行数
@参数:column 光标要设置在的列数
@返回值:无
*/
void set_AC_position(char line, column)
{
// 当设置光标位置在第一行时
if(line==1)
{
// 把列减一,再把高位置1(看DDRAM的具体地址就知道了),就是在该位置设置光标的指令
LCD1602_write_order(0x80|(column-1));
}
// 当设置光标位置在第二行时
else
{
// DDRAM中第二行的地址就比第一行多0x40,加上0x40就是第一行相同列的地址了
LCD1602_write_order(0x80|(column-1+0x40));
}
}
/*
@函数作用:初始化LCD1602
@参数:无
@返回值:无
*/
void LCD1602_init()
{
LCD1602_write_order(0x38); // 八位数据接口,两行显示,5*7的点阵
LCD1602_write_order(0x0c); // 显示开,光标关,闪烁关
LCD1602_write_order(0x06); // 数据读写操作后,光标自动加一,画面不动
LCD1602_write_order(0x01); // 清屏
}
/*
@函数作用:在LCD1602的显示屏上显示一个字符
@参数:line 行
@参数:column 列
@参数:Data 要显示的字符,也可以写CGROM中的地址
@返回值:无
*/
void LCD1602_show_char(char line, column, Data)
{
// 设置光标位置
set_AC_position(line, column);
LCD1602_write_data(Data);
}
/*
@函数作用:在屏幕上显示字符串
@参数:line 行
@参数:column 列
@参数:*string 要显示的字符串首地址(字符串:”123“的本质是一个以'\0'结尾的数组:{'1','2','3','\0'})
@返回值:无
*/
void LCD1602_show_string(char line, column, char*string)
{
char i;
set_AC_position(line, column);
for(i=0;string[i]!='\0';i++)
{
LCD1602_write_data(string[i]);
}
}
/*
@函数作用:返回x的y次方
@参数: x 底数
@参数: y 指数/幂
@返回值:x^y
*/
int LCD_pow(int x,int y)
{
unsigned char i;
int temp=1;
for(i=0;i<y;i++)
{
temp*=x;
}
return temp;
}
/*
@函数作用:在屏幕上显示无符号十进制数
@参数:line 行
@参数:column 列
@参数:Data 要显示的无符号整数
@参数:len 整数显示的位宽,不足的优先显示低位,多余的用0补充在高位
@返回值:无
*/
void LCD1602_show_un_num(char line, column, unsigned int Data, char len)
{
unsigned char i;
set_AC_position(line, column);
for(i=len;i>0;--i)
{
// 取出789的百位数:789 / 100 % 10 = 7 % 10 = 7
// 取出789的十位数:789 / 10 % 10 = 78 % 10 = 8
// 取出789的个位数:789 / 1 % 10 = 789 % 10 = 9
// LCD_pow的返回值是10的i-1次方
// 通过上面的方法算出来的是一个整数,而不是字符,想要显示ASCII对应的字节,需要加上偏移量,也就是字符0,或者加上字符0在ASCII中对应的字节:0x30
LCD1602_write_data(Data/LCD_pow(10,i-1)%10+'0');
}
}
/*
@函数作用:在屏幕上显示有符号十进制数
@参数:line 行
@参数:column 列
@参数:Data 要显示的有符号整数
@参数:len 整数显示的位宽,不足的优先显示低位,多余的用0补充在高位
@返回值:无
*/
void LCD1602_show_sig_num(char line, column, int Data, char len)
{
unsigned char i;
set_AC_position(line, column);
if(Data<0)
{
// 当Data为负数时把Data取反,并在前面加上一个负号
LCD1602_write_data('-');
Data=-Data;
}
else
{
// Data为正数时,只需要在前面加上正号
LCD1602_write_data('+');
}
for(i=len;i>0;--i)
{
LCD1602_write_data(Data/LCD_pow(10,i-1)%10+'0');
}
}
/*
@函数作用:显示无符号十进制小数
@参数:line 行
@参数:column 列
@参数:Data 要显示的小数
@参数:int_len 数据整数部分的位宽
@参数:float_len 数据小数部分的位宽,类似精度,显示几位小数
@返回值:无
*/
void LCD1602_show_un_float(char line, column, float Data, char int_len, float_len)
{
unsigned char i;
unsigned int int_Data; // 数据的整数部分
unsigned int float_Data; // 数据的小数部分
set_AC_position(line, column); // 设置光标位置
int_Data=(unsigned int)Data; // 取出数据的整数部分,直接强转成int型就可以了
// 取出数据的小数部分
// 原数据减去整数部分,就是小数部分了
// 再乘以10的位宽次方,最后强转成int型,最后把这个int型数据按照显示整数的方法显示在小数点后面即可
// 假设传过来的位宽为:2,那么就是小数部分乘以100,再强转成int型
float_Data=(Data-int_Data)*LCD_pow(10, float_len);
// 根据整数部分的位宽显示数据的整数部分
for(i=int_len;i>0;--i)
{
LCD1602_write_data(int_Data/LCD_pow(10,i-1)%10+'0');
}
LCD1602_write_data('.'); // 显示完整数部分了,后面跟一个小数点再显示小数部分
// 根据小数部分的位宽显示数据的小数部分
for(i=float_len;i>0;--i)
{
LCD1602_write_data(float_Data/LCD_pow(10,i-1)%10+'0');
}
}
/*
@函数作用:显示有符号十进制小数
@参数:line 行
@参数:column 列
@参数:Data 要显示的小数
@参数:int_len 数据整数部分的位宽
@参数:float_len 数据小数部分的位宽,类似精度,显示几位小数
@返回值:无
*/
void LCD1602_show_sig_float(char line, column, float Data, char int_len, float_len)
{
set_AC_position(line, column);
if(Data<0)
{
// Data小于0时把Data取反,并在前面显示负号
LCD1602_write_data('-');
Data=-Data;
}
else
{
// Data大于0,在前面显示正号
LCD1602_write_data('+');
}
// 列数column已经用来显示一个符号位了
// 调用显示无符号小数的函数时会在设置一次光标,所以要把列数加一,防止符号位被数字位覆盖
column++;
LCD1602_show_un_float(line, column, Data, int_len, float_len);
}
/*
@函数作用:显示二进制数
@参数:line 行
@参数:column 列
@参数:Data 要显示的数,0~255
@参数:len 整数显示的位宽,不足的优先显示低位,多余的用0补充在高位
*/
void LCD1602_show_bin_num(char line, column, unsigned char Data, char len)
{
unsigned char i;
set_AC_position(line, column);
for(i=len;i>0;--i)
{
LCD1602_write_data(Data/LCD_pow(2,i-1)%2+'0');
}
}
/*
@函数作用:显示十六进制数
@参数:line 行
@参数:column 列
@参数:Data 要显示的数,0~255
@参数:len 整数显示的位宽,不足的优先显示低位,多余的用0补充在高位
*/
void LCD1602_show_hex_num(char line, column, unsigned char Data, char len)
{
unsigned char i, temp;
set_AC_position(line, column);
for(i=len;i>0;--i)
{
temp=Data/LCD_pow(16,i-1)%16;
if(temp>=10)
{
LCD1602_write_data('A'+temp-10);
}
else
{
LCD1602_write_data(temp+'0');
}
}
}
头文件
LCD1602.h
#ifndef __LCD1602_h__
#define __LCD1602_h__
// 初始化LCD1602
void LCD1602_init();
// 写入指令,例如左移指令:0x18
void LCD1602_write_order(unsigned char order);
// 显示一个字符
void LCD1602_show_char(char line, column, Data);
// 显示字符串
void LCD1602_show_string(char line, column, char*string);
// 显示无符号十进制整数
void LCD1602_show_un_num(char line, column, unsigned int Data, char len);
// 显示有符号十进制整数
void LCD1602_show_sig_num(char line, column, int Data, char len);
// 显示无符号十进制小数
void LCD1602_show_un_float(char line, column, float Data, char int_len, float_len);
// 显示有符号十进制小数
void LCD1602_show_sig_float(char line, column, float Data, char int_len, float_len);
// 显示二进制数
void LCD1602_show_bin_num(char line, column, unsigned char Data, char len);
// 显示十六进制数
void LCD1602_show_hex_num(char line, column, unsigned char Data, char len);
#endif
LCD1602简单使用
这里就不写很多代码了,之前也用过很多,除了调用的函数名有变化外,就只 多加了两个函数:浮点数的显示
#include <REGX52.H>
#include "LCD1602.h"
#include "delay.h"
void main()
{
LCD1602_init();
LCD1602_show_string(1,1,"live in the moment, love me chine");
// 显示无符号小数
LCD1602_show_un_float(2,1,11.22,2,2);
// 显示有符号小数
LCD1602_show_sig_float(2,8,-11.22,2,2);
while(1)
{
// 每两秒调用一次屏幕左移的指令,实现流水字幕的效果
delay(2000);
LCD1602_write_order(0x18);
}
}
直流电机驱动
直流电机介绍
- 直流电机是一种将电能转换为机械能的装置。一般的直流电机有两个电极,当电极正接时,电机正转,当电极反接时,电机反转
- 直流电机主要由永磁体(定子)、线圈(转子)和换向器组成
- 除直流电机外,常见的电机还有步进电机、舵机、无刷电机、空心杯电机等
电机的驱动电路
大功率直接驱动:不能改变旋转方向
H桥驱动:可以改变旋转方向
原理图
引脚介绍:
P25:不属于步进电机的,是连接蜂鸣器的
ULN2003D
概述
-
ULN2003D是一种集成式继电器驱动器芯片,常用于控制大功率负载,如步进电机、直流电机或继电器。它包含七个独立的开关驱动器,每个驱动器能够提供高达500mA的电流。这些驱动器可以直接连接到微控制器的数字输出引脚,从而实现对外部负载的控制。
-
ULN2003D的工作原理是通过控制输入端的电平来控制输出端的开关状态。当输入端施加高电平时,相应的输出端将导通,允许电流通过,从而驱动外部负载。相反,当输入端施加低电平时,输出端将断开,阻止电流通过。
-
该芯片还包含了内部保护电路,能够有效地抑制负载感应电压的反向传播,从而保护驱动器和微控制器不受损坏。
总之,ULN2003D通过控制输入信号来驱动负载,同时具备内部保护功能,从而适用于各种电路控制应用。
特点
ULN2003 是高耐压、大电流达林顿陈列,由七个硅 NPN 达林顿管组成。
该电路的特点如下:
- ULN2003 的每一对达林顿都串联一个 2.7K 的基极电阻,在 5V 的工作电压下它能与 TTL 和 CMOS 电路 直接相连,可以直接处理原先需要标准逻辑缓冲器来处理的数据。
- ULN2003 工作电压高,工作电流大,灌电流可达 500mA,并且能够在关态时承受 50V 的电压,输出还 可以在高负载电流并行运行。
- ULN2003 采用 DIP—16 或 SOP—16 塑料封装
极限值与电特性
应用电路
PWM
-
PWM代表脉宽调制(Pulse Width Modulation),是一种用于控制模拟电子设备的数字技术。通过调整信号的脉冲宽度,PWM可以模拟出模拟信号的效果,常见的应用包括电机控制、LED亮度调节、音频处理等。
-
PWM的基本原理是通过改变脉冲信号的占空比来控制输出信号的平均功率。脉冲信号由一个周期性的方波组成,每个周期包括一个高电平(表示“开”状态)和一个低电平(表示“关”状态)。通过调整高电平和低电平的持续时间比例,可以改变脉冲信号的占空比,从而控制输出信号的平均功率。
-
在电机控制中,PWM可以用来调节电机的转速和转矩,通过改变电机驱动器的PWM信号的占空比,可以实现对电机的精确控制。在LED亮度调节中,PWM可以控制LED灯的亮度,通过改变PWM信号的占空比,可以实现LED的调光效果。在音频处理中,PWM可以用来模拟出不同频率的声音信号,通过调整PWM信号的频率和占空比,可以实现音频信号的数字合成和处理。
总的来说,PWM是一种非常灵活和高效的控制技术,广泛应用于各种电子设备和系统中。
PWM重要参数
周期 = TS 频率 = 1 / TS 占空比 = TON / TS 精度 = 占空比变化步距
- 周期:即高电平和低电平的持续时间之和。频率越高,脉冲信号的周期越短,控制精度和响应速度越高,但同时也会增加系统的噪声和功耗。
- 频率: 是指1秒钟内信号从高电平到低电平再回到高电平的次数(一个周期);也就是一秒钟PWM有多少个周期
- 占空比:PWM信号的占空比指的是高电平的持续时间占整个周期的比例。占空比越高,输出信号的平均功率越大,控制效果越强,但同时也会增加系统的热损耗和电磁干扰。
- 精度:PWM信号的精度指的是控制系统对PWM信号频率和占空比的控制精度。精度越高,系统对PWM信号的控制越精确,输出信号的稳定性和可靠性也越高。
呼吸灯案例:利用循环实现PWM
#include <REGX52.H>
sbit LED = P2^0;
void delay(unsigned int i)
{
while(i--);
}
void main()
{
while(1)
{
unsigned char time, i;
// 每一次循环减少等量的占空比,直至为0%。LED慢慢变亮
for(time=0;time<100;time++)
{
// 相同的一个周期持续20次
for(i=0;i<20;i++)
{
// delay(100)为一个周期,time为低电平占这个周期的时间
LED=0;
delay(time);
LED=1;
delay(100-time);
}
}
// LED慢慢熄灭
for(time=100;time>0;time--)
{
for(i=0;i<20;i++)
{
LED=0;
delay(time);
LED=1;
delay(100-time);
}
}
}
}
电机调速案例:利用定时器实现PWM
#include <REGX52.H>
#include "timer0_mode1.h"
#include "timer_one_key.h"
sbit motor=P1^0;
unsigned char key_num, compare_value, counter, t0_counter;
void main()
{
timer0_mode1_init();
while(1)
{
// 获取独立按键的键码
key_num=key();
switch(key_num)
{
case 1:
compare_value=0; // 按键按下1电机停止旋转
break;
case 2:
compare_value=50; // 按键按下2占空比为50%
break;
case 3:
compare_value=70; // 按键按下3占空比为70%
break;
case 4:
compare_value=100; // 按键按下4占空比为100%
break;
}
}
}
void timer0() interrupt 1
{
// 设置初值,100us记一次数,中断100次为一个周期
// 也就是每10ms一个周期,也就是100MHz的频率
TL0 = 0xA4; //设置定时初始值
TH0 = 0xFF; //设置定时初始值
counter++;
counter%=100; // 当counter加到100时,再%100就会等于0,这样就可以不用写if判断
// compare_value的范围是0~100,与counter的范围一直
// 当compare_value小于counter时输出低电平,电机停止旋转
// 当compare_value大于counter时输出高电平,电机开始旋转
// 所以设置compare_value相当于在设置占空比,compare_value越大占空比越大
// 再根据需求设置compare_value的值就可以控制电机的旋转速度
if(counter>compare_value)
{
motor=0;
}
else
{
motor=1;
}
// 每20ms扫描一次独立按键
t0_counter++;
if(t0_counter==200)
{
t0_counter=0;
key_loop();
}
}
AD和DA
AD/DA介绍
AD:Analog to Digital ,模拟-数字转换,将模拟信号转化为计算机可操作的数字信号
DA:Digital to Analog ,数字-模拟转换,将计算机输出的数字信号转换为模拟信号
硬件电路模型
工作原理简介
AD:模拟量(例如温度、光照等)的不同会使电路的电压也不同,再经过封装好的芯片操作过后会把电压转变为8位、12位、16位二进制数,位数越高数字量的精细程度也就越高
例如单片机中的电压范围在0~5v,那么用8位二进制表示电压时,0x00表示0v,0x80表示2.5v,0xff表示5v。
DA:与AD相反,单片机输出二进制数,再经过DAC转换后就会输出相应的电压
硬件电路
AD
本开发板使用的芯片是:XPT2046,触摸屏的芯片
我们只使用其中的ADC模块来测开发板上的光敏电阻、热敏电阻、滑动变阻产生的模拟量
光敏电阻:GR1;热敏电阻:NTC1;滑动变阻:AD1
对应原理图中分别对应XPT2046中的AIN2、AIN1、AIN0
也就是只需要测量VBAT、Y+、X+三个引脚
DA
AD/DA原理
XPT2046(SPI通信)
介绍
-
XPT2046是一种触摸屏控制器芯片,通常用于智能手机、平板电脑和其他触摸设备中。它的主要功能是将触摸屏的模拟触摸位置信息转换为数字信号,以便设备的微控制器或处理器进行处理。
-
XPT2046内部集成了一个12位模数转换器(ADC),用于对触摸屏的模拟信号进行采样和转换为数字数据。它还提供了压力测量、省电模式和对不同触摸屏技术的支持等功能。
-
该芯片还具有SPI接口,这使得它可以方便地与各种微控制器和处理器进行通信。XPT2046还支持多种不同的触摸屏技术,包括电阻式和电容式触摸屏。
总的来说,XPT2046是一款功能强大的触摸屏控制器芯片,能够高效地将触摸屏的模拟信号转换为数字信号,并提供了多种功能和灵活性,适用于各种触摸设备的设计和应用。
这里只使用他的ADC功能
时序图
只使用ADC功能时只需要操作下面四个引脚即可
-
CS:拉低表示开始传输或接收数据
-
DCLK:上升沿接收数据,下降沿发送数据
-
DIN:接收数据(高位在前)
控制字节由DIN输入,他用来启动转换、寻址、设置ADC分辨率、配置和对XPT2046进行掉电设置
-
起始位:第一位,即S位。控制字的首位必须是1,即S=1。在XPT2046的DIN引脚检测到起始位前,所有的输入都被忽略
-
地址(A2~A0):选择多路选择器的现行通道(只需要看前面6个即可)
在AD的原理图中有提到过,这次的测量目标是X+、Y+和VBAT(N表示负号,P表示正号)
也就是A2~A0的值分别为:010、001、101
其中X+(XP)的值两个地址:001和011都可以测,随便选一个即可
-
MODE:模式选择位,用于设置ADC的分辨率。MODE=0使用12位模式;MODE=1使用8位模式
-
SER/DFR:控制参考源模式。SER/DFR=1,选择单端模式;SER/DFR=0,选择差分模式。这里使用单端模式,该模式转换器的参考电压固定为GND引脚的电压,也就是5v
-
PD1:内部参考电压开关。PD1=0关闭内部参考电压。XPT2046的内部参考电压为2.5v。
-
PD0:低功率模式选择位,置1或置0都可
-
-
DOUT:发送数据(高位在前)
发送数据,一次性发送16位数据,当MODE=1时,是8位模式,也就是16位数据中只有高位的前8位是有效位,后8位都为0,所以要把数据右移8位
当MODE=0,是12位模式,只有高12位是有效位,低4位都为0,所以要把数据右移4位
完成一个动作需要的时间
本开发板一个时钟周期大于这些时间,所以不需要在代码中延时,如果单片机的一个时钟周期很快,在操作时序时就需要进行延时,确保数据正确的传输
XPT2046代码/AD代码
XPT2046.c
#include <REGX52.H>
// 引脚定义
sbit XPT2046_DIN = P3^4;
sbit XPT2046_CS = P3^5;
sbit XPT2046_DCLK = P3^6;
sbit XPT2046_DOUT = P3^7;
/*
@函数作用:从输出模拟信号的硬件中,把输出的模拟量转换为数字量
@参数:cmd 控制字,用于寻址、设置分辨率等等
@返回值:数字信号
*/
unsigned int XPT2046_read_data(unsigned char cmd)
{
// 要返回的数字信号
unsigned int AD_value=0x0000;
unsigned char i;
// 初始化
XPT2046_CS=0;
XPT2046_DCLK=0;
for(i=0;i<8;i++)
{
// 将控制字节从高位到低位逐位写入DIN
XPT2046_DIN=cmd&(0x80>>i);
// DCLK上升沿期间写入数据
XPT2046_DCLK=1;
XPT2046_DCLK=0;
}
for(i=0;i<16;i++)
{
// DCLK下降沿期间读出数据
XPT2046_DCLK=1;
XPT2046_DCLK=0;
// 读取DOUT的电平,DOUT为高电平时将数字信号对应的地方置1
if(XPT2046_DOUT){AD_value|=(0x8000>>i);}
}
// CS复位
XPT2046_CS=1;
// 当控制字节中的第3位:MODE=1时是8位模式,只有高8位是有效字节
if(cmd&0x08)
{
return AD_value>>=8;
}
// 当控制字节中的第3位:MODE=0时是12位模式,只有高12位是有效字节
else
{
return AD_value>>4;
}
}
XPT2046.h
#ifndef __XPT2046_h__
#define __XPT2046_h__
// 控制字节:8位的分辨率,单端模式,参考外部电压
#define XPT2046_XP_8 0x9C
#define XPT2046_YP_8 0xDC
#define XPT2046_VBAT_8 0xAC
#define XPT2046_AUX_8 0xEC
// 控制字节:12位的分辨率,单端模式,参考外部电压
#define XPT2046_XP_12 0x94
#define XPT2046_YP_12 0xD4
#define XPT2046_VBAT_12 0xA4
#define XPT2046_AUX_12 0xE4
// cmd:控制字节。返回值:模拟信号转化为的数字信号,16位二进制
unsigned int XPT2046_read_data(unsigned char cmd);
#endif
PWM型DA代码
PWM型DA本身就是PWM,只不过在硬件上加了一个低通滤波,可以把电压稳定下来
通过改变占空比也能实现改变电压大小的现象,所以DA并没有AD的应用范围广,可以使用PWM代替DA
代码现象
通过AD转换器将模拟量转数字量
#include <REGX52.H>
#include "XPT2046.h"
#include "LCD1602.h"
void main()
{
unsigned int AD_value;
LCD1602_init();
LCD1602_show_string(1,1,"AD1 GR1 NTC1");
while(1)
{
// 滑动变阻的模拟量转数字量
AD_value=XPT2046_read_data(XPT2046_XP_8);
LCD1602_show_un_num(2,1,AD_value,3);
// 光敏电阻的模拟量转数字量
AD_value=XPT2046_read_data(XPT2046_VBAT_8);
LCD1602_show_un_num(2,5,AD_value,3);
// 热敏电阻的模拟量转数字量
AD_value=XPT2046_read_data(XPT2046_YP_8);
LCD1602_show_un_num(2,9,AD_value,3);
}
}
PWM型DA
#include <REGX52.H>
#include "timer0_mode1.h"
#include "delay.h"
sbit DA=P2^1;
unsigned char compare_value, counter, i;
void main()
{
timer0_mode1_init();
while(1)
{
for(i=0;i<100;i++)
{
compare_value=i;
delay(20);
}
for(i=100;i>0;i--)
{
compare_value=i;
delay(20);
}
}
}
void timer0() interrupt 1
{
// 设置初值,100us记一次数,中断100次为一个周期
// 也就是每10ms一个周期,也就是100MHz的频率
TL0 = 0xA4; //设置定时初始值
TH0 = 0xFF; //设置定时初始值
counter++;
counter%=100;
if(counter>compare_value)
{
DA=0;
}
else
{
DA=1;
}
}
红外遥控(外部中断)
红外遥控介绍
- 红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专用的红外接收头进行解调输出
- 通信方式:单工,异步
- 红外LED波长:940nm
- 通信协议标准:NEC标准
硬件电路
发送电路
第一种电路,两个IO口,一个IO口不停的发送38KHz的方波,另一个IO口传输数据
只需要一个IO口,但是需要发出如下的波形
接收电路
想要接收红外信号,要把自然光过滤、38KHz的频率过滤等等,最终要的信号就是发送电路中IN输出的数据
上面这个是一体化红外接收头,其中内部OUT这部分的电路已经处理好这些东西了,OUT发出的数据与起发送电路的 IN 输出的数据是一致的
红外信号会在很短的时间跑完,所以需要把OUT接在外部中断的引脚上,每来一个下降沿就中断一下,优先处理红外信号。如下图
P32为本开发板的外部中断,OUT也是接在了P32上
基本的发送和接收
红外NEC编码协议
- Start:起始
- DATA:数据
- Repeat:重复接收一次刚刚的数据
示波器获取的波形
解析
51单片机外部中断
- STC89C52有4个外部中断
- STC89C52的触发方式:下降沿触发和低电平触发
外部中断寄存器
单片机中的外部中断的引脚直接接在 INT0 和 INT1 上
也就是P32接INT0,P33接INT1
-
IT0/IT1: 等于1时,外部来下降沿时触发中断;等于0时,外部来低电平时触发中断(非门电路,对值取反)
-
IE0/IE1:中断标志位,等于1时说明来中断请求了
-
EX0/EX1:中断使能,置1才允许中断
-
EA:总中断开关,置0后关闭所有中断
-
PX0/PX1:选择中断优先级
遥控器对应的指令
外部中断代发封装
INT0.c
#include <REGX52.H>
/*
@函数作用:初始化外部中断0
@参数:无
@返回值:无
*/
void INT0_init()
{
IT0=1; // 下降沿触发
IE0=0; // 中断标志位置0
EX0=1; // 允许外部中断触发
EA=1; // 总中断开关打开
PX0=1; // 最高优先级
}
INT0.h
#ifndef __INT0_h__
#define __INT0_h__
// 初始化外部中断0
void INT0_init();
/*
// 中断号 0
void INT0_routine() interrupt 0
{
}
*/
#endif
定时器只用于记时代码封装
timer0_mode1_counter.c
#include <REGX52.H>
// 初始化定时器T0的模式1
void timer0_mode1_counter_init()
{
TMOD &= 0xf0; // 配置定时器
TMOD |= 0x01; // 配置定时器
TF0 = 0; // 将溢出标志清零
TR0 = 0; // 不允许进行中断
TH0 = 0; // 初始化计数器为0
TL0 = 0; // 初始化计数器为0
}
/*
@函数作用:配置计数器初值
@参数:count 范围0~65535
@返回值:无
*/
void timer0_set_count(unsigned int count)
{
TH0=count/256;
TL0=count%256;
}
/*
@函数作用:返回计数器TH和TL的值
@参数:无
@返回值:计数器的值 0~65535
*/
unsigned int timer0_get_count()
{
return (TH0<<8)|TL0;
}
/*
@函数作用:计数器开关
@参数:run_flag 是否开启计数器,范围1和0
@返回值:无
*/
void timer0_run_counter(unsigned char run_flag)
{
TR0=run_flag;
}
timer0_mode1_counter.h
// 防止头文件重复包含的
#ifndef __timer0_mode1_counter_h__
#define __timer0_mode1_counter_h__
// 初始化定时器T0,使用计数功能,不中断
void timer0_mode1_counter_init();
// 配置计数器初值
void timer0_set_count(unsigned int count);
//函数作用:返回计数器TH和TL的值
unsigned int timer0_get_count();
// 计数器开关
void timer0_run_counter(unsigned char run_flag);
#endif
红外接收代码封装
IR.c
#include <REGX52.H>
#include "timer0_mode1_counter.h"
#include "INT0.h"
// 0:空闲;1:起始信号;2:接收数据;
unsigned char IR_state; // 红外接收器的状态。
// 注意:晶振是11.0592,一个机器周期为11.0592/12=0.9216us
// 计数器的值要乘以机器周期才是对应的时间
unsigned int IR_time; // 两个下降沿之间的时间
unsigned char IR_repeat_flag; // 是否重发标志位
unsigned char IR_data_flag; // 数据是否接收到标志位
unsigned char IR_data[4]; // 32位数据分为4个字节接收
unsigned char IR_data_count; // 接收的到的第几位数据
unsigned char IR_addr; // 红外发送的地址
unsigned char IR_cmd; // 发送的指令
/*
@函数作用:初始化红外模块
@参数:无
@返回值:无
*/
void IR_init()
{
INT0_init(); // 初始化外部中断
timer0_mode1_counter_init(); // 初始化计数器
}
/*
@函数作用:是否成功接收到数据
@参数:无
@返回值:0:没有接收到数据。1:接收到了数据
*/
unsigned char IR_get_data_flag()
{
if(IR_data_flag)
{
IR_data_flag=0;
return 1;
}
return 0;
}
/*
@函数作用:是否接收到重发标志位
@参数:无
@返回值:0:没有接收到重发标志位。1:接收到重发标志位
*/
unsigned char IR_get_repeat_flag()
{
if(IR_repeat_flag)
{
IR_repeat_flag=0;
return 1;
}
return 0;
}
/*
@函数作用:返回红外发送端的地址
@参数:无
@返回值:地址
*/
unsigned char IR_get_addr()
{
return IR_addr;
}
/*
@函数作用:返回发送端发送的指令
@参数:无
@返回值:指令
*/
unsigned char IR_get_cmd()
{
return IR_cmd;
}
/*
@函数作用:外部中断函数
@参数:无
@返回值:无
*/
void INT0_routine() interrupt 0
{
if(IR_state==0)
{
timer0_set_count(0); // 计数器初始化0
timer0_run_counter(1); // 计数器开始计数
IR_state=1; // 转态置1,准备接收起始标志
}
else if(IR_state==1)
{
IR_time=timer0_get_count(); // 读取计数器的值
timer0_set_count(0); // 计数器清0
// 两个下降沿之间的时间在9+4.5ms左右,说明这是起始标志,准备接收数据
if(IR_time>12442-500 && IR_time<12442+500)
{
IR_state=2; // 接收器的状态置2,开始接收数据
}
// 两个下降沿之间的时间在9+2.25ms左右,这次的数据与上一次的数据一致
else if(IR_time>=10368-500 && IR_time<=10368+500)
{
IR_repeat_flag=1; // 重发标志位置1
IR_state=0; // 接收器的状态置0,进入空闲状态
timer0_run_counter(0); // 接收完一帧数据停止计数器记数
}
else // 数据解码出错
{
IR_state=1;
}
}
else if(IR_state==2)
{
IR_time=timer0_get_count(); // 读取计数器的值
timer0_set_count(0); // 计数器清0
// 两个下降沿之间的时间在560+560us左右,说明写入的数据位是0
if(IR_time>1032-500 && IR_time<1032+500)
{
IR_data[IR_data_count/8]&=~(0x01<<(IR_data_count%8));
IR_data_count++; // 接收的数据个数自增
}
// 两个下降沿之间的时间在560+1690us左右,说明写入的数据位是1
else if(IR_time>2074-500 && IR_time<2074+500)
{
IR_data[IR_data_count/8]|=0x01<<(IR_data_count%8);
IR_data_count++; // 接收的数据个数自增
}
// 数据解码出错
else
{
IR_data_count=0; // 接收的数据个数清零
IR_state=1; // 重新进入空闲状态
}
// 校验接收的数据是否有误
if(IR_data_count>=32)
{
IR_data_count=0; // 接收的数据个数清零
if((IR_data[0] == ~IR_data[1]) && (IR_data[2] == ~IR_data[3]))
{
IR_addr=IR_data[0]; // 把校验成功的地址数据赋值
IR_cmd=IR_data[2]; // 把校验成功的命令数据赋值
IR_data_flag=1; // 数据校验成功,标志位置1
}
timer0_run_counter(0); // 计数器停止
IR_state=0; // 数据接收完毕,进入空闲状态
}
}
}
IR.h
// 防止头文件重复包含的
#ifndef __timer0_mode1_counter_h__
#define __timer0_mode1_counter_h__
// 遥控器上的按键对应的指令
#define IR_POWER 0x45
#define IR_MODE 0x46
#define IR_MUTE 0x47
#define IR_START_STOP 0x44
#define IR_PREVIOUS 0x40
#define IR_NEXT 0x43
#define IR_EQ 0x07
#define IR_VOL_MINUS 0x15
#define IR_VOL_ADD 0x09
#define IR_0 0x16
#define IR_RPT 0x19
#define IR_USD 0x0D
#define IR_1 0x0C
#define IR_2 0x18
#define IR_3 0x5E
#define IR_4 0x08
#define IR_5 0x1C
#define IR_6 0x5A
#define IR_7 0x42
#define IR_8 0x52
#define IR_9 0x4A
// 是否接收到数据
unsigned char IR_get_data_flag();
// 是否接收到重发标志位
unsigned char IR_get_repeat_flag();
// 获取地址
unsigned char IR_get_addr();
// 获取指令
unsigned char IR_get_cmd();
// 初始化
void IR_init();
#endif
定时器1代码封装
timer1_mode1.c
#include <REGX52.H>
// 初始化定时器T0的模式1
void timer1_mode1_init()
{
// 配置定时器
TMOD &= 0x0f; // 把TMOD的高四位清零,低四位保持不变
TMOD |= 0x10; // 把TMOD的第4位置1,低四位保持不变
// 将溢出标志清零
TF1 = 0;
// 允许中断控制
TR1 = 1;
// 初始化计数器:0
TH1 = 0;
TL1 = 0;
// 允许定时器T0中断
ET1 = 1;
// 打开总中断
EA = 1;
}
timer1_mode1.h
// 防止头文件重复包含的
#ifndef __timer1_mode1_h__
#define __timer1_mode1_h__
/*
@参数:无
@返回值:无
@作用:初始化定时器T1的模式1(16位计数器)
*/
void timer1_mode1_init();
// 中断号
// void Timer1() interrupt 3
// {
//
// }
// 与ifndef配套的
#endif
红外遥控应用
在LCD上显示遥控器的地址、指令等
#include <REGX52.H>
#include "LCD1602.h"
#include "IR.h"
unsigned char num;
unsigned char addr;
unsigned char cmd;
void main()
{
LCD_Init();
IR_init();
LCD_ShowString(1,1,"ADDR CMD NUM");
while(1)
{
// 当接收到数据的标志位或重发标志位为1时读取地址和指令
// 这样可以实现连发的功能。长按一个按键可以不停的响应该指令
if(IR_get_data_flag() || IR_get_repeat_flag())
{
addr=IR_get_addr();
cmd=IR_get_cmd();
LCD_ShowHexNum(2,1,addr,2);
LCD_ShowHexNum(2,7,cmd,2);
if(cmd==IR_VOL_MINUS)
{
num--;
}
if(cmd==IR_VOL_ADD)
{
num++;
}
LCD_ShowNum(2,12,num,3);
}
}
}
红外遥控电机调速
#include <REGX52.H>
#include "timer1_mode1.h"
#include "IR.h"
sbit motor=P1^0;
unsigned char compare_value;
unsigned char counter;
unsigned char cmd;
void main()
{
timer1_mode1_init();
IR_init();
while(1)
{
if(IR_get_data_flag())
{
cmd=IR_get_cmd();
if(cmd==IR_0)
{
compare_value=0;
}
if(cmd==IR_1)
{
compare_value=50;
}
if(cmd==IR_2)
{
compare_value=75;
}
if(cmd==IR_3)
{
compare_value=100;
}
}
}
}
// 定时器1中断
void timer1() interrupt 3
{
// 设置初值,100us记一次数,中断100次为一个周期
// 也就是每10ms一个周期,也就是100MHz的频率
TL1 = 0xA4; //设置定时初始值
TH1 = 0xFF; //设置定时初始值
counter++;
counter%=100; // 当counter加到100时,再%100就会等于0,这样就可以不用写if判断
// compare_value的范围是0~100,与counter的范围一直
// 当compare_value小于counter时输出低电平,电机停止旋转
// 当compare_value大于counter时输出高电平,电机开始旋转
// 所以设置compare_value相当于在设置占空比,compare_value越大占空比越大
// 再根据需求设置compare_value的值就可以控制电机的旋转速度
if(counter>compare_value)
{
motor=0;
}
else
{
motor=1;
}
}
标签:P2,51,unsigned,char,单片机,开发,cease,b1,define
From: https://www.cnblogs.com/liuhousheng/p/17930877.html