How To: Interface an irradiance sensor with Sensorpedia (guide 4)

February 26th, 2009 by Christopher Tomkins-Tinch

tsl230_side_view

We have previously detailed how to read in an LM34 temperature sensor, an ultrasonic rangefinder, and a light color sensor. This guide will show how to interface with a TSL230 light-to-frequency converter.

First, we’ll need to wire it up. Here is a diagram of our setup:

schematic_large

This looks something like this when breadboarded:

tsl230_breadboard_overview

tsl230_breadboard

Note that we’ve used a 0.1 microfarad capacitor to decouple the power supply lines and reduce noise introduced by the power supply. The LM34 temperature sensor was easy to read because it is an analog sensor whose voltage varies linearly with temperature . The TSL230 is more tricky to read because of its output behavior. It outputs a square wave with a frequency proportional to irradiance. The greater the irradiance, the higher the frequency.

Here we can see a functional diagram of the TSL230 (from the datasheet):

tsl230_functional_diagram_from_datasheetS0 and S1 are pins on the TSL230 that control the sensitivity of the TSL230. Pins S2 and S3 control a divide-by factor for the output. Here we can see how frequency varies with irradiance (also from the datasheet):

tsl230_freq_vs_irradiance_from_datasheet

In this guide, we will not consider responsivity varying as a function of wavelength, and we will assume a monochromic 640 [nm] light source. A calibrated source with a known spectral power distribution along with the spectral responsivity for this particular sensor would be needed to calibrate it. The expected responsivity curve for a TSL230 looks like this: (also from the datasheet):

tsl230_responsivity_from_datasheet

This curve is similar to any silicon-based detector. As this guide focuses on interfacing the TSL230 to Sensorpedia and not on data acquisition, calibration of the will be left as an exercise to the reader.

To do more logic-intensive operations like initialization of the sensor and unit adjustments, we will need to program the Arduino. Since the TSL230 outputs a square wave with frequency proportional to the intensity of incident light, we will need to find a way to read this frequency. From the Arduino’s perspective, this square wave looks like a series of binary digits, with +5VDC high being 1, and 0VDC low being 0. By counting the number of high states in a known time period, we can find the frequency.

The Arduino supports setting interrupts, so we will be using an interrupt-driven program. As each pulse from the TSL230 comes in, the program running on the Arduino will halt and a function specific to our interrupt trigger (in this case, the rising edge of each pulse) will fire, incrementing a pulse count. After the interrupt function has finished, the program continues on as it normally would. We will use a timer to output the pulse count at a known time interval, in our case every 1000 [ms].

Knowing the pulse count, we can use the known scaling factor to convert this into a frequency, which can then in turn be converted into values with useful units attached, watts/meter^2.

Here we see the code for the Arduino.

//            Pin Mappings
//   ______________________________
//  |__TSL230_Pin__|__Arduino_Pin__|
//  |       6      |       2       |
//  |       1      |       7       |
//  |       2      |       6       |
//  |       7      |       4       |
//  |       8      |       5       |
//   ------------------------------
#define TSL_OUTPUT_PIN              2 
#define TSL_S0	                    8 
#define TSL_S1	                    6 
#define TSL_S2	                    4
#define TSL_S3                      5 
#define SAMPLE_DURATION          1000 //1 second is 1000 ms
 
unsigned long pulseCount          = 0;
unsigned long currentSampleTime   = millis();
unsigned long previousSampleTime  = currentSampleTime;
unsigned int sampleTimeDifference = 0;
unsigned int tsl230scalingFactor  = 1;
unsigned int tsl230sensitivity    = 1;
 
void setup() {
  Serial.begin(9600);//start serial communication
 
  attachInterrupt(0, incrementPulseCount, RISING);// add an intterupt on pin 2, trigger on rising pulse (0->5V rising edge)
 
  pinMode(TSL_OUTPUT_PIN, INPUT);
  pinMode(TSL_S2, OUTPUT);  
  pinMode(TSL_S3, OUTPUT);
  pinMode(TSL_S0, OUTPUT);
  pinMode(TSL_S1, OUTPUT);
 
  // Set sensitivity and scaling factor
  setTsl230Sensitivity(1); //valid values are 0,1,10,100
  setTsl230Scaling(10);    //valid values are 1,2,10,100
}
 
