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

ProblemPrzyczynaDziałanie
NACK po zapisieczas tWR, przekroczenie stronyACK polling lub opóźnienie; wyrównaj do page size
ARLO/BERRkolizja, zakłóceniaSTOP, reinit, losowy backoff 2–7 ms, max 3 próby
BUSY „zawieszone”SDA trzymane niskoodzysk zegarem: 9 impulsów SCL w GPIO OD
Wearnadpisywanie tych samych komórekrotacja 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

  1. Journal (append-only): każdy zapis to nowy rekord; ostatni z ważnym commit markerem jest aktywny.
  2. A/B config: dwa sloty konfiguracyjne; pisz do nieaktywnego, na końcu atomowy flip.
  3. CRC/wersjonowanie: CRC32 payload + magic + wersja struktury.
  4. 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

KryteriumCelPo korektach
NACK/ARLO na 10⁵ transakcji<0.2%0.05–0.12%
Uszkodzone rekordy po BOR (test 500 cykli)00
Awaryjne powroty do „factory defaults”00
Ś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.