Shop OBEX P1 Docs P2 Docs Learn Events
Code for BH1750 ambient light sensor — Parallax Forums

Code for BH1750 ambient light sensor

I'm trying to improve my weather monitoring system and bought one of the BH1750 sensors to keep track of sunlight. When I looked for some code examples on this forum and the web I could find nothing in SPIN, my favorite. Only found a post about someone looking for STAMP code and one of another using a PICAXX. There is plenty ARDUINO stuff so from those posts and the ARDUINO code I came up with this basic program. I'm just a part time hobbyist so it's just what I'm able to do. Might be that one of you guys would write a professional program for it!
Aaron
CON
{  Uses BH1750 light sensor to read lux of ambient light
I2C slave address is $23      '7 bit is %0100_011
Instructions hex $  (partial list)
  0       Power down
  1       Power on
  7       reset
  10       continuos H mode begining at 1 lux
  11         "       "       "        "  .5 lux
  13      continuos L resolution mode      
  20      one time H mode
  21      one time H mode 2
  23      one time L mode

  In this program  "I2C Spin driver v1.2"  is included in top object

}      
  _clkmode = xtal1 + pll16x      '80 MHz system clock
  _xinfreq = 5_000_000

 
  wrt    = $23      '%0100_0110  i2c write address
  rd     = $22      '%0100_0111  i2c read address
  hcon   = $10      '0001_0000   'continuos high resolution mode starting at 1 lux
  hone   = $21                   'one time high resolution mode starting at 1 lux
  lcon   = $13                   'continuos low resolution mode
  lone   = $23                   'one time low resolution mode
  scl_pin   = 20                 'I2C clock pin
  sda_pin   = 21                 'data pin
  basepin   =  0                 'basepin for TV
  

OBJ
  ' I2C in top object
  tv    :"TV_TEXT"
   
VAR
  byte hbyt, lbyt
  word lux, level
   
PUB Main
  
  tv.start(basepin)
  tv.str(string("tv is working"))    'just to prove
  waitcnt(clkfreq*2+cnt)
  tv.clear  

  repeat
    command(wrt,$01)   'power-on command if using one time mode, else comment out 'includes Stop command   
    command(wrt,hone)  'one time high resolution mode starting at 1 lux   'includes Stop command
    'command(wrt,lone) 'one time low resolution mode      'includes Stop command
      
    waitcnt(clkfreq+cnt)
    tv.clear    
    hbyt:=read(wrt,rd)    'read(device,address)    'includes Stop command
    'tv.bin(hbyt,8)       'TV instructions to show LSB and MSB commented out
    lbyt:=read_next(wrt)  'read_next(device)       'includes Stop command
    'tv.out(4)
    'tv.bin(lbyt,8)
    level:= lbyt | hbyt<<8    'OR low byte with high byte shifted left 8 places to get 16 bits
    lux:=level/12*10     ' level / 1.2
    tv.move(5,18)        'display mid screen
    tv.dec(lux)     
    
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
'I2C object in top object
PUB write(device,address,data)                                                  ' Write a single byte

  send_start(device,address)                                                    ' Send a start bit, device ID, and register address
  I2C_write(data)                                                               ' Send the data byte
  I2C_stop                                                                      ' Send a stop bit
  result := true
  
PUB write_page(device,address,data_address,bytes)                               ' Write many bytes

  send_start(device,address)                                                    ' Send a start bit, device ID, and register address    
  repeat bytes                                                                                                                         
    I2C_write(byte[data_address])                                               ' Send the data byte from an array                     
    data_address++                                                             
  I2C_stop                                                                      ' Send a stop bit 
  result := true

PUB command(device,comm)                                                        ' Write the device and address, no data.  Used in the altimeter

  send_start(device,comm)                                                       ' Send a start bit, device ID, and command
  I2C_stop                                                                      ' Send a stop bit
  result := true
                                                            
