嵌入式非易失性數(shù)據(jù)存儲實踐)
1. 為什么偏偏是 MRAM 加 PIC18 這個組合1.1 從兩個器件的定位說起MR25H40CDF 是一顆 4Mbit 的磁阻隨機(jī)存取存儲器也就是 MRAM接口是標(biāo)準(zhǔn) SPI。PIC18LF47K40 是 Microchip 自家的一款 8 位單片機(jī)帶 SPI 外設(shè)工作電壓范圍寬低功耗版本在 1.8V 到 3.6V 之間都能跑。把這兩顆芯片放在一起本質(zhì)上是在解決一個很具體的問題工業(yè)現(xiàn)場和嵌入式設(shè)備里需要一種既能像 RAM 一樣快速隨機(jī)寫入、又能在斷電后保住數(shù)據(jù)的存儲介質(zhì)同時主控又不想上太復(fù)雜的文件系統(tǒng)或者外部存儲控制器。我最早接觸這個組合是在一個數(shù)據(jù)采集終端項目上。那個設(shè)備每分鐘要記錄一次傳感器讀數(shù)包括溫度、壓力、流量還要保存設(shè)備運行狀態(tài)和故障日志。一開始用的是 EEPROM寫次數(shù)很快就到瓶頸了一天寫一千多次一年就是三十多萬次EEPROM 的百萬次擦寫壽命看著夠但實際工業(yè)環(huán)境里溫度一高壽命衰減比手冊上寫的快得多。后來換成 FRAM價格又上去了。再后來看到 MRAM讀寫速度接近 SRAM寫壽命幾乎無限功耗還低就決定試一試。MR25H40CDF 的 4Mbit 容量換算過來是 512KB對于存日志、配置參數(shù)、校準(zhǔn)數(shù)據(jù)、斷點續(xù)傳的緩存來說這個容量剛剛好。它不需要像 Flash 那樣先擦后寫也不需要像 EEPROM 那樣等寫入周期完成SPI 接口直接發(fā)命令就能讀寫對 PIC18 這種資源有限的 8 位機(jī)來說驅(qū)動寫起來非常輕。1.2 SPI 接口在這套方案里的角色SPI 是這套方案里唯一的通信橋梁。PIC18LF47K40 的 MSSP 模塊可以配置成 SPI 主模式MR25H40CDF 作為從設(shè)備四根線SCK、SDI、SDO、CS。這里有個細(xì)節(jié)MR25H40CDF 的 SPI 模式支持 Mode 0 和 Mode 3也就是 CPOL 和 CPHA 的組合。實際用下來Mode 0 最穩(wěn)因為大多數(shù) 8 位機(jī)的 SPI 外設(shè)默認(rèn)就是 Mode 0時鐘空閑低電平數(shù)據(jù)在上升沿采樣。SPI 時鐘頻率方面MR25H40CDF 最高支持 40MHz但 PIC18LF47K40 在 3.3V 下 SPI 時鐘最快也就 10MHz 左右具體取決于系統(tǒng)時鐘和分頻設(shè)置。我一般會把 SPI 時鐘設(shè)在 4MHz 到 8MHz 之間原因有兩個一是 8 位機(jī)的 SPI 移位寄存器在高速下容易受 PCB 走線影響二是 MRAM 本身讀寫沒有等待周期4MHz 已經(jīng)足夠快沒必要為了那點速度去冒信號完整性的風(fēng)險。注意MR25H40CDF 的 CS 建立時間和保持時間要留夠手冊上寫的是 CS 下降沿到第一個 SCK 上升沿至少 5ns最后一個 SCK 下降沿到 CS 上升沿至少 5ns。實際布線時如果 CS 走線太長建議在軟件里加幾個 NOP 再拉 CS別小看這幾個納秒我遇到過因為 CS 時序不對導(dǎo)致讀出來全是 0xFF 的情況。1.3 這套方案適合誰如果你在做工業(yè)數(shù)據(jù)記錄儀、智能儀表、醫(yī)療設(shè)備、車載終端或者任何需要頻繁寫入小量數(shù)據(jù)、又不想上文件系統(tǒng)的嵌入式項目這套組合值得認(rèn)真考慮。它不適合大容量存儲場景512KB 存不了音頻和圖像但存參數(shù)、日志、狀態(tài)機(jī)快照、掉電保護(hù)數(shù)據(jù)綽綽有余。對嵌入式軟件工程師來說驅(qū)動寫起來不復(fù)雜對硬件工程師來說外圍電路簡單不需要額外的電荷泵或者升壓電路。2. MR25H40CDF 的底層操作邏輯2.1 命令集與地址空間MR25H40CDF 的命令集很精簡核心就幾條WREN、WRDI、RDSR、WRSR、READ、WRITE。地址是 19 位因為 4Mbit 等于 512K 字節(jié)需要 19 根地址線才能覆蓋。SPI 傳輸時地址分三個字節(jié)發(fā)第一個字節(jié)的最高三位是無效的實際有效的是低 5 位加上后面兩個完整字節(jié)湊成 19 位。讀操作最簡單拉低 CS發(fā) READ 命令 0x03接著發(fā)三個字節(jié)地址然后就可以連續(xù)讀數(shù)據(jù)了地址會自動遞增讀到 512KB 末尾會回卷到 0。寫操作稍微多一步先發(fā) WREN 命令 0x06拉高 CS再拉低 CS發(fā) WRITE 命令 0x02發(fā)地址發(fā)數(shù)據(jù)。寫完之后 CS 拉高數(shù)據(jù)才真正寫入 MRAM 單元。這里有個和 Flash 完全不同的地方MRAM 不需要擦除。Flash 寫之前必須先擦整個扇區(qū)MRAM 直接覆蓋寫就行。這意味著你的驅(qū)動里不需要維護(hù)擦除狀態(tài)也不需要做磨損均衡。我一開始還習(xí)慣性地想加擦除邏輯后來發(fā)現(xiàn)完全是多余的。2.2 狀態(tài)寄存器與寫保護(hù)狀態(tài)寄存器里有兩個位值得關(guān)注WEL 和 BP0、BP1。WEL 是寫使能鎖存每次寫操作前必須通過 WREN 命令置位寫完成后自動清零。BP0 和 BP1 是塊保護(hù)位可以保護(hù)部分地址空間不被寫入。工業(yè)場景里我一般會把配置參數(shù)區(qū)設(shè)成寫保護(hù)防止程序跑飛時誤改校準(zhǔn)數(shù)據(jù)。狀態(tài)寄存器的讀法拉低 CS發(fā) RDSR 命令 0x05然后讀一個字節(jié)。這個字節(jié)的 bit0 是 WELbit1 是 WEL 的鏡像bit2 和 bit3 是 BP0 和 BP1bit7 是狀態(tài)寄存器的寫保護(hù)位。實際調(diào)試時如果寫不進(jìn)去第一件事就是讀狀態(tài)寄存器看 WEL 有沒有置起來。2.3 時序參數(shù)與實操計算MR25H40CDF 的讀寫周期時間很短手冊上寫的是 35ns 左右但這是芯片內(nèi)部的操作時間SPI 接口的傳輸時間才是瓶頸。假設(shè) SPI 時鐘 4MHz一個字節(jié) 8 位傳輸時間是 2 微秒。寫一個 32 字節(jié)的數(shù)據(jù)包加上命令和地址開銷總共 36 字節(jié)傳輸時間約 72 微秒。如果每分鐘寫一次平均功耗幾乎可以忽略。但如果你要做高頻采集比如每毫秒寫一次那就要算一下總線占用率。每毫秒 72 微秒占用率 7.2%對 PIC18 來說完全可以接受。我實測過在 8MHz SPI 時鐘下連續(xù)寫 512 字節(jié)的緩沖區(qū)耗時大約 520 微秒包括 CS 翻轉(zhuǎn)和命令開銷。提示MRAM 的寫操作是即時完成的不像 Flash 需要等待內(nèi)部編程周期。所以 WRITE 命令發(fā)完CS 拉高之后數(shù)據(jù)就已經(jīng)在 MRAM 里了不需要輪詢狀態(tài)寄存器等待寫完成。這一點在掉電保護(hù)場景里特別重要斷電瞬間只要 CS 已經(jīng)拉高數(shù)據(jù)就安全了。3. PIC18LF47K40 的 SPI 外設(shè)配置3.1 MSSP 模塊初始化步驟PIC18LF47K40 的 SPI 用 MSSP1 模塊配置步驟我習(xí)慣按這個順序來設(shè)置 TRIS 寄存器把 SCK、SDO 設(shè)成輸出SDI 設(shè)成輸入CS 用普通 GPIO 控制。關(guān)閉 MSSP 模塊清零 SSP1CON1 的 SSPEN 位。設(shè)置 SSP1STAT 的 CKE 位和 SSP1CON1 的 CKP 位確定 SPI 模式。Mode 0 對應(yīng) CKE0、CKP0。設(shè)置 SSP1CON1 的 SSPM 位選主模式時鐘分頻。比如 SSPM0b0000 是 Fosc/4。使能 MSSP置位 SSPEN。配置 CS 引腳為輸出默認(rèn)拉高。這里有個坑PIC18 的 SPI 發(fā)送和接收是同一個寄存器 SSP1BUF。寫 SSP1BUF 啟動發(fā)送讀 SSP1BUF 獲取接收數(shù)據(jù)。發(fā)送完成標(biāo)志是 SSP1IF但實際用的時候我一般直接輪詢 SSP1STAT 的 BF 位BF 置位表示接收緩沖區(qū)滿這時候讀 SSP1BUF 就能拿到數(shù)據(jù)。3.2 字節(jié)收發(fā)函數(shù)的寫法底層字節(jié)收發(fā)函數(shù)是整個驅(qū)動的基礎(chǔ)我一般寫成這樣uint8_t spi_transfer(uint8_t data) { SSP1BUF data; while (!SSP1STATbits.BF); return SSP1BUF; }這個函數(shù)看起來簡單但有幾個細(xì)節(jié)要注意。第一SSP1BUF 是雙緩沖的寫進(jìn)去之后硬件自動開始移位讀的時候如果 BF 沒置位讀出來的是上次的舊數(shù)據(jù)。第二在高速 SPI 下while 循環(huán)等待 BF 的時間很短但如果編譯器優(yōu)化等級太高可能會把循環(huán)優(yōu)化掉建議加 volatile 或者用 NOP 填充。CS 的控制我單獨寫函數(shù)void mram_cs_low(void) { LATCbits.LATC0 0; __delay_us(1); } void mram_cs_high(void) { __delay_us(1); LATCbits.LATC0 1; }那個 1 微秒的延時是為了滿足 CS 建立和保持時間雖然手冊上只要求 5ns但實際 PCB 上走線有電容多等一點更穩(wěn)。3.3 讀寫函數(shù)的完整實現(xiàn)讀函數(shù)void mram_read(uint32_t addr, uint8_t *buf, uint16_t len) { mram_cs_low(); spi_transfer(0x03); spi_transfer((addr 16) 0x07); spi_transfer((addr 8) 0xFF); spi_transfer(addr 0xFF); for (uint16_t i 0; i len; i) { buf[i] spi_transfer(0x00); } mram_cs_high(); }寫函數(shù)void mram_write(uint32_t addr, uint8_t *buf, uint16_t len) { mram_cs_low(); spi_transfer(0x06); mram_cs_high(); mram_cs_low(); spi_transfer(0x02); spi_transfer((addr 16) 0x07); spi_transfer((addr 8) 0xFF); spi_transfer(addr 0xFF); for (uint16_t i 0; i len; i) { spi_transfer(buf[i]); } mram_cs_high(); }注意地址的高 3 位被屏蔽掉了因為 19 位地址最大就是 0x7FFFF。如果你傳的地址超過這個范圍實際會回卷所以上層調(diào)用時要自己保證地址合法。注意WREN 命令和 WRITE 命令之間必須拉高 CS這是 MRAM 的協(xié)議要求。我見過有人把 WREN 和 WRITE 放在同一個 CS 低電平周期里結(jié)果寫不進(jìn)去。手冊上寫得很清楚WREN 之后 CS 必須有一個上升沿寫使能鎖存才會生效。4. 工業(yè)場景下的數(shù)據(jù)存儲策略4.1 日志存儲的環(huán)形緩沖區(qū)設(shè)計工業(yè)設(shè)備日志的特點是寫入頻繁、讀取少、需要保留最近一段時間的數(shù)據(jù)。我在 MRAM 里劃出 256KB 做日志區(qū)用環(huán)形緩沖區(qū)管理。每條日志固定 32 字節(jié)包含時間戳、傳感器數(shù)據(jù)、狀態(tài)碼、校驗和。寫指針和讀指針存在 MRAM 的固定地址里每次寫入后更新寫指針。環(huán)形緩沖區(qū)的容量是 256KB 除以 32 字節(jié)等于 8192 條日志。按每分鐘一條算可以存 5 天多的數(shù)據(jù)。如果設(shè)備每天上傳一次完全夠用。寫指針和讀指針的更新要原子操作我一般先寫數(shù)據(jù)再更新寫指針最后更新校驗。如果斷電發(fā)生在寫數(shù)據(jù)之后、更新指針之前那條日志會被視為無效下次啟動時跳過。4.2 配置參數(shù)的雙備份機(jī)制配置參數(shù)區(qū)我放在 MRAM 的前 4KB分成兩份每份 2KB互為備份。每份包含一個頭部頭部里有版本號、長度、CRC32 校驗。寫入時先寫備份區(qū)再寫主區(qū)。讀取時先讀主區(qū)校驗失敗再讀備份區(qū)。如果兩個都失敗加載默認(rèn)參數(shù)。這個雙備份機(jī)制在工業(yè)現(xiàn)場救過我好幾次。有一次設(shè)備在寫入?yún)?shù)時突然斷電主區(qū)的 CRC 校驗失敗但備份區(qū)是完整的設(shè)備啟動后自動恢復(fù)了參數(shù)沒有停機(jī)。4.3 掉電保護(hù)與數(shù)據(jù)完整性MRAM 的最大優(yōu)勢就是掉電不丟數(shù)據(jù)但前提是數(shù)據(jù)已經(jīng)寫進(jìn)去了。在掉電檢測電路觸發(fā)中斷后PIC18 還有幾十毫秒的供電時間足夠把關(guān)鍵數(shù)據(jù)寫進(jìn) MRAM。我一般會在掉電中斷里做三件事保存當(dāng)前狀態(tài)機(jī)快照、更新日志寫指針、寫一個掉電標(biāo)記。下次上電時看到掉電標(biāo)記就知道上次是非正常關(guān)機(jī)可以做相應(yīng)的恢復(fù)處理。提示掉電中斷里不要做復(fù)雜計算只做最必要的寫入。MRAM 寫入速度快但 SPI 傳輸需要時間32 字節(jié)的數(shù)據(jù)在 4MHz SPI 下需要約 72 微秒加上命令開銷不到 100 微秒。只要供電電容夠大完全來得及。5. 調(diào)試過程中踩過的坑與排查方法5.1 SPI 讀出來全是 0xFF 或 0x00這是最常見的問題。0xFF 通常表示 MISO 線一直高電平0x00 表示一直低電平。排查順序先確認(rèn) CS 有沒有正常拉低。用示波器看 CS 引腳如果一直是高說明 GPIO 配置錯了。確認(rèn) SCK 有沒有波形。如果沒有檢查 MSSP 模塊有沒有使能SSPM 位有沒有設(shè)對。確認(rèn) MISO 線有沒有接對。MR25H40CDF 的 SDO 接 PIC 的 SDI很多人會接反。確認(rèn) MRAM 的供電和地有沒有接好。我遇到過因為地線虛焊導(dǎo)致讀出來全是 0xFF 的情況。5.2 寫入后讀出來數(shù)據(jù)不對寫入不成功的原因通常有幾個WREN 命令沒有正確發(fā)送或者 WREN 和 WRITE 之間沒有 CS 上升沿。地址超出了 19 位范圍導(dǎo)致回卷到錯誤位置。狀態(tài)寄存器的塊保護(hù)位被設(shè)置了目標(biāo)地址被保護(hù)。SPI 模式不對MRAM 支持 Mode 0 和 Mode 3如果 PIC 配成 Mode 1 或 Mode 2數(shù)據(jù)會錯位。我一般會寫一個測試函數(shù)先往地址 0x00000 寫 0x55再讀出來對比。如果不對再往 0x7FFFF 寫 0xAA讀出來對比。這樣可以快速定位是地址問題還是數(shù)據(jù)問題。5.3 高速 SPI 下的數(shù)據(jù)錯誤當(dāng) SPI 時鐘超過 8MHz 時如果 PCB 走線太長或者沒有阻抗匹配容易出現(xiàn)數(shù)據(jù)錯誤。解決方法降低 SPI 時鐘到 4MHz 測試如果正常說明是信號完整性問題。在 SCK 和 MOSI 上串 22 歐姆到 33 歐姆的電阻減少反射??s短走線盡量讓 PIC 和 MRAM 靠近。在 MRAM 的 VCC 引腳旁邊加 0.1uF 和 1uF 的退耦電容。5.4 常見問題速查表現(xiàn)象可能原因排查方法讀出來全是 0xFFMISO 未連接或 CS 未拉低示波器看 CS 和 MISO 波形讀出來全是 0x00MISO 對地短路或 MRAM 未供電萬用表測 MISO 對地電阻寫入后數(shù)據(jù)不變WREN 未生效或塊保護(hù)讀狀態(tài)寄存器看 WEL 和 BP 位數(shù)據(jù)錯位SPI 模式不對確認(rèn) CPOL0, CPHA0高速下偶發(fā)錯誤信號完整性降速測試加串阻地址回卷地址超過 0x7FFFF檢查地址計算邏輯6. 性能實測與優(yōu)化建議6.1 讀寫速度實測數(shù)據(jù)我在 PIC18LF47K40 上跑了幾個測試系統(tǒng)時鐘 64MHzSPI 時鐘 8MHz結(jié)果如下操作數(shù)據(jù)量耗時單字節(jié)寫1 字節(jié)約 8 微秒32 字節(jié)寫32 字節(jié)約 45 微秒512 字節(jié)寫512 字節(jié)約 520 微秒單字節(jié)讀1 字節(jié)約 6 微秒512 字節(jié)讀512 字節(jié)約 515 微秒寫操作比讀操作多了一個 WREN 命令和 CS 翻轉(zhuǎn)所以單字節(jié)寫比單字節(jié)讀慢一點。但批量寫的時候WREN 的開銷被攤薄了512 字節(jié)寫的耗時和讀差不多。6.2 功耗優(yōu)化MR25H40CDF 的待機(jī)電流很低典型值 10 微安左右。寫操作時電流會升到幾毫安但持續(xù)時間很短。對于電池供電的設(shè)備我一般會在不訪問 MRAM 時把 CS 拉高讓 MRAM 進(jìn)入待機(jī)模式。PIC18 的 SPI 模塊在不使用時也可以關(guān)閉進(jìn)一步降低功耗。實測下來一個每分鐘寫一次日志的設(shè)備MRAM 的平均功耗不到 1 微安對整體功耗幾乎沒有影響。6.3 代碼優(yōu)化技巧把常用的讀寫函數(shù)放在 RAM 里執(zhí)行減少 Flash 訪問時間。用 DMA 或者中斷驅(qū)動的 SPI 傳輸減少 CPU 等待時間。PIC18LF47K40 沒有 DMA但可以用 SPI 中斷。批量讀寫時盡量一次傳輸多個字節(jié)減少 CS 翻轉(zhuǎn)次數(shù)。地址計算用位操作代替乘除法PIC18 的乘除法比較慢。提示如果你用的是 XC8 編譯器記得把優(yōu)化等級開到 -O2 或者 -O3否則 SPI 傳輸函數(shù)里的循環(huán)會生成很冗余的代碼。我對比過優(yōu)化前后 512 字節(jié)寫的耗時差了將近 30%。7. 這套方案還能怎么擴(kuò)展MR25H40CDF 加 PIC18LF47K40 的組合本質(zhì)上是一個小型的非易失性數(shù)據(jù)存儲系統(tǒng)。如果你需要更大的容量可以并聯(lián)多顆 MRAM用不同的 CS 控制。如果需要更高的可靠性可以在 MRAM 里做三副本存儲每次寫入寫三份讀取時投票。如果需要和上位機(jī)通信可以在 PIC18 上跑一個簡單的 Modbus 協(xié)議把 MRAM 里的數(shù)據(jù)映射成寄存器。我在一個項目里把 MRAM 分成了四個區(qū)參數(shù)區(qū)、日志區(qū)、緩存區(qū)、固件備份區(qū)。固件備份區(qū)存的是 PIC18 的固件鏡像配合 Bootloader 可以實現(xiàn)遠(yuǎn)程升級。升級失敗時Bootloader 從 MRAM 里讀回舊固件恢復(fù)運行。這個方案在工業(yè)現(xiàn)場很實用因為現(xiàn)場設(shè)備拆機(jī)燒錄的成本太高。最后分享一個小技巧MRAM 的寫壽命幾乎是無限的但 SPI 接口的 CS 引腳有機(jī)械壽命限制如果你用繼電器控制 CS那壽命就受限了。所以 CS 一定要用 GPIO 直接控制不要經(jīng)過任何機(jī)械開關(guān)。另外MRAM 的數(shù)據(jù)保持時間在 85 度下是 10 年如果設(shè)備工作在更高溫度建議定期刷新數(shù)據(jù)比如每年重寫一次關(guān)鍵參數(shù)區(qū)。