FM24C04A külső FRAM használata

Előbb-utóbb kevésnek fog bizonyulni az ATmega328 memória készlete, ezért rákényszerülünk majd külső memóriák használatára. Ha a működési sebesség nem kritikus, akkor az I2C buszon kommunikáló külső memória chip-ek vagy modulok a legkényelmesebbek. Ha nem szeretnéd a tárolt adatokat a tápfesz kikapcsolásakor elveszíteni, akkor eeprom vagy fram memóriát kell használnod. Hogy eldönthesd mikor melyiket célszerű használnod, ismerd meg a memória típusok működését.

Készítettem egy bonyolult programot, ami hőmérséklet mérési eredményeket állított elő 3 másodpercenként. A mérési eredményeket átlagoltam, azaz a memóriába 3 másodpercenként írtam újra az adatokat. Végül az átlag értékeket óránként egy SD kártyára írtam. Elméletben minden tökéletesen működött. Azonban a program időnként (havonta egy-két alakalommal) lefagyott. Nem komoly probléma egy amatőr szerkezet esetén, nyomtam egy reset gombot és minden ment tovább. Azonban nagyon bosszantott, hogy folyamatosan figyelni kellett a szerkezetre. A lefagyást okozó körülményt nem sikerült megtalálni, pedig a programban minden lehetséges részt többször is átalakítottam. Végül feladtam, és elhatároztam, hogy a watcdog áramkörre bízom az újraindítást. Ha a program lefagy, a vatchdog újraindítja, és észre sem veszem, hogy történt valami. Az egy órás intervallumban a mérési eredmények átlagolása rövidebb ideig tart ugyan, de még így is jobb a helyzet, mintha esetleg órákig nem veszem észre a lefagyást, és nem tárolok egyáltalán mérési eredményeket. Gondolkodni kezdtem, hogyan lehetne ezt a problémát megoldani. Az ATmega328 EEPROM memóriája erre alkalmatlan, mert a 100ezer írást nagyon gyorsan elhasználom. Első ötletem, hogy keresni kell egy másik eszközt az adattárolásra. Nézzük csak, mennyi írásról van szó? Három másodpercenként mérek és tárolok, azaz 20 írás percenként. Ez napi 28000 írás, ez évi 10 millió. Ha legalább 10 évig szeretném használni az eszközömet, akkor 100millió írásra alkalmas eszközt kell keresni, vagy tudomásul kell venni, hogy havonta cserélni kell a memória elemet (EEPROM esetén). Esetleg szóba jöhet egy SRAM, amit elemmel vagy akkumulátorral védek az áramszünetek okozta adatvesztéstől.

Melyik az az eszköz, ami elvisel 100 millió írást? A fenti ismertető alapján az FRAM a nyerő. Nyilván elég lusta vagyok hozzá, hogy akkumulátort építsek egy ram-ra, így SRAM-ot nem fogok használni. Az adatlap alapján 100 éves használati időre számíthatok, így rögvest meg is rendeltem 5db-ot 250Ft-ért. Azonban csak felület szerelt kivitelben létezik, ezért kénytelen voltam hozzá rendelni áramköri lemezt (350Ft 10db), amire a rendelkezésemre álló durva eszközökkel is van remény, hogy beforraszthatom. Féltem a forrasztástól, de mind az öt ramot sikerült hibátlanul beépíteni. Az eredmény így néz ki forrasztás után:

Természetesen létezik hozzá könyvtár. Nem is igazán kell kifejezetten FRAM-hoz való könyvtár, hiszen az EEPROM és a statikus ram is ugyanúgy működik. Természetesen most a kicsi kapacitású eszközökről beszélek, azokról melyeknek a memória címe 8 bites. Bár létezik FRAM-ból is sokkal nagyobb (pl. 256kbit), azok címzése már 16 bites, pont úgy mint a hasonló méretű EEPROM-ok esetében. A címzés egyébként csak annyit jelent, hogy az I2C buszon az eszköz megszólítása után, egy vagy két byte-on a címet kell kiírni az eszközbe, és ezt követően lehet írni vagy olvasni a tartalmat. Ennél a típusnál a cím 8 bites, tehát egy I2C busz írási művelettel kiküldjük a címet, és jöhetnek az adatok.

Az I2C busz címet két chip kivezetéssel összesen négyféle címre tudjuk beállítani. Így összesen négy eszközt köthetünk a buszra. Mivel egy FM24C04B FRAM 512byte kapacitású, a két külső cím-vezeték mellett van egy harmadik cím bit is, amivel az FRAM-on belül tudunk a két 256 byte-os lap közül választani. Tehát az eszköz címzése így néz ki:

