Leaner meaner code for the Cave Pearls.

The new terminal block protoshield makes the RTC hookup easy!

The new  TinyDuino terminal block protoshield makes the RTC hookup easy!

I have spent some time making that rough code more efficient, so that I can buffer more sensor read cycles to the Arduino’s very limited SRAM before having to burn away battery power writing data to the SD cards. There were several great references out there to guide me, and a few helpful people took time to email me suggestions, some of which have been implemented here:

Eliminate unnecessary cruft:

I stopped dumping all the data into a text string, which got rid of the commas, spaces, etc that I had been buffering for nothing. I guess this is a pretty common newbie mistake.

And instead of buffering the entire date & time stamp, I simply record it once at the beginning of a sample cycle and then use a one byte record/offset number, as I can re-calculate the actual time for each sample from this offset number. This saved a huge amount of free ram! (Thanks Mike!)

Use the smallest variable types possible.

I converted the floating point temperature readings into sets of two integers (the part before the decimal place, and the part after the decimal place) and any integers that were storing values less than 256, are now bytes, or uint8_t, which cut them down by half. The single time stamp per cycle is now stored in a well defined “char” variable. One person suggested that with some fancy bit management I could probably cut those variables in half again, but I have not quite wrapped my head around bit shifting & two’s complement yet. (Thanks Ken!)

Write better code:

The crude code duplication of the earlier versions has been replaced with array variables and for loops, which you control via the SampleInterval and SamplesPerCycle defines at the beginning of the script. These cascade through to the array definitions. I should mention here that I tried this approach before when I had the data packed into strings and it simply did not work. The freeRam routine showed me that every cycle bled away another byte or two till the system crashed. But AFTER I got rid of all the strings in the script, the loops are stable. I suspect there is a bug in the way the AVR compiler handles string data.

The net result of these changes is that each record went from about 60 characters down to 12 bytes of data, so this script can easily buffer 10 cycles or more of data and still leave 680 bytes of free ram. A nice safe margin away from the 512 bytes that I have to leave available for the SD.h buffer.

I expect to sample the sensors between 50-100 times per day, but for power management, I only want to write to the SD card once per day. So now I need to look at a second level of buffering, with eeprom memory…

// Date, Time and Alarm functions using a DS3231 RTC connected via I2C and Wire lib by https://github.com/MrAlvin/RTClib
// based largely on Jean-Claude Wippler from JeeLab’s excellent RTC library https://github.com/jcw
// clear alarm interupt from http://forum.arduino.cc/index.php?topic=109062.0
// get temp from http://forum.arduino.cc/index.php/topic,22301.0.html which does not use the RTCLIB!
// BMA250_I2C_Sketch.pde -BMA250 Accelerometer using I2C from http://www.dsscircuits.com/accelerometer-bma250.html
// combined with internal voltage reading trick //forum.arduino.cc/index.php/topic,15629.0.html
// floats to string conversion: http://dereenigne.org/arduino/arduino-float-to-string// free ram code trick: http://learn.adafruit.com/memories-of-an-arduino/measuring-free-memory
// power saving during sleep from http://www.gammon.com.au/forum/?id=11497

#include <SD.h> //a memory hog – takes 512 bytes of ram just to run!
#include <Wire.h>
#include <SPI.h> // not used here, but needed to prevent a RTClib compile error
#include <avr/sleep.h>
#include <RTClib.h>

#ifndef cbi //defs for stopping the ADC during sleep mode
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif

#define DS3231_I2C_ADDRESS 104 //for the RTC temp reading function

#define BMA250 0x18
#define BW 0x08 //7.81Hz bandwith
#define GSEL 0x03 // set range 0x03 – 2g, 0x05 – 4, 0x08 – 8g, 0x0C – 16g

#define SampleInterval 1
// power-down time in minutes before interupt triggers the next sample

#define SamplesPerCycle 10
//# of sample cycles before writing to the sd card

RTC_DS3231 RTC;
byte Alarmhour = 1;
byte Alarmminute = 1;
byte dummyRegister;
byte INTERRUPT_PIN = 2;
volatile boolean clockInterrupt = false;
byte tMSB, tLSB; //for the RTC temp reading function
float RTCTempfloat;
char CycleTimeStamp[ ]= “0000/00/00,00:00:00”;
byte Cycle=0;

const byte chipSelect = 10; //sd card chip select