PUB read(device,address)                                                        ' Read a single byte

  send_start(device,address)                                                    ' Send a start bit, device ID, and register address  
  I2C_start                                                                     ' Send a restart
  I2C_write(device << 1 | 1)                                                    ' Send the device ID with the read bit set
  result := I2C_read                                                            ' Read a byte into the result
  I2C_nak                                                                       ' Send a NAK bit
  I2C_stop                                                                      ' Send a stop bit

PUB read_next(device)                                                           ' Read from next address

  I2C_start                                                                     ' Send a start bit
  I2C_write(device << 1 | 1)                                                    ' Send the device ID with the read bit set
  result := I2C_read                                                            ' Read a byte into the result 
  I2C_nak                                                                       ' Send a NAK bit              
  I2C_stop                                                                      ' Send a stop bit             

PUB read_page(device,address,data_address,bytes)                                ' Read many bytes

  send_start(device,address)                                                    ' Send a start bit, device ID, and register address   
  I2C_start                                                                     ' Send a restart                                      
  I2C_write(device << 1 | 1)                                                    ' Send the device ID with the read bit set            
  repeat bytes                                                                                                                        
    byte[data_address] := I2C_read                                              ' Read a byte into an array
    if bytes-- > 1                                                                                                                    
      I2C_ack                                                                   ' Send an ACK bit if more bytes are to be read
    else
      I2C_nak                                                                   ' Otherwise, send a NAK bit
    data_address++                                                              
  I2C_stop                                                                      ' Send a stop bit
  dira[sda_pin]~
  result := true

PUB read_words(device,address,data_address,words)                               ' Read many words - written specifically for devices that store readings as high_byte,low_byte   ($01,$23)
                                                                                '  If read into an array using the read_page method, the byte order would be reversed when trying to operate on the word ($2301)
  send_start(device,address)                                                    ' Send a start bit, device ID, and register address        
  I2C_start                                                                     ' Send a restart                                           
  I2C_write(device << 1 | 1)                                                    ' Send the device ID with the read bit set                 
  repeat words                                                                                                                             
    word [data_address] := I2C_read << 8                                        ' Read a byte and store in the hi-byte of a word
    I2C_ack                                                                     ' Send an ACK bit to advance the slave to the low-byte     
    word[data_address] := word[data_address] | I2C_read                         ' Read a byte and store in the low-byte of a word          
    if words-- > 1                                                                                                                         
      I2C_ack                                                                   ' Send an ACK bit if more bytes are to be read             
    else                                                                                                                                   
      I2C_nak                                                                   ' Otherwise, send a NAK bit                                
    data_address += 2                                                                                                                 
  I2C_stop                                                                      ' Send a stop bit                                     
  dira[sda_pin]~
  result := true      

PRI send_start (device, address) | NAK, t

  nak := 1
  t := cnt 

  repeat while nak                                                              
    if cnt - t > clkfreq / 100
      abort false
    I2C_start
    nak := I2C_write(device << 1)
  'if device & %1111_1000 == EEPROM
  '  I2C_write(address >> 8)
  I2C_write(address)  
  
PRI I2C_start                                                                   ' scl_pin 
  dira[scl_pin] := 0                                                                ' sda_pin 
  waitpeq(|<scl_pin,|<scl_pin,0)
  dira[sda_pin] := 0                                                                ' Who woulda thunk that 'dira[sda_pin] := 0' executes faster than 'dira[sda_pin]~'
  dira[sda_pin] := 1
  dira[scl_pin] := 1

PRI I2C_write(data)                                                             '   (Write)      (Read ACK or NAK)
                                                                                '                      
  data := (data ^ $FF)<< 24                                                     ' scl_pin    
  repeat 8                                                                      ' sda_pin  ───────            
    dira[sda_pin] := data <-= 1                                                     
    dira[scl_pin] := 0                                                                                                 
    waitpeq(|<scl_pin,|<scl_pin,0)
    dira[scl_pin] := 1

  dira[sda_pin] := 0
  dira[scl_pin] := 0
  waitpeq(|<scl_pin,|<scl_pin,0)
  result := ina[sda_pin]
  dira[scl_pin] := 1
  
