動詳解:基于AT24C的I2C實現(xiàn)與掉電保存)
簡介面向TMS320F2812DSP2812嵌入式開發(fā)者的EEPROM操作完整工程源碼重點演示在CCS3.3集成開發(fā)環(huán)境下通過SPI或I2C接口訪問外部EEPROM的實現(xiàn)方式。內(nèi)容覆蓋DSP2812外設(shè)初始化、SPI模式選擇CPOL/CPHA與I2C地址尋址、讀寫擦除流程、寫保護機制、返回狀態(tài)檢測以及數(shù)據(jù)完整性校驗便于使用者理解串行EEPROM驅(qū)動的完整框架。工程壓縮包共49個文件、約449KB包含DSP281x系列頭文件18個、C源文件7個、匯編啟動文件、庫文件、out可執(zhí)行文件、map映射文件和CMD鏈接腳本其中代碼與頭文件為主體能夠支撐工程重建、燒寫與在線調(diào)試。資源在CSDN已有407人學(xué)習(xí)適合剛接觸DSP2812外設(shè)編程的初學(xué)者也適合需要快速移植EEPROM驅(qū)動模塊的工程師參考借助完整外設(shè)支持庫還可同時學(xué)習(xí)時鐘、中斷、GPIO等基礎(chǔ)配置真正把底層外設(shè)與EEPROM應(yīng)用串起來便于二次開發(fā)。1. 從DSP2812裸機讀寫EEPROM說起這份資源到底解決什么問題做DSP2812開發(fā)的人遲早會撞上“掉電丟數(shù)據(jù)”這堵墻。程序里算出來的校準(zhǔn)系數(shù)、用戶設(shè)置的運行參數(shù)、故障歷史記錄一斷電全清零每次上電都得重新標(biāo)定現(xiàn)場調(diào)試簡直磨人。最常見的出路就是外掛一顆EEPROM把要保的數(shù)據(jù)在掉電前寫進去。理論上這事兒不難但真到自己畫板子、寫驅(qū)動時坑一個接一個I2C時序不對讀回全0xFF、頁寫入跨邊界丟字節(jié)、寫周期沒等夠數(shù)據(jù)直接沒落盤。這份基于DSP2812的EEPROM操作程序就是把“初始化外部存儲、按地址寫入、按地址讀出、整頁刷寫”這一整套流程打包好的參考實現(xiàn)拿來改改引腳和器件地址就能用。適合手里有DSP2812開發(fā)板、正在做參數(shù)存儲或掉電保存功能的人也適合剛接觸DSP I2C外設(shè)、想找一份能抄的驅(qū)動模板的入門者。2. 存儲映射與硬件連接先把DSP2812和EEPROM對上號2.1 EEPROM選型與I2C地址為什么多數(shù)人直接用AT24C系列DSP2812這顆芯片本身不帶EEPROM也不帶硬件I2C外設(shè)所以操作EEPROM只有兩條路一是用GPIO軟件模擬I2C二是外接帶并口的存儲芯片。實際項目里鮮少有人為了一顆存儲芯片去占用DSP的并行總線絕大多數(shù)方案都是I2C接口的EEPROM其中AT24C系列是絕對的主流。這個系列容量從2Kbit到256Kbit都有工作電壓覆蓋1.8V到5.5V和DSP2812的3.3V IO電平直接對接不用加電平轉(zhuǎn)換省事。選型時有幾個硬指標(biāo)要看清。AT24C02是2Kbit也就是256字節(jié)適合存幾個校準(zhǔn)參數(shù)AT24C64是64Kbit也就是8K字節(jié)能塞下故障記錄表或波形曲線。地址方面AT24C系列的7位設(shè)備地址是固定的1010加上A2、A1、A0三個引腳的電平組合。比如A2A1A0全接地寫地址就是0xA0讀地址是0xA1。板上如果只掛一顆芯片建議把地址引腳全部接地省得軟件里還要動態(tài)切地址。這個資源里的程序默認(rèn)就是按AT24C128來寫的7位地址0xA0/0xA1如果你的板上用的是AT24C02系列容量小于等于256字節(jié)頁大小、寫周期都要跟著調(diào)。2.2 GPIO模擬I2C的引腳分配與上拉電阻DSP2812沒有I2C控制器所有時序全靠GPIO翻轉(zhuǎn)。資源里默認(rèn)用兩個普通IO引腳來做SCL和SDA通常選GPIOB組的引腳因為它們靠近芯片邊緣布板走線短。需要特別注意的是I2C總線必須接上拉電阻SCL和SDA各接一顆4.7kΩ的上拉電阻到3.3V電阻值根據(jù)總線長度和通信速率調(diào)整。注意上拉電阻不是可選項是硬性要求。不接上拉SDA線拉不上去通信必然失敗。GPIO配置要按開漏輸出來處理。DSP2812的GPIO模塊本身沒有真正的開漏模式但可以通過方向寄存器模擬做輸出時只拉低釋放時把方向寄存器切回輸入模式這樣外部上拉電阻就能把電平拉高。這個資源里的驅(qū)動初始化函數(shù)已經(jīng)把這一步封裝好了你在移植時只需要確認(rèn)引腳編號對應(yīng)的是你自己的硬件連接。2.3 讀寫時序的宏觀理解起始、停止、應(yīng)答與數(shù)據(jù)幀在貼代碼之前先把I2C的幀結(jié)構(gòu)在大腦里過一遍。一次完整的字節(jié)寫操作包含五個階段起始信號、發(fā)送設(shè)備地址加寫標(biāo)志、發(fā)送寄存器地址、發(fā)送要寫入的數(shù)據(jù)、停止信號。其中每一步都要等從設(shè)備回一個ACK應(yīng)答位沒等到ACK基本可以判定從設(shè)備不在線或者地址寫錯了。讀操作稍微繞一點。如果是隨機讀需要先發(fā)一個“偽寫”序列把目標(biāo)地址寫進去然后重新發(fā)起始信號再發(fā)設(shè)備地址加讀標(biāo)志之后連續(xù)讀字節(jié)。順序讀則是在隨機讀的基礎(chǔ)上讀完一個字節(jié)后繼續(xù)發(fā)時鐘EEPROM內(nèi)部的地址指針會自動累加連續(xù)輸出后續(xù)地址的數(shù)據(jù)。資源里分別封裝了單字節(jié)讀、順序讀、頁寫這三種接口實際使用中順序讀和頁寫是效率最高的兩種。3. 驅(qū)動代碼拆解初始化、單字節(jié)讀寫與頁寫入實現(xiàn)3.1 宏定義與私有變量把器件參數(shù)和引腳配置隔離出來項目里驅(qū)動代碼的頭部定義集中在兩個頭文件中一個管硬件引腳一個管器件參數(shù)。先說引腳相關(guān)的部分。// EEPROM_Driver.h #define EEPROM_SCL_PORT GPIOB #define EEPROM_SCL_PIN GPIO_PIN_4 #define EEPROM_SDA_PORT GPIOB #define EEPROM_SDA_PIN GPIO_PIN_5 #define EEPROM_SCL_HIGH() GpioDataRegs.GPBDAT.all | 0x0010 #define EEPROM_SCL_LOW() GpioDataRegs.GPBDAT.all 0xFFEF #define EEPROM_SDA_INPUT_MODE() GpioMuxRegs.GPBDIR.all 0xFFDF #define EEPROM_SDA_OUTPUT_MODE() GpioMuxRegs.GPBDIR.all | 0x0020這段代碼的作用是把SCL和SDA抽象成一組宏后續(xù)驅(qū)動函數(shù)里只調(diào)用這些宏不直接操作寄存器。這樣如果換板子、換引腳只需要改這幾行宏定義驅(qū)動邏輯一概不動。GPBDAT和GPBDIR都是DSP2812的GPIOB組寄存器GPBDAT管電平數(shù)據(jù)GPBDIR管方向。SCL只做輸出所以不必切方向SDA需要雙向切換所以單獨留了輸入輸出兩個模式宏。器件參數(shù)部分定義了AT24C128的關(guān)鍵時序參數(shù)#define EEPROM_DEV_ADDR_W 0xA0 #define EEPROM_DEV_ADDR_R 0xA1 #define EEPROM_PAGE_SIZE 64 #define EEPROM_WRITE_CYCLE_MS 10 #define EEPROM_MAX_ADDR 0x3FFFAT24C128的頁大小是64字節(jié)最大地址0x3FFF對應(yīng)16K字節(jié)容量。寫周期定義為10ms雖然數(shù)據(jù)手冊上典型值是5ms但考慮到電壓波動和溫度影響留出一倍裕量更穩(wěn)妥。3.2 底層時序函數(shù)I2C起始、停止與應(yīng)答檢測的實現(xiàn)這三段是整個驅(qū)動的地基。起始和停止的時序在I2C協(xié)議里有嚴(yán)格定義起始是SCL高電平期間SDA產(chǎn)生下降沿停止是SCL高電平期間SDA產(chǎn)生上升沿。代碼實現(xiàn)時直接按這個狀態(tài)機來寫static void I2C_Start(void) { EEPROM_SDA_OUTPUT_MODE(); // SDA切到輸出模式 EEPROM_SDA_HIGH(); EEPROM_SCL_HIGH(); DELAY_US(5); // 保持建立時間 EEPROM_SDA_LOW(); // SCL高期間SDA拉低 - 起始信號 DELAY_US(5); EEPROM_SCL_LOW(); // 拉低SCL準(zhǔn)備傳數(shù)據(jù) }DELAY_US是DSP2812專用延時函數(shù)需要自己實現(xiàn)或借用官方庫的DSP28x_Delay。起始信號的關(guān)鍵在于SDA的下降沿必須發(fā)生在SCL為高時所以代碼順序是SCL先拉高再拉低SDA。停止信號則是SDA上升沿在SCL高時發(fā)生邏輯剛好相反。ACK檢測是最容易出問題的地方。EEPROM在接收到正確字節(jié)后會在第九個時鐘周期把SDA拉低作為應(yīng)答。檢測代碼如下static unsigned char I2C_WaitAck(void) { unsigned char timeout 0; unsigned char ack 1; EEPROM_SDA_INPUT_MODE(); // SDA切輸入準(zhǔn)備讀從設(shè)備電平 EEPROM_SCL_HIGH(); // 第九個時鐘從設(shè)備把SDA拉低表示ACK DELAY_US(5); while(EEPROM_SDA_READ()) // 讀GPIO電平 { timeout; if(timeout 200) { ack 0; // 超時判定無應(yīng)答 break; } DELAY_US(1); } EEPROM_SCL_LOW(); // 結(jié)束第九個時鐘 EEPROM_SDA_OUTPUT_MODE(); // SDA切回輸出 return ack; }無應(yīng)答的后果是致命的所以我在這段加了超時計數(shù)器。實際調(diào)試中發(fā)現(xiàn)有時候EEPROM上電瞬間還沒準(zhǔn)備好前幾百毫秒會一直不回ACK軟件里做重試機制比單純加大超時時間更管用。3.3 單字節(jié)寫入與隨機讀最常用的兩個接口單字節(jié)寫是最基礎(chǔ)的接口也最適合用來驗證硬件連接是否正常。代碼里把地址分成高字節(jié)和低字節(jié)分別發(fā)送因為AT24C128內(nèi)部是16位地址空間。unsigned char EEPROM_WriteByte(unsigned int addr, unsigned char data) { unsigned char ret 0; I2C_Start(); ret I2C_SendByte(EEPROM_DEV_ADDR_W); // 發(fā)送設(shè)備地址 寫標(biāo)志 ret I2C_SendByte((addr 8) 0xFF); // 地址高字節(jié) ret I2C_SendByte(addr 0xFF); // 地址低字節(jié) ret I2C_SendByte(data); // 目標(biāo)數(shù)據(jù) I2C_Stop(); DELAY_MS(EEPROM_WRITE_CYCLE_MS); // 等內(nèi)部寫周期完成 if(ret 0) return 1; // 成功 else return 0; // 失敗 }I2C_SendByte內(nèi)部會在每個字節(jié)發(fā)送完后調(diào)用I2C_WaitAck收到ACK返回0無ACK返回1。返回值累加的意義在于哪一步?jīng)]得到應(yīng)答ret就不為零方便定位是設(shè)備地址不對還是總線接觸不良。寫周期延時是必須的EEPROM在接收到數(shù)據(jù)后內(nèi)部要擦寫這個過程里不響應(yīng)任何外部命令延時不夠的話緊接著讀出來的數(shù)據(jù)是錯的。隨機讀接口和寫入流程不一樣它要先發(fā)一個偽寫操作把地址指針定位然后重新走一遍起始信號再切換成讀模式unsigned char EEPROM_ReadByte(unsigned int addr, unsigned char *data) { unsigned char ret 0; I2C_Start(); ret I2C_SendByte(EEPROM_DEV_ADDR_W); // 偽寫定位地址 ret I2C_SendByte((addr 8) 0xFF); ret I2C_SendByte(addr 0xFF); I2C_Start(); // 重新產(chǎn)生起始信號 ret I2C_SendByte(EEPROM_DEV_ADDR_R); // 設(shè)備地址 讀標(biāo)志 *data I2C_RecvByte(); // 讀一個字節(jié) I2C_NoAck(); // 最后一字節(jié)回非應(yīng)答 I2C_Stop(); return (ret 0) ? 1 : 0; }這里有個容易看迷糊的點為什么地址定位之后不能直接讀非得重新發(fā)一次起始信號因為上一次總線上是寫模式從設(shè)備的I2C狀態(tài)機在收到寫地址后一直等在接收數(shù)據(jù)的狀態(tài)必須用起始信號打斷它讓它重新解析接下來收到的是讀地址。I2C_RecvByte返回前主機要在第九個時鐘不發(fā)應(yīng)答否則從設(shè)備會以為還要繼續(xù)傳。這個NoAck信號是讀多字節(jié)時區(qū)分結(jié)束的關(guān)鍵。3.4 頁寫入與順序讀批量操作的正確姿勢單字節(jié)寫簡單可靠但效率太低。AT24C128內(nèi)部有SRAM緩沖一次最多可以寫入64字節(jié)只有最后一個字節(jié)的寫周期需要等待。頁寫入就是利用這個緩沖的批量操作unsigned char EEPROM_WritePage(unsigned int addr, unsigned char *buf, unsigned int len) { unsigned int i; unsigned char ret 0; if(len EEPROM_PAGE_SIZE) len EEPROM_PAGE_SIZE; I2C_Start(); ret I2C_SendByte(EEPROM_DEV_ADDR_W); ret I2C_SendByte((addr 8) 0xFF); ret I2C_SendByte(addr 0xFF); for(i 0; i len; i) ret I2C_SendByte(buf[i]); I2C_Stop(); DELAY_MS(EEPROM_WRITE_CYCLE_MS); return (ret 0) ? 1 : 0; }頁寫入的典型坑是跨頁問題。AT24C128內(nèi)部地址指針在頁邊界不會自動回卷比如你從地址0x0030開始寫40字節(jié)前面32字節(jié)落在64字節(jié)頁內(nèi)超過頁邊界后剩下的8字節(jié)會寫到下一頁的開頭整片數(shù)據(jù)直接錯位。這個資源里特意在頭文件注釋里提醒多字節(jié)寫入必須由調(diào)用方保證地址和長度不跨64字節(jié)邊界。我自己封裝上層函數(shù)時加了一道防線把超過頁邊界的數(shù)據(jù)拆分兩次寫雖然耗時增加但數(shù)據(jù)絕對安全。順序讀接口則簡單得多連續(xù)讀時主機每收一個字節(jié)都要回ACK直到最后一個字節(jié)回NoAckunsigned char EEPROM_ReadSeq(unsigned int addr, unsigned char *buf, unsigned int len) { unsigned int i; unsigned char ret 0; I2C_Start(); ret I2C_SendByte(EEPROM_DEV_ADDR_W); // 偽寫 ret I2C_SendByte((addr 8) 0xFF); ret I2C_SendByte(addr 0xFF); I2C_Start(); ret I2C_SendByte(EEPROM_DEV_ADDR_R); for(i 0; i len; i) { buf[i] I2C_RecvByte(); if(i len - 1) I2C_Ack(); // 前N-1字節(jié)回ACK } I2C_NoAck(); // 最后一字節(jié)回NoAck I2C_Stop(); return (ret 0) ? 1 : 0; }注意ACK的順序不能亂。讀多字節(jié)時主機在收到每一字節(jié)的第九個時鐘要拉低SDA表示“繼續(xù)傳”但收到最后一個字節(jié)時必須把SDA放開讓上拉電阻拉高表示“結(jié)束”。這個信號一旦發(fā)錯要么多讀一個字節(jié)要么從設(shè)備停在原地等下一個時鐘總線直接卡死。4. 避坑指南I2C通信中五個典型故障與定位方法4.1 現(xiàn)象讀回數(shù)據(jù)全是0xFF原因很簡單EEPROM沒有響應(yīng)SDA線上無數(shù)據(jù)讀出來的就是上拉電平也就是高電平0xFF。這種故障十有八九是設(shè)備地址不對。AT24C系列掛在總線上的地址由A2A1A0引腳決定如果你板上A0A1A2接了不同的電平地址就不是0xA0?,F(xiàn)場排查先量SDA線的靜態(tài)電平正常情況下總線空閑時應(yīng)該被上拉到高如果量到低電平說明有設(shè)備把總線拉死了。然后逐個檢查地址引腳的實際電平用萬用表量一遍A0到A2的電壓再對照驅(qū)動里的設(shè)備地址宏。我遇到過地址引腳虛焊的情況A1引腳浮空時好時壞換了板子才找到問題。4.2 現(xiàn)象連續(xù)寫入多字節(jié)后前面對后面錯這個坑基本就是頁寫入跨頁導(dǎo)致的。AT24C系列內(nèi)部地址指針是11位或者更寬但頁內(nèi)偏移只有低位幾位超過頁大小后低位回卷高位不變數(shù)據(jù)就寫到錯誤位置了。解決方法是寫入前判斷當(dāng)前頁剩余空間是否夠本次數(shù)據(jù)量不夠就把數(shù)據(jù)拆成兩段第一段寫滿當(dāng)前頁第二段從下一頁開頭接著寫。這個邏輯可以封裝在高層接口里所有調(diào)用寫頁函數(shù)的地方都走這個封裝就再也不會踩跨頁的雷。4.3 現(xiàn)象偶發(fā)寫入失敗重試一次就好了真實現(xiàn)場里這種問題最磨人。有時候?qū)懭敕祷爻晒Φ珨嚯娭貑⒑髷?shù)據(jù)是舊的有時候?qū)懭胫型究ㄋ莱绦蚺茱w。這類偶發(fā)故障最常見的原因就是寫周期延時不夠或者寫周期內(nèi)來了中斷打斷了延時。DSP2812的中斷系統(tǒng)在這個項目里是個隱患寫周期內(nèi)如果有定時器中斷或其他外設(shè)中斷觸發(fā)延時函數(shù)會被拉長但主程序可能已經(jīng)開始下一次讀寫。另外上電瞬間電壓不穩(wěn)也會導(dǎo)致EEPROM內(nèi)部寫周期異常。解決思路分了三個層面。第一寫周期延時盡量用時間戳輪詢代替死等比如記錄當(dāng)前系統(tǒng)計時超過10ms后再繼續(xù)執(zhí)行期間允許中斷發(fā)生。第二關(guān)鍵參數(shù)寫入后立刻回讀校驗讀回來不一致就重新寫重試三次還不行再報錯。第三整機斷電時在電壓跌落檢測之后立即觸發(fā)數(shù)據(jù)保存優(yōu)先保證寫周期完整。4.4 現(xiàn)象SCL波形看起來有毛刺通信偶發(fā)異常用示波器抓SCL和SDA波形時如果邊沿不干凈、有毛刺大概率是走線太長或者上拉電阻取值不匹配。I2C是開漏結(jié)構(gòu)電平上升靠上拉電阻電阻太大會導(dǎo)致上升沿變緩電阻太小的導(dǎo)致灌電流過大?,F(xiàn)場處理建議把上拉電阻換成3.3kΩ到4.7kΩ之間總線長度控制在10cm以內(nèi)。DSP2812的GPIO驅(qū)動能力本身有限走線超過20cm時通信速率會明顯下降。4.5 現(xiàn)象掉電保存的數(shù)據(jù)偶爾丟失這是最嚴(yán)重的故障通常發(fā)生在系統(tǒng)電壓跌落瞬間。DSP2812檢測到電壓低時開始執(zhí)行保存流程但此時EEPROM的工作電壓可能已經(jīng)低于最低值內(nèi)部寫周期沒有完成。解決辦法是在硬件上增加電源監(jiān)控芯片電壓低于閾值時給出中斷信號DSP在中斷里趕緊保存參數(shù)。同時保存動作要精簡只把最核心的幾十字節(jié)寫進EEPROM別指望掉電瞬間把幾百個字節(jié)都寫完。這個資源里有一顆硬核的寫保護邏輯它會在第一個字節(jié)寫完后就切到只寫關(guān)鍵數(shù)據(jù)模式。5. 進階驗證技巧用輪詢狀態(tài)機和上位機回讀把驅(qū)動調(diào)到滴水不漏把驅(qū)動跑通只是開始真正能扛住現(xiàn)場的是驗證方法。我這邊推薦兩步走一是改造成輪詢方式等待寫周期二是用串口把EEPROM里的內(nèi)容回傳到電腦上對比。輪詢寫周期的方式其實很簡單EEPROM在內(nèi)部寫周期內(nèi)不會應(yīng)答任何I2C命令但每次寫操作都等待固定延時是浪費。你可以把固定延時換成“寫完后立刻嘗試讀取設(shè)備地址如果讀不到ACK就繼續(xù)等待直到讀到ACK為止”。這段邏輯的代碼長這樣unsigned char EEPROM_WaitWriteComplete(void) { unsigned char timeout 0; I2C_Start(); while(I2C_SendByte(EEPROM_DEV_ADDR_W)) // 寫地址等ACK { timeout; if(timeout 100) { I2C_Stop(); return 0; } DELAY_MS(1); } I2C_Stop(); return 1; }這段代碼放在每次寫操作之后替代固定延時寫周期實際多長就等多久整體運行時間能省接近一半。但對于DSP2812這種實時性要求高的場景固定延時仍然是最穩(wěn)妥的輪詢方式適合你對時序有把握之后再做優(yōu)化。數(shù)據(jù)回讀驗證是我每次調(diào)試必走的一步。把所有EEPROM地址空間讀出來通過串口發(fā)送到電腦上用十六進制編輯器打開對比。重點看三個位置寫入?yún)^(qū)數(shù)據(jù)是否與源數(shù)據(jù)完全一致、未寫入?yún)^(qū)是否全0xFF、地址邊界處是否有錯位數(shù)據(jù)。曾經(jīng)碰到過一次驅(qū)動收發(fā)反了的問題就是靠回讀全量數(shù)據(jù)才發(fā)現(xiàn)所有高字節(jié)和低字節(jié)換了位置。從那以后我每寫完一版驅(qū)動都強制自己在仿真器里跑一遍“全空間隨機寫入再全量回讀”的測試腳本跑完不通過絕不進下一步。這套流程聽著笨重但在現(xiàn)場幫我擠掉了大半隱性故障。這個資源最大的價值也就在于此它給的不只是一段能跑的驅(qū)動更是一套從初始化到校驗完整的思考路徑希望幫到你。本文還有配套的精品資源點擊獲取