EEPROM I²C i Flash STM32: diagnostyka, wear leveling i bezpieczne zapisy
/pomiary/eeprom-flash-bledy-retry-fallback-agd • STM32F1/F4 • 24C32/24C64 • emulacja EEPROM w Flash
1. Zakres i cel
Stabilizacja zapisu/odczytu konfiguracji w AGD: obsługa NACK/ARLO, zapisy stronicowe EEPROM, emulacja EEPROM w Flash STM32, wear leveling i mechanizmy spójności odporne na zanik zasilania.
2. Objawy i sygnatury
2.1 Telemetria
[09:14.207] I2C1: SR1=AF NACK @addr=0xA0 page=3 [09:14.211] I2C1: ARLO=1 podczas kolizji z czujnikiem NTC [09:14.430] CFG: CRC mismatch (slot=B) → fallback to A [09:14.612] FLASH: erase fail @0x0803C000 RETRY#1 OK
2.2 Wzorce
- NACK przy przekroczeniu granicy strony lub zbyt krótkiej pauzie write-cycle.
- Utrata arbitrażu (ARLO) przy współdzielonej magistrali z innym masterem.
- Konfiguracja uszkodzona po zaniku zasilania podczas zapisu.
Uwaga: Po zapisie do 24Cxx wymagaj ACK polling lub opóźnienia >= tWR (typ. 5 ms). Zawsze wyrównuj zapis do granic stron.
3. Logi i zrzuty
I2C1_SR1=0x0400(AF) I2C1_SR2=0x0003(BUSY,MSL) — NACK I2C1_SR1=0x0200(ARLO) — utrata arbitrażu; STOP + backoff FLASH_SR: EOP=1 PGERR=0 WRPRTERR=0 — OK po retry RCC_CSR: BORRSTF=1 — zanik podczas zapisu (sekcja 6)
Wystąpienia BOR korelują z niespójnymi rekordami; wymagane strategie atomowego commitowania.
4. EEPROM I²C — błędy i recovery
| Problem | Przyczyna | Działanie |
|---|---|---|
| NACK po zapisie | czas tWR, przekroczenie strony | ACK polling lub opóźnienie; wyrównaj do page size |
| ARLO/BERR | kolizja, zakłócenia | STOP, reinit, losowy backoff 2–7 ms, max 3 próby |
| BUSY „zawieszone” | SDA trzymane nisko | odzysk zegarem: 9 impulsów SCL w GPIO OD |
| Wear | nadpisywanie tych samych komórek | rotacja stron (log-structured), limit cykli zapisu |
// Page write z ACK polling (24C32/64)
bool eep_write_page(uint16_t addr, const uint8_t* p, uint16_t n){
uint16_t page = 32; // dopasuj do układu
uint16_t chunk = page - (addr % page);
if(n < chunk) chunk = n;
if(i2c_tx_eeprom(addr, p, chunk) != OK) return false;
// ACK polling
for(int i=0;i<50;i++){ if(i2c_probe(EEP_ADDR)==OK) break; delay_ms(1); }
return (n==chunk) ? true : eep_write_page(addr+chunk, p+chunk, n-chunk);
}
// Polityka retry + backoff
for(int t=0;t<3;t++){
if(eep_xfer()==OK) break;
delay_ms(2 + (rand()%6)); // 2–7 ms
}
Praktyka: Buforuj rekord z nagłówkiem {magic, ver, len, crc, seq}, zapisuj sekwencyjnie w obszarze logu i kończ commit markerem.
5. Wewnętrzna Flash — emulacja i wear leveling
5.1 Założenia
- Obszar „EEPROM emu” w ostatnich sektorach Flash.
- Rekordy append-only z numerem sekwencji i CRC.
- Kopiowanie żywe (GC) do świeżego sektora po zapełnieniu.
5.2 Różnice F1 vs F4
- F1: kasowanie stron 1–2 kB; uważaj na przerwania.
- F4: sektory o zmiennym rozmiarze; blokuj tylko krótko BASEPRI.
// Rekord w Flash (packed)
typedef struct __attribute__((packed)){
uint16_t magic; // 0xEE7A
uint8_t ver; // format
uint8_t len; // długość payload
uint32_t seq; // monotonic
uint32_t crc; // CRC32 payload
uint8_t data[]; // payload
} rec_t;
// Zapis „append-only” (szkic)
bool flash_log_append(const void* data, uint8_t len){
rec_t hdr = {.magic=0xEE7A, .ver=1, .len=len, .seq=next_seq(), .crc=crc32(data,len)};
size_t need = sizeof(rec_t) + len;
if(space_left() < need) garbage_collect(); // przeniesienie najnowszych kluczy
flash_prog(cur_ptr, &hdr, sizeof(hdr));
flash_prog(cur_ptr+sizeof(hdr), data, len);
flash_prog(commit_ptr(), "\xA5", 1); // znacznik commit
return true;
}
// Garbage collection (idea)
void garbage_collect(void){
// skanuj od końca: dla każdego klucza skopiuj rekord o najwyższym seq
// po sukcesie: erase starego sektora
}
Uwaga: Programuj słowami zgodnie z wymaganiami rodziny; przed zapisem sprawdź, czy obszar jest skasowany (0xFF). Erase tylko poza ISR.
6. Spójność danych: journal, A/B i CRC
- Journal (append-only): każdy zapis to nowy rekord; ostatni z ważnym commit markerem jest aktywny.
- A/B config: dwa sloty konfiguracyjne; pisz do nieaktywnego, na końcu atomowy flip.
- CRC/wersjonowanie: CRC32 payload + magic + wersja struktury.
- Power-fail-safe: PVD na progu ~2.9–3.0 V; blokuj nowe zapisy poniżej progu.
// PVD guard podczas zapisu
bool cfg_commit_safe(const void* cfg, size_t len){
if(!pvd_is_ok()) return false; // napięcie zbyt niskie
if(!storage_begin()) return false; // lock + przygotowanie
bool ok = storage_append(cfg,len); // EEPROM lub Flash
storage_end();
return ok;
}
// Walidacja po starcie
bool cfg_load(void){
if(load_journal_last_valid(&cfg)) return true;
if(load_slot_A(&cfg) && crc_ok(&cfg)) return true;
if(load_slot_B(&cfg) && crc_ok(&cfg)) return true;
return load_factory_defaults(&cfg);
}
Praktyka: Ogranicz częstotliwość zapisów (np. debounce 2–5 s) i łącz wiele zmian w jeden commit.
7. Kryteria akceptacji
| Kryterium | Cel | Po korektach |
|---|---|---|
| NACK/ARLO na 10⁵ transakcji | <0.2% | 0.05–0.12% |
| Uszkodzone rekordy po BOR (test 500 cykli) | 0 | 0 |
| Awaryjne powroty do „factory defaults” | 0 | 0 |
| Średni czas zapisu konfigu | <8 ms (EEPROM) | ~5–7 ms |
8. Uwagi serwisowe
Konfigurację zapisuj wyłącznie po udanym cyklu sterowania. Rejestruj przyczyny resetów (RCC_CSR) i licznik nieudanych commitów. Dla długich wiązek I²C dobierz podciąganie do pojemności pętli; rozważ przeniesienie wrażliwych zapisów do Flash z journalingiem.