目录
模块化编程
1、模块化编程的优点:
传统的编程方式:将所有的代码放在main.c里面,当使用的模块较多时,会使代码的耦合性较强,不利于代码的组织和管理。
模块化编程:将各个不同模块的代码放在不同的.c文件里面,在.h文件里提供外部可调用的声明。其他.c文件想使用其中的代码时,只需要#include "xx.h"即可。可提高代码的可阅读性,可维护性,可移植性等。
2、模块化编程注意事项:
1、.c文件:函数、变量的定义;
2、.h文件:可被外部调用的函数、变量的声明(函数声明为函数头后加上分号);
3、任何被调用的函数和变量在一个.c文件里必须有定义或声明;
4、任何被外部调用的函数与变量都必须有定义或声明,每一个函数名和变量都只能被声明一次;
5、使用到的自定义函数的.c文件必须添加到工程参与编译;
6、使用到的.h文件必须放在编译器可找到的地方,如工程文件夹根目录、安装目录、自定义;
3、c语言预编译:
C语言的预编译以#开头,作用是在真正的编译开始之前,对代码做一些处理
预编译 | 意义 |
---|---|
#include <REGX52.H> | 把REGX52.H文件的内容搬到此处 |
#define PI 3.14 | 不带参数的“宏”定义,定义PI,将PI替换为3.14 |
#define ABC | 定义ABC |
#define ABC | 取消之前的定义ABC |
#ifdef ABC | 如果定义xx_H |
#ifndef XX_H | 如果没有定义XX_H |
#endif | 与#ifndef,#if匹配,组成“括号” |
#if,#else,#elif | 与if,else,elif相似 |
在预编译中,存在#的预编译语句后不用加分号。
函数声明的注意事项:
因为当调用外部函数是需要#include对应的.h文件,而当.h文件被多次调用时,就容易造成函数的重复声明。所以在.h文件中,经常要加入以下代码:
#ifndef __DELAY_H__//如果DELAY没有定义
#define __DELAY_H__//就定义DELAY
void Delay(unsigned int xms);//函数声明
#endif//标志判断结束
//这样.h 文件中的函数就不会被重复声明。
LCD1602调试工具
由于在教程中已给出LCD1602的驱动代码,所以本此笔记中有关LCD1602中的内容将直接使用所提供的函数使用LCD1602。
使用LCD1602液晶屏作为调试窗口,提供类似printf函数的功能,可实时观察单片机内部数据的变换情况。
并且,由于LCD1602的显示不需要反复地扫描,不会长时间占用单片机的运行时间,比数码管和LED点阵屏更适应多种情况。与串口通信相比,不需要其他电脑之类的电子设备连接。所以LCD1602时理想的调试和演示。
教程中提供的代码如下:
函数 | 作用 |
---|---|
LCD_Init(); | 初始化 |
LCD_ShowChar(1,1,'A'); | 显示一个字符 |
LCD_ShowString(1,3,"Hello"); | 显示字符串 |
LCD_ShowNum(1,9,123,3); | 显示十进制数字 |
LCD_ShowSignedNum(1,13,-66,2); | 显示有符号十进制数字 |
LCD_ShowHexNum(2,1,0xA8,2); | 显示十六进制数字 |
LCD_ShowBinNum(2,4,0xAA,8); | 显示二进制数字 |
运用模块化编程,可轻松移植该代码。
矩阵键盘
矩阵键盘的输入方式:
在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式。
采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态。
原理:读取第1行(列)→读取第2行(列) →读取第3行(列) → ……,然后快速循环这个过程,最终实现所有按键同时检测的效果
矩阵键盘中涉及的P1口和P2,P3口均为准双向口输出类型。该类型的引脚为高电平时,驱动能力很弱。当引脚为低电平时,驱动能力很强,可吸收相当大的电流。
在教程中使用的开发板中的P1_5引脚与蜂鸣器的引脚冲突,所以教程中采用逐列扫描。但我的开发板中,P1_5引脚并不与蜂鸣器引脚冲突,所以,我才用逐行扫描代码如下:
#include <REGX52.H>
#include "Delay.h" //Delay函数已经封装在"Delay.h"中
unsigned char matrix()
{
unsigned char a=0;
P1=0xff;
P1_7=0;
if(P1_3==0){Delay(10);while(P1_3==0);Delay(10);a=1;}
if(P1_2==0){Delay(10);while(P1_2==0);Delay(10);a=2;}
if(P1_1==0){Delay(10);while(P1_1==0);Delay(10);a=3;}
if(P1_0==0){Delay(10);while(P1_0==0);Delay(10);a=4;}
P1=0xff;
P1_6=0;
if(P1_3==0){Delay(10);while(P1_3==0);Delay(10);a=5;}
if(P1_2==0){Delay(10);while(P1_2==0);Delay(10);a=6;}
if(P1_1==0){Delay(10);while(P1_1==0);Delay(10);a=7;}
if(P1_0==0){Delay(10);while(P1_0==0);Delay(10);a=8;}
P1=0xff;
P1_5=0;
if(P1_3==0){Delay(10);while(P1_3==0);Delay(10);a=9;}
if(P1_2==0){Delay(10);while(P1_2==0);Delay(10);a=10;}
if(P1_1==0){Delay(10);while(P1_1==0);Delay(10);a=11;}
if(P1_0==0){Delay(10);while(P1_0==0);Delay(10);a=12;}
P1=0xff;
P1_4=0;
if(P1_3==0){Delay(10);while(P1_3==0);Delay(10);a=13;}
if(P1_2==0){Delay(10);while(P1_2==0);Delay(10);a=14;}
if(P1_1==0){Delay(10);while(P1_1==0);Delay(10);a=15;}
if(P1_0==0){Delay(10);while(P1_0==0);Delay(10);a=16;}
return a;
}
在记录按键按下时,除了可以使用循环来检测松手外(使用循环检测松手的弊端是:按住按键时,程序会卡住),还可以通过检测上一次按键的状态与此次按键的状态变化来记录按键按下。如以下代码:
#include <REGX52.H>
#include "Delay.h"
unsigned char matrix()
{
unsigned char a=0,a_last=0;
P1=0xff;
P1_7=0;
if(P1_3==0&&a_last==0){Delay(10);a=1;}
if(P1_2==0&&a_last==0){Delay(10);a=2;}
if(P1_1==0&&a_last==0){Delay(10);a=3;}
if(P1_0==0&&a_last==0){Delay(10);a=4;}
P1=0xff;
P1_6=0;
if(P1_3==0&&a_last==0){Delay(10);a=5;}
if(P1_2==0&&a_last==0){Delay(10);a=6;}
if(P1_1==0&&a_last==0){Delay(10);a=7;}
if(P1_0==0&&a_last==0){Delay(10);a=8;}
P1=0xff;
P1_5=0;
if(P1_3==0&&a_last==0){Delay(10);a=9;}
if(P1_2==0&&a_last==0){Delay(10);a=10;}
if(P1_1==0&&a_last==0){Delay(10);a=11;}
if(P1_0==0&&a_last==0){Delay(10);a=12;}
P1=0xff;
P1_4=0;
if(P1_3==0&&a_last==0){Delay(10);a=13;}
if(P1_2==0&&a_last==0){Delay(10);a=14;}
if(P1_1==0&&a_last==0){Delay(10);a=15;}
if(P1_0==0&&a_last==0){Delay(10);a=16;}
a_last=a;
return a;
}
上述代码用于检测按键按下,并且不会使程序卡住。
由于检测按键的代码属于比较容易模块化的代码,所以,最好将该函数模块化,以便以后反复利用该代码。
定时器
定时器概况
51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成。
定时器作用:
(1)用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作。
(2)替代长时间的Delay,提高CPU的运行效率和处理速度。
定时器个数:3个(T0、T1、T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源。
一般来说,T0和T1的操作方式是所有51单片机所共有的。
定时器在单片机内部根据时钟的输出信号,每隔一次脉冲信号,计数单元的数值就增加一,当计数单元数值增加到“设定的定时器提醒时间”时,计数单元就会向中断系统发出中断申请,使程序跳到中断服务函数中执行。
注意:其中TCON(定时器控制寄存器)可位寻址;TMOD(定时器模式寄存器),TL0,TL1,TH0,TH1不可位寻址
定时器控制寄存器:![](https://i-blog.csdnimg.cn/blog_migrate/ce919dd51178b2fc74f8320a0e4db994.png)
寄存器是连接软硬件的媒介。在单片机中寄存器就是一段特殊的RAM存储器,一方面,寄存器可以存储和读取数据,另一方面,每一个寄存器背后都连接了一根导线,控制着电路的连接方式寄存器相当于一个复杂机器的“操作按钮”。
由于TCON寄存器可以位寻址,且暂时不需要用到外部中断。定时器1和定时器0的控制寄存器,即TF0,TR0,TR1,TF1。
本笔记中将以配置定时器0为例子,所以将值配置TF0和TR0。
定时器模式寄存器:
由图可知,M1和M0共同控制定时器的模式,GATE控制是否打开定时器,C/T控制定时器作为定时器或计时器。
STC89C52的T0和T1均有四种工作模式:
模式0:13位定时器/计数器
模式1:16位定时器/计数器(常用)
模式2:8位自动重装模式 (即不需要中断后将计数器重新赋值,多用于计算波特率)
模式3:两个8位计数器工作模式
因为模式1最为常用,所以,下文以模式1为样例,解释如何配置定时器:
1:定时器的时钟部分,每个一定时间发出一段脉冲;
SYSclK:系统时钟,即晶振周期,本开发板上的晶振为11.0592MHz;
T0Pin:可用于外接外部时钟;
C/T口:选择接入SYSclK还是T0Pin,用于将定时器设置为定时器还是计数器;
其定时器有两种计数速率:
一种是12T模式,每12个时钟加1,所以每一个“1”就是1微秒。
另一种是6T模式,每6个时钟加1,所以每一个“1”就是0.5微秒,是前一种速度的两倍。
2:定时器的计数部分,每接收到一个脉冲信号,计数器加1,计数范围是0到65535;
3:定时器的中断部分,TL0和TH0每溢出一次,TF0就变成1,并发出中断申请;
4:非门:与“!”同义;
5:或门:与“||”同义;
6:与门:与“&&”同义;
中断系统![](https://i-blog.csdnimg.cn/blog_migrate/7215b4618c3681be4a483cef70269feb.png)
中断结构
中断资源:
中断源个数:8个(外部中断0、定时器0中断、外部中断1、定时器1中断、串口中断、定时器2中断、外部中断2、外部中断3)
中断优先级个数:4个
注意:中断的资源和单片机的型号是关联在一起的,不同的型号可能会有不同的中断资源,
中断结构:
配置中断系统时,首先需要将中断允许控制寄存打开
(EA为整体的寄存器控制开关;
EX0,ET0,EX1,ET1,ES,ET2,EX2,EX3这些寄存器用于控制某一个中断源;)
之后配置该中断的中断优先级。
于是,当中断申请发出时,现在中断允许控制寄存器中判断是否允许中断,再判断其优先级高低,最后由中断号跳转到指定程序,执行完成返回正常程序。
各个中断的触发方式如下图:
各个中断查询次序号如下图:
中断寄存器![](https://i-blog.csdnimg.cn/blog_migrate/8e8c3a142d90a485ee3b146db0141c07.png)
由于IE寄存器可以位寻址,且我们只需要用到定时器0。所以,我们仅需配置ET0口。
中断优先级
本开发板上共有4个中断优先级,当多个中断申请同时发出时,单片机会根据中断优先级的高低判断优先处理哪一个中断。
由于本阶段接触的中断控制仅包含定时器,所以这里只介绍定时器的中断优先级控制。
本开发板上具有3个定时器T0,T1,T2,针对于不同的定时器,有以下的中断优先级控制
定时器0控制示例
用LCD1602作为显示屏,显示时钟的代码如下:
#include <REGX52.H>
#include "LCD1602.h" //使用LCD1602
void Timer0Init() //定时器0初始化。每1000微秒,产生一次中断。晶振频率:11.0592MHz
{
TMOD &= 0xF0; //把TMOD的低四位清零,高四位保持不变。
TMOD |= 0x01; //对TMOD的低四位赋值,高四位保持不变
//由于目的是使每1000微秒产生一次中断,所以计数器的初始应该为65535-1000=64535
TL0 = 0x66; //将64535变成16进制并取前8位得0x66。
TH0 = 0xFC; //将64535变成16进制并去后8位得0xfc。
TF0 = 0; //初始化TF0,使中断申请关闭
TR0 = 1; //开启定时器0
ET0=1; //定时器0中断申请控制允许
EA=1; //所有的中断申请启动
PT0=0; //配置中断优先级
}
unsigned int hour,minute,second; //定义小时,分钟,秒
int cnt=0; //定义计数器,用于记录每一毫秒
void main()
{
Timer0Init(); //配置定时器0,
LCD_Init(); //配置LCD1602
LCD_ShowString(1,2,"clock"); //使第一行显示“clock”
LCD_ShowString(2,1," : : "); //使第二行显示冒号
while(1) //死循环,将程序限制在循环内
{
LCD_ShowNum(2,1,hour,2); //显示小时
LCD_ShowNum(2,4,minute,2); //显示分钟
LCD_ShowNum(2,7,second,2); //显示秒
}
}
void Timer0_Rountine() interrupt 1
//其中”interrupt 1"是关键字,定时器0的中断触发后,将跳转到后面存在该关键字的函数处执行
{
TL0 = 0x66; //初始化计数器的值为64535
TH0 = 0xFC;
cnt++; //每一毫秒加一
if(cnt>=1000){ //每过1000毫秒秒数加一,cnt清零
cnt=0;
second++;
}
if(second>=60) //每过60秒分钟数加一,second清零
{
second=0;
minute++;
}
if(minute>=60) //每过60分钟小时数加一,minute清零
{
minute=0;
hour++;
}
if(hour>=24) //每过24小时,小时数清零
{
hour=0;
}
}
注意:
1、利用‘&=’,‘|=’,‘^=’等可实现对不可位寻址的引脚的对位赋值。
2、当需要新的不同的定时器配置时,可以在stc-isp上的“定时器计算器上获得相关配置代码。
3、之后,我们可以将上述代码中定时器0的初始化函数模块化,一边以后调用。(定时器中断函数由于和主函数的耦合性较强,一般不建议模块化。)
串口通信
串口概况
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大的扩展了单片机的应用范围,增强了单片机系统的硬件实力。
51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。而在我的开发板上,已经集成有UART和USB的转换器,所以只需要用USB数据线将单片机与电脑直接连接即可。
下图为UART的接口及引脚定义
1、流控:用于控制数据流的大小。(51单片机中不支持流控,所以可以忽略)
补充:有一种与DB9相似的接口名为VGA,共有3排15个引脚,一般用于传输视频。而UART接口只传输数据。
蓝牙串口:可是串口通过蓝牙发送或接受信息。
有关串口的硬件电路:
简单双向串口通信有两根通信线(发送端TXD和接收端RXD)
TXD与RXD要交叉连接
当只需单向的数据传输时,可以直接一根通信线
当电平标准不一致时,需要加电平转换芯片
电平标准:
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
TTL电平:+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等
全双工:通信双方可以在同一时刻互相传输数据。
使用不同的两条线来进行双方的接受和发送,因此允许双方同时相互传输数据。
半双工:通信双方可以互相传输数据,但必须分时复用一根数据线。
使用同一条线来进行双方的接受和发送,因此双方不能同时相互传输数据。
单工:通信只能有一方发送到另一方,不能反向传输。
仅允许一方发出信息,另一方接受信息
异步:通信双方各自约定通信速率。
在异步通信中,字符帧格式和波特率是两个重要指标。
1、字符帧也称数据帧,由起始位,数据位,奇偶校验位和停止位4部分组成。
起始位:位于字符帧开头,只占一位,始终为逻辑0低电平,用于向接受设备表示发送端开始发送的一帧信息。
数据位:紧跟起始位之后,位数由用户自取,低位在前,高位在后。
奇偶校验位:校验数据信息是否出错,使用奇校验还是偶校验由用户确定。
停止位:位于字符帧末尾,为逻辑1高电平,可取1位,1.5位或2位,用于表示字符信息已发送完成。
2、波特率:
波特率是串口通信的速率(发送和接收各数据位的间隔时间)
波特率的定义是每一秒传送二进制数码的位数(又称比特数),单位是b/s。
计算方式:波特率=1/定时器的溢出时间/16*10^6。
异步通信的优点是不需要传送同步脉冲,字符帧长度不受限制,所需设备简单。缺点是字符帧中包含了起始位和停止位,降低了有效数据的传输速率。
同步:通信双方靠一根时钟线来约定通信速率
通过以条时钟线将双方连接在一起,使通信的速率严格同步。
同步通信的数据传输速率较高,但缺点是要求接受时钟和发送时钟严格同步。
总线:连接各个设备的数据传输线路(类似于一条马路,把路边各住户连接起来,使住户可以相互交流)
在上述表格中的4个接口中,只有UART不符合总线的特点,因此没有总线。
51单片机的UART
STC89C52有1个UART
STC89C52的UART有四种工作模式:
模式0:同步移位寄存器
模式1:8位UART,波特率可变(常用)
模式2:9位UART,波特率固定
模式3:9位UART,波特率可变
由于模式1较为常用,因此后续内容将以模式1为示例(由于模式1中没有校验位,因此之后的代码中的异步通信中不再配置校验位)
下图红色方框为引脚对应的单片机接口
串口结构![](https://i-blog.csdnimg.cn/blog_migrate/b045bd2b0ca48d4856d86f912a9f8499.png)
1、总线;
2、SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器,读操作时,读出的是接收寄存器;
3、定时器:用于计算波特率;
4、中断系统;
下图中的红色方框内为串口接受和发送数据后,去往的中断逻辑。
串口相关寄存器![](https://i-blog.csdnimg.cn/blog_migrate/59f551e77d62d0eb659645fd54cc7917.png)
使用串口时只需要配置以下寄存器即可:
SCON:串口控制寄存器
PCON:电源控制寄存器
IE:中断系统
串口通信示例
下列代码实现将通过串口由电脑发送数据给LED,点亮LED,并发送会接受值的功能
#include <REGX52.H>
/**
* @brief 串口初始化,4800bps@11.0592MHz
* @param 无
* @retval 无
*/
void UART_INIT(void) //4800bps@11.0592MHz
{
PCON |= 0x80; //使能波特率倍速位SMOD
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //清除定时器1模式位
TMOD |= 0x20; //设定定时器1为8位自动重装方式
TL1 = 0xF4; //设定定时初值
TH1 = 0xF4; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
EA=1; //使能中断
ES=1; //允许中断
PT0=0; //配置中断优先级
}
/**
* @brief 串口发送一个数据
* @param a,要发送的数据
* @retval 无
*/
void UART_SEND(unsigned char a)
{
SBUF=a; //串口发送数据a
while(TI==0);
TI=0; //将TI清零
}
void main()
{
UART_INIT();
while(1)
{
}
}
void UART_Routine() interrupt 4
{
if(RI==1) //判断中断来自接受数据
{
P2=~SBUF; //将电脑发送数据给串口,接收到的数据使LED灯点亮
UART_SEND(~P2); //将接受到的数据发回给电脑
RI=0; //将RI清零
}
}
LED点阵屏
LED点阵屏概况
LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯珠亮灭来显示文字、图片、视频等。
LED点阵屏广泛应用于各种公共场合,如汽车报站器、广告屏以及公告牌等。
LED点阵屏分类 按颜色:单色、双色(显示红色,绿色)、全彩(显示红色,绿色,蓝色)。
按像素:8 * 8,16 * 16等(大规模的LED点阵通常由很多个8 * 8小点阵拼接而成)。
LED点阵屏的结构类似于数码管,只不过是数码管把每一列的像素以“8”字型排列而已。
LED点阵屏与数码管一样,有共阴和共阳两种接法,不同的接法对应的电路结构。
不同LED点阵屏需要进行逐行或逐列扫描,才能使所有LED同时显示,在使LED点阵屏点亮时,需要单片机持续扫描,因此也会占用单片机运行。
由上图得,当P0口接低电平,DP口接高电平时,就能点亮该LED点阵屏中的某个点.
由于单片机的引脚属于"弱上拉"类型,所以DP口不可以直接接在单片机的引脚上,否则LED的亮度会很暗.需要经过一个放大电路,将电流放大,才能驱动LED.
74HC595![](https://i-blog.csdnimg.cn/blog_migrate/719bb2c9d334267801cd60f5518a0d9e.png)
74HC595是串行输入并行输出的移位寄存器,可用3根线输入串行数据,8根线输出并行数据,多片级联后,可输出16位、24位、32位等,常用于IO口扩展。
特点 :
8 位串行输入
8 位串行或并行输出
存储状态寄存器,三种状态
输出寄存器可以直接清除
100MHz 的移位频率
输出能力
并行输出,总线驱动
串行输出;标准
中等规模集成电路
SER串行数据进入移位寄存器,串行时钟SRCLK每有一次上升沿,SER前进一位,当计存器时钟RCLK变成高电平时,移位计存器中的所有数据并行输出到Q口端.
注意:每一次时钟上升沿之后要将上一次的时钟清零.
当串行输入的数据超过移位寄存器的存储范围时,之前的数据将通过QH'口移出移位计时器,这时如果在QH'口后端有下一个移位寄存器记录数据的话,就能实现16位,24位,甚至32位的多片级联,扩展I/O口.
缺点:对时钟的速度有较高的要求,并且会导致传输数据的速度变慢.
由于该74HC595芯片仅由引脚16提供电源,因此,当需要同时输出多个高电平时,电流的驱动能力会较弱.
特殊功能寄存器的声明![](https://i-blog.csdnimg.cn/blog_migrate/b0a4b04f3737bc738fcf161392366d64.png)
sfr(special function register):
特殊功能寄存器声明 例:sfr P0 = 0x80; 声明P0口寄存器,物理地址为0x80
sbit(special bit):
特殊位声明 例:sbit P0_1 = 0x81; 或 sbit P0_1 = P0^1; 声明P0寄存器的第1位可位寻址/不可位寻址:
在单片机系统中,操作任意寄存器或者某一位的数据时,必须给出其物理地址,又因为一个寄存器里有8位,所以位的数量是寄存器数量的8倍,单片机无法对所有位进行编码,故每8个寄存器中,只有一个是可以位寻址的。对不可位寻址的寄存器,若要只操作其中一位而不影响其它位时,可用“&=”、“|=”、“^=”的方法进行位操作.
在每一个.c文件中,都需要对某个寄存器声明之后,才能使用声明后的寄存器名称.
LED点阵屏显示示例
使用LED点阵屏显示的代码如下:
由于LED点阵屏是以扫描的方式显示,因此在没有延时和清零的情况下容易出现显示结果的串位.
所以,需要进行对LED进行消影,即在段选之后延时1毫秒,并清零.
#include <REGX52.H> //包含头文件REGX52
#include "Delay.H" //包含已经模块化的头文件Delay
sbit RCK=P3^5; //对P3_5重新声明
sbit SCK=P3^6; //对P3_6重新声明
sbit SER=P3^4; //对P3_4重新声明
void _74hc595(unsigned char a) //定义函数使串行输入数据到74HC595,然后并行输出
{
unsigned char i; //定义循环计数器
for(i=0;i<8;i++) //移位寄存器一共有8位,所以逐位移位
{
SER=a&(0x80>>i); //取数据a的第几位
SCK=1; //向下移一位
SCK=0; //将时钟清零
}
RCK=1; //将8位数据输出
RCK=0; //将时钟清零
}
void mztrix(unsigned char column,date) //定义函数点亮第column行,二进制数为date的灯
{
_74hc595(date); //将date输入到74HC595进行并联输出
P0=~(0x80>>column); //将需要检测的行位置移位到对应的column行
Delay(1); //延时1毫秒,避免LED亮度下降
P0=0xff; //将所有的LED清零
}
unsigned char code ANimation[]={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x24,0x29,0xFF,0x28,0x00,0x1F,0x15,0x7F,0x15,0x1F,0x00,0x00,0x08,0x08,0x7F,0x2A,
0x29,0x08,0x00,0x00,0x44,0x48,0x50,0x6F,0x48,0x44,0x00,0xA1,0xD7,0xD1,0xFF,0xD5,
0xB5,0x81,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
}; //定义数组,用于存放动画中须点亮LED灯
void main()
{
unsigned char i=0,k=0,cnt=0;
//定义i为扫描的LED的行位置;定义cnt为动画的时钟;定义k为动画每过一定时间的偏移量
while(1)
{
for(i=0;i<8;i++) //扫描显示LED点阵屏
mztrix(i,ANimation[i+k]);
cnt++; //时钟加一
if(cnt>=10){
cnt=0; //重置时钟
k++; //偏移量加一
}
if(k>=48) //当偏移量足够大时,重置偏移量,使动画循环播放.
k=0;
}
}
DS1302芯片
DS1302介绍![](https://i-blog.csdnimg.cn/blog_migrate/a2e581fa38e61a4873e19ad9927bdb7d.jpeg)
上图为我所使用的单片机上的DS1302芯片
DS1302是由美国DALLAS公司推出的一款实时时钟电路,具有涓流充电能力,属于低功耗产品。该芯片包含了实时时钟/日历和31字节静态RAM,可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能。DS1302与单片机之间的通信,仅需三根I/O线:复位(RET)、I/O数据线、串行时钟,通过这三线接口与CPU进行同步通信,
补充:RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片。DS1302就是一种实时时钟芯片。
同类芯片还有:
DS3231:一款低成本、高精度的I²C实时时钟(RTC),该器件包含了集成的温补晶振(TCXO)和晶体。
DS12C887:这款芯片集成了晶体振荡器和外部锂电池相关电路于芯片内部,具有低功耗、外围接口简单、精度高、工作稳定可靠等优点。
等等……
DS1302的优点:
1、DS1302通过内部电路处理后向时钟芯片提供一个稳定的脉冲,使得DS1302的精度较高,稳定性强。
2、DS1302具备涓流充电的能力,可以在备用电源的支持下持续计时,甚至在断电情况下也能继续工作。
3、DS1302的计时功能相对独立,不会占用CPU的运行时间。
DS1302的结构![](https://i-blog.csdnimg.cn/blog_migrate/9abe223f2c28fabe7ddd60dc49cc1480.png)
管脚 | 名称 | 功能 |
---|---|---|
1 | VCC2 | 双供电配置中的主电源供应管脚,VCC1 连接到备用电源,在主电源 失效时保持时间和日期数据.DS1302 工作于 VCC1 和 VCC2 中较大者. 当 VCC2比 VCC1高 0.2V时,VCC2 给 DS1302供电.当 VCC1 比 VCC2高 时, VCC1给 DS1302供电. |
2,3 | X1,X2 | 与标准的32.768kHz 石英晶体相连. 内部振荡器被设计与指定的6pF 装载电容的晶体一起工作. DS1302也可以被外部的 32.768kHz 振荡器驱动. 这种配置下, X1 与 外部震荡信号连接,X2悬浮. |
4 | GND | 电源地 |
5 | CE | 输入.CE信号在读写时必须保持高电平.此管脚内部有一个 40kΩ(典 型值)的下拉电阻连接到地. |
6 | I/O | 输入/推挽输出.I/O 管脚是三线接口的双向数据管脚.此管脚内部有 一个 40kΩ(典型值)的下拉电阻连接到地. |
7 | SCLK | 输入. SCLK 用来同步串行接口上的数据动作.此管脚内部有一个 40kΩ(典型值)的下拉电阻连接到地. |
8 | VCC1 | 低功率工作在单电源和电池工作系统和低功率备用电池.在使用涓流 充电的系统中,这个管脚连接到可再充能量源. |
由于我的单片机上的DS1302上没有连接备用电池,因此掉电状态下,我的DS1302不能运行。
1、电源控制
2、连接晶振,输出一个标准的1赫兹频率
3、与单片机的通信电路,实现对DS1302的访问
4、内部寄存器,用于存放与时间相关的数据或其他数据
与时钟相关的寄存器
寄存器定义:
命令字:
命令字启动每一次数据传输. MSB (位 7)必须是逻辑 1. 如果是 0, 则禁止对 DS1302写入.
位 6 在逻辑 0时规定为时钟/日历数据,逻辑 1时为 RAM数据.
位 1 至 位 5 表示了输入输出的指定寄存器.
LSB (位 0) 在逻辑0时为写操作(输出),逻辑 1时为读操作(输入).
命令字以 LSB (位 0)开始总是输入. (即从最低位开始输入)
时钟暂停标志 : 秒寄存器的位 7被定义为时钟暂停标志. 当此为置 1时,时钟振荡器暂停, DS1302进入漏 电流小于 100nA的低功耗备用模式. 当此为置 0时,时钟开始.初始加电状态未定义.
写保护位: 控制寄存器的位 7是写保护位,前 7位(位 0至位 6)被强制为 0且读取时总是读 0. 在任 何对时钟或 RAM的写操作以前,位 7必须为 0.当为高时,写保护位禁止任何寄存器的写操 作. 初始加电状态未定义. 因此,在试图写器件之前应该清除 WP位.
访问DS1302
时序定义:![](https://i-blog.csdnimg.cn/blog_migrate/bc4ace2cbb7e302e3a9805fec475be87.png)
只有当CE置一时,单片机才能对DS1302进行有效的读写;
当数据有单片机传给DS1302时,会在SCLK的上升沿周期中,控制I/O口写入数据。即A,C,D区。
当数据有DS1302传给单片机时,会在SCLK的下降沿周期中,控制I/O口输出数据。即B区。
无论时输入还是输出,都是由低位开始到高位。
由此可看出,DS1302的通信协议是一种半双工,同步的通信协议。(这很重要,这导致了在后面的读取数据时,需要将IO口清零,以防止上一次的传输对下一次传输造成影响)
BCD码的转换
BCD码(Binary Coded Decimal),用4位二进制数来表示1位十进制数
例:0001 0011表示13,1000 0101表示85,0001 1010不合法
在十六进制中的体现:0x13表示13,0x85表示85,0x1A不合法
BCD码转十进制:DEC=BCD/16 * 10+BCD%16; (2位BCD)
十进制转BCD码:BCD=DEC/10 * 16+DEC%10; (2位BCD)
使用DS1302示例
使用DS1302输入和输出秒
#include <REGX52.H>
#include "lcd1602.h"
#include "ds1302.h"
sbit DS1302_SCLK=P3^6; //将引脚重命名
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
void DS1302_init() //将DS1302初始化
{
DS1302_CE=0;
DS1302_SCLK=0;
}
void DS1302_write(unsigned char address,unsigned char input)
//address为输入的命令字,input为输入的内容。
{
unsigned char i=0; //定义计数器
DS1302_CE=1; //启动使能DS1302
for(i=0;i<8;i++) //定义8次循环
{
DS1302_IO=address&(0x01<<i); //将address的第i位赋值给DS1302_IO
DS1302_SCLK=1; //上升沿,将DS1302_IO的数据输入DS1302
DS1302_SCLK=0; //回复下降沿
}
for(i=0;i<8;i++) //定义8次循环
{
DS1302_IO=input&(0x01<<i); //将input的第i位赋值给DS1302_IO
DS1302_SCLK=1; //上升沿,将DS1302_IO的数据输入DS1302
DS1302_SCLK=0; //回复下降沿
}
DS1302_CE=0; //关闭使能DS1302
}
unsigned char DS1302_read(unsigned char address)
//address为输入的命令字
{
unsigned char i;
unsigned char output=0x00;
//定义计数器i,输出内容output
DS1302_CE=1; //启动使能DS1302
for(i=0;i<8;i++) //定义8次循环
{
DS1302_IO=address&(0x01<<i); //将address的第i位赋值给DS1302_IO
DS1302_SCLK=0; //回复下降沿
DS1302_SCLK=1; //上升沿,将DS1302_IO的数据输入DS1302
//这样使得命令字输入完成后,DS1302_SCLK=1
}
for(i=0;i<8;i++) //定义8次循环
{
DS1302_SCLK=1; //回复上升沿
DS1302_SCLK=0; //下降沿,将DS1302的数据输入DS1302_IO
if(DS1302_IO) output|=(0x01<<i); //用output的第i位接受DS1302_IO的值
}
DS1302_CE=0; //关闭使能DS1302
DS1302_IO=0;
/*此时,需要将DS1302_IO清零。
由于DS1302芯片是一个双向通信设备,它既可以向主控制器发送数据,也可以从主控制器接收数据。在读取数据的过程中,主控制器会通过DS1302_IO向DS1302芯片发送命令和读取数据的指令。
为了确保下一次读取操作的正确性,需要将DS1302_IO清零,以清除可能存在的输入信号。这样可以防止上一次读取操作对下一次读取操作产生干扰或影响。*/
return output; // 将函数返回output
}
unsigned char second; //定义秒变量
void main ()
{
LCD_Init(); //将LCD初始化
LCD_ShowString(1,1,"RTC"); //使其第一行,第一列显示"RTC"
DS1302_init(); //初始化DS1302
DS1302_write(0x8E,0x00); //解除写保护
DS1302_write(0x80,0x55); //写入时间,这里写入的0x55是BCD码形式
DS1302_write(0x8E,0x80); //开启写保护
while(1)
{
second=DS1302_read(0x81); //循环访问DS1302的秒位置
LCD_ShowNum(2,1,second/16*10+second%16,2); //循环刷新LCD1602,并将BCD码转换成十进制
}
}
使用DS1302制作小型可调时钟
(包括年,月,日,时,分,秒,日期,修改时间,控制闪烁等功能)
首先是模块化好的DS1302函数:
#include <REGX52.H>
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
unsigned char time[]={23,11,20,7,23,59,55};
void DS1302_init()
{
DS1302_CE=0;
DS1302_SCLK=0;
}
void DS1302_write(unsigned char address,unsigned char input)
{
unsigned char i=0;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=address&(0x01<<i);
DS1302_SCLK=1;
DS1302_SCLK=0;
}
for(i=0;i<8;i++)
{
DS1302_IO=input&(0x01<<i);
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;
}
unsigned char DS1302_read(unsigned char address)
{
unsigned char i;
unsigned char output=0x00;
//output|=0x01;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=address&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
for(i=0;i<8;i++)
{
DS1302_SCLK=1;
DS1302_SCLK=0;
if(DS1302_IO) output|=(0x01<<i);
}
DS1302_CE=0;
DS1302_IO=0;
return output;
}
void DS1302_settime()
{
DS1302_write(0x8e,0x00);
DS1302_write(0x8c,time[0]/10*16+time[0]%10);
DS1302_write(0x88,time[1]/10*16+time[1]%10);
DS1302_write(0x86,time[2]/10*16+time[2]%10);
DS1302_write(0x8a,time[3]/10*16+time[3]%10);
DS1302_write(0x84,time[4]/10*16+time[4]%10);
DS1302_write(0x82,time[5]/10*16+time[5]%10);
DS1302_write(0x80,time[6]/10*16+time[6]%10);
DS1302_write(0x8e,0x80);
}
void DS1302_readtime()
{
unsigned char a;
a=DS1302_read(0x8d);
time[0]=a/16*10+a%16;
a=DS1302_read(0x89);
time[1]=a/16*10+a%16;
a=DS1302_read(0x87);
time[2]=a/16*10+a%16;
a=DS1302_read(0x8b);
time[3]=a/16*10+a%16;
a=DS1302_read(0x85);
time[4]=a/16*10+a%16;
a=DS1302_read(0x83);
time[5]=a/16*10+a%16;
a=DS1302_read(0x81);
time[6]=a/16*10+a%16;
}
接下来是声明
#ifndef __ds1302_H__
#define __ds1302_H__
extern unsigned char time[];
void DS1302_init();
void DS1302_write(unsigned char address,unsigned char input);
unsigned char DS1302_read(unsigned char address);
void DS1302_settime();
void DS1302_readtime();
#endif
主函数中引用的其他函数在之前的部分中有体现过,不再列出。
最后是主函数:
#include <REGX52.H>
#include "lcd1602.h"
#include "ds1302.h"
#include "order.h"
#include "timer0.h"
int a=0;
void main ()
{
unsigned char mode,cnt=0;
LCD_Init();
DS1302_init();
DS1302_settime();
while(1){
if(order()==1) mode=1;
if(order()==2) mode=2;
switch(mode)
{case 1:
{
LCD_ShowString(1,3,"-");
LCD_ShowString(1,6,"-");
LCD_ShowString(1,9,"-");
LCD_ShowString(2,3,":");
LCD_ShowString(2,6,":");
DS1302_readtime();
LCD_ShowNum(1,1,time[0],2);
LCD_ShowNum(1,4,time[1],2);
LCD_ShowNum(1,7,time[2],2);
LCD_ShowNum(1,10,time[3],1);
LCD_ShowNum(2,1,time[4],2);
LCD_ShowNum(2,4,time[5],2);
LCD_ShowNum(2,7,time[6],2);
break;
}
case 2:{
cnt=-1;
Timer0Init();
while(order()!=1)
{
if(order()==2)
{
while(order()==2);
cnt++;
cnt=cnt%7;
}
if(order()==3)
{
while(order()==3);
time[cnt]++;
}
if(order()==4)
{
while(order()==4);
time[cnt]--;
}
switch(cnt)
{
case 0:{time[cnt]=time[cnt]%100;break;}
case 1:{
time[cnt]=time[cnt]%12;
if(time[cnt]==0) time[cnt]=12;
break;
}
case 2:
{
if(time[1]==1||time[1]==3||time[1]==5||time[1]==7||time[1]==8||time[1]==10||time[1]==12)
{
time[cnt]=time[cnt]%31;
if(time[cnt]==0) time[cnt]=31;
}
if(time[1]==4||time[1]==6||time[1]==9||time[1]==11)
{
time[cnt]=time[cnt]%30;
if(time[cnt]==0) time[cnt]=30;
}
if(time[1]==2)
{
if(time[0]%4)
{
time[cnt]=time[cnt]%28;
if(time[cnt]==0) time[cnt]=28;
}
else
{
time[cnt]=time[cnt]%29;
if(time[cnt]==0) time[cnt]=29;
}
}
break;
}
case 3:
{
time[cnt]=time[cnt]%7;
if(time[cnt]==0) time[cnt]=7;
break;
}
case 4:{time[cnt]=time[cnt]%24;break;}
case 5:{time[cnt]=time[cnt]%60;break;}
case 6:{time[cnt]=time[cnt]%60;break;}
}
if(a>=500)
{
switch(cnt)
{
case 0:{LCD_ShowString(1,1," ");break;}
case 1:{LCD_ShowString(1,4," ");break;}
case 2:{LCD_ShowString(1,7," ");break;}
case 3:{LCD_ShowString(1,10," ");break;}
case 4:{LCD_ShowString(2,1," ");break;}
case 5:{LCD_ShowString(2,4," ");break;}
case 6:{LCD_ShowString(2,7," ");break;}
}
}
else
{
LCD_ShowNum(1,1,time[0],2);
LCD_ShowNum(1,4,time[1],2);
LCD_ShowNum(1,7,time[2],2);
LCD_ShowNum(1,10,time[3],1);
LCD_ShowNum(2,1,time[4],2);
LCD_ShowNum(2,4,time[5],2);
LCD_ShowNum(2,7,time[6],2);
}
}
DS1302_settime();
break;}
}
}
}
void Timer0_Rountine() interrupt 1
{
TL0 = 0x66;
TH0 = 0xFC;
a++;
if(a>=1000) a=0;
}
我的学习笔记到此为止,谢谢阅读!