HexiLogger, an Arduino based data logger
The purpose of this project was to create a simple, portable device that would
periodically read sensors and then store the sensor data so it could be retrieved later.
The result is the HexiLogger, "hexi" because it can support up to six different sensor inputs and "logger" because it will store the input data on a removable SD memory card.
Hardware
The HexiLogger hardware consists of the following modules:
- ATmega328 microcontroller on a Diecimila Arduino board.
- AdaFruit GPS shield, minus the GPS module
- Homebrew user interface shield
- Sensors
I began the project using the ATmega168 chip that came with the Diecimila board but quickly discovered that the Fat16 libraries needed to support the SD memory card pushed the limit of the 168's flash RAM. Upgrading to the 328 chip was easy to do. Just swap the chips and then change the Arduino development environment to compile for the ATmega328 (Tools/Board). Doing that doubled the program space and stopped the strange behavior I was seeing witht eh 168 chip.
Stacked on the Arduino board is an AdaFruit GPS shield. I was only interested in using the shield's SD card, so there is no GPS module or supporting GPS code involved in this project. The only modifications I made to the GPS shield was to add a set of three female header pins to the top of the GPS shield for access to the Arduino analog inputs and data lines D0-D7.
Attached to the top of the GPS shield is a homebrew user interface shield that allows logging interval input, user feedback LEDs and connections for the sensors.
The sensors used are TMP36 from Analog Devices in a TO-92 package but any sensors that can be attached to an Arduino could be used.
This image shows the device in breadboard development phase with test sensors attached.
This image shows the GPS shield lifted off to view the SD card holder underneath.
This image is the final configuration with all the breadboard hardware moved on to a custom shield that sits on top of the GPS shield.
Schematic
Software
The HexiLogger software consists of the following:
- Fat16 Libraries
- User interface supporting code
- Sensor support
Several libraries exist for Arduino/Fat16 interaction, and many of them are variations of each other. I tried a couple of different ones and settled on Fat16 because of its straight forward SD card support. Installation of the library is simple, just add it to arduino-0016/hardware/libraries folder and put #include "Fat16.h" and #include "SdCard.h" into the code. HexiLogger routines initializeCard() and writedataToCard() manage the storage file on the SD card.
The user interface switches are read by the HexiLogger function DetermineTimeDelay() which sets the interval that the sensors will be read. displayCardErrorIndicator(), displayRunIndicator() and
displayWriteIndicator() all handle the LED display.
More information about how the input switches are used to set the interval can be found in one of my other projects.
Each type of sensor will need its own software function in order to convert the raw data from the analog inputs into a meaningful value. I have included four, processLM35SensorDegF(), processTMP36SensorDegF(), processTMP36SensorDegC(),
and processPhotoCell() to handle some types of sensors that I used during development.
Operation Flow
The basic operation of the device is as follows:
1) When powered on, read the user input switches and determine the data logging interval.
2) Check the SD card.
if the card is missing or a file can't be created then display error indicator and stop.
if the SD card is operational, create a file and write out the file header.
3) Read the sensors, converting the data if necessary.
4) Open the file on the SD card, write the sensor data to the file.
5) Close the file.
6) Pause the required interval.
Go back to 3) and repeat.
Continue to loop through steps 3, 4, 5 and 6 until the power is removed.
Output Data File
The output file on the SD card will look like this:
Interval 10 minutes
a0,a1,a2,a3,a4,a5
59,57,0,0,0,0
56,56,0,0,0,0
55,57,0,0,0,0
55,57,0,0,0,0
55,57,0,0,0,0
56,58,0,0,0,0
etc.
The first two lines are the file header, showing the interval that the sensors were read and the sensor order in the CSV data. All the remaining lines are the sensor readings. The CSV format makes it easy to graph in a spreadsheet application.
Since HexiLogger is a simple device and does not use a Real Time Clock chip to determine the interval, there is no way to write out the time of the readings to each line in the file. However it would be a simple task to write a small PC program that takes as input the starting time, the SD Card file and sensor names and would produce additional columns like this.
Interval 10 minutes
time,inside,outside,n/a,na/,n/a,n/a
09:00,59,57,0,0,0,0
09:10,56,56,0,0,0,0
09:20,55,57,0,0,0,0
09:30,55,57,0,0,0,0
etc.
When I find some spare time I will write and post the code to such an application.
HexiLogger Test
To test the final project, I attached two TMP36 temperature sensors, set the interval to be 10 minutes and attached a new 9v battery to the HexiLogger.
One sensor was inside my vehicle and one sensor was outside under the vehicle in the shade.
I wanted to monitor the rise in temperature inside the closed vehicle as the outside air temperature rose throughout the day. Also I wanted to test to see how long the HexiLogger could run on a 9v battery.
I started the logger at 6:00am and let it run until the battery was dead. Below is a graph of the results.
Conclusion and Code
Running off a new 9v battery, HexiLogger was able to log data every 10 minutes for 8 hours. Not bad!
Changing the Arduino code to put the microcontroller into sleep mode between loggings might extend the battery life but unfortunately would prevent using delay() to manage the interval and would require more hardware, like a real time clock to trigger an interrupt. For now, I'll just use a different power supply if I need extended logging.
Arduino Code
/*
HexiLogger
* reads 6 analog inputs
* saves the data to SD card in Fat16 format
* pauses for specified interval
* repeat
Hardware base: Arduino ATmega328
This Arduino software is released to the public
domain "as is". The author makes no warranty
expressed or implied and assumes no responsibilities.
Feel free to fold, bend, spindle or mutilate as you see fit.
*/
//################################################################
#include "Fat16.h"
#include "SdCard.h"
SdCard card;
Fat16 file;
char name[] = "HLG-00.TXT";
int interval = 0;
unsigned long timeDelay = 0;
// user input switches for log interval
int switch_A = 7;
int switch_B = 6;
int switch_C = 5;
int switch_MS = 4;
// feedback LEDs
int greenLED = 3;
int yellowLED = 2;
float a0, a1, a2, a3, a4, a5;
int samplestaken = 0;
boolean cardError;
boolean useMinutes;
//------------------------------------------------
void displayCardErrorIndicator() {
digitalWrite(yellowLED, HIGH);
digitalWrite(greenLED, HIGH);
delay(500);
digitalWrite(yellowLED, LOW);
digitalWrite(greenLED, LOW);
delay(500);
}
//------------------------------------------------
void displayRunIndicator(){
digitalWrite(yellowLED, LOW);
digitalWrite(greenLED, HIGH);
}
//------------------------------------------------
void displayWriteIndicator(){
digitalWrite(yellowLED, HIGH);
digitalWrite(greenLED, LOW);
}
//------------------------------------------------
float processLM35SensorDegF(float rawIn){
return ((rawIn / 1024.0) * 5.0) * 100.0;
}
//------------------------------------------------
float processTMP36SensorDegF(float rawIn){
float v = rawIn * 5.0 / 1024;
// temperature C
float tc = (v - 0.5) * 100 ;
// temperature F
return (tc * 9 / 5) + 32;
}
//------------------------------------------------
float processTMP36SensorDegC(float rawIn){
float v = rawIn * 5.0 / 1024;
// temperature C
return (v - 0.5) * 100 ;
}
//------------------------------------------------
float processPhotoCell(float rawIn){
return (1024.0 - rawIn);
}
//------------------------------------------------
void getAnalogInputs(){
// Change depending on type of sensors attached
//a0 = processLM35SensorDegF(analogRead(0));
//a1 = processPhotoCell(analogRead(1));
a0 = processTMP36SensorDegF(analogRead(0));
a1 = processTMP36SensorDegF(analogRead(1));
// change when sensors attached
a2 = 0; // analogRead(2);
a3 = 0; // analogRead(3);
a4 = 0; //analogRead(4);
a5 = 0; //analogRead(5);
}
//------------------------------------------------
void setup() {
pinMode(switch_A, INPUT);
pinMode(switch_B, INPUT);
pinMode(switch_C, INPUT);
pinMode(switch_MS, INPUT);
pinMode(yellowLED, OUTPUT);
pinMode(greenLED, OUTPUT);
timeDelay = DetermineTimeDelay();
//5 second delay from power on to logging start
delay(5000);
//Serial.begin(9600);
initializeCard();
}
//------------------------------------------------
void loop() {
// ---- used for testing ---------------
//timeDelay = DetermineTimeDelay();
//initializeCard();
//Serial.println(timeDelay);
//delay(1500);
//--------------------------------------
if (cardError)
// blink both LEDs and do nothing else
displayCardErrorIndicator();
else {
// card and file ok, start logging
displayWriteIndicator();
getAnalogInputs();
writeDataToCard();
displayRunIndicator();
delay(timeDelay);
}
}
//--------------------------------------------------
void initializeCard() {
if (!card.init()) error("card.init");
if (!Fat16::init(card, 1))
if (!Fat16::init(card, 0))
error("Fat16::init");
for (int i = 0; i <> 15) {
file.sync();
samplestaken = 0;
}
if (file.isOpen())
file.close();
}
//---------------------------------------------------------
void error(char *str) {
Serial.print("error: ");
Serial.println(str);
cardError = true;
}
//--------------------------------------------------------
unsigned long DetermineTimeDelay(){
/* reads input switche bank and returns the number of milliseconds
switch_MS = 0 : seconds
swtich_MS = 1 : minutes
Switch
(CBA) Time
-----------------------
000 5
001 10
010 15
011 30
100 45
101 60
110 90
111 120
-----------------------
*/
boolean swA, swB, swC;
unsigned long timedelay = 0;
if (digitalRead(switch_MS) == HIGH)
useMinutes = true; // switch is up, up minutes
else
useMinutes = false; // switch is down, use seconds
if (digitalRead(switch_C) == HIGH)
swC = true;
else
swC = false;
if (digitalRead(switch_B) == HIGH)
swB = true;
else
swB = false;
if (digitalRead(switch_A) == HIGH)
swA = true;
else
swA = false;
if (!swC && !swB && !swA){
interval = 5;
if (!useMinutes)
timedelay = 5000;
else
timedelay = 300000;
}
else if (!swC && !swB && swA){
interval = 10;
if (!useMinutes)
timedelay = 10000;
else
timedelay = 600000;
}
else if (!swC && swB && !swA){
interval = 15;
if (!useMinutes)
timedelay = 15000;
else
timedelay = 900000;
}
else if (!swC && swB && swA){
interval = 30;
if (!useMinutes)
timedelay = 30000;
else
timedelay = 1800000;
}
else if (swC && !swB && !swA){
interval = 45;
if (!useMinutes)
timedelay = 45000;
else
timedelay = 2700000;
}
else if (swC && !swB && swA){
interval = 60;
if (!useMinutes)
timedelay = 60000;
else
timedelay = 3600000;
}
else if (swC && swB && !swA){
interval = 90;
if (!useMinutes)
timedelay = 90000;
else
timedelay = 5400000;
}
else if (swC && swB && swA){
interval = 120;
if (!useMinutes)
timedelay = 120000;
else
timedelay = 7200000;
}
else
// default to 1 min in case sw pack goes bad
timedelay = 600000;
return timedelay;
}