From 88ba3ced1531732e16893e2db7cdcd5bc337299d Mon Sep 17 00:00:00 2001 From: Bryan Souza Date: Tue, 29 Oct 2024 10:57:52 -0700 Subject: [PATCH 01/10] added support for LSM303DLHC e-Compass; --- lsm303dlhc/lsm303dlhc.go | 232 +++++++++++++++++++++++++++++++++++++++ lsm303dlhc/registers.go | 75 +++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 lsm303dlhc/lsm303dlhc.go create mode 100644 lsm303dlhc/registers.go diff --git a/lsm303dlhc/lsm303dlhc.go b/lsm303dlhc/lsm303dlhc.go new file mode 100644 index 000000000..e48cfc1b3 --- /dev/null +++ b/lsm303dlhc/lsm303dlhc.go @@ -0,0 +1,232 @@ +// Package lsm303dlhc implements a driver for the LSM303dlhc, +// a 3 axis accelerometer/magnetic sensor which is included on BBC micro:bits v1.5. +// +// Datasheet: https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf +package lsm303dlhc // import "tinygo.org/x/drivers/lsm303dlhc" + +import ( + "math" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" +) + +// Device wraps an I2C connection to a LSM303dlhc device. +type Device struct { + bus drivers.I2C + AccelAddress uint8 + MagAddress uint8 + AccelPowerMode uint8 + AccelRange uint8 + AccelDataRate uint8 + MagPowerMode uint8 + MagSystemMode uint8 + MagDataRate uint8 + buf [6]uint8 +} + +// Configuration for LSM303dlhc device. +type Configuration struct { + AccelPowerMode uint8 + AccelRange uint8 + AccelDataRate uint8 + MagPowerMode uint8 + MagSystemMode uint8 + MagDataRate uint8 +} + +// commented out "Connected" related lines since the DLHC sensor does not have the WHO_AM_I registers + +// var errNotConnected = errors.New("lsm303dlhc: failed to communicate with either accel or magnet sensor") + +// New creates a new LSM303DLHC connection. The I2C bus must already be configured. +// +// This function only creates the Device object, it does not touch the device. +func New(bus drivers.I2C) *Device { + return &Device{ + bus: bus, + AccelAddress: ACCEL_ADDRESS, + MagAddress: MAG_ADDRESS, + } +} + +// Connected returns whether both sensor on LSM303dlhc has been found. +// It does two "who am I" requests and checks the responses. +// func (d *Device) Connected() bool { +// data1, data2 := []byte{0}, []byte{0} +// legacy.ReadRegister(d.bus, uint8(d.AccelAddress), ACCEL_WHO_AM_I, data1) +// legacy.ReadRegister(d.bus, uint8(d.MagAddress), MAG_WHO_AM_I, data2) +// return data1[0] == 0x33 && data2[0] == 0x40 +// } + +// Configure sets up the LSM303dlhc device for communication. +func (d *Device) Configure(cfg Configuration) (err error) { + + // Verify unit communication + // if !d.Connected() { + // return errNotConnected + // } + + if cfg.AccelDataRate != 0 { + d.AccelDataRate = cfg.AccelDataRate + } else { + d.AccelDataRate = ACCEL_DATARATE_100HZ + } + + if cfg.AccelPowerMode != 0 { + d.AccelPowerMode = cfg.AccelPowerMode + } else { + d.AccelPowerMode = ACCEL_POWER_NORMAL + } + + if cfg.AccelRange != 0 { + d.AccelRange = cfg.AccelRange + } else { + d.AccelRange = ACCEL_RANGE_2G + } + + if cfg.MagPowerMode != 0 { + d.MagPowerMode = cfg.MagPowerMode + } else { + d.MagPowerMode = MAG_POWER_NORMAL + } + + if cfg.MagDataRate != 0 { + d.MagDataRate = cfg.MagDataRate + } else { + d.MagDataRate = MAG_DATARATE_10HZ + } + + if cfg.MagSystemMode != 0 { + d.MagSystemMode = cfg.MagSystemMode + } else { + d.MagSystemMode = MAG_SYSTEM_CONTINUOUS + } + + data := d.buf[:1] + + data[0] = byte(d.AccelDataRate<<4 | d.AccelPowerMode | 0x07) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), ACCEL_CTRL_REG1_A, data) + if err != nil { + return + } + + data[0] = byte(0x80 | d.AccelRange<<4) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), ACCEL_CTRL_REG4_A, data) + if err != nil { + return + } + + data[0] = byte(0xC0) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), CRA_REG_M, data) + if err != nil { + return + } + + // Temperature compensation is on for magnetic sensor + data[0] = byte(0x80 | d.MagPowerMode<<4 | d.MagDataRate<<2 | d.MagSystemMode) + err = legacy.WriteRegister(d.bus, uint8(d.MagAddress), MAG_MR_REG_M, data) + if err != nil { + return + } + + return nil +} + +// ReadAcceleration reads the current acceleration from the device and returns +// it in µg (micro-gravity). When one of the axes is pointing straight to Earth +// and the sensor is not moving the returned value will be around 1000000 or +// -1000000. +func (d *Device) ReadAcceleration() (x, y, z int32, err error) { + data := d.buf[:6] + err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), ACCEL_OUT_AUTO_INC, data) + if err != nil { + return + } + + rangeFactor := int16(0) + switch d.AccelRange { + case ACCEL_RANGE_2G: + rangeFactor = 1 + case ACCEL_RANGE_4G: + rangeFactor = 2 + case ACCEL_RANGE_8G: + rangeFactor = 4 + case ACCEL_RANGE_16G: + rangeFactor = 12 // the readings in 16G are a bit lower + } + + x = int32(int32(int16((uint16(data[1])<<8|uint16(data[0])))>>4*rangeFactor) * 1000000 / 1024) + y = int32(int32(int16((uint16(data[3])<<8|uint16(data[2])))>>4*rangeFactor) * 1000000 / 1024) + z = int32(int32(int16((uint16(data[5])<<8|uint16(data[4])))>>4*rangeFactor) * 1000000 / 1024) + return +} + +// ReadPitchRoll reads the current pitch and roll angles from the device and +// returns it in micro-degrees. When the z axis is pointing straight to Earth +// the returned values of pitch and roll would be zero. +func (d *Device) ReadPitchRoll() (pitch, roll int32, err error) { + + x, y, z, err := d.ReadAcceleration() + if err != nil { + return + } + xf, yf, zf := float64(x), float64(y), float64(z) + pitch = int32((math.Round(math.Atan2(yf, math.Sqrt(math.Pow(xf, 2)+math.Pow(zf, 2)))*(180/math.Pi)*100) / 100) * 1000000) + roll = int32((math.Round(math.Atan2(xf, math.Sqrt(math.Pow(yf, 2)+math.Pow(zf, 2)))*(180/math.Pi)*100) / 100) * 1000000) + return + +} + +// ReadMagneticField reads the current magnetic field from the device and returns +// it in mG (milligauss). 1 mG = 0.1 µT (microtesla). +func (d *Device) ReadMagneticField() (x, y, z int32, err error) { + + if d.MagSystemMode == MAG_SYSTEM_SINGLE { + cmd := d.buf[:1] + cmd[0] = byte(0x80 | d.MagPowerMode<<4 | d.MagDataRate<<2 | d.MagSystemMode) + err = legacy.WriteRegister(d.bus, uint8(d.MagAddress), MAG_MR_REG_M, cmd) + if err != nil { + return + } + } + + data := d.buf[0:6] + legacy.ReadRegister(d.bus, uint8(d.MagAddress), MAG_OUT_AUTO_INC, data) + + x = int32(int16((uint16(data[1])<<8 | uint16(data[0])))) + y = int32(int16((uint16(data[3])<<8 | uint16(data[2])))) + z = int32(int16((uint16(data[5])<<8 | uint16(data[4])))) + return +} + +// ReadCompass reads the current compass heading from the device and returns +// it in micro-degrees. When the z axis is pointing straight to Earth and +// the y axis is pointing to North, the heading would be zero. +// +// However, the heading may be off due to electronic compasses would be effected +// by strong magnetic fields and require constant calibration. +func (d *Device) ReadCompass() (h int32, err error) { + + x, y, _, err := d.ReadMagneticField() + if err != nil { + return + } + xf, yf := float64(x), float64(y) + h = int32(float32((180/math.Pi)*math.Atan2(yf, xf)) * 1000000) + return +} + +// ReadTemperature returns the temperature in Celsius milli degrees (°C/1000) +func (d *Device) ReadTemperature() (t int32, err error) { + + data := d.buf[:2] + err = legacy.ReadRegister(d.bus, uint8(d.MagAddress), TEMP_OUT_AUTO_INC, data) + if err != nil { + return + } + + r := int16((uint16(data[1])<<8 | uint16(data[0]))) >> 4 // temperature offset from 25 °C + t = 25000 + int32((float32(r)/8)*1000) + return +} diff --git a/lsm303dlhc/registers.go b/lsm303dlhc/registers.go new file mode 100644 index 000000000..09206f3d0 --- /dev/null +++ b/lsm303dlhc/registers.go @@ -0,0 +1,75 @@ +package lsm303dlhc + +const ( + + // Constants/addresses used for I2C. + ACCEL_ADDRESS = 0x19 + MAG_ADDRESS = 0x1E + + // i2C 8-bit subaddress (SUB): the 7 LSb represent the actual register address + // while the MSB enables address auto increment. + // If the MSb of the SUB field is 1, the SUB (register address) is + // automatically increased to allow multiple data read/writes. + ADDR_AUTO_INC_MASK = 0x80 + + // accelerometer registers. + ACCEL_CTRL_REG1_A = 0x20 + ACCEL_CTRL_REG4_A = 0x23 + ACCEL_OUT_X_L_A = 0x28 + ACCEL_OUT_X_H_A = 0x29 + ACCEL_OUT_Y_L_A = 0x2A + ACCEL_OUT_Y_H_A = 0x2B + ACCEL_OUT_Z_L_A = 0x2C + ACCEL_OUT_Z_H_A = 0x2D + ACCEL_OUT_AUTO_INC = ACCEL_OUT_X_L_A | ADDR_AUTO_INC_MASK + + // magnetic sensor registers. + MAG_MR_REG_M = 0x02 + MAG_OUT_X_L_M = 0x68 + MAG_OUT_X_H_M = 0x69 + MAG_OUT_Y_L_M = 0x6A + MAG_OUT_Y_H_M = 0x6B + MAG_OUT_Z_L_M = 0x6C + MAG_OUT_Z_H_M = 0x6D + MAG_OUT_AUTO_INC = MAG_OUT_X_L_M | ADDR_AUTO_INC_MASK + + // temperature sensor registers. + CRA_REG_M = 0x80 + TEMP_OUT_L_M = 0x32 + TEMP_OUT_H_M = 0x31 + TEMP_OUT_AUTO_INC = TEMP_OUT_L_M | ADDR_AUTO_INC_MASK + + // accelerometer power mode. + ACCEL_POWER_NORMAL = 0x00 // default + ACCEL_POWER_LOW = 0x08 + + // accelerometer range. + ACCEL_RANGE_2G = 0x00 // default + ACCEL_RANGE_4G = 0x01 + ACCEL_RANGE_8G = 0x02 + ACCEL_RANGE_16G = 0x03 + + // accelerometer data rate. + ACCEL_DATARATE_1HZ = 0x01 + ACCEL_DATARATE_10HZ = 0x02 + ACCEL_DATARATE_25HZ = 0x03 + ACCEL_DATARATE_50HZ = 0x04 + ACCEL_DATARATE_100HZ = 0x05 // default + ACCEL_DATARATE_200HZ = 0x06 + ACCEL_DATARATE_400HZ = 0x07 + ACCEL_DATARATE_1344HZ = 0x09 // 5376Hz in low-power mode + + // magnetic sensor power mode. + MAG_POWER_NORMAL = 0x00 // default + MAG_POWER_LOW = 0x01 + + // magnetic sensor operate mode. + MAG_SYSTEM_CONTINUOUS = 0x00 // default + MAG_SYSTEM_SINGLE = 0x01 + + // magnetic sensor data rate + MAG_DATARATE_10HZ = 0x00 // default + MAG_DATARATE_20HZ = 0x01 + MAG_DATARATE_50HZ = 0x02 + MAG_DATARATE_100HZ = 0x03 +) From 4277aa796c47235a1add9351f33fcc2941cf4484 Mon Sep 17 00:00:00 2001 From: Bryan Souza Date: Sat, 2 Nov 2024 08:48:34 -0700 Subject: [PATCH 02/10] fixed the spelling in the Connection error message; Initial support for LSM303DLHC added; --- lsm303agr/lsm303agr.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lsm303agr/lsm303agr.go b/lsm303agr/lsm303agr.go index 35a4c929f..f6848eb25 100644 --- a/lsm303agr/lsm303agr.go +++ b/lsm303agr/lsm303agr.go @@ -36,7 +36,7 @@ type Configuration struct { MagDataRate uint8 } -var errNotConnected = errors.New("lsm303agr: failed to communicate with either acel or magnet sensor") +var errNotConnected = errors.New("lsm303agr: failed to communicate with either accel or magnet sensor") // New creates a new LSM303AGR connection. The I2C bus must already be configured. // From 805e2a02f82b3f902525d857bd7fdb67e5c5e97e Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Wed, 25 Jun 2025 10:03:41 +0200 Subject: [PATCH 03/10] lsm6ds3tr: avoid unnecessary heap allocations (#766) * lsm6ds3tr: avoid unnecessary heap allocations * lsm6ds3tr: use helper functions, for readability * lsm6ds3tr: return slice of the internal buffer on readBytes --- lsm6ds3tr/lsm6ds3tr.go | 59 +++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/lsm6ds3tr/lsm6ds3tr.go b/lsm6ds3tr/lsm6ds3tr.go index b8be8d8b8..76b91d1da 100644 --- a/lsm6ds3tr/lsm6ds3tr.go +++ b/lsm6ds3tr/lsm6ds3tr.go @@ -8,7 +8,6 @@ import ( "errors" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" ) type AccelRange uint8 @@ -26,7 +25,7 @@ type Device struct { accelSampleRate AccelSampleRate gyroRange GyroRange gyroSampleRate GyroSampleRate - buf [6]uint8 + buf [7]uint8 // up to 6 bytes for read + 1 byte for the register address } // Configuration for LSM6DS3TR device. @@ -84,30 +83,20 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { d.gyroSampleRate = GYRO_SR_104 } - data := d.buf[:1] - // Configure accelerometer - data[0] = uint8(d.accelRange) | uint8(d.accelSampleRate) - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL1_XL, data) + err = d.writeByte(CTRL1_XL, uint8(d.accelRange)|uint8(d.accelSampleRate)) if err != nil { return } - // Set ODR bit - err = legacy.ReadRegister(d.bus, uint8(d.Address), CTRL4_C, data) - if err != nil { - return - } - data[0] = data[0] &^ BW_SCAL_ODR_ENABLED - data[0] |= BW_SCAL_ODR_ENABLED - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL4_C, data) + // Enable ODR scaling + err = d.setBits(CTRL4_C, BW_SCAL_ODR_ENABLED) if err != nil { return } // Configure gyroscope - data[0] = uint8(d.gyroRange) | uint8(d.gyroSampleRate) - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL2_G, data) + err = d.writeByte(CTRL2_G, uint8(d.gyroRange)|uint8(d.gyroSampleRate)) if err != nil { return } @@ -118,8 +107,10 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { // Connected returns whether a LSM6DS3TR has been found. // It does a "who am I" request and checks the response. func (d *Device) Connected() bool { - data := d.buf[:1] - legacy.ReadRegister(d.bus, uint8(d.Address), WHO_AM_I, data) + data, err := d.readBytes(WHO_AM_I, 1) + if err != nil { + return false + } return data[0] == 0x6A } @@ -128,8 +119,7 @@ func (d *Device) Connected() bool { // and the sensor is not moving the returned value will be around 1000000 or // -1000000. func (d *Device) ReadAcceleration() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUTX_L_XL, data) + data, err := d.readBytes(OUTX_L_XL, 6) if err != nil { return } @@ -153,8 +143,7 @@ func (d *Device) ReadAcceleration() (x, y, z int32, err error) { // rotation along one axis and while doing so integrate all values over time, // you would get a value close to 360000000. func (d *Device) ReadRotation() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUTX_L_G, data) + data, err := d.readBytes(OUTX_L_G, 6) if err != nil { return } @@ -177,8 +166,7 @@ func (d *Device) ReadRotation() (x, y, z int32, err error) { // ReadTemperature returns the temperature in celsius milli degrees (°C/1000) func (d *Device) ReadTemperature() (t int32, err error) { - data := d.buf[:2] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUT_TEMP_L, data) + data, err := d.readBytes(OUT_TEMP_L, 2) if err != nil { return } @@ -187,3 +175,26 @@ func (d *Device) ReadTemperature() (t int32, err error) { t = 25000 + (int32(int16((int16(data[1])<<8)|int16(data[0])))*125)/32 return } + +func (d *Device) readBytes(reg, size uint8) ([]byte, error) { + d.buf[0] = reg + err := d.bus.Tx(d.Address, d.buf[0:1], d.buf[1:size+1]) + if err != nil { + return nil, err + } + return d.buf[1 : size+1], nil +} + +func (d *Device) writeByte(reg, value uint8) error { + d.buf[0] = reg + d.buf[1] = value + return d.bus.Tx(d.Address, d.buf[0:2], nil) +} + +func (d *Device) setBits(reg, bits uint8) error { + data, err := d.readBytes(reg, 1) + if err != nil { + return err + } + return d.writeByte(reg, (data[0]&^bits)|bits) +} From d02d21ecea979c8526ae70187eacd47c493cf8ef Mon Sep 17 00:00:00 2001 From: JP Hastings-Spital Date: Mon, 30 Jun 2025 08:16:15 +0100 Subject: [PATCH 04/10] feat: allow gps init with address Adafruit's Mini GPS PA1010D Module works with this device driver, but requires 0x10 as the address, rather than 0x42. This change allows the device to be initialised with whatever i2c address is needed, while maintaining backward compatibility. Adds new constants to allow easy configuration of both the ublox device and the PA1010D. --- examples/gps/i2c/main.go | 2 +- gps/gps.go | 8 +++++++- gps/registers.go | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/gps/i2c/main.go b/examples/gps/i2c/main.go index 8f7873cca..123cc261a 100644 --- a/examples/gps/i2c/main.go +++ b/examples/gps/i2c/main.go @@ -10,7 +10,7 @@ import ( func main() { println("GPS I2C Example") machine.I2C0.Configure(machine.I2CConfig{}) - ublox := gps.NewI2C(machine.I2C0) + ublox := gps.NewI2CWithAddress(machine.I2C0, gps.UBLOX_I2C_ADDRESS) parser := gps.NewParser() var fix gps.Fix for { diff --git a/gps/gps.go b/gps/gps.go index 86bfd6a17..16ba870a5 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -69,10 +69,16 @@ func NewUART(uart drivers.UART) Device { } // NewI2C creates a new I2C GPS connection. +// Uses the default i2c address (0x42) for backward compatibility reasons. func NewI2C(bus drivers.I2C) Device { + return NewI2CWithAddress(bus, I2C_ADDRESS) +} + +// NewI2CWithAddress creates a new I2C GPS connection on the provided address +func NewI2CWithAddress(bus drivers.I2C, i2cAddress uint16) Device { return Device{ bus: bus, - address: I2C_ADDRESS, + address: i2cAddress, buffer: make([]byte, bufferSize), bufIdx: bufferSize, sentence: strings.Builder{}, diff --git a/gps/registers.go b/gps/registers.go index 5ef9918a3..6d74d8740 100644 --- a/gps/registers.go +++ b/gps/registers.go @@ -4,7 +4,11 @@ package gps // The I2C address which this device listens to. const ( - I2C_ADDRESS = 0x42 + // To ensure backward compatibility + I2C_ADDRESS = UBLOX_I2C_ADDRESS + + UBLOX_I2C_ADDRESS = 0x42 + PA1010D_I2C_ADDRESS = 0x10 ) const ( From 7f1bd681e0e744a75bee5bcdec399e69571cac17 Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Sun, 13 Jul 2025 14:53:58 +0200 Subject: [PATCH 05/10] ssd1306: avoid unnecessary heap allocations (#767) * ssd1306: avoid unnecessary heap allocations * ssd1306: extract i2c and spi bus implementations * ssd1306: refactor tests -- show fps and heap usage * ssd1306: bring back the lost exported methods * Adjust examples * Fix smoketests for ssd1306 --- examples/ssd1306/i2c_128x32/main.go | 51 ------- examples/ssd1306/i2c_128x64/main.go | 60 -------- examples/ssd1306/main.go | 59 ++++++++ examples/ssd1306/main_i2c_xiao-ble.go | 38 ++++++ examples/ssd1306/main_spi_thumby.go | 27 ++++ examples/ssd1306/main_spi_xiao-rp2040.go | 40 ++++++ examples/ssd1306/spi_128x64/main.go | 48 ------- examples/ssd1306/spi_thumby/main.go | 50 ------- smoketest.sh | 5 +- ssd1306/ssd1306.go | 166 ++++------------------- ssd1306/ssd1306_i2c.go | 52 +++++++ ssd1306/ssd1306_spi.go | 68 ++++++++++ 12 files changed, 316 insertions(+), 348 deletions(-) delete mode 100644 examples/ssd1306/i2c_128x32/main.go delete mode 100644 examples/ssd1306/i2c_128x64/main.go create mode 100644 examples/ssd1306/main.go create mode 100644 examples/ssd1306/main_i2c_xiao-ble.go create mode 100644 examples/ssd1306/main_spi_thumby.go create mode 100644 examples/ssd1306/main_spi_xiao-rp2040.go delete mode 100644 examples/ssd1306/spi_128x64/main.go delete mode 100644 examples/ssd1306/spi_thumby/main.go create mode 100644 ssd1306/ssd1306_i2c.go create mode 100644 ssd1306/ssd1306_spi.go diff --git a/examples/ssd1306/i2c_128x32/main.go b/examples/ssd1306/i2c_128x32/main.go deleted file mode 100644 index 8baf37d75..000000000 --- a/examples/ssd1306/i2c_128x32/main.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "machine" - - "image/color" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.I2C0.Configure(machine.I2CConfig{ - Frequency: machine.TWI_FREQ_400KHZ, - }) - - display := ssd1306.NewI2C(machine.I2C0) - display.Configure(ssd1306.Config{ - Address: ssd1306.Address_128_32, - Width: 128, - Height: 32, - }) - - display.ClearDisplay() - - x := int16(0) - y := int16(0) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 31 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/ssd1306/i2c_128x64/main.go b/examples/ssd1306/i2c_128x64/main.go deleted file mode 100644 index a17010def..000000000 --- a/examples/ssd1306/i2c_128x64/main.go +++ /dev/null @@ -1,60 +0,0 @@ -// This example shows how to use 128x64 display over I2C -// Tested on Seeeduino XIAO Expansion Board https://wiki.seeedstudio.com/Seeeduino-XIAO-Expansion-Board/ -// -// According to manual, I2C address of the display is 0x78, but that's 8-bit address. -// TinyGo operates on 7-bit addresses and respective 7-bit address would be 0x3C, which we use below. -// -// To learn more about different types of I2C addresses, please see following page -// https://www.totalphase.com/support/articles/200349176-7-bit-8-bit-and-10-bit-I2C-Slave-Addressing - -package main - -import ( - "machine" - - "image/color" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.I2C0.Configure(machine.I2CConfig{ - Frequency: machine.TWI_FREQ_400KHZ, - }) - - display := ssd1306.NewI2C(machine.I2C0) - display.Configure(ssd1306.Config{ - Address: 0x3C, - Width: 128, - Height: 64, - }) - - display.ClearDisplay() - - x := int16(0) - y := int16(0) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 63 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/ssd1306/main.go b/examples/ssd1306/main.go new file mode 100644 index 000000000..a7f412d48 --- /dev/null +++ b/examples/ssd1306/main.go @@ -0,0 +1,59 @@ +package main + +// This example shows how to use SSD1306 OLED display driver over I2C and SPI. +// +// Check the `newSSD1306Display()` functions for I2C and SPI initializations. + +import ( + "runtime" + + "image/color" + "time" +) + +func main() { + + display := newSSD1306Display() + display.ClearDisplay() + + w, h := display.Size() + x := int16(0) + y := int16(0) + deltaX := int16(1) + deltaY := int16(1) + + traceTime := time.Now().UnixMilli() + 1000 + frames := 0 + ms := runtime.MemStats{} + + for { + pixel := display.GetPixel(x, y) + c := color.RGBA{255, 255, 255, 255} + if pixel { + c = color.RGBA{0, 0, 0, 255} + } + display.SetPixel(x, y, c) + display.Display() + + x += deltaX + y += deltaY + + if x == 0 || x == w-1 { + deltaX = -deltaX + } + + if y == 0 || y == h-1 { + deltaY = -deltaY + } + + frames++ + now := time.Now().UnixMilli() + if now >= traceTime { + runtime.ReadMemStats(&ms) + println("TS", now, "| FPS", frames, "| HeapInuse", ms.HeapInuse) + traceTime = now + 1000 + frames = 0 + } + } + +} diff --git a/examples/ssd1306/main_i2c_xiao-ble.go b/examples/ssd1306/main_i2c_xiao-ble.go new file mode 100644 index 000000000..c074df262 --- /dev/null +++ b/examples/ssd1306/main_i2c_xiao-ble.go @@ -0,0 +1,38 @@ +//go:build xiao_ble + +// This initializes SSD1306 OLED display driver over I2C. +// +// Seeed XIAO BLE board + SSD1306 128x32 I2C OLED display. +// +// Wiring: +// - XIAO GND -> OLED GND +// - XIAO 3v3 -> OLED VCC +// - XIAO D4 (SDA) -> OLED SDA +// - XIAO D5 (SCL) -> OLED SCK +// +// For your case: +// - Connect the display to I2C pins on your board. +// - Adjust I2C address and display size as needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.I2C0.Configure(machine.I2CConfig{ + Frequency: 400 * machine.KHz, + SDA: machine.SDA0_PIN, + SCL: machine.SCL0_PIN, + }) + display := ssd1306.NewI2C(machine.I2C0) + display.Configure(ssd1306.Config{ + Address: ssd1306.Address_128_32, // or ssd1306.Address + Width: 128, + Height: 32, // or 64 + }) + return display +} diff --git a/examples/ssd1306/main_spi_thumby.go b/examples/ssd1306/main_spi_thumby.go new file mode 100644 index 000000000..f68164b18 --- /dev/null +++ b/examples/ssd1306/main_spi_thumby.go @@ -0,0 +1,27 @@ +//go:build thumby + +// This initializes SSD1306 OLED display driver over SPI. +// +// Thumby board has a tiny built-in 72x40 display. +// +// As the display is built-in, no wiring is needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.SPI0.Configure(machine.SPIConfig{}) + display := ssd1306.NewSPI(machine.SPI0, machine.THUMBY_DC_PIN, machine.THUMBY_RESET_PIN, machine.THUMBY_CS_PIN) + display.Configure(ssd1306.Config{ + Width: 72, + Height: 40, + ResetCol: ssd1306.ResetValue{28, 99}, + ResetPage: ssd1306.ResetValue{0, 5}, + }) + return display +} diff --git a/examples/ssd1306/main_spi_xiao-rp2040.go b/examples/ssd1306/main_spi_xiao-rp2040.go new file mode 100644 index 000000000..fd50bd459 --- /dev/null +++ b/examples/ssd1306/main_spi_xiao-rp2040.go @@ -0,0 +1,40 @@ +//go:build xiao_rp2040 + +// This initializes SSD1306 OLED display driver over SPI. +// +// Seeed XIAO RP2040 board + SSD1306 128x64 SPI OLED display. +// +// Wiring: +// - XIAO GND -> OLED GND +// - XIAO 3v3 -> OLED VCC +// - XIAO D8 (SCK) -> OLED D0 +// - XIAO D10 (SDO) -> OLED D1 +// - XIAO D4 -> OLED RES +// - XIAO D5 -> OLED DC +// - XIAO D6 -> OLED CS +// +// For your case: +// - Connect the display to SPI pins on your board. +// - Adjust RES, DC and CS pins as needed. +// - Adjust SPI frequency as needed. +// - Adjust display size as needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 50 * machine.MHz, + }) + display := ssd1306.NewSPI(machine.SPI0, machine.D5, machine.D4, machine.D6) + display.Configure(ssd1306.Config{ + Width: 128, + Height: 64, + }) + return display +} diff --git a/examples/ssd1306/spi_128x64/main.go b/examples/ssd1306/spi_128x64/main.go deleted file mode 100644 index 094f5cab6..000000000 --- a/examples/ssd1306/spi_128x64/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "image/color" - "machine" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.SPI0.Configure(machine.SPIConfig{ - Frequency: 8000000, - }) - display := ssd1306.NewSPI(machine.SPI0, machine.P8, machine.P7, machine.P9) - display.Configure(ssd1306.Config{ - Width: 128, - Height: 64, - }) - - display.ClearDisplay() - - x := int16(64) - y := int16(32) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 63 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/ssd1306/spi_thumby/main.go b/examples/ssd1306/spi_thumby/main.go deleted file mode 100644 index b2a41e503..000000000 --- a/examples/ssd1306/spi_thumby/main.go +++ /dev/null @@ -1,50 +0,0 @@ -// This example using the SSD1306 OLED display over SPI on the Thumby board -// A very tiny 72x40 display. -package main - -import ( - "image/color" - "machine" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.SPI0.Configure(machine.SPIConfig{}) - display := ssd1306.NewSPI(machine.SPI0, machine.THUMBY_DC_PIN, machine.THUMBY_RESET_PIN, machine.THUMBY_CS_PIN) - display.Configure(ssd1306.Config{ - Width: 72, - Height: 40, - ResetCol: ssd1306.ResetValue{28, 99}, - ResetPage: ssd1306.ResetValue{0, 5}, - }) - - display.ClearDisplay() - - x := int16(36) - y := int16(20) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 71 { - deltaX = -deltaX - } - - if y == 0 || y == 39 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/smoketest.sh b/smoketest.sh index f21e2be55..01cc2a97d 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -65,8 +65,9 @@ tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/shifter/ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/sht3x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/sht4x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/shtc3/main.go -tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1306/i2c_128x32/main.go -tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1306/spi_128x64/main.go +tinygo build -size short -o ./build/test.hex -target=xiao-ble ./examples/ssd1306/ +tinygo build -size short -o ./build/test.hex -target=xiao-rp2040 ./examples/ssd1306/ +tinygo build -size short -o ./build/test.hex -target=thumby ./examples/ssd1306/ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1331/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7735/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7789/main.go diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index dd8ebeb62..2e65a3a2f 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -6,11 +6,9 @@ package ssd1306 // import "tinygo.org/x/drivers/ssd1306" import ( "errors" "image/color" - "machine" "time" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" "tinygo.org/x/drivers/pixel" ) @@ -23,16 +21,15 @@ type ResetValue [2]byte // Device wraps I2C or SPI connection. type Device struct { - bus Buser - buffer []byte - width int16 - height int16 - bufferSize int16 - vccState VccMode - canReset bool - resetCol ResetValue - resetPage ResetValue - rotation drivers.Rotation + bus Buser + buffer []byte + width int16 + height int16 + vccState VccMode + canReset bool + resetCol ResetValue + resetPage ResetValue + rotation drivers.Rotation } // Config is the configuration for the display @@ -51,51 +48,15 @@ type Config struct { Rotation drivers.Rotation } -type I2CBus struct { - wire drivers.I2C - Address uint16 -} - -type SPIBus struct { - wire drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin -} - type Buser interface { - configure() error - tx(data []byte, isCommand bool) error - setAddress(address uint16) error + configure(address uint16, size int16) []byte // configure the bus and return the image buffer to use + command(cmd uint8) error // send a command to the display + flush() error // send the image to the display, faster than "tx()" in i2c case since avoids slice copy + tx(data []byte, isCommand bool) error // generic transmit function } type VccMode uint8 -// NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. -func NewI2C(bus drivers.I2C) Device { - return Device{ - bus: &I2CBus{ - wire: bus, - Address: Address, - }, - } -} - -// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. -func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) Device { - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - return Device{ - bus: &SPIBus{ - wire: bus, - dcPin: dcPin, - resetPin: resetPin, - csPin: csPin, - }, - } -} - // Configure initializes the display with default configuration func (d *Device) Configure(cfg Config) { var zeroReset ResetValue @@ -109,9 +70,6 @@ func (d *Device) Configure(cfg Config) { } else { d.height = 64 } - if cfg.Address != 0 { - d.bus.setAddress(cfg.Address) - } if cfg.VccState != 0 { d.vccState = cfg.VccState } else { @@ -127,11 +85,9 @@ func (d *Device) Configure(cfg Config) { } else { d.resetPage = ResetValue{0, uint8(d.height/8) - 1} } - d.bufferSize = d.width * d.height / 8 - d.buffer = make([]byte, d.bufferSize) d.canReset = cfg.Address != 0 || d.width != 128 || d.height != 64 // I2C or not 128x64 - d.bus.configure() + d.buffer = d.bus.configure(cfg.Address, d.width*d.height/8) time.Sleep(100 * time.Nanosecond) d.Command(DISPLAYOFF) @@ -193,11 +149,22 @@ func (d *Device) Configure(cfg Config) { d.Command(NORMALDISPLAY) d.Command(DEACTIVATE_SCROLL) d.Command(DISPLAYON) + +} + +// Command sends a command to the display +func (d *Device) Command(command uint8) { + d.bus.command(command) +} + +// Tx sends data to the display; if isCommand is false, this also updates the image buffer. +func (d *Device) Tx(data []byte, isCommand bool) error { + return d.bus.tx(data, isCommand) } // ClearBuffer clears the image buffer func (d *Device) ClearBuffer() { - for i := int16(0); i < d.bufferSize; i++ { + for i := 0; i < len(d.buffer); i++ { d.buffer[i] = 0 } } @@ -223,7 +190,7 @@ func (d *Device) Display() error { d.Command(d.resetPage[1]) } - return d.Tx(d.buffer, false) + return d.bus.flush() } // SetPixel enables or disables a pixel in the buffer @@ -252,12 +219,10 @@ func (d *Device) GetPixel(x int16, y int16) bool { // SetBuffer changes the whole buffer at once func (d *Device) SetBuffer(buffer []byte) error { - if int16(len(buffer)) != d.bufferSize { + if len(buffer) != len(d.buffer) { return errBufferSize } - for i := int16(0); i < d.bufferSize; i++ { - d.buffer[i] = buffer[i] - } + copy(d.buffer, buffer) return nil } @@ -266,79 +231,6 @@ func (d *Device) GetBuffer() []byte { return d.buffer } -// Command sends a command to the display -func (d *Device) Command(command uint8) { - d.bus.tx([]byte{command}, true) -} - -// setAddress sets the address to the I2C bus -func (b *I2CBus) setAddress(address uint16) error { - b.Address = address - return nil -} - -// setAddress does nothing, but it's required to avoid reflection -func (b *SPIBus) setAddress(address uint16) error { - // do nothing - println("trying to Configure an address on a SPI device") - return nil -} - -// configure does nothing, but it's required to avoid reflection -func (b *I2CBus) configure() error { return nil } - -// configure configures some pins with the SPI bus -func (b *SPIBus) configure() error { - b.csPin.Low() - b.dcPin.Low() - b.resetPin.Low() - - b.resetPin.High() - time.Sleep(1 * time.Millisecond) - b.resetPin.Low() - time.Sleep(10 * time.Millisecond) - b.resetPin.High() - - return nil -} - -// Tx sends data to the display -func (d *Device) Tx(data []byte, isCommand bool) error { - return d.bus.tx(data, isCommand) -} - -// tx sends data to the display (I2CBus implementation) -func (b *I2CBus) tx(data []byte, isCommand bool) error { - if isCommand { - return legacy.WriteRegister(b.wire, uint8(b.Address), 0x00, data) - } else { - return legacy.WriteRegister(b.wire, uint8(b.Address), 0x40, data) - } -} - -// tx sends data to the display (SPIBus implementation) -func (b *SPIBus) tx(data []byte, isCommand bool) error { - var err error - - if isCommand { - b.csPin.High() - b.dcPin.Low() - b.csPin.Low() - - err = b.wire.Tx(data, nil) - b.csPin.High() - } else { - b.csPin.High() - b.dcPin.High() - b.csPin.Low() - - err = b.wire.Tx(data, nil) - b.csPin.High() - } - - return err -} - // Size returns the current size of the display. func (d *Device) Size() (w, h int16) { return d.width, d.height diff --git a/ssd1306/ssd1306_i2c.go b/ssd1306/ssd1306_i2c.go new file mode 100644 index 000000000..19f3a1cc9 --- /dev/null +++ b/ssd1306/ssd1306_i2c.go @@ -0,0 +1,52 @@ +package ssd1306 + +import ( + "tinygo.org/x/drivers" +) + +type I2CBus struct { + wire drivers.I2C + address uint16 + buffer []byte // buffer to avoid heap allocations +} + +// NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. +func NewI2C(bus drivers.I2C) *Device { + return &Device{ + bus: &I2CBus{ + wire: bus, + address: Address, + }, + } +} + +// configure address for the I2C bus and allocate the buffer +func (b *I2CBus) configure(address uint16, size int16) []byte { + if address != 0 { + b.address = address + } + b.buffer = make([]byte, size+2) // +1 for the mode and +1 for a command + return b.buffer[2:] // return the image buffer +} + +// command sends a command to the display +func (b *I2CBus) command(cmd uint8) error { + b.buffer[0] = 0x00 // Command mode + b.buffer[1] = cmd + return b.wire.Tx(b.address, b.buffer[:2], nil) +} + +// flush sends the image to the display +func (b *I2CBus) flush() error { + b.buffer[1] = 0x40 // Data mode + return b.wire.Tx(b.address, b.buffer[1:], nil) +} + +// tx sends data to the display +func (b *I2CBus) tx(data []byte, isCommand bool) error { + if isCommand { + return b.command(data[0]) + } + copy(b.buffer[2:], data) + return b.flush() +} diff --git a/ssd1306/ssd1306_spi.go b/ssd1306/ssd1306_spi.go new file mode 100644 index 000000000..d96299de5 --- /dev/null +++ b/ssd1306/ssd1306_spi.go @@ -0,0 +1,68 @@ +package ssd1306 + +import ( + "machine" + "time" + + "tinygo.org/x/drivers" +) + +type SPIBus struct { + wire drivers.SPI + dcPin machine.Pin + resetPin machine.Pin + csPin machine.Pin + buffer []byte // buffer to avoid heap allocations +} + +// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. +func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) *Device { + dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + return &Device{ + bus: &SPIBus{ + wire: bus, + dcPin: dcPin, + resetPin: resetPin, + csPin: csPin, + }, + } +} + +// configure pins with the SPI bus and allocate the buffer +func (b *SPIBus) configure(address uint16, size int16) []byte { + b.csPin.Low() + b.dcPin.Low() + b.resetPin.Low() + + b.resetPin.High() + time.Sleep(1 * time.Millisecond) + b.resetPin.Low() + time.Sleep(10 * time.Millisecond) + b.resetPin.High() + + b.buffer = make([]byte, size+1) // +1 for a command + return b.buffer[1:] // return the image buffer +} + +// command sends a command to the display +func (b *SPIBus) command(cmd uint8) error { + b.buffer[0] = cmd + return b.tx(b.buffer[:1], true) +} + +// flush sends the image to the display +func (b *SPIBus) flush() error { + return b.tx(b.buffer[1:], false) +} + +// tx sends data to the display +func (b *SPIBus) tx(data []byte, isCommand bool) error { + b.csPin.High() + b.dcPin.Set(!isCommand) + b.csPin.Low() + err := b.wire.Tx(data, nil) + b.csPin.High() + return err +} From 0559a5a07a99c3054651d6616fdc59d6cc5c2162 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 16 Jun 2025 17:10:53 +0200 Subject: [PATCH 06/10] ws2812: add RP2350 support Adding 150MHz support for the RP2350 --- ws2812/ws2812-asm_cortexm.go | 360 +++++++++++++++++++++++++++++++++++ ws2812/ws2812.go | 2 +- ws2812/ws2812_cortexm.go | 3 + 3 files changed, 364 insertions(+), 1 deletion(-) diff --git a/ws2812/ws2812-asm_cortexm.go b/ws2812/ws2812-asm_cortexm.go index a13e7fafb..1f06c3335 100644 --- a/ws2812/ws2812-asm_cortexm.go +++ b/ws2812/ws2812-asm_cortexm.go @@ -931,6 +931,356 @@ void ws2812_writeByte125(char c, uint32_t *portSet, uint32_t *portClear, uint32_ [portClear]"m"(*portClear)); } +__attribute__((always_inline)) +void ws2812_writeByte150(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 53 - 55 cycles or 353.3ns - 366.7ns + // T1H: 158 - 160 cycles or 1053.3ns - 1066.7ns + // TLD: 173 - cycles or 1153.3ns - + uint32_t value = (uint32_t)c << 24; + char i = 8; + __asm__ __volatile__( + "1: @ send_bit\n" + "\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t lsls %[value], #1 @ [1]\n" + "\t bcs.n 2f @ [1/3] skip_store\n" + "\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n" + "\t2: @ skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t subs %[i], #1 @ [1]\n" + "\t beq.n 3f @ [1/3] end\n" + "\t b 1b @ [1/3] send_bit\n" + "\t3: @ end\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} + __attribute__((always_inline)) void ws2812_writeByte168(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { // Timings: @@ -1832,6 +2182,16 @@ func (d Device) writeByte125(c byte) { interrupt.Restore(mask) } +func (d Device) writeByte150(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte150(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} + func (d Device) writeByte168(c byte) { portSet, maskSet := d.Pin.PortMaskSet() portClear, maskClear := d.Pin.PortMaskClear() diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index d5860f64b..dee3cfbc8 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -1,7 +1,7 @@ // Package ws2812 implements a driver for WS2812 and SK6812 RGB LED strips. package ws2812 // import "tinygo.org/x/drivers/ws2812" -//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 168 200 +//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 168 200 //go:generate go run gen-ws2812.go -arch=tinygoriscv 160 320 import ( diff --git a/ws2812/ws2812_cortexm.go b/ws2812/ws2812_cortexm.go index b818a670c..b1f6ed273 100644 --- a/ws2812/ws2812_cortexm.go +++ b/ws2812/ws2812_cortexm.go @@ -31,6 +31,9 @@ func (d Device) WriteByte(c byte) error { case 125_000_000: // 125 MHz e.g. rp2040 originally d.writeByte125(c) return nil + case 150_000_000: // 150MHz, e.g. rp2350 + d.writeByte150(c) + return nil case 168_000_000: // 168MHz, e.g. stm32f405 d.writeByte168(c) return nil From 71e51eebc46f9a72fce39d6e5ccebfd321e92024 Mon Sep 17 00:00:00 2001 From: Bryan Souza Date: Mon, 28 Jul 2025 19:23:21 -0700 Subject: [PATCH 07/10] Added LSM303DLHC to smoketest and added an example; --- examples/lsm303dlhc/main.go | 58 +++++++++++++++++++++++++++++++++++++ smoketest.sh | 1 + 2 files changed, 59 insertions(+) create mode 100644 examples/lsm303dlhc/main.go diff --git a/examples/lsm303dlhc/main.go b/examples/lsm303dlhc/main.go new file mode 100644 index 000000000..9e27eaffc --- /dev/null +++ b/examples/lsm303dlhc/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/lsm303dlhc" +) + +func main() { + + // LSM303DLHC is connected to the I2C0 bus on Adafruit Feather M4 via pins: 20(SDA) and 21(SCL). + machine.I2C0.Configure(machine.I2CConfig{}) + + sensor := lsm303dlhc.New(machine.I2C0) + //default settings + err := sensor.Configure(lsm303dlhc.Configuration{ + AccelPowerMode: lsm303dlhc.ACCEL_POWER_NORMAL, + AccelRange: lsm303dlhc.ACCEL_RANGE_2G, + AccelDataRate: lsm303dlhc.ACCEL_DATARATE_100HZ, + MagPowerMode: lsm303dlhc.MAG_POWER_NORMAL, + MagSystemMode: lsm303dlhc.MAG_SYSTEM_CONTINUOUS, + MagDataRate: lsm303dlhc.MAG_DATARATE_10HZ, + }) + if err != nil { + for { + println("Failed to configure", err.Error()) + time.Sleep(time.Second) + } + } + + for { + accel_x, accel_y, accel_z, err := sensor.ReadAcceleration() + if err != nil { + println("Failed to read accel", err.Error()) + } + println("ACCEL_X:", accel_x, " ACCEL_Y:", accel_y, " ACCEL_Z:", accel_z) + + mag_x, mag_y, mag_z, err := sensor.ReadMagneticField() + if err != nil { + println("Failed to read mag", err.Error()) + } + println("MAG_X:", mag_x, " MAG_Y:", mag_y, " MAG_Z:", mag_z) + + pitch, roll, _ := sensor.ReadPitchRoll() + println("Pitch:", float32(pitch), " Roll:", float32(roll)) + + heading, _ := sensor.ReadCompass() + println("Heading:", float32(heading), "degrees") + + temp, _ := sensor.ReadTemperature() + println("Temperature:", float32(temp)/1000, "*C") + + println("\n") + time.Sleep(time.Millisecond * 250) + } + +} diff --git a/smoketest.sh b/smoketest.sh index 569a0d817..3ee1beebc 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -44,6 +44,7 @@ tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341 tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis3dh/main.go tinygo build -size short -o ./build/test.hex -target=nano-33-ble ./examples/lps22hb/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/lsm303agr/main.go +tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/lsm303dlhc/main.go tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/lsm6ds3/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mag3110/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp23017/main.go From 0b8f125138403946560769b7b149c73265edbef8 Mon Sep 17 00:00:00 2001 From: Bryan Souza Date: Wed, 30 Jul 2025 07:34:04 -0700 Subject: [PATCH 08/10] Removed unnecessary comments; --- lsm303dlhc/lsm303dlhc.go | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/lsm303dlhc/lsm303dlhc.go b/lsm303dlhc/lsm303dlhc.go index e48cfc1b3..cbe051bc6 100644 --- a/lsm303dlhc/lsm303dlhc.go +++ b/lsm303dlhc/lsm303dlhc.go @@ -1,7 +1,9 @@ // Package lsm303dlhc implements a driver for the LSM303dlhc, -// a 3 axis accelerometer/magnetic sensor which is included on BBC micro:bits v1.5. +// a 3 axis accelerometer/magnetic sensor typically available on breakout boards. // // Datasheet: https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf + + package lsm303dlhc // import "tinygo.org/x/drivers/lsm303dlhc" import ( @@ -35,12 +37,7 @@ type Configuration struct { MagDataRate uint8 } -// commented out "Connected" related lines since the DLHC sensor does not have the WHO_AM_I registers - -// var errNotConnected = errors.New("lsm303dlhc: failed to communicate with either accel or magnet sensor") - // New creates a new LSM303DLHC connection. The I2C bus must already be configured. -// // This function only creates the Device object, it does not touch the device. func New(bus drivers.I2C) *Device { return &Device{ @@ -50,23 +47,10 @@ func New(bus drivers.I2C) *Device { } } -// Connected returns whether both sensor on LSM303dlhc has been found. -// It does two "who am I" requests and checks the responses. -// func (d *Device) Connected() bool { -// data1, data2 := []byte{0}, []byte{0} -// legacy.ReadRegister(d.bus, uint8(d.AccelAddress), ACCEL_WHO_AM_I, data1) -// legacy.ReadRegister(d.bus, uint8(d.MagAddress), MAG_WHO_AM_I, data2) -// return data1[0] == 0x33 && data2[0] == 0x40 -// } // Configure sets up the LSM303dlhc device for communication. func (d *Device) Configure(cfg Configuration) (err error) { - // Verify unit communication - // if !d.Connected() { - // return errNotConnected - // } - if cfg.AccelDataRate != 0 { d.AccelDataRate = cfg.AccelDataRate } else { From d61842b74b630488db088d7e010f908a607be152 Mon Sep 17 00:00:00 2001 From: Bryan Souza Date: Wed, 30 Jul 2025 07:40:46 -0700 Subject: [PATCH 09/10] fixed format error; --- lsm303dlhc/lsm303dlhc.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/lsm303dlhc/lsm303dlhc.go b/lsm303dlhc/lsm303dlhc.go index cbe051bc6..6e0c2fa68 100644 --- a/lsm303dlhc/lsm303dlhc.go +++ b/lsm303dlhc/lsm303dlhc.go @@ -3,7 +3,6 @@ // // Datasheet: https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf - package lsm303dlhc // import "tinygo.org/x/drivers/lsm303dlhc" import ( @@ -47,7 +46,6 @@ func New(bus drivers.I2C) *Device { } } - // Configure sets up the LSM303dlhc device for communication. func (d *Device) Configure(cfg Configuration) (err error) { From b66b84ff6830f6b5f349bc60f1178ebb8694515d Mon Sep 17 00:00:00 2001 From: Artur Nasyrov Date: Fri, 25 Jul 2025 15:54:43 +0300 Subject: [PATCH 10/10] Add ens160 i2c driver Driver for ENS160 sensor: https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf --- ens160/ens160.go | 225 ++++++++++++++++++++++++++++++++++++++++ ens160/ens160_test.go | 54 ++++++++++ ens160/registers.go | 65 ++++++++++++ examples/ens160/main.go | 56 ++++++++++ smoketest.sh | 1 + 5 files changed, 401 insertions(+) create mode 100644 ens160/ens160.go create mode 100644 ens160/ens160_test.go create mode 100644 ens160/registers.go create mode 100644 examples/ens160/main.go diff --git a/ens160/ens160.go b/ens160/ens160.go new file mode 100644 index 000000000..04f5a33ee --- /dev/null +++ b/ens160/ens160.go @@ -0,0 +1,225 @@ +// Package ens160 provides a driver for the ScioSense ENS160 digital gas sensor. +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf +package ens160 + +import ( + "encoding/binary" + "errors" + "time" + + "tinygo.org/x/drivers" +) + +const ( + defaultTimeout = 30 * time.Millisecond + shortTimeout = 1 * time.Millisecond +) + +// Conversion constants for environment data compensation. +const ( + kelvinOffsetMilli = 273150 // 273.15 K in milli-units + tempRawFactor = 64 // As per datasheet for TEMP_IN + humRawFactor = 512 // As per datasheet for RH_IN + milliFactor = 1000 // For converting from milli-units + roundingTerm = milliFactor / 2 // For rounding before integer division +) + +// validityStrings provides human-readable descriptions for validity flags. +var validityStrings = [...]string{ + ValidityNormalOperation: "normal operation", + ValidityWarmUpPhase: "warm-up phase, wait ~3 minutes for valid data", + ValidityInitialStartUpPhase: "initial start-up phase, wait ~1 hour for valid data", + ValidityInvalidOutput: "invalid output", +} + +// Device wraps an I2C connection to an ENS160 device. +type Device struct { + bus drivers.I2C // I²C implementation + addr uint16 // 7‑bit bus address, promoted to uint16 per drivers.I2C + + // shadow registers / last measurements + lastTvocPPB uint16 + lastEco2PPM uint16 + lastAqiUBA uint8 + lastValidity uint8 // Store the latest validity status + + // pre‑allocated buffers + wbuf [5]byte // longest write: reg + 4 bytes (TEMP+RH) + rbuf [5]byte // longest read: DATA burst (5 bytes) +} + +// New returns a new ENS160 driver. +func New(bus drivers.I2C, addr uint16) *Device { + if addr == 0 { + addr = DefaultAddress + } + return &Device{ + bus: bus, + addr: addr, + lastValidity: ValidityInvalidOutput, + } +} + +// Connected returns whether a ENS160 has been found. +func (d *Device) Connected() bool { + d.wbuf[0] = regPartID + err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:2]) + return err == nil && d.rbuf[0] == LowPartID && d.rbuf[1] == HighPartID +} + +// Configure sets up the device for reading. +func (d *Device) Configure() error { + // 1. Soft-reset. The device will automatically enter IDLE mode. + if err := d.write1(regOpMode, ModeReset); err != nil { + return err + } + time.Sleep(defaultTimeout) + + // 2. Clear GPR registers, then go to STANDARD mode. + if err := d.write1(regCommand, cmdClrGPR); err != nil { + return err + } + time.Sleep(defaultTimeout) + + if err := d.write1(regOpMode, ModeStandard); err != nil { + return err + } + time.Sleep(defaultTimeout) + + return nil +} + +// calculateTempRaw converts temperature from milli-degrees Celsius to the sensor's raw format. +func calculateTempRaw(tempMilliC int32) uint16 { + // Clip temperature + const ( + minC = -40 * 1000 + maxC = 85 * 1000 + ) + if tempMilliC < minC { + tempMilliC = minC + } else if tempMilliC > maxC { + tempMilliC = maxC + } + + // Integer fixed-point conversion to format required by the sensor. + // Formula from datasheet: T_IN = (T_ambient_C + 273.15) * 64 + return uint16((((tempMilliC + kelvinOffsetMilli) * tempRawFactor) + roundingTerm) / milliFactor) +} + +// calculateHumRaw converts relative humidity from milli-percent to the sensor's raw format. +func calculateHumRaw(rhMilliPct int32) uint16 { + // Clip humidity + if rhMilliPct < 0 { + rhMilliPct = 0 + } else if rhMilliPct > 100*1000 { + rhMilliPct = 100 * 1000 + } + + // Integer fixed-point conversion to format required by the sensor. + // Formula from datasheet: RH_IN = (RH_ambient_% * 512) + return uint16(((rhMilliPct * humRawFactor) + roundingTerm) / milliFactor) +} + +// SetEnvDataMilli sets the ambient temperature and humidity for compensation. +// +// tempMilliC is the temperature in milli-degrees Celsius. +// rhMilliPct is the relative humidity in milli-percent. +func (d *Device) SetEnvDataMilli(tempMilliC, rhMilliPct int32) error { + tempRaw := calculateTempRaw(tempMilliC) + humRaw := calculateHumRaw(rhMilliPct) + + d.wbuf[0] = regTempIn // start address (auto‑increment) + binary.LittleEndian.PutUint16(d.wbuf[1:3], tempRaw) + binary.LittleEndian.PutUint16(d.wbuf[3:5], humRaw) + + return d.bus.Tx(d.addr, d.wbuf[:5], nil) +} + +// Update refreshes the concentration measurements. +func (d *Device) Update(which drivers.Measurement) error { + if which&drivers.Concentration == 0 { + return nil // nothing requested + } + + const maxTries = 1000 + var ( + status uint8 + validity uint8 + ) + var gotData bool + + // Poll DEVICE_STATUS until NEWDAT or timeout + for range maxTries { + var err error + status, err = d.read1(regStatus) + if err != nil { + return err + } + if status&statusSTATER != 0 { + return errors.New("ENS160: error (STATER set)") + } + validity = (status & statusValidityMask) >> statusValidityShift + + if status&statusNEWDAT != 0 { + gotData = true + break // Always break when data available + } + time.Sleep(shortTimeout) + } + if !gotData { + return errors.New("ENS160: timeout waiting for NEWDAT") + } + + // Burst-read data regardless of validity state + d.wbuf[0] = regAQI + if err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:5]); err != nil { + return errors.New("ENS160: burst read failed") + } + + d.lastAqiUBA = d.rbuf[0] + d.lastTvocPPB = binary.LittleEndian.Uint16(d.rbuf[1:3]) + d.lastEco2PPM = binary.LittleEndian.Uint16(d.rbuf[3:5]) + d.lastValidity = validity // Store the validity status + + return nil +} + +// TVOC returns the last total‑VOC concentration in parts‑per‑billion. +func (d *Device) TVOC() uint16 { return d.lastTvocPPB } + +// ECO2 returns the last equivalent CO₂ concentration in parts‑per‑million. +func (d *Device) ECO2() uint16 { return d.lastEco2PPM } + +// AQI returns the last Air‑Quality Index according to UBA (1–5). +func (d *Device) AQI() uint8 { return d.lastAqiUBA } + +// Validity returns the current operating state of the sensor. +func (d *Device) Validity() uint8 { + return d.lastValidity +} + +// ValidityString returns a human-readable string describing the current validity status. +func (d *Device) ValidityString() string { + if int(d.lastValidity) < len(validityStrings) { + return validityStrings[d.lastValidity] + } + return "unknown" +} + +// write1 writes a single byte to a register. +func (d *Device) write1(reg, val uint8) error { + d.wbuf[0] = reg + d.wbuf[1] = val + return d.bus.Tx(d.addr, d.wbuf[:2], nil) +} + +// read1 reads a single byte from a register. +func (d *Device) read1(reg uint8) (uint8, error) { + d.wbuf[0] = reg + if err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:1]); err != nil { + return 0, err + } + return d.rbuf[0], nil +} diff --git a/ens160/ens160_test.go b/ens160/ens160_test.go new file mode 100644 index 000000000..022de1616 --- /dev/null +++ b/ens160/ens160_test.go @@ -0,0 +1,54 @@ +package ens160 + +import ( + "testing" +) + +func TestCalculateTempRaw(t *testing.T) { + testCases := []struct { + name string + tempMilliC int32 + expectedRaw uint16 + }{ + {"25°C", 25000, 19082}, + {"-10.5°C", -10500, 16810}, + {"Min temp", -40000, 14922}, + {"Below min", -50000, 14922}, + {"Max temp", 85000, 22922}, + {"Above max", 90000, 22922}, + {"Zero", 0, 17482}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + raw := calculateTempRaw(tc.tempMilliC) + if raw != tc.expectedRaw { + t.Errorf("expected %d, got %d", tc.expectedRaw, raw) + } + }) + } +} + +func TestCalculateHumRaw(t *testing.T) { + testCases := []struct { + name string + rhMilliPct int32 + expectedRaw uint16 + }{ + {"50%", 50000, 25600}, + {"0%", 0, 0}, + {"100%", 100000, 51200}, + {"Below 0%", -10000, 0}, + {"Above 100%", 110000, 51200}, + {"33.3%", 33300, 17050}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + raw := calculateHumRaw(tc.rhMilliPct) + if raw != tc.expectedRaw { + t.Errorf("expected %d, got %d", tc.expectedRaw, raw) + } + }) + } +} diff --git a/ens160/registers.go b/ens160/registers.go new file mode 100644 index 000000000..0650c91b1 --- /dev/null +++ b/ens160/registers.go @@ -0,0 +1,65 @@ +package ens160 + +// DefaultAddress is the default I2C address for the ENS160 when the ADDR pin is +// connected to high (3.3V). When connected to low (GND), the address is 0x52. +const DefaultAddress = 0x53 + +// Registers +const ( + regPartID = 0x00 + regOpMode = 0x10 + regConfig = 0x11 + regCommand = 0x12 + regTempIn = 0x13 + regRhIn = 0x15 + regStatus = 0x20 + regAQI = 0x21 + regTVOC = 0x22 + regECO2 = 0x24 + regDataT = 0x30 + regDataRH = 0x32 + regMISR = 0x38 + regGPRWrite = 0x40 + regGPRRead = 0x48 +) + +// Operating modes +const ( + ModeDeepSleep = 0x00 + ModeIdle = 0x01 + ModeStandard = 0x02 + ModeReset = 0xF0 +) + +// Status register bits +const ( + statusSTATAS = 1 << 7 + statusSTATER = 1 << 6 + + statusValidityMask = 0x0C + statusValidityShift = 2 + + statusNEWDAT = 1 << 1 + statusNEWGPR = 1 << 0 +) + +// Validity flags +const ( + ValidityNormalOperation = 0x00 + ValidityWarmUpPhase = 0x01 // need ~3 minutes until valid data + ValidityInitialStartUpPhase = 0x02 // need ~1 hour until valid data + ValidityInvalidOutput = 0x03 +) + +// Commands +const ( + cmdNOP = 0x00 + cmdGetAppVer = 0x0E + cmdClrGPR = 0xCC +) + +// Part IDs +const ( + LowPartID = 0x60 + HighPartID = 0x01 +) diff --git a/examples/ens160/main.go b/examples/ens160/main.go new file mode 100644 index 000000000..d3571601d --- /dev/null +++ b/examples/ens160/main.go @@ -0,0 +1,56 @@ +// This example demonstrates ENS160 usage. +// +// Wiring: +// - VCC to 3.3V, GND to ground +// - SDA to board SDA, SCL to board SCL + +package main + +import ( + "time" + + "machine" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/ens160" +) + +func main() { + err := machine.I2C0.Configure(machine.I2CConfig{ + Frequency: 400 * machine.KHz, + }) + if err != nil { + println("Failed to configure I2C:", err) + } + + dev := ens160.New(machine.I2C0, ens160.DefaultAddress) + + connected := dev.Connected() + if !connected { + println("ENS160 not detected") + return + } + println("ENS160 detected") + + if err := dev.Configure(); err != nil { + println("Failed to configure ENS160:", err) + } + + for { + err := dev.Update(drivers.Concentration) + if err != nil { + println("Error reading ENS160: %v\n", err) + time.Sleep(5 * time.Second) + continue + } + + println( + "AQI:", dev.AQI(), + "TVOC:", dev.TVOC(), + "eCO2:", dev.ECO2(), + "Validity:", dev.ValidityString(), + ) + + time.Sleep(2 * time.Second) + } +} diff --git a/smoketest.sh b/smoketest.sh index 01cc2a97d..fef87ce58 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -141,6 +141,7 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/tmc2209/mai tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/main.go tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpmem/main.go tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/max6675/main.go +tinygo build -size short -o ./build/test.hex -target=pico ./examples/ens160/main.go # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina)