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.
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
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;
}
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);
}
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);
}
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);
}
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);
}