PRI I2C_read                                                                    '      (Read)  
  dira[sda_pin] := 0                                                                '             
  repeat 8                                                                      ' scl_pin  
    dira[scl_pin] := 0                                                              ' sda_pin ───────
    waitpeq(|<scl_pin,|<scl_pin,0)
    result := result << 1 | ina[sda_pin]
    dira[scl_pin] := 1
    
PRI I2C_ack                                                                     ' scl_pin           
  dira[sda_pin] := 1                                                                ' sda_pin 
  dira[scl_pin] := 0
  waitpeq(|<scl_pin,|<scl_pin,0)
  dira[scl_pin] := 1

PRI I2C_nak                                                                     ' scl_pin 
  dira[sda_pin] := 0                                                                ' sda_pin 
  dira[scl_pin] := 0
  waitpeq(|<scl_pin,|<scl_pin,0)
  dira[scl_pin] := 1

PRI I2C_stop                                                                    ' scl_pin 
  dira[sda_pin] := 1                                                                ' sda_pin 
  dira[scl_pin] := 0
  waitpeq(|<scl_pin,|<scl_pin,0)
  dira[sda_pin] := 0

{{
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                   TERMS OF USE: MIT License                                                  │                                                            
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation    │ 
│files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy,    │
│modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software│
│is furnished to do so, subject to the following conditions:                                                                   │
│                                                                                                                              │
│The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.│
│                                                                                                                              │
│THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE          │
│WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR         │
│COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,   │
│ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                         │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
}}  

