Page 1 of 1

Taos TSL230R light sensor C tutorial

Posted: Thu Feb 09, 2012 2:55 am
by Shane
The TSL230R light sensor IC is an interesting package: a light sensing circuit wrapped up in a clear plastic casing. This neat little device will convert irradiance (the light energy on the surface of the sensor) into frequency. Working with a simple input concept like a frequency means that we won’t have to build any extra circuitry to get the full range of information from the circuit, and having an accurate measure of radiance means that we’ll be able to convert easily over to illuminance, which is how the light looks to us. Obviously, once we can answer the question about how light looks, we can use this information to control other things. (Some great examples are: camera exposure, dimming displays, machine vision, etc.)

This guide is intended to walk you through the basics of interfacing the TSL230 series of chips from Taos with your friendly Arduino microcontroller. The specifics of the chip’s operation may not be painfully obvious the first read over the datasheet, but this guide expects you’ve at least read the important parts: which pins are which, that you can change its sensitivity, and that you can change the scale of the output.

SETUP

The only things we’ll require besides the TSL230R and an arduino is a breadboard, a 0.1uF ceramic capacitor, and some jumper wires. Wire up the pins on the TSL230R as so:
  • S0-S3: wire each of these to an arduino input, avoid digital in 2, as we’ll need that
  • OE: The output enable pin should be brought low by connecting it to GND. This pin enables or disables the output frequency. Bringing the pin HIGH will result in disabling output, so for our purposes, we’ll hard-wire it to GND, enabling output and saving an I/O line on the arduino.
  • Vcc: Connect Vcc to +5V from the arduino. On the hole nearest this pin on the breadboard, connect one leg of your 0.1uF capacitor, and the other to GND. The capacitor is necessary to help filter the power to prevent fluctuations in the output of the chip.
  • GND: connect to GND
  • OUT: Connect this pin to digital in 2 on the arduino. We’re going to use an interrupt, so we need to hook up to either in 2 or 3.
This would be a good time to define the pins in our code:

Code: Select all

#define TSL_FREQ_PIN 2 // output use digital pin2 for interrupt
#define TSL_S0	     5
#define TSL_S1	     6
#define TSL_S2	     7
#define TSL_S3       8
OUTPUT BASICS

So, now that we’ve got everything hooked up, let’s get down to work. The TSL230 has two types of output: a pulse train, and a square wave. The pulse train is a series of high pulses that give us the exact frequency being recorded at that time. Unfortunately, as the pulse high times are in the nano-seconds, they’re gone before we can effectively measure them. The square wave output, on the other hand, has a 50% duty-cycle, meaning the high pulses and low pulses are exactly the same length – 1/2 the frequency. This allows us to register the high pulses in Arduino’s micro-second world.

Pins S2 and S3 control the output scaling, the divide-by types (2, 10, and 100) produce the square wave we want. For now, we’ll choose divide by 100, which has both S2 and S3 high.

The sensitivity controls how many receptors are active on the chip at once. When its set to higher sensitivity, the chip can register smaller and smaller amounts of light, but loses the ability to register higher levels of light. For now, we’ll stick with 1x sensitivity, meaning S0 will be high and S1 will be low.

To read the square wave, you’ve probably seen examples of using pulseIn() – this has a couple of disadvantages, the first being that you’re only measuring one cycle of the frequency (it can change often throughout a second) and the second and most glaring, being that pulseIn() requires a delay while trying to read the input pin. That means for very low light levels (1 cycle per second) you could be waiting up to an entire second for one measurement! A much better way is to take a measure of how many pulses there are per second, which will average out any fluctuations, and if we use an interrupt, we can do anything else we like while we count the pulses.

Let’s go ahead and set everything up, including our interrupt.

Code: Select all


unsigned long pulse_cnt = 0;

void setup() {

  // attach interrupt to pin2, send output pin of TSL230R to arduino 2
  // call handler on each rising pulse

 attachInterrupt(0, add_pulse, RISING);

 pinMode(TSL_FREQ_PIN, INPUT);
 pinMode(TSL_S0, OUTPUT);
 pinMode(TSL_S1, OUTPUT);
 pinMode(TSL_S2, OUTPUT);
 pinMode(TSL_S3, OUTPUT);

 digitalWrite(TSL_S0, HIGH);
 digitalWrite(TSL_S1, LOW);
 digitalWrite(TSL_S2, HIGH);
 digitalWrite(TSL_S3, HIGH);
}

