diff --git a/bh1745/bh1745.go b/bh1745/bh1745.go new file mode 100644 index 000000000..e8a3d08f4 --- /dev/null +++ b/bh1745/bh1745.go @@ -0,0 +1,231 @@ +// Package bh1745 implements a driver for the bh1745 color sensor +// It detects 16-bit RGBC (Red, Green, Blue, Clear) color over +// I2C interface. +// +// Datasheet: https://www.mouser.co.uk/datasheet/2/348/bh1745nuc-e-519994.pdf +// +// Pimoroni bh1745-python driver +// https://github.com/pimoroni/bh1745-python/blob/main/bh1745/__init__.py + +package bh1745 // import "tinygo.org/x/drivers/bh1745" + +import ( + "math" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" +) + +// RGBCData holds one set of raw channel readings. +type RGBCData struct { + R, G, B, C uint16 +} + +// Device is the BH1745 RGBC colour sensor driver. +type Device struct { + bus drivers.I2C + Address uint16 + + r, g, b, c uint16 // cached raw RGBC readings from the last Update call +} + +// New creates a new BH1745 driver. The I2C bus must already be configured. +// Call Configure() before calling Read(). +func New(bus drivers.I2C) Device { + return Device{ + bus: bus, + Address: Address, + } +} + +// Configure resets the sensor and sets it up with default settings: +// 160 ms measurement time, 1× ADC gain, RGBC measurement enabled. +// +// The undocumented write of 0x02 to MODE_CONTROL3 (register 0x44) is included +// here; without it the sensor reports incorrect RGBC values. This matches the +// workaround applied in Pimoroni's Enviro Indoor firmware. +func (d *Device) Configure() error { + // Software reset. + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, []byte{SW_RESET}); err != nil { + return err + } + + // Wait for sw_reset to self-clear (typically < 1 ms; allow up to 200 ms). + for i := 0; i < 200; i++ { + var buf [1]byte + if err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, buf[:]); err != nil { + return err + } + if buf[0]&SW_RESET == 0 { + break + } + time.Sleep(1 * time.Millisecond) + } + + // Clear int_reset flag. + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, []byte{0x00}); err != nil { + return err + } + + // 160 ms measurement time. + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL1, []byte{byte(MeasTime160ms)}); err != nil { + return err + } + + // Enable RGBC with 1× gain. + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, []byte{RGBC_EN | byte(Gain1X)}); err != nil { + return err + } + + // Undocumented workaround: writing 0x02 to MODE_CONTROL3 is required for + // the sensor to report correct colour data. Must be applied after reset. + if err := legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL3, []byte{0x02}); err != nil { + return err + } + + // Wait for one full 160 ms measurement cycle before the first read. + time.Sleep(160 * time.Millisecond) + return nil +} + +// Connected returns true when the sensor is present and the part ID and +// manufacturer ID both match expected values. +func (d *Device) Connected() bool { + var buf [1]byte + legacy.ReadRegister(d.bus, uint8(d.Address), REG_SYSTEM_CONTROL, buf[:]) + if buf[0]&PART_ID_MASK != PART_ID { + return false + } + legacy.ReadRegister(d.bus, uint8(d.Address), REG_MANUFACTURER, buf[:]) + return buf[0] == MANUFACTURER_ID +} + +// SetMeasurementTime changes the RGBC integration time. Wait at least one full +// cycle at the new time before calling Read() again. +func (d *Device) SetMeasurementTime(t MeasurementTime) { + legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL1, []byte{byte(t) & 0x07}) +} + +// SetADCGain changes the ADC gain multiplier. +func (d *Device) SetADCGain(g ADCGain) { + var buf [1]byte + legacy.ReadRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, buf[:]) + buf[0] = (buf[0] &^ 0x03) | byte(g) + legacy.WriteRegister(d.bus, uint8(d.Address), REG_MODE_CONTROL2, buf[:]) +} + +// Update reads a fresh set of RGBC values from the sensor and caches them. +// Pass drivers.Luminosity (or drivers.AllMeasurements) to trigger a read. +// The cached values are then available via R(), G(), B(), C(), Luminosity(), +// and ColorTemperature(). +func (d *Device) Update(which drivers.Measurement) error { + if which&drivers.Luminosity == 0 { + return nil + } + var buf [8]byte + if err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_RED_DATA_LSB, buf[:]); err != nil { + return err + } + d.r = uint16(buf[1])<<8 | uint16(buf[0]) + d.g = uint16(buf[3])<<8 | uint16(buf[2]) + d.b = uint16(buf[5])<<8 | uint16(buf[4]) + d.c = uint16(buf[7])<<8 | uint16(buf[6]) + return nil +} + +// R returns the cached raw red channel value from the last Update call. +func (d *Device) R() uint16 { return d.r } + +// G returns the cached raw green channel value from the last Update call. +func (d *Device) G() uint16 { return d.g } + +// B returns the cached raw blue channel value from the last Update call. +func (d *Device) B() uint16 { return d.b } + +// C returns the cached raw clear channel value from the last Update call. +func (d *Device) C() uint16 { return d.c } + +// Luminosity returns the illuminance in lux calculated from the cached RGBC +// values. Call Update first. +func (d *Device) Luminosity() uint32 { + return CalcLux(d.r, d.g, d.b, d.c) +} + +// ColorTemperature returns the correlated colour temperature in Kelvin +// calculated from the cached RGBC values. Call Update first. +func (d *Device) ColorTemperature() uint32 { + return CalcColorTemperature(d.r, d.g, d.b, d.c) +} + +// Read returns the last cached raw RGBC channel values without performing I2C. +// Call Update first to refresh the values. +// +// Deprecated: call Update then use R(), G(), B(), C() instead. +func (d *Device) Read() RGBCData { + return RGBCData{R: d.r, G: d.g, B: d.b, C: d.c} +} + +// CalcLux calculates the illuminance in lux from raw RGBC values. +// +// Formula ported from Pimoroni's Enviro Indoor firmware (indoor.py), tuned for +// the BH1745 configured at 160 ms integration time and 1× gain. +func CalcLux(r, g, b, c uint16) uint32 { + if g < 1 { + return 0 + } + rf := float32(r) + gf := float32(g) + cf := float32(c) + var tmp float32 + if cf/gf < 0.160 { + tmp = 0.202*rf + 0.766*gf + } else { + tmp = 0.159*rf + 0.646*gf + } + if tmp < 0 { + return 0 + } + return uint32(tmp) +} + +// CalcColorTemperature calculates the correlated colour temperature (CCT) in Kelvin +// from raw RGBC values. +// +// Formula ported from Pimoroni's Enviro Indoor firmware (indoor.py). +// Returns 0 when the light level is too low for a reliable estimate. +func CalcColorTemperature(r, g, b, c uint16) uint32 { + if g < 1 { + return 0 + } + rf := float64(r) + gf := float64(g) + bf := float64(b) + cf := float64(c) + sum := rf + gf + bf + if sum < 1 { + return 0 + } + + rRatio := rf / sum + bRatio := bf / sum + + var ct float64 + if cf/gf < 0.160 { + bEff := bRatio * 3.13 + if bEff > 1 { + bEff = 1 + } + ct = (1-bEff)*12746*math.Exp(-2.911*rRatio) + bEff*1637*math.Exp(4.865*bRatio) + } else { + bEff := bRatio * 10.67 + if bEff > 1 { + bEff = 1 + } + ct = (1-bEff)*16234*math.Exp(-2.781*rRatio) + bEff*1882*math.Exp(4.448*bRatio) + } + if ct > 10000 { + ct = 10000 + } + return uint32(ct) +} diff --git a/bh1745/registers.go b/bh1745/registers.go new file mode 100644 index 000000000..8c36e69e4 --- /dev/null +++ b/bh1745/registers.go @@ -0,0 +1,68 @@ +// Package bh1745 provides a driver for the BH1745NUC RGBC colour sensor by ROHM. +// +// Datasheet: https://fscdn.rohm.com/en/products/databook/datasheet/ic/sensor/light/bh1745nuc-e.pdf +package bh1745 + +// I2C addresses (set by ADDR pin: low → 0x38, high → 0x39). +const ( + Address uint16 = 0x38 + AddressAlt uint16 = 0x39 +) + +// Registers +const ( + REG_SYSTEM_CONTROL = 0x40 // sw_reset (bit 7), int_reset (bit 6), part_id (bits 5:0) + REG_MODE_CONTROL1 = 0x41 // measurement_time (bits 2:0) + REG_MODE_CONTROL2 = 0x42 // valid (bit 7 RO), rgbc_en (bit 4), adc_gain (bits 1:0) + REG_MODE_CONTROL3 = 0x44 // write 0x02 to activate (undocumented; 0x00 = off) + REG_RED_DATA_LSB = 0x50 // red channel, little-endian uint16 at 0x50–0x51 + REG_GREEN_DATA_LSB = 0x52 + REG_BLUE_DATA_LSB = 0x54 + REG_CLEAR_DATA_LSB = 0x56 + REG_DINT_DATA_LSB = 0x58 + REG_INTERRUPT = 0x60 // int_status (bit 7 RO), int_latch (bit 4), int_source (bits 3:2), int_en (bit 0) + REG_PERSISTENCE = 0x61 // persistence mode (bits 1:0) + REG_THRESHOLD_LSB = 0x62 // interrupt low threshold, little-endian uint16 + REG_THRESHOLD_MSB = 0x64 // interrupt high threshold, little-endian uint16 + REG_MANUFACTURER = 0x92 // manufacturer ID +) + +// SYSTEM_CONTROL bits +const ( + SW_RESET = 0x80 // software reset; self-clears when reset is complete + INT_RESET = 0x40 // interrupt reset + PART_ID_MASK = 0x3F // read-only part ID field + PART_ID = 0x0B // expected part ID value +) + +// MODE_CONTROL2 bits +const ( + VALID = 0x80 // read-only; 1 when RGBC data is valid after first measurement + RGBC_EN = 0x10 // enable RGBC measurement +) + +// Chip constants +const ( + MANUFACTURER_ID = 0xE0 +) + +// MeasurementTime represents the RGBC integration time. +type MeasurementTime byte + +const ( + MeasTime160ms MeasurementTime = 0b000 // 160 ms + MeasTime320ms MeasurementTime = 0b001 // 320 ms + MeasTime640ms MeasurementTime = 0b010 // 640 ms + MeasTime1280ms MeasurementTime = 0b011 // 1280 ms + MeasTime2560ms MeasurementTime = 0b100 // 2560 ms + MeasTime5120ms MeasurementTime = 0b101 // 5120 ms +) + +// ADCGain represents the ADC gain multiplier. +type ADCGain byte + +const ( + Gain1X ADCGain = 0b00 // 1× + Gain2X ADCGain = 0b01 // 2× + Gain16X ADCGain = 0b10 // 16× +) diff --git a/examples/bh1745/main.go b/examples/bh1745/main.go new file mode 100644 index 000000000..63d7d6891 --- /dev/null +++ b/examples/bh1745/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "machine" + "strconv" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/bh1745" +) + +func main() { + machine.I2C0.Configure(machine.I2CConfig{}) + + sensor := bh1745.New(machine.I2C0) + + if !sensor.Connected() { + println("BH1745 not detected") + return + } + println("BH1745 detected") + + if err := sensor.Configure(); err != nil { + println("configure error:", err.Error()) + return + } + + for { + if err := sensor.Update(drivers.Luminosity); err != nil { + println("read error:", err.Error()) + time.Sleep(500 * time.Millisecond) + continue + } + + println("R:", strconv.Itoa(int(sensor.R())), + " G:", strconv.Itoa(int(sensor.G())), + " B:", strconv.Itoa(int(sensor.B())), + " C:", strconv.Itoa(int(sensor.C()))) + println("Lux:", strconv.Itoa(int(sensor.Luminosity()))) + println("CCT:", strconv.Itoa(int(sensor.ColorTemperature())), "K") + println("---") + + time.Sleep(500 * time.Millisecond) + } +} diff --git a/smoketest.sh b/smoketest.sh index 75316bbae..5e6d94323 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -167,3 +167,4 @@ tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb # network examples (comboat) tinygo build -size short -o ./build/test.hex -target=elecrow-rp2040 -stack-size 8kb ./examples/net/tlsclient/ tinygo build -size short -o ./build/test.hex -target=elecrow-rp2350 -stack-size 8kb ./examples/net/ntpclient/ +tinygo build -size short -o ./build/test.hex -target=pico ./examples/bh1745