8.1 实时时钟简介
RTC(Real Time Clock),是实时时钟的缩写,实时时钟是日常生活中应用最为广泛的功能。它为人们提供精确的实时时间,或者为电子系统提供精确的时间基准,目前实时时钟芯片大多采用精度较高的晶体振荡器作为时钟源。有些时钟芯片为了在主电源掉电时,还可以工作,需要外加电池供电。
现在的高端处理器大都内置了RTC模块,但是由于51单片机速度较慢,主要用于低端的控制系统中,所以没有内置RTC模块,需要采用时钟芯片来完成这个功能,现在常用的时钟芯片有很多,现在以DS1302为例说明时钟芯片的使用方法。
8.2 DS1302简介
8.2.1 DS1302概述
DS1302是美国DALLAS公司推出的一种高性能、低功耗的实时时钟芯片,附加31字节静态RAM,采用SPI三线接口与CPU进行同步通信,并可采用突发方式一次传送多个字节的时钟信号和RAM数据。实时时钟可提供秒、分、时、日、星期、月和年,一个月小于31天时可以自动调整,且具有闰年补偿功能。工作电压宽达2.5~5.5V。采用双电源供电(主电源和备用电源),可设置备用电源充电方式,提供了对备用电源进行涓细电流充电的能力。
8.2.2通信协议
在之前的章节中,除了USART那一部分,都是采用了并行通信作为数据传输的方式,并行通信虽然速度很快,但是对硬件有着很高的要求,比如如果传输8位的数据,就需要8根通信线,如果是16位的数据就需要16根通信线,并且随着通信线长度不一样,可能会存在数据错误或者丢失的情况。串行通信虽然速度没有并行通信那么高,但是一根数据线可以传送任意字节的数据,降低了设计中布线的难度。
DS1302就是串行通信方式,芯片的引脚分布如下图所示。
引脚编号 | 英文缩写 | 引脚功能 |
1 | VCC2 | 主电源 |
2 | X1 | 32.768KHz晶振 |
3 | X2 | 32.768KHz晶振 |
4 | GND | 数字地 |
5 | RST | 复位 |
6 | I/O | 数据输入/输出 |
7 | CLK | 时钟输入 |
8 | VCC1 | 备用电源(接电池) |
串行通信中,用到了两个端口,时钟信号CLK和数据信号I/O,时钟信号用于提供数据发送的脉冲,数据信号I/O用于将数据拆成0101的形式发送过去,DS1302的时序包括读和写两种时序,时序图如下图所示。
(1)写时序
(2)读时序
8.2.3 RTC内部寄存器
(1)秒寄存器
读地址:0x81
写地址:0x80
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | 数据范围 |
CH | Second 1 | Second 2 | 0~59 |
Bit 7:时钟开关
0:关闭
1:开启
Bit 6~Bit 4:秒数据十位
Bit 3~Bit 0:秒数据个位
(2)分钟寄存器
读地址:0x83
写地址:0x82
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | 数据范围 |
- | Minute 1 | Minute 2 | 0~59 |
Bit 6~Bit 4:分钟数据十位
Bit 3~Bit 0:分钟数据个位
(3)小时寄存器
读地址:0x85
写地址:0x84
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | 数据范围 |
12/24 | 0 | Hour 1 | Hour 2 | 1~12 0~23 | ||||
AM/PM | Hour 1 |
Bit 7:小时制选择
0:24小时制
1:12小时制
Bit 5~Bit 4:小时数据十位(24小时制)
当Bit 7设置为12小时制的时候Bit5代表上下午,Bit 4代表小时数据的十位
Bit 3~Bit 0:小时数据个位
(4)日期寄存器
读地址:0x87
写地址:0x86
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | 数据范围 |
0 | 0 | Data 1 | Data 2 | 1~31 |
Bit 5~Bit 4:日期数据十位
Bit 3~Bit 0:日期数据个位
(5)月份寄存器
读地址:0x89
写地址:0x88
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | 数据范围 |
0 | 0 | 0 | Month 1 | Month 2 | 1~12 |
Bit 5~Bit 4:月份数据十位
Bit 3~Bit 0:月份数据个位
(6)星期寄存器
读地址:0x8B
写地址:0x8A
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | 数据范围 |
0 | 0 | 0 | 0 | 0 | Day | 1~7 |
Bit 2~Bit 0:星期数据个位
(7)年份寄存器
读地址:0x8D
写地址:0x8C
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | 数据范围 |
Year 1 | Year 2 | 0~99 |
Bit 7~Bit 4:年份数据十位
Bit 3~Bit 0:年份数据个位
(8)写保护寄存器
读地址:0x8F
写地址:0x8E
Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 | 数据范围 |
WP | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Bit 7:写保护控制
0:关闭写保护
1:开启写保护
8.2.4 原理图
8.3 例程分析
(1)由于程度很长,只做几个重点位置的讲解。先来看显示部分
在之前1602显示的实验上增加了一个函数LCD_Show_String,这个函数用于在屏幕任意位置显示字符串,C语言中的字符串其实是一个一维数组,这个一维数组中存放的是ASCII码,假设定义一个字符串Hello World,那么实际在单片机里面存储的数据如下表所示
00 H | 01 H | 02 H | 03 H | 04 H | 05 H | 06 H | 07 H | 08 H | 09 H | 0A H |
H | e | l | l | o | W | o | r | l | d |
换算到16进制里面就是
00 H | 01 H | 02 H | 03 H | 04 H | 05 H | 06 H | 07 H | 08 H | 09 H | 0A H |
0x48 | 0x65 | 0x6C | 0x6C | 0x6F | 0x20 | 0x57 | 0x6F | 0x72 | 0x6C | 0x64 |
现在来分析这个子函数
第112行:使用switch语句来进行坐标转换,因为LCD1602第1行第1个位置的地址是0x80,第2行第1个位置的地址则是0xC0,所以需要用分支语句来控制最后的地址
第115行,如果是第1行(第1行用0表示的),那么地址就是行地址加列地址,1602内部规定了列地址从0~15,如果是第1行第2个位置,那么具体的地址就应该是0x80+1=0x81,如果是第2行第5个位置就应该是0xC0+4=0xC4
第124行:地址设置属于输入命令,所以应该调用LCD命令写入函数,将之前的地址数据写入LCD1602中
第125行:由于LCD1602设置了地址自动加一,所以写入连续的数据的时候不需要频繁设置地址,这就可以采用循环的方式把字符串写进去,ASCII虽然有128个数据,但是能够显示的数据并不多,仔细观察ASCII码表可以发现,只有空格之后的数据是可以显示的,之前的都是控制字符,而空格的ASCII码值是0x20,程序中的\0的ASCII码值是0x00,也就是说当检测到要写入的数据是0x00的时候就说明字符串写完了,此时结束循环即可
第127行:利用LCD数据写入函数把指针指向的地址里面的数据写入LCD1602
第128行:指针自增,为了让指针指向的下一个字符的地址,因为数组里面的数据在地址中都是连续存放的,如果第一个字符的地址是0x00,那么下一个字符的地址就一定是0x01
(2)然后我们来看DS1302的驱动函数,重点分析如何将一个字节拆分成0101的二进制位发出去,并分析如何将0101的二进制位变成一个完整的字节。
假设存在1个字节0x23,现在我想把这个字节从最低位到最高位一位一位的将数据传送出去,应该怎么办呢?
首先23 H=0010 0011B,最低位是1,最高位是0,现在将0x23&0x01进行运算,结果当然是0x01,这时,我们就应该将数据线变成1,然后0x23往右移动一个二进制位,得出的结果是11 H=0001 0001 B(这里有一个重点,数据右移的时候,最高位是补0的,数据左移的时候,最低位补0)。
假设上面的数据右移了2次后,最初的23 H变成了08 H=0000 1000 B,现在继续对0x08&0x01做运算得出的结果是0,这时,将数据线变为0,如此循环8次,就可以将1个字节分成串行数据一位一位的传送出去了。
上图所示的代码就是串行数据的发送与接收,下面开始考虑接收,如何将串行数据拼接成并行数据呢?
假设串行数据先发送最低位,首先将一个数据00 H右移一个二进制位,得出的数据当然还是00 H,然后如果数据总线上的电平是1,那么此时就把00 H和80 H做或运算,得出的结果就是80 H,然后下一个电平的时候80 H右移一个二进制位,得出的结果是40 H,如果此时数据线的电平还是1,那就继续和80 H做或运算,得C0 H,最终通过8次运算,就可以将1个字节全部接收完毕。
根据上面的分析和DS1302的时序图,就可以写出DS1302读取数据的函数,如下图所示。
(3)下面我们来分析下如何将DS1302计算得出的数据显示在屏幕上,主函数的程序如下图所示。
在while循环里面,由于数据不连续,所以需要先写显示的地址,然后写入数据以显示年为例,由于年份后面2位(个位和十位)的坐标是第1行的第4列和第5列,所以只需要将地址设置成第一行的第4列就行了,由于1602内部地址从0开始,所以第1行的第4列地址应该是0x80+3。
第229行和第230行里面,数据除以10取整数部分和除以10取余数部分都比较容易理解,那么为什么要加上0x30呢,这是因为ASCII码表里面,0~9的ASCII值是0x30~0x39,所以如果不加0x30,那么写入的0~9实际是控制字符,刚才说过了ASCII码表里面0x20之前的都是控制字符,直接写入0x00~0x09是不显示的,所以加上0x30之后,9就变成了0x39。
8.4 完整代码
/********************************************************************************************************* 头 文 件 引 用*********************************************************************************************************/#include //导入51单片机头文件/********************************************************************************************************* 数 据 类 型 定 义*********************************************************************************************************/#define u8 unsigned char //定义无符号字符型数据(0~255)#define u16 unsigned int //定义无符号整型数据(0~65535)/********************************************************************************************************* 硬 件 端 口 定 义*********************************************************************************************************///LCD1602控制端口#define LCD_DB P0 //LCD数据口sbit LCD_RS = P2^0 ; //数据命令选择sbit LCD_RW = P2^1 ; //读写控制sbit LCD_EN = P2^2 ; //使能控制//DS1302控制端口sbit DS_CLK = P2^6 ; //串行时钟sbit DS_RST = P2^5 ; //复位sbit DS_IO = P2^7 ; //串行数据/********************************************************************************************************* 数 据 结 构 定 义*********************************************************************************************************/typedef struct{ u8 Second; //秒 u8 Minute; //分 u8 Hour; //时 u8 Date; //日 u8 Month; //月 u8 Year; //年}DS1302_Data;DS1302_Data Time;/********************************************************Name :delay_msFunction :毫秒延时函数Paramater : ms:延时的时间Return :None********************************************************/void delay_ms( u16 ms ){ u8 i ; while( --ms ) for( i=0; i<110; i++ ) ;}/********************************************************************************************************* LCD1602 显 示 程 序*********************************************************************************************************//********************************************************Name :LCD_Write_CommandFunction :LCD写入命令Paramater : Command:命令代码Return :None********************************************************/void LCD_Write_Command( u8 Command ){ LCD_RS = 0 ; //命令模式 LCD_RW = 0 ; //写模式 LCD_EN = 0 ; //使能复位 LCD_DB = Command ; //发送数据到P0总线 delay_ms( 5 ) ; LCD_EN = 1 ; //使能拉高 delay_ms( 1 ) ; LCD_EN = 0 ; //下降沿数据写入 delay_ms( 1 ) ;}/********************************************************Name :LCD_Write_DataFunction :LCD写入数据Paramater : Data:数据Return :None********************************************************/void LCD_Write_Data( u8 Data ){ LCD_RS = 1 ; //数据模式 LCD_RW = 0 ; //写模式 LCD_EN = 0 ; //使能复位 LCD_DB = Data ; //发送数据到P0总线 delay_ms( 5 ) ; LCD_EN = 1 ; //使能拉高 delay_ms( 1 ) ; LCD_EN = 0 ; //下降沿数据写入 delay_ms( 1 ) ;}/********************************************************Name :LCD_InitFunction :LCD初始化Paramater :NoneReturn :None********************************************************/void LCD_Init(){ LCD_Write_Command( 0x38 ) ; //8位总线宽度+显示2行+每个字符占用5×10的点阵 LCD_Write_Command( 0x0C ) ; //开启显示+关闭光标+关闭光标显示 LCD_Write_Command( 0x06 ) ; //光标右移+写入数据后显示屏不移动 LCD_Write_Command( 0x01 ) ; //清屏}/********************************************************Name :LCD_Show_StringFunction :LCD显示字符串Paramater :NoneReturn :None********************************************************/void LCD_Show_String( u8 x, u8 y, u8 *str ){ u8 Address ; //计算坐标 switch( y ) { case 0: Address=0x80+x ; //第一行数据地址 break; case 1: Address=0xC0+x ; //第二行数据地址 break; default: break; } //写入数据 LCD_Write_Command( Address ) ; //设置写入地址 while( *str!='\0' ) { LCD_Write_Data( *str ) ; //写入数据 str ++ ; //指针地址累加 }}/********************************************************************************************************* DS1302 时 钟 程 序*********************************************************************************************************//********************************************************Name :DS1302_Write_ByteFunction :DS1302写入字节Paramater : Byte:写入的字节Return :None********************************************************/void DS1302_Write_Byte( u8 Byte ){ u8 i ; for( i=0; i<8; i++ ) { if( ( Byte&0x01 )==0x01 ) //判断最低位是1 DS_IO = 1 ; //数据线拉高发送1 else DS_IO = 0 ; //数据线拉低发送0 Byte >>= 1 ; //数据右移一个位 DS_CLK = 0 ; //时钟线复位 DS_CLK = 1 ; //时钟线拉高产生上升沿 }}/********************************************************Name :DS1302_Read_ByteFunction :DS1302读取字节Paramater :NoneReturn :读取的字节********************************************************/u8 DS1302_Read_Byte(){ u8 i, Byte ; DS_CLK = 1 ; //时钟线拉高 Byte = 0 ; for( i=0; i<8; i++ ) { Byte >>= 1 ; //数据右移一个位 DS_CLK = 0 ; //时钟线拉低产生下降沿 if( DS_IO==1 ) //判断数据线上的值为1 Byte |= 0x80 ; //字节写入1 DS_CLK = 1 ; //时钟线拉高 } return Byte ;}/********************************************************Name :DS1302_Read_TimeFunction :DS1302读取时间Paramater :NoneReturn :None********************************************************/void DS1302_Read_Time(){ u8 i, Byte ; u8 Read_Address[] = { 0x81, 0x83, 0x85, 0x87, 0x89, 0x8D } ; //寄存器地址 for( i=0; i<6; i++ ) { DS_RST = 0 ; //复位 DS_CLK = 0 ; //时钟线复位 DS_RST = 1 ; //停止复位 DS1302_Write_Byte( Read_Address[ i ] ) ; //发送地址 Byte = DS1302_Read_Byte() ; //读取数据 switch( i ) { case 0: Time.Second = ( ( Byte&0xF0 )>>4 )*10+( Byte&0x0F ) ; //计算秒 break ; case 1: Time.Minute = ( ( Byte&0xF0 )>>4 )*10+( Byte&0x0F ) ; //计算分 break ; case 2: Time.Hour = ( ( Byte&0xF0 )>>4 )*10+( Byte&0x0F ) ; //计算时 break ; case 3: Time.Date = ( ( Byte&0xF0 )>>4 )*10+( Byte&0x0F ) ; //计算日 break ; case 4: Time.Month = ( ( Byte&0xF0 )>>4 )*10+( Byte&0x0F ) ; //计算月 break ; case 5: Time.Year = ( ( Byte&0xF0 )>>4 )*10+( Byte&0x0F ) ; //计算年 break ; } }}/********************************************************************************************************* 主 函 数*********************************************************************************************************/void main(){ LCD_Init() ; LCD_Show_String( 0, 0, " 2000 - 00 - 00 " ) ; LCD_Show_String( 0, 1, " 00 : 00 : 00 " ) ; while( 1 ) { DS1302_Read_Time() ; //DS1302读取时间 //显示年 LCD_Write_Command( 0x80+3 ) ; //写入显示地址 LCD_Write_Data( 0x30+Time.Year/10 ) ; //写入十位 LCD_Write_Data( 0x30+Time.Year%10 ) ; //写入个位 //显示月 LCD_Write_Command( 0x80+8 ) ; //写入显示地址 LCD_Write_Data( 0x30+Time.Month/10 ) ; //写入十位 LCD_Write_Data( 0x30+Time.Month%10 ) ; //写入个位 //显示日 LCD_Write_Command( 0x80+13 ) ; //写入显示地址 LCD_Write_Data( 0x30+Time.Date/10 ) ; //写入十位 LCD_Write_Data( 0x30+Time.Date%10 ) ; //写入个位 //显示时 LCD_Write_Command( 0xC0+2 ) ; //写入显示地址 LCD_Write_Data( 0x30+Time.Hour/10 ) ; //写入十位 LCD_Write_Data( 0x30+Time.Hour%10 ) ; //写入个位 //显示分 LCD_Write_Command( 0xC0+7 ) ; //写入显示地址 LCD_Write_Data( 0x30+Time.Minute/10 ) ; //写入十位 LCD_Write_Data( 0x30+Time.Minute%10 ) ; //写入个位 //显示秒 LCD_Write_Command( 0xC0+12 ) ; //写入显示地址 LCD_Write_Data( 0x30+Time.Second/10 ) ; //写入十位 LCD_Write_Data( 0x30+Time.Second%10 ) ; //写入个位 }}