動AT24C02:I2C EEPROM讀寫避坑與實戰(zhàn))
簡介這份資源面向使用STM32F407進行嵌入式開發(fā)的工程師與學(xué)習(xí)者聚焦HAL庫下I2C總線讀寫AT24C02 EEPROM的完整實現(xiàn)。內(nèi)容涵蓋RCC與GPIO時鐘配置、I2C外設(shè)初始化、主模式收發(fā)函數(shù)調(diào)用、AT24C02頁寫與隨機讀時序以及超時與錯誤狀態(tài)檢查等關(guān)鍵環(huán)節(jié)適合需要掌握片上外設(shè)驅(qū)動與數(shù)據(jù)持久化存儲的中級開發(fā)者參考。資源包共1532個文件以950個C源文件與270個頭文件為主體另含啟動匯編、鏈接腳本、工程配置與少量說明文本壓縮包約7.47MB工程結(jié)構(gòu)完整可直接導(dǎo)入調(diào)試。目前已有427人學(xué)習(xí)下載。通過該工程可快速理解I2C通信流程與EEPROM讀寫邏輯并在此基礎(chǔ)上擴展DMA傳輸、多設(shè)備總線仲裁與穩(wěn)定性優(yōu)化為實際項目中的數(shù)據(jù)存儲方案提供可復(fù)用的代碼基礎(chǔ)。1. 從一次“玄學(xué)”讀寫出錯說起這套 AT24C02 驅(qū)動到底能解決什么如果你在用 STM32F407 的 HAL 庫調(diào) I2C 讀寫 AT24C02大概率遇到過這種情況單步調(diào)試一切正常全速跑起來讀出來的數(shù)據(jù)偶爾錯一位或者干脆卡在HAL_I2C_Mem_Read里等到超時。這不是你代碼寫錯了而是 EEPROM 的頁寫時序、應(yīng)答輪詢和 HAL 庫的超時機制沒對齊。這套資源就是圍繞 STM32F407 HAL 庫 I2C 外設(shè)讀寫 AT24C02 這條鏈路把初始化、字節(jié)讀寫、頁寫、連續(xù)讀、寫保護處理全部拆開講清楚。它適合正在做參數(shù)存儲、掉電保存、設(shè)備序列號寫入這類功能的嵌入式開發(fā)者尤其是從標(biāo)準庫轉(zhuǎn) HAL 庫、對 I2C 時序還沒完全吃透的人。你拿到的不只是一段能跑的代碼而是一套能解釋“為什么這么寫”的配置邏輯。2. 硬件連接與 CubeMX 配置先把 I2C 外設(shè)的底層參數(shù)定下來2.1 AT24C02 的引腳與地址規(guī)則AT24C02 是 2Kbit 容量的 I2C EEPROM換算成字節(jié)就是 256 字節(jié)頁大小 8 字節(jié)。它一共 8 個引腳實際接線的只有四個VCC、GND、SCL、SDA。A0、A1、A2 三個地址引腳決定器件在 I2C 總線上的 7 位從機地址。AT24C02 的固定前綴是1010后面三位由 A0~A2 的硬件電平?jīng)Q定。如果三個腳全部接地7 位地址就是1010000即 0x50。HAL 庫函數(shù)里填的地址參數(shù)需要左移一位變成 0xA0 作為寫地址、0xA1 作為讀地址。很多人第一次調(diào)不通就是直接把 0x50 填進HAL_I2C_Mem_Read的 DevAddress 參數(shù)結(jié)果自然沒有應(yīng)答。WP 引腳是寫保護接高電平時芯片進入只讀狀態(tài)接低電平才允許寫入。如果你發(fā)現(xiàn)寫入后讀出來還是舊數(shù)據(jù)先拿萬用表量一下 WP 腳的電平這個坑我見過太多次。SDA 和 SCL 上拉電阻一般取 4.7kΩ 到 10kΩSTM32F407 的 I2C 引腳是開漏輸出沒有外部上拉的話總線根本拉不高示波器上看就是一條爬不上去的斜線。2.2 CubeMX 里 I2C 參數(shù)怎么填在 CubeMX 里選 I2C1模式選I2C速度模式一般選Standard Mode或Fast Mode。AT24C02 支持 100kHz 和 400kHz但實際布線較長或者上拉電阻偏大時建議先用 100kHz 調(diào)通再往 400kHz 提。Clock Speed 這一欄填 100000 或 400000CubeMX 會自動算 CCR 和 TRISE 的值。下面這張表是我在 STM32F407 主頻 168MHz 下常用的兩組配置參數(shù)標(biāo)準模式快速模式I2C Clock Speed100000 Hz400000 HzDuty Cycle2:12:1Analog FilterEnableEnableDigital Filter00上拉電阻4.7kΩ2.2kΩ生成代碼后MX_I2C1_Init函數(shù)里會看到hi2c1.Init.ClockSpeed等字段被賦值。如果你后續(xù)要動態(tài)改速度直接改這個結(jié)構(gòu)體再調(diào)HAL_I2C_Init就行但注意改完要重新使能外設(shè)。2.3 GPIO 模式與復(fù)用配置I2C 的 SCL 和 SDA 必須配置為復(fù)用開漏模式CubeMX 里選I2C1_SCL和I2C1_SDA的復(fù)用功能后GPIO 模式會自動設(shè)為GPIO_MODE_AF_OD上拉設(shè)為GPIO_PULLUP速度等級建議GPIO_SPEED_FREQ_HIGH。這里有個細節(jié)STM32F407 的內(nèi)部上拉電阻大約 40kΩ做 I2C 上拉太弱所以外部上拉電阻不能省。我一般會在原理圖里固定放兩顆 4.7kΩ 電阻不管內(nèi)部上拉開不開外部都焊上。/* I2C1 初始化片段由 CubeMX 生成后手動補充部分參數(shù) */ hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 100000; /* 先用 100kHz 調(diào)通 */ hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; /* 占空比 2:1 */ hi2c1.Init.OwnAddress1 0; /* 主機模式不用管自身地址 */ hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; /* 允許時鐘拉伸 */ if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); }這段代碼里NoStretchMode設(shè)為DISABLE表示允許從機拉伸時鐘。AT24C02 在寫入周期內(nèi)會拉低 SCL 告訴主機“我還在忙”如果禁止時鐘拉伸主機就會在從機沒準備好時強行發(fā)時鐘讀出來的數(shù)據(jù)必然出錯。DutyCycle在標(biāo)準模式下其實影響不大但快速模式下 2:1 和 16:9 的取舍會影響高低電平持續(xù)時間一般選 2:1 兼容性更好。3. HAL 庫讀寫 AT24C02從字節(jié)寫到頁寫的完整實現(xiàn)3.1 單字節(jié)寫與寫周期等待AT24C02 的單字節(jié)寫流程是主機發(fā)啟動條件發(fā)設(shè)備寫地址發(fā)一個字節(jié)的內(nèi)存地址發(fā)一個字節(jié)的數(shù)據(jù)發(fā)停止條件。HAL 庫把這個過程封裝成了HAL_I2C_Mem_Write。但關(guān)鍵點在于EEPROM 收到停止條件后并不會立刻完成內(nèi)部寫入而是進入一個約 5ms 的內(nèi)部寫周期這段時間內(nèi)它不會應(yīng)答任何總線請求。如果你緊接著發(fā)下一次讀寫就會因為收不到 ACK 而返回 HAL_ERROR 或 HAL_TIMEOUT。/** * brief 向 AT24C02 指定地址寫入一個字節(jié) * param pData: 待寫入字節(jié)的指針 * param addr: EEPROM 內(nèi)部字節(jié)地址范圍 0x00~0xFF * retval HAL_OK 表示成功 */ HAL_StatusTypeDef AT24C02_WriteByte(I2C_HandleTypeDef *hi2c, uint8_t addr, uint8_t data) { HAL_StatusTypeDef status; uint32_t tickstart HAL_GetTick(); /* 等待上一次寫周期結(jié)束最多等 10ms */ do { status HAL_I2C_IsDeviceReady(hi2c, 0xA0, 3, 10); if (status HAL_OK) break; } while ((HAL_GetTick() - tickstart) 10); if (status ! HAL_OK) return status; /* 發(fā)起單字節(jié)寫 */ status HAL_I2C_Mem_Write(hi2c, 0xA0, addr, I2C_MEMADD_SIZE_8BIT, data, 1, 100); return status; }這段代碼里HAL_I2C_IsDeviceReady是關(guān)鍵。它的作用是發(fā)一個空地址探測從機是否應(yīng)答如果 EEPROM 還在寫周期內(nèi)就不會回 ACK函數(shù)返回 HAL_BUSY 或 HAL_ERROR。循環(huán)等待直到設(shè)備就緒或者超過 10ms 超時。0xA0是寫地址I2C_MEMADD_SIZE_8BIT表示內(nèi)存地址是 8 位AT24C02 的地址范圍只有 256 字節(jié)所以用 8 位地址模式。最后一個參數(shù)100是超時時間單位毫秒對于單字節(jié)寫來說足夠。3.2 頁寫與跨頁邊界處理AT24C02 的頁大小是 8 字節(jié)。頁寫的意思是你一次可以連續(xù)寫最多 8 個字節(jié)但前提是這 8 個字節(jié)不能跨越頁邊界。頁邊界就是地址能被 8 整除的位置比如 0x00、0x08、0x10 等。如果你從地址 0x06 開始寫 4 個字節(jié)寫到 0x09 就跨頁了EEPROM 內(nèi)部會回卷到當(dāng)前頁的開頭也就是 0x00把后面的數(shù)據(jù)覆蓋掉。這個行為是芯片設(shè)計決定的不是 bug但如果你不知道就會丟數(shù)據(jù)。/** * brief 頁寫函數(shù)自動處理跨頁拆分 * param addr: 起始地址 * param pData: 數(shù)據(jù)緩沖區(qū) * param len: 數(shù)據(jù)長度 * retval HAL_OK 表示全部寫入成功 */ HAL_StatusTypeDef AT24C02_WritePage(I2C_HandleTypeDef *hi2c, uint8_t addr, uint8_t *pData, uint16_t len) { HAL_StatusTypeDef status; uint16_t written 0; while (written len) { /* 計算當(dāng)前頁還能寫幾個字節(jié) */ uint8_t page_remain 8 - (addr % 8); uint8_t chunk (len - written page_remain) ? (len - written) : page_remain; /* 等待寫周期結(jié)束 */ uint32_t tickstart HAL_GetTick(); do { status HAL_I2C_IsDeviceReady(hi2c, 0xA0, 3, 10); if (status HAL_OK) break; } while ((HAL_GetTick() - tickstart) 10); if (status ! HAL_OK) return status; /* 寫當(dāng)前頁內(nèi)的數(shù)據(jù) */ status HAL_I2C_Mem_Write(hi2c, 0xA0, addr, I2C_MEMADD_SIZE_8BIT, pData[written], chunk, 100); if (status ! HAL_OK) return status; addr chunk; written chunk; } return HAL_OK; }這個函數(shù)的邏輯是每次循環(huán)先算當(dāng)前地址距離頁邊界還剩幾個字節(jié)然后只寫這么多。寫完一頁后地址前進繼續(xù)下一輪。addr % 8得到的是當(dāng)前地址在頁內(nèi)的偏移8 - offset就是本頁剩余空間。如果剩余數(shù)據(jù)長度小于頁剩余空間就只寫剩余長度。這樣無論從哪個地址開始寫、寫多長都不會跨頁回卷。注意每次寫之前都要等寫周期因為頁寫雖然一次寫多個字節(jié)但內(nèi)部寫周期仍然是約 5ms。3.3 連續(xù)讀與隨機讀讀操作比寫操作簡單因為讀不會觸發(fā)內(nèi)部寫周期不需要等待。AT24C02 支持當(dāng)前地址讀、隨機讀和順序讀。HAL 庫的HAL_I2C_Mem_Read可以直接實現(xiàn)隨機讀和連續(xù)讀。/** * brief 從 AT24C02 連續(xù)讀取多個字節(jié) * param addr: 起始地址 * param pData: 接收緩沖區(qū) * param len: 讀取長度 * retval HAL_OK 表示成功 */ HAL_StatusTypeDef AT24C02_ReadBytes(I2C_HandleTypeDef *hi2c, uint8_t addr, uint8_t *pData, uint16_t len) { return HAL_I2C_Mem_Read(hi2c, 0xA1, addr, I2C_MEMADD_SIZE_8BIT, pData, len, 100); }這里設(shè)備地址填0xA1是 0x50 左移一位后加 1 得到的讀地址。I2C_MEMADD_SIZE_8BIT同樣表示 8 位內(nèi)存地址。讀取長度可以超過頁大小因為讀操作沒有頁邊界限制EEPROM 會自動遞增地址讀到 0xFF 后回卷到 0x00。超時時間給 100ms 足夠因為讀操作不涉及內(nèi)部寫周期。3.4 寫保護與數(shù)據(jù)校驗WP 引腳拉高時芯片只讀寫入操作會返回 HAL_ERROR 或者看似成功但實際沒寫進去。我一般會在初始化時把 WP 拉低寫入完成后如果需要保護再拉高。另外重要數(shù)據(jù)寫入后建議立刻讀回來比對確認寫入成功。/** * brief 寫入并校驗一個字節(jié) * retval HAL_OK 表示寫入且校驗通過 */ HAL_StatusTypeDef AT24C02_WriteVerify(I2C_HandleTypeDef *hi2c, uint8_t addr, uint8_t data) { uint8_t readback 0; HAL_StatusTypeDef status; status AT24C02_WriteByte(hi2c, addr, data); if (status ! HAL_OK) return status; status AT24C02_ReadBytes(hi2c, addr, readback, 1); if (status ! HAL_OK) return status; return (readback data) ? HAL_OK : HAL_ERROR; }這個校驗函數(shù)在寫入后立刻讀回如果數(shù)據(jù)不一致就返回錯誤。對于存儲設(shè)備序列號、校準參數(shù)這類不能出錯的數(shù)據(jù)我建議都走一遍校驗。代價是多一次讀操作但換來的是確定性。4. 避坑與排查I2C 讀寫 EEPROM 最常見的五個翻車點4.1 現(xiàn)象HAL_I2C_Mem_Read一直返回 HAL_TIMEOUT原因通常有三個一是設(shè)備地址填錯把 7 位地址 0x50 直接填進去而不是 0xA1二是 SDA 或 SCL 沒有上拉電阻總線一直處于低電平三是 EEPROM 正在寫周期內(nèi)沒有等待就發(fā)起讀操作。排查時先用示波器看 SCL 和 SDA 有沒有波形如果沒有波形說明 GPIO 復(fù)用沒配對或者外設(shè)沒使能。如果有波形但地址字節(jié)后沒有 ACK就是地址錯了或者芯片沒供電。如果地址有 ACK 但數(shù)據(jù)階段超時檢查是不是在寫周期內(nèi)發(fā)起了讀。4.2 現(xiàn)象寫入后讀出來是舊數(shù)據(jù)先量 WP 引腳電平高電平就是寫保護生效了。如果 WP 是低電平檢查寫入函數(shù)有沒有等寫周期。AT24C02 的寫周期典型值 5ms最大 10ms如果你寫完立刻讀芯片還沒完成內(nèi)部寫入讀到的自然是舊數(shù)據(jù)。另外檢查頁寫有沒有跨頁跨頁會導(dǎo)致數(shù)據(jù)回卷覆蓋看起來就像沒寫進去。4.3 現(xiàn)象連續(xù)寫多個字節(jié)時只有第一個字節(jié)正確這是典型的頁寫跨頁問題。比如從地址 0x07 開始寫 4 個字節(jié)第一個字節(jié)寫到 0x07第二個字節(jié)本應(yīng)寫到 0x08但 0x08 是下一頁的起始地址EEPROM 內(nèi)部地址計數(shù)器會回卷到 0x00把后面的數(shù)據(jù)寫到 0x00 和 0x01。解決辦法就是用 3.2 節(jié)的頁寫函數(shù)自動按頁拆分。4.4 現(xiàn)象I2C 總線死鎖SCL 被從機拉低不釋放這種情況通常發(fā)生在主機復(fù)位后從機還在寫周期內(nèi)或者總線受到干擾導(dǎo)致狀態(tài)機錯亂。STM32F407 的 I2C 外設(shè)有總線恢復(fù)機制但 HAL 庫沒有直接提供。我一般會在初始化前手動模擬幾個 SCL 脈沖把從機狀態(tài)清掉把 SCL 配成普通 GPIO 輸出發(fā) 9 個時鐘脈沖再把 SDA 拉高發(fā)停止條件最后重新初始化 I2C 外設(shè)。這個操作在MX_I2C1_Init之前做能解決大部分死鎖。4.5 現(xiàn)象CubeMX 生成的代碼里 I2C 速度設(shè)為 400kHz 但實際通信失敗400kHz 對總線上拉電阻和布線電容有要求。如果上拉電阻是 10kΩ上升沿會變緩400kHz 下可能采不到高電平。把上拉電阻換成 2.2kΩ 到 4.7kΩ 試試。另外檢查DutyCycle設(shè)置快速模式下 2:1 和 16:9 會影響時序余量如果從機兼容性一般先用 100kHz 調(diào)通再提速。5. 進階技巧用結(jié)構(gòu)體映射和雙備份提升參數(shù)存儲可靠性5.1 把參數(shù)結(jié)構(gòu)體直接映射到 EEPROM 地址實際項目里很少一個字節(jié)一個字節(jié)地存參數(shù)更常見的做法是定義一個結(jié)構(gòu)體把整個結(jié)構(gòu)體寫到 EEPROM 的固定地址。AT24C02 只有 256 字節(jié)結(jié)構(gòu)體不能太大但存一組校準參數(shù)、設(shè)備配置足夠了。/* 參數(shù)結(jié)構(gòu)體總大小 16 字節(jié)放在 EEPROM 0x10 起始地址 */ typedef struct { uint16_t device_id; /* 設(shè)備編號 */ uint16_t baudrate; /* 通信波特率 */ uint8_t calib_offset; /* 校準偏移 */ uint8_t work_mode; /* 工作模式 */ uint8_t reserved[10]; /* 預(yù)留擴展 */ } DeviceConfig_t; #define CONFIG_ADDR 0x10 /** * brief 保存配置到 EEPROM */ HAL_StatusTypeDef Config_Save(I2C_HandleTypeDef *hi2c, DeviceConfig_t *cfg) { return AT24C02_WritePage(hi2c, CONFIG_ADDR, (uint8_t *)cfg, sizeof(DeviceConfig_t)); } /** * brief 從 EEPROM 加載配置 */ HAL_StatusTypeDef Config_Load(I2C_HandleTypeDef *hi2c, DeviceConfig_t *cfg) { return AT24C02_ReadBytes(hi2c, CONFIG_ADDR, (uint8_t *)cfg, sizeof(DeviceConfig_t)); }這里用(uint8_t *)cfg把結(jié)構(gòu)體指針強轉(zhuǎn)成字節(jié)指針sizeof(DeviceConfig_t)得到結(jié)構(gòu)體總字節(jié)數(shù)。注意結(jié)構(gòu)體要做字節(jié)對齊否則不同編譯器下大小可能不一致。我一般會在結(jié)構(gòu)體里顯式加reserved字段湊齊 4 字節(jié)對齊避免編譯器插入填充字節(jié)導(dǎo)致讀寫長度對不上。5.2 雙備份加校驗字防止寫入過程中掉電EEPROM 最大的風(fēng)險是寫入過程中掉電導(dǎo)致數(shù)據(jù)寫了一半。解決辦法是在 EEPROM 里存兩份配置每份后面跟一個校驗字。加載時先讀第一份校驗通過就用不通過再讀第二份兩份都不通過就加載默認值。區(qū)域起始地址長度內(nèi)容備份 A0x1016 字節(jié)配置數(shù)據(jù)校驗 A0x202 字節(jié)0x5A5A 表示有效備份 B0x3016 字節(jié)配置數(shù)據(jù)校驗 B0x402 字節(jié)0x5A5A 表示有效寫入時先寫備份 B 和校驗 B再寫備份 A 和校驗 A。這樣即使寫 A 的過程中掉電B 仍然是完整的。加載時先讀 A 校驗失敗再讀 B 校驗。校驗字用固定值 0x5A5A雖然簡單但足夠區(qū)分“寫過”和“沒寫過”的狀態(tài)。如果你需要更強的校驗可以加 CRC16但 AT24C02 空間有限16 字節(jié)數(shù)據(jù)加 2 字節(jié)校驗已經(jīng)占了 36 字節(jié)再加 CRC 就要壓縮數(shù)據(jù)結(jié)構(gòu)了。5.3 用HAL_I2C_IsDeviceReady做寫完成檢測的細節(jié)前面代碼里用了HAL_I2C_IsDeviceReady輪詢等待寫周期這個函數(shù)的參數(shù)含義值得說清楚。第一個參數(shù)是 I2C 句柄第二個是設(shè)備地址7 位地址左移后的值第三個Trials是嘗試次數(shù)第四個Timeout是每次嘗試的超時時間。我一般設(shè)Trials3、Timeout10意思是每次探測最多等 10ms最多試 3 次。如果 EEPROM 在 5ms 內(nèi)完成寫入第一次探測就返回 HAL_OK如果超過 10ms 還沒就緒就再試一次??偟却龝r間不超過 30ms對大多數(shù)應(yīng)用足夠了。注意HAL_I2C_IsDeviceReady發(fā)的是空地址探測不會讀寫任何數(shù)據(jù)所以不會干擾 EEPROM 內(nèi)部狀態(tài)。但如果你在中斷里調(diào)用它注意它內(nèi)部有阻塞等待不要在高速中斷里用。從那以后我每次調(diào) I2C EEPROM都強制先跑一遍HAL_I2C_IsDeviceReady確認設(shè)備在線再走讀寫流程這個習(xí)慣幫我省掉了至少一半的“玄學(xué)”問題。希望幫到你。本文還有配套的精品資源點擊獲取