void loop() {

}

void add_pulse() {

  // increase pulse count
 pulse_cnt++;
 return;
}

Notice that our interrupt just increases a counter, that’s all we need it to do. We’re going to check the frequency and re-set it every second in our code. Note that the interrupt is given the argument RISING, this means that it’ll only trigger when the signal changes from LOW to HIGH. We only need to count the pulses per second.

Code: Select all


 // 1000ms = 1s
#define READ_TM 1000 

...

 // two variables used to track time
unsigned long cur_tm = millis();
unsigned long pre_tm = cur_tm;

 // we'll need to access the amount
 // of time passed
unsigned int tm_diff = 0;
...

void loop() {

  // check the value of the light sensor every READ_TM ms

    // calculate how much time has passed

 pre_tm   = cur_tm;
 cur_tm   = millis();

 if( cur_tm > pre_tm ) {
	tm_diff += cur_tm - pre_tm;
 }
 else if( cur_tm < pre_tm ) {
           // handle overflow and rollover (Arduino 011)
	tm_diff += ( cur_tm + ( 34359737 - pre_tm ));
 } 

   // if enough time has passed to do a new reading...

 if( tm_diff >= READ_TM ) {

       // re-set the ms counter
   tm_diff = 0;

      // get our current frequency reading
   unsigned long frequency = get_tsl_freq();

 }

}

unsigned long get_tsl_freq() {
    // copy pulse counter and multiply.
    // the multiplication is necessary for the current
    // frequency scaling level.  Please see the
    // OUTPUT SCALING section below for more info

  unsigned long freq = pulse_cnt * 100;

   // re-set pulse counter
  pulse_cnt = 0;

  return(freq);
}

CONVERTING FREQUENCY TO ENERGY

In the above code, we’ve gotten the frequency by counting the number of high pulses per second. This will work fine for our purposes, for the time being. The frequency sent to us by the chip correlates to a particular amount of radiant energy being received by it. We need to convert it to a more useful representation, and our target is a measurement of micro-watts (uW) per centimetre squared (cm2). The datasheet shows us a graph (figure 1) on page 4 that shows us that at a wavelength of 640nm, and 1x sensitivity, the relationship between uW/cm2 and the frequency is a ratio of 1:10. Or, the energy is 1/10th the frequency — uW/cm2 = Freq / 10.

An important point, saved for the last page of the datasheet, is that the actual sensor has an area of about 0.92mm2. This is about 0.0092cm2, whereas the application note found here indicates a size of 0.0136cm2 – I prefer this number. The size of the sensor is important, as we need to figure out how much energy is landing on a square centimetre of space, but we’re much smaller than that. We’ll need to multiply up the conversion to figure how much radiant energy would be landing in the square centimetre that surrounds us.

Also note that the sensor is more or less sensitive to other wavelengths than 640nm, so we’ll work straight from the graph in the datasheet figure 1, and stick with 640nm. Here’s a function to convert the frequency into uW/cm2:

Code: Select all

float calc_uwatt_cm2(unsigned long freq) {

  // get uW observed - assume 640nm wavelength 

    // note the divide-by factor of ten,
    // maps to a sensitivity of 1x

  float uw_cm2 = (float) freq / (float) 10;

    // extrapolate into entire cm2 area

  uw_cm2       *= ( (float) 1 / (float) 0.0136 );

  return(uw_cm2);

}
Now we have a measure of not only the uW observed, but we would likely observe, were we to have a sensor that was actually 1cm2.


INCREASING SENSITIVITY

So, the room goes dark, and your frequency lingers around, erm, 2. The TSL230R has a more sensitivity than you might imagine, but you’re going to give up some of your bright-end for it. You’ll be able to go from 0.1 uW to 0.001, at maximum sensitivity. The ramification of this, is that you’re going to have to change the divide-by factor for converting frequency to energy.

If your readings are pretty low, and you’ve done everything else right, it’s time to up the sensitivity a bit.

Code: Select all


  // need to measure what to divide freq by
  // 1x sensitivity = 10,
  // 10x sens       = 100,
  // 100x sens      = 1000