Ha portscenner programot futtatunk, akkor két címen is válaszol, a decimális 80 és 81 címeken (Ha az A2 és A1 földre van kötve)! Gyakorlatilag olyan, mintha két eszközünk is lenne az I2C buszon, csak ezt egyetlen tokba építették össze. Egy címen 256 byte memóriát tudunk címezni, a két ezközből jön össze az 512 byte (4Kbit). Mivel két hardveresen beállítható címvezeték van, összesen négy FRAM chip-et lehet az I2C buszhoz kapcsolni. Tehát 2Kbyte (16Kbit) memóriát tudunk így kialakítani. Adatgyűjtésre nagyon kevés, tehát marad ilyen esetben az SD kártya, de átmeneti mérési eredmények tárolásához nagyos sok. Ha elveszti a rendszer a tápfeszültséget, akkor minden tárolt adat megmarad, és ez nagy előny lehet. Gyakorlatilag a programban rendelkezésre álló ram területet tudjuk vele kiterjeszteni, de az alérés a belső ram-hoz képest csigalassú!

Találtam egy kifejezetten FM24C04A FRAM-hoz készült könyvtárat, de nem tetszett. Az volt vele a bajom, hogy külön kellett a 256 byte-os lap címeket beállítani, ráadásul a byte, int és long típusú változókra külön függvényeket készített a könyvtár írója. Ez persze nem baj, csak kicsit kényelmesebbnek éreztem, ha egy függvényem lesz az olvasásra, egy pedig az írásra. Nem akartam a lap címzésekkel foglalkozni, egyben szerettem volna kezelni a 0 – 511 címtartományt. Így aztán csináltam magamnak egy kombinált függvényt, ami mindig long típusban adja vissza a ram-ból kiolvasott értéket. A függvényeknek van egy változó típus paramétere, amivel beállítható, hogy hány bájton tárolja az adatot, amit megadtunk, illetve hány bájtról olvassa ki az adatot a megadott kezdőcímtől. A függvény igen buta jelenleg, nem ellenőrzi az I2C buszon zajló dolgokat, hibákat nem kezel. Funkcionálisan jól működik, a példa programban igyekeztem minden értékhatárra egy-egy mintát elhelyezni.

A példa programot még fejlesztgetem. Hamarosan gyakorlatban is használni fogom, és akkor még biztosan javítgatok rajta. Most még lehetnek benne hibák is!

#include <Wire.h>

//Az i2c címet az A2 és A1 kivezetések állapota határozza meg
//Például, ha A2 = VCC és A1 = GND, írjunk: FRAM fram (0b10);
byte A0A1=0b00;
byte _i2cAddress= (0b101000 | A0A1)<<1;
byte Page=0;

              
void setup() {
Wire.begin();  
  Serial.begin(9600);
  Serial.println("indul...");
  //Írás a 0. lapra a ramban
  fram_write(0,0,1);             //byte 0
  fram_write(1,255,1);           //byte 255
  fram_write(10,0,2);            // Int 0
  fram_write(12,-32768,2);       //Int -32768 
  fram_write(14,32767,2);        //Int 32767
  fram_write(20,0,3);            //Long 0
  fram_write(24,-2147483648,3);  //Long -2,147,483,648
  fram_write(28,2147483647,3);   //Long 2,147,483,647
  //Írás a 1. lapra a ramban
  fram_write(256,0,1);           //byte 0
  fram_write(257,255,1);         //byte 255
  fram_write(266,0,2);           // Int 0
  fram_write(268,-32768,2);      //Int -32768 
  fram_write(270,32767,2);       //Int 32767
  fram_write(272,0,3);           //Long 0
  fram_write(276,-2147483648,3); //Long -2,147,483,648
  fram_write(280,2147483647,3);  //Long 2,147,483,647
  //olvasás a 0. lapról 
  Serial.print("Byte visszaolvasva 0. page (0 - 255):");
  Serial.print(fram_read(0,1));
  Serial.print(" - ");Serial.println(fram_read(1,1));
  Serial.print("Int visszaolvasva 0. page (min >> 0 >> max):");
  Serial.print(fram_read(12,2));
  Serial.print(" >> ");Serial.print(fram_read(10,2));
  Serial.print(" >> ");Serial.println(fram_read(14,2));
  Serial.print("Long visszaolvasva 0. page (min >> 0 >> max):");
  Serial.print(fram_read(24,3));
  Serial.print(" >> ");Serial.print(fram_read(20,3));
  Serial.print(" >> ");Serial.println(fram_read(28,3));
  //olvasás a 1. lapról 
  Serial.print("Byte visszaolvasva 1. page (0 - 255):");
  Serial.print(fram_read(256,1));
  Serial.print(" - ");Serial.println(fram_read(257,1));
  Serial.print("Int visszaolvasva 1. page (min >> 0 >> max):");
  Serial.print(fram_read(266,2));
  Serial.print(" >> ");Serial.print(fram_read(268,2));
  Serial.print(" >> ");Serial.println(fram_read(270,2));
  Serial.print("Long visszaolvasva 1. page (min >> 0 >> max):");
  Serial.print(fram_read(276,3));
  Serial.print(" >> ");Serial.print(fram_read(272,3));
  Serial.print(" >> ");Serial.println(fram_read(280,3));
}