void setTsl230Sensitivity(unsigned int sensitivity){
  //        Selectable Options: Sensitivity
  //         _____________________________
  //        |__S1__|__S0__|__Sensitivity__|
  //        |   L  |   L  |  Power down   |
  //        |   L  |   H  |      1x       |
  //        |   H  |   L  |     10x       |
  //        |   H  |   H  |    100x       |
  //         -----------------------------
 
  //set initial state, also serves as a defualt if the parameters are out of range
  digitalWrite(TSL_S0, HIGH);
  digitalWrite(TSL_S1, LOW);
  tsl230sensitivity = 1;
 
  switch(sensitivity){
    case 0:
      digitalWrite(TSL_S0, LOW); //this is the powered-down state
      digitalWrite(TSL_S1, LOW);
      tsl230sensitivity = 0;
    break;
    case 1:
      digitalWrite(TSL_S0, HIGH);
      digitalWrite(TSL_S1, LOW);
      tsl230sensitivity = 1;
    break;
    case 10:
      digitalWrite(TSL_S0, LOW);
      digitalWrite(TSL_S1, HIGH);
      tsl230sensitivity = 10;
    break;
    case 100:
      digitalWrite(TSL_S0, HIGH);
      digitalWrite(TSL_S1, HIGH);
      tsl230sensitivity = 100;
    break;
    }
}
 
void setTsl230Scaling(unsigned int scaling){
  //          Selectable Options: Scaling
  //   _________________________________________
  //  |__S3__|__S2__|__f_O_Scaling_(divide_by)__|
  //  |   L  |   L  |             1             |
  //  |   L  |   H  |             2             |
  //  |   H  |   L  |            10             |
  //  |   H  |   H  |           100             |
  //   -----------------------------------------
 
  //set initial state, also serves as a defualt if the parameters are out of range
  digitalWrite(TSL_S2, LOW);
  digitalWrite(TSL_S3, LOW);
  tsl230scalingFactor = 1;  
 
  switch(scaling){
    case 1:
      digitalWrite(TSL_S2, LOW);
      digitalWrite(TSL_S3, LOW);
      tsl230scalingFactor = 1;
    break;  
    case 2:
      digitalWrite(TSL_S2, HIGH);
      digitalWrite(TSL_S3, LOW);
      tsl230scalingFactor = 2;
    break;
    case 10:
      digitalWrite(TSL_S2, LOW);
      digitalWrite(TSL_S3, HIGH);
      tsl230scalingFactor = 10;
    break;
    case 100:
      digitalWrite(TSL_S2, HIGH);
      digitalWrite(TSL_S3, HIGH);
      tsl230scalingFactor = 100;
    break;
  }
}
 
void loop() { // output the current irradiance every SAMPLE_PERIOD ms
  previousSampleTime  = currentSampleTime; //set the previous time value
  currentSampleTime   = millis(); //get the current time value
 
  if( currentSampleTime > previousSampleTime ) {  // calculate how much time has passed. if the current time value is later longer than the last one, add the add the difference
    sampleTimeDifference += currentSampleTime - previousSampleTime;
  }
  else if( currentSampleTime < previousSampleTime ) { //otherwise, it has rolled over, so subtract from the maximum
    sampleTimeDifference += ( currentSampleTime + ( 34359737 - previousSampleTime )); //since millis() rolls over after ~9.5 hours, 34359737
  } 
 
  if( sampleTimeDifference >= SAMPLE_DURATION ) {// if SAMPLE_DURATION ms have passed, output a reading
    sampleTimeDifference = 0;  // re-set the sample time counter
    unsigned long frequency = getTsl230Frequency(); // get the current frequency reading
    int irradiance = calculateWattsPerMeterSquared(frequency); //calculate irradiance
    Serial.println(irradiance); //output irradiance
  }
}
 
void incrementPulseCount() {
  pulseCount++;// increment the pulse count
  return;
}
 
unsigned long getTsl230Frequency() {
  unsigned long frequencyValue = pulseCount * tsl230scalingFactor;// copy the pulse count value and multiply by the scaling factor.
  pulseCount = 0;// re-set pulse counter
 
  return(frequencyValue); //return frequency value
}
 
