How to make it easier to control external EEPROMs using templates

Zápisník experimentátora

Hierarchy: Externá EEPROM

In this article we will continue with previous article, in which we explained the basic use of an external EEPROM. We will design a class that allows us to conveniently store any object in EEPROM. We will use C++ templates and we will add two features to the class that will perform this task. An example will be for 24LC16B, but this tutorial will be universal for any EEPROM.

Used parts

Only a few parts are sufficient for this experiment:

  • Arduino Pro Mini (link) - I used it because it will fit into the smallest breadboard.
  • Breadboard 400 Holes (link) - We have enough space for the two microswitches on it.
  • Converter CP2102 USB to Serial (link) - The converter is used to program the Arduino.
  • Microswitches (link, link) - We need two pieces. Two and four-pins, both can be used in the breadboard. Sometimes the pins are longer, or they have strange protrusions. Do not be afraid to cut off those ends so they can get stuck.
  • EEPROM 24LC16B

The Arduino is designed to have an I2C (extraordinary on extra pins in the middle of Arduin) with an pin header with holes to make it easy to connect with the EEPROM via two wires.

Two buttons are used to run specific tests and are connected to pins 6 and 8 on Arduino. The EEPROM is connected to the power supply and the address pins A0, A1 and A2 are not connected. This only applies to this particular type of EEPROM. Others need to connect these pins to GND or VCC to set the I2C address. You should always check the datasheet for the specific type and set it according to it.

Example

C++ classes allow us to use not only existing classes, but we can build new ones from existing classes and add new functions to them. In this example, we derive the TemplatedEEPROM class that contains two template functions from the extEEPROM class (used in the previous example).

  • get - Using this function, we get the contents of the variable from EEPROM.
  • put - Using this function, we write the contents of the variable into EEPROM.

In the example, we use two test functions to test our template functions. Let's go straight to the commented example where we'll see all the features. I will not comment on the contents of the dump.h. The functions contained therein are described, for example, in the article Dump content of the variable to the serial port.

These two lines tell the compiler to include the other files in the compilation. In our case this is the library for external EEPROM and auxiliary serial port functions.

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

By defining a new class in the main program, I dismissed the Arduino parser, which will make formal declarations of functions for you, and therefore I have to declare all the functions used manually. In the world of Arduino, we rarely have to do this, but this was probably the example. The intent of this C++ operation is simple. If the function is used before it is known, the compiler throws an error. Therefore, it is necessary to formally describe the function. It is enough for compiler for the translation and the linker can easily handle the resulting program.

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

This is how the derived class is written. The first function is a constructor, which in this case is only a repetition of the original constructor from the external EEPROM library. But it must be there because otherwise I could not define a variable of that type. And when we create derived types, we almost always add our own code. This function without code is an exception.

The other two functions are templates. This means that it's just a function template with a particular type that creates a compiler for us. The code is not complicated. Just retype the type T into uint8_t (the address in the RAM reference to the pointer), calculate the size in bytes with the function sizeof, and then send it to the EEPROM. I used pointer this for clarity to see which class functions I use for writing. In C++ it is not mandatory to put it there, but there are languages that require it.

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;
    }
};

And because we already have a class defined, we can define a TemplatedEEPROM variable that can also use original class functions, including our new ones.

TemplatedEEPROM eep(kbits_16, 1, 16);

In the function loop, we only control the key press and call functions accordingly.

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);
  }
}

In order to have some variables, I have defined one text string and one structure and filled them with specific values.

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

In the function eeTemplateWrite, the contents of these variables are stored in specific EEPROM locations.

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);
}

In the function eeTemplateRead, we retrieve them to local variables to send their contents to the serial port and compare them to the original variables. Note that I've defined the size of read_string variable bigger than we need for the original content. This is the usual tactic when loading strings. We will declare a buffer of sufficient size and, for example, we can read one line from a source one by one. The buffer must have as much space as the largest text string, otherwise we would overwrite the data over the reserved space when filling the variable, and the consequences would probably be fatal to the run of the program.

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);
}

And there's just a program listing. You see that all variables are equal in content, and that tells us that I have typed those template functions correctly.

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 .......

Source code

The source code is located on GitHub.


23.10.2017


Menu