Thursday, February 3, 2011

Let's Add Sensors, Part 2

In my last post, we worked through digital inputs using push-buttons, and we also started looking at interrupts.  This time we'll start looking at how to interface an analog sensor.  These sensors can be range-finders (I have a Sharp GP2D12), potentiometers (adjustable resistors) and environmental sensors (temperature, humidity, force, accelerometer, gyros, et cetera.)

Now that we know what we're dealing with, let's get down to business... 

The analog pins are extremely easy to read, in fact, all you need is
int sensorValue = analogRead(SENSOR_PIN);
No setting up of pins, no starting of any magic converters, just go and read the thing. Not bad.

Now that you know how to read your analog input, you need to make sense of it. This is when you'll need to look at the datasheet for your sensor. You can look at the one for my sensor above. In there, you'll probably find some sort of chart that maps the output voltage to whatever it is measuring (in this case, Volts-to-cm.)  Here's the one for my sensor:



Now, keeping in mind that the 10-bit A/D converter will give us 1024 possible results and I'm not that completely concerned with precision (and I'm lazy), we can pick some points on the graph. Another thing to consider is that the A/D converter doesn't return volts, it gives you a number from 0 to 1023 equaling 0 to 5 volts. I eye-balled the chart above and came up with the following values:
 

VoltsA/D ValueDistance
2.449010
2.040813
1.632717
1.428620
1.020430
0.8016338
0.7014340
0.6513350
0.5010260
0.459270
0.408280

Alternatively, you could calibrate this table to your exact sensor by following this tutorial and using a ruler and adjust the values accordingly.  I'm not particularly concerned with precision at the moment, but I may come back to that option later.

After we have our distance points, we'll use our handy geometry skills to make a slope between two known points using the following slope equations:
y = m * x + c
where:
   y = value
   x = distance
   m = slope
   c = constant adjustment
 and:
m = (y1 - y2) / (x1 - x2)
where:
   x1, y1 = the first known point on our graph
   x2, y2 = the second known point on our graph

Another thing to keep in mind is sensor timing.  Using the timing chart from the datasheet we see that that the sensor will only be able to provide data so quickly, and there's a delay between the measurement and the availability of the data. Knowing this, we need to add an initial delay in the setup routine and a delay between every time we read the sensor data.


If we stop our sketch with just a loop that continuously reads the data and logs it to Serial, it would give us a ridiculous amount of output. So, we're going to combine this sketch with the last one and modify it to only output once when the button is pressed, sort of like a camera shutter.

Here's what we've got going so far:
 
/*
 *  Analog Sensor Test (Sharp GP2D12)
 *  with button for logging enable.
 *
 */


// This identifies the interrupt number for our chipset:
//   0 = digial pin 2
//   1 = digital pin 3
#define INTERRUPT_NUM 1 

// set up the button pin to match the interrupt number.
#define BUTTON_PIN 3 
// set up the a pin drive the button
#define DRIVER_PIN 4 

// identify the current LED state this is volatile because 
// the toggleLed routine which changes the value will be
// called from an interrupt. This is important because 
// we'll be using this variable in our loop routine.
volatile int button_state = HIGH; 

// our sensor has a maximum time between the measurement and
// the data being ready to read.  This is just an initial 
// delay that is only added on the start of the program.
#define SENSOR_INIT_DELAY 5

// our sensor has a delay between read-times of 48ms, meaning
// new data is only available every 48 ms.
#define SENSOR_DELAY 48

// select the analog pin we're going to use
#define SENSOR_PIN A0

struct ValueToDist {
  int value;
  int dist;
};

// We need to identify values to feed to our algorithm
// and since we know that the sensor is only ranged from
// 0 - 2.5V, we can ignore half the data points, additionally
// we're going to give a relatively small sample set (compared
// to the 1024 possible values)
ValueToDist valueDistTable[] =
 { {490, 10},
   {408, 13},
   {327, 17},
   {286, 20},
   {204, 30},
   {163, 38},
   {143, 40},
   {133, 50},
   {102, 60},
   {92,  70},
   {82,  80} };

int getDistance( int value )
{
  ValueToDist left;
  ValueToDist right;
  float slope;
  float c;
  
  int result = -1;
  
  // Test for the value being out of bounds
  if (value > valueDistTable[0].value || 
      value < valueDistTable[11].value )
  {
    Serial.println("value out of bounds");
    result = -1;
  }
  // test for the value at the closer edge of valid values
  else if ( value == valueDistTable[0].value )
  {
    result = valueDistTable[0].dist;
  }
  // test for the value at the further edge of valid values
  else if ( value == valueDistTable[11].value)
  {
    result = valueDistTable[11].dist;
  }
  // handle all other points in between
  else
  {
    // loop through the table, except the last one
    for (int i = 0; i < 11; i++ )
    {
      // if the value is between the current and next point
      if ( value <= valueDistTable[i].value && 
           value > valueDistTable[i+1].value )
      {

         Serial.print("found value range, upper =");
         Serial.print(valueDistTable[i].value);
         Serial.print(" lower =");
         Serial.println(valueDistTable[i+1].value );

         // using a simple slope algorithm to find
         // the value on the line between two known
         // points
         //   value=slope * dist + c
         left=valueDistTable[i];
         right=valueDistTable[i+1];

         // calculate the slope from the known points
         slope = ( (left.value - right.value) / (left.dist - right.dist));
         // get the slope constant using one of the known points
         c = left.value-slope*left.dist;

         // get the results from the algorithm
         result=int((value - c) / slope);

         // break out of the loop, because we've found what we're looking for
         break;
      }
    }
  }  
  
  return result;
}

void toggleLed()
{
  // read in the current state of the button
  button_state=digitalRead(BUTTON_PIN);
}

void setup()
{
  // add the driver pin
  pinMode(DRIVER_PIN, OUTPUT);
  // set the driver pin to LOW
  digitalWrite(DRIVER_PIN, LOW);
  
  // add the driver pin
  pinMode(BUTTON_PIN, INPUT);
  // enable the pull-up register
  digitalWrite(BUTTON_PIN, HIGH);
  
  // Identify the routine and conditions for our interrupt. An interrupt
  // can have only one routine attached, and that routine takes no inputs
  // and returns no output.  The specified routine will be called in 
  // the midst of the normal loop processing, so any variables that are
  // changed should probably be marked as volatile, if they are real-time
  // critical.  For our purposes, it's not necessary, but it may be 
  // significant if we have more robust processing of data changed in an 
  // interrupt routine.
  attachInterrupt(INTERRUPT_NUM, toggleLed, CHANGE); 
 
  // start serial port at 9600 bps:
  Serial.begin(9600);

  delay(SENSOR_INIT_DELAY);
}

void loop()
{
  // As noted above, the sensor can only translate data so
  // quickly, so each time through loop, we need to delay 
  // the read of the next sensor value
  delay(SENSOR_DELAY);                  

  // only calculate the distance if the button is pressed
  if (LOW == button_state)
  {
    // read the value from the sensor:
    int sensorValue = analogRead(SENSOR_PIN);
    int dist = getDistance(sensorValue);

    // if the button is being pressed, send the data to the
    // serial interface.
    Serial.print("sensor val = ");
    Serial.print(sensorValue);
    Serial.print("  distance = " );
    Serial.println(dist);
    
    // reset the button state so that we only get one reading
 // per button press
    button_state=HIGH;
  }
}

Here's the fritzing diagram of our setup:


and here's the link for the code.

No comments:

Post a Comment