Ako si uľahčiť ovládanie externej EEPROM pomocou šablón

Zápisník experimentátora

Hierarchy: Externá EEPROM

V tomto článku nadviažeme na predchádzajúci, v ktorom sme si vysvetlili základné použitie externej EEPROM. Navrhneme si triedu, ktorá nám umožní pohodlne ukladať do EEPROM ľubovoľný objekt. Využijeme pritom šablóny jazyka C++ a doplníme do triedy dve funkcie, ktoré budú plniť túto úlohu. Príklad bude pre 24LC16B, ale až na drobnosti bude tento návod univerzálny pre ľubovolnú EEPROM.

Použité súčiastky

Na tento experiment stačí iba pár súčiastok:

  • Arduino Pro Mini (link) - Použil som ho preto, lebo sa zmestí aj na najmenšie skúšobné pole.
  • Breadboard 400 Holes (link) - Na ňom máme dosť miesta na dva mikrospínače.
  • Prevodník CP2102 USB to Serial (link) - Prevodník slúži na naprogramovanie Arduina.
  • Mikrospínače (link, link) - Potrebujeme dva kusy. Do skúšobného pola sa dajú použiť aj štvorpinové aj dvojpinové. Niekedy sú piny dlhé, alebo majú podivné výstupky. Nebojte sa tie konce odstrihnúť, aby sa lepšie dali zastrčiť.
  • EEPROM 24LC16B

Arduino je upravené tak, že má na I2C (nachádza sa netradične na extra pinoch uprostred Arduina) prispájkovanú pin lištu s otvormi, aby sa pomocou dvoch prepojovacích vodičov dalo ľahko prepojiť s EEPROM.

Dve tlačidlá sa používajú na spustenie konkrétnych testov a sú pripojené na piny 6 a 8 na Arduine. EEPROM je pripojená na napájanie a adresové piny A0, A1 a A2 sú nezapojené. To platí ale iba pre tento konkrétny typ EEPROM. Ostatné musia obvykle pripojiť tieto piny na GND alebo na VCC, čím si nastavia I2C adresu. Treba si vždy skontrolovať datasheet ku konkrétnemu typu a nastaviť to podľa neho.

Príklad

Triedy jazyka C++ nám umožňujú používať nielen existujúce triedy, ale vďaka dedičnosti si môžeme z existujúcich tried vytvoriť nové, ktoré doplníme o nové funkcie. V tomto príklade si od triedy extEEPROM (použitej v predchádzajúcom príklade) odvodíme triedu TemplatedEEPROM, ktorá bude obsahovať dve šablónové funkcie.

  • get - Pomocou funkcie naplníme obsah premennej ľubovoľného typu uloženým obsahom z EEPROM.
  • put - Pomocou funkcie uložíme obsah premennej ľubovoľného typu do EEPROM.

V príklade potom použijeme dve testovacie funkcie na otestovanie našich šablónových funkcii. Poďme rovno na komentovaný príklad, kde uvidíme všetky funkcie. Nebudem komentovať obsah súboru dump.h. Funkcie, ktoré sa v ňom nachádzajú, sú popísané napríklad v článku Dump obsahu premennej na sériový port.

Týmito dvomi riadkami hovoríme kompilátoru, ktoré ďalšie súbory chceme do kompilácie zahrnúť. V našom prípade to je knižnica pre externú EEPROM a pomocné výpisy na sériový port.

#include <extEEPROM.h>
#include "dump.h"

Definovaním novej triedy v hlavnom programe som zmiatol parser Arduina, ktorý za vás porobí formálne deklarácie funkcií a preto som musel všetky použité funkcie deklarovať ručne. Vo svete Arduina to musíme málokedy robiť, ale toto bol asi ten príklad. Zmysel tejto operácie v C++ je jednoduchý. Ak je funkcia použitá skôr, ako je známa, kompilátor vyhodí chybu. Preto je potrebné funkcie aspoň formálne popísať, čo kompilátoru na preklad stačí a linker si už s vytvorením výsledného programu ľahko poradí.

void dumpEEPROM(uint32_t startAddr, uint32_t nBytes);
void eeErase(uint8_t chunk, uint32_t startAddr, uint32_t endAddr);
void eeTemplateWrite();
void eeTemplateRead();

Takto sa zapisuje odvodená trieda. Prvá funkcia je konštruktor, ktorý je v tomto pripade iba zopakovaním pôvodného konštruktora z knižnice externej EEPROM. Ale musí tam byť, pretože inak by som nemohol definovať premennú daného typu. A keď vytvárame odvodené typy, skoro vždy pridávame aj vlastný kód. Táto funkcia bez kódu je skôr výnimkou.

