Skip to content

Commit 50b7412

Browse files
author
Al Hoang
committed
bh1745 driver and driver example
Assisted-by: Claude Code
1 parent 04acd8e commit 50b7412

3 files changed

Lines changed: 344 additions & 0 deletions

File tree

bh1745/bh1745.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Package bh1745 implements a driver for the bh1745 color sensor
2+
// It detects 16-bit RGBC (Red, Green, Blue, Clear) color over
3+
// I2C interface.
4+
//
5+
// Datasheet: https://www.mouser.co.uk/datasheet/2/348/bh1745nuc-e-519994.pdf
6+
//
7+
// Pimoroni bh1745-python driver
8+
// https://github.com/pimoroni/bh1745-python/blob/main/bh1745/__init__.py
9+
10+
package bh1745 // import "tinygo.org/x/drivers/bh1745"
11+
12+
import (
13+
"math"
14+
"time"
15+
16+
"tinygo.org/x/drivers"
17+
"tinygo.org/x/drivers/internal/legacy"
18+
)
19+
20+
// RGBCData holds one set of raw channel readings.
21+
type RGBCData struct {
22+
R, G, B, C uint16
23+
}
24+
25+
// Device is the BH1745 RGBC colour sensor driver.
26+
type Device struct {
27+
bus drivers.I2C
28+
Address uint16
29+
30+
r, g, b, c uint16 // cached raw RGBC readings from the last Update call
31+
}
32+
33+
// New creates a new BH1745 driver. The I2C bus must already be configured.
34+
// Call Configure() before calling Read().
35+
func New(bus drivers.I2C) Device {
36+
return Device{
37+
bus: bus,
38+
Address: Address,
39+
}
40+
}
41+
42+
// Configure resets the sensor and sets it up with default settings:
43+
// 160 ms measurement time, 1× ADC gain, RGBC measurement enabled.
44+
//
45+
// The undocumented write of 0x02 to MODE_CONTROL3 (register 0x44) is included
46+
// here; without it the sensor reports incorrect RGBC values. This matches the
47+
// workaround applied in Pimoroni's Enviro Indoor firmware.
48+
func (d *Device) Configure() error {
49+
// Software reset.
50+
if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, []byte{SW_RESET}); err != nil {
51+
return err
52+
}
53+
54+
// Wait for sw_reset to self-clear (typically < 1 ms; allow up to 200 ms).
55+
for i := 0; i < 200; i++ {
56+
var buf [1]byte
57+
if err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, buf[:]); err != nil {
58+
return err
59+
}
60+
if buf[0]&SW_RESET == 0 {
61+
break
62+
}
63+
time.Sleep(1 * time.Millisecond)
64+
}
65+
66+
// Clear int_reset flag.
67+
if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, []byte{0x00}); err != nil {
68+
return err
69+
}
70+
71+
// 160 ms measurement time.
72+
if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL1, []byte{byte(MeasTime160ms)}); err != nil {
73+
return err
74+
}
75+
76+
// Enable RGBC with 1× gain.
77+
if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, []byte{RGBC_EN | byte(Gain1X)}); err != nil {
78+
return err
79+
}
80+
81+
// Undocumented workaround: writing 0x02 to MODE_CONTROL3 is required for
82+
// the sensor to report correct colour data. Must be applied after reset.
83+
if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL3, []byte{0x02}); err != nil {
84+
return err
85+
}
86+
87+
// Wait for one full 160 ms measurement cycle before the first read.
88+
time.Sleep(160 * time.Millisecond)
89+
return nil
90+
}
91+
92+
// Connected returns true when the sensor is present and the part ID and
93+
// manufacturer ID both match expected values.
94+
func (d *Device) Connected() bool {
95+
var buf [1]byte
96+
legacy.ReadRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, buf[:])
97+
if buf[0]&PART_ID_MASK != PART_ID {
98+
return false
99+
}
100+
legacy.ReadRegister(d.bus, uint8(d.Address), REG_MANUFACTURER, buf[:])
101+
return buf[0] == MANUFACTURER_ID
102+
}
103+
104+
// SetMeasurementTime changes the RGBC integration time. Wait at least one full
105+
// cycle at the new time before calling Read() again.
106+
func (d *Device) SetMeasurementTime(t MeasurementTime) {
107+
legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL1, []byte{byte(t) & 0x07})
108+
}
109+
110+
// SetADCGain changes the ADC gain multiplier.
111+
func (d *Device) SetADCGain(g ADCGain) {
112+
var buf [1]byte
113+
legacy.ReadRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, buf[:])
114+
buf[0] = (buf[0] &^ 0x03) | byte(g)
115+
legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, buf[:])
116+
}
117+
118+
// Update reads a fresh set of RGBC values from the sensor and caches them.
119+
// Pass drivers.Luminosity (or drivers.AllMeasurements) to trigger a read.
120+
// The cached values are then available via R(), G(), B(), C(), Luminosity(),
121+
// and ColorTemperature().
122+
func (d *Device) Update(which drivers.Measurement) error {
123+
if which&drivers.Luminosity == 0 {
124+
return nil
125+
}
126+
var buf [8]byte
127+
if err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_RED_DATA_LSB, buf[:]); err != nil {
128+
return err
129+
}
130+
d.r = uint16(buf[1])<<8 | uint16(buf[0])
131+
d.g = uint16(buf[3])<<8 | uint16(buf[2])
132+
d.b = uint16(buf[5])<<8 | uint16(buf[4])
133+
d.c = uint16(buf[7])<<8 | uint16(buf[6])
134+
return nil
135+
}
136+
137+
// R returns the cached raw red channel value from the last Update call.
138+
func (d *Device) R() uint16 { return d.r }
139+
140+
// G returns the cached raw green channel value from the last Update call.
141+
func (d *Device) G() uint16 { return d.g }
142+
143+
// B returns the cached raw blue channel value from the last Update call.
144+
func (d *Device) B() uint16 { return d.b }
145+
146+
// C returns the cached raw clear channel value from the last Update call.
147+
func (d *Device) C() uint16 { return d.c }
148+
149+
// Luminosity returns the illuminance in lux calculated from the cached RGBC
150+
// values. Call Update first.
151+
func (d *Device) Luminosity() uint32 {
152+
return CalcLux(d.r, d.g, d.b, d.c)
153+
}
154+
155+
// ColorTemperature returns the correlated colour temperature in Kelvin
156+
// calculated from the cached RGBC values. Call Update first.
157+
func (d *Device) ColorTemperature() uint32 {
158+
return CalcColorTemperature(d.r, d.g, d.b, d.c)
159+
}
160+
161+
// Read returns the last cached raw RGBC channel values without performing I2C.
162+
// Call Update first to refresh the values.
163+
//
164+
// Deprecated: call Update then use R(), G(), B(), C() instead.
165+
func (d *Device) Read() RGBCData {
166+
return RGBCData{R: d.r, G: d.g, B: d.b, C: d.c}
167+
}
168+
169+
// CalcLux calculates the illuminance in lux from raw RGBC values.
170+
//
171+
// Formula ported from Pimoroni's Enviro Indoor firmware (indoor.py), tuned for
172+
// the BH1745 configured at 160 ms integration time and 1× gain.
173+
func CalcLux(r, g, b, c uint16) uint32 {
174+
if g < 1 {
175+
return 0
176+
}
177+
rf := float32(r)
178+
gf := float32(g)
179+
cf := float32(c)
180+
var tmp float32
181+
if cf/gf < 0.160 {
182+
tmp = 0.202*rf + 0.766*gf
183+
} else {
184+
tmp = 0.159*rf + 0.646*gf
185+
}
186+
if tmp < 0 {
187+
return 0
188+
}
189+
return uint32(tmp)
190+
}
191+
192+
// CalcColorTemperature calculates the correlated colour temperature (CCT) in Kelvin
193+
// from raw RGBC values.
194+
//
195+
// Formula ported from Pimoroni's Enviro Indoor firmware (indoor.py).
196+
// Returns 0 when the light level is too low for a reliable estimate.
197+
func CalcColorTemperature(r, g, b, c uint16) uint32 {
198+
if g < 1 {
199+
return 0
200+
}
201+
rf := float64(r)
202+
gf := float64(g)
203+
bf := float64(b)
204+
cf := float64(c)
205+
sum := rf + gf + bf
206+
if sum < 1 {
207+
return 0
208+
}
209+
210+
rRatio := rf / sum
211+
bRatio := bf / sum
212+
213+
var ct float64
214+
if cf/gf < 0.160 {
215+
bEff := bRatio * 3.13
216+
if bEff > 1 {
217+
bEff = 1
218+
}
219+
ct = (1-bEff)*12746*math.Exp(-2.911*rRatio) + bEff*1637*math.Exp(4.865*bRatio)
220+
} else {
221+
bEff := bRatio * 10.67
222+
if bEff > 1 {
223+
bEff = 1
224+
}
225+
ct = (1-bEff)*16234*math.Exp(-2.781*rRatio) + bEff*1882*math.Exp(4.448*bRatio)
226+
}
227+
if ct > 10000 {
228+
ct = 10000
229+
}
230+
return uint32(ct)
231+
}

