mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-24 00:19:00 +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
|
||||
}
|
||||
Reference in New Issue
Block a user