void loop() {
  
}

long fram_read(int fram_cim, byte fram_adat_tipus) {
// fram_cim 0-511
// fram_adat_tipus 1-byte,2-int,3-long
byte page=0;
if (fram_cim<256) {page=0;} else {page=1;fram_cim=fram_cim-256;}
    Wire.beginTransmission(_i2cAddress | (page&1));
    Wire.write(fram_cim);
    Wire.endTransmission(); 
    switch (fram_adat_tipus) {
    case 1:
      Wire.requestFrom(_i2cAddress | (page&1),1);
      return Wire.read();
      break;
    case 2:
      Wire.requestFrom(_i2cAddress | (page&1),2);
      return int(Wire.read()) | int(Wire.read())<<8;
      break;
    case 3:
      Wire.requestFrom(_i2cAddress | (page&1),4);
      return long(Wire.read()) | long(Wire.read())<<8 | long(Wire.read())<<16 | long(Wire.read())<<24 ;
      break;
    }
}

void fram_write(int fram_cim, long fram_adat, byte fram_adat_tipus) {
// fram_cim 0-511
// fram_adat long, amit a függvémy szükség szerint byte, vagy int-re alakít
// fram_adat_tipus 1-byte,2-int,3-long
byte page=0;
byte adat0;
byte adat1;
byte adat2;
byte adat3;
if (fram_cim<256) {page=0;} else {page=1;fram_cim=fram_cim-256;}
    Wire.beginTransmission(_i2cAddress | (page&1));
    Wire.write(fram_cim);
    switch (fram_adat_tipus) {
    case 1:
      Wire.write(fram_adat);
      Wire.endTransmission(); 
      break;
    case 2:
      adat0=fram_adat & 0xFF;
      adat1=(fram_adat>>8) & 0xFF;
      Wire.write(adat0);
      Wire.write(adat1);
      Wire.endTransmission(); 
      break;
    case 3:
      adat0=fram_adat & 0xFF;
      adat1=(fram_adat>>8) & 0xFF;
      adat2=(fram_adat>>16) & 0xFF;
      adat3=(fram_adat>>24) & 0xFF;
      Wire.write(adat0);
      Wire.write(adat1);
      Wire.write(adat2);
      Wire.write(adat3);
      Wire.endTransmission(); 
      break;
    }
}

Miközben dolgoztam az FRAM megismerésén, gondolkodtam azon is, hogyan tudnám a legkényelmesebben felhasználni programjaimban. Végül is a külső ram azért van, hogy tápfeszültség kiesés esetén is megőrizze a szükséges adatokat. Még egy bonyolult programban sincs sok ilyen adat (legalább is nekem nem sikerült megfelelően bonyolult problémát kitalálnom). Azonban az adatok amiket nem szeretném ha elfelejtene a rendszer, nem egyszerű számok, hanem valamilyen feldolgozott értékek. Pl. a napi hőmérséklet minimuma, maximuma, átlaga, mérések száma stb. Ezért írtam egy olyan függvényt, illetve függvény gyűjteményt, aminek a használatakor nem kell foglalkoznom az adatok értékelésével, közvetlenül a számomra szükséges feldolgozott adatot kaphatom vissza. Talán mások számára is hasznos lehet, ezért a forráskódok menüpontba beraktam ennek a függvény gyűjteménynek a leírását és programját. Ha érdekel, akkor itt megtalálod!

Memória típusok