bh1745/registers.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Package bh1745 provides a driver for the BH1745NUC RGBC colour sensor by ROHM.
2+
//
3+
// Datasheet: https://fscdn.rohm.com/en/products/databook/datasheet/ic/sensor/light/bh1745nuc-e.pdf
4+
package bh1745
5+
6+
// I2C addresses (set by ADDR pin: low → 0x38, high → 0x39).
7+
const (
8+
Address uint16 = 0x38
9+
AddressAlt uint16 = 0x39
10+
)
11+
12+
// Registers
13+
const (
14+
REG_SYSTEM_CONTROL = 0x40 // sw_reset (bit 7), int_reset (bit 6), part_id (bits 5:0)
15+
REG_MODE_CONTROL1 = 0x41 // measurement_time (bits 2:0)
16+
REG_MODE_CONTROL2 = 0x42 // valid (bit 7 RO), rgbc_en (bit 4), adc_gain (bits 1:0)
17+
REG_MODE_CONTROL3 = 0x44 // write 0x02 to activate (undocumented; 0x00 = off)
18+
REG_RED_DATA_LSB = 0x50 // red channel, little-endian uint16 at 0x50–0x51
19+
REG_GREEN_DATA_LSB = 0x52
20+
REG_BLUE_DATA_LSB = 0x54
21+
REG_CLEAR_DATA_LSB = 0x56
22+
REG_DINT_DATA_LSB = 0x58
23+
REG_INTERRUPT = 0x60 // int_status (bit 7 RO), int_latch (bit 4), int_source (bits 3:2), int_en (bit 0)
24+
REG_PERSISTENCE = 0x61 // persistence mode (bits 1:0)
25+
REG_THRESHOLD_LSB = 0x62 // interrupt low threshold, little-endian uint16
26+
REG_THRESHOLD_MSB = 0x64 // interrupt high threshold, little-endian uint16
27+
REG_MANUFACTURER = 0x92 // manufacturer ID
28+
)
29+
30+
// SYSTEM_CONTROL bits
31+
const (
32+
SW_RESET = 0x80 // software reset; self-clears when reset is complete
33+
INT_RESET = 0x40 // interrupt reset
34+
PART_ID_MASK = 0x3F // read-only part ID field
35+
PART_ID = 0x0B // expected part ID value
36+
)
37+
38+
// MODE_CONTROL2 bits
39+
const (
40+
VALID = 0x80 // read-only; 1 when RGBC data is valid after first measurement
41+
RGBC_EN = 0x10 // enable RGBC measurement
42+
)
43+
44+
// Chip constants
45+
const (
46+
MANUFACTURER_ID = 0xE0
47+
)
48+
49+
// MeasurementTime represents the RGBC integration time.
50+
type MeasurementTime byte
51+
52+
const (
53+
MeasTime160ms MeasurementTime = 0b000 // 160 ms
54+
MeasTime320ms MeasurementTime = 0b001 // 320 ms
55+
MeasTime640ms MeasurementTime = 0b010 // 640 ms
56+
MeasTime1280ms MeasurementTime = 0b011 // 1280 ms
57+
MeasTime2560ms MeasurementTime = 0b100 // 2560 ms
58+
MeasTime5120ms MeasurementTime = 0b101 // 5120 ms
59+
)
60+
61+
// ADCGain represents the ADC gain multiplier.
62+
type ADCGain byte
63+
64+
const (
65+
Gain1X ADCGain = 0b00 // 1×
66+
Gain2X ADCGain = 0b01 // 2×
67+
Gain16X ADCGain = 0b10 // 16×
68+
)

