試)
簡介一份面向51單片機初學者的電子萬年歷設計資料以LCD12864為顯示終端重點呈現(xiàn)時鐘獲取、時間格式轉(zhuǎn)換與顯示刷新等核心邏輯適合課程設計、畢設或單片機入門實踐。壓縮包共16個文件大小僅115KB涵蓋C語言源碼、Keil工程文件uvproj/uvopt、Proteus仿真電路DSN及可直接燒錄的HEX文件還有編譯過程生成的OBJ、LST、M51等輔助文件便于對照分析。目前已有991人學習下載可用于快速搭建基于12864的萬年歷實驗。包內(nèi)附帶了完整的工程備份與設計源文件讀者既能直接運行仿真查看效果也能通過閱讀源碼和編譯中間文件理解51單片機控制LCD12864的底層過程對學習定時器、實時時鐘和液晶驅(qū)動均有實際參考價值。1. 為什么“過時的”51單片機加電子萬年歷依然是練手首選先說個大實話電子萬年歷這個項目放在2025年看芯片不算新、屏不算大、功能也不算炫。但如果你去看各大電子社區(qū)的熱搜詞51單片機、電子萬年歷、LCD12864這幾個詞常年不跌。原因很簡單這個項目幾乎把單片機開發(fā)里最常用的“零件”全串起來了——按鍵輸入、顯示驅(qū)動、時鐘芯片的時序讀寫、單總線溫度傳感器、中斷定時器、狀態(tài)機編程甚至掉電保存。它能解決一個很實際的問題讓剛接觸嵌入式的人在兩周到一個月內(nèi)把“點燈”和“做產(chǎn)品”之間的鴻溝填平。市面上很多課設、畢設、競賽題都脫胎于電子萬年歷的變體比如“基于51單片機的多功能日歷”“帶鬧鐘的時鐘顯示系統(tǒng)”。做完這一套后面再去碰智能家居、物聯(lián)網(wǎng)節(jié)點之類的項目硬件和軟件邏輯都會順很多。我在帶新人時最喜歡讓他們先做這個項目原因也很樸素它能把“會讀代碼”和“會寫代碼”區(qū)分開。51單片機雖然老但正因為老資料透明、例程多、仿真環(huán)境成熟遇到問題你不會被工具鏈卡住而是把精力留在真正的邏輯調(diào)試上。LCD12864又比1602多出“中文顯示”的樂趣成就感來得很直接。所以這篇就按我實際做過的方案來講。整個系統(tǒng)以STC89C52RC為主控搭配DS1302時鐘芯片、DS18B20溫度傳感器、LCD12864帶字庫液晶屏再加三個按鍵和一個蜂鳴器。接下來從硬件選型、原理圖設計、顯示驅(qū)動、軟件架構(gòu)、調(diào)試排障一路講到底。2. 硬件選型與原理圖設計每個引腳都不能白接2.1 主控芯片選擇STC89C52RC為什么是它STC89C52RC算是國內(nèi)51開發(fā)板上最常見的芯片。它的優(yōu)勢不是性能而是友好5V供電和LCD12864、DS1302、DS18B20這些外設電平天然匹配Flash程序存儲器可以用串口ISP下載不需要額外燒錄器IO口是準雙向口做按鍵輸入時多數(shù)情況下內(nèi)部弱上拉夠用。有人會問直接用STC12、STC15這種1T單片機不是更快嗎確實如果你以后要刷復雜界面或做動畫1T的單片機更合適。但新手階段12T的經(jīng)典51主頻12MHz指令周期1us配合萬年歷這種秒級變化的應用性能綽綽有余。更重要的是教程和代碼范例絕大多數(shù)都基于傳統(tǒng)51時序踩坑成本最低。2.2 DS1302時鐘芯片的電路細節(jié)DS1302是Dallas公司的一顆串行實時時鐘芯片用三根線控制SCLK、IO、CE有些資料叫RST。它能輸出年月日時分秒和星期內(nèi)置RAM可以存配置內(nèi)置涓流充電電路可以用在可充電紐扣電池方案上。電路上最容易翻車的是三處。第一32.768kHz晶振。這顆晶振的負載電容通常是6pF不要隨便用12pF甚至20pF的否則可能不起振或走時不準。第二IO線必須接一個4.7k到10k的上拉電阻DS1302的數(shù)據(jù)引腳是雙向的沒有上拉容易出現(xiàn)讀數(shù)據(jù)不穩(wěn)定。第三后備電池電路。如果你用CR2032這種不可充電的紐扣電池DS1302的涓流充電寄存器地址0x90必須寫0x00關(guān)閉充電功能否則相當于給一次性電池充電很可能把電池充鼓包。如果用的是可充電的LIR2032才可以寫0xA5開啟一個二極管加2kΩ電阻的涓流充電。DS1302引腳連接建議 VCC2 - 主電源5V或VCC1備用電池 VCC1 - 紐扣電池正極CR2032 GND - 公共地 SCLK - P1.0 IO - P1.1外部上拉4.7k CE/RST - P1.2 X1 X2 - 32.768kHz晶振2.3 LCD12864、DS18B20和按鍵的接線邏輯LCD12864這里用的是帶字庫的ST7920控制器版本市場上常見的藍底白字或黃綠底屏基本都是它。它有兩種接口并行8位和串行3線。對51來說IO資源不算特別緊張但我強烈建議新手用串行模式測試——接線少、時序簡單而且萬年歷這種靜態(tài)文字刷新場景根本用不到并行模式的高吞吐量。串行模式下三個引腳CS片選、SID串行數(shù)據(jù)、SCLK串行時鐘。習慣上我會把CS當“使能”只有CS為低時才接受數(shù)據(jù)。接法建議CS接P2.5SID接P2.6SCLK接P2.7然后VL對比度引腳上接一個10k電位器到GND。很多人前期黑屏就是對比度沒調(diào)好。DS18B20是單總線器件數(shù)據(jù)線DQ接P1.3同時外接一個4.7k上拉到5V。單總線上只有一個設備時可以不接上拉也能跑但溫度高、線長的時候數(shù)據(jù)容易錯所以上拉電阻還是得加。按鍵我習慣接P3.0、P3.1、P3.2分別對應“設置/切換”“加”“減”按鍵另一端接地單片機內(nèi)部弱上拉即可。蜂鳴器用有源5V蜂鳴器通過PNP三極管S8550驅(qū)動基極串1k電阻接P2.0引腳為低電平時蜂鳴器響。2.4 電源與復位穩(wěn)字當頭系統(tǒng)用USB供5V即可。但要注意LCD12864背光電流加上單片機和其他器件整機電流可能到120mA以上USB口供電沒問題如果用7805從12V降壓7805必須加散熱片。每個芯片的電源引腳旁邊放一個104去耦電容DS1302附近再加10uF電解電容這樣走時更穩(wěn)定。復位電路用標準的10uF電容加10k電阻。晶振用11.0592MHz因為如果需要串口下載/調(diào)試11.0592MHz才能得到標準的9600波特率如果確定不用串口用12MHz也行但51成品板普遍帶USB轉(zhuǎn)串口下載建議直接上11.0592MHz省得后面調(diào)波特率時后悔。完成以上接線后硬件部分就算搭好了一個可以開始寫代碼的架子。我建議在做PCB前先拿開發(fā)板和杜邦線把外設點起來確認每一顆芯片都能正常工作再考慮畫板。3. LCD12864顯示驅(qū)動的核心操作帶字庫版本的優(yōu)勢就在這里3.1 初始化順序與基本命令帶字庫的ST7920上電后默認是8位并行模式如果要切到串行模式初始化時有個固定套路。我整理出的穩(wěn)定初始化過程是這樣的上電后延時至少50ms 發(fā)送命令 0x30 // 8位數(shù)據(jù)長度基本指令集 延時5ms 發(fā)送命令 0x30 // 再發(fā)一次 延時5ms 發(fā)送命令 0x0C // 顯示開光標關(guān) 延時5ms 發(fā)送命令 0x01 // 清屏 延時5ms 發(fā)送命令 0x06 // 寫入后地址自動加一在串行模式下每次發(fā)送的“字節(jié)”其實是對應8位數(shù)據(jù)拆成兩部分的先發(fā)高4位再發(fā)低4位。具體來說串行協(xié)議是CS拉低后每次傳輸先收到5個1作為同步位然后是RW0表示寫RS表示寫的是命令還是數(shù)據(jù)最后是8位內(nèi)容分高4位和低4位兩段發(fā)送。新手最容易錯的就是這個《先高4位后低4位》的順序反了就會出現(xiàn)完整亂碼。3.2 漢字顯示原理寫兩個字節(jié)就能顯示一個漢字帶字庫款最大的好處是不需要自己做字模。ST7920內(nèi)部固化了8192個中文字符和英文字符只要你往DDRAM寫入GB2312編碼的兩個字節(jié)屏上就會顯示對應漢字。比如“電”字內(nèi)碼是0xB5 0xE7“子”字內(nèi)碼是0xD7 0xD3。寫字符串時直接按GB2312編碼發(fā)送即可。在Keil里寫u8 code str[] 電子萬年歷編譯器生成的字符串本身就是GB2312編碼注意Keil默認編碼是GB2312/ANSI不是UTF-8。DDRAM地址要特別注意ST7920的顯示地址不是連續(xù)的四行。第一行地址從0x80到0x87第二行從0x90到0x97第三行從0x88到0x8F第四行從0x98到0x9F。第三行地址反而在第二行后面、但實際屏幕位置是第三行這是很多人顯示內(nèi)容串位的原因。我的顯示框架是維護一個二維數(shù)組作為顯示緩沖區(qū)需要更新時算出目標行地址調(diào)用一個LCD_WriteString(row, col, str)函數(shù)。比如第一行顯示“2025年01月15日”第二行顯示“12:30:45 星期三”第三行顯示“溫度: 25.5C”每行的列坐標從0到7。這樣主邏輯只需要改數(shù)據(jù)剩下的由顯示函數(shù)負責。3.3 擴展指令與繪圖模式該不該碰ST7920還有一個擴展指令集通過0x34從基本指令集切入可以操作GDRAM畫點陣圖。理論上可以做開機動畫、曲線圖。但要注意GDRAM的64x32點陣和屏幕上的128x64像素不是一一對應的它把128x64分成上下兩個區(qū)域每個區(qū)域?qū)粔K64x32的GDRAM寫入地址還有行號反轉(zhuǎn)的坑。對萬年歷項目來說用繪圖模式做開機Logo可以但不要把核心顯示邏輯建立在繪圖模式上否則后期改需求會很痛苦。我個人的建議是主界面全部用字符顯示先保證信息正確有余力再研究圖形把開機Logo、體溫計圖標之類的當作畫龍點睛。因為萬年歷項目核心價值在邏輯完整、走時準確而不是炫繪圖。4. 軟件框架DS1302時間讀取、按鍵狀態(tài)機與顯示刷新協(xié)同4.1 時間基準DS1302讀寫函數(shù)與BCD碼DS1302的操作本質(zhì)上就是模擬三線時序。寫一個字節(jié)時在SCLK上升沿把IO引腳的數(shù)據(jù)鎖存讀一個字節(jié)時在SCLK下降沿之后從IO讀取數(shù)據(jù)。字節(jié)傳輸是低位在前。讀時間核心代碼大致長這樣unsigned char DS1302_ReadByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { dat 1; if (DS1302_IO) dat | 0x80; DS1302_SCLK 1; DS1302_SCLK 0; } return dat; } unsigned char DS1302_ReadReg(unsigned char addr) { unsigned char dat; DS1302_CE 0; DS1302_SCLK 0; DS1302_CE 1; DS1302_WriteByte(addr | 0x01); // READ命令 dat DS1302_ReadByte(); DS1302_CE 0; return dat; }注意DS1302內(nèi)部時間寄存器的數(shù)據(jù)是BCD碼。比如秒寄存器讀出0x35代表35秒而不是十進制的53。顯示前要轉(zhuǎn)換成十進制的字符比較省事的寫法是tmp sec_bcd; sec_high tmp 4; // 十位 sec_low tmp 0x0F; // 個位寫時間的時候反過來把十進制數(shù)拆成高4位、低4位再拼成BCD。新手常見錯誤是直接把十進制數(shù)寫進去結(jié)果顯示出來的秒變成16進制格式比如15秒變成21秒。4.2 按鍵掃描與菜單狀態(tài)機用狀態(tài)機管理“設置時間”流程萬年歷除了顯示最常用的操作就是“長按設置鍵進入調(diào)時”“短按切換調(diào)整項”“加減修改數(shù)值”。三個按鍵怎么組織才不亂我用的方案是一個狀態(tài)變量加三個動作state 0 正常顯示 state 1 設置年 state 2 設置月 state 3 設置日 state 4 設置星期 state 5 設置時 state 6 設置分 state 7 設置秒“設置/切換”鍵在state0時按一次進入state1在state1到state7時按一次進入下一項在state7再按一次就退出并寫回DS1302。加減鍵根據(jù)當前state修改對應的臨時變量。這個狀態(tài)機思路最關(guān)鍵的優(yōu)點是菜單項增加或減少時只需要改動狀態(tài)切換函數(shù)里的case主循環(huán)不用大改。按鍵掃描我不用連續(xù)檢測而是用“沿觸發(fā)”的方式記錄上一次的按鍵狀態(tài)本次掃描發(fā)現(xiàn)電平變化且按下才認為是一次有效按下。這樣即使按鍵沒有立即釋放也不會連續(xù)觸發(fā)幾十次。再配合10ms延時消抖基本能做到手感準確。4.3 顯示刷新策略別把主循環(huán)的時間全花在刷新上LCD12864串行模式下刷一幀全屏大概需要10到20毫秒聽起來不長如果主循環(huán)里每秒刷十幾次CPU就一直在干刷屏這件事按鍵掃描和DS1302讀寫都會受影響。更好的做法是標志位驅(qū)動定時器每1毫秒中斷一次在中斷里維護一個節(jié)拍計數(shù)。主循環(huán)里每隔250ms讀一次DS1302只有當秒數(shù)發(fā)生變化時才刷新時間顯示溫度每隔2秒讀一次刷新溫度顯示。也就是說主循環(huán)不是“不斷刷屏”而是“問一下數(shù)據(jù)變了嗎變了才刷”。用一個簡單的代碼框架形容while (1) { Key_Scan(); // 按鍵掃描更新狀態(tài)機 if (flag_250ms) // 250ms定時標志 { flag_250ms 0; Read_Time(); // 讀DS1302 Display_Time(); // 刷新時間區(qū)域 } if (flag_2s) // 2s定時標志 { flag_2s 0; Read_Temp(); // 讀DS18B20 Display_Temp(); // 刷新溫度區(qū)域 } }這樣做還有一個好處調(diào)試時不容易出現(xiàn)“按鍵按一下屏幕閃半天”的鬼畜現(xiàn)象。顯示刷新越克制系統(tǒng)越穩(wěn)定。4.4 DS18B20溫度讀取單總線協(xié)議注意點DS18B20讀取流程大致是復位脈沖→存在脈沖→寫ROM命令0xCC跳過地址匹配→寫功能命令0x44啟動轉(zhuǎn)換→等待轉(zhuǎn)換完成→再次復位→寫0xCC→寫0xBE讀暫存器→連續(xù)讀兩個字節(jié)得到溫度值。最容易踩坑的是等待轉(zhuǎn)換時間。12位分辨率下DS18B20轉(zhuǎn)換時間最長要750ms。如果你剛發(fā)啟動轉(zhuǎn)換命令立刻去讀溫度讀到的可能是上一次的值甚至0xFFFF。實際項目中溫度值沒必要每秒刷新我一般2秒讀一次中間隔的時間足夠完成轉(zhuǎn)換。溫度數(shù)據(jù)處理有一個特殊點DS18B20讀出的16位溫度值是二進制補碼格式正溫度時高字節(jié)是0x00~0x07負溫度時高字節(jié)是0xFFF8之類。顯示前必須先判斷符號位如果最高位為1要取反加一得到絕對值再在前面加負號。否則零下溫度會顯示成6553.x這種離譜值。5. 調(diào)試排障三個典型的“卡住”場景與完整排查鏈路5.1 DS1302讀出來全是FF或全0怎么查這是我遇到最多的問題?,F(xiàn)象是上電后時間顯示--:--:--調(diào)試發(fā)現(xiàn)讀DS1302寄存器返回全是0xFF。排查鏈路如下第一步測電源。用萬用表量DS1302的VCC2是否為5VVCC1紐扣電池是否為3V左右。如果你用可充電方案但寄存器寫錯了電池可能被過充到4V以上也會導致芯片工作異常。第二步測晶振。用示波器探頭點X1引腳應該能看到32.768kHz的正弦波沒有示波器就干脆換一顆晶振試試或者用手觸摸晶振引腳如果時間突然有變化說明接觸不良。第三步檢查IO上拉。IO引腳沒有上拉時讀時序會出現(xiàn)讀不到正確電平的情況表現(xiàn)為讀取穩(wěn)定返回0x00或0xFF。第四步檢查SCLK和CE的時序順序。DS1302要求先CE拉高再發(fā)命令字節(jié)如果CE和命令之間沒有延時芯片可能不響應。我自己的經(jīng)驗是先把讀秒命令循環(huán)執(zhí)行并延時200ms觀察秒數(shù)據(jù)是否會變化。如果秒寄存器能變化說明晶振和讀寫都沒問題如果數(shù)據(jù)能變化只是誤差大那問題多半在晶振頻率或負載電容而不是時序。這樣能快速縮小范圍不用從頭開始猜。5.2 LCD12864黑屏、花屏、亂碼的定位黑屏先查三件事供電、對比度、初始化時序。LCD12864的VL引腳懸空或接地太深看起來就是一片黑塊。正常情況下VL加一個10k電位器調(diào)到屏幕剛好出現(xiàn)淡淡底色的位置再進一步微調(diào)文字清晰度?;ㄆ梁蛠y碼更多是時序問題。如果是串行模式先確認同步頭連續(xù)5個1和RS/RW位是否正確。一個非常隱蔽的坑是某些ST7920屏對CS拉低的時機敏感必須保證在發(fā)第一個同步位之前CS已經(jīng)穩(wěn)定為低。如果CS和SCLK共用一個引腳或者接錯會出現(xiàn)“前幾行正常、后幾行亂碼”的現(xiàn)象。另外一個高頻問題是初始化順序。有人直接發(fā)0x0C開顯示但沒先發(fā)0x30把芯片切到8位/串行模式屏幕會一直處于不工作狀態(tài)。注意ST7920上電默認是8位并行模式串行模式下必須通過時序“騙”它進入串行狀態(tài)這就是為什么要連續(xù)發(fā)0x30多次的原因。少了這一步后面發(fā)任何命令都相當于在給“一個還在并行的芯片”送數(shù)據(jù)。5.3 按鍵按一次卻觸發(fā)幾十次這個問題根源多半不是“按鍵壞了”而是消抖和觸發(fā)邊沿沒有處理好。純延時消抖只能濾掉機械抖動但不解決“按下時電平穩(wěn)定后主循環(huán)還在反復掃描”的問題。正確方案是“檢測下降沿/上升沿”用狀態(tài)位記憶上一次電平。比如if (Key_Now 0 Key_Last 1) // 原來高現(xiàn)在低說明剛按下 { key_event KEY_SET; } Key_Last Key_Now;這樣即使按鍵按住不放只有第一次的“下降沿”會被識別為一次有效動作。如果需要“長按連加”功能再額外增加一個“按下持續(xù)計時”變量超過1秒后每200ms自動加一但那是另一個獨立邏輯不要和單次觸發(fā)混在一起。5.4 走時誤差大誤差從哪來排除了代碼邏輯錯誤后萬年歷走時誤差主要來自DS1302的晶振。負載電容選擇不合適、PCB布線寄生電容過大都會讓32.768kHz頻率偏離累積下來一天慢幾秒到幾十秒。想提升精度最務實的方法是用可調(diào)電容或者預留貼片電容位置調(diào)試時用手靠近晶振觀察誤差變化。更省事的方案是直接用帶溫補的DS3231精度能到幾個ppm但51項目為了控制成本還是以DS1302居多。如果走時不準但差異固定可以在軟件里做系數(shù)補償比如實測一天快3秒每8小時修正減1秒效果也還不錯。6. 從功能機到成品農(nóng)歷、鬧鐘與工程化擴展6.1 農(nóng)歷顯示查表法是最可靠的方案電子萬年歷加上農(nóng)歷顯示檔次立刻上一個臺階。農(nóng)歷計算沒有簡單的公式能推算任意年份業(yè)界最常見的方法是把1900年到2100年的農(nóng)歷每個月大小、是否有閏月這些信息壓縮存儲成一張表。每個年份用4個字節(jié)表示然后程序通過查表和累加天數(shù)把公歷日期轉(zhuǎn)換成農(nóng)歷日期。具體算法細節(jié)比較多核心思路是計算從1900年1月31日農(nóng)歷1900年正月初一到目標日期的總天數(shù)然后遍歷年份表逐年減去當年農(nóng)歷總天數(shù)得到目標日期在農(nóng)歷年的偏移量再根據(jù)月份大小表找出對應的農(nóng)歷月和日同時判斷是否在閏月區(qū)間。寫起來不算難但表格數(shù)據(jù)一定要驗證網(wǎng)上流傳的農(nóng)歷表有個別年份數(shù)據(jù)是錯的我通常拿手機上日歷隨機抽查三年。6.2 鬧鐘功能的兩種實現(xiàn)鬧鐘最簡單的實現(xiàn)是軟件比較設定一個目標時分在定時器中斷或主循環(huán)里每秒檢查當前時間和鬧鐘時間是否相等一旦相等就置位蜂鳴器標志位。另一種做法是利用DS1302內(nèi)部的鬧鐘寄存器到點后INT引腳拉低單片機檢測外部中斷來觸發(fā)鬧鈴。個人建議新手用軟件比較代碼直觀且不受DS1302批次差異影響等主框架穩(wěn)定后再研究中斷方式。蜂鳴器響鈴后要設計一個“停止響鈴”的機制按壓任意鍵停止本次鬧鐘。如果設置了貪睡功能可以再額外加一個10分鐘后重響的邏輯。這個功能很能體現(xiàn)產(chǎn)品思維完成它你會對“用戶體驗”有初步概念。6.3 工程化建議模塊化與可移植性最后說說代碼組織。我在做這類項目時習慣把每一類外設封裝成獨立文件main.c管主邏輯和狀態(tài)機ds1302.c管時鐘讀寫ds18b20.c管溫度采集lcd12864.c管顯示驅(qū)動key.c管按鍵掃描menu.c管菜單流程。接口盡量固定成幾個函數(shù)比如DS1302_ReadTime()、LCD_DisplayString()、DS18B20_GetTemp()。這樣做最大的好處是將來你換一塊屏幕、換一個時鐘芯片只需要替換對應的底層驅(qū)動文件上層邏輯幾乎不用動。另外所有涉及時間的變量建議用結(jié)構(gòu)體封裝typedef struct { unsigned char year; unsigned char month; unsigned char day; unsigned char week; unsigned char hour; unsigned char minute; unsigned char second; } RTC_Time;這樣讀一次DS1302就把全部時間信息塞進一個結(jié)構(gòu)體顯示、比較、鬧鐘判斷都拿這個結(jié)構(gòu)體操作比零散的全局變量清晰太多。6.4 后續(xù)還能怎么擴展萬年歷這個項目的擴展上限其實不低。你可以加DHT11做濕度顯示加光敏電阻做屏幕背光自動調(diào)節(jié)加紅外接收頭用遙控器設置鬧鐘也可以通過藍牙串口模塊把時間同步到手機。如果芯片升級到STC15系列主頻跑到24MHz以上LCD12864甚至可以做簡單動畫菜單配合25系列Flash存字庫整個產(chǎn)品的完成度會更高。從我實際做項目的經(jīng)驗來看很多人不是卡在代碼不會寫而是卡在“不知道下一步該做什么”。電子萬年歷最好的地方在于它是一個閉環(huán)系統(tǒng)從需求到硬件、從驅(qū)動到應用、從調(diào)試到優(yōu)化每一步你都要自己走一遍。走完這一遍你對單片機的理解就不再是“看過教程”而是真的上手了。本文還有配套的精品資源點擊獲取