From 6d431e0726f690a8dc80ef53cbda5c60ce715834 Mon Sep 17 00:00:00 2001 From: Warren Guy <5602790+warrenguy@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:45:33 +0100 Subject: [PATCH] Add I2C INA219 driver (#705) INA219: add INA219 driver --- examples/ina219/main.go | 29 +++++ ina219/config.go | 183 +++++++++++++++++++++++++++++++ ina219/errors.go | 13 +++ ina219/ina219.go | 202 ++++++++++++++++++++++++++++++++++ ina219/ina219_test.go | 232 ++++++++++++++++++++++++++++++++++++++++ ina219/registers.go | 13 +++ smoketest.sh | 1 + 7 files changed, 673 insertions(+) create mode 100644 examples/ina219/main.go create mode 100644 ina219/config.go create mode 100644 ina219/errors.go create mode 100644 ina219/ina219.go create mode 100644 ina219/ina219_test.go create mode 100644 ina219/registers.go diff --git a/examples/ina219/main.go b/examples/ina219/main.go new file mode 100644 index 0000000..50fcba2 --- /dev/null +++ b/examples/ina219/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/ina219" +) + +func main() { + machine.I2C0.Configure(machine.I2CConfig{}) + + dev := ina219.New(machine.I2C0) + dev.Configure() + + for { + busVoltage, shuntVoltage, current, power, err := dev.Measurements() + if err != nil { + println("Error reading measurements", err) + } + + println("Bus Voltage:", busVoltage, "V") + println("Shunt Voltage:", shuntVoltage/100, "mV") + println("Current:", current, "mA") + println("Power:", power, "mW") + + time.Sleep(10 * time.Millisecond) + } +} diff --git a/ina219/config.go b/ina219/config.go new file mode 100644 index 0000000..4c02179 --- /dev/null +++ b/ina219/config.go @@ -0,0 +1,183 @@ +package ina219 + +type Config struct { + // BusVoltageRange sets the bus voltage range. + BusVoltageRange BusVoltageRange + + // PGA sets the programmable gain amplifier. + PGA PGA + + // BusADC sets the bus ADC resolution. + BusADC BusADC + + // ShuntADC sets the shunt ADC resolution. + ShuntADC ShuntADC + + // Mode sets the operating mode. + Mode Mode + + // Calibration sets the calibration value for the expected + // voltage and current values. + Calibration Calibration + + // 1000 / uA per bit + CurrentDivider float32 + + // 1mW per bit + PowerMultiplier float32 +} + +// RegisterValue returns the register value of the configuration. +func (c *Config) RegisterValue() uint16 { + return c.BusVoltageRange.RegisterValue() | + c.PGA.RegisterValue() | + c.BusADC.RegisterValue() | + c.ShuntADC.RegisterValue() | + c.Mode.RegisterValue() +} + +// Generate a new configuration from a register value. +func NewConfig(config int16, calibration int16) Config { + return Config{ + BusVoltageRange: BusVoltageRange(config >> 13 & 0x1), + PGA: PGA(config >> 11 & 0x3), + BusADC: BusADC(config >> 7 & 0xF), + ShuntADC: ShuntADC(config >> 3 & 0xF), + Mode: Mode(config & 0x7), + Calibration: Calibration(calibration), + } +} + +// Configurations from +// https://github.com/adafruit/Adafruit_INA219/blob/master/Adafruit_INA219.cpp +var ( + // Config32V2A is a configuration for a 32V 2A range. + Config32V2A = Config{ + BusVoltageRange: Range32V, + PGA: PGA8, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration16V400mA, + CurrentDivider: 10.0, + PowerMultiplier: 2.0, + } + + // Config32V1A is a configuration for a 32V 1A range. + Config32V1A = Config{ + BusVoltageRange: Range32V, + PGA: PGA8, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration32V1A, + CurrentDivider: 25.0, + PowerMultiplier: 0.8, + } + + // Config16V400mA is a configuration for a 16V 400mA range. + Config16V400mA = Config{ + BusVoltageRange: Range16V, + PGA: PGA1, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration16V400mA, + CurrentDivider: 20.0, + PowerMultiplier: 1.0, + } +) + +// BusVoltageRange is the bus voltage range. +type BusVoltageRange int8 + +const ( + Range16V BusVoltageRange = 0 // 0-16V + Range32V BusVoltageRange = 1 // 0-32V +) + +func (r BusVoltageRange) RegisterValue() uint16 { + return uint16(r) << 13 +} + +// PGA is the programmable gain amplifier. +type PGA int8 + +const ( + PGA1 PGA = 0 // 40mV + PGA2 PGA = 1 // 80mV + PGA4 PGA = 2 // 160mV + PGA8 PGA = 3 // 320mV +) + +func (p PGA) RegisterValue() uint16 { + return uint16(p) << 11 +} + +// BusADC is the bus ADC resolution. +type BusADC int8 + +const ( + ADC9 BusADC = 0 // 9-bit + ADC10 BusADC = 1 // 10-bit + ADC11 BusADC = 2 // 11-bit + ADC12 BusADC = 3 // 12-bit +) + +func (b BusADC) RegisterValue() uint16 { + return uint16(b) << 7 +} + +// ShuntADC is the shunt ADC resolution. +type ShuntADC int8 + +const ( + SADC9 ShuntADC = 0 // 9-bit + SADC10 ShuntADC = 1 // 10-bit + SADC11 ShuntADC = 2 // 11-bit + SADC12 ShuntADC = 3 // 12-bit +) + +func (s ShuntADC) RegisterValue() uint16 { + return uint16(s) << 3 +} + +// Mode is the operating mode. +type Mode int8 + +const ( + ModePowerDown Mode = 0 // power-down + ModeTrigShunt Mode = 1 // triggered shunt voltage + ModeTrigBus Mode = 2 // triggered bus voltage + ModeTrigShuntBus Mode = 3 // triggered shunt and bus voltage + ModeADCOff Mode = 4 // ADC off + ModeContShunt Mode = 5 // continuous shunt voltage + ModeContBus Mode = 6 // continuous bus voltage + ModeContShuntBus Mode = 7 // continuous shunt and bus voltage +) + +// ModeTriggered is a mask for triggered modes. +const ModeTriggeredMask Mode = 0x4 + +// ModeTriggered returns true if the mode is a triggered mode. +func ModeTriggered(m Mode) bool { + return m != ModePowerDown && m&ModeTriggeredMask == 0 +} + +func (m Mode) RegisterValue() uint16 { + return uint16(m) +} + +// Calibration is the calibration register for the INA219. Values from: +// https://github.com/adafruit/Adafruit_INA219/blob/master/Adafruit_INA219.cpp +type Calibration uint16 + +const ( + Calibration32V2A Calibration = 4096 + Calibration32V1A Calibration = 10240 + Calibration16V400mA Calibration = 8192 +) + +func (c Calibration) RegisterValue() uint16 { + return uint16(c) +} diff --git a/ina219/errors.go b/ina219/errors.go new file mode 100644 index 0000000..fe078fc --- /dev/null +++ b/ina219/errors.go @@ -0,0 +1,13 @@ +package ina219 + +type ErrOverflow struct{} + +func (e ErrOverflow) Error() string { return "overflow" } + +type ErrNotReady struct{} + +func (e ErrNotReady) Error() string { return "not ready" } + +type ErrConfigMismatch struct{} + +func (e ErrConfigMismatch) Error() string { return "config mismatch" } diff --git a/ina219/ina219.go b/ina219/ina219.go new file mode 100644 index 0000000..0f7c299 --- /dev/null +++ b/ina219/ina219.go @@ -0,0 +1,202 @@ +package ina219 + +import ( + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" +) + +// An INA219 device. +type Device struct { + bus drivers.I2C + Address uint16 + config Config +} + +// Create a new INA219 device with the default configuration +// and the given I2C bus at the default address. +// +// Set Address after New to change the address. +// +// Call Configure after New to write the configuration to the +// device. If you don't call Configure, the device may have a +// different configuration and the power divider and current +// multiplier are probably wrong. +func New(bus drivers.I2C) Device { + return Device{ + bus: bus, + Address: Address, + config: Config32V2A, + } +} + +// Set the configuration for the device. This only changes the +// configuration in memory, not on the device. Call Configure +// to write the configuration to the device. +func (d *Device) SetConfig(config Config) { + d.config = config +} + +// Write the current configuration to the device. +func (d *Device) Configure() (err error) { + if err = d.WriteRegister( + RegConfig, + d.config.RegisterValue(), + ); err != nil { + return + } + + if err = d.WriteRegister( + RegCalibration, + d.config.Calibration.RegisterValue(), + ); err != nil { + return + } + + var readConfig Config + // make sure the configuration is read back correctly + if readConfig, err = d.ReadConfig(); err != nil { + return + } else if readConfig.RegisterValue() != d.config.RegisterValue() { + err = ErrConfigMismatch{} + } else if readConfig.Calibration.RegisterValue() != d.config.Calibration.RegisterValue() { + err = ErrConfigMismatch{} + } + + return +} + +// Trigger a conversion. This is only necessary if the device is in +// trigger mode. In continuous mode (the default), the device will +// automatically trigger conversions and this has no effect. See +// config.go. +// +// Triggering a conversion or reading the "power" register resets +// the conversion ready bit. +func (d *Device) Trigger() (err error) { + // Only trigger if the mode is one of the triggered modes. + if ModeTriggered(d.config.Mode) { + err = d.WriteRegister(RegConfig, d.config.RegisterValue()) + } + return +} + +// Measurements reads the bus voltage, shunt voltage, current, and power +// from the device. +func (d *Device) Measurements() ( + busVoltage int16, + shuntVoltage int16, + current float32, + power float32, + err error, +) { + // Attempt to read bus voltage first, so we can check for overflow + // or conversion not ready. + if busVoltage, err = d.BusVoltage(); err != nil { + return + } + + // Read the rest of the values, reading Power last, which resets + // the conversion ready bit (relevant for triggered modes). + if shuntVoltage, err = d.ShuntVoltage(); err != nil { + return + } + + if current, err = d.Current(); err != nil { + return + } + + if power, err = d.Power(); err != nil { + return + } + + return +} + +// BusVoltage reads the "bus" voltage in millivolts. +// +// It returns an error if the value is invalid due to overflow +// or if the conversion is not ready yet. In a continuous mode +// there should always be a measurement available after the +// device is ready. See above notes on Trigger. +func (d *Device) BusVoltage() (voltage int16, err error) { + val, err := d.ReadRegister(RegBusVoltage) + if err != nil { + return + } + + // The overflow bit is set, so the values are invalid. + if val&(1<<0) != 0 { + err = ErrOverflow{} + return + } + + // The conversion is not ready yet. + if ModeTriggered(d.config.Mode) && val&(1<<1) != 0 { + err = ErrNotReady{} + return + } + + voltage = (int16(val) >> 3) * 4 + return +} + +// ShuntVoltage reads the "shunt" voltage in 100ths of a millivolt. +func (d *Device) ShuntVoltage() (voltage int16, err error) { + return d.ReadRegister(RegShuntVoltage) +} + +// Current reads the current in milliamps. +func (d *Device) Current() (current float32, err error) { + val, err := d.ReadRegister(RegCurrent) + if err != nil { + return + } + + current = float32(val) / d.config.CurrentDivider + return +} + +// Power reads the power in milliwatts. +func (d *Device) Power() (power float32, err error) { + val, err := d.ReadRegister(RegPower) + if err != nil { + return + } + + power = float32(val) * d.config.PowerMultiplier + return +} + +// Read the configuration from the device. +func (d *Device) ReadConfig() (config Config, err error) { + var cfg, cal int16 + if cfg, err = d.ReadRegister(RegConfig); err != nil { + return + } + + if cal, err = d.ReadRegister(RegCalibration); err != nil { + return + } + + config = NewConfig(cfg, cal) + return +} + +// Read a register from the device. +func (d *Device) ReadRegister(reg uint8) (val int16, err error) { + buf := make([]byte, 2) + + err = legacy.ReadRegister(d.bus, uint8(d.Address), reg, buf) + if err != nil { + return + } + + val = int16(buf[0])<<8 | int16(buf[1]&0xff) + return +} + +// Write to a register on the device. +func (d *Device) WriteRegister(reg uint8, val uint16) error { + buf := []byte{byte(val >> 8), byte(val & 0xff)} + return legacy.WriteRegister(d.bus, uint8(d.Address), reg, buf) +} diff --git a/ina219/ina219_test.go b/ina219/ina219_test.go new file mode 100644 index 0000000..f5a12ab --- /dev/null +++ b/ina219/ina219_test.go @@ -0,0 +1,232 @@ +package ina219 + +import ( + "fmt" + "testing" + + qt "github.com/frankban/quicktest" + "tinygo.org/x/drivers/tester" +) + +func TestDefaultAddress(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + dev := New(bus) + c.Assert(dev.Address, qt.Equals, uint16(Address)) +} + +func TestBusVoltage(t *testing.T) { + t.Run("valid", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: (4200 << 3) / 4, // 4.2V + } + bus.AddDevice(fake) + + dev := New(bus) + voltage, err := dev.BusVoltage() + c.Assert(err, qt.IsNil) + c.Assert(voltage, qt.Equals, int16(4200)) + }) + + t.Run("overflow", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: (1 >> 0), // overflow + } + bus.AddDevice(fake) + + dev := New(bus) + _, err := dev.BusVoltage() + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err, qt.ErrorMatches, ErrOverflow{}.Error()) + }) + + t.Run("not ready", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: ((4200 << 3) / 4) | (1 << 1), // not ready + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.Mode = ModeTrigBus + _, err := dev.BusVoltage() + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err, qt.ErrorMatches, ErrNotReady{}.Error()) + }) +} + +func TestShuntVoltage(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegShuntVoltage: 0x1234, + } + bus.AddDevice(fake) + + dev := New(bus) + voltage, err := dev.ShuntVoltage() + c.Assert(err, qt.IsNil) + c.Assert(voltage, qt.Equals, int16(0x1234)) +} + +func TestCurrent(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegCurrent: 420 * 6.9, // 420mA + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.CurrentDivider = 6.9 + current, err := dev.Current() + c.Assert(err, qt.IsNil) + c.Assert(current, qt.Equals, float32(420)) +} + +func TestPower(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegPower: 420 / 0.8, // 420mW + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.PowerMultiplier = 0.8 + power, err := dev.Power() + c.Assert(err, qt.IsNil) + c.Assert(power, qt.Equals, float32(420)) +} + +func TestReadConfig(t *testing.T) { + // use the default configurations + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegConfig: tc.RegisterValue(), + RegCalibration: tc.Calibration.RegisterValue(), + } + bus.AddDevice(fake) + + dev := New(bus) + config, err := dev.ReadConfig() + c.Assert(err, qt.IsNil) + c.Assert(config.BusADC, qt.Equals, tc.BusADC) + c.Assert(config.BusVoltageRange, qt.Equals, tc.BusVoltageRange) + c.Assert(config.Calibration, qt.Equals, tc.Calibration) + c.Assert(config.Mode, qt.Equals, tc.Mode) + c.Assert(config.PGA, qt.Equals, tc.PGA) + c.Assert(config.ShuntADC, qt.Equals, tc.ShuntADC) + }) + } +} + +func TestWriteConfig(t *testing.T) { + // use the default configurations + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegConfig: 0, + RegCalibration: 0, + } + + dev := New(bus) + dev.config = tc + err := dev.Configure() + c.Assert(err, qt.IsNil) + c.Assert(fake.Registers[RegConfig], qt.Equals, tc.RegisterValue()) + c.Assert(fake.Registers[RegCalibration], qt.Equals, tc.Calibration.RegisterValue()) + }) + } +} + +func TestSetConfig(t *testing.T) { + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + dev := New(bus) + dev.SetConfig(tc) + c.Assert(dev.config, qt.Equals, tc) + }) + } +} + +func TestTrigger(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegConfig: Config32V2A.RegisterValue(), + } + + dev := New(bus) + dev.config = Config32V2A + dev.config.Mode = ModeTrigBus + err := dev.Trigger() + c.Assert(err, qt.IsNil) + c.Assert(fake.Registers[RegConfig], qt.Equals, dev.config.RegisterValue()) +} + +func TestMeasurements(t *testing.T) { + bvVal := int16(4200) + svVal := int16(1234) + iVal := float32(420) + pVal := float32(420) + + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: uint16(((4200 << 3) / 4) | (1 << 1)), + RegShuntVoltage: uint16(svVal), + RegCurrent: uint16(iVal * Config16V400mA.CurrentDivider), + RegPower: uint16(pVal / Config16V400mA.PowerMultiplier), + } + + dev := New(bus) + dev.config = Config16V400mA + bv, sv, i, p, err := dev.Measurements() + c.Assert(err, qt.IsNil) + c.Assert(bv, qt.Equals, bvVal) + c.Assert(sv, qt.Equals, svVal) + c.Assert(i, qt.Equals, iVal) + c.Assert(p, qt.Equals, pVal) + +} diff --git a/ina219/registers.go b/ina219/registers.go new file mode 100644 index 0000000..f1bae3a --- /dev/null +++ b/ina219/registers.go @@ -0,0 +1,13 @@ +package ina219 + +// The default I2C address for this device. +const Address = 0x40 + +const ( + RegConfig uint8 = 0x0 + RegShuntVoltage uint8 = 0x1 + RegBusVoltage uint8 = 0x2 + RegPower uint8 = 0x3 + RegCurrent uint8 = 0x4 + RegCalibration uint8 = 0x5 +) diff --git a/smoketest.sh b/smoketest.sh index 211e92f..b09c063 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -109,6 +109,7 @@ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/tim tinygo build -size short -o ./build/test.hex -target=pico ./examples/qmi8658c/main.go tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8591/ tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina260/main.go +tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina219/main.go tinygo build -size short -o ./build/test.hex -target=nucleo-l432kc ./examples/aht20/main.go tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/sdcard/console/ tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/i2csoft/adt7410/