examples/bh1745/main.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
"machine"
5+
"strconv"
6+
"time"
7+
8+
"tinygo.org/x/drivers"
9+
"tinygo.org/x/drivers/bh1745"
10+
)
11+
12+
func main() {
13+
machine.I2C0.Configure(machine.I2CConfig{})
14+
15+
sensor := bh1745.New(machine.I2C0)
16+
17+
if !sensor.Connected() {
18+
println("BH1745 not detected")
19+
return
20+
}
21+
println("BH1745 detected")
22+
23+
if err := sensor.Configure(); err != nil {
24+
println("configure error:", err.Error())
25+
return
26+
}
27+
28+
for {
29+
if err := sensor.Update(drivers.Luminosity); err != nil {
30+
println("read error:", err.Error())
31+
time.Sleep(500 * time.Millisecond)
32+
continue
33+
}
34+
35+
println("R:", strconv.Itoa(int(sensor.R())),
36+
" G:", strconv.Itoa(int(sensor.G())),
37+
" B:", strconv.Itoa(int(sensor.B())),
38+
" C:", strconv.Itoa(int(sensor.C())))
39+
println("Lux:", strconv.Itoa(int(sensor.Luminosity())))
40+
println("CCT:", strconv.Itoa(int(sensor.ColorTemperature())), "K")
41+
println("---")
42+
43+
time.Sleep(500 * time.Millisecond)
44+
}
45+
}

0 commit comments

Comments
 (0)