本例直接忽略了星期這項內容,通過上、下、左、右、回車、ESC 這 6 個按鍵可以調整時間。這也是一個具有綜合練習性質的實例,雖然在功能實現上沒有多少難度,但要進行的操作卻比較多而且煩瑣,同學們可以從中體會到把繁雜的功能實現分解為一步步函數操作的必要性以及方便靈活性。簡單說一下這個程序的幾個要點,方便大家閱讀理解程序。
- 把 DS1302 的底層操作封裝為一個 DS1302.c 文件,對上層應用提供基本的實時時間的操作接口,這個文件也是我們的又一個功能模塊了,我們的積累也越來越多了。
- 定義一個結構體類型 sTime 用來封裝日期時間的各個元素,又用該結構體定義了一個時間緩衝區變量 bufTime 來暫存從 DS1302 讀出的時間和設置時間時的設定值。需要注意的是在其它文件中要使用這個結構體變量時,必須首先再聲明一次 sTime 類型;
- 定義一個變量 setIndex 來控製當前是否處於設置時間的狀態,以及設置時間的哪一位,該值為 0 就表示正常運行,1~12 分別代表可以修改日期時間的 12 個位;
- 由於這節課的程序功能要進行時間調整,用到了 1602 液晶的光標功能,添加了設置光標的函數,我們要改變哪一位的數字,就在 1602 對應位置上進行光標閃爍,所以 Lcd1602.c在之前文件的基礎上添加了兩個控製光標的函數;
- 時間的顯示、增減、設置移位等上層功能函數都放在 main.c 中來實現,當按鍵需要這些函數時則在按鍵文件中做外部聲明,這樣做是為了避免一組功能函數分散在不同的文件內而使程序顯得淩亂。
/***************************DS1302.c 文件程序源代碼*****************************/ #include <reg52.h> sbit DS1302_CE = P1^7; sbit DS1302_CK = P3^5; sbit DS1302_IO = P3^4; struct sTime { //日期時間結構體定義 unsigned int year; //年 unsigned char mon; //月 unsigned char day; //日 unsigned char hour; //時 unsigned char min; //分 unsigned char sec; //秒 unsigned char week; //星期 }; /* 發送一個字節到 DS1302 通信總線上 */ void DS1302ByteWrite(unsigned char dat){ unsigned char mask; for (mask=0x01; mask!=0; mask<<=1){ //低位在前,逐位移出 if ((mask&dat) != 0){ //首先輸出該位數據 DS1302_IO = 1; }else{ DS1302_IO = 0; } DS1302_CK = 1; //然後拉高時鍾 DS1302_CK = 0; //再拉低時鍾,完成一個位的操作 } DS1302_IO = 1; //最後確保釋放 IO 引腳 } /* 由 DS1302 通信總線上讀取一個字節 */ unsigned char DS1302ByteRead(){ unsigned char mask; unsigned char dat = 0; for (mask=0x01; mask!=0; mask<<=1){ //低位在前,逐位讀取 if (DS1302_IO != 0){ //首先讀取此時的 IO 引腳,並設置 dat 中的對應位 dat |= mask; } DS1302_CK = 1; //然後拉高時鍾 DS1302_CK = 0; //再拉低時鍾,完成一個位的操作 } return dat; //最後返回讀到的字節數據 } /* 用單次寫操作向某一寄存器寫入一個字節,reg-寄存器地址,dat-待寫入字節 */ void DS1302SingleWrite(unsigned char reg, unsigned char dat){ DS1302_CE = 1; //使能片選信號 DS1302ByteWrite((reg<<1)|0x80); //發送寫寄存器指令 DS1302ByteWrite(dat); //寫入字節數據 DS1302_CE = 0; //除能片選信號 } /* 用單次讀操作從某一寄存器讀取一個字節,reg-寄存器地址,返回值-讀到的字節 */ unsigned char DS1302SingleRead(unsigned char reg){ unsigned char dat; DS1302_CE = 1; //使能片選信號 DS1302ByteWrite((reg<<1)|0x81); //發送讀寄存器指令 dat = DS1302ByteRead(); //讀取字節數據 DS1302_CE = 0; //除能片選信號 return dat; } /* 用突發模式連續寫入 8 個寄存器數據,dat-待寫入數據指針 */ void DS1302BurstWrite(unsigned char *dat){ unsigned char i; DS1302_CE = 1; DS1302ByteWrite(0xBE); //發送突發寫寄存器指令 for (i=0; i<8; i++){ //連續寫入 8 字節數據 DS1302ByteWrite(dat[i]); } DS1302_CE = 0; } /* 用突發模式連續讀取 8 個寄存器的數據,dat-讀取數據的接收指針 */ void DS1302BurstRead(unsigned char *dat){ unsigned char i; DS1302_CE = 1; DS1302ByteWrite(0xBF); //發送突發讀寄存器指令 for (i=0; i<8; i++){ //連續讀取 8 個字節 dat[i] = DS1302ByteRead(); } DS1302_CE = 0; } /* 獲取實時時間,即讀取 DS1302 當前時間並轉換為時間結構體格式 */ void GetRealTime(struct sTime *time){ unsigned char buf[8]; DS1302BurstRead(buf); time->year = buf[6] + 0x2000; time->mon = buf[4]; time->day = buf[3]; time->hour = buf[2]; time->min = buf[1]; time->sec = buf[0]; time->week = buf[5]; } /* 設定實時時間,時間結構體格式的設定時間轉換為數組並寫入 DS1302 */ void SetRealTime(struct sTime *time){ unsigned char buf[8]; buf[7] = 0; buf[6] = time->year; buf[5] = time->week; buf[4] = time->mon; buf[3] = time->day; buf[2] = time->hour; buf[1] = time->min; buf[0] = time->sec; DS1302BurstWrite(buf); } /* DS1302 初始化,如發生掉電則重新設置初始時間 */ void InitDS1302(){ unsigned char dat; struct sTime code InitTime[] = { //2013 年 10 月 8 日 12:30:00 星期二 0x2013,0x10,0x08, 0x12,0x30,0x00, 0x02 }; DS1302_CE = 0; //初始化 DS1302 通信引腳 DS1302_CK = 0; dat = DS1302SingleRead(0); //讀取秒寄存器 if ((dat & 0x80) != 0){ //由秒寄存器最高位 CH 的值判斷 DS1302 是否已停止 DS1302SingleWrite(7, 0x00); //撤銷寫保護以允許寫入數據 SetRealTime(&InitTime); //設置 DS1302 為默認的初始時間 } }DS1302.c 最終向外提供出與具體時鍾芯片寄存器位置無關的、由時間結構類型 sTime 作為接口的實時時間的讀取和設置函數,如此處理體現了我們前麵提到過的層次化編程的思想。應用層可以不關心底層實現細節,底層實現的改變也不會對應用層造成影響,比如說日後你可能需要換一款時鍾芯片,而它與 DS1302 的操作和時間寄存器順序是不同的,那麽你需要做的也僅是針對這款新的時鍾芯片設計出底層操作函數,最終提供出同樣的以 sTime 為接口的操作函數即可,應用層無需做任何的改動。
/***************************Lcd1602.c 文件程序源代碼*****************************/ #include <reg52.h> #define LCD1602_DB P0 sbit LCD1602_RS = P1^0; sbit LCD1602_RW = P1^1; sbit LCD1602_E = P1^5; /* 等待液晶準備好 */ void LcdWaitReady(){ unsigned char sta; LCD1602_DB = 0xFF; LCD1602_RS = 0; LCD1602_RW = 1; do { LCD1602_E = 1; sta = LCD1602_DB; //讀取狀態字 LCD1602_E = 0; } while (sta & 0x80); //bit7 等於 1 表示液晶正忙,重複檢測直到其等於 0 為止 } /* 向 LCD1602 液晶寫入一字節命令,cmd-待寫入命令值 */ void LcdWriteCmd(unsigned char cmd){ LcdWaitReady(); LCD1602_RS = 0; LCD1602_RW = 0; LCD1602_DB = cmd; LCD1602_E = 1; LCD1602_E = 0; } /* 向 LCD1602 液晶寫入一字節數據,dat-待寫入數據值 */ void LcdWriteDat(unsigned char dat){ LcdWaitReady(); LCD1602_RS = 1; LCD1602_RW = 0; LCD1602_DB = dat; LCD1602_E = 1; LCD1602_E = 0; } /* 設置顯示 RAM 起始地址,亦即光標位置,(x,y)-對應屏幕上的字符坐標 */ void LcdSetCursor(unsigned char x, unsigned char y){ unsigned char addr; if (y == 0){ //由輸入的屏幕坐標計算顯示 RAM 的地址 addr = 0x00 + x; //第一行字符地址從 0x00 起始 }else{ addr = 0x40 + x; //第二行字符地址從 0x40 起始 } LcdWriteCmd(addr | 0x80); //設置 RAM 地址 } /* 在液晶上顯示字符串,(x,y)-對應屏幕上的起始坐標,str-字符串指針 */ void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str){ LcdSetCursor(x, y); //設置起始地址 while (*str != '\0'){ //連續寫入字符串數據,直到檢測到結束符 LcdWriteDat(*str++); } } /* 打開光標的閃爍效果 */ void LcdOpenCursor(){ LcdWriteCmd(0x0F); } /* 關閉光標顯示 */ void LcdCloseCursor(){ LcdWriteCmd(0x0C); } /* 初始化 1602 液晶 */ void InitLcd1602(){ LcdWriteCmd(0x38); //16*2 顯示,5*7 點陣,8 位數據接口 LcdWriteCmd(0x0C); //顯示器開,光標關閉 LcdWriteCmd(0x06); //文字不動,地址自動+1 LcdWriteCmd(0x01); //清屏 }為了本例的具體需求,在之前文件的基礎上添加兩個控製光標效果打開和關閉的函數,雖然函數都很簡單,但為了保持程序整體上良好的模塊化和層次化,還是應該在液晶驅動文件內以函數的形式提供,而不是由應用層代碼直接來調用具體的液晶寫命令操作。
/***************************keyboard.c 文件程序源代碼****************************/
(此處省略,可參考之前章節的代碼)
/*****************************main.c 文件程序源代碼******************************/ #include <reg52.h> struct sTime { //日期時間結構體定義 unsigned int year; unsigned char mon; unsigned char day; unsigned char hour; unsigned char min; unsigned char sec; unsigned char week; }; bit flag200ms = 1; //200ms 定時標誌 struct sTime bufTime; //日期時間緩衝區 unsigned char setIndex = 0; //時間設置索引 unsigned char T0RH = 0; //T0 重載值的高字節 unsigned char T0RL = 0; //T0 重載值的低字節 void ConfigTimer0(unsigned int ms); void RefreshTimeShow(); extern void InitDS1302(); extern void GetRealTime(struct sTime *time); extern void SetRealTime(struct sTime *time); extern void KeyScan(); extern void KeyDriver(); extern void InitLcd1602(); extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str); extern void LcdSetCursor(unsigned char x, unsigned char y); extern void LcdOpenCursor(); extern void LcdCloseCursor(); void main(){ unsigned char psec=0xAA; //秒備份,初值 AA 確保首次讀取時間後會刷新顯示 EA = 1; //開總中斷 ConfigTimer0(1); //T0 定時 1ms InitDS1302(); //初始化實時時鍾 InitLcd1602(); //初始化液晶 //初始化屏幕上固定不變的內容 LcdShowStr(3, 0, "20 - - "); LcdShowStr(4, 1, " : : "); while (1){ KeyDriver(); //調用按鍵驅動 if (flag200ms && (setIndex == 0)){ //每隔 200ms 且未處於設置狀態時, flag200ms = 0; GetRealTime(&bufTime); //獲取當前時間 if (psec != bufTime.sec){ //檢測到時間有變化時刷新顯示 RefreshTimeShow(); psec = bufTime.sec; //用當前值更新上次秒數 } } } } /* 將一個 BCD 碼字節顯示到屏幕上,(x,y)-屏幕起始坐標,bcd-待顯示 BCD 碼 */ void ShowBcdByte(unsigned char x, unsigned char y, unsigned char bcd){ unsigned char str[4]; str[0] = (bcd >> 4) + '0'; str[1] = (bcd&0x0F) + '0'; str[2] = '\0'; LcdShowStr(x, y, str); } /* 刷新日期時間的顯示 */ void RefreshTimeShow(){ ShowBcdByte(5, 0, bufTime.year); ShowBcdByte(8, 0, bufTime.mon); ShowBcdByte(11, 0, bufTime.day); ShowBcdByte(4, 1, bufTime.hour); ShowBcdByte(7, 1, bufTime.min); ShowBcdByte(10, 1, bufTime.sec); } /* 刷新當前設置位的光標指示 */ void RefreshSetShow(){ switch (setIndex){ case 1: LcdSetCursor(5, 0); break; case 2: LcdSetCursor(6, 0); break; case 3: LcdSetCursor(8, 0); break; case 4: LcdSetCursor(9, 0); break; case 5: LcdSetCursor(11, 0); break; case 6: LcdSetCursor(12, 0); break; case 7: LcdSetCursor(4, 1); break; case 8: LcdSetCursor(5, 1); break; case 9: LcdSetCursor(7, 1); break; case 10: LcdSetCursor(8, 1); break; case 11: LcdSetCursor(10, 1); break; case 12: LcdSetCursor(11, 1); break; default: break; } } /* 遞增一個 BCD 碼的高位 */ unsigned char IncBcdHigh(unsigned char bcd){ if ((bcd&0xF0) < 0x90){ bcd += 0x10; }else{ bcd &= 0x0F; } return bcd; } /* 遞增一個 BCD 碼的低位 */ unsigned char IncBcdLow(unsigned char bcd){ if ((bcd&0x0F) < 0x09){ bcd += 0x01; }else{ bcd &= 0xF0; } return bcd; } /* 遞減一個 BCD 碼的高位 */ unsigned char DecBcdHigh(unsigned char bcd){ if ((bcd&0xF0) > 0x00){ bcd -= 0x10; }else{ bcd |= 0x90; } return bcd; } /* 遞減一個 BCD 碼的低位 */ unsigned char DecBcdLow(unsigned char bcd){ if ((bcd&0x0F) > 0x00){ bcd -= 0x01; }else{ bcd |= 0x09; } return bcd; } /* 遞增時間當前設置位的值 */ void IncSetTime(){ switch (setIndex){ case 1: bufTime.year = IncBcdHigh(bufTime.year); break; case 2: bufTime.year = IncBcdLow(bufTime.year); break; case 3: bufTime.mon = IncBcdHigh(bufTime.mon); break; case 4: bufTime.mon = IncBcdLow(bufTime.mon); break; case 5: bufTime.day = IncBcdHigh(bufTime.day); break; case 6: bufTime.day = IncBcdLow(bufTime.day); break; case 7: bufTime.hour = IncBcdHigh(bufTime.hour); break; case 8: bufTime.hour = IncBcdLow(bufTime.hour); break; case 9: bufTime.min = IncBcdHigh(bufTime.min); break; case 10: bufTime.min = IncBcdLow(bufTime.min); break; case 11: bufTime.sec = IncBcdHigh(bufTime.sec); break; case 12: bufTime.sec = IncBcdLow(bufTime.sec); break; default: break; } RefreshTimeShow(); RefreshSetShow(); } /* 遞減時間當前設置位的值 */ void DecSetTime(){ switch (setIndex){ case 1: bufTime.year = DecBcdHigh(bufTime.year); break; case 2: bufTime.year = DecBcdLow(bufTime.year); break; case 3: bufTime.mon = DecBcdHigh(bufTime.mon); break; case 4: bufTime.mon = DecBcdLow(bufTime.mon); break; case 5: bufTime.day = DecBcdHigh(bufTime.day); break; case 6: bufTime.day = DecBcdLow(bufTime.day); break; case 7: bufTime.hour = DecBcdHigh(bufTime.hour); break; case 8: bufTime.hour = DecBcdLow(bufTime.hour); break; case 9: bufTime.min = DecBcdHigh(bufTime.min); break; case 10: bufTime.min = DecBcdLow(bufTime.min); break; case 11: bufTime.sec = DecBcdHigh(bufTime.sec); break; case 12: bufTime.sec = DecBcdLow(bufTime.sec); default: break; } RefreshTimeShow(); RefreshSetShow(); } /* 右移時間設置位 */ void RightShiftTimeSet(){ if (setIndex != 0){ if (setIndex < 12){ setIndex++; }else{ setIndex = 1; } RefreshSetShow(); } } /* 左移時間設置位 */ void LeftShiftTimeSet(){ if (setIndex != 0){ if (setIndex > 1){ setIndex--; }else{ setIndex = 12; } RefreshSetShow(); } } /* 進入時間設置狀態 */ void EnterTimeSet(){ setIndex = 2; //把設置索引設置為 2,即可進入設置狀態 LeftShiftTimeSet(); //再利用現成的左移操作移到位置 1 並完成顯示刷新 LcdOpenCursor(); //打開光標閃爍效果 } /* 退出時間設置狀態,save-是否保存當前設置的時間值 */ void ExitTimeSet(bit save){ setIndex = 0; //把設置索引設置為 0,即可退出設置狀態 if (save){ //需保存時即把當前設置時間寫入 DS1302 SetRealTime(&bufTime); } LcdCloseCursor(); //關閉光標顯示 } /* 按鍵動作函數,根據鍵碼執行相應的操作,keycode-按鍵鍵碼 */ void KeyAction(unsigned char keycode){ if ((keycode>='0') && (keycode<='9')){ //本例中不響應字符鍵 }else if (keycode == 0x26){ //向上鍵,遞增當前設置位的值 IncSetTime(); }else if (keycode == 0x28){ //向下鍵,遞減當前設置位的值 DecSetTime(); }else if (keycode == 0x25){ //向左鍵,向左切換設置位 LeftShiftTimeSet(); }else if (keycode == 0x27){ //向右鍵,向右切換設置位 RightShiftTimeSet(); }else if (keycode == 0x0D){ //回車鍵,進入設置模式/啟用當前設置值 if (setIndex == 0){ //不處於設置狀態時,進入設置狀態 EnterTimeSet(); }else{ //已處於設置狀態時,保存時間並退出設置狀態 ExitTimeSet(1); } }else if (keycode == 0x1B){ //Esc 鍵,取消當前設置 ExitTimeSet(0); } } /* 配置並啟動 T0,ms-T0 定時時間 */ void ConfigTimer0(unsigned int ms){ unsigned long tmp; //臨時變量 tmp = 11059200 / 12; //定時器計數頻率 tmp = (tmp * ms) / 1000; //計算所需的計數值 tmp = 65536 - tmp; //計算定時器重載值 tmp = tmp + 28; //補償中斷響應延時造成的誤差 T0RH = (unsigned char)(tmp>>8); //定時器重載值拆分為高低字節 T0RL = (unsigned char)tmp; TMOD &= 0xF0; //清零 T0 的控製位 TMOD |= 0x01; //配置 T0 為模式 1 TH0 = T0RH; //加載 T0 重載值 TL0 = T0RL; ET0 = 1; //使能 T0 中斷 TR0 = 1; //啟動 T0 } /* T0 中斷服務函數,執行按鍵掃描和 200ms 定時 */ void InterruptTimer0() interrupt 1{ static unsigned char tmr200ms = 0; TH0 = T0RH; //重新加載重載值 TL0 = T0RL; KeyScan(); //按鍵掃描 tmr200ms++; if (tmr200ms >= 200){ //定時 200ms tmr200ms = 0; flag200ms = 1; } }main.c 主文件,負責所有應用層的功能實現,文件比較長,還是那句話“不難但比較煩瑣”,希望對具體問題分析細化能力還不太強的同學們把這個文件多練習幾遍,學習一下其中把具體問題逐步細化並一步步實現出來的編程思想,多進行此類練習,鍛煉程序思維能力,將來遇到具體項目設計需求的時候,你很快就可以找到方法並實現它們了。