int calc_sensitivity = 10;

...

void sensitivity( bool dir ) {

  // adjust sensitivity in 3 steps of 10x either direction

  int pin_0;
  int pin_1;

  if( dir == true ) {

      // increasing sensitivity

      // -- already as high as we can get
    if( calc_sensitivity == 1000 )
      return;

    if( calc_sensitivity == 100 ) {
        // move up to max sensitivity
      pin_0 = true;
      pin_1 = true;
    }
    else {
        // move up to med. sesitivity
      pin_0 = false;
      pin_1 = true;
    }

      // increase sensitivity divider
    calc_sensitivity *= 10;
  }
  else {
      // reducing sensitivity

      // already at lowest setting

    if( calc_sensitivity == 10 )
      return;

    if( calc_sensitivity == 100 ) {
        // move to lowest setting
      pin_0 = true;
      pin_1 = false;
    }
    else {
        // move to medium sensitivity
      pin_0 = false;
      pin_1 = true;
    }

      // reduce sensitivity divider
    calc_sensitivity = calc_sensitivity / 10;
  }

    // make any necessary changes to pin states

 digitalWrite(TSL_S0, pin_0);
 digitalWrite(TSL_S1, pin_1);

 return;
}

float calc_uwatt_cm2(unsigned long freq) {

  // get uW observed - assume 640nm wavelength
  // calc_sensitivity is our divide-by to map to a given signal strength
  // for a given sensitivity (each level of greater sensitivity reduces the signal
  // (uW) by a factor of 10)

  float uw_cm2 = (float) freq / (float) calc_sensitivity;

    // extrapolate into entire cm2 area

  uw_cm2       *= ( (float) 1 / (float) 0.0136 );

  return(uw_cm2);

}
Now we have a function, that we can call with HIGH to increase sensitivity, LOW to decrease sensitivity, and our frequency conversion to energy scales for us.

In short – the higher sensitivities allow you to differentiate very low amounts of light. Use them in darker settings, and 1x sensitivity out in bright or daylight settings.

OUTPUT SCALING
A topic we haven’t touched on yet is the impact of output scaling. Output frequency scaling changes the way the chip reports frequency to you. Like sensitivity, frequency scaling allows you to measure different ranges of frequency. The divide by 2 method provides the lowest range of frequency, but with the most amount of resolution, divide by ten covers the mid-range of frequency with a moderate amount of
resolution, and divide by 100 covers the high range of frequency with low resolution.

Frequency scaling says that the chip will report one value representing the average value read for x periods – where x is 2, 10, or 100. Simply stated: multiply the frequency you read by the scaling level you have set. For example, in divide-by-2, you multiply your reading by two, and divide-by-100, you multiply your reading by one hundred.

Here’s some code to handle changing the output scaling:

Code: Select all


  // set our frequency multiplier to a default of 1
  // which maps to output frequency scaling of 100x

int freq_mult = 100;

...

void set_scaling ( int what ) {

  // set output frequency scaling
  // adjust frequency multiplier and set proper pin values
  // e.g.:
  // scale = 2 == freq_mult = 2
  // scale = 10 == freq_mult = 10
  // scale = 100 == freq_mult = 100

  int pin_2 = HIGH;
  int pin_3 = HIGH;

  switch( what ) {
    case 2:
      pin_3     = LOW;
      freq_mult = 2;
      break;
    case 10:
      pin_2     = LOW;
      freq_mult = 10;
      break;
    case 100:
      freq_mult = 100;
      break;
    default:
        // don't do anything with levels
        // we don't recognize
      return;
  }

    // set the pins to their appropriate levels

  digitalWrite(TSL_S2, pin_2);
  digitalWrite(TSL_S3, pin_3);

  return;
}

unsigned long get_tsl_freq() {

    // we have to scale out the frequency --
    // Scaling on the TSL230R requires us to multiply by a factor
    // to get actual frequency

  unsigned long freq = pulse_cnt * freq_mult;

    // reset the pulse counter

  pulse_cnt = 0;

  return(freq);
}
Now, we can change the output scaling at whim, and since we modified the frequency function, we don’t have to change any other code.

