Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl Monk, Perl Meditation
 
PerlMonks  

comment on

( [id://3333]=superdoc: 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.


In reply to Human-visible light levels - Adafruit Breakout board with I2C by anita2R

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post; it's "PerlMonks-approved HTML":



  • Are you posting in the right place? Check out Where do I post X? to know for sure.
  • Posts may use any of the Perl Monks Approved HTML tags. Currently these include the following:
    <code> <a> <b> <big> <blockquote> <br /> <dd> <dl> <dt> <em> <font> <h1> <h2> <h3> <h4> <h5> <h6> <hr /> <i> <li> <nbsp> <ol> <p> <small> <strike> <strong> <sub> <sup> <table> <td> <th> <tr> <tt> <u> <ul>
  • Snippets of code should be wrapped in <code> tags not <pre> tags. In fact, <pre> tags should generally be avoided. If they must be used, extreme care should be taken to ensure that their contents do not have long lines (<70 chars), in order to prevent horizontal scrolling (and possible janitor intervention).
  • Want more info? How to link or How to display code and escape characters are good places to start.
Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others surveying the Monastery: (4)
As of 2024-03-29 04:43 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found