Mielőtt külső memória használatába kezdünk, érdemes kicsit feleleveníteni a memóriákkal kapcsolatos tudásunkat. Lássuk sorban a jellegzetes memóriák működését és tulajdonságaikat. A DRAM memóriával nem foglalkozok, mert az Arduino környezetben nem feltétlenül praktikus, inkább a számítógépek világában elterjedt típus.

SRAM: (pl. PCF8570). A memóriában az egyes bitek tárolását minimum 6 tranzisztorral megvalósított logikai (flip-flop) áramkör tárolja. A memória élettartama gyakorlatilag végtelen, azonban tápfeszültség kikapcsoláskor elveszti a tartalmát. Az adatlap akkumulátoros táplálást javasol a következő áramköri megoldással:

Az SRAM sebességével sem lehet kifogás. Az áramkörök átbillenéséhez néhány nanosec szükséges, így ez a ram típus nagy sebességű felhasználásokra is alkalmas. Pl. az ATmega chip-be épített ram is ilyen elven működik. Ebben a ram-ban tárolja a program változókat futás közben. Mivel egyetlen bit tárolásához elég sok tranzisztor kell, nagy a chip-ben elfoglalt alapterület, így ez a ram nem gyártható gazdaságosan nagy kapacitásban. Persze felmerül a kérdés, hogy mi a „nagy” kapacitás. A mai világban azt hiszem a gigabyte már nagynak számít!

EEPROM: (pl. 24LC04B): Egyetlen bit tárolásához két tranzisztor szükséges. A tároló cella egy CMOS tranzisztor, aminek a vezérlő gate elektródája felett egy szigetelő rétegben kialakított térrészbe kerül programozáskor elektron többlet. Ha van elektron a térrészben az 1, ha nincs az 0. Törléskor az elektronokat fordított feszültséggel eltávolítják ebből a szigetelőanyaggal körülvett „eltemetett” rétegből. Sajnos a töltések beinnyektálása és kiszivattyúzása közben egyre több elektron esik csapdába a szigetelő anyagban is, és ezek egy idő után olyan mértékben halmozódnak fel, hogy már nem lehet a 0 és 1 állapotot megkülönböztetni. Ekkor az EEPROM cella már nem használható tovább. Ez általában 100 ezer és 1 millió írás környékén következik be attól függően, milyen vékony a szigetelő anyag. Nyilván a vékonyabb szigetelő rétegű EEPROM jobb minőségű és többször írható. Mondanom sem kell, hogy az EEPROM tápfeszültség kikapcsolásakor is megtartja a tárolt információt, hiszen az eltemetett rétegben tárolt elektronok maguktól nem tudnak kijönni, Illetve idővel mégis megszöknek, de ez kb. 10 év után eredményezi a tárolt információ megváltozását. Tehát egy EEPROM-ban nem célszerű fiatalkori fényképeinket archiválni, mert unokáink már nem tudják majd kiolvasni és megnézni, milyen szépek is voltunk valaha. AZ USB pendrive-ok is EEPROM cellákban tárolnak, így javasolt néhány évente újra írni bennük a tárolt információt. Nagy előnye az EEPROM típusú ramnak, hogy 3,3V-os és 5V-os rendszerekhez is közvetlenül illeszthető. Az ATmega chipekben is találunk EEPROM memóriát. Ez gyengébb minőségű, kb. 100ezer újraírást visel el. Meg kell még említeni a sebességét is. Sajnos az EEPROM nem egy gyors memória néhány milisec egy cella megírása. Nyilván egy byte-ot párhuzamosan írnak, de már néhány kbyte adat tárolása is tetemes időt vesz igénybe. Minél több cellát írunk egyszerre, annál gyorsabb lesz a EEPROM működése, így érdemes kompromisszumot kialakítani. Egy ilyen kompromisszum eredménye a flash memória.  

FLASH MEMÓRIA: Ez tulajdonképpen az EEPROM speciális változata. A tároló cellákat blokkokba szervezik, és egy-egy blokkot egyszerre lehet írni és törlni. Ettől az írás sebessége nagyon nagy lehet. Azonban a blokkban található tároló elemek esetlegesen akkor is törlésre és írásra kerülnek, ha az adott cellát éppen nem akarjuk írni. Így aztán a flash memóriát inkább nagyobb állományok tárolására célszerű használni. Nem is véletlen, hogy pl. az USB pendrive-ok és SD kártyák flash memóriát tartalmaznak.