Use a lower output scaling for moderate levels of light or to tell minute differences in levels, or use higher scaling for brighter areas, but with less ability to tell the difference between small amounts. For most photographic applications, you’ll prefer divide-by-10 or divide-by-100, to prevent constant jitter, whereas live luminance measurement applications will prefer divide-by-2.


CONVERTING TO WHAT WE SEE

You’ve probably heard the term Lux before, as in “lux meter” or, this light produces this many “lux”. Lux is, essentially, the measure of perceived brightness falling on, or coming from an amount of surface. We’ll be using a surface size of square meters, m2, in our examples. Now, note that we’re talking about “perceived”, as in perceived with our eyes. To our eyes, different wavelengths of light register as brighter or less bright. This is called photometry, what we’ve been dealing with up to this point is measuring actual energy, which is radiometry.

To go from the radiometric (actual) data to photometric (perceived) data, we’ll need to convert the energy into lumens per square meter (lux) from micro-watts per square centimetre. In the process, we’ll need to determine how bright the light appears to us. We’ll use the luminous efficiency function to do this. Now, this function is dependent on wavelength – our eyes perceive different wavelengths more or less brightly. To keep matters simple, we’ll pretend like our light source is monochromatic — that is consists of only a single wavelength. We’ll pick the same wavelength we have been using, 640nm.

To perform this conversion, we’ll need to find the luminous efficiency of 640nm. Fortunately for us, this is empirical data, so we can just look it up in the CIE table. We’ll use CIE Vm(l) from 1978, and find that 640nm has an efficiency of 0.175.

Code: Select all

...

void loop() {

  // check the value of the light sensor every READ_TM ms

    // calculate how much time has passed

 pre_tm   = cur_tm;
 cur_tm   = millis();

 if( cur_tm > pre_tm ) {
	tm_diff += cur_tm - pre_tm;
 }
 else if( cur_tm < pre_tm ) {
           // handle overflow and rollover (Arduino 011)
	tm_diff += ( cur_tm + ( 34359737 - pre_tm ));
 } 

   // if enough time has passed to do a new reading...

 if( tm_diff >= READ_TM ) {

       // re-set the ms counter
   tm_diff = 0;

      // get our current frequency reading
   unsigned long frequency = get_tsl_freq();

      // calculate radiant energy
   float uw_cm2 = calc_uwcm2( frequency );

      // calculate illuminance
   float lux    = calc_lux_single( uw_cm2, 0.175 );

 }

}

...

float calc_lux_single(float uw_cm2, float efficiency) {

    // calculate lux (lm/m^2), using standard formula:
    // Xv = Xl * V(l) * Km
    // Xl is W/m^2 (calculate actual receied uW/cm^2, extrapolate from sensor size (0.0136cm^2)
    // to whole cm size, then convert uW to W)
    // V(l) = efficiency function (provided via argument)
    // Km = constant, lm/W @ 555nm = 683 (555nm has efficiency function of nearly 1.0)
    //
    // Only a single wavelength is calculated - you'd better make sure that your
    // source is of a single wavelength...  Otherwise, you should be using
    // calc_lux_gauss() for multiple wavelengths

      // convert to w_m2
  float w_m2 = (u_cm2 / (float) 1000000) * (float) 100;

      // calculate lux
  float lux  = w_m2 * efficiency * (float) 683;

  return(lux);
}
And, there you have it – you can now calculate the Lux for a single wavelength. Now, obviously most light sources we deal with produce light in many wavelengths at once, but we’ll save the more complex calculations for our next post on the subject. In the next one, we’ll cover calculating lux for multiple wavelengths, converting to Exposure Value (EV), calculating exposure time from EV/aperture/ISO, and other conversions.

TSL230R: Photographic Conversions

Posted: Thu Feb 09, 2012 3:14 am
by Shane
In the previous post on using the Taos TSL230R with the Arduino, I covered the basic operations of the chip, and some essential conversions for going from radiometric to photometric representation of its data. In this post, we’ll expand on that knowledge to calculate exposure times and apertures using the Exposure Value system and produce much more accurate lux calculations using multiple wavelengths of light. After reading both of these tutorials, you should have enough information to create your own photographic light meter using a few simple components.

GETTING STARTED

Before we go any further, we’ll bring forward some code from the previous example, and set this as our starting point:

Code: Select all


