diff --git a/examples/honeyhsc/example.go b/examples/honeyhsc/example.go new file mode 100644 index 000000000..1f34d2139 --- /dev/null +++ b/examples/honeyhsc/example.go @@ -0,0 +1,49 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/honeyhsc" +) + +// Data taken from https://github.com/rodan/honeywell_hsc_ssc_i2c/blob/master/hsc_ssc_i2c.cpp +// these defaults are valid for the HSCMRNN030PA2A3 chip +const ( + i2cAddress = 0x28 + // 10% + outputMinimum = 0x666 + // 90% of 2^14 - 1 + outputMax = 0x399A + // min is 0 for sensors that give absolute values + pressureMin = 0 + // 30psi (and we want results in millipascals) + // pressureMax = 206842.7 + pressureMax = 206843 * 1000 +) + +func main() { + bus := machine.I2C0 + err := bus.Configure(machine.I2CConfig{ + Frequency: 400_000, // 100kHz minimum and 400kHz I2C maximum clock. 50 to 800 for SPI. + SDA: machine.I2C0_SDA_PIN, + SCL: machine.I2C0_SCL_PIN, + }) + if err != nil { + panic(err.Error()) + } + sensor := honeyhsc.NewDevI2C(bus, i2cAddress, outputMinimum, outputMax, pressureMin, pressureMax) + for { + time.Sleep(time.Second) + const measuremask = drivers.Pressure | drivers.Temperature + err := sensor.Update(measuremask) + if err != nil { + println("error updating measurements:", err.Error()) + continue + } + P := sensor.Pressure() + T := sensor.Temperature() + println("pressure:", P, "temperature:", T) + } +} diff --git a/honeyhsc/hsc.go b/honeyhsc/hsc.go new file mode 100644 index 000000000..efc58a51b --- /dev/null +++ b/honeyhsc/hsc.go @@ -0,0 +1,191 @@ +package honeyhsc + +import ( + "errors" + "math" + + "tinygo.org/x/drivers" +) + +var ( + errSensorMissing = errors.New("hsc: not connected") + errDiagnostic = errors.New("hsc: diagnostic error") +) + +const ( + measuremask = drivers.Pressure | drivers.Temperature + statusMask = 0b1100_0000 + statusOffset = 6 +) + +// DevI2C is the TruStability® High Accuracy Silicon Ceramic (HSC) Series is a piezoresistive silicon pressure sensor offering a ratiometric +// analog or digital output for reading pressure over the specified full scale pressure span and temperature range. +type DevI2C struct { + bus drivers.I2C + dev + addr uint8 + buf [6]byte +} + +// NewDevI2C creates and returns a new DevI2C that communicates with an HSC device over the provided I2C bus. +// Parameters: +// - bus: the I2C bus to use. +// - addr: the 7-bit I2C address of the sensor. +// - outMin, outMax: raw output code range (counts) corresponding to the pressure span. Depends on sensor model. +// - pMin, pMax: pressure range endpoints in millipascals (mPa). Depends on sensor model. +// +// The returned DevI2C will use these calibration parameters to convert raw bridge counts to pressure. +func NewDevI2C(bus drivers.I2C, addr, outMin, outMax uint16, pMin, pMax int32) *DevI2C { + h := &DevI2C{ + bus: bus, + addr: uint8(addr), + dev: dev{ + cmin: outMin, + cmax: outMax, + pmin: pMin, + pmax: pMax, + }, + } + return h +} + +// ReadTemperature reads and returns the temperature in milliKelvin (mC) from the I2C-attached HSC device. +// It performs an Update internally to get the latest temperature value. +func (h *DevI2C) ReadTemperature() (int32, error) { + err := h.Update(drivers.Temperature) + if err != nil { + return 0, err + } + return h.Temperature(), nil +} + +// Update reads both temperature and pressure data from the I2C-attached HSC device when +// the requested measurement mask includes pressure or temperature. +// If neither pressure nor temperature is requested, Update is a no-op. +func (d *DevI2C) Update(which drivers.Measurement) error { + // Update performs an I2C transaction to read 4 bytes, parses the status bits, 14-bit bridge data and + // temperature bits, and forwards them to the internal update routine. Any I2C transport error is returned, + // as well as errors produced by the internal update (e.g. errSensorMissing, errDiagnostic). + if which&measuremask == 0 { + return nil + } + rbuf := d.buf[:4] + wbuf := d.buf[4:6] + const reg = 0 + value := (d.addr << 1) | 1 + wbuf[0] = reg + wbuf[1] = value + err := d.bus.Tx(uint16(d.addr), wbuf, rbuf) + if err != nil { + return err + } + status := (rbuf[0] & statusMask) >> statusOffset + bridgeData := (uint16(rbuf[0]&^statusMask) << 8) | uint16(rbuf[1]) + tempData := uint16(rbuf[2])<<8 | uint16(rbuf[3]&0xe0)>>5 + return d.dev.update(status, bridgeData, tempData) +} + +type pinout func(level bool) + +// DevI2C is the TruStability® High Accuracy Silicon Ceramic (HSC) Series is a piezoresistive silicon pressure sensor offering a ratiometric +// analog or digital output for reading pressure over the specified full scale pressure span and temperature range. +type DevSPI struct { + spi drivers.SPI + cs pinout + dev + buf [4]byte +} + +// NewDevSPI creates and returns a new DevSPI that communicates with an HSC device over SPI. +// Parameters: +// - conn: the SPI connection to use. +// - cs: a chip-select function that drives the device select line low/high. +// - outMin, outMax: raw output code range (counts) corresponding to the pressure span. Depends on sensor model. +// - pMin, pMax: pressure range endpoints in millipascals (mPa). Depends on sensor model. +// +// The function returns the constructed DevSPI and an error value (currently always nil). +func NewDevSPI(conn drivers.SPI, cs pinout, outMin, outMax uint16, pMin, pMax int32) (*DevSPI, error) { + h := &DevSPI{ + spi: conn, + cs: cs, + dev: dev{ + cmin: outMin, + cmax: outMax, + pmin: pMin, + pmax: pMax, + }, + } + return h, nil +} + +// ReadTemperature reads and returns the temperature in milliKelvin (mC) from the SPI-attached HSC device. +// It performs an Update internally to get the latest temperature value. +func (h *DevSPI) ReadTemperature() (int32, error) { + err := h.Update(drivers.Temperature) + if err != nil { + return 0, err + } + return h.Temperature(), nil +} + +// Update reads pressure and temperature data from the SPI-attached HSC device when the requested measurement mask includes +// pressure or temperature. If neither pressure nor temperature is requested, Update is a no-op. +func (h *DevSPI) Update(which drivers.Measurement) error { + // It toggles the provided chip-select, performs an SPI transfer to read 4 bytes, parses the status bits, + // 14-bit bridge data and temperature bits, and forwards them to the internal update routine. Any SPI + // transport error is returned, as well as errors produced by the internal update (e.g. errSensorMissing, errDiagnostic). + if which&measuremask == 0 { + return nil + } + buf := &h.buf + h.cs(false) + err := h.spi.Tx(nil, buf[:4]) + h.cs(true) + if err != nil { + return err + } + // First two bits are status bits. + status := (buf[0] & statusMask) >> statusOffset + bridgeData := (uint16(buf[0]&^statusMask) << 8) | uint16(buf[1]) + + tempData := uint16(buf[2])<<8 | uint16(buf[3]&0xe0)>>5 + return h.dev.update(status, bridgeData, tempData) +} + +type dev struct { + pressure int32 + temp int32 + cmin, cmax uint16 + pmin, pmax int32 +} + +// Pressure returns the most recently computed pressure value in millipascals (mPa). +// The value is taken from the last successful Update. +func (d *dev) Pressure() int32 { + return d.pressure +} + +// Temperature returns the most recently read temperature value in milliKelvin (mC). +// The value is taken from the last successful Update. +func (d *dev) Temperature() int32 { + return d.temp + 273_150 +} + +// update interprets raw sensor fields (status, bridgeData, tempData) and updates the dev's stored +// pressure and temperature. It returns errSensorMissing when the temperature raw value indicates no sensor +// (tempData == math.MaxUint16), errDiagnostic when the status indicates a device diagnostic condition +// (status == 3), or nil on success. Pressure is computed with integer arithmetic using the configured +// cmin/cmax -> pmin/pmax linear mapping in order to avoid overflows. +func (d *dev) update(status uint8, bridgeData, tempData uint16) error { + if tempData == math.MaxUint16 { + return errSensorMissing + } else if status == 3 { + return errDiagnostic + } + + // Take care not to overflow here. + p := (int32(bridgeData)-int32(d.cmin))*(d.pmax-d.pmin)/int32(d.cmax-d.cmin) + d.pmin + d.temp = int32(tempData) + d.pressure = p + return nil +}