uint8_t dataArray[16]; //variables for accellerometer reading
int8_t BMAtemp; //Temp is in units of 0.5 degrees C
//8 bits given in two’s complement representation
float BMAtempfloat;//float BMATempHolder;
//char BMATempHolder[ ]= “00.00”;
//components for holding BMA250 temp as two integers – we have no negative temps in our application
uint8_t wholeBMAtemp[SamplesPerCycle],fracBMAtemp[SamplesPerCycle];

int x,y,z; //these guys range to negative values
int xAcc[SamplesPerCycle],yAcc[SamplesPerCycle],zAcc[SamplesPerCycle];

uint8_t wRTCtemp[SamplesPerCycle],fRTCtemp[SamplesPerCycle]; //components for holding RTC temp as two intergers
int temp3231;
int Vcc[SamplesPerCycle];//the supply voltage via 1.1 internal band gap

byte ledpin = 13; //led indicator pin not used in this code

void setup () {

pinMode(INTERRUPT_PIN, INPUT);
digitalWrite(INTERRUPT_PIN, HIGH);//pull up the interrupt pin
pinMode(13, OUTPUT); // initialize the LED pin as an output.
digitalWrite(13, HIGH); // turn the LED on to warn against SD card removal

Serial.begin(9600);
Wire.begin();
RTC.begin();
DateTime now = RTC.now();
DateTime compiled = DateTime(__DATE__, __TIME__);
if (now.unixtime() < compiled.unixtime()) { //only update rtc if needed
Serial.println(F(“RTC is older than compile time! Updating”));
RTC.adjust(DateTime(__DATE__, __TIME__));
}
clearClockTrigger(); //stops RTC from holding the interrupt low if system reset
// time for next alarm
Alarmhour = now.hour();
Alarmminute = now.minute()+ SampleInterval ;
if (Alarmminute > 59) { //error catch – if Alarmminute=60 the interrupt never triggers due to rollover
Alarmminute = 0; Alarmhour = Alarmhour+1; if (Alarmhour > 23) {Alarmhour =0;}
}

initializeBMA(); //initialize the accelerometer – do I have to do this on every wake cycle?

//get the SD card ready
pinMode(chipSelect, OUTPUT); //make sure that the default chip select pin is set to output, even if you don’t use it
Serial.print(F(“Initializing SD card…”));
if (!SD.begin(chipSelect)) { // see if the card is present and can be initialized:
Serial.println(F(“Card failed, or not present”)); // don’t do anything more:
return;
}
Serial.println(F(“card initialized.”));
Serial.print(F(“The sample interval for this series is: “));Serial.print(SampleInterval);Serial.println(F(” minutes”));
Serial.println(F(“Timestamp Y/M/D, HH:MM:SS,Time offset, Vcc = , X = , Y = , Z = , BMATemp (C) , RTC temp (C)”));

File dataFile = SD.open(“datalog.txt”, FILE_WRITE); //PRINT THE DATA FILE HEADER
if (dataFile) { // if the file is available, write to it:
dataFile.print(F(“The sample interval for this series is: “));dataFile.print(SampleInterval);dataFile.println(F(” minutes.”));
dataFile.print(F(“A full timestamp will be generated every”));dataFile.print(SamplesPerCycle);dataFile.println(F(” samples.”));
dataFile.println(F(“YYYY/MM/DD HH:MM:SS, Cycle# = Time offset, Vcc(mV), X = , Y = , Z = , BMATemp (C) , RTC temp (C)”));
dataFile.close();
}
else { //if the file isn’t open, pop up an error:
Serial.println(F(“Error opening datalog.txt file!”));
}
digitalWrite(13, LOW);
}

void loop () {

for (int Cycle = 0; Cycle < SamplesPerCycle; Cycle++) { //this counts from 0 to (SamplesPerCycle-1)

if (clockInterrupt) {
clearClockTrigger();
}

read3AxisAcceleration(); //loads up the acc data variables
DateTime now = RTC.now(); // Read the time and date from the RTC

if(Cycle==0){   //timestamp for each cycle only gets set on first pass
sprintf(CycleTimeStamp, “%04d/%02d/%02d %02d:%02d:%02d”, now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second());
}

xAcc[Cycle]=x;yAcc[Cycle]=y;zAcc[Cycle]=z; //BMAtemp_[Cycle] = BMAtempfloat;
wholeBMAtemp[Cycle] = (int)BMAtempfloat; fracBMAtemp[Cycle]= (BMAtempfloat – wholeBMAtemp[Cycle]) * 100; // Float split into 2 integers
//you can use sprintf(BMATempHolder, “%2d.%2d”, wholeBMAtemp[Cycle], fracBMAtemp[Cycle]) if we need to recompose that float later
Vcc[Cycle] = (readVcc()); RTCTempfloat= get3231Temp();
wRTCtemp[Cycle] = (int)RTCTempfloat; fRTCtemp[Cycle]= (RTCTempfloat – wRTCtemp[Cycle]) * 100; // Float split into 2 integers

//main serial line output loop – which can be commented out for deployment
Serial.print(CycleTimeStamp); Serial.print(F(” Cycle “)); Serial.print(Cycle);Serial.print(“,”); Serial.print(Vcc[Cycle]); Serial.print(“,”);
Serial.print(xAcc[Cycle]); Serial.print(“,”);Serial.print(yAcc[Cycle]); Serial.print(“,”); ;Serial.print(zAcc[Cycle]); Serial.print(“,”);
Serial.print(wholeBMAtemp[Cycle]);Serial.print(“.”);Serial.print(fracBMAtemp[Cycle]);Serial.print(“,”);
Serial.print(wRTCtemp[Cycle]);Serial.print(“.”);Serial.print(fRTCtemp[Cycle]);
Serial.print(F(“, Ram:”));Serial.print(freeRam());
delay(50); //short delay to clear com lines

// dump data to the sd card on the last cycle but only if Vcc is above 2.85 volts
if (Cycle==(SamplesPerCycle-1) && Vcc[Cycle] >= 2850){
Serial.print(F(” –write data –“)); delay (50);// this line for debugging only

File dataFile = SD.open(“datalog.txt”, FILE_WRITE);

if (dataFile) { // if the file is available, write to it:

for (int i = 0; i < SamplesPerCycle; i++) { //loop to dump out one line of data per cycle
if (i=0){dataFile.print(CycleTimeStamp);}
dataFile.print(F(“,offset:”));dataFile.print(i);dataFile.print(“,”);dataFile.print(Vcc[i]); dataFile.print(“,”);
dataFile.print(xAcc[i]); dataFile.print(“,”);dataFile.print(yAcc[i]); dataFile.print(“,”);dataFile.print(zAcc[i]); dataFile.print(“,”);
dataFile.print(wholeBMAtemp[i]);dataFile.print(“.”);dataFile.print(fracBMAtemp[Cycle]);dataFile.print(“,”);
dataFile.print(wRTCtemp[i]);dataFile.print(“.”);dataFile.println(fRTCtemp[i]);

}
dataFile.close();
}
else { //if the file isn’t open, pop up an error:
Serial.println(F(“Error opening datalog.txt file”));
}
}

// setNextAlarmTime();
Alarmhour = now.hour(); Alarmminute = now.minute()+SampleInterval;
if (Alarmminute > 59) { //error catch – if alarmminute=60 the interrupt never triggers due to rollover!
Alarmminute =0; Alarmhour = Alarmhour+1; if (Alarmhour > 23) {Alarmhour =0;}
}
RTC.setAlarm1Simple(Alarmhour, Alarmminute);
RTC.turnOnAlarm(1);

//print lines commented out for deployment
Serial.print(F(” Alarm Set:”)); Serial.print(now.hour(), DEC); Serial.print(‘:’); Serial.print(now.minute(), DEC);
Serial.print(F(” Sleep:”)); Serial.print(SampleInterval);Serial.println(F(” min.”));
delay(100); //a delay long enought to boot out the serial coms

sleepNow(); //the sleep call is inside the main cycle counter loop
}

}
void sleepNow() {
// set the unused digital pins to output low – but only worth 1-2 µA during sleep
// so not sure if its worth doing this or not.
// for (byte i = 3; i <= 13; i++)
// {
// pinMode (i, OUTPUT);
// digitalWrite (i, LOW);
// }

cbi(ADCSRA,ADEN); // Switch ADC OFF: worth 334 µA during sleep
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
attachInterrupt(0,clockTrigger, LOW);
// turn off brown-out enable in software: worth 25 µA during sleep
// BODS must be set to one and BODSE must be set to zero within four clock cycles
MCUCR = bit (BODS) | bit (BODSE); // turn on brown-out enable select
MCUCR = bit (BODS); // The BODS bit is automatically cleared after three clock cycles
sleep_mode();
//HERE AFTER WAKING UP
sleep_disable();
detachInterrupt(0);
sbi(ADCSRA,ADEN); // Switch ADC converter back ON
//digitalWrite(13, HIGH); this doesnt work because of conflict with sd card chip select
}

void clockTrigger() {
clockInterrupt = true; //do something quick, flip a flag, and handle in loop();
}

void clearClockTrigger()
{
Wire.beginTransmission(0x68); //Tell devices on the bus we are talking to the DS3231
Wire.write(0x0F); //Tell the device which address we want to read or write
Wire.endTransmission(); //Before you can write to and clear the alarm flag you have to read the flag first!
Wire.requestFrom(0x68,1); // Read one byte
dummyRegister=Wire.read(); // In this example we are not interest in actually using the bye
Wire.beginTransmission(0x68); //Tell devices on the bus we are talking to the DS3231
Wire.write(0x0F); //Tell the device which address we want to read or write
Wire.write(0b00000000); //Write the byte. The last 0 bit resets Alarm 1
Wire.endTransmission();
clockInterrupt=false; //Finally clear the flag we use to indicate the trigger occurred
}

// could also use RTC.getTemperature() from the library here as in:
// RTC.convertTemperature(); //convert current temperature into registers
// Serial.print(RTC.getTemperature()); //read registers and display the temperature

float get3231Temp()
{
//temp registers (11h-12h) get updated automatically every 64s
Wire.beginTransmission(DS3231_I2C_ADDRESS);
Wire.write(0x11);
Wire.endTransmission();
Wire.requestFrom(DS3231_I2C_ADDRESS, 2);

if(Wire.available()) {
tMSB = Wire.read(); //2’s complement int portion
tLSB = Wire.read(); //fraction portion

temp3231 = ((((short)tMSB << 8 | (short)tLSB) >> 6) / 4.0); // Allows for readings below freezing – Thanks to Coding Badly
//temp3231 = (temp3231 * 1.8 + 32.0); // Convert Celcius to Fahrenheit
return temp3231;

}
else {
temp3231 = 255.0; //Use a value of 255 to error flag that we did not get temp data from the ds3231
}

return temp3231;
}
byte read3AxisAcceleration()
{
Wire.beginTransmission(BMA250);
Wire.write(0x02);
Wire.endTransmission();
Wire.requestFrom(BMA250,7);
for(int j = 0; j < 7;j++)
{
dataArray[j] = Wire.read();
}
if(!bitRead(dataArray[0],0)){return(0);}

BMAtemp = dataArray[6];
x = dataArray[1] << 8;
x |= dataArray[0];
x >>= 6;
y = dataArray[3] << 8;
y |= dataArray[2];
y >>= 6;
z = dataArray[5] << 8;
z |= dataArray[4];
z >>= 6;

BMAtempfloat = (BMAtemp*0.5)+24.0;
}
byte initializeBMA()
{
Wire.beginTransmission(BMA250);
Wire.write(0x0F); //set g
Wire.write(GSEL);
Wire.endTransmission();
Wire.beginTransmission(BMA250);
Wire.write(0x10); //set bandwith
Wire.write(BW);
Wire.endTransmission();
return(0);
}

long readVcc() { //trick to read the Vin using internal 1.1 v as a refrence
long result;
// Read 1.1V reference against AVcc
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
delay(3); // Wait for Vref to settle
ADCSRA |= _BV(ADSC); // Convert
while (bit_is_set(ADCSRA,ADSC));
result = ADCL;
result |= ADCH<<8;
result = 1126400L / result; // Back-calculate AVcc in mV
return result;
}

int freeRam () {
extern int __heap_start, *__brkval;
int v;
return (int) &v – (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}

2 thoughts on “Leaner meaner code for the Cave Pearls.

  1. Adam

    I am having a compiling error on line 176 and 177 when I try to compile this code. It is telling me the Alarm and Sleep is not defined in this scope. I can get the original code to compile from your “Talking the talk” section though.

    1. edmallon Post author

      Don’t bother with the crufty old stuff posted here, and trying to copy out of WordPress is a pain because it always changes the quotes (“) into something that is non-functional in the IDE, among many other html baggage issues that get introduced. Go to the versions at the project Github and then you can see how the code progressed through time in a format that is much more IDE friendly

Comments are closed.