/*
   Example code for the Taos TSL230R and
   Arduino.

   C. A. Church - 11/2008

   This work is licensed under a Creative Commons
   Attribution-Share Alike 3.0 license.

*/

  // TSL230R Pin Definitions

#define TSL_FREQ_PIN 2 // output use digital pin2 for interrupt
#define TSL_S0       5
#define TSL_S1       6
#define TSL_S2       7
#define TSL_S3       8  

  // how often to calculate frequency
  // 1000ms = 1s  

#define READ_TM 1000

  // our pulse counter for our interrupt

unsigned long pulse_cnt = 0; 

  // two variables used to track time  

unsigned long cur_tm = millis();
unsigned long pre_tm = cur_tm;  

  // we'll need to access the amount
  // of time passed  

unsigned int tm_diff = 0;  

  // set our frequency multiplier to a default of 1
  // which maps to output frequency scaling of 100x

int freq_mult = 100;

  // need to measure what to divide freq by
  // 1x sensitivity = 10,
  // 10x sens = 100,
  // 100x sens = 1000

int calc_sensitivity = 10;

void setup() {  

  // attach interrupt to pin2,
  // send output pin of TSL230R to arduino 2
  // call handler on each rising pulse  

  attachInterrupt(0, add_pulse, RISING);  

  // setup TSL230R pins

  pinMode(TSL_FREQ_PIN, INPUT);
  pinMode(TSL_S0, OUTPUT);
  pinMode(TSL_S1, OUTPUT);
  pinMode(TSL_S2, OUTPUT);
  pinMode(TSL_S3, OUTPUT);  

  // 1x sensitivity,
  // divide-by-100 scaling

  digitalWrite(TSL_S0, HIGH);
  digitalWrite(TSL_S1, LOW);
  digitalWrite(TSL_S2, HIGH);
  digitalWrite(TSL_S3, HIGH);
}  

void loop() {

  // check the value of the light sensor every READ_TM ms

  // calculate how much time has passed

  pre_tm = cur_tm;
  cur_tm = millis();

  if( cur_tm > pre_tm ) {
    tm_diff += cur_tm - pre_tm;
  }
  else if( cur_tm < pre_tm ) {
      // handle overflow and rollover (Arduino 011)
    tm_diff += ( cur_tm + ( 34359737 - pre_tm ));
  }

    // if enough time has passed to
    // do a new reading...

  if( tm_diff >= READ_TM ) {

      // re-set the ms counter

    tm_diff = 0;

      // get our current frequency reading

    unsigned long frequency = get_tsl_freq();

      // calculate radiant energy

    float uw_cm2 = calc_uwcm2( frequency );

     // calculate illuminance

    float lux = calc_lux_single( uw_cm2, 0.175 );

  }

}

unsigned long get_tsl_freq() {

  // we have to scale out the frequency --
  // Scaling on the TSL230R requires us to multiply by a factor
  // to get actual frequency

  unsigned long freq = pulse_cnt * freq_mult;

    // reset the pulse counter

  pulse_cnt = 0;

  return(freq);
}

float calc_lux_single(float uw_cm2, float efficiency) {

  // calculate lux (lm/m^2), using standard formula:
  // Xv = Xl * V(l) * Km
  // Xl is W/m^2 (calculate actual receied uW/cm^2, extrapolate from sensor size (0.0136cm^2)
  // to whole cm size, then convert uW to W)
  // V(l) = efficiency function (provided via argument)
  // Km = constant, lm/W @ 555nm = 683 (555nm has efficiency function of nearly 1.0)
  //
  // Only a single wavelength is calculated - you'd better make sure that your
  // source is of a single wavelength... Otherwise, you should be using
  // calc_lux_gauss() for multiple wavelengths

    // convert to w_m2

  float w_m2 = (u_cm2 / (float) 1000000) * (float) 100;

    // calculate lux

  float lux = w_m2 * efficiency * (float) 683;

  return(lux);
}

float calc_uwatt_cm2(unsigned long freq) {

  // get uW observed - assume 640nm wavelength
  // calc_sensitivity is our divide-by to map to a given signal strength
  // for a given sensitivity (each level of greater sensitivity reduces the signal
  // (uW) by a factor of 10)

  float uw_cm2 = (float) freq / (float) calc_sensitivity;

    // extrapolate into entire cm2 area

  uw_cm2 *= ( (float) 1 / (float) 0.0136 );

  return(uw_cm2);

}