Comments

  • I don't have time to write your whole program (unless you agree to my rate <grin>), but I will offer up my generic I2C code that I've used in lots of personal projects and commercial devices.

    I would suggest you create a BH1750 object that uses a generic I2C object, then attach your BH1750 object to your application. The "problem" with referring to Arduino examples -- in my opinion -- is that they are just examples and many are not suited for real-world applications. It takes a bit of work to do what I'm suggesting, but in the end you'll have cleaner code that will be easier to build on. Structurally, your code should look like:
    Application
    |
    +-- BH1750
        |
        +-- I2C
    

    I've attached a really simple port expander object that uses I2C to give you a model to work from.
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2020-09-28 19:19
    Any success at all with the program you wrote?

    I've written code for other i2c light sensors (e.g. Intersil/Renesas ISL29033), so I took a quick look at the BH1750 to look for similarities. They are quite different though. The command set for reading the BH1750 is quite simple really. Much of the i2c code you have ported is superfluous.

    One thing I see in your code is that it is reading the high byte and the low byte with two separate commands. However, one command should return both the high and low byte into a 16 bit word. More like this in the loop:
        command($23,$01)   'power-on command, wakes up the sensor
        command($23,$20)  'one time high resolution mode starting at 1 lux
        waitcnt(clkfreq/5+cnt).  ' conversion time <200ms
        result := read_word($23)   'grabs the data word and and shuts down the sensor
    

    And to go with that, here is a hugely simplified version of your read_words command:
    PUB read_word(device)                               ' 
      I2C_start                          
      I2C_write(device << 1 | 1)     ' Send the device ID with the read bit set                 
      result := I2C_read << 8         ' Read a byte and store in the hi-byte of a word
      I2C_ack  
      result |=  I2C_read                ' Read a byte and store in the low-byte of a word          
      I2C_nak                                 ' done                            
      I2C_stop                               
    

    I haven't tried it of course, but I think all you really need is the two higher level methods, command and read_word, and the lower level routines that they call.
  • Tracy
    Yes, I am able to read the lux in all modes. Only it doesn't go below about 300 lux and above about 50,000 it seems that the sensor is over run and the value of steady light drops continuously.

    I'll try your code. It does look simple.

    It looks like the methods you are calling must be in the top object. I looked for an I2C object that had the methods I thought I needed and wrote it into the top object and was planning on getting rid of everything that wasn't needed. I had not done that yet hence long superfluous code.

    Thanks for your time and thoughts.
    Aaron
  • ChrisGaddChrisGadd Posts: 310
    edited 2020-09-29 14:57
    Based on the datasheet, I am surprised that code produced a useful result. The BH1750 appears to be another device that follows the i2c standard not at all.
    Writing is done by sending the device ID and a one byte command, and reading is done by sending the ID with read-bit set, and then immediately returns two bytes of data.
    You are using the i2c.command method I intended for the writes, but the read method sends ID and a register address, then retrieves a data byte. So when you send hbyt:=read(wrt,rd) you're using rd as an address register that the BH1750 doesn't possess. Also, my i2c driver only requires a single 7-bit id; the read and write methods handle the r/w bit automatically.
    It seems to me that in order to work, you should use readNext(deviceID) for both bytes. Tracey's read_word method also looks like it would work, as it omits the address register.

    I don't understand your reason for moving all of the low-level i2c methods to the top object. The i2c driver is intended to be used as a child object, much like the tv object you're using.

    Unless I'm seriously running low on space, I would never consider trimming code out of objects. While 32KB seems tiny in the age of 64GB or more PCs, I've yet to ever run out. And if you do in fact need the space, then I believe PropellerIDE has an option to automatically eliminate unused methods when compiling.

    Finally, you're dividing the result as lux:=level/12*10 ' level / 1.2 which is going to result in losing some lsbs. Instead, multiply by 10 first, and then divide by 12.


  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2020-09-29 14:39
    I just read the datasheet for this part from Rohm, and all I can say is I'm not impressed at all. The biggest problems are due to poor design and poor documentation. It doesn't seem to mention how you know for certain when the chip has a new reading other than waiting for a long time. Normally if an I2C device is busy it will not ack its device address, so this should be the method described, but it isn't. Instead they offer the impractical wait 180ms (that's forever in processor terms). Then there is the dumb DVI input that needs to be pulsed to reset the device although you could use an RC to do that if the supply rise time is not too slow.

    Anyway, I'd recommend just sending out the "continuous high res read" command at startup and then whenever you want a reading, just read the two bytes as that will have the latest reading. There are no special I2C routines required for this device and doing it this way simplifies everything and you don't need to wait, except for maybe the first reading but if you send out the "continuous read" command just after startup and have some kind of start-up delay before starting your main loop, then this should be the only delay necessary. Worst that could happen is the first reading is "something" :)

    To initialize:
    START SEND($46) (ackd) SEND($10) (ackd) STOP
    

    Whenever you want a reading:
    START SEND($47) (ackd) GET(HBYTE) NAK GET(LBYTE) NAK STOP
    RESULT = (HBYTE<<8 + LBYTE) * 10 / 12
    

    BTW, In Tachyon that would be, to initialize:
    $10 $46 IO!
    

    Then define this routine to read:
    pub LIGHT@ ( -- level )   <I2C $47 I2C! ackI2C@ 8<< nakI2C@ + I2C>   10 * 12 / ;
    

    Here is where I try out the routines and measure the timing and the result from the non-existent chip.
    TF5> $10 $46 IO! ---  ok
    TF5> pub LIGHT@ ( -- level )   <I2C $47 I2C! ackI2C@ 8<< nakI2C@ + I2C>   10 * 12 / ;
    TF5> LIGHT@ . --- 54612  ok
    TF5> LAP LIGHT@ LAP .LAP --- 19,712 cycles = 197.120us  ok
    
  • Thanks all for the info. As I said in post #1 " I'm just a part time hobbyist". I do agree that this module is less than I had hoped for. I'll be researching to find a better one. In the mean time I'll try out some of this new code.

    Even so, anytime I ask a question on this forum I learn more than expected. Thanks for that also.
    Aaron
  • AGCBAGCB Posts: 327
    edited 2020-09-30 12:07
    Here's the latest with help from above posters.

    It now works down to 1 lux (hand nearly covering sensor) to mid 50K's (my brightest flash light at 1/2").

    In the weather program, I would as Peter said "send out the "continuous read" command just after startup and have some kind of start-up delay before starting your main loop, then this should be the only delay necessary" and then just read it periodically and do 'what ever'.

    Thanks much
    Aaron
    CON
    {  Uses BH1750 light sensor to read lux of ambient light
    
    Much help on this iteration from Parallax forum members: JonnyMac, TracyAllen, ChrisGadd, Peter Jakacki
    
    I2C slave address is $23      '7 bit is %0100_011
    Instructions hex $  (partial list)
      0       Power down
      1       Power on
      7       reset
      10       continuos H mode begining at 1 lux
      11         "       "       "        "  .5 lux
      13      continuos L resolution mode      
      20      one time H mode
      21      one time H mode 2
      23      one time L mode
    
      In this program partial "I2C Spin driver v1.2"  is included in top object
    }      
      _clkmode = xtal1 + pll16x      '80 MHz system clock
      _xinfreq = 5_000_000 
     
      wrt    = $23      '%0100_0110  i2c write address
      rd     = $22      '%0100_0111  i2c read address
      hcon   = $10      '0001_0000   'continuos high resolution mode starting at 1 lux
      hone   = $21                   'one time high resolution mode starting at 1 lux
      lcon   = $13                   'continuos low resolution mode
      lone   = $23                   'one time low resolution mode
      scl_pin   = 20                 'I2C clock pin
      sda_pin   = 21                 'data pin
      basepin   =  0                 'basepin for TV
      
    
    OBJ
     ' i2c   :"I2C Spin driver v1.2"  (partial included in top object)
      tv    :"TV_TEXT"
       
    VAR
      byte hbyt, lbyt
      word lux, level
       
    PUB Main  
      tv.start(basepin)
      tv.str(string("tv is working"))    'just to prove
      waitcnt(clkfreq*2+cnt)
      tv.clear  
    
      repeat
        'command(wrt,$01)   'power-on command if using one time mode, else comment out 'includes Stop command   
        'command(wrt,hone)  'one time high resolution mode starting at 1 lux   'includes Stop command
        'command(wrt,lone) 'one time low resolution mode      'includes Stop command
    
        command(wrt,$01)   'power-on command, wakes up the sensor
        command(wrt,$10)   'continuos high resolution mode starting at 1 lux
        waitcnt(clkfreq/5+cnt)  ' conversion time <200ms
        'result := read_word($23)   'grabs the data word and and shuts down the sensor
          
        waitcnt(clkfreq+cnt)
        tv.clear    
        
        'level:=read_word(wrt)
        lux:=read_word(wrt)*10/12     ' level / 1.2
        tv.move(5,18)        'display mid screen
        tv.dec(lux)
        
    PUB read_word(device)                               ' 
      I2C_start                          
      I2C_write(device << 1 | 1)     ' Send the device ID with the read bit set                 
      result := I2C_read << 8         ' Read a byte and store in the hi-byte of a word
      I2C_ack  
      result |=  I2C_read                ' Read a byte and store in the low-byte of a word          
      I2C_nak                                 ' done                            
      I2C_stop 
    
    PUB command(device,comm)                                                        ' Write the device and address, no data.  Used in the altimeter
    
      send_start(device,comm)                                                       ' Send a start bit, device ID, and command
      I2C_stop                                                                      ' Send a stop bit
      result := true                                                            
    
    PRI send_start (device, address) | NAK, t 
      nak := 1
      t := cnt 
    
      repeat while nak                                                              
        if cnt - t > clkfreq / 100
          abort false
        I2C_start
        nak := I2C_write(device << 1)
      
      I2C_write(address)  
      
    PRI I2C_start                                                                   ' scl_pin 
      dira[scl_pin] := 0                                                                ' sda_pin 
      waitpeq(|<scl_pin,|<scl_pin,0)
      dira[sda_pin] := 0                                                                ' Who woulda thunk that 'dira[sda_pin] := 0' executes faster than 'dira[sda_pin]~'
      dira[sda_pin] := 1
      dira[scl_pin] := 1
    
    PRI I2C_write(data)                                                             '   (Write)      (Read ACK or NAK)
                                                                                    '                      
      data := (data ^ $FF)<< 24                                                     ' scl_pin    
      repeat 8                                                                      ' sda_pin  ───────            
        dira[sda_pin] := data <-= 1                                                     
        dira[scl_pin] := 0                                                                                                 
        waitpeq(|<scl_pin,|<scl_pin,0)
        dira[scl_pin] := 1
    
      dira[sda_pin] := 0
      dira[scl_pin] := 0
      waitpeq(|<scl_pin,|<scl_pin,0)
      result := ina[sda_pin]
      dira[scl_pin] := 1
      
    PRI I2C_read                                                                    '      (Read)  
      dira[sda_pin] := 0                                                                '             
      repeat 8                                                                      ' scl_pin  
        dira[scl_pin] := 0                                                              ' sda_pin ───────
        waitpeq(|<scl_pin,|<scl_pin,0)
        result := result << 1 | ina[sda_pin]
        dira[scl_pin] := 1
        
    PRI I2C_ack                                                                     ' scl_pin           
      dira[sda_pin] := 1                                                                ' sda_pin 
      dira[scl_pin] := 0
      waitpeq(|<scl_pin,|<scl_pin,0)
      dira[scl_pin] := 1
    
    PRI I2C_nak                                                                     ' scl_pin 
      dira[sda_pin] := 0                                                                ' sda_pin 
      dira[scl_pin] := 0
      waitpeq(|<scl_pin,|<scl_pin,0)
      dira[scl_pin] := 1
    
    PRI I2C_stop                                                                    ' scl_pin 
      dira[sda_pin] := 1                                                                ' sda_pin 
      dira[scl_pin] := 0
      waitpeq(|<scl_pin,|<scl_pin,0)
      dira[sda_pin] := 0
    
    
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2020-09-30 20:04
    Congratulations on getting it working! I presume you are using a breakout board, I'm wondering where from?

    Agreeing with Peter here, the data sheet for the sensor is terrible, more than an issue of translation, and the design is strange. For example, there is that DVI reset pin--The breakout board must handle that with an RC circuit or something. The statements about the resolution and the modes are confusing, as is the business about changing the sensitivity via the integration time register. Have to read between the lines and experiment.
    However, the i2c interface is about as simple as it gets.

    The main thing for the weather station is, does it give accurate results for your sunlight measurement? What aspect of sunlight? A measurement of lux gets at how well a person can see. Alternatively, there could be measurement of solar energy to get at the heating energy in watts per square meter, or PAR in µmoles per square meter per second to get at at potential plant growth. They are three different but largely correlated measurements. There are lots of other conehead possibilities. Net radiation, incoming minus outgoing light and heat, etc. Full sunlight is over 100000 lux, so this sensor with a range up to 65565 lux would need to be tuned or filtered to catch the peaks. Most of these sensors are meant for controlling screen brightness under relatively low levels well under 10klux.

  • >>> I presume you are using a breakout board, I'm wondering where from?
    China, where else

    >>>What aspect of sunlight?
    I'm weather buff and just want to record peak brightness times and radiation heat. Watch how the seasons progress etc.
    I have a weather station now on a QuickStart board and another temperature monitoring program on another QuickStart. That one keeps track of my outdoor wood furnace and to some extent controls it. I'd like to ad the solar radiation, ground temp at several depths, rain rate and totals, indoor humidity and ground moisture and whatever else sounds fun.

    I put this light sensor and QuickStart board on an old laptop that has Propeller Tool on it and took it outside today. I'll probably need to put a darkening screen on the sensor but I'm really not interested in exact lux values, only differences from hour to hour and day to day. So I'm satisfied!
    Thanks for your interest
    Aaron

    PS I guess the forum won't let me attach an image w/o a URL
  • Oh here we go
    3396 x 2341 - 930K
    3644 x 2300 - 1M
Sign In or Register to comment.