mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-17 21:23:58 +00:00
Add ens160 i2c driver
Driver for ENS160 sensor: https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf
This commit is contained in:
committed by
deadprogram
parent
9717f365d9
commit
24a9d7dcc9
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.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.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=feather-nrf52840 ./examples/max6675/main.go
|
||||||
|
tinygo build -size short -o ./build/test.hex -target=pico ./examples/ens160/main.go
|
||||||
# network examples (espat)
|
# network examples (espat)
|
||||||
tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/
|
tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/
|
||||||
# network examples (wifinina)
|
# network examples (wifinina)
|
||||||
|
|||||||
Reference in New Issue
Block a user