void set_scaling ( int what ) {

  // set output frequency scaling
  // adjust frequency multiplier and set proper pin values
  // e.g.:
  // scale = 2 == freq_mult = 2
  // scale = 10 == freq_mult = 10
  // scale = 100 == freq_mult = 100

  int pin_2 = HIGH;
  int pin_3 = HIGH;

  switch( what ) {
    case 2:
      pin_3 = LOW;
      freq_mult = 2;
      break;
    case 10:
      pin_2 = LOW;
      freq_mult = 10;
      break;
    case 100:
      freq_mult = 100;
      break;
    default:
        // don't do anything with levels
        // we don't recognize
      return;
   }

    // set the pins to their appropriate levels

  digitalWrite(TSL_S2, pin_2);
  digitalWrite(TSL_S3, pin_3);

  return;
}

void sensitivity( bool dir ) {

  // adjust sensitivity in 3 steps of 10x either direction

  int pin_0;
  int pin_1;

  if( dir == true ) {

      // increasing sensitivity

      // -- already as high as we can get
    if( calc_sensitivity == 1000 )
      return;

    if( calc_sensitivity == 100 ) {
        // move up to max sensitivity
      pin_0 = HIGH;
      pin_1 = HIGH;
    }
    else {
        // move up to med. sesitivity
      pin_0 = LOW;
      pin_1 = HIGH;
    }

      // increase sensitivity divider
    calc_sensitivity *= 10;
  }
  else {
      // reducing sensitivity

      // already at lowest setting

    if( calc_sensitivity == 10 )
      return;

    if( calc_sensitivity == 100 ) {
        // move to lowest setting
      pin_0 = HIGH;
      pin_1 = LOW;
    }
    else {
        // move to medium sensitivity
      pin_0 = LOW;
      pin_1 = HIGH;
    }

      // reduce sensitivity divider
    calc_sensitivity = calc_sensitivity / 10;
  }

    // make any necessary changes to pin states

 digitalWrite(TSL_S0, pin_0);
 digitalWrite(TSL_S1, pin_1);

 return;
}
REFLECTIVE VS. INCIDENT METERING

It is important to understand the different ways we can meter light for photographic purposes. In reflective metering the light that is measured is that which is reflected off the subject – that is, you point your meter at the subject being photographed. In incident metering the light that is measured is that which is incident upon the subject – that is you stand where your subject is, and point your meter at the light source.

In all of our calculations below, we will be operating from the aspect of reflective light metering. The formulas vary slightly for incident metering and one should take that into account when designing a project. Please note as well, that during testing, you should be metering reflected light as well – you will not get an accurate reading by pointing a light right as the sensor. Instead, use a sheet of white paper or such and measure the light reflecting off your paper.

EXPOSURE VALUE CALCULATIONS

Exposure Values are used in photography to represent different combinations of shutter speed and aperture that result in the same exposure. Normally, EV is calculated regardless of actual light – it’s a means of expressing a set of camera settings (speed and aperture). However, if we follow the APEX System we find that we can relate EV to a combination of brightness value (Bv) and film/sensor sensitivity (Sv).

In the Additive APEX System, the exposure calculation is defined: Ev = Av + Tv = Bv + Sv. For an in-depth explanation of how to calculate this, please refer to the article linked above. (Or, just read the code below!) It should be fairly obvious that, in such a calculation, if we know any three values — we can determine the fourth.

Our first task will be to calculate the exposure time, in seconds, given the following information: Aperture, ISO, and Lux. For this, and all calculations, we are going to need the Light Meter Calibration Constant (K) – we’ll use the standard constant for reflective meters made by Pentax, because I’m a Pentax guy, which is 14. We will also need to know the relationship between between the ASA Arithmetic Speed Value and the ASA Speed Value, which is approximately 0.3.

So, let’s create a function that will give us the EV for our combination of illuminance and film speed first

Code: Select all


