In my last blog post, I showed you the schematic of a I2C data logger I built. Here I will discuss some sample code used for this data logger and how to make it even more flexible.

I will use the triple accelerometer digital level I built a while ago as an example and use it as the data source to measure vibrations and accelerations. The type of data source is not that important as long as it supports I2C.

The following is a picture of this setup, as you can see the accelerometer and the data logger are connected via the I2C bus.

I2C Data Logger

In this example, the accelerometer is set as an I2C slave. The I2C related code is shown below:

void TWIRequest()
{
    byte xlow, xhigh, ylow, yhigh, zlow, zhigh;
    byte ay[6];

    xlow = vx & 255;
    xhigh = (vx & (255 << 8)) >> 8;
    ylow = vy & 255;
    yhigh = (vy & (255 << 8)) >> 8;
    zlow = vz & 255;
    zhigh = (vz & (255 << 8)) >> 8;

    ay[0] = xlow;
    ay[1] = xhigh;
    ay[2] = ylow;
    ay[3] = yhigh;
    ay[4] = zlow;
    ay[5] = zhigh;

    Wire.send(ay, 6);
}
......
void setup() {
    ......
    Wire.begin(TWI_ADDR);
    Wire.onRequest(TWIRequest);
}

As you can see, whenever the accelerometer receives a request from the I2C master, the TWIRequest method is automatically called and the values for the x,y,z axes are then sent. Note that since the values for the x,y,z axes are integers and not bytes, the value for each axis is represented by two bytes. After the value bytes are transferred to to host, the bytes are assembled back into integers again. The rest of the code used for the accelerometer is similar to what I had written about before so I will not repeat here.

The code on the data logger side is a bit more complex as it needs to handle RTC/SD Card and the I2C communication between the slave and master devices.

Here is the code listing:

#define __AVR_ATmega328P__

#include <binary.h>
#include <HardwareSerial.h>
#include <pins_arduino.h>
#include <WConstants.h>
#include <wiring.h>
#include <wiring_private.h>
#include <Wire/utility/twi.h>
#include <Wire/Wire.h>
#include <WProgram.h>
#include <EEPROM/EEPROM.h>
#include <SdFat/SdFat.h>
#include <SdFat/SdFatUtil.h>

#define DS3232_I2C_ADDRESS B01101000  // This is the I2C address 7bits
const int ACC_SENSOR_ADDR = 100;

Sd2Card card;
SdVolume volume;
SdFile root;
SdFile file;

typedef struct {
  byte second;
  byte minute;
  byte hour;
  byte dayOfWeek;
  byte dayOfMonth;
  byte month;
  byte year;
} Date;

Date pD;

byte decToBcd(byte val)
{
  return ( (val/10*16) + (val%10) );
}

byte bcdToDec(byte val)
{
  return ( (val/16*10) + (val%16) );
}

void getDateDS3232()
{
  // Reset the register pointer
  Wire.beginTransmission(DS3232_I2C_ADDRESS);
  Wire.send(0x00); //move to reg 0
  Wire.endTransmission();

  int vBytesToRead = 7;
  Wire.requestFrom(DS3232_I2C_ADDRESS, vBytesToRead);
  
  pD.second     = bcdToDec(Wire.receive() & 0x7f);
  pD.minute     = bcdToDec(Wire.receive() & 0x7f);
  pD.hour       = bcdToDec(Wire.receive() & 0x3f); // Need to change this if 12 hour am/pm
  pD.dayOfWeek  = bcdToDec(Wire.receive() & 0x07); //0= sunday
  pD.dayOfMonth = bcdToDec(Wire.receive() & 0x3f);
  pD.month      = bcdToDec(Wire.receive() & 0x1f);
  pD.year       = bcdToDec(Wire.receive());

  Wire.endTransmission();
}

void setControlRegisters(){
  Wire.beginTransmission(DS3232_I2C_ADDRESS);
  Wire.send(0x0E); //Goto register 0Eh
  Wire.send(B00011100);
  Wire.send(B10000000);
  Wire.endTransmission();
}

void setupRTC3232(){
  Wire.begin();
  setControlRegisters();

}

char formatStrFileName[]="%02d%02d%02d%02d.txt";
char fileName[20];

char *ftoa(char *a, double f, int precision)
{
  long p[] = { 0,10,100,1000,10000,100000,1000000,10000000,100000000  };

  char *ret = a;
  long heiltal = (long)f;
  itoa(heiltal, a, 10);
  while (*a != '\0') a++;
  *a++ = '.';
  long desimal = abs((long)((f - heiltal) * p[precision]));
  itoa(desimal, a, 10);
  return ret;
}

void setup() {
  setupRTC3232();

  pinMode(16, OUTPUT);
  digitalWrite(16, LOW);  
  pinMode(17, OUTPUT);
  digitalWrite(17, HIGH); 

  card.init();
  volume.init(card);
  root.openRoot(volume);

  getDateDS3232();
  sprintf(fileName, formatStrFileName, pD.month, pD.dayOfMonth, pD.second, random(100));
  file.open(root, fileName, O_CREAT | O_EXCL | O_WRITE);
  file.writeError = false;
}

