#!/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 = ''; my $group = ''; # 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 off!\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; } # ****************************************** #