動(dòng)DS1302實(shí)時(shí)時(shí)鐘:從時(shí)序模擬到定時(shí)開關(guān)實(shí)戰(zhàn))
1. 為什么還要折騰 DS1302 這顆老芯片前陣子幫朋友做一個(gè)帶定時(shí)開關(guān)的控制器主控用的是 STM32F103C8T6 最小系統(tǒng)板需求很簡(jiǎn)單每天固定時(shí)間點(diǎn)觸發(fā)繼電器斷電重啟后時(shí)間不能丟。第一反應(yīng)肯定是上 DS3231溫補(bǔ)晶振、I2C 接口、精度高但翻了下手頭的元件盒DS3231 模塊用完了倒是翻出來(lái)幾顆當(dāng)年買錯(cuò)的 DS1302 直插芯片和配套的 32.768kHz 晶振。想著反正精度要求不高一天誤差幾秒也能接受就干脆用 DS1302 把這事做了。DS1302 這顆芯片玩過 51 單片機(jī)的人基本都認(rèn)識(shí)屬于那種入門必點(diǎn)的外設(shè)。它用的是三線串行接口嚴(yán)格說不是標(biāo)準(zhǔn) SPI也不是 I2C而是自己的一套時(shí)序協(xié)議一根 CE有的資料叫 RST、一根 SCLK、一根雙向 IO。正因?yàn)闀r(shí)序是自己定義的所以移植到 STM32 上不能直接套用硬件 SPI 或 I2C 外設(shè)得老老實(shí)實(shí)用 GPIO 模擬時(shí)序。這也是很多人第一次在 STM32 上驅(qū)動(dòng) DS1302 時(shí)踩坑的地方——習(xí)慣了 HAL 庫(kù)一把梭突然要自己數(shù)微秒節(jié)奏就亂了。這篇筆記就是把我從接線、寫時(shí)序、調(diào)讀寫、處理 BCD 碼到最終跑通定時(shí)功能的完整過程整理出來(lái)。內(nèi)容偏向?qū)嵅俅a基于 STM32 HAL 庫(kù)但時(shí)序邏輯是通用的換成標(biāo)準(zhǔn)庫(kù)或者別的 Cortex-M 芯片改改 GPIO 操作就能用。適合兩類人看一類是剛接觸 STM32、想找個(gè)簡(jiǎn)單外設(shè)練手時(shí)序操作的新手另一類是手頭正好有 DS1302 庫(kù)存、想快速把它用起來(lái)的老玩家。我會(huì)把每個(gè)關(guān)鍵決策背后的原因講清楚比如為什么上拉電阻不能省、為什么寫保護(hù)要先關(guān)、為什么讀出來(lái)的秒要屏蔽最高位這些細(xì)節(jié)文檔里往往一筆帶過但實(shí)際調(diào)試時(shí)全是坑。2. DS1302 到底是個(gè)什么東西先把原理捋順2.1 三線接口的本質(zhì)不是 SPI 也不是 I2C很多人看到 DS1302 三根線第一反應(yīng)是這不就是 SPI 嗎。其實(shí)差得遠(yuǎn)。標(biāo)準(zhǔn) SPI 有明確的 CPOL/CPHA 四種模式主機(jī)在時(shí)鐘邊沿采樣從機(jī)在另一個(gè)邊沿輸出而且 SPI 是主機(jī)驅(qū)動(dòng)時(shí)鐘、數(shù)據(jù)全雙工。DS1302 的協(xié)議更原始它把一次通信拆成命令字節(jié) 數(shù)據(jù)字節(jié)兩個(gè)階段命令字節(jié)固定從 bit0 開始發(fā)數(shù)據(jù)字節(jié)則是 bit0 先發(fā)還是 bit7 先發(fā)取決于讀還是寫。具體來(lái)說DS1302 的通信規(guī)則是這樣的CE 拉高之后SCLK 上升沿把數(shù)據(jù)寫進(jìn)芯片下降沿把數(shù)據(jù)從芯片讀出來(lái)。注意這里有個(gè)容易搞混的點(diǎn)——寫的時(shí)候是上升沿采樣讀的時(shí)候是下降沿輸出。所以你在寫時(shí)序函數(shù)的時(shí)候如果是寫操作先準(zhǔn)備好 IO 電平再拉高 SCLK如果是讀操作先拉低 SCLK再去讀 IO 電平。這個(gè)順序反了讀出來(lái)的數(shù)據(jù)就是亂的。命令字節(jié)的格式也值得說一下。bit7 固定是 1表示使能訪問bit6 是 RAM 還是 CLOCK 的選擇0 表示時(shí)鐘寄存器1 表示 RAMbit1 到 bit5 是寄存器地址bit0 是讀寫位0 寫 1 讀。舉個(gè)例子要寫秒寄存器地址 0x80命令字節(jié)就是 0x80要讀秒寄存器命令字節(jié)就是 0x81。這個(gè)規(guī)律記住之后后面所有寄存器的操作都能推出來(lái)。2.2 寄存器地圖哪些能用哪些別碰DS1302 的時(shí)鐘寄存器一共 8 個(gè)地址從 0x80 到 0x8E全是偶地址寫讀的時(shí)候地址加 1。我整理了一張表把常用寄存器的位定義列出來(lái)方便對(duì)照寄存器寫地址讀地址bit7bit6bit5bit4bit3bit2bit1bit0秒0x800x81CH10秒秒個(gè)位分0x820x83010分分個(gè)位時(shí)0x840x85012/2410時(shí)/PM時(shí)個(gè)位日0x860x8700010日日個(gè)位月0x880x8900010月月個(gè)位周0x8A0x8B00000周年0x8C0x8D10年年個(gè)位寫保護(hù)0x8E0x8FWP0000000這里有幾個(gè)關(guān)鍵位必須注意。秒寄存器的 bit7 是 CH 位Clock Halt上電默認(rèn)是 1意思是時(shí)鐘停止。你如果只寫了時(shí)間沒清這個(gè)位會(huì)發(fā)現(xiàn)時(shí)間根本不走讀出來(lái)永遠(yuǎn)是那個(gè)值。我第一次調(diào)的時(shí)候就栽在這盯著串口打印的固定時(shí)間看了半天以為是晶振沒起振換了兩個(gè)晶振才發(fā)現(xiàn)是 CH 位沒清。寫保護(hù)寄存器 0x8E 的 bit7 是 WP 位上電默認(rèn)是 1表示寫保護(hù)開啟這時(shí)候你往任何時(shí)鐘寄存器寫數(shù)據(jù)都會(huì)被忽略。所以標(biāo)準(zhǔn)流程是先寫 0x8E 寫 0x00 關(guān)保護(hù)改完時(shí)間再寫 0x8E 寫 0x80 開保護(hù)。這個(gè)順序不能省省了就會(huì)出現(xiàn)寫進(jìn)去讀出來(lái)還是舊值的詭異現(xiàn)象。2.3 備用電池和晶振硬件上最容易翻車的兩個(gè)點(diǎn)DS1302 有個(gè) VCC1 和 VCC2 雙電源引腳VCC1 接備用電池VCC2 接主電源。芯片內(nèi)部有個(gè)電源檢測(cè)電路主電源掉電時(shí)自動(dòng)切到電池保證時(shí)鐘繼續(xù)走。這里有個(gè)細(xì)節(jié)電池電壓要低于主電源電壓否則芯片可能判斷不出主電源掉電一直用電池供電反而加速電池消耗。一般用 3V 的 CR2032 紐扣電池主電源 3.3V壓差剛好合適。晶振這塊DS1302 要求外接 32.768kHz 晶振而且對(duì)負(fù)載電容有要求。市面上常見的圓柱形晶振負(fù)載電容一般是 6pF 或 12.5pF你得配對(duì)應(yīng)的電容。我手頭那批晶振是 6pF 的一開始隨手焊了兩個(gè) 22pF 的電容上去結(jié)果時(shí)間走得明顯偏慢一天能差十幾秒。后來(lái)查了晶振規(guī)格書換成兩個(gè) 6pF 的電容誤差就降到一天兩三秒了。如果你對(duì)精度有要求建議直接上 DS3231省得跟電容較勁。另外DS1302 的 IO 引腳是開漏輸出必須接上拉電阻一般 4.7k 到 10k 都行。我習(xí)慣用 4.7k上升沿更陡一點(diǎn)時(shí)序更穩(wěn)。有些模塊板子上已經(jīng)集成了上拉電阻如果你是自己畫板子千萬(wàn)別忘了這三個(gè)電阻否則讀出來(lái)的數(shù)據(jù)全是 0xFF。3. 硬件接線與工程搭建3.1 接線方案三根信號(hào)線怎么分配我用的 STM32F103C8T6 最小系統(tǒng)板DS1302 模塊是那種帶電池座的小板子。接線如下DS1302 引腳STM32 引腳說明VCC23.3V主電源VCC1CR2032 正極備用電池GNDGND共地CEPA4片選/復(fù)位SCLKPA5串行時(shí)鐘IOPA6雙向數(shù)據(jù)X1/X232.768kHz 晶振晶振引腳這里 CE、SCLK、IO 我選了 PA4、PA5、PA6其實(shí)隨便選三個(gè)普通 GPIO 都行只要不是那種有特殊功能的引腳比如 JTAG 占用的 PA13/PA14/PA15。選 PA4 到 PA6 純粹是因?yàn)樗鼈儼ぶ泳€方便而且不跟串口沖突調(diào)試的時(shí)候能用串口打印時(shí)間。IO 引腳要注意配置成開漏輸出加外部上拉或者配置成推挽輸出但在讀的時(shí)候切換成輸入模式。我兩種都試過開漏加外部上拉更省事不用來(lái)回切模式代碼也簡(jiǎn)潔。如果你板子上沒有外部上拉那就得在代碼里動(dòng)態(tài)切換輸入輸出方向稍微麻煩一點(diǎn)。3.2 工程配置CubeMX 里怎么點(diǎn)用 CubeMX 配置的話PA4、PA5 配成 GPIO_OutputPA6 配成 GPIO_Output 開漏模式Output Open Drain上拉使能。時(shí)鐘樹按默認(rèn)的 72MHz 配就行DS1302 的時(shí)序?qū)χ黝l不敏感因?yàn)槲覀兪强寇浖訒r(shí)控制節(jié)奏的。延時(shí)函數(shù)這塊我直接用 HAL 庫(kù)的HAL_Delay()做毫秒級(jí)延時(shí)微秒級(jí)延時(shí)用__NOP()循環(huán)湊。DS1302 的時(shí)序要求其實(shí)很寬松SCLK 最高頻率 2MHz也就是周期 500ns我們只要保證每個(gè)電平保持個(gè)一兩微秒就絕對(duì)安全。所以微秒延時(shí)不用太精確用循環(huán)空轉(zhuǎn)就行。void ds1302_delay_us(uint16_t us) { while (us--) { __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); } }這個(gè)延時(shí)函數(shù)在 72MHz 下大概每個(gè)循環(huán) 1 微秒左右不用精確到納秒因?yàn)?DS1302 對(duì)時(shí)序的容忍度很高。我實(shí)測(cè)過就算延時(shí)翻倍或者減半讀寫照樣正常。3.3 引腳宏定義方便移植的寫法為了讓代碼好移植我把引腳操作都封裝成宏#define DS1302_CE_PORT GPIOA #define DS1302_CE_PIN GPIO_PIN_4 #define DS1302_SCLK_PORT GPIOA #define DS1302_SCLK_PIN GPIO_PIN_5 #define DS1302_IO_PORT GPIOA #define DS1302_IO_PIN GPIO_PIN_6 #define DS1302_CE_HIGH() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET) #define DS1302_CE_LOW() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET) #define DS1302_SCLK_HIGH() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET) #define DS1302_SCLK_LOW() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET) #define DS1302_IO_HIGH() HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_SET) #define DS1302_IO_LOW() HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET) #define DS1302_IO_READ() HAL_GPIO_ReadPin(DS1302_IO_PORT, DS1302_IO_PIN)這樣寫的好處是換芯片或者換引腳的時(shí)候只改這幾個(gè)宏就行底層時(shí)序函數(shù)一行都不用動(dòng)。我后來(lái)把這個(gè)驅(qū)動(dòng)移植到另一塊 GD32 的板子上就花了五分鐘改宏定義。4. 時(shí)序代碼逐行拆解4.1 寫一個(gè)字節(jié)從 bit0 開始上升沿有效DS1302 寫字節(jié)的規(guī)則是從 bit0 開始每次把數(shù)據(jù)放到 IO 上然后拉高 SCLK再拉低 SCLK循環(huán) 8 次。注意命令字節(jié)和數(shù)據(jù)字節(jié)都是 bit0 先發(fā)這點(diǎn)跟 SPI 的 MSB first 不一樣別搞混了。void ds1302_write_byte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); ds1302_delay_us(1); DS1302_SCLK_HIGH(); ds1302_delay_us(1); DS1302_SCLK_LOW(); ds1302_delay_us(1); dat 1; } }這段代碼里dat 0x01取最低位然后右移保證下一次取的是新的最低位。延時(shí)放在拉高 SCLK 之前和之后是為了滿足建立時(shí)間和保持時(shí)間。DS1302 的建立時(shí)間要求是 50ns保持時(shí)間也是 50ns我們延時(shí) 1 微秒余量足夠大。4.2 讀一個(gè)字節(jié)下降沿之后采樣讀字節(jié)的規(guī)則稍微繞一點(diǎn)DS1302 在 SCLK 下降沿把數(shù)據(jù)輸出到 IO 上所以我們要先拉低 SCLK等一會(huì)兒再去讀 IO 電平。注意讀的時(shí)候也是從 bit0 開始讀到的第一位要放到變量的 bit0 位置。uint8_t ds1302_read_byte(void) { uint8_t i, dat 0; for (i 0; i 8; i) { dat 1; if (DS1302_IO_READ()) dat | 0x80; DS1302_SCLK_HIGH(); ds1302_delay_us(1); DS1302_SCLK_LOW(); ds1302_delay_us(1); } return dat; }這里有個(gè)細(xì)節(jié)dat 1放在讀之前然后如果讀到 1 就dat | 0x80。這樣第一次讀到的位最終會(huì)落在 bit0第二次落在 bit1以此類推。如果你寫成先讀再移位順序就反了讀出來(lái)的數(shù)據(jù)是 bit 倒序的。我第一次寫的時(shí)候就是先讀后移結(jié)果讀出來(lái)的秒是 0x80 而不是 0x01查了半天才發(fā)現(xiàn)是移位順序錯(cuò)了。4.3 寫寄存器命令字節(jié)加數(shù)據(jù)字節(jié)寫寄存器的流程是CE 拉高發(fā)命令字節(jié)發(fā)數(shù)據(jù)字節(jié)CE 拉低。CE 拉低之后一次通信結(jié)束。注意 CE 拉高之后要先延時(shí)一小會(huì)兒再發(fā)第一個(gè)字節(jié)給芯片一點(diǎn)準(zhǔn)備時(shí)間。void ds1302_write_reg(uint8_t cmd, uint8_t dat) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); ds1302_delay_us(2); ds1302_write_byte(cmd); ds1302_write_byte(dat); DS1302_CE_LOW(); ds1302_delay_us(2); }CE 拉低的時(shí)候順便把 SCLK 也拉低這是個(gè)好習(xí)慣保證下次通信從已知狀態(tài)開始。有些資料說 CE 拉低之后 SCLK 要保持低電平其實(shí)拉低 CE 之后芯片內(nèi)部會(huì)復(fù)位時(shí)序SCLK 什么狀態(tài)無(wú)所謂但統(tǒng)一拉低更規(guī)范。4.4 讀寄存器注意命令字節(jié)的 bit0 要置 1讀寄存器的命令字節(jié)是寫命令字節(jié)加 1也就是 bit0 從 0 變 1。比如讀秒寄存器寫命令是 0x80讀命令就是 0x81。讀的時(shí)候發(fā)完命令字節(jié)之后IO 要切換成輸入模式如果用的是開漏加外部上拉就不用切直接讀就行。uint8_t ds1302_read_reg(uint8_t cmd) { uint8_t dat; DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); ds1302_delay_us(2); ds1302_write_byte(cmd); dat ds1302_read_byte(); DS1302_CE_LOW(); ds1302_delay_us(2); return dat; }如果你用的是推挽輸出模式讀之前要把 IO 引腳切成輸入模式讀完之后再切回輸出。我一開始用推挽讀出來(lái)的數(shù)據(jù)全是 0xFF就是因?yàn)?IO 還在輸出模式芯片輸出的數(shù)據(jù)和 MCU 輸出的電平打架了。后來(lái)改成開漏加外部上拉這個(gè)問題就沒了。5. BCD 碼轉(zhuǎn)換與時(shí)間讀寫5.1 BCD 碼為什么 0x23 表示 23DS1302 內(nèi)部所有時(shí)間寄存器都是 BCD 碼格式也就是用 4 個(gè)二進(jìn)制位表示一個(gè)十進(jìn)制數(shù)字。比如秒寄存器讀到 0x23不是十進(jìn)制的 35而是表示 23 秒。高 4 位是十位低 4 位是個(gè)位。這種格式在早期 RTC 芯片里很常見因?yàn)橛?BCD 碼做十進(jìn)制顯示的時(shí)候不用做除法。轉(zhuǎn)換公式很簡(jiǎn)單uint8_t bcd_to_dec(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } uint8_t dec_to_bcd(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); }比如 0x23 轉(zhuǎn)十進(jìn)制高 4 位是 2乘以 10 得 20低 4 位是 3加起來(lái) 23。反過來(lái) 23 轉(zhuǎn) BCD23 除以 10 得 2左移 4 位變成 0x2023 模 10 得 3或上去就是 0x23。5.2 設(shè)置時(shí)間先關(guān)寫保護(hù)再清 CH 位設(shè)置時(shí)間的完整流程是這樣的先關(guān)寫保護(hù)然后依次寫秒、分、時(shí)、日、月、周、年最后開寫保護(hù)。注意寫秒的時(shí)候要把 CH 位清零否則時(shí)鐘不走。void ds1302_set_time(uint8_t year, uint8_t month, uint8_t day, uint8_t week, uint8_t hour, uint8_t min, uint8_t sec) { ds1302_write_reg(0x8E, 0x00); // 關(guān)寫保護(hù) ds1302_write_reg(0x80, dec_to_bcd(sec) 0x7F); // 秒清CH位 ds1302_write_reg(0x82, dec_to_bcd(min)); // 分 ds1302_write_reg(0x84, dec_to_bcd(hour)); // 時(shí)24小時(shí)制 ds1302_write_reg(0x86, dec_to_bcd(day)); // 日 ds1302_write_reg(0x88, dec_to_bcd(month)); // 月 ds1302_write_reg(0x8A, dec_to_bcd(week)); // 周 ds1302_write_reg(0x8C, dec_to_bcd(year)); // 年 ds1302_write_reg(0x8E, 0x80); // 開寫保護(hù) }這里dec_to_bcd(sec) 0x7F是為了確保 CH 位是 0。如果你寫的時(shí)間秒數(shù)是奇數(shù)比如 23 秒dec_to_bcd(23)得到 0x23最高位本來(lái)就是 0與 0x7F 之后還是 0x23。但如果之前 CH 位是 1這個(gè)與操作就能把它清掉。5.3 讀取時(shí)間秒寄存器要屏蔽 CH 位讀時(shí)間的時(shí)候秒寄存器的最高位是 CH 位不是時(shí)間數(shù)據(jù)所以要屏蔽掉。其他寄存器直接轉(zhuǎn) BCD 就行。void ds1302_get_time(uint8_t *year, uint8_t *month, uint8_t *day, uint8_t *week, uint8_t *hour, uint8_t *min, uint8_t *sec) { *sec bcd_to_dec(ds1302_read_reg(0x81) 0x7F); *min bcd_to_dec(ds1302_read_reg(0x83)); *hour bcd_to_dec(ds1302_read_reg(0x85) 0x3F); *day bcd_to_dec(ds1302_read_reg(0x87) 0x3F); *month bcd_to_dec(ds1302_read_reg(0x89) 0x1F); *week bcd_to_dec(ds1302_read_reg(0x8B) 0x07); *year bcd_to_dec(ds1302_read_reg(0x8D)); }小時(shí)寄存器我屏蔽了 bit6 和 bit7因?yàn)?bit7 是 12/24 小時(shí)制選擇位bit6 在 12 小時(shí)制下是 AM/PM 標(biāo)志。我們統(tǒng)一用 24 小時(shí)制所以把高兩位都屏蔽掉。日寄存器屏蔽 bit6 和 bit7月寄存器屏蔽 bit5 到 bit7周寄存器屏蔽 bit3 到 bit7這些都是保留位讀出來(lái)可能是隨機(jī)值屏蔽掉更安全。6. 調(diào)試過程中踩過的坑和排查方法6.1 讀出來(lái)全是 0xFF 或者 0x00這是最常見的問題基本可以鎖定三個(gè)原因。第一上拉電阻沒接或者阻值太大IO 線在空閑時(shí)處于浮空狀態(tài)讀出來(lái)就是隨機(jī)的。用萬(wàn)用表量一下 IO 線對(duì) VCC 的電阻正常應(yīng)該是 4.7k 左右。第二CE 引腳沒有正確拉高芯片根本沒被選中。用示波器看 CE 波形發(fā)命令的時(shí)候應(yīng)該是高電平。第三晶振沒起振芯片內(nèi)部時(shí)鐘停了這時(shí)候讀寄存器可能返回默認(rèn)值。拿示波器探頭碰一下晶振引腳能看到 32.768kHz 的正弦波就說明起振了。我遇到過一次讀出來(lái)全是 0xFF查了半天發(fā)現(xiàn)是 IO 引腳配置成了推挽輸出讀的時(shí)候沒切成輸入MCU 和 DS1302 在 IO 線上打架。改成開漏加外部上拉之后問題消失。6.2 時(shí)間不走讀出來(lái)永遠(yuǎn)是設(shè)定值這個(gè)基本就是 CH 位沒清。DS1302 上電默認(rèn) CH1時(shí)鐘停止。你寫時(shí)間的時(shí)候如果只寫了秒數(shù)據(jù)沒清 CH 位芯片就一直在暫停狀態(tài)。解決辦法就是在寫秒寄存器的時(shí)候確保最高位是 0。我習(xí)慣用 0x7F強(qiáng)制清零這樣不管之前 CH 是什么狀態(tài)寫完之后一定是走的。還有一種可能是寫保護(hù)沒關(guān)。DS1302 上電默認(rèn) WP1所有寫操作都被忽略。你寫進(jìn)去的數(shù)據(jù)實(shí)際上沒生效讀出來(lái)還是舊值。標(biāo)準(zhǔn)流程是先寫 0x8E 寫 0x00再寫時(shí)間最后寫 0x8E 寫 0x80。這個(gè)順序不能亂。6.3 時(shí)間走得快或者走得慢如果一天誤差在幾秒以內(nèi)那是晶振精度問題正常。DS1302 配普通晶振一天差個(gè)三五秒很常見。如果一天差幾十秒甚至幾分鐘那就要檢查負(fù)載電容了。晶振規(guī)格書上的負(fù)載電容值要跟實(shí)際焊接的電容匹配6pF 的晶振配 6pF 的電容12.5pF 的晶振配 12.5pF 的電容。電容大了走得慢電容小了走得快。另外晶振的走線要盡量短遠(yuǎn)離高頻信號(hào)線。我有一次把晶振線布在了 SPI 時(shí)鐘線旁邊時(shí)間走得亂七八糟后來(lái)重新布線就好了。如果對(duì)精度要求高建議直接換 DS3231內(nèi)置溫補(bǔ)晶振一年誤差也就幾分鐘。6.4 常見問題速查表現(xiàn)象可能原因排查方法解決辦法讀出全 0xFFIO 無(wú)上拉量 IO 對(duì) VCC 電阻加 4.7k 上拉讀出全 0x00CE 未拉高示波器看 CE 波形檢查 CE 宏定義時(shí)間不走CH 位為 1讀秒寄存器看 bit7寫秒時(shí) 0x7F寫入無(wú)效寫保護(hù)未關(guān)讀 0x8E 看 bit7先寫 0x00 關(guān)保護(hù)時(shí)間偏差大負(fù)載電容不匹配查晶振規(guī)格書換匹配電容數(shù)據(jù)亂跳時(shí)序太快加大延時(shí)延時(shí)加到 2us偶爾讀錯(cuò)電源不穩(wěn)量 VCC 紋波加 0.1uF 去耦7. 從驅(qū)動(dòng)到應(yīng)用定時(shí)開關(guān)的完整實(shí)現(xiàn)7.1 每秒讀一次時(shí)間比較觸發(fā)條件驅(qū)動(dòng)調(diào)通之后應(yīng)用邏輯就簡(jiǎn)單了。我在主循環(huán)里每 500ms 讀一次時(shí)間然后跟設(shè)定的觸發(fā)時(shí)間比較。如果時(shí)和分都匹配就觸發(fā)繼電器同時(shí)記錄一個(gè)標(biāo)志位防止同一分鐘內(nèi)重復(fù)觸發(fā)。uint8_t last_min 0xFF; while (1) { uint8_t year, month, day, week, hour, min, sec; ds1302_get_time(year, month, day, week, hour, min, sec); if (min ! last_min) { last_min min; if (hour target_hour min target_min) { relay_on(); HAL_Delay(1000); relay_off(); } } HAL_Delay(500); }這里用last_min做去重保證一分鐘只觸發(fā)一次。如果你需要觸發(fā)多個(gè)時(shí)間點(diǎn)可以搞個(gè)數(shù)組遍歷比較。7.2 掉電恢復(fù)電池供電下的時(shí)間保持DS1302 接上 CR2032 之后主電源掉電時(shí)芯片自動(dòng)切到電池時(shí)鐘繼續(xù)走。我實(shí)測(cè)過拔掉主電源放了一晚上第二天上電讀出來(lái)的時(shí)間還是準(zhǔn)的誤差在正常范圍內(nèi)。注意電池正極接 VCC1負(fù)極接 GND別接反了。CR2032 的壽命大概兩三年如果發(fā)現(xiàn)掉電后時(shí)間丟失先量一下電池電壓低于 2.5V 就該換了。7.3 精度優(yōu)化軟件補(bǔ)償?shù)乃悸啡绻悴幌霌Q DS3231又想讓 DS1302 準(zhǔn)一點(diǎn)可以在軟件里做補(bǔ)償。具體做法是先讓 DS1302 跑一天記錄誤差比如一天快 5 秒那就在每天固定時(shí)間點(diǎn)把秒數(shù)減 5。這個(gè)補(bǔ)償邏輯可以放在主循環(huán)里每天執(zhí)行一次。不過這種方法比較粗糙適合對(duì)精度要求不高的場(chǎng)景。真要高精度還是建議上溫補(bǔ)晶振的方案。8. 代碼組織與移植建議8.1 文件劃分驅(qū)動(dòng)層和應(yīng)用層分開我習(xí)慣把 DS1302 的驅(qū)動(dòng)單獨(dú)放在ds1302.c和ds1302.h里只暴露ds1302_init()、ds1302_set_time()、ds1302_get_time()三個(gè)接口。應(yīng)用層的定時(shí)邏輯放在main.c里這樣換項(xiàng)目的時(shí)候驅(qū)動(dòng)直接復(fù)制過去就能用。頭文件里只放函數(shù)聲明和必要的宏具體的引腳定義放在源文件里或者用條件編譯區(qū)分不同板子。我一般會(huì)在頭文件里留一個(gè)DS1302_USE_OPEN_DRAIN的宏用來(lái)切換開漏和推挽模式方便適配不同硬件。8.2 移植到其他平臺(tái)改三個(gè)地方就行這套代碼移植到別的平臺(tái)比如 GD32、ESP32 或者 51 單片機(jī)只需要改三個(gè)地方GPIO 操作宏、微秒延時(shí)函數(shù)、初始化函數(shù)里的 GPIO 配置。時(shí)序邏輯本身是純軟件模擬的跟硬件平臺(tái)無(wú)關(guān)。我把它移植到 GD32F103 上的時(shí)候就改了宏定義和延時(shí)函數(shù)編譯下載直接就跑起來(lái)了。如果你用的是 RTOS注意 DS1302 的讀寫操作不要被打斷否則時(shí)序會(huì)亂??梢栽谧x寫前后關(guān)中斷或者用互斥鎖保護(hù)。我一般是在ds1302_write_reg和ds1302_read_reg里關(guān)中斷操作完再開這樣最省事。9. 一些零散但有用的經(jīng)驗(yàn)DS1302 的 IO 引腳在 CE 拉低之后會(huì)變成高阻態(tài)這時(shí)候如果外部上拉電阻太大IO 線上的電平可能會(huì)慢慢漂移。我試過用 10k 上拉發(fā)現(xiàn)讀數(shù)據(jù)偶爾會(huì)出錯(cuò)換成 4.7k 之后就穩(wěn)了。所以上拉電阻別省4.7k 是個(gè)比較穩(wěn)妥的值。寫命令字節(jié)的時(shí)候bit7 必須是 1這個(gè)位叫使能位。如果你不小心寫成了 0芯片會(huì)忽略整個(gè)命令。我一開始看資料沒注意這個(gè)細(xì)節(jié)命令字節(jié)直接寫寄存器地址結(jié)果怎么都不對(duì)。后來(lái)才發(fā)現(xiàn)地址要或上 0x80 才是完整的命令字節(jié)。DS1302 的 RAM 寄存器地址 0xC0 到 0xFD可以當(dāng)普通存儲(chǔ)器用掉電不丟數(shù)據(jù)有電池的情況下。我有時(shí)候會(huì)拿它存一些配置參數(shù)比如觸發(fā)時(shí)間、設(shè)備編號(hào)之類的省得外掛 EEPROM。不過 RAM 容量只有 31 字節(jié)存不了太多東西。最后說一個(gè)調(diào)試技巧如果你手頭沒有示波器可以用 LED 來(lái)粗略判斷時(shí)序。在ds1302_write_byte里每發(fā)一位就翻轉(zhuǎn)一次 LED如果 LED 閃爍頻率均勻說明時(shí)序在跑如果 LED 常亮或者常滅說明程序卡在某個(gè)循環(huán)里了。這個(gè)方法雖然土但應(yīng)急的時(shí)候挺好用。這個(gè) DS1302 的驅(qū)動(dòng)我從第一次調(diào)通到現(xiàn)在已經(jīng)在好幾個(gè)小項(xiàng)目里復(fù)用了每次都是復(fù)制粘貼改引腳十分鐘搞定。雖然芯片老但勝在便宜、好買、資料多對(duì)于精度要求不高的場(chǎng)景完全夠用。如果你也在用這顆芯片希望這篇筆記能幫你少走點(diǎn)彎路。