float calculateWattsPerMeterSquared(unsigned long frequency) { // return approx irradiance, assume monochromatic 640 [nm] source as per datasheet graph of Output Frequency vs Irradiance
  float wattsPerMeterSquared = ( (float) frequency / ((float) 10 * (float)tsl230sensitivity)) * 0.01; //equation as per the datasheet, factor of 0.01 to go from uW/cm^2 to W/m^2
 
  return(wattsPerMeterSquared);
}

We will be adapting the Atom server from our last guide to serve an Atom feed of data from our TSL230:

//We'll need a few things
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.IO;
using System.IO.Ports;
using System.Net;
using System.Xml;
using System.Xml.Serialization;
using System.Runtime.InteropServices;
using System.Threading;
using WebFeeds;
using WebFeeds.Feeds;
using WebFeeds.Feeds.Atom;
 
 
namespace LightSensorAtomServer{ 
    class MainProgram {
 
        // Create the serial port with basic settings  
        private SerialPort port = new SerialPort("COM17", 9600, Parity.None, 8, StopBits.One); //the serial port we will use to read in data. note that the COM port must be that which our data is coming in on. something like COM5 for windows, or /dev/ttyUSB0 for POSIX
 
        static void Main(string[] args){
            new MainProgram(); //instantiate the program
        }
 
        MainProgram(){
            SerialPortStart(); //initialize the serial port
            if (port.IsOpen)
                startServer(); //start up our HTTP server, which will in turn be responsible for calling methods to read in data, generate an Atom feed, and serve it
            else
                Console.WriteLine("An open serial port is needed to read data. Make sure the port settings are correct.");
        }
 
 
        public void startServer(){
            HttpListener listener = new HttpListener(); //create a new HTTP listener
            listener.Prefixes.Add("http://*:80/"); //make it listen to all requests on the standard web port, 80
            listener.Start(); //start listening
            Console.WriteLine("Listening...");
            for (; ; ) //run the server forever
            {
                HttpListenerContext context = listener.GetContext(); //gives us access to information about requests
                new Thread(new Worker(context, writeFeed(ReadData())).ProcessRequest).Start(); //make a new thread to respond, pass it the context AND the feed we created. Note that we are reading in data and then writing a feed. Effect: We wait for the latest data before serving
            }
        }
 
        class Worker{
            private HttpListenerContext context; //a ListenerContext local to this class and thus thread
            private byte[] byteFeedArray; //this will hold our feed output as a byte array
 
            public Worker(HttpListenerContext context, byte[] byteFeedArray){//set our instance variables according to the parameters passed in
                this.context = context; //set our HTTPListenerContext
                this.byteFeedArray = byteFeedArray; //set our data feed to the output generated above
            }
 
            public void ProcessRequest(){ //answer our web request
                string msg = context.Request.HttpMethod + " " + context.Request.Url;
                Console.WriteLine(msg); //output the request to the console so we can spot strange requests
                if (context.Request.HttpMethod == "GET") { //if they are requesting the data with a GET request
                    context.Response.ContentType = "application/xhtml+xml"; //since we are outputting Atom, set the MIME type of the data output to XML 
                    context.Response.ContentLength64 = byteFeedArray.Length; //set our content length
                    context.Response.OutputStream.Write(byteFeedArray, 0, byteFeedArray.Length); //write out our entire feed byte array
                    context.Response.OutputStream.Close(); //close the request, we're done serving to this request
                } else {} //we're not allowing other HTTP verbs
            }
        }
 
