Code for BH1750 ambient light sensor
AGCB
Posts: 327
in Propeller 1
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
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 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:
I've attached a really simple port expander object that uses I2C to give you a model to work from.
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:
And to go with that, here is a hugely simplified version of your read_words command:
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.
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
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.
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:
Whenever you want a reading:
BTW, In Tachyon that would be, to initialize:
Then define this routine to read:
Here is where I try out the routines and measure the timing and the result from the non-existent chip.
Even so, anytime I ask a question on this forum I learn more than expected. Thanks for that also.
Aaron
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
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.
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