動(dòng)DS1302實(shí)時(shí)時(shí)鐘:GPIO模擬時(shí)序與避坑指南)
從買(mǎi)來(lái)那塊 DS1302 模塊到把它在 STM32 上徹底調(diào)通我前后折騰了兩天。網(wǎng)上關(guān)于 DS1302 的代碼確實(shí)不少但很多都是 51 平臺(tái)直接復(fù)制過(guò)來(lái)的要么沒(méi)講時(shí)序要么對(duì) STM32 的 GPIO 模式切換一筆帶過(guò)。這篇學(xué)習(xí)筆記把整個(gè)驅(qū)動(dòng)從零寫(xiě)一遍包括為什么用 GPIO 模擬時(shí)序、命令字節(jié)的坑、BCD 碼換算、寫(xiě)保護(hù)和時(shí)鐘暫停位把你可能遇到的坑提前踩平。適合正在學(xué) STM32、想搞懂串行時(shí)序、或者需要給手頭項(xiàng)目加一個(gè)掉電不丟時(shí)間的實(shí)時(shí)時(shí)鐘的開(kāi)發(fā)者參考。1. 思路先行為什么用 GPIO 模擬 DS1302而不是硬件 SPI1.1 DS1302 是什么這個(gè)項(xiàng)目能學(xué)到什么DS1302 是一顆非常經(jīng)典的實(shí)時(shí)時(shí)鐘芯片內(nèi)部自帶秒、分、時(shí)、日、月、星期、年的寄存器還有 31 字節(jié)的非易失性靜態(tài) RAM。它的典型應(yīng)用就是給嵌入式系統(tǒng)提供“掉電后繼續(xù)走”的時(shí)間基準(zhǔn)主電源 VCC2 正常供電時(shí)芯片工作外部斷電后由 VCC1 上的紐扣電池或大電容維持計(jì)時(shí)。這種機(jī)制對(duì)很多需要記錄事件時(shí)間戳、定時(shí)任務(wù)、電子鐘表、數(shù)據(jù)記錄儀之類(lèi)的項(xiàng)目非常有用。驅(qū)動(dòng) DS1302 對(duì)你理解 STM32 的 GPIO 操作也很有幫助。它用的是三線串行接口CE、SCLK、IO看起來(lái)像 SPI但實(shí)際時(shí)序有自己的一套。手寫(xiě)這套驅(qū)動(dòng)你會(huì)把“時(shí)序圖怎么讀”“命令字節(jié)怎么組織”“雙向 IO 怎么處理”“BCD 碼怎么換算”這些問(wèn)題一次性過(guò)一遍。這些基本功在以后驅(qū)動(dòng)傳感器、存儲(chǔ)器、顯示屏?xí)r都會(huì)反復(fù)用到。從我自己的角度看這個(gè)項(xiàng)目特別適合作為“學(xué)習(xí)筆記”的素材因?yàn)樗倪壿嫴粡?fù)雜但每個(gè)環(huán)節(jié)都能挖出細(xì)節(jié)。只要認(rèn)真走一遍以后再遇到類(lèi)似的串行器件上手速度會(huì)快很多。1.2 GPIO 模擬 vs 硬件 SPI我為什么這樣選DS1302 的三線接口在某些代碼里被歸類(lèi)成“類(lèi) SPI”也有人直接用硬件 SPI 去驅(qū)動(dòng)過(guò)。但從數(shù)據(jù)手冊(cè)的時(shí)序來(lái)看它跟標(biāo)準(zhǔn) SPI 并不是一回事DS1302 需要 CE 拉高作為一次傳輸?shù)拈_(kāi)始數(shù)據(jù)按 LSB 先發(fā)命令字節(jié)帶有固定的高位標(biāo)志位而且 IO 是半雙工雙向口。用硬件 SPI 不是不行但你需要額外處理 IO 方向切換、調(diào)整時(shí)鐘極性和相位還可能被 SPI 外設(shè)的字節(jié)長(zhǎng)度、片選控制等問(wèn)題困擾。折騰半天最后發(fā)現(xiàn)還是 GPIO 模擬更干凈。GPIO 模擬的好處是每個(gè)時(shí)鐘沿、每個(gè)電平變化都在你自己手里。出問(wèn)題時(shí)可以用邏輯分析儀抓波形一步一步對(duì)時(shí)序圖定位非??臁S1302 的時(shí)鐘頻率上限大約是 2MHz實(shí)際應(yīng)用里我們通常給它 1us 級(jí)別的半周期延時(shí)這個(gè)速度對(duì) GPIO 來(lái)說(shuō)毫無(wú)壓力完全不用擔(dān)心模擬方式跑不動(dòng)。這個(gè)選擇也符合我自己的習(xí)慣能用簡(jiǎn)單方案把事情做穩(wěn)就不用復(fù)雜方案。特別是學(xué)習(xí)階段每行代碼都看得懂、每個(gè)狀態(tài)都測(cè)得出來(lái)遠(yuǎn)比“外設(shè)配置對(duì)但說(shuō)不清為什么”有價(jià)值。2. 時(shí)序與寄存器最容易翻車(chē)的三個(gè)關(guān)鍵細(xì)節(jié)2.1 命令字節(jié)、LSB 和讀寫(xiě)時(shí)序的關(guān)鍵點(diǎn)DS1302 的每次操作都是從 CE 拉高開(kāi)始然后主機(jī)通過(guò) IO 發(fā)送一個(gè)命令字節(jié)再要么寫(xiě)一個(gè)數(shù)據(jù)字節(jié)要么讀一個(gè)數(shù)據(jù)字節(jié)最后 CE 拉低。命令字節(jié)的格式是固定的bit7 固定為 1bit6 用于區(qū)分操作對(duì)象0 表示操作時(shí)鐘寄存器1 表示操作內(nèi)部 RAMbit5 到 bit1 是寄存器地址bit0 表示讀或?qū)? 寫(xiě)、1 讀。另一個(gè)容易忽略的地方是數(shù)據(jù)是 LSB first也就是低位在前。很多從 SPI 轉(zhuǎn)過(guò)來(lái)的人習(xí)慣性先發(fā)高位結(jié)果一上來(lái)就讀到完全錯(cuò)亂的數(shù)據(jù)。寫(xiě)一個(gè)字節(jié)時(shí)每次先把當(dāng)前 bit 放到 IO 上然后產(chǎn)生一個(gè) SCLK 上升沿芯片在這個(gè)上升沿采樣數(shù)據(jù)。讀一個(gè)字節(jié)時(shí)芯片會(huì)在 SCLK 下降沿之后把數(shù)據(jù)放到 IO 上所以主機(jī)應(yīng)該在 SCLK 高電平期間讀取 IO或者下降沿后稍作延時(shí)再讀。我實(shí)測(cè)在 SCLK 高電平期間讀是穩(wěn)定的前提是 IO 方向已經(jīng)切對(duì)。還有一個(gè)細(xì)節(jié)很關(guān)鍵讀命令發(fā)送完最后一個(gè) bit 之后IO 要立刻從輸出轉(zhuǎn)為輸入。如果切換慢了芯片開(kāi)始輸出數(shù)據(jù)時(shí)你的引腳還在驅(qū)動(dòng)總線兩邊打架輕則讀到錯(cuò)誤數(shù)據(jù)重則可能損傷 IO。這也是我為什么在底層函數(shù)里把“釋放 IO”放在讀字節(jié)循環(huán)前面。2.2 寄存器地址表與 BCD 碼換算DS1302 的時(shí)鐘寄存器從秒到年寫(xiě)地址和讀地址相差 1規(guī)律非常明顯。我整理了一張常用地址表寄存器寫(xiě)地址讀地址說(shuō)明秒0x800x81bit7 是時(shí)鐘暫停位 CH分0x820x83無(wú)特殊標(biāo)志位時(shí)0x840x85bit7 是 12/24 小時(shí)模式標(biāo)志日0x860x87無(wú)特殊標(biāo)志位月0x880x89無(wú)特殊標(biāo)志位星期0x8A0x8B取值范圍 1 到 7年0x8C0x8D兩位年份如 24 表示 2024寫(xiě)保護(hù)0x8E0x8Fbit7 置 1 禁止寫(xiě)入涓流充電0x900x91用于給備份電容充電時(shí)鐘突發(fā)0xBE0xBF一次連續(xù)讀寫(xiě)全部時(shí)鐘寄存器RAM 突發(fā)0xFE0xFF一次連續(xù)讀寫(xiě) 31 字節(jié) RAM芯片內(nèi)部的時(shí)間數(shù)據(jù)全部用 BCD 碼保存也就是說(shuō)一個(gè)字節(jié)的高四位表示十位低四位表示個(gè)位。比如秒寄存器里存 0x45就代表 45 秒而不是十進(jìn)制的 45 對(duì)應(yīng)的 0x2D。所以讀寫(xiě)時(shí)間時(shí)必須有十進(jìn)制到 BCD、BCD 到十進(jìn)制的轉(zhuǎn)換函數(shù)。這個(gè)換算本身不難但漏掉任何一個(gè)方向都會(huì)讓你調(diào)半天。2.3 寫(xiě)保護(hù)位、時(shí)鐘暫停位和 24 小時(shí)模式的坑寫(xiě)保護(hù)是新手第一個(gè)容易踩的坑。DS1302 上電后寫(xiě)保護(hù)位不一定處于關(guān)閉狀態(tài)如果你不先把寫(xiě)保護(hù)位寄存器0x8E的 bit7 清零那么所有寫(xiě)入操作都會(huì)被忽略時(shí)間怎么都寫(xiě)不進(jìn)去。很多人調(diào)了很久“寫(xiě)不進(jìn)去”最后發(fā)現(xiàn)就是少了這一句。第二個(gè)坑是秒寄存器里的 bit7也就是時(shí)鐘暫停位。當(dāng)這個(gè)位為 1 時(shí)芯片內(nèi)部振蕩器被關(guān)閉秒數(shù)根本不會(huì)走。如果你發(fā)現(xiàn)模塊能讀到時(shí)間但秒數(shù)一直不變十有八九是這個(gè)位沒(méi)清。初始化時(shí)讀秒寄存器把 bit7 清掉再寫(xiě)回去時(shí)鐘才會(huì)真正跑起來(lái)。第三個(gè)坑比較隱蔽是小時(shí)寄存器的掩碼處理。秒、分寄存器讀出來(lái)時(shí)通常用 0x7F 把最高位清掉小時(shí)寄存器卻有人用 0x3F。問(wèn)題在于 24 小時(shí)制下小時(shí)十位的 2 存儲(chǔ)在 bit6如果你用 0x3F 去掩碼會(huì)把 20 點(diǎn)到 23 點(diǎn)的數(shù)據(jù)直接變成 0 點(diǎn)到 3 點(diǎn)。正確做法是設(shè)置時(shí)間時(shí)用 0x7F 清除 12/24 模式的 bit7讀取時(shí)也用 0x7F 保留 bit6。這個(gè)細(xì)節(jié)不踩一次很難意識(shí)到我在日志里看到 20 點(diǎn)讀成 00 點(diǎn)的時(shí)候還納悶了很久。3. 驅(qū)動(dòng)代碼實(shí)現(xiàn)從底層字節(jié)讀寫(xiě)到時(shí)間封裝3.1 引腳定義與 GPIO 初始化開(kāi)漏模式的便利我先說(shuō) GPначала引腳分配。以 STM32 的 GPIOB 為例PB0 接 CEPB1 接 SCLKPB2 接 IO。芯片的 VCC2 接 3.3VGND 接地IO 線上加一個(gè) 4.7k 歐姆上拉電阻到 3.3VVCC1 我接了一個(gè)紐扣電池座。32.768kHz 晶振的兩端按模塊設(shè)計(jì)接好通常模塊上已經(jīng)預(yù)留了電容位置。初始化 GPIO 時(shí)我對(duì)三個(gè)引腳用了不同策略。CE 和 SCLK 是主機(jī)輸出配置成推挽輸出。IO 是雙向口我選擇了開(kāi)漏輸出模式并開(kāi)啟內(nèi)部上拉配合外部 4.7k 上拉電阻。為什么用開(kāi)漏因?yàn)?STM32 的開(kāi)漏輸出模式下輸出數(shù)據(jù)寄存器寫(xiě) 1 時(shí)引腳處于釋放狀態(tài)此時(shí)可以把這個(gè)引腳當(dāng)輸入用直接讀 GPIO 的輸入數(shù)據(jù)寄存器就能拿到外部電平。這樣 IO 方向切換不再需要重新配置 GPIO 模式寫(xiě)數(shù)據(jù)時(shí)寫(xiě) 0 和寫(xiě) 1讀數(shù)據(jù)時(shí)寫(xiě) 1 釋放總線然后讀穩(wěn)定又干凈。如果使用推挽輸出理論上寫(xiě)數(shù)據(jù)更快但讀取前必須把引腳從輸出模式切換到輸入模式切換時(shí)機(jī)不對(duì)很容易產(chǎn)生毛刺。對(duì)比下來(lái)開(kāi)漏加外部上拉更合適尤其適合 DS1302 這種低速半雙工接口。#include main.h #include stdint.h #define DS1302_CE_PORT GPIOB #define DS1302_CE_PIN GPIO_PIN_0 #define DS1302_SCLK_PORT GPIOB #define DS1302_SCLK_PIN GPIO_PIN_1 #define DS1302_IO_PORT GPIOB #define DS1302_IO_PIN GPIO_PIN_2 #define DS1302_CE_LOW() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET) #define DS1302_CE_HIGH() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET) #define DS1302_SCLK_LOW() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET) #define DS1302_SCLK_HIGH() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET) #define DS1302_IO_WRITE(v) HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, (v) ? GPIO_PIN_SET : GPIO_PIN_RESET) #define DS1302_IO_READ() HAL_GPIO_ReadPin(DS1302_IO_PORT, DS1302_IO_PIN) static void ds1302_gpio_init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio.Pin DS1302_CE_PIN | DS1302_SCLK_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, gpio); gpio.Pin DS1302_IO_PIN; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOB, gpio); DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_IO_WRITE(1); }初始化最后那三行不是擺設(shè)。CE 低電平讓芯片處于待機(jī)狀態(tài)SCLK 低電平保證沒(méi)有時(shí)鐘沿觸發(fā)誤動(dòng)作IO 寫(xiě) 1 釋放總線避免復(fù)位后引腳狀態(tài)不確定拖住總線。3.2 底層讀寫(xiě)一個(gè)字節(jié)實(shí)現(xiàn)和原理看代碼前先明確一下 DS1302 的位時(shí)序。寫(xiě)字節(jié)時(shí)主機(jī)把當(dāng)前位放到 IO 上然后 SCLK 拉高形成上升沿芯片采樣再拉低準(zhǔn)備下一位。讀字節(jié)時(shí)芯片在 SCLK 下降沿之后更新輸出所以主機(jī)可以在 SCLK 高電平期間讀取 IO。static void ds1302_delay_us(uint32_t us) { for (uint32_t i 0; i us; i) { for (volatile uint32_t j 0; j 16U; j) { __NOP(); } } } static void ds1302_write_byte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { DS1302_IO_WRITE(dat 0x01); DS1302_SCLK_HIGH(); ds1302_delay_us(1); DS1302_SCLK_LOW(); ds1302_delay_us(1); dat 1; } } static uint8_t ds1302_read_byte(void) { uint8_t i, dat 0; DS1302_IO_WRITE(1); for (i 0; i 8; i) { dat 1; DS1302_SCLK_HIGH(); ds1302_delay_us(1); if (DS1302_IO_READ()) { dat | 0x80; } DS1302_SCLK_LOW(); ds1302_delay_us(1); } return dat; }讀函數(shù)里每次循環(huán)先把 dat 右移一位再根據(jù) IO 電平?jīng)Q定要不要置最高位相當(dāng)于在收集一個(gè)反轉(zhuǎn)的移位寄存器。為什么不在循環(huán)開(kāi)始直接讀 IO 再移位也可以但要保證第 0 位落在 bit0而不是最后變成反序。我習(xí)慣先移位再置位配合 LSB first 的特性最后自然得到正確的字節(jié)。延時(shí) 1us 是故意的。DS1302 的 SCLK 最快大概到 2MHz半個(gè)周期就是 250ns 左右不延時(shí)也能跑。但實(shí)測(cè)中如果 STM32 主頻較高、代碼優(yōu)化開(kāi)得激進(jìn)沒(méi)有延時(shí)的時(shí)鐘沿翻轉(zhuǎn)容易超出芯片規(guī)格導(dǎo)致偶發(fā)讀取錯(cuò)誤。加 1us 延時(shí)后時(shí)序余量很充足又不影響實(shí)際體驗(yàn)屬于“花小錢(qián)買(mǎi)個(gè)穩(wěn)”。3.3 寄存器讀寫(xiě)與時(shí)間結(jié)構(gòu)封裝底層寫(xiě)好之后寄存器讀寫(xiě)就很直白了。注意我的接口約定所有寄存器參數(shù)統(tǒng)一使用偶數(shù)地址也就是手冊(cè)里的“寫(xiě)地址”函數(shù)內(nèi)部把 bit0 改成 0 或 1。void ds1302_write_reg(uint8_t reg, uint8_t val) { reg 0xFE; DS1302_CE_HIGH(); ds1302_write_byte(reg); ds1302_write_byte(val); DS1302_CE_LOW(); } uint8_t ds1302_read_reg(uint8_t reg) { uint8_t val; reg | 0x01; DS1302_CE_HIGH(); ds1302_write_byte(reg); val ds1302_read_byte(); DS1302_CE_LOW(); return val; }每次傳輸 CE 都要經(jīng)歷一次拉低拉高。CE 拉低是固定的結(jié)束標(biāo)志不要省略。如果沒(méi)有在讀完最后一個(gè) bit 后把 CE 拉低芯片不會(huì)正常結(jié)束本次傳輸下一次操作時(shí)序會(huì)亂。這個(gè)坑我在調(diào)試突發(fā)模式時(shí)踩過(guò)后面會(huì)細(xì)說(shuō)。接下來(lái)是 BCD 轉(zhuǎn)換和時(shí)間結(jié)構(gòu)。typedef struct { uint8_t year; uint8_t month; uint8_t day; uint8_t week; uint8_t hour; uint8_t min; uint8_t sec; } ds1302_time_t; static uint8_t ds1302_dec_to_bcd(uint8_t dec) { return (uint8_t)(((dec / 10) 4) | (dec % 10)); } static uint8_t ds1302_bcd_to_dec(uint8_t bcd) { return (uint8_t)((bcd 4) * 10 (bcd 0x0F)); } void ds1302_set_time(const ds1302_time_t *tm) { ds1302_write_reg(0x8E, 0x00); ds1302_write_reg(0x80, ds1302_dec_to_bcd(tm-sec) 0x7F); ds1302_write_reg(0x82, ds1302_dec_to_bcd(tm-min) 0x7F); ds1302_write_reg(0x84, ds1302_dec_to_bcd(tm-hour) 0x7F); ds1302_write_reg(0x86, ds1302_dec_to_bcd(tm-day) 0x3F); ds1302_write_reg(0x88, ds1302_dec_to_bcd(tm-month) 0x1F); ds1302_write_reg(0x8A, ds1302_dec_to_bcd(tm-week) 0x07); ds1302_write_reg(0x8C, ds1302_dec_to_bcd(tm-year)); ds1302_write_reg(0x8E, 0x80); } void ds1302_get_time(ds1302_time_t *tm) { uint8_t b; b ds1302_read_reg(0x80); tm-sec ds1302_bcd_to_dec(b 0x7F); b ds1302_read_reg(0x82); tm-min ds1302_bcd_to_dec(b 0x7F); b ds1302_read_reg(0x84); tm-hour ds1302_bcd_to_dec(b 0x7F); b ds1302_read_reg(0x86); tm-day ds1302_bcd_to_dec(b 0x3F); b ds1302_read_reg(0x88); tm-month ds1302_bcd_to_dec(b 0x1F); b ds1302_read_reg(0x8A); tm-week ds1302_bcd_to_dec(b 0x07); b ds1302_read_reg(0x8C); tm-year ds1302_bcd_to_dec(b); }設(shè)置時(shí)間函數(shù)第一行關(guān)閉寫(xiě)保護(hù)最后一行為可選的重新開(kāi)啟寫(xiě)保護(hù)。如果你打算頻繁校時(shí)可以留著寫(xiě)保護(hù)打開(kāi)防止其他代碼誤操作時(shí)間寄存器。讀時(shí)間函數(shù)里的小時(shí)掩碼用了 0x7F而不是常見(jiàn)的 0x3F這一點(diǎn)我在前文反復(fù)強(qiáng)調(diào)過(guò)20 點(diǎn)到 23 點(diǎn)的十位信息在 bit6用錯(cuò)掩碼會(huì)直接把時(shí)間讀錯(cuò)。3.4 main 函數(shù)接入示例接入工程很簡(jiǎn)單初始化調(diào)用一次 gpio init如果模塊是第一次上電或者需要校時(shí)就調(diào)用一次 set_time之后循環(huán)讀時(shí)間即可。int main(void) { HAL_Init(); SystemClock_Config(); ds1302_gpio_init(); ds1302_time_t t { .year 24, .month 5, .day 18, .week 6, .hour 10, .min 30, .sec 0 }; ds1302_set_time(t); while (1) { ds1302_get_time(t); printf(%02u-%02u-%02u %02u:%02u:%02u week%u\r\n, t.year, t.month, t.day, t.hour, t.min, t.sec, t.week); HAL_Delay(1000); } }注意演示代碼里每次上電都 set_time實(shí)際項(xiàng)目不該這么做否則每次斷電重啟時(shí)間都會(huì)重置。正確做法是只做初始化清除寫(xiě)保護(hù)和時(shí)鐘暫停位讓芯片跟著備用電池繼續(xù)走。如果需要判斷“是否需要校時(shí)”可以存一個(gè)標(biāo)志位到 DS1302 內(nèi)置 RAM或者通過(guò)按鍵、上位機(jī)命令主動(dòng)校時(shí)。4. 調(diào)不通怎么辦常見(jiàn)癥狀排查與避坑實(shí)錄4.1 讀數(shù)全是 0xFF 或 0x00優(yōu)先排查這五點(diǎn)如果你讀出來(lái)的時(shí)間全是 0xFF或者全是 0x00不要急著懷疑芯片壞了。按照下面這個(gè)順序查絕大多數(shù)問(wèn)題都能定位。第一查接線。CE、SCLK、IO 三根線有沒(méi)有接反VCC2 的電壓是否在 2.0V 到 5.5V 之間GND 是不是和 STM32 共地。很多模塊自帶電池座電池極性反了也會(huì)導(dǎo)致怪現(xiàn)象。第二查 IO 上拉。如果用開(kāi)漏模式但沒(méi)有外部上拉只是依賴 STM32 內(nèi)部上拉讀數(shù)據(jù)時(shí)總線可能被拉到一個(gè)不穩(wěn)定的中間態(tài)尤其在 IO 方向切換的瞬間。所以外部 4.7k 上拉強(qiáng)烈建議要加。第三查 SCLK 延時(shí)。代碼里的 1us 延時(shí)是否真的生效。有些工程開(kāi)了 O3 優(yōu)化空循環(huán)可能被編譯器優(yōu)化掉一部分導(dǎo)致時(shí)鐘翻轉(zhuǎn)過(guò)快。第四查 CE 時(shí)序。每次讀寫(xiě)都必須 CE 高電平開(kāi)始、低電平結(jié)束。如果 CE 一直懸空或者沒(méi)有正確拉低芯片的傳輸狀態(tài)會(huì)混亂。第五查命令字節(jié)。最簡(jiǎn)單的方法是買(mǎi)個(gè)十幾塊錢(qián)的邏輯分析儀抓一下 CE、SCLK、IO 三根線對(duì)一下命令字節(jié)是不是 0x80 寫(xiě)、0x81 讀這樣的正確序列。沒(méi)有邏輯分析儀的話可以用 GPIO 翻轉(zhuǎn)法在讀寫(xiě)函數(shù)前后翻轉(zhuǎn)一個(gè)調(diào)試引腳再用示波器看間隔。我這里遇到過(guò)一次比較隱蔽的情況讀出來(lái)全是 0xFF追查發(fā)現(xiàn)是 IO 引腳在初始化時(shí)被配置成了推挽輸出寫(xiě)數(shù)據(jù)時(shí)正常但讀數(shù)據(jù)前切換輸入模式?jīng)]有成功。換成開(kāi)漏模式后問(wèn)題再?zèng)]出現(xiàn)過(guò)。4.2 秒數(shù)不走、分鐘亂跳的處理秒數(shù)完全不動(dòng)大概率是時(shí)鐘暫停位。秒寄存器 bit7 是 CH為 1 時(shí)振蕩器關(guān)閉。初始化時(shí)或者每次 set_time 時(shí)把這個(gè)位清 0秒針就開(kāi)始走了。如果清 0 之后走了幾秒又停住可能是代碼某處又把秒寄存器寫(xiě)成帶 CH 位的數(shù)據(jù)比如沒(méi)有掩碼直接寫(xiě)回了一個(gè)含 0x80 的 BCD 值。分鐘亂跳則常發(fā)生在連續(xù)單字節(jié)讀取時(shí)。因?yàn)槊牒头质莾蓚€(gè)獨(dú)立寄存器讀取過(guò)程中如果恰好跨越秒進(jìn)位邊界你可能讀到“新秒 舊分”的組合看起來(lái)就是分鐘突然變化。避免辦法有兩個(gè)。第一個(gè)是連續(xù)讀兩次秒寄存器如果兩次結(jié)果不一致說(shuō)明跨越了進(jìn)位重新讀一遍完整時(shí)間。第二個(gè)更優(yōu)雅直接使用時(shí)鐘突發(fā)模式一次讀出秒、分、時(shí)、日、月、星期、年 7 個(gè)字節(jié)減少跨寄存器的不一致窗口。突發(fā)讀命令是 0xBFCE 拉高后發(fā)命令然后連續(xù)讀 7 個(gè)字節(jié)最后 CE 拉低。要注意的是CE 保持高電平期間芯片會(huì)持續(xù)輸出數(shù)據(jù)所以必須按需要的長(zhǎng)度讀讀完立刻拉低 CE否則會(huì)進(jìn)入循環(huán)輸出時(shí)序全部亂掉。4.3 掉電不保存和涓流充電的配置掉電后時(shí)間丟失最常見(jiàn)原因是 VCC1 沒(méi)有接備用電源或者紐扣電池沒(méi)電了。DS1302 正常工作時(shí)由 VCC2 供電斷電后切換到 VCC1。注意 VCC1 的電壓不能高于 VCC2 加 0.3V否則內(nèi)部電源切換電路可能出問(wèn)題。所以 VCC1 接 3V 紐扣電池或 2.4V 到 3.6V 的超級(jí)電容是穩(wěn)妥的。如果你想用超級(jí)電容并希望主電源在的時(shí)候自動(dòng)給它充電可以配置涓流充電寄存器 0x90。這個(gè)寄存器的用法比較講究高四位 TCS 必須寫(xiě)成 1010 才會(huì)使能充電中間兩位選擇二極管數(shù)量低兩位選擇充電電阻。一個(gè)常見(jiàn)配置是 0xA5代表一個(gè)二極管加 2k 歐姆電阻也有模塊出廠配置成 0xA6 或 0xA7。如果這個(gè)寄存器的 TCS 位不是 1010涓流充電是關(guān)閉的。我自己踩過(guò)充電相關(guān)的一個(gè)坑一開(kāi)始為了省事直接寫(xiě) 0xA5結(jié)果 0.22F 的超級(jí)電容很快就充滿了但芯片時(shí)間正常。后來(lái)?yè)Q了大容量電容才發(fā)現(xiàn)如果電容太大2k 電阻的充電電流可能偏大需要根據(jù)電容容量調(diào)整電阻檔位。所以沒(méi)什么特別需求時(shí)不要把涓流充電開(kāi)得太激進(jìn)寧可電容大一點(diǎn)、充得慢一點(diǎn)。5. 最后再分享三個(gè)實(shí)用小技巧5.1 讀時(shí)間更穩(wěn)的一個(gè)小技巧突發(fā)讀模式前面提到突發(fā)模式可以減少跨秒進(jìn)位的讀數(shù)不一致問(wèn)題這里直接給一個(gè)簡(jiǎn)單的實(shí)現(xiàn)。時(shí)鐘突發(fā)讀命令是 0xBFCE 高電平期間連續(xù)讀 7 個(gè)字節(jié)最后 CE 拉低。void ds1302_get_time_burst(ds1302_time_t *tm) { uint8_t buf[7]; uint8_t i; DS1302_CE_HIGH(); ds1302_write_byte(0xBF); for (i 0; i 7; i) { buf[i] ds1302_read_byte(); } DS1302_CE_LOW(); tm-sec ds1302_bcd_to_dec(buf[0] 0x7F); tm-min ds1302_bcd_to_dec(buf[1] 0x7F); tm-hour ds1302_bcd_to_dec(buf[2] 0x7F); tm-day ds1302_bcd_to_dec(buf[3] 0x3F); tm-month ds1302_bcd_to_dec(buf[4] 0x1F); tm-week ds1302_bcd_to_dec(buf[5] 0x07); tm-year ds1302_bcd_to_dec(buf[6]); }這段代碼與單字節(jié)讀取的效果差別在讀數(shù)的連續(xù)性。對(duì)精度要求不高的場(chǎng)景用單字節(jié)逐項(xiàng)讀取也夠但如果你做的是數(shù)據(jù)記錄儀這類(lèi)對(duì)時(shí)間一致性有要求的設(shè)備建議直接用突發(fā)讀。5.2 31 字節(jié) RAM 的妙用和擴(kuò)展玩法DS1302 除了時(shí)間寄存器還有一塊 31 字節(jié)的 RAM地址從 0xC0 到 0xFD寫(xiě)地址是偶數(shù)讀地址對(duì)應(yīng)加 1。這塊 RAM 在電池供電時(shí)會(huì)保持?jǐn)?shù)據(jù)非常適合存放一些掉電不能丟的小參數(shù)比如設(shè)備編號(hào)、校時(shí)標(biāo)志、上次關(guān)機(jī)時(shí)間。使用方式就是普通的寄存器讀寫(xiě)只是地址范圍換一下。我自己的擴(kuò)展玩法是做一個(gè)“開(kāi)機(jī)判斷”首次上電時(shí)讀 RAM 里的一個(gè)標(biāo)志字節(jié)如果不是預(yù)設(shè)魔數(shù)就認(rèn)為模塊從未初始化自動(dòng)執(zhí)行一次 set_time并把魔數(shù)寫(xiě)入 RAM。這樣既不會(huì)每次開(kāi)機(jī)都重置時(shí)間也能在換電池后自動(dòng)重新校時(shí)。這個(gè)思路可以移植到很多帶掉電存儲(chǔ)需求的場(chǎng)景里。另外如果你需要輸出更精確的秒信號(hào)可以看 DS1302 的 SQW 引腳功能不過(guò)很多模塊沒(méi)把該引腳引出來(lái)。真要高精度長(zhǎng)穩(wěn)計(jì)時(shí)還是直接選帶溫度補(bǔ)償?shù)膶?shí)時(shí)時(shí)鐘方案DS1302 的優(yōu)勢(shì)是便宜、簡(jiǎn)單、入門(mén)友好。這塊芯片雖然老但它把串行時(shí)序、雙向 IO、寄存器操作這幾個(gè)嵌入式基本功都串了起來(lái)。我第一次調(diào)通的時(shí)候看到串口打印出穩(wěn)定的秒數(shù)心里還是挺有成就感的。如果你也卡在某個(gè)讀數(shù)的怪問(wèn)題上試著用邏輯分析儀抓一下波形很多時(shí)候一眼就能看出問(wèn)題。代碼我已經(jīng)整理到開(kāi)源倉(cāng)庫(kù)里對(duì)照這篇筆記看應(yīng)該能把 DS1302 在 STM32 上順利跑起來(lái)。