float calc_ev( float lux, int iso ) {

    // calculate EV using APEX method:
    // Ev = Av + Tv = Bv + Sv

    // We'll use the right-hand side for this operation

    // Bv = log2( B/NK )
    // Sv = log2( NSx )

  float Sv = log( (float) 0.3 * (float) iso ) / log(2);

  float Bv = log( lux / ( (float) 0.3 * (float) 14 ) ) / log(2);

  return( Bv + Sv );

}
Note that in the above equation, we use log(x) / log(2) everywhere, this is because the arduino environment does not support log2() by default, and we find that the logarithm of any base other than the natural is the natural logarithm divided by the natural logarithm of the base. So, we get l2(x) by dividing ln(x) by ln(2).

Now that we have the relevant EV, to get the exposure time, in seconds, we need to take our Ev calculated from two of the required three values (Bv, Sv, Av) and compare it to the third to get the new value out.

Code: Select all


float calc_exp_tm ( float ev, float aperture  ) {

    // Ev = Av + Tv = Bv + Sv
    // need to determine Tv value, so Ev - Av = Tv
    // Av = log2(Aperture^2)
    // Tv = log2( 1/T ) = log2(T) = 2^(Ev - Av)

  float exp_tm = ev - ( log( pow(aperture, 2) ) / log(2) );

  float exp_log = pow(2, exp_tm); 

  return( exp_log  );
}
Note that this function returns a floating point number that we would divide 1 second by to get the final exposure time. I.e., it would return 10 for 1/10th second, 1.5 for .5?, and 0.05 for 20?. The following function will make it easy to convert this time into milliseconds for direct exposure control:

Code: Select all


unsigned int calc_exp_ms( float exp_tm ) {

  unsigned int cur_exp_tm = 0;

  // calculate mS of exposure, given a divisor exposure time  

   if( exp_tm >= 2 ) {

      // deal with times less than or equal to half a second

     if( exp_tm >= (float) int(exp_tm) + (float) 0.5 ) {
         // round up
       exp_tm = int(exp_tm) + 1;
     }
     else {
         // round down
       exp_tm = int(exp_tm);
     }

     cur_exp_tm = 1000 / exp_tm;

   }
   else if( exp_tm >= 1 ) {
     // deal with times larger than 1/2 second

     float disp_v = 1 / exp_tm;
       // get first significant digit
     disp_v       = int( disp_v * 10 );
     cur_exp_tm = ( 1000 * disp_v ) / 10;

   }
    else {
       // times larger than 1 second
     int disp_v = int( (float) 1 / exp_tm);
     cur_exp_tm = 1000 * disp_v;

    }

 return(cur_exp_tm);
}
Now we have two functions that will return us the EV equivalent given our brightness and speed values, a third to extrapolate exposure time from EV and aperture, and a fourth to convert exposure time to mS (for the purposes of controlling a camera, etc.).

It should be fairly obvious at this point as to how to extrapolate other values from combinations of three factors, but here’s another function that calculates aperture given the combination of Bv, Sv, and Tv. (i.e. you know exposure time but not aperture):

Code: Select all


float calc_exp_aperture( float ev, float exp_tm ) {

  float exp_apt = ev - ( log( (float) 1 / exp_tm ) / log(2) );
  float apt_log = pow(2, exp_apt);

  return( apt_log );
}
So, there you have it – you now know (well, we hope you do!) how to calculate meaningful photographic data from illuminance data (lux) we were able to calculate in our previous tutorial.

ACCURATE MEASUREMENT OF MULTIPLE WAVELENGTHS

Ok, so it’s time to get a little more complicated here. Up and to this point, we’ve only been calculating the relative intensity of a single wavelength of light. It’s pretty rare that we photograph a subject illuminated by a laser or well-tuned diode that only produces a single wavelength of light. More than likely, we’re photographing in daylight, with a flash, or under hot lights in the studio. To accurately measure these sources, we have to take into account the fact that they are made up of different wavelengths of light, and that each wavelength is more or less efficient (visible to our eyes).

As pointed out previously, we are only going to deal with photoptic vision, that is the way our eyes work in ‘bright’ environments. Our eyes change behavior when the light level drops very low (think in a dark room at night), and scotoptic vision kicks in. We rarely photograph in levels this dark, so we won’t cover this type of vision.

To properly calculate the lux for mixed wavelengths, we need to integrate as such: lx = 683 * ? V(?)J(?) d?

