← Toate poveștile

Un ring buffer în flash: cum un gateway off-grid nu pierde date cât timp doarme

Într-o stupină nu există curent electric, deci gateway-ul trebuie să stea stins aproape toată noaptea. Cum un ring buffer nevolatil în flash păstrează telemetria stupilor, supraviețuiește repornirilor și nu șterge nimic înainte de confirmarea din cloud.

Un ring buffer în flash: cum un gateway off-grid nu pierde date cât timp doarme

Într-o stupină nu există o priză de curent. E o frază banală și decide bună parte din arhitectura bjtOS. Din când în când te obligă chiar să redeschizi alegeri care păreau închise: exact asta s-a întâmplat când am abordat buffering-ul gateway-ului, adică cutia care adună datele tuturor stupilor dintr-o stupină și le trimite în cloud.

Rezultatul este un ring buffer nevolatil în flash: un buffer circular scris în memorie permanentă, care supraviețuiește opririi și repornirilor. În acest text povestesc de ce e nevoie de el, cum e făcut și lucrul care m-a surprins cel mai mult proiectându-l: că instinctul răspândit al lui “nu scrie prea des în flash, o consumi” este adevărat doar pe jumătate, iar jumătatea care lipsește este chiar cea de care ai nevoie ca să decizi.

Problema: piesa cea mai capabilă este și cea mai flămândă

Gateway-ul are înăuntru două creiere foarte diferite.

Unul este un mic calculator Linux. Știe să vorbească cu rețeaua celulară, să cifreze, să gestioneze cozi, să reîncerce când ceva merge prost. E comod și e și flămând: aprins douăzeci și patru de ore din douăzeci și patru cere aproape zece wattori într-o noapte de iarnă.

Celălalt este un microcontroler cu radio, cel care stă în ascultarea nodurilor din stupi. Consumă vreo patruzeci de miliwați, cu un ordin de mărime mai puțin, și poate rămâne aprins la nesfârșit fără să apese pe bilanțul energetic.

Socoteala e simplă. Dacă vreau un gateway alimentat de un panou solar de dimensiune rezonabilă, așa cum am povestit vorbind despre cât durează un stup pe baterie, calculatorul nu poate sta aprins toată noaptea. Trebuie să stea stins aproape mereu și să se aprindă de două sau trei ori între apus și răsărit ca să trimită totul în bloc.

Bine. Dar senzorii stupilor nu știu că acel calculator doarme. Mesajele radio continuă să sosească toată noaptea. Cineva trebuie să ia notă.

De ce nu ajunge memoria volatilă

Primul răspuns evident e să ții datele în RAM pe microcontroler, care oricum e mereu aprins. E și răspunsul greșit, iar ajungi acolo doar gândindu-te la cum arată cu adevărat o noapte off-grid.

Noaptea panoul nu produce. Bateria cedează, mai ales pe frig, și mai ales când modemul celular cere curent ca să se agațe de rețea. Un brownout, adică o scădere de tensiune care repornește microcontrolerul, nu este un incident exotic de trecut printre eventualitățile îndepărtate: este condiția normală pe care sistemul trebuie să o suporte.

Iar o repornire golește RAM-ul. O noapte întreagă de date dispărută, în tăcere, fără ca nimeni să bage de seamă până a doua zi dimineața.

Deci buffer-ul trebuie să stea în memorie nevolatilă, adică în flash: o memorie care păstrează ce a scris chiar și cu curentul deconectat.

Program și erase nu sunt aceeași operațiune

Aici vine partea care mi-a schimbat felul de a proiecta lucrul acesta.

Cine lucrează cu embedded poartă cu sine un reflex condiționat: nu scrie prea mult în flash, o consumi. E o indicație sănătoasă, dar dacă te oprești acolo ajungi să proiectezi în jurul problemei în loc să treci prin ea.