SD kártya: Ez valójában egy flash memória, így nem meglepő, hogy átlagosan 100.000 újraírást garantálnak a gyártók. Nem is kell több, hiszen fényképezőgépekkel, mp3 lejátszókkal ennek töredékét sem tudjuk elhasználni. Óriási tároló kapacitásúak, ezért adatgyűjtésre, adatrögzítésre ideálisak. Arduino amatőrök számára nagy hátrány (vagy épp előny), hogy a rendelkezésre álló SD kártya kezelő program könyvtárak fileok kezelésére lettek kialakítva. Egy SD kártyára nem lehet csak egyszerűen adatokat írni, állományokat (pl. txt vagy csv) kell létrehozni, és azok tartalmába lehet adatokat beleírni. Visszaolvasáskor meg kell nyitni az állományt és meg kell keresni az adatot. Sok esetben ez a cél, de a program meglehetősen bonyolult tud lenni. Ráadásul az SD kártya kezelő függvények rengeteg program memóriát esznek meg (kb. 10Kbyte), tehát egy-egy mérési adat vagy program paraméter tárolására ez nem igazán jó. Ha sok-sok mérési eredményt akarunk valahol rögzíteni, akkor megtaláltuk az igazi eszközt.

F-RAM: (pl. FM24C04A). Az írások száma (100.000.000.000.000)  10 a tizennegyediken. Működési elve szerint a tároló elem egy feroelektromos hatással megspékelt kondenzátor (nem mágneses elvű, mágneses térre nem érzékeny!!). Ennek a kondenzátornak a fegyverzetei között egy ólom-cirkonát-titanát réteget alakítanak ki (nyugi, én is olvastam, nem magamtól tudok ilyen okos dolgokat). Ebben a rétegben a molekulák képesek az elektromos tér irányába befordulni, tehát az anyag emlékezik. Amikor feszültséget kapcsolunk egy emlékező kondenzátorra, és a molekulák „befordult” állapotban vannak (1-et tárol a tároló elem), a molekulák visszafordulásához plusz energia kell. Ilyenkor a kondenzátorba befolyó áram egy extra áram impulzust tartalmaz. Ha a molekula éppen fordítva állt (0-át tárol), akkor nincs áram impulzus. Természetesen a kiolvasással azonnal el is veszítettük a tárolt információt, tehát minden egyes kiolvasás után vissza kell írni a z információt a kondenzátorba, ami egy megfelelő feszültség rákapcsolásával történik meg. Ha 0 volt a kondenzátorban tárolt érték, akkor természetesen nem kell csinálni semmit, csak akkor kell vissza írni, ha a 1 volt a tartalom. Ennek a RAM típusnak hátránya lehet, hogy 4V és 5,5V közötti feszültség tartományban működik, így 3,3V-os rendszerekben nem lehet használni, vagy szintillesztőre lesz szükség, és külön 5V-os tápegységre. Olvastam valahol, hogy már kifejlesztették a 3V-on is működő FRAM típust, de még nem találkoztam vele. Az FRAM nagyon gyors. Közel az SRAM sebességével képes működni, így nagyon ígéretes típus, de még nehézségekbe ütközik a nagy méret gyártása. Ennek oka, hogy a kondenzátornak a térfogatát nem lehet tetszőlegesen kicsire készíteni, mert ha csökkentjük a befordulásra képes molekulák számát, csökken az áramimpulzus nagysága is, amit viszont érzékelni kell.
Érdekes történelmi analógia, hogy a távoli múltban a számítógépek hőskorában egy hasonló elven működő memória típussal kezdődött a ram-ok története. Ferritgyűrűs memóriának hívták. 1-2mm átmérőjű ferrit gyűrűkön vezettek át vezetékeket, és áram impulzusokkal felmágnesezték az a gyűrűt. Kiolvasáskor érzékelték, hogy mágneses volt, vagy sem. 1Kbyte memória kapacitás egy utazó bőrönd méret. Minden bizonnyal azért lett a Pentagon a világ legnagyobb épülete, mert kellett az atomrakéták irányításához 10Mbyte memória!
F-RAM-ot már használtam. Készítettem is hozzá egy univerzális függvényt, amit különféle tároló cella típusok létrehozására lehet használni. Lehetséges “beledobálni” az F-RAM-ba értékeket, és kiolvasáskor a függvény visszaadja az értékek maximumát, minimumát átlagát. Talán másoknak is felmerülnek hasonló feladatai mint nekem.