V() is the standard luminosity function we discussed in the earlier post, and J() is the power spectral density function for the given wavelength. The hard part here is the power spectral density function – we either have to calculate for a blackbody radiator at a particular temperature using Planck’s Law (which is fairly difficult on the arduino, and is also highly inaccurate for light bulbs and the like) or, we have to figure out this data empirically. To measure this information for a given light source, we’d have to use a spectrum analyzer (which we can build with a TSL230R, but that’s a topic for another day) on that light source. Fortunately for us, the CIE provides a couple of tables, that give us the PSD values for two illuminants: ‘A’, which corresponds roughly to an incandescent lightbulb, and D65, which corresponds roughly to mid-day sunlight.

We’ll need to create three arrays to perform this calculation, one holds the wavelengths of light we’re willing to calculate for (as we have interest in integrating from 0 to infinity), one that holds the luminous efficiency function for each of those wavelengths, and one that gives us the power spectral density function for each wavelength we want to account for.

So, let’s create our tables and our function:

Code: Select all


  // our wavelengths (nm) we're willing to calculate illuminance for (lambda)

int wavelengths[18] = { 380, 400, 420, 440, 460, 480, 500, 520, 540, 560, 580, 600, 620, 640, 660, 680, 700, 720 };

  // the CIE V(l) for photopic vision - CIE Vm(l) 1978 - mapping to the same (l) above

float v_lambda[18]  = { 0.0002, 0.0028, 0.0175, 0.0379, 0.06, 0.13902, 0.323, 0.71, 0.954, 0.995, 0.87, 0.631, 0.381, 0.175, 0.061, 0.017, 0.004102, 0.001047 };

  // CIE SPD graph for Illuminant A light sources, again mapping to same lambda as included in wavelengths

float ilA_spd[18] = { 9.795100, 14.708000, 20.995000, 28.702700, 37.812100, 48.242300, 59.861100, 72.495900, 85.947000, 100.000000, 114.436000, 129.043000, 143.618000, 157.979000, 171.963000, 185.429000, 198.261000, 210.365000 };

...

float calc_lux_gauss( float uw_cm2 ) {

    // # of wavelengths mapped to V(l) values - better have
    // enough V(l) values!

  int nm_cnt = sizeof(wavelengths) / sizeof(int);

    // W/m2 from uW/cm2

  float w_m2 =  ( uw_cm2 / (float) 1000000 ) * (float) 100;

  float result = 0;

    // integrate XlV(l) dl
    // Xl = uW-m2-nm caclulation weighted by the CIE lookup for the given light
    //   temp
    // V(l) = standard luminous efficiency function

  for( int i = 0; i < nm_cnt; i++) {

    if( i > 0) {
      result +=  ( ilA_spd[i] / (float) 1000000)  * (wavelengths[i] - wavelengths[i - 1]) * w_m2  * v_lambda[i];
    }
    else {
      result += ( ilA_spd[i] / (float) 1000000) * wavelengths[i] * w_m2 * v_lambda[i];
    }

  }

    // multiply by constant Km and return

  return(result * (float) 683);
}
If your background is in anything but calculus or photometry, it can be a bit of a beast to get your head around, but take some time to thoroughly read and understand the above function and tables before using them. Effectively, we are determining the efficiency function by jumping between two points on our wavelength graph, and estimating the efficiency per nm of wavelength for each nm between our current and last point. We sum these up, and then multiple by the efficiency constant of 683 (lux per w/cm2 at 555nm) to determine the lux, given the radiometric energy observed for a particular multiple-wavelength light source. Simply replace any calls to calc_lux_single() with a call to calc_lux_gauss().

That wraps it up for this tutorial on photographic conversions. You now have enough information to build a basic photographic light meter using an Arduino and a TSL230R light sensor. In the next tutorial on this subject, we’ll work on increasing accuracy by calculating for the frequency response curve of the sensor its self, and adjusting for temperature impacts on the dark frequency of the chip.

Re: Taos TSL230R light sensor C tutorial

Posted: Fri Sep 07, 2018 12:47 am
by Aks
Hello Sir,

I would like to use the sensor for sun just for a test of about 5 - 10 minutes. Is it possible?
Thank you.

Re: Taos TSL230R light sensor C tutorial

Posted: Tue Sep 11, 2018 11:07 pm
by SevenZero
Should be possible. Yes.