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 ) ; //寫入個位 }}