diff --git a/ads1015/ads1015.go b/ads1015/ads1015.go new file mode 100644 index 000000000..f9c8ff87d --- /dev/null +++ b/ads1015/ads1015.go @@ -0,0 +1,315 @@ +// Package ads1015 provides a driver for the ADS1015 4-channel 12-bit ADC +// +// This driver is based on https://github.com/RobTillaart/ADS1X15 +// Datasheet: https://www.ti.com/lit/ds/symlink/ads1015.pdf +package ads1015 // import "tinygo.org/x/drivers/ads1015" + +import ( + "errors" + "time" + + "tinygo.org/x/drivers" +) + +// The datasheet defines the LSB as FS/2048, but we divide by the maximum +// positive code (2047) so that the maximum ADC output maps to full scale. +const maxCode = 2047 + +const conversionTimeout = 100 * time.Millisecond + +var ( + ErrInvalidChannel = errors.New("ads1015: invalid channel") + ErrTimeout = errors.New("ads1015: conversion timeout") +) + +// Config contains the ADS1015 conversion settings. +type Config struct { + // Gain selects the full-scale input range of the PGA. + Gain Gain + + // Mode selects between continuous and single-shot conversions. + Mode Mode + + // DataRate sets the output data rate. + DataRate DataRate + + // ComparatorMode, ComparatorPolarity, ComparatorLatch, and + // ComparatorQueue configure the ALERT/RDY comparator. Leave + // ComparatorQueue at ComparatorQueueDisable (the default) to disable + // the comparator. + ComparatorMode ComparatorMode + ComparatorPolarity ComparatorPolarity + ComparatorLatch ComparatorLatch + ComparatorQueue ComparatorQueue + + conversionPollInterval time.Duration +} + +// DefaultConfig selects the widest gain range (+/-6.144V), single-shot +// mode, 1600 SPS, and disables the comparator. +var DefaultConfig = Config{ + Gain: Gain6144mV, + Mode: ModeSingle, + DataRate: DataRate1600SPS, + ComparatorMode: ComparatorModeTraditional, + ComparatorPolarity: ComparatorPolarityActiveLow, + ComparatorLatch: ComparatorNonLatching, + ComparatorQueue: ComparatorQueueDisable, +} + +// Device is an ADS1015 ADC connected over I2C. +type Device struct { + bus drivers.I2C + Address uint16 + config Config +} + +// New returns a new ADS1015 driver using DefaultConfig. +// Set Address after New if the ADDR pin is different +func New(bus drivers.I2C) *Device { + config := DefaultConfig + config.conversionPollInterval = ConversionDuration(config.DataRate) + + return &Device{ + bus: bus, + Address: Address, + config: config, + } +} + +// Config returns the currently stored configuration. +func (d *Device) Config() Config { + return d.config +} + +// Configure stores config and writes it to the device +func (d *Device) Configure(config Config) error { + d.config = config + d.config.conversionPollInterval = ConversionDuration(d.config.DataRate) + return d.startConversion(muxSingleEnded[0]) +} + +// Connected reports whether an ADS1015 responds on the I2C bus. +func (d *Device) Connected() bool { + _, err := d.readRegister(regConfig) + return err == nil +} + +// SetThresholdLow sets the low threshold used by the comparator. +func (d *Device) SetThresholdLow(v uint16) error { + return d.writeRegister(regLowThreshold, v) +} + +// ThresholdLow returns the low threshold used by the comparator. +func (d *Device) ThresholdLow() (int16, error) { + v, err := d.readRegister(regLowThreshold) + return int16(v), err +} + +// SetThresholdHigh sets the high threshold used by the comparator. +func (d *Device) SetThresholdHigh(v uint16) error { + return d.writeRegister(regHighThreshold, v) +} + +// ThresholdHigh returns the high threshold used by the comparator. +func (d *Device) ThresholdHigh() (int16, error) { + v, err := d.readRegister(regHighThreshold) + return int16(v), err +} + +// EnableConversionReadyPin repurposes the ALERT/RDY pin: instead of acting +// as a threshold comparator, it pulses once per completed conversion. This +// lets external hardware (e.g. an MCU interrupt) detect a finished +// conversion without polling Ready() over I2C. +// +// It does so by writing the high and low threshold registers to the +// special values documented in the datasheet (high threshold's MSB set, +// low threshold's MSB clear), and, if needed, moving ComparatorQueue off +// ComparatorQueueDisable so the pin stays active; the new ComparatorQueue +// takes effect starting with the next conversion. +func (d *Device) EnableConversionReadyPin() error { + if d.config.ComparatorQueue == ComparatorQueueDisable { + d.config.ComparatorQueue = ComparatorQueueAfter1Conv + } + if err := d.SetThresholdHigh(conversionReadyHiThresh); err != nil { + return err + } + return d.SetThresholdLow(conversionReadyLoThresh) +} + +// Read performs a conversion using the given mux setting and returns the +// raw signed 12-bit result. Use ReadADC or ReadADCDifferentialXX for the +// common cases. +func (d *Device) Read(mux Mux) (int16, error) { + if err := d.startConversion(mux); err != nil { + return 0, err + } + + if d.config.Mode == ModeSingle { + // Allow the device to clear the OS bit after starting the conversion. + time.Sleep(d.config.conversionPollInterval) + + start := time.Now() + + for { + ready, err := d.Ready() + if err != nil { + return 0, err + } + if ready { + break + } + if time.Since(start) > conversionTimeout { + return 0, ErrTimeout + } + time.Sleep(d.config.conversionPollInterval) + } + } else { + // In continuous mode, give the device time to complete a + // conversion at the new mux setting; otherwise a stale value left + // over from the previous mux setting would be returned. + time.Sleep(d.config.conversionPollInterval) + } + + return d.Value() +} + +// ReadADC performs a conversion on the given single-ended channel (0-3) and +// returns the raw signed 12-bit result. +func (d *Device) ReadADC(channel uint8) (int16, error) { + if channel > 3 { + return 0, ErrInvalidChannel + } + return d.Read(muxSingleEnded[channel]) +} + +// ReadVoltage performs a conversion on the given single-ended channel (0-3) +// and returns the result in millivolts, using the currently configured gain. +func (d *Device) ReadVoltage(channel uint8) (int32, error) { + raw, err := d.ReadADC(channel) + if err != nil { + return 0, err + } + return d.ToVoltage(raw), nil +} + +// ReadADCDifferential01 performs a differential conversion between AIN0 and +// AIN1 and returns the raw signed 12-bit result. +func (d *Device) ReadADCDifferential01() (int16, error) { + return d.Read(MuxDiff01) +} + +// ReadADCDifferential03 performs a differential conversion between AIN0 and +// AIN3 and returns the raw signed 12-bit result. +func (d *Device) ReadADCDifferential03() (int16, error) { + return d.Read(MuxDiff03) +} + +// ReadADCDifferential13 performs a differential conversion between AIN1 and +// AIN3 and returns the raw signed 12-bit result. +func (d *Device) ReadADCDifferential13() (int16, error) { + return d.Read(MuxDiff13) +} + +// ReadADCDifferential23 performs a differential conversion between AIN2 and +// AIN3 and returns the raw signed 12-bit result. +func (d *Device) ReadADCDifferential23() (int16, error) { + return d.Read(MuxDiff23) +} + +// ToVoltage converts a raw reading, such as one returned by ReadADC, to +// milli volts using the currently configured gain. +func (d *Device) ToVoltage(raw int16) int32 { + return (int32(raw) * d.config.Gain.FullScaleVoltage()) / maxCode +} + +// Request starts a conversion using the given mux setting without waiting +// for it to complete. Use Ready and Value to retrieve the result once it is +// available; Read does both in one call. +func (d *Device) Request(mux Mux) error { + return d.startConversion(mux) +} + +// RequestADC starts a conversion on the given single-ended channel (0-3) +// without waiting for it to complete. +func (d *Device) RequestADC(channel uint8) error { + if channel > 3 { + return ErrInvalidChannel + } + return d.Request(muxSingleEnded[channel]) +} + +// Ready reports whether the most recently requested conversion has finished. +// In single-shot mode, it reports whether the conversion is complete. +// In continuous mode, it reports whether the current conversion has finished; +// it may therefore return false while a conversion is in progress. +func (d *Device) Ready() (bool, error) { + config, err := d.readRegister(regConfig) + if err != nil { + return false, err + } + return config&configOSReady != 0, nil +} + +// Value reads the most recent conversion result from the conversion +// register, without starting a new conversion. +func (d *Device) Value() (int16, error) { + raw, err := d.readRegister(regConversion) + if err != nil { + return 0, err + } + // The 12-bit result is left-justified in the 16-bit register. + return int16(raw) >> 4, nil +} + +// startConversion writes the configuration register to (re)start a +// conversion using the given mux setting and the stored configuration. +func (d *Device) startConversion(mux Mux) error { + config := configOSStart | + uint16(mux) | + uint16(d.config.Gain) | + uint16(d.config.Mode) | + uint16(d.config.DataRate) | + uint16(d.config.ComparatorMode) | + uint16(d.config.ComparatorPolarity) | + uint16(d.config.ComparatorLatch) | + uint16(d.config.ComparatorQueue) + return d.writeRegister(regConfig, config) +} + +func (d *Device) readRegister(reg uint8) (uint16, error) { + buf := make([]byte, 2) + if err := d.bus.Tx(d.Address, []byte{reg}, buf); err != nil { + return 0, err + } + return uint16(buf[0])<<8 | uint16(buf[1]), nil +} + +func (d *Device) writeRegister(reg uint8, value uint16) error { + return d.bus.Tx(d.Address, []byte{reg, byte(value >> 8), byte(value)}, nil) +} + +// ConversionDuration returns the nominal conversion period derived from the +// data rate specified in the ADS1015 datasheet. +// The data rates are specified in Table 8-3; the conversion periods below +// are calculated as 1 / SPS and rounded to the nearest microsecond. +func ConversionDuration(dataRate DataRate) time.Duration { + switch dataRate { + case DataRate128SPS: + return 7813 * time.Microsecond + case DataRate250SPS: + return 4000 * time.Microsecond + case DataRate490SPS: + return 2041 * time.Microsecond + case DataRate920SPS: + return 1087 * time.Microsecond + case DataRate1600SPS: + return 625 * time.Microsecond + case DataRate2400SPS: + return 417 * time.Microsecond + case DataRate3300SPS: + return 303 * time.Microsecond + default: + return 8 * time.Millisecond + } +} diff --git a/ads1015/ads1015_test.go b/ads1015/ads1015_test.go new file mode 100644 index 000000000..6959d740f --- /dev/null +++ b/ads1015/ads1015_test.go @@ -0,0 +1,399 @@ +package ads1015 + +import ( + "errors" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "tinygo.org/x/drivers/tester" +) + +// newFakeDevice wires a Device to an in-memory I2C bus with one fake +// ADS1015 at the default address. The register map starts in the +// power-on-like state: idle (OS/ready bit set) and zeroed thresholds. +func newFakeDevice(c *qt.C) (*Device, *tester.I2CDevice16) { + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, uint8(Address)) + fake.Registers = map[uint8]uint16{ + regConversion: 0, + regConfig: configOSReady, + regLowThreshold: 0, + regHighThreshold: 0, + } + bus.AddDevice(fake) + + return New(bus), fake +} + +// TestNew checks that New() stores the given bus/address and precomputes +// conversionPollInterval from DefaultConfig's DataRate, since Read() relies +// on that field rather than recomputing it on every call. +func TestNew(t *testing.T) { + c := qt.New(t) + dev, _ := newFakeDevice(c) + + c.Assert(dev.Address, qt.Equals, uint16(Address)) + + got := dev.Config() + c.Assert(got.Gain, qt.Equals, DefaultConfig.Gain) + c.Assert(got.Mode, qt.Equals, DefaultConfig.Mode) + c.Assert(got.DataRate, qt.Equals, DefaultConfig.DataRate) + c.Assert(got.conversionPollInterval, qt.Equals, ConversionDuration(DefaultConfig.DataRate)) +} + +// TestConfigure checks that Configure() stores the given config, derives +// conversionPollInterval from its DataRate (issue 1), and starts a +// conversion on channel 0 by writing the config register accordingly. +func TestConfigure(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + + cfg := Config{ + Gain: Gain4096mV, + Mode: ModeContinuous, + DataRate: DataRate250SPS, + ComparatorMode: ComparatorModeWindow, + ComparatorPolarity: ComparatorPolarityActiveHigh, + ComparatorLatch: ComparatorLatching, + ComparatorQueue: ComparatorQueueAfter2Conv, + } + err := dev.Configure(cfg) + c.Assert(err, qt.IsNil) + + got := dev.Config() + c.Assert(got.Gain, qt.Equals, cfg.Gain) + c.Assert(got.DataRate, qt.Equals, cfg.DataRate) + c.Assert(got.conversionPollInterval, qt.Equals, ConversionDuration(DataRate250SPS)) + + want := configOSStart | uint16(MuxSingle0) | uint16(cfg.Gain) | uint16(cfg.Mode) | + uint16(cfg.DataRate) | uint16(cfg.ComparatorMode) | uint16(cfg.ComparatorPolarity) | + uint16(cfg.ComparatorLatch) | uint16(cfg.ComparatorQueue) + c.Assert(fake.Registers[regConfig], qt.Equals, want) +} + +// TestRequest checks that Request() (and thus Read(), which shares the same +// startConversion() call) assembles the config register from every Config +// field plus the mux argument, with the OS/start bit set to trigger a new +// conversion. This is the "config word that startConversion() writes" +// coverage called out in issue 8. +func TestRequest(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + + cfg := Config{ + Gain: Gain0512mV, + Mode: ModeSingle, + DataRate: DataRate3300SPS, + ComparatorMode: ComparatorModeTraditional, + ComparatorPolarity: ComparatorPolarityActiveLow, + ComparatorLatch: ComparatorNonLatching, + ComparatorQueue: ComparatorQueueAfter4Conv, + } + c.Assert(dev.Configure(cfg), qt.IsNil) + + err := dev.Request(MuxDiff13) + c.Assert(err, qt.IsNil) + + want := configOSStart | uint16(MuxDiff13) | uint16(cfg.Gain) | uint16(cfg.Mode) | + uint16(cfg.DataRate) | uint16(cfg.ComparatorMode) | uint16(cfg.ComparatorPolarity) | + uint16(cfg.ComparatorLatch) | uint16(cfg.ComparatorQueue) + c.Assert(fake.Registers[regConfig], qt.Equals, want) +} + +// TestValue checks the sign extension in Value(): the ADS1015 left-justifies +// its 12-bit result in the 16-bit conversion register, so Value() must +// arithmetic-shift right by 4 to recover a signed 12-bit code, not just mask +// off the low bits. +func TestValue(t *testing.T) { + cases := []struct { + name string + raw uint16 + want int16 + }{ + {"positive full scale", 0x7FF0, 2047}, + {"negative full scale", 0x8000, -2048}, + {"minus one", 0xFFF0, -1}, + {"small positive", 0x0010, 1}, + {"zero", 0x0000, 0}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + fake.Registers[regConversion] = tc.raw + + got, err := dev.Value() + c.Assert(err, qt.IsNil) + c.Assert(got, qt.Equals, tc.want) + }) + } +} + +// TestToVoltage checks the raw-code-to-millivolt conversion for each gain +// setting. It uses codes at +/-maxCode so the expected result is an exact, +// hand-computable value, which also documents issue 3: a raw reading of +// +2047 (not +2048) maps to the full-scale voltage, because ToVoltage +// divides by maxCode (2047) rather than the datasheet's 2048. +func TestToVoltage(t *testing.T) { + cases := []struct { + gain Gain + raw int16 + want int32 + }{ + {Gain6144mV, 2047, 6144}, + {Gain6144mV, -2047, -6144}, + {Gain4096mV, 2047, 4096}, + {Gain2048mV, 2047, 2048}, + {Gain1024mV, 2047, 1024}, + {Gain0512mV, 2047, 512}, + {Gain0256mV, 2047, 256}, + {Gain6144mV, 0, 0}, + } + for _, tc := range cases { + c := qt.New(t) + dev, _ := newFakeDevice(c) + cfg := dev.Config() + cfg.Gain = tc.gain + c.Assert(dev.Configure(cfg), qt.IsNil) + + c.Assert(dev.ToVoltage(tc.raw), qt.Equals, tc.want) + } +} + +// TestReady checks that Ready() reflects the OS bit of the config register: +// clear while a conversion is in progress, set once it completes. +func TestReady(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + + fake.Registers[regConfig] = 0x0000 + ready, err := dev.Ready() + c.Assert(err, qt.IsNil) + c.Assert(ready, qt.IsFalse) + + fake.Registers[regConfig] = configOSReady + ready, err = dev.Ready() + c.Assert(err, qt.IsNil) + c.Assert(ready, qt.IsTrue) +} + +// TestConnected checks that Connected() turns an I2C error from reading the +// config register into false, and a successful read into true. +func TestConnected(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + c.Assert(dev.Connected(), qt.IsTrue) + + fake.Err = errors.New("nack") + c.Assert(dev.Connected(), qt.IsFalse) +} + +// TestThresholds checks that SetThresholdLow/SetThresholdHigh write the raw +// 16-bit value given, and ThresholdLow/ThresholdHigh read it back +// reinterpreted as signed (needed since the comparator thresholds are +// signed 12-bit codes left-justified the same way as the conversion +// register). +func TestThresholds(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + + c.Assert(dev.SetThresholdLow(0xFF00), qt.IsNil) + low, err := dev.ThresholdLow() + c.Assert(err, qt.IsNil) + c.Assert(low, qt.Equals, int16(-256)) + c.Assert(fake.Registers[regLowThreshold], qt.Equals, uint16(0xFF00)) + + c.Assert(dev.SetThresholdHigh(0x7F00), qt.IsNil) + high, err := dev.ThresholdHigh() + c.Assert(err, qt.IsNil) + c.Assert(high, qt.Equals, int16(0x7F00)) + c.Assert(fake.Registers[regHighThreshold], qt.Equals, uint16(0x7F00)) +} + +// TestEnableConversionReadyPin checks that it writes the documented +// "conversion ready" sentinel values (high threshold MSB set, low threshold +// MSB clear, issue 7's magic-value constants) to the threshold registers, +// and that it only promotes ComparatorQueue off ComparatorQueueDisable, so +// it never clobbers a queue depth the caller already chose. +func TestEnableConversionReadyPin(t *testing.T) { + t.Run("disabled queue is promoted", func(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + + err := dev.EnableConversionReadyPin() + c.Assert(err, qt.IsNil) + c.Assert(fake.Registers[regHighThreshold], qt.Equals, conversionReadyHiThresh) + c.Assert(fake.Registers[regLowThreshold], qt.Equals, conversionReadyLoThresh) + c.Assert(dev.Config().ComparatorQueue, qt.Equals, ComparatorQueueAfter1Conv) + }) + + t.Run("existing queue depth is preserved", func(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + cfg := dev.Config() + cfg.ComparatorQueue = ComparatorQueueAfter4Conv + c.Assert(dev.Configure(cfg), qt.IsNil) + + err := dev.EnableConversionReadyPin() + c.Assert(err, qt.IsNil) + c.Assert(fake.Registers[regHighThreshold], qt.Equals, conversionReadyHiThresh) + c.Assert(fake.Registers[regLowThreshold], qt.Equals, conversionReadyLoThresh) + c.Assert(dev.Config().ComparatorQueue, qt.Equals, ComparatorQueueAfter4Conv) + }) +} + +// TestReadADCInvalidChannel checks that channel numbers outside 0-3 are +// rejected before touching the bus, for both the blocking and +// request/response APIs. +func TestReadADCInvalidChannel(t *testing.T) { + c := qt.New(t) + dev, _ := newFakeDevice(c) + + _, err := dev.ReadADC(4) + c.Assert(err, qt.Equals, ErrInvalidChannel) + + err = dev.RequestADC(4) + c.Assert(err, qt.Equals, ErrInvalidChannel) +} + +// TestReadVoltage checks that ReadVoltage combines a conversion on the +// requested channel with ToVoltage's gain-scaled conversion. +func TestReadVoltage(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + cfg := dev.Config() + cfg.Gain = Gain2048mV + cfg.DataRate = DataRate3300SPS + c.Assert(dev.Configure(cfg), qt.IsNil) + + // Full-scale positive code, left-justified. + fake.Registers[regConversion] = 0x7FF0 + + mv, err := dev.ReadVoltage(0) + c.Assert(err, qt.IsNil) + c.Assert(mv, qt.Equals, int32(2048)) + c.Assert(fake.Registers[regConfig]&uint16(MuxSingle0), qt.Equals, uint16(MuxSingle0)) +} + +// TestReadContinuousModeWaitsForDataRate is a regression test for issue 1: +// in continuous mode, Read() must wait at least one full conversion period +// at the configured DataRate before reading Value(), or it can return the +// previous mux setting's stale result. Before the fix this only slept a +// fixed 1ms, which is far shorter than the ~7.8ms a 128 SPS conversion +// takes. +func TestReadContinuousModeWaitsForDataRate(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + + cfg := dev.Config() + cfg.Mode = ModeContinuous + cfg.DataRate = DataRate128SPS + c.Assert(dev.Configure(cfg), qt.IsNil) + + fake.Registers[regConversion] = 0x0100 + + start := time.Now() + val, err := dev.Read(MuxSingle1) + elapsed := time.Since(start) + + c.Assert(err, qt.IsNil) + c.Assert(val, qt.Equals, int16(0x0100)>>4) + c.Assert(elapsed >= ConversionDuration(DataRate128SPS), qt.IsTrue, + qt.Commentf("Read() only waited %s, want at least %s", elapsed, ConversionDuration(DataRate128SPS))) +} + +// TestReadSingleShotWaitsBeforeFirstPoll is a regression test for issue 4: +// Read() must sleep before its first Ready() poll in single-shot mode. The +// fake device reports "ready" immediately (its OS bit is never cleared by a +// real conversion), which reproduces the worst case where the very first +// poll would misreport completion; the elapsed time must still cover one +// full conversion period, proving the pre-poll sleep executed. +func TestReadSingleShotWaitsBeforeFirstPoll(t *testing.T) { + c := qt.New(t) + dev, fake := newFakeDevice(c) + + cfg := dev.Config() + cfg.Mode = ModeSingle + cfg.DataRate = DataRate128SPS + c.Assert(dev.Configure(cfg), qt.IsNil) + + // The fake never clears the OS bit, so without a pre-poll sleep, + // Read() would return almost instantly. + fake.Registers[regConfig] = configOSReady + fake.Registers[regConversion] = 0x0200 + + start := time.Now() + val, err := dev.Read(MuxSingle2) + elapsed := time.Since(start) + + c.Assert(err, qt.IsNil) + c.Assert(val, qt.Equals, int16(0x0200)>>4) + c.Assert(elapsed >= ConversionDuration(DataRate128SPS), qt.IsTrue, + qt.Commentf("Read() returned after only %s, want at least %s", elapsed, ConversionDuration(DataRate128SPS))) +} + +// stuckBus is a drivers.I2C fake whose ADS1015 emulation clears the OS/ready +// bit whenever the config register is written with the start bit set, and +// never sets it again, as if a conversion had stalled. tester.I2CDevice16's +// plain register map can't express this: startConversion's own write always +// includes the OS/start bit (0x8000), and since that bit shares its +// position with the OS/ready bit read back, a static map immediately reads +// back as "ready". +type stuckBus struct { + config uint16 +} + +func (b *stuckBus) Tx(addr uint16, w, r []byte) error { + switch { + case len(w) == 1 && r != nil: + var val uint16 + if w[0] == regConfig { + val = b.config + } + r[0], r[1] = byte(val>>8), byte(val) + case len(w) == 3: + val := uint16(w[1])<<8 | uint16(w[2]) + if w[0] == regConfig { + b.config = val &^ configOSReady + } + } + return nil +} + +// TestReadTimeout checks that Read() gives up and returns ErrTimeout in +// single-shot mode if the OS bit never sets, instead of polling forever. +func TestReadTimeout(t *testing.T) { + c := qt.New(t) + dev := New(&stuckBus{}) + + cfg := dev.Config() + cfg.Mode = ModeSingle + cfg.DataRate = DataRate3300SPS + c.Assert(dev.Configure(cfg), qt.IsNil) + + _, err := dev.Read(MuxSingle0) + c.Assert(err, qt.Equals, ErrTimeout) +} + +// TestConversionDuration checks the datasheet-derived period for every +// DataRate value, plus the fallback for an unrecognized value. +func TestConversionDuration(t *testing.T) { + cases := []struct { + rate DataRate + want time.Duration + }{ + {DataRate128SPS, 7813 * time.Microsecond}, + {DataRate250SPS, 4000 * time.Microsecond}, + {DataRate490SPS, 2041 * time.Microsecond}, + {DataRate920SPS, 1087 * time.Microsecond}, + {DataRate1600SPS, 625 * time.Microsecond}, + {DataRate2400SPS, 417 * time.Microsecond}, + {DataRate3300SPS, 303 * time.Microsecond}, + {DataRate(0xFFFF), 8 * time.Millisecond}, + } + for _, tc := range cases { + c := qt.New(t) + c.Assert(ConversionDuration(tc.rate), qt.Equals, tc.want) + } +} diff --git a/ads1015/registers.go b/ads1015/registers.go new file mode 100644 index 000000000..783da8c5a --- /dev/null +++ b/ads1015/registers.go @@ -0,0 +1,146 @@ +package ads1015 + +// Address is the default I2C address of the ADS1015. The actual address +// depends on how the ADDR pin is wired +const AddressToGND uint16 = 0b1001000 +const AddressToVDD uint16 = 0b1001001 +const AddressToSDA uint16 = 0b1001010 +const AddressToSCL uint16 = 0b1001011 +const Address = AddressToGND // default address + +// Registers, see the datasheet Table 8-2 +const ( + regConversion uint8 = 0b00 + regConfig uint8 = 0b01 + regLowThreshold uint8 = 0b10 + regHighThreshold uint8 = 0b11 +) + +// Config register bit masks, see the datasheet section 8.6.3. +const ( + // configOSStart, written to bit 15, starts a single conversion. + configOSStart uint16 = 0x8000 + // configOSReady, read from bit 15, is set when no conversion is + // in progress. + configOSReady uint16 = 0x8000 +) + +// Mux selects the input(s) measured by a conversion. +type Mux uint16 + +// Table 8-4 +const ( + MuxDiff01 Mux = 0b000 << 12 // AINP = AIN0, AINN = AIN1 + MuxDiff03 Mux = 0b001 << 12 // AINP = AIN0, AINN = AIN3 + MuxDiff13 Mux = 0b010 << 12 // AINP = AIN1, AINN = AIN3 + MuxDiff23 Mux = 0b011 << 12 // AINP = AIN2, AINN = AIN3 + MuxSingle0 Mux = 0b100 << 12 // AINP = AIN0, AINN = GND + MuxSingle1 Mux = 0b101 << 12 // AINP = AIN1, AINN = GND + MuxSingle2 Mux = 0b110 << 12 // AINP = AIN2, AINN = GND + MuxSingle3 Mux = 0b111 << 12 // AINP = AIN3, AINN = GND +) + +// muxSingleEnded maps a channel number (0-3) to its Mux setting. +var muxSingleEnded = [4]Mux{MuxSingle0, MuxSingle1, MuxSingle2, MuxSingle3} + +// Gain selects the full-scale input range of the programmable gain +// amplifier (PGA). +type Gain uint16 + +const ( + Gain6144mV Gain = 0b000 << 9 // +/-6.144V + Gain4096mV Gain = 0b001 << 9 // +/-4.096V + Gain2048mV Gain = 0b010 << 9 // +/-2.048V, power-on default + Gain1024mV Gain = 0b011 << 9 // +/-1.024V + Gain0512mV Gain = 0b100 << 9 // +/-0.512V + Gain0256mV Gain = 0b101 << 9 // +/-0.256V + + // The 0b110 and 0b111 codes repeat the previous setting. +) + +// FullScaleVoltage returns the largest voltage magnitude, in milliVolts, that a +// conversion at this gain can represent. +func (g Gain) FullScaleVoltage() int32 { + switch g { + case Gain6144mV: + return 6144 + case Gain4096mV: + return 4096 + case Gain2048mV: + return 2048 + case Gain1024mV: + return 1024 + case Gain0512mV: + return 512 + case Gain0256mV: + return 256 + default: + return 0 + } +} + +// Mode selects between continuous and single-shot conversions. +type Mode uint16 + +const ( + ModeContinuous Mode = 0b0 << 8 + ModeSingle Mode = 0b1 << 8 +) + +// DataRate sets the output data rate, in samples per second (SPS). +type DataRate uint16 + +const ( + DataRate128SPS DataRate = 0b000 << 5 + DataRate250SPS DataRate = 0b001 << 5 + DataRate490SPS DataRate = 0b010 << 5 + DataRate920SPS DataRate = 0b011 << 5 + DataRate1600SPS DataRate = 0b100 << 5 // power-on default + DataRate2400SPS DataRate = 0b101 << 5 + DataRate3300SPS DataRate = 0b110 << 5 + + // 0b111 repeats the previous data-rate setting. +) + +// ComparatorMode selects between traditional and window comparator modes. +type ComparatorMode uint16 + +const ( + ComparatorModeTraditional ComparatorMode = 0b0 << 4 + ComparatorModeWindow ComparatorMode = 0b1 << 4 +) + +// ComparatorPolarity sets the polarity of the ALERT/RDY pin when the +// comparator is active. +type ComparatorPolarity uint16 + +const ( + ComparatorPolarityActiveLow ComparatorPolarity = 0b0 << 3 + ComparatorPolarityActiveHigh ComparatorPolarity = 0b1 << 3 +) + +// ComparatorLatch enables or disables the latching comparator. +type ComparatorLatch uint16 + +const ( + ComparatorNonLatching ComparatorLatch = 0b0 << 2 + ComparatorLatching ComparatorLatch = 0b1 << 2 +) + +// ComparatorQueue sets how many successive conversions must lie beyond the +// threshold before ALERT/RDY is asserted, or disables the comparator. +type ComparatorQueue uint16 + +const ( + ComparatorQueueAfter1Conv ComparatorQueue = 0b00 + ComparatorQueueAfter2Conv ComparatorQueue = 0b01 + ComparatorQueueAfter4Conv ComparatorQueue = 0b10 + ComparatorQueueDisable ComparatorQueue = 0b11 // power-on default +) + +const ( + // Conversion Ready mode (bit 15) + // Hi_thresh MSB = 1 , Lo_thresh MSB = 0 + conversionReadyHiThresh uint16 = 0b1 << 15 + conversionReadyLoThresh uint16 = 0b0 << 15 +) diff --git a/examples/ads1015/main.go b/examples/ads1015/main.go new file mode 100644 index 000000000..efc659aa9 --- /dev/null +++ b/examples/ads1015/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/ads1015" +) + +var adc = ads1015.New(machine.I2C1) + +func main() { + if err := machine.I2C1.Configure(machine.I2CConfig{ + SDA: machine.P0_17, + SCL: machine.P0_20, + Frequency: 2.0 * machine.MHz, + }); err != nil { + println("could not configure I2C:", err.Error()) + return + } + + if !adc.Connected() { + println("ADS1015 not detected") + return + } + + config := ads1015.DefaultConfig + config.Gain = ads1015.Gain4096mV + if err := adc.Configure(config); err != nil { + println("could not configure ADS1015:", err.Error()) + return + } + + for { + for channel := uint8(0); channel < 4; channel++ { + raw, err := adc.ReadADC(channel) + if err != nil { + println("could not read channel:", err.Error()) + continue + } + voltage := adc.ToVoltage(raw) + + print("AIN", channel, ": raw=", raw, " voltage=") + println(voltage, "mV") + } + + println() + time.Sleep(500 * time.Millisecond) + } +} diff --git a/smoketest.sh b/smoketest.sh index 3d20b67d8..1fe2ec559 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -6,6 +6,7 @@ tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/adafruit4650 +tinygo build -size short -o ./build/test.hex -target=nicenano ./examples/ads1015/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adt7410/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/adxl345/main.go tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/amg88xx