În flash-ul NOR, cel integrat în microcontrolerele pe care le folosim, există două operațiuni bine distincte:

  • Program, scrierea propriu-zisă. Pune la zero biții necesari, se face în bucățele mici, chiar de multe ori în interiorul aceluiași sector, cu condiția să scrii pe spațiu încă neatins. Nu uzează în mod apreciabil.
  • Erase, ștergerea. Readuce un sector întreg, tipic 4 KB, la toți de unu. Aceasta este operațiunea cu endurance limitat, de ordinul a o sută de mii de cicluri pe sector.

Consecința practică răstoarnă instinctul. A scrie fiecare mesaj în parte imediat ce sosește nu e o risipă: e un program, costă puțin și face buffer-ul rezistent la căderile de tensiune, pentru că dacă microcontrolerul moare o milisecundă mai târziu data e deja acolo. Ce trebuie numărat nu este numărul de scrieri, ci numărul de ture pe care buffer-ul le face pe memorie, pentru că la fiecare tură se plătește o ștergere pe sectoarele reciclate.

E o distincție care permite să alegi comportamentul cel mai prudent, adică scriu imediat și mereu, fără să îl plătești în durata de viață a hardware-ului.

Un inel, doi indici și capacitatea de a se reconstrui

Structura buffer-ului este cea mai veche din meserie: un buffer circular, sau ring buffer. Un cap care avansează scriind, o coadă care avansează citind, un inel de căsuțe pe care se rotesc amândouă. Când capul ajunge la fund repornește de la început, refolosind spațiul deja citit. Este o FIFO de dimensiune fixă: nu crește niciodată, nu fragmentează niciodată și are o limită superioară de ocupare cunoscută din start. Pe un dispozitiv care trebuie să funcționeze singur ani de zile, previzibilitatea valorează mai mult decât eleganța.

Fiecare înregistrare scrisă poartă un antet minimal: un marcator ca să fie recunoscută, lungimea și un CRC, adică un cod de control care spune dacă conținutul este integru. Servesc pentru momentul cel mai rău, care este repornirea.

La pornire microcontrolerul nu se încrede în nimic: rescanează partiția și reconstruiește capul și coada de la zero, citind înregistrările una câte una. Dacă tensiunea a căzut la jumătatea unei scrieri, acea înregistrare nu trece verificarea CRC, este aruncată, iar coada rămâne consistentă. Buffer-ul se repară singur, fără să aibă nevoie ca cineva să îi fi lăsat vorbă ce făcea cu o clipă înainte de a muri.

Peste flash-ul brut se poate așeza un filesystem gândit pentru microcontrolere, care aduce cu sine wear leveling-ul, adică distribuirea uzurii pe toate sectoarele, și rezistența la pierderea de tensiune deja rezolvate. Se plătește puțină scriere în plus pentru metadate, ceea ce la volumele noastre e irelevant, și în schimb eviți să rescrii de mână o clasă de bug-uri pe care alții le-au plătit deja. Gestiunea manuală a partiției brute rămâne opțiunea de rezervă, dacă într-o zi ar fi nevoie de o eficiență mai deterministă.

Numerele, care sunt partea liniștitoare

Să luăm o stupină de douăzeci de stupi, cazul nostru de studiu tipic.

O înregistrare încape în vreo patruzeci de byte, deci într-un sector de 4 KB intră vreo sută. O noapte de iarnă lungă de paisprezece ore, cu ritm de transmisie des, produce ceva de genul cinci sute șaizeci de mesaje: puțin peste douăzeci de kilobyte.

Douăzeci de kilobyte. Pe o partiție căreia îi dedicăm între cinci sute și o mie.

Și atunci de ce atât de largă, dacă vârful e atât de mic? Nu pentru noaptea normală. Pentru noaptea în care modemul nu prinde rețeaua. În acel caz calculatorul se trezește, încearcă, eșuează, iar buffer-ul nu este golit: datele rămân acolo și pleacă la fereastra următoare. Marja nu servește la a conține traficul, servește la a cumpăra zile de autonomie când acoperirea celulară lipsește.