        public byte[] writeFeed(string data) { //this function is responsible for actually creating and outputting the byte array of our feed from data and stored variables
            byte[] byteFeedArray; //a byte array to store our finished feed array
            int streamPosition; //We will store the output of WebFeeds in a MemoryStream, and use this streamPosition variable to help us keep track of where we are in reading it out
            AtomFeed sensorFeed = new AtomFeed(); //This AtomFeed object is the real deal. It will be the one serialized and outputted to our final byte array
 
            string feedTitle      = "TSL230 Irradiance Data Feed"; //set the title for our feed
            string feedSubtitle   = "A series of irradiance values [W/m^2]."; //set the subtitle for our feed
            string feedAuthorName = "Chris Tomkins-Tinch"; //set the feed author name
            string feedAuthorUri  = "http://www.christomkintinch.com";//set the feed author uri
            double feedLat        = 35.9302; //set the feed lat
            double feedLon        = -84.3112; //...and lon
 
            string entryTitle     = "Irradiance Data"; //set the entry title (we'll only have one for now)
            string entryContent   = data; //set the data to that passed in as a parameter
            double entryLat       = 35.9302; //set the entry lat
            double entryLon       = -84.3112;//...and lon
 
            sensorFeed = generateFeed(System.Guid.NewGuid().ToString(), feedTitle, feedSubtitle, feedAuthorName, feedAuthorUri, feedLat.ToString(), feedLon.ToString());//pass all of the above local variables to the function that will output a new AtomFeed with all of the needed metadata, set our local sensorFeed to this output
 
 
            XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces();//make a Serializer so we can add some namespaces to our feed
            namespaces.Add("georss", "http://www.georss.org/georss"); //and the georss namespace
            sensorFeed.AddNamespaces(namespaces); //add these namespaces to the sensor feed
 
            sensorFeed.Entries.Add(generateEntry(System.Guid.NewGuid().ToString(), entryTitle, entryContent, entryLat.ToString(), entryLon.ToString()));//pass all of the above local variables to the function that will output a new AtomEntry with all of the needed elements and added to the feed we made above
 
            MemoryStream memoryStream = new MemoryStream(); //make a new MemoryStream to hold the output of the XML Serializer
            FeedSerializer.SerializeXml(sensorFeed, memoryStream, null, true); //serialize our AtomFeed (sensorFeed) and put the output into our memory stream
 
            memoryStream.Seek(0, SeekOrigin.Begin);// prepare to start navigating and outputting from our memory stream. set the position to the beginning of the stream.
 
            byteFeedArray = new byte[memoryStream.Length]; //initialize our feed byte array to be the same length as our memory stream
            streamPosition = memoryStream.Read(byteFeedArray, 0, 20);// Read the first 20 bytes from the stream.
 
 
            while (streamPosition < memoryStream.Length) {// Read the remaining bytes, byte by byte.
                byteFeedArray[streamPosition++] = Convert.ToByte(memoryStream.ReadByte());
            }
 
            return byteFeedArray; //output the final byte array.  this contains the serialized xml of our AtomFeed which has been converted from its memory stream location to a byte array. now we can serve it.
        }
 
        public AtomFeed generateFeed(string feedUuid, string feedTitle, string feedSubtitle, string feedAuthorName, string feedAuthorUri, string feedLat, string feedLon){
            AtomFeed feed = new AtomFeed(); //an AtomFeed from the JSONFX WebFeeds lib
            XmlDocument xmlDoc = new XmlDocument(); //an XmlDocument so we can make XML elements
 
            feed.Updated = new AtomDate(DateTime.UtcNow); //set the feed updated time to the current time
            feed.Title = feedTitle; //set the feed title to that specified by the parameter
            feed.ID = feedUuid; //set the feed universally unique identifier to that specified by the parameter
            AtomPerson author = new AtomPerson(); //make a new AtomPerson construct
            author.Name = feedAuthorName; //add the author name specified in the parameter to the author construct
            author.Uri = feedAuthorUri; //add the author URL specified in the parameter to the author construct
            feed.Authors.Add(author); //add the author construct to the feed. this will generate the author element and child elements of name, url, etc.
            AtomLink feedLink = new AtomLink("http://www.example.com/sensor/feed/url");//add a link construct to our feed. this represents where we can go to retrieve this feed
            feed.Links.Add(feedLink); //add the feed link to our feed
            feed.Categories.Add(new AtomCategory("light")); //add a category construct
            feed.Categories.Add(new AtomCategory("irradiance"));  // and another
            feed.Categories.Add(new AtomCategory("arduino"));// and one more
 
            XmlElement georssPoint = xmlDoc.CreateElement("georss", "point", "http://www.georss.org/georss"); //make a new XML element using the XmlDocument we defined above. specify its namespace as georss, and add the georss namespace to that link. WebFeeds will take care of consolidating like namespaces and adding them to the feed level of the outputted document
            georssPoint.InnerText = feedLat + " " + feedLon; //make a space delimited string to represent our location, per the GeoRSS simple spec
            feed.ElementExtensions.Add(georssPoint); //add our newly minted GeoRSS XmlElement to our feed
 
            return feed; //return our feed. we aren't done with it yet, but now we have added all of the feed-level metadata
        }
 