Zvyšné dve funkcie sú šablónové. To znamená, že je to len šablóna skutočnej funkcie s konkrétnym typom, ktorý za nás vytvorí kompilátor. Kód nie je zložitý. Iba si pretypujeme typ T na uint8_t (adresu v RAM referencie na pointer), vypočítame veľkosť v bajtoch funkciou sizeof a po jednom znaku ju pošleme do EEPROM. Pointer this som použil kvôli prehľadnosti, aby ste videli, ktoré funkcie triedy používam na zápis. V C++ to nie je povinné tam dať, ale sú jazyky, ktoré to zase vyžadujú.

class TemplatedEEPROM : public extEEPROM
{
  public:
    TemplatedEEPROM(eeprom_size_t deviceCapacity, byte nDevice, unsigned int pageSize, byte eepromAddr = 0x50)
      : extEEPROM(deviceCapacity, nDevice, pageSize, eepromAddr)
    {}

    template< typename T > T &get( int idx, T &t ) {
      uint8_t *ptr = (uint8_t*) &t;
      for ( int count = sizeof(T) ; count ; --count )
        *ptr++ = this->read(idx++);
      return t;
    }

    template< typename T > const T &put( int idx, const T &t ) {
      const uint8_t *ptr = (const uint8_t*) &t;
      for ( int count = sizeof(T) ; count ; --count )
        this->write(idx++, *ptr++);
      return t;
    }
};

A pretože už máme triedu definovanú, môžeme si definovať premennú typu TemplatedEEPROM, ktorá vie používať aj funkcie pôvodnej triedy, aj naše nové doplnené.

TemplatedEEPROM eep(kbits_16, 1, 16);

Vo funkcii loop iba kontrolujeme stlačenie tlačidla a podľa toho zavoláme funkcie.

void loop(void)
{
  if (digitalRead(btnStart) == LOW) {
    delay(100);
    eeTemplateWrite();
    eeTemplateRead();
  }

  if (digitalRead(btnErase) == LOW) {
    delay(100);
    eeErase(chunkSize, 0, totalKBytes * 1024);
    dumpEEPROM(0, totalKBytes * 1024);
  }
}

Aby sme mali nejaké premenné, zadefinoval som jeden textový reťazec a jednu štruktúru a naplnil ich konkrétnymi hodnotami.

char demo_string[] = "0123456789";
struct _demo_struct {
  int8_t a;
  int16_t b;
  int32_t c;
} demo_struct = {1, 2, 3};

Vo funkcii eeTemplateWrite obsah týchto premenných uložíme na konkrétne miesta do EEPROM.

void eeTemplateWrite()
{
  Serial.println(F(""));
  Serial.println(F("Write test"));

  DUMP(demo_string);
  eep.put(0x0000, demo_string);

  DUMP(demo_struct);
  eep.put(0x0010, demo_struct);
}

Vo funkcii eeTemplateRead si ich zase načítame do lokálnych premenných, aby sme ich obsah mohli poslať na sériový port a porovnať ich s pôvodnými premennými. Všimnite si, že premennú read_string som definoval väčšiu, než potrebujeme na pôvodný obsah. To je obvyklá taktika pri načítávaní reťazcov. Vyčleníme si buffer dostatočnej veľkosti a do neho si napríklad postupne čítame jednotlivé riadky z nejakého zdroja. Buffer musí mať toľko miesta, ako je najväčší textový reťazec, ináč by sme si pri naplnení premennej prepísali údaje cez koniec vyhradeného miesta a následky by boli pravdepodobne fatálne na ďalší beh programu.

void eeTemplateRead()
{
  Serial.println(F(""));
  Serial.println(F("Read test"));
  dumpEEPROM(0, 0x0020);

  char read_string[0x000f];
  eep.get(0x0000, read_string);
  DUMP(read_string);

  _demo_struct read_struct;
  eep.get(0x0010, read_struct);
  DUMP(read_struct);
}

A tu už je len výpis z programu. Vidíte, že všetky premenné sa obsahovo rovnajú a to nám hovorí, že som tie šablónové funkcie napísal správne.

Press button '6' to start write test
Press button '8' to start erase test
Write test
Dump: demo_string
0107 - 30 31 32 33 34 35 36 37 38 39 00 0123456789.
Dump: demo_struct
0100 - 01 02 00 03 00 00 00 .......
Read test
EEPROM DUMP 0x0 0x20 0 32
0x0000 30 31 32 33 34 35 36 37  38 39 00 FF FF FF FF FF
0x0010 01 02 00 03 00 00 00 FF  FF FF FF FF FF FF FF FF
Dump: read_string
08E5 - 30 31 32 33 34 35 36 37 38 39 00 FF FF FF FF 0123456789.....
Dump: read_struct
08C5 - 01 02 00 03 00 00 00 .......

Zdrojový kód

Zdrojový kód sa nachádza na GitHub.


07.04.2017


Menu