Sunday, 5 June 2016

Writing data from Arduino to 24LC256 eeprom chip over serial/UART

Since spending some time with Arduino in recent months (not necessarily our microcontroller of choice, but when working on collaborative projects, sometimes you just have to go with what others are comfortable with) we seem to have spent quite a bit of time writing very similar routines for storing and playing back animation data from eeprom.

One of the easiest external eeprom chips to work with is the 24LC256 chip, and the Wire library in Arduino makes it super-easy to interface with (it's not actually that difficult to do, with a bit of pin-wiggling, but Arduino is all about making things easier, so let's stick with that!)

(although many I2C devices require pull-up resistors on the CLK and SDA lines - thanks to open drain collectors on most devices - we can leave them off if connecting to an Arduino, and use the internal pull-up resistors on the AVR I/O pins)

The chip is wired up as any other I2C device would  be to the Arduino. We're using an Arduino Pro mini - yours may be different, but the main thing to remember is that I2C clock SCK/CLK goes to analogue pin A5. I2C data SDA/DAT goes to A4



Now for some simple code to test everything is working.
Here's a basic framework we like to use. It allows for sending data over serial, in short "packets". We tend to stick to value-to-hex-to-ascii and back again conversions for sending data over methods we're unsure about (this project might end up receiving serial data over bluetooth, or wifi, or even RF radio) so we're going to receive an entire packet of data as a string, and then parse it.

It also means that sending data to the device should be relatively straightforward - simply encode each value into a hexadecimal value and send as a string of 00-FF.


// this for reading/writing to eeprom 24LC256 chip
#include <Wire.h>
#define disk1 0x50      //Address of 24LC256 eeprom chip

long tmr;
long tmrStart;
long tmrNow;
int serialState=0;

String incomingMessage;
String msgToParse;
bool msgWaiting=false;
unsigned int eepromAddress;

// just some temporary variables
int i;
int j;
int k;
int h;

void writeEEPROM(unsigned int eeaddress, byte data) {
   Wire.beginTransmission(disk1);
   Wire.write((int)(eeaddress >> 8));    // MSB
   Wire.write((int)(eeaddress & 0xFF)); // LSB
   Wire.write(data);
   Wire.endTransmission();
   delay(5); // writing can take up to 5m/s per address
}

byte readEEPROM(unsigned int eeaddress) {
   byte rdata = 0xFF;
   Wire.beginTransmission(disk1);
   Wire.write((int)(eeaddress >> 8));    // MSB
   Wire.write((int)(eeaddress & 0xFF)); // LSB
   Wire.endTransmission();
   Wire.requestFrom(disk1, 1);
   if (Wire.available()){ rdata = Wire.read();}
   return rdata;
}

void resetTimer(){
   tmrStart=millis();
   tmr=0;
}

long getTime(){
   long t=millis();
   t=t-tmrStart;
   return t;
}

void getSerialData(){
   if(Serial.available() == true){
      while(Serial.available() == true && msgWaiting==false){
         byte b=Serial.read();
         if(b==0x0a){
            // this is an end of string/message terminating character
            // so update the message to parse and set a flag
            msgToParse = incomingMessage;
            incomingMessage = "";
            msgWaiting = true;
            
         }else if(b==0x0d){
            // ignore linefeed characters
         }else{
            // keep adding incoming characters to the receive string
            incomingMessage = incomingMessage + char(b);
         }         
      }      
   }
}

int getValueFrom(int k){
   // assumes a two-character value from the message string
   // starting at position k - so a string like AFE0596 might
   // be from 1 = 0xFE, from 3 = 0x05, from 5 = 0x96
   int b1 = int(msgToParse.charAt(k));
   int b2 = int(msgToParse.charAt((k+1)));

   if(b1 >= 48 && b1 <= 57){ b1 = b1 - 48;}
   if(b1 >= 65 && b1 <= 74){ b1 = b1 - 55;}
   if(b2 >= 48 && b2 <= 57){ b2 = b2 - 48;}
   if(b2 >= 65 && b2 <= 74){ b2 = b2 - 55;}
   
   int j = b1 * 16;
   j = j + b2;
   return(j);
}