Despre uzură: circa o mie trei sute de înregistrări pe zi fac treisprezece sectoare pe zi. Pe o partiție de un megabyte, adică două sute cincizeci și șase de sectoare, inelul face o tură cam la douăzeci de zile. Ies mai puțin de douăzeci de ștergeri pe sector pe an, față de un endurance de o sută de mii de cicluri. Constrângerea, pur și simplu, nu este aceasta: suntem la o scară pe care hardware-ul nu o va vedea niciodată. O verifici o dată cu o foaie de calcul și apoi încetezi să te uiți la ea.

Merită spus pentru că e genul de socoteală pe care merită mereu să o faci înainte: m-a scutit să optimizez împotriva unei probleme care nu există și m-a lăsat să aleg comportamentul cel mai sigur fără sentimente de vinovăție.

Regula care face totul cinstit: se șterge doar după confirmare

Ultima piesă e cea care transformă un buffer într-o garanție.

Când calculatorul se trezește, microcontrolerul nu îi predă datele ca apoi să scape de ele. I le dă citindu-le din coadă, iar coada avansează doar când sosește confirmarea că acele date au fost acceptate de partea cealaltă, în cloud. Nicio ștergere înainte de confirmare. Niciodată.

Rezultatul este o livrare at-least-once, cel puțin o dată: în cazul cel mai rău același mesaj sosește de două ori, iar în aval este recunoscut ca duplicat și eliminat, pentru că fiecare mesaj poartă cu sine un identificator stabil. Cazul cel mai rău acceptabil este duplicatul. Pierderea nu este.

Cele două cozi lucrează așadar în serie și fiecare are meseria sa. Cea din flash de pe microcontroler acumulează pe termen lung și supraviețuiește calculatorului stins. Cea de pe calculator, care în sistemul nostru există deja și este verificată, gestionează sesiunea individuală către cloud cu reconectările și încercările sale. Este aceeași filosofie cu care s-a născut tot lanțul de transport al bjtOS, pe care am povestit-o în am construit drumul înaintea senzorilor: data nu se pierde niciodată, se acumulează până când există un mod de a o livra.

Ce rămâne deschis

Aș fi necinstit să o povestesc ca pe un lucru încheiat. Acesta este un proiect scris, dimensionat și argumentat, nu încă un cod care rulează într-o stupină. Ce lipsește știu cu precizie și este partea cea mai instructivă:

  • protocolul de descărcare, adică modul în care cele două creiere își vorbesc în timpul transferului, cu marcatorii de început și sfârșit de sesiune și avansarea cozii legată de confirmare;
  • ora, pe care astăzi microcontrolerul o primește de la calculator, care la rândul lui o ia din rețea. Cu calculatorul stins trebuie să și-o păstreze singur între o trezire și alta și să se realinieze când celălalt se reaprinde;
  • traseul urgent: o alarmă antifurt nu poate aștepta fereastra nocturnă, trebuie să aprindă calculatorul imediat. Iar pentru ca receptorul să o poată recunoaște fără să deschidă conținutul mesajului, e nevoie de un bit de prioritate lizibil în antet, care trebuie coordonat cu firmware-ul nodurilor și deci cu contractul wire versionat;
  • watchdog-ul: ce se întâmplă dacă acel calculator se aprinde și nu confirmă niciodată, pentru că a rămas blocat la pornire sau la agățarea de rețea.

Criteriul: ce se întâmplă în momentul cel mai rău

Alegerea de fond însă este luată și rezistă la un criteriu simplu pe care îl folosesc des când proiectez: ce se întâmplă în momentul cel mai rău?

Momentul cel mai rău aici este o noapte de februarie, cu bateria scăzută, cu modemul care nu găsește semnal și cu tensiunea care dansează. În acel moment, undeva, trebuie să existe un caiet pe care e deja scris tot și care nu se șterge când se stinge lumina.

Este același principiu care ghidează tot restul sistemului: un stup nativ digital se proiectează pornind de la constrângeri, nu de la senzori. Dacă te interesează imaginea de ansamblu există cum funcționează un stup smart, iar despre ceea ce construim găsești pagina dedicată tehnologiei și cea produsului.