Beefy Boxes and Bandwidth Generously Provided by pair Networks
more useful options
 
PerlMonks  

Human-visible light levels - Adafruit Breakout board with I2C

by anita2R (Scribe)
on Feb 12, 2017 at 19:52 UTC ( [id://1181859]=CUFP: print w/replies, xml ) Need Help??

AdaFruit sells a 'breakout' board for the dual-sensor TLS2561 Light-to-Digital Converter. The board provides i2c communicatiion and 3.3 or 5 volt operation.

Adafruit provides software suitable for the Arduino and the sensor manufacturer provides some pseudo-code and code suitable for microprocessors. As a Raspberry Pi user I needed a Linux solution, so I have produced a short Perl script to obtain light levels in Lux, from the board.

The TLS2561 uses 2 sensors, one of which only responds to infrared, so making it possible to get an approximation of human-visible light levels, by subtraction the infrared value from the infrared + visible value, (plus some mathematical manipulations).

Before using the script, the user needs to replace <username> and <group> with their own username and group as the script must be called as root or using sudo, and once the i2c object is created the script falls back to the user's permissions.

The script can be called 'as is' and default values will be used, or parameters can be passed which change the sensor sensitivity and the integration (sensing) time which affects the available range from the chip's two ADC's.

The sensitivity -s parameter takes values of 0 or 1 (normal or x16 sensitivity).
The integration -i parameter takes values of 0, 1 or 2 (13.7mS, 101mS or 402mS).
If the script is called with the 'verbose' -v parameter on its own or with -s &/or -i, additional information is printed, including the raw sensor values.

The math for doing the Lux calculations comes from the TLS2561 datasheet, which hopefully I have interpreted correctly! As I don't have a Lux meter, I can't be sure, but the results under different lighting conditions appear 'reasonable'.

Script - readI2cLux.pl:

#!/usr/bin/perl # # readI2cLux.pl # Version 1.00 # 12 February 2017 # # A script to read the light levels from a 2-sensor TSL2561 # mounted on an Adafruit breakout board (#439) # # The board is at address 0x39 on the Raspberry Pi's i2c bus #1 # This script obtains the on-chip ADC values from both sensors # and converts the values to a 'visible light' value in Lux # # Channel 0 responds to visible + infrared wavelengths # Channel 1 responds to infrared wavelengths only # The difference can be used to approximate human visible light levels # The sensing device is a "TSL2561_FN" according to the Adafruit # breakout board information at: # https://github.com/adafruit/ # TSL2561-breakout-board-PCB/blob/master/TSL2561_5V_REV-A.brd # # The TSL2561 can be sent instructions to change gain and # change integration time - which changes the effective sensitivity # # With maximum time to sample, the ADC conversion is 16 bits # but at the shortest sampling period the range is only 0 - 5047. # Sensor saturation can be assessed by testing for the # maximum ADC value for the selected integration time. # # The light level is calculated according to the information in the # TLS2561 datasheet from the manufacturer # Texas Advanced Optoelectronic Solutions # using the T/FN/CL Package dataset, which can be found at: # https://www.adafruit.com/datasheets/TSL2561.pdf # # Command line parameters can be passed: # -i 0, 1 or 2 to set short, medium or long integration time # -s 0 or 1 to set normal or high sensitivity # -v verbose - to print a number of interim values & settings # use HiPi::Utils; use HiPi::BCM2835::I2C qw( :all ); use Getopt::Long; # use strict; use warnings; # # ********************************************************** # # *** User entered values required before running script *** # # Setup regular user & group id's for permission drop-back my $user = '<username>'; my $group = '<usergroup>'; # Setup bus number (#1 is used on all recent RPi's) my $i2c_bus = BB_I2C_PERI_1; # Setup sensor i2c address - default is 0x39 my $i2c_addr = 0x39; # alternatives 0x29 and 0x49 can be set on breakout board # # ********************************************************** # # Create an i2c device object my $objI2C = HiPi::BCM2835::I2C->new( peripheral => $i2c_bus, address => $i2c_addr ); # Now drop back to regular user HiPi::Utils::drop_permissions_name( $user, $group ); # Get the command line parameters GetOptions( 'i=i' => \my $integ, 's=i' => \my $sens, 'v' => \my $verbose ); # Initialize ( $sens, $integ ) = init( $sens, $integ ); # Delay before reading delay( $integ ); # Read sensors / test for saturation my ( $ch0, $ch1 ) = readSn( $integ ); # Scale raw results for sensitivity and integration time ( $ch0, $ch1 ) = scale( $ch0, $ch1, $sens, $integ ); # Convert channel values into a single Lux value my $lux = convert( $ch0, $ch1 ); # Report lux value to nearest whole number my $box = '*' x ( 29 + length( int( $lux ))); print "\n$box\n"; printf "* Visible light level: %u lux *\n" , $lux; print "$box\n\n"; # Send 0x00 to control register to put device into standby mode $objI2C->bus_write( 0x80, 0x00 ); # exit 0; # ****************************************** # # ******** Subroutines / Functions ********* # # ****************************************** # # # *************** Initialize *************** # sub init { my $sn = $_[0]; # sensitivity parameter - if any my $ig = $_[1]; # integration parameter - if any # send 0x03 to control register to bring device out of standby $objI2C->bus_write( 0x80, 0x03 ); # read register 0x1 ('Timing') to get current settings # of sensitiviy and integration time my @tm_reg = $objI2C->bus_read( 0x81, 1 ); printf "Timing register (old):\t%08b\n" , $tm_reg[0] if $verbose; # set sensitivity # 0 = 1x # 1 = 16x if( ! defined $sn ) { # no -s parameter - use existing setting # get sensitivity value from timing register (bit 4) $sn = ( $tm_reg[0] & 0b00010000 ) >> 4; print "Using prev. sensitivity:$sn\n" if $verbose; } elsif(( $sn != 0 ) && ( $sn != 1 )) { $sn = 0; print "Not a valid sensitivity value (0 or 1)\n"; print "Sensitivity has been set to 0 (normal sensitivity)\n"; } # set integration time # 0 = 13.7ms # 1 = 101ms # 2 = 402ms if( ! defined $ig ) { # no -i parameter so use existing setting # get integration value from timing register (bits 0 & 1) $ig = $tm_reg[0] & 0b00000011; print "Using prev. integration:$ig\n" if $verbose; } elsif(( $ig != 0 ) && ( $ig != 1) && ( $ig != 2 )) { $ig = 2; print "Not a valid integration value (0, 1 or 2)\n"; print "Integration has been set to 2 (402 mS)\n"; } # create new value for timing register my $tm_reg_new = (( $sn << 4 ) | $ig ); printf "Timing Register (new):\t%08b\n", $tm_reg_new if $verbose; # write to timing register $objI2C->bus_write( 0x81, $tm_reg_new ); return( $sn, $ig ); } # ***************** Delay ****************** # sub delay { my $ig = $_[0]; # integration value # delay before read # hash of delays for each integration value my %delay = ( 0, 25, 1, 115, 2, 425 ); $objI2C->delay( $delay{ $ig } ); print "Delay:\t\t\t$delay{ $ig } mS\n" if $verbose; return; } # ************** Read Sensors ************** # sub readSn { my $ig = $_[0]; # integration value # get combined data - 4 bytes starting at register 0xC (Ch. 0 LSB) # this bus_write sets the 'read word' bit to 1 (bit 5) # and the start address to 0xC (bits 0-3) # bit 7 must be 1 (see datasheet) $objI2C->bus_write( 0xAC ); # note that this device ignores the read address in bus_read # reading is from the address set using the above write command my @comb = $objI2C->bus_read( 0, 4 ); my $raw0 = ( $comb[1] * 256 ) + $comb[0]; my $raw1 = ( $comb[3] * 256 ) + $comb[2]; print "Channel 0 raw value:\t$raw0\n" if $verbose; print "Channel 1 raw value:\t$raw1\n" if $verbose; # test for saturation # hash of maximum ADC values for each integration value my %max_adc = ( 0, 5047, 1, 37177, 2, 65535 ); my $satn = $max_adc{ $ig }; print "Saturation value:\t$satn\n" if $verbose; if(( $raw0 >= $satn ) || ( $raw1 >= $satn )) { # one or both sensors saturated - stop now! print "Sensor saturation - change sensitivity or turn the lights o +ff!\n\n"; exit 1; } return( $raw0, $raw1 ); } # ***************** Scale ****************** # sub scale { my $chn0 = $_[0]; # channel 0 raw value my $chn1 = $_[1]; # channel 1 raw value my $sn = $_[2]; # sensitivity my $ig = $_[3]; # integration # adjust for sensitivity if( $sn == 0 ) { $chn0 = ( $chn0 * 16 ); $chn1 = ( $chn1 * 16 ); } # adjust for integration time # 13.7mS scales: 322/11 my $tint0 = ( 322 / 11 ); # 101mS scales: 322/81 my $tint1 = ( 322 / 81 ); # 402mS does not require scaling (322/322) # 402mS is 322*918/735 # where 735 is nominal oscillator frequency in KHz # Number of clock cycles is 918 * 11, 81 or 322 # 13.7mS is 11*918/735 & 101mS is 81*918/735 if( $ig == 0 ) { $chn0 = ( $chn0 * $tint0 ); $chn1 = ( $chn1 * $tint0 ); } elsif( $ig == 1 ) { $chn0 = ( $chn0 * $tint1 ); $chn1 = ( $chn1 * $tint1 ); } printf "Channel 0 scaled value:\t%u\n", $chn0 if $verbose; printf "Channel 1 scaled value:\t%u\n", $chn1 if $verbose; return( $chn0, $chn1 ); } # **************** Convert ***************** # sub convert { my $chn0 = $_[0]; # scaled channel 0 value my $chn1 = $_[1]; # scaled channel 1 value # Ch. 1 / Ch. 0 ratio # make sure channel 0 value is not zero (divide by zero error) my $ch_ratio; if( $chn0 > 0 ) { $ch_ratio = ( $chn1 / $chn0 ); printf "Ratio is:\t\t%.3f\n", $ch_ratio if $verbose; } else { # warn and exit - can't compute a value print "Channel 1 value is zero - can't compute lux\n\n"; exit 2; } # Can't compute visible Lux if ratio < 0 if( $ch_ratio < 0 ) { print "Channel ratio is less than zero - can't compute lux\n\n +"; exit 3; } # lux is calculated using empirical values # which depend on the channel ratio in defined ranges. # The values all come from the TSL2561 datasheet (T/FN/CL dataset) my $vis_lux; if( $ch_ratio <= 0.5 ) { $vis_lux = ( $chn0 * 0.03040 ) - ( $chn0 * 0.062 * ( $ch_ratio + ** 1.4 )); } elsif( $ch_ratio > 0.5 && $ch_ratio <= 0.61 ) { $vis_lux = ( $chn0 * 0.02240 ) - ( $chn1 * 0.031 ); } elsif( $ch_ratio > 0.61 && $ch_ratio <= 0.8 ) { $vis_lux = ( $chn0 * 0.01280 ) - ( $chn1 * 0.0153 ); } elsif( $ch_ratio > 0.8 && $ch_ratio <= 1.3 ) { $vis_lux = ( $chn0 * 0.00146 ) - ( $chn1 * 0.00112 ); } else { $vis_lux = 0; } return $vis_lux; } # ****************************************** #

Sample calls:
Standard sensitivity and shortest sampling duration:
sudo readI2cLux.pl -s 0 -i 0
x16 sensitivity and longest sampling duration:
sudo readI2cLux.pl -s 1 -i 2
Use default or last applied settings and get some additional feedback
sudo readI2cLux.pl -v

Example output with -v
sudo readI2cLux.pl -s 1 -i 1 -v

Timing register (old): 00010010 Timing Register (new): 00010001 Delay: 115 mS Channel 0 raw value: 4514 Channel 1 raw value: 298 Saturation value: 37177 Channel 0 scaled value: 17944 Channel 1 scaled value: 1184 Ratio is: 0.066 ******************************** * Visible light level: 520 lux * ********************************

The breakout board from AdaFruit also includes an interrupt pin, but I have not programmed for its use, and the pin does not need to be connected. Also the adjacent 3vo pin can be left disconnected - the supply goes to the Vin pin.

Leaving the 'Addr' pin disconnected selects the default 0x39 device address on the i2c bus.

Any suggestions for improvements in the code or the math would be welcome.

Replies are listed 'Best First'.
Re: Human-visible light levels - Adafruit Breakout board with I2C
by stevieb (Canon) on Feb 13, 2017 at 14:38 UTC

    This is great, and I even bought a couple so I can include this hardware in my Raspberry Pi work I've been doing. I don't have any comments on the code as of yet... I'll report back here after the units arrive and I've had time to read the datasheet and do some testing.

    If you do a fair amount of work on stuff like this, you may be interested in checking out some of my mentioned RPi stuff in my CPAN home page (the RPi::* and WiringPi::API may be the ones of interest).

    I do have proper light meters as the purpose for my RPi work started off as a project to automate indoor grow rooms, but this hardware will fit in nicely (I admit I was having so much fun writing this code, that I would randomly just buy different ICs just to get familiar with programming them). There are apps out there for smartphones that are reasonably accurate (at least accurate enough to tell whether your numbers are within a reasonable range). You may want to take a look at some of them.

      Thanks for the suggestion about an app to measure light levels.

      stevieb

      "There are apps out there for smartphones that are reasonably accurate (at least accurate enough to tell whether your numbers are within a reasonable range)."

      A quick check with an app for my ipad shows that the results from the breakout board are in the right ballpark.
      I need to test over a wider range of light intensities, but now I feel happier that I haven't got it completely wrong!

        I've found that dealing directly with hardware and reading registers yourself can be tedious and frustrating, especially when it's difficult to find reference points (testing lux is definitely one of these times, as someone across the 'net may have several 1kW bulbs burning, but their bulbs may have x, or y, or z or any number of other spectrum-diffusing things happening).

        I'm glad the recommendation for an app helped. After my units arrive, I'll drum up some code being non-biased, and we can compare some numbers (again though, I'm sure my light source'll be different from yours. However, there's always one that is pretty consistent ;)

        I often write C against my Arduino that has analog inherent, then translate the C code to something I can provide a Perl API for, so I get results on two separate platforms (for instance, my ADCs/dpots I test against the Arduino, then port to the Rasperry Pi). I'm looking forward to getting these units so I can have someone live-time to compare numbers with and discuss.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: CUFP [id://1181859]
Approved by marto
Front-paged by Arunbear
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others browsing the Monastery: (6)
As of 2024-03-19 02:35 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found