void parseMessage(){   
   int deviceID = getValueFrom(0);
   int commandByte = getValueFrom(2);
   int commandValue = getValueFrom(4);

   Serial.print(F("device:"));
   Serial.print(deviceID);
   Serial.print(F(" command:"));
   Serial.print(commandByte);   
   Serial.println(" ");

   Serial.print(F(" value:"));
   Serial.print(commandValue);

   switch(commandByte){      

      case 'W':
      // write data to the specified eeprom address
      // (address should be two bytes)
      
      eepromAddress = commandValue * 256;
      commandValue = getValueFrom(6);
      eepromAddress += commandValue;

      Serial.print(F("writing to eeprom address: "));
      Serial.println(eepromAddress);

      // now go through the string until there are no more characters left
      // and "stream write" them to the eeprom chip
      k = 8;
      j = msgToParse.length();
      Serial.print(j);
      Serial.println(" characters in string");
      
      h = 0;
      while(k < j){
         commandValue = getValueFrom(k);

         Serial.print("pos:");
         Serial.print(k);
         Serial.print(" value:");
         Serial.println(commandValue);
         
         writeEEPROM(eepromAddress, byte(commandValue));
         eepromAddress++;
         h++;
         k += 2;
      }

      Serial.print(h);
      Serial.println(F(" bytes written"));
      break;

      case 'Q':
      // testing: read back data from eeprom address
      eepromAddress = commandValue * 256;
      commandValue = getValueFrom(6);
      eepromAddress += commandValue;

      commandValue = getValueFrom(8);

      Serial.print(F("reading "));
      Serial.print(commandValue);
      Serial.print(F(" bytes from eeprom address: "));
      Serial.println(eepromAddress);

      for(h=0; h < commandValue; h++){
         byte b = readEEPROM(eepromAddress);
         Serial.print(F("value: "));
         Serial.println(b);
         eepromAddress++;
      }
      
   }
}

void setup() {
   // put your setup code here, to run once:
   Serial.begin(57600);
   Wire.begin();   
}

void loop() {
   // put your main code here, to run repeatedly:

   getSerialData();
   if(msgWaiting==true){
      Serial.print(F("received: "));
      Serial.println(msgToParse);
      parseMessage();   
      msgWaiting=false;
   }
   
}

This approach allows us to send "command strings" to different devices on a network and - where necessary - have each device save the data sent to it, to it's own local eeprom chip.

For example, let's send the command "turn your RGB LED to blue" to device number 3.
Our data packets are in the form:

device_id : command_byte : values/parameters

So let's say we use the command byte (or character) 'C' for colour. And perhaps blue could be a colour in a palette, indexed at... let's say 15. So to send our message, we'd send the decimal values

3 (device id) 67 (ascii for the letter C) 15 (the colour palette index).

Converting this into hex, we end up with 03, 43, 0F
So we send a string (followed by the usual carriag return/line feed combination) 03430F.
That's all well and good, but what about this pesky eeprom chip?

Well, having now understood our "data packet protocol" we can tweak it a bit, so that when we receive a particular "command byte" (let's say, W) it's an instruction to write the data that follows to the eeprom chip. We'll also need to send an address to write the data to. And that should be enough.

Except, if were writing a few bytes into consecutive locations, that's a lot of message overhead per byte. So we made the routine a little more intelligent: if it receives a write command, it expects the next two bytes to be the eeprom memory location to start writing to, and then every pair of characters after that represents the hex value to be stored. The routine then updates the eeprom address (increases it by one) so that a number of bytes can be "streamed" and written to eeprom.

Here's an example.
Let's say we want device number 5 to store the values 48,19,203 to it's external eeprom chip, starting at address 100 (so 48 goes into location 100, 19 into 101, 203 into 102 etc).

We'd need to send the decimal values 5 (device id), 87 (ascii for W), 0, 100 (memory location), 48, 19, 203 (values - note the zero before the value 100 - that's because our memory location is a two-byte value). Turning this into hex, we get 0500643013CB



Great - so it says it wrote that data to eeprom; how do we find out?
In our routine we wrote some test code, that allows us to send a Q character, followed by an address (two-bytes) and a number of bytes to read back from memory. So we send

decimal value 5 (device id), 81 (ascii for Q), 0, 100 (memory location), 3 (number of bytes to read) which turns into the string of hex characters: 0551006403


So now we have a simple framework for robustly sending messages that can be decoded in a nice-easy-to-understand string format (a lot of people don't like character/byte arrays for some reason). The routine is written so that a carriage return indicates the end of a "string" and parsing will begin at the next available opportunity in the main loop. Even if string decoding hasn't begun, the next message can start to arrive over serial.

There are a couple of caveats:
The Arduino serial library uses interrupts to receive data into a circular buffer. This buffer has only 64 bytes, so don't make your strings of text too long! If you need to store lots of data to eeprom, split it up and send it over a number of shorter messages.

Also, if an entire string has been received but parsing hasn't begun before another full string (terminating with the CrLf combination) is received, it's quite possible that the earlier message will be lost. A simple "stack" of messages could be implemented if necessary, but for now we're just sticking with a relatively simple framework.