char lineStr[50];
char lineFormat[] ="%02d%02d%02d %d %d %d";

int counter = 0;
byte xlow, xhigh, ylow, yhigh, zlow, zhigh;
int x, y, z;

void loop() {
  counter++;
  getDateDS3232();

  Wire.requestFrom(ACC_SENSOR_ADDR, 6);

  xlow = Wire.receive();
  xhigh = Wire.receive();
  ylow = Wire.receive();
  yhigh = Wire.receive();
  zlow = Wire.receive();
  zhigh = Wire.receive();

  x = xlow | xhigh << 8;
  y = ylow | yhigh << 8;
  z = zlow | zhigh << 8;

  sprintf(lineStr, lineFormat, pD.hour, pD.minute, pD.second, x, y, z);

  file.println(lineStr);
  if (counter == 100) {
    counter = 0;
    file.close();
    file.open(root, fileName, O_CREAT | O_APPEND | O_WRITE);
  }

  delay(100);
}

As you can see from the code above, the accelerometer data is retrieved using the Wire.requestFrom statement, and 6 bytes are reassembled into the three integer values.

A trick was used to prevent frequent write activities to the SD card. In the code above, we close the file handle at an interval of every 100 lines and re-open the file for the coming operations. As a draw back, if power is lost before the file handle closes, up to 100 line of data can be lost. But for long running data logging operations, this should not be an issue.

The following images illustrate some of the data logged during a test run. In this particular case, I placed the data logger in my car and drove around the town. The accelerometer is in 2g mode and the recorded data has a 12bit resolution. The x axis data can be used to analyze the number of turns I made during that trip. The y axis data can be used to analyze the acceleration/deceleration and the z axis data can be used to analyze the road surface condition. The flat regions can be used to count how many times I had stopped in that trip.

X Axis
X Axis
Y Axis
Y Axis
Z Axis
Z Axis

Even though the example I used above needed to code the data logger side specifically to accommodate the data coming from the accelerometer, this data logger code can be generalized a bit more to accommodate a larger set of I2C slave devices.

One way to achieve this is as follows:

Upon powering up, the slave would send a “magic” packet to the master, inside which the slave’s data type format (e.g. whether it is integer or byte) and data length (how many bytes are there in the payload) are included. As long as all the slaves use the same handshake packet data sequence, we can detect what specific data format (i.e. whether it is byte or integer and what is the length) a slave uses at the I2C master device side.

The slave side code may look like the following (we use five consecutive 200’s as the magic packet and the data type and data length are immediately followed. In this case, we use 1 to represent integer and 2 to represent byte):

#include <Wire.h>

const int TWI_ADDR = 100;

byte HAND_SHAKE_PACKET[]={200,200,200,200,200, 1, 10};
boolean configured = false;

void TWIRequest()
{
  if (configured) {
  byte ay1[] = {0,1,2,3,4,5,6,7,8,9};
    Wire.send(ay1, 10);
  } else {   
    Wire.send(HAND_SHAKE_PACKET,7);
    configured = true;
  }
}

void setup() {
  Wire.begin(TWI_ADDR);
  Wire.onRequest(TWIRequest);
}

void loop()
{
}

On the I2C master’s side, we can use a fixed slave address. Note that the fixed address is only possible if the slave side is using the same device or the slave side uses a microcontroller. Since different slave devices may transmit data of different lengths, the I2C master need to be able to handle all the possibilities. One way to achieve this is to set the data length (NUM_DATAPOINTS) to be the longest among all possible I2C slaves. If the data provided to the I2C master is less than the preset data length, the received data would simply be 0’s after the desired length. A more elegant approach would be using the exact number of bytes indicated in the magic packet. Depending on the data type, the I2C master would handle the incoming data accordingly.

The code on the I2C master side can be very complex, but the general approach can be illustrated as follows:

#include <Wire.h>

const int ACC_SENSOR_ADDR = 100;
const int NUM_DATAPOINTS = 10;

void setup()
{
  Wire.begin();
}

void loop()
{
  Serial.println("------");  
  Wire.requestFrom(ACC_SENSOR_ADDR, NUM_DATAPOINTS);
  
  //procedures to handle the magic packets
  ...
  for (int i = 0 ; i < NUM_DATAPOINTS; i++) {
    byte d =  Wire.receive();

  //code to handle different data type depending on what is read from the master packet
    ...
  }
  
  delay(100);
}

Since the magic packet is only sent during a power cycle event on the I2C slave’s side, this approach requires the power at the master’s side to be stable. Should the I2C master loose power, it would not be able to infer the data type of the incoming packets unless the slave is restarted. More advanced messaging scheme can be address this shortcoming, but it is beyond the scope of this discussion.

Be Sociable, Share!