        public AtomEntry generateEntry(string entryUuid, string entryTitle, string entryContent, string entryLat, string entryLon) {//Make and return a simple Atom entry
            AtomEntry entry = new AtomEntry(); //an AtomEntry from the JSONFX WebFeeds lib
            XmlDocument xmlDoc = new XmlDocument(); //an XmlDocument so we can make XML elements
 
            entry.Updated = new AtomDate(DateTime.UtcNow); //set the entry updated time to the current time
            entry.Title = entryTitle;//set the entry title to that specified by the parameter
            entry.ID = entryUuid;//set the feed universally unique identifier to that specified by the parameter
            entry.Content = entryContent;//set the feed content to that specified by the parameter, in this case our data
 
            XmlElement georssPoint = xmlDoc.CreateElement("georss", "point", "http://www.georss.org/georss");//make a new XML element using the XmlDocument we defined above. specify its namespace as georss, and add the georss namespace to that link. WebFeeds will take care of consolidating like namespaces and adding them to the feed level of the outputted document
            georssPoint.InnerText = entryLat + " " + entryLon;//make a space delimited string to represent our location, per the GeoRSS simple spec
            entry.ElementExtensions.Add(georssPoint);//add our newly minted GeoRSS XmlElement to our entry
 
            return entry;//return our entry object. now it can be added to a feed
        }
 
        public void SerialPortStart() {
            try{
                port.Open();//begin communication
            }
            catch (Exception e){
                Console.WriteLine(e.Message);
                throw;
            }
 
        } 
 
        public string ReadData() {
            char tempChar; //a local byte variable to hold output from the serial buffer
            string serialOutputString = ""; //a local variable to hold the string we will buid from the serial output
 
            port.DiscardInBuffer();//wipe the serial buffer so we know this is a new reading
 
            while (serialOutputString.IndexOf("n") == -1){ //wait for an end-of-line character, so we know our next reading is complete
                tempChar = (char)port.ReadChar(); //read in a char
                serialOutputString = serialOutputString + tempChar; //and add the byte to our string
                Console.WriteLine(serialOutputString);
            }
 
            serialOutputString = port.ReadLine();
 
            serialOutputString = serialOutputString.Replace("n", "").Replace("r", ""); //once we're here we have reached an end-of-line and are now our of the loop. strip the newline characters
            Console.WriteLine(serialOutputString); //write the values to the console since they're nice to see.
            return serialOutputString; //return the serial output. we're finished reading from the serial port for now
        }
    }
}

This is the resulting XML:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns:georss="http://www.georss.org/georss" xmlns="http://www.w3.org/2005/Atom">
	<georss:point>35.9302 -84.3112</georss:point>
	<author>
		<name>Chris Tomkins-Tinch</name>
		<uri>http://www.christomkintinch.com/</uri>
	</author>
	<category term="light" />
	<category term="irradiance" />
	<category term="arduino" />
	<link href="http://www.example.com/sensor/feed/url" />
	<id>afa7c0c7-0baa-4899-a507-dc5c4569b2f1</id>
	<title>TSL230 Irradiance Data Feed</title>
	<updated>2009-02-26T00:09:58Z</updated>
	<entry>
		<georss:point>35.9302 -84.3112</georss:point>
		<id>7f815f6e-7ac7-4ebd-9a53-2aaaa892732d</id>
		<title>Irradiance Data</title>
		<updated>2009-02-26T00:09:58Z</updated>
		<content>12</content>
	</entry>
</feed>

tsl230-irradiance-data-feed

Share and Enjoy:
  • TwitThis
  • Facebook
  • del.icio.us
  • Google
  • LinkedIn

3 Responses to “How To: Interface an irradiance sensor with Sensorpedia (guide 4)”

  1. Mostafa Shaeri Says:

    Thank you for your good teaching.

  2. La Zorrera » Blog Archive » Recursos sobre Arduino Says:

    [...] Interface an irradiance sensor [...]

  3. Sensorpedia » Blog Archive » Sensorpedia student returns to map sensors, work on mobile apps Says:

    [...] How To: Interface an irradiance sensor with Sensorpedia [...]

Leave a Reply