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 ) ;                              //写入个位  }}