From edf9ba92be40e4122f0189bbde026d749d17d1cb Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Sun, 23 Aug 2020 18:41:07 +0100 Subject: [PATCH] mcp23017: new driver for MCP23017 (I2C port expander) --- Makefile | 2 + examples/mcp23017/main.go | 39 ++++ mcp23017/device.go | 362 +++++++++++++++++++++++++++++++++++ mcp23017/device_test.go | 173 +++++++++++++++++ mcp23017/fakedev_test.go | 97 ++++++++++ mcp23017/multidevice.go | 190 ++++++++++++++++++ mcp23017/multidevice_test.go | 166 ++++++++++++++++ 7 files changed, 1029 insertions(+) create mode 100644 examples/mcp23017/main.go create mode 100644 mcp23017/device.go create mode 100644 mcp23017/device_test.go create mode 100644 mcp23017/fakedev_test.go create mode 100644 mcp23017/multidevice.go create mode 100644 mcp23017/multidevice_test.go diff --git a/Makefile b/Makefile index dc0b3e8..e0d02b6 100644 --- a/Makefile +++ b/Makefile @@ -83,6 +83,8 @@ smoke-test: @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mag3110/main.go @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp23017/main.go + @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp3008/main.go @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=microbit ./examples/microbitmatrix/main.go diff --git a/examples/mcp23017/main.go b/examples/mcp23017/main.go new file mode 100644 index 0000000..93354be --- /dev/null +++ b/examples/mcp23017/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "machine" + + "tinygo.org/x/drivers/mcp23017" +) + +func main() { + err := machine.I2C0.Configure(machine.I2CConfig{ + Frequency: machine.TWI_FREQ_400KHZ, + }) + if err != nil { + panic(err) + } + dev, err := mcp23017.NewI2C(machine.I2C0, 0x20) + if err != nil { + panic(err) + } + // Configure pin 0 for input and all the others for output. + if err := dev.SetModes([]mcp23017.PinMode{ + mcp23017.Input | mcp23017.Pullup, + mcp23017.Output, + }); err != nil { + panic(err) + } + input := dev.Pin(0) + outputMask := ^mcp23017.Pins(1 << 0) // All except pin 0 + inputVal, err := input.Get() + if err != nil { + panic(err) + } + println("input value: ", inputVal) + // Set the values of all the output pins. + err = dev.SetPins(0b1011011_01101110, outputMask) + if err != nil { + panic(err) + } +} diff --git a/mcp23017/device.go b/mcp23017/device.go new file mode 100644 index 0000000..341a043 --- /dev/null +++ b/mcp23017/device.go @@ -0,0 +1,362 @@ +// Package mcp23017 implements a driver for the MCP23017 +// I2C port expander chip. See https://www.microchip.com/wwwproducts/en/MCP23017 +// for details of the interface. +// +// It also provides a way of joining several such devices into one logical +// device (see the Devices type). +package mcp23017 + +import ( + "errors" +) + +const ( + // hwAddressFixed holds the bits of the hardware address + // that are fixed by the chip. Bits 0-3 (those in hwAddressMask) + // are user-defined by the A0-A2 pins on the chip. + hwAddress = uint8(0b010_0000) + // hwAddressMask holds the bits that are significant in hwAddress. + hwAddressMask = uint8(0b111_1000) +) + +type register uint8 + +const ( + // The following registers all refer to port A (except + // rIOCON with is port-agnostic). + // ORing them with portB makes them refer to port B. + rIODIR = register(0x00) // I/O direction. 0=output; 1=input. + rIOPOL = register(0x02) // Invert input values. 0=normal; 1=inverted. + rGPINTEN = register(0x04) + rDEFVAL = register(0x06) + rINTCON = register(0x08) + rIOCON = register(0x0A) + rGPPU = register(0x0C) // Pull up; 0=no pull-up; 1=pull-up. + rINTF = register(0x0E) + rINTCAP = register(0x10) + rGPIO = register(0x12) // GPIO pin values. + rOLAT = register(0x14) + registerCount = 0x16 + + portB = register(0x1) +) + +// PinCount is the number of GPIO pins available on the chip. +const PinCount = 16 + +// PinMode represents a possible I/O mode for a pin. +// The zero value represents the default value +// after the chip is reset (input). +type PinMode uint8 + +const ( + // Input configures a pin as an input. + Input = PinMode(0) + // Output configures a pin as an output. + Output = PinMode(1) + + // Direction is the bit mask of the pin mode representing + // the I/O direction. + Direction = PinMode(1) + + // Pullup can be bitwise-or'd with Input + // to cause the pull-up resistor on the pin to + // be enabled. + Pullup = PinMode(2) + + // Invert can be bitwise-or'd with Input to + // cause the pin value to reflect the inverted + // value on the pin. + Invert = PinMode(4) +) + +// ErrInvalidHWAddress is returned when the hardware address +// of the device is not valid (only some bits can be set by the +// address pins). +var ErrInvalidHWAddress = errors.New("invalid hardware address") + +// I2C represents an I2C bus. It is notably implemented by the +// machine.I2C type. +type I2C interface { + ReadRegister(addr uint8, r uint8, buf []byte) error + WriteRegister(addr uint8, r uint8, buf []byte) error +} + +// New returns a new MCP23017 device at the given I2C address +// on the given bus. +// It returns ErrInvalidHWAddress if the address isn't possible for the device. +// +// By default all pins are configured as inputs. +func NewI2C(bus I2C, address uint8) (*Device, error) { + if address&hwAddressMask != hwAddress { + return nil, ErrInvalidHWAddress + } + d := &Device{ + bus: bus, + addr: address, + } + pins, err := d.GetPins() + if err != nil { + return nil, errors.New("cannot initialize mcp23017 device at " + hex(address) + ": " + err.Error()) + } + d.pins = pins + return d, nil +} + +func hex(x uint8) string { + digits := "0123456789abcdef" + return "0x" + digits[x>>4:x>>4+1] + digits[x&0xf:x&0xf+1] +} + +// Device represents an MCP23017 device. +type Device struct { + // TODO would it be good to have a mutex here so that independent goroutines + // could change pins without needing to do the locking themselves? + + // bus holds the reference the I2C bus that the device lives on. + // It's an interface so that we can write tests for it. + bus I2C + addr uint8 + // pins caches the most recent pin values that have been set. + // This enables us to change individual pin values without + // doing a read followed by a write. + pins Pins +} + +// GetPins reads all 16 pins from ports A and B. +func (d *Device) GetPins() (Pins, error) { + return d.readRegisterAB(rGPIO) +} + +// SetPins sets all the pins for which mask is high +// to their respective values in pins. +// +// That is, it does the equivalent of: +// +// for i := 0; i < PinCount; i++ { +// if mask.Get(i) { +// d.Pin(i).Set(pins.Get(i)) +// } +// } +func (d *Device) SetPins(pins, mask Pins) error { + if mask == 0 { + return nil + } + newPins := (d.pins &^ mask) | (pins & mask) + if newPins == d.pins { + return nil + } + err := d.writeRegisterAB(rGPIO, newPins) + if err != nil { + return err + } + d.pins = newPins + return nil +} + +// Pin returns a Pin representing the given pin number (from 0 to 15). +// Pin numbers from 0 to 7 represent port A pins 0 to 7. +// Pin numbers from 8 to 15 represent port B pins 0 to 7. +func (d *Device) Pin(pin int) Pin { + if pin < 0 || pin >= PinCount { + panic("pin out of range") + } + var mask Pins + mask.High(pin) + return Pin{ + dev: d, + mask: mask, + pin: uint8(pin), + } +} + +// SetAllModes sets the mode of all the pins in a single operation. +// If len(modes) is less than PinCount, all remaining pins +// will be set fo modes[len(modes)-1], or PinMode(0) if +// modes is empty. +// +// If len(modes) is greater than PinCount, the excess entries +// will be ignored. +func (d *Device) SetModes(modes []PinMode) error { + defaultMode := PinMode(0) + if len(modes) > 0 { + defaultMode = modes[len(modes)-1] + } + var dir, pullup, invert Pins + for i := 0; i < PinCount; i++ { + mode := defaultMode + if i < len(modes) { + mode = modes[i] + } + if mode&Direction == Input { + dir.High(i) + } + if mode&Pullup != 0 { + pullup.High(i) + } + if mode&Invert != 0 { + invert.High(i) + } + } + if err := d.writeRegisterAB(rIODIR, dir); err != nil { + return err + } + if err := d.writeRegisterAB(rGPPU, pullup); err != nil { + return err + } + if err := d.writeRegisterAB(rIOPOL, invert); err != nil { + return err + } + return nil +} + +// GetModes reads the modes of all the pins into modes. +// It's OK if len(modes) is not PinCount - excess entries +// will be left unset. +func (d *Device) GetModes(modes []PinMode) error { + dir, err := d.readRegisterAB(rIODIR) + if err != nil { + return err + } + pullup, err := d.readRegisterAB(rGPPU) + if err != nil { + return err + } + invert, err := d.readRegisterAB(rIOPOL) + if err != nil { + return err + } + if len(modes) > PinCount { + modes = modes[:PinCount] + } + for i := range modes { + mode := Output + if dir.Get(i) { + mode = Input + } + if pullup.Get(i) { + mode |= Pullup + } + if invert.Get(i) { + mode |= Invert + } + modes[i] = mode + } + return nil +} + +func (d *Device) writeRegisterAB(r register, val Pins) error { + // We rely on the auto-incrementing sequential write + // and the fact that registers alternate between A and B + // to write both ports in a single operation. + buf := [2]byte{uint8(val), uint8(val >> 8)} + return d.bus.WriteRegister(d.addr, uint8(r&^portB), buf[:]) +} + +func (d *Device) readRegisterAB(r register) (Pins, error) { + // We rely on the auto-incrementing sequential write + // and the fact that registers alternate between A and B + // to read both ports in a single operation. + var buf [2]byte + if err := d.bus.ReadRegister(d.addr, uint8(r), buf[:]); err != nil { + return Pins(0), err + } + return Pins(buf[0]) | (Pins(buf[1]) << 8), nil +} + +// Pin represents a single GPIO pin on the device. +type Pin struct { + // mask holds the mask of the pin. + mask Pins + // pin holds the actual pin number. + pin uint8 + dev *Device +} + +// Set sets the pin to the given value. +func (p Pin) Set(value bool) error { + // TODO currently this always writes both registers when + // technically it only needs to write one. We could potentially + // optimize that. + if value { + return p.dev.SetPins(^Pins(0), p.mask) + } else { + return p.dev.SetPins(0, p.mask) + } +} + +// High is short for p.Set(true). +func (p Pin) High() error { + return p.Set(true) +} + +// High is short for p.Set(false). +func (p Pin) Low() error { + return p.Set(false) +} + +// Get returns the current value of the given pin. +func (p Pin) Get() (bool, error) { + // TODO this reads 2 registers when we could read just one. + pins, err := p.dev.GetPins() + if err != nil { + return false, err + } + return pins&p.mask != 0, nil +} + +// SetMode configures the pin to the given mode. +func (p Pin) SetMode(mode PinMode) error { + // We could use a more efficient single-register + // read/write pattern but setting pin modes isn't an + // operation that's likely to need to be efficient, so + // use less code and use Get/SetModes directly. + modes := make([]PinMode, PinCount) + if err := p.dev.GetModes(modes); err != nil { + return err + } + modes[p.pin] = mode + return p.dev.SetModes(modes) +} + +// GetMode returns the mode of the pin. +func (p Pin) GetMode() (PinMode, error) { + modes := make([]PinMode, PinCount) + if err := p.dev.GetModes(modes); err != nil { + return 0, err + } + return modes[p.pin], nil +} + +// Pins represents a bitmask of pin values. +// Port A values are in bits 0-8 (numbered from least significant bit) +// Port B values are in bits 9-15. +type Pins uint16 + +// Set sets the value for the given pin. +func (p *Pins) Set(pin int, value bool) { + if value { + p.High(pin) + } else { + p.Low(pin) + } +} + +// Get returns the value for the given pin. +func (p Pins) Get(pin int) bool { + return (p & pinMask(pin)) != 0 +} + +// High is short for p.Set(pin, true). +func (p *Pins) High(pin int) { + *p |= pinMask(pin) +} + +// Low is short for p.Set(pin, false). +func (p *Pins) Low(pin int) { + *p &^= pinMask(pin) +} + +func pinMask(pin int) Pins { + return 1 << pin +} diff --git a/mcp23017/device_test.go b/mcp23017/device_test.go new file mode 100644 index 0000000..65f0057 --- /dev/null +++ b/mcp23017/device_test.go @@ -0,0 +1,173 @@ +package mcp23017 + +import ( + "fmt" + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestGetPins(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev := bus.addDevice(0x20) + fdev.Registers[rGPIO] = 0b10101100 + fdev.Registers[rGPIO|portB] = 0b01010011 + dev, err := NewI2C(bus, 0x20) + c.Assert(err, qt.IsNil) + pins, err := dev.GetPins() + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.Equals, Pins(0b01010011_10101100)) +} + +func TestSetPins(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev := bus.addDevice(0x20) + fdev.Registers[rGPIO] = 0b00001111 + fdev.Registers[rGPIO|portB] = 0b11110000 + dev, err := NewI2C(bus, 0x20) + c.Assert(err, qt.IsNil) + pins, err := dev.GetPins() + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.Equals, Pins(0b11110000_00001111)) + + err = dev.SetPins(0b01100000_00110000, 0b10101010_01010101) + c.Assert(err, qt.IsNil) + pins, err = dev.GetPins() + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.Equals, Pins(0b01110000_0001_1010)) + + // The logic uses the cached value of the pins rather than + // reading it from the registers each time. + fdev.Registers[rGPIO] = 0 + fdev.Registers[rGPIO|portB] = 0 + + err = dev.SetPins(0b01000000_00110000, 0b01100000_00000000) + c.Assert(err, qt.IsNil) + pins, err = dev.GetPins() + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.Equals, Pins(0b01010000_00011010)) +} + +func TestSetGetModes(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev := bus.addDevice(0x20) + dev, err := NewI2C(bus, 0x20) + c.Assert(err, qt.IsNil) + // Calling SetModes with less items in than there are + // pins should use the last item for all the unspecified ones. + err = dev.SetModes([]PinMode{Input | Invert, Output}) + c.Assert(err, qt.IsNil) + c.Assert(fdev.Registers[rIODIR], qt.Equals, uint8(0b00000001)) + c.Assert(fdev.Registers[rIOPOL], qt.Equals, uint8(0b00000001)) + c.Assert(fdev.Registers[rGPPU], qt.Equals, uint8(0)) + + modes := make([]PinMode, 17) + err = dev.GetModes(modes) + c.Assert(err, qt.IsNil) + c.Assert(modes[0], qt.Equals, Input|Invert) + for i, m := range modes[1:16] { + c.Assert(m, qt.Equals, Output, qt.Commentf("index %d", i)) + } + c.Assert(modes[16], qt.Equals, PinMode(0)) + + // Using an empty slice should reset all the modes to the initial state. + err = dev.SetModes(nil) + c.Assert(err, qt.IsNil) + c.Assert(fdev.Registers[rIODIR], qt.Equals, uint8(0b11111111)) + c.Assert(fdev.Registers[rIOPOL], qt.Equals, uint8(0)) + c.Assert(fdev.Registers[rGPPU], qt.Equals, uint8(0)) +} + +func TestPinSetGet(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev := bus.addDevice(0x20) + dev, err := NewI2C(bus, 0x20) + c.Assert(err, qt.IsNil) + pin := dev.Pin(1) + v, err := pin.Get() + c.Assert(err, qt.Equals, nil) + c.Assert(v, qt.Equals, false) + err = pin.Set(true) + c.Assert(err, qt.Equals, nil) + c.Assert(fdev.Registers[rGPIO], qt.Equals, uint8(0b10)) + v, err = pin.Get() + c.Assert(err, qt.Equals, nil) + c.Assert(v, qt.Equals, true) + err = pin.Set(false) + c.Assert(err, qt.Equals, nil) + c.Assert(fdev.Registers[rGPIO], qt.Equals, uint8(0)) + err = pin.High() + c.Assert(err, qt.Equals, nil) + c.Assert(fdev.Registers[rGPIO], qt.Equals, uint8(0b10)) + err = pin.Low() + c.Assert(err, qt.Equals, nil) + c.Assert(fdev.Registers[rGPIO], qt.Equals, uint8(0)) +} + +func TestPinMode(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev := bus.addDevice(0x20) + dev, err := NewI2C(bus, 0x20) + c.Assert(err, qt.IsNil) + pin := dev.Pin(1) + mode, err := pin.GetMode() + c.Assert(err, qt.IsNil) + c.Assert(mode, qt.Equals, PinMode(0)) + c.Assert(mode&Direction, qt.Equals, Input) + + err = pin.SetMode(Input | Pullup | Invert) + c.Assert(err, qt.IsNil) + c.Assert(fdev.Registers[rIODIR], qt.Equals, uint8(0b11111111)) + c.Assert(fdev.Registers[rIOPOL], qt.Equals, uint8(0b10)) + c.Assert(fdev.Registers[rGPPU], qt.Equals, uint8(0b10)) + + mode, err = pin.GetMode() + c.Assert(err, qt.IsNil) + c.Assert(mode, qt.Equals, Input|Pullup|Invert) + + // Set another pin to output. + err = dev.Pin(2).SetMode(Output) + c.Assert(err, qt.IsNil) + c.Assert(fdev.Registers[rIODIR], qt.Equals, uint8(0b11111011)) + c.Assert(fdev.Registers[rIOPOL], qt.Equals, uint8(0b10)) + c.Assert(fdev.Registers[rGPPU], qt.Equals, uint8(0b10)) + + // Check that changing a pin in port B works too. + err = dev.Pin(8).SetMode(Output) + c.Assert(err, qt.IsNil) + c.Assert(fdev.Registers[rIODIR], qt.Equals, uint8(0b11111011)) + c.Assert(fdev.Registers[rIODIR|portB], qt.Equals, uint8(0b11111110)) + c.Assert(fdev.Registers[rIOPOL], qt.Equals, uint8(0b10)) + c.Assert(fdev.Registers[rIOPOL|portB], qt.Equals, uint8(0)) + c.Assert(fdev.Registers[rGPPU], qt.Equals, uint8(0b10)) + c.Assert(fdev.Registers[rGPPU|portB], qt.Equals, uint8(0)) +} + +func TestPins(t *testing.T) { + c := qt.New(t) + var p Pins + p.Set(1, true) + c.Assert(p, qt.Equals, Pins(0b10)) + c.Assert(p.Get(1), qt.Equals, true) + c.Assert(p.Get(0), qt.Equals, false) + c.Assert(p.Get(16), qt.Equals, false) + p.High(2) + c.Assert(p, qt.Equals, Pins(0b110)) + p.Low(1) + c.Assert(p, qt.Equals, Pins(0b100)) +} + +func TestInitWithError(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev := bus.addDevice(0x20) + fdev.Err = fmt.Errorf("some error") + dev, err := NewI2C(bus, 0x20) + c.Assert(err, qt.ErrorMatches, `cannot initialize mcp23017 device at 0x20: some error`) + c.Assert(dev, qt.IsNil) +} diff --git a/mcp23017/fakedev_test.go b/mcp23017/fakedev_test.go new file mode 100644 index 0000000..e987eb7 --- /dev/null +++ b/mcp23017/fakedev_test.go @@ -0,0 +1,97 @@ +package mcp23017 + +import ( + qt "github.com/frankban/quicktest" +) + +// fakeBus implements the I2C interface in memory for testing. +type fakeBus struct { + c *qt.C + devs []*fakeDev +} + +// newBus returns a fakeBus instance that uses c to flag errors +// if they happen. After creating a fakeBus instance, add devices +// to it with addDevice before using the interface. +func newBus(c *qt.C) *fakeBus { + return &fakeBus{ + c: c, + } +} + +// fakeDev represents a device on the bus. +type fakeDev struct { + c *qt.C + addr uint8 + // Registers holds the device registers. It can be inspected + // or changed as desired for testing. + Registers [registerCount]uint8 + // If Err is non-nil, it will be returned as the error from the + // I2C methods. + Err error +} + +// addDevice adds a new device at the given address. +func (bus *fakeBus) addDevice(addr uint8) *fakeDev { + dev := &fakeDev{ + c: bus.c, + addr: addr, + Registers: [registerCount]uint8{ + // IODIRA and IODIRB are all ones by default. + rIODIR: 0xff, + rIODIR | portB: 0xff, + }, + } + bus.devs = append(bus.devs, dev) + return dev +} + +// ReadRegister implements I2C.ReadRegister. +func (bus *fakeBus) ReadRegister(addr uint8, r uint8, buf []byte) error { + return bus.findDev(addr).readRegister(r, buf) +} + +// WriteRegister implements I2C.WriteRegister. +func (bus *fakeBus) WriteRegister(addr uint8, r uint8, buf []byte) error { + return bus.findDev(addr).writeRegister(r, buf) +} + +func (d *fakeDev) readRegister(r uint8, buf []byte) error { + if d.Err != nil { + return d.Err + } + d.assertRegisterRange(r, buf) + copy(buf, d.Registers[r:]) + return nil +} + +func (d *fakeDev) writeRegister(r uint8, buf []byte) error { + if d.Err != nil { + return d.Err + } + d.assertRegisterRange(r, buf) + copy(d.Registers[r:], buf) + return nil +} + +// assertRegisterRange asserts that reading or writing the given +// register and subsequent registers is in range of the available registers. +func (d *fakeDev) assertRegisterRange(r uint8, buf []byte) { + if int(r) >= len(d.Registers) { + d.c.Fatalf("register read/write [%#x, %#x] start out of range", r, int(r)+len(buf)) + } + if int(r)+len(buf) > len(d.Registers) { + d.c.Fatalf("register read/write [%#x, %#x] end out of range", r, int(r)+len(buf)) + } +} + +// findDev returns the device with the given address. +func (bus *fakeBus) findDev(addr uint8) *fakeDev { + for _, dev := range bus.devs { + if dev.addr == addr { + return dev + } + } + bus.c.Fatalf("invalid device addr %#x passed to i2c bus", addr) + panic("unreachable") +} diff --git a/mcp23017/multidevice.go b/mcp23017/multidevice.go new file mode 100644 index 0000000..da2c730 --- /dev/null +++ b/mcp23017/multidevice.go @@ -0,0 +1,190 @@ +package mcp23017 + +// All is a convenience value that represents all pins high (or all mask bits one). +var All = PinSlice{0xffff} + +// Devices holds a slice of devices that can be treated as one +// contiguous set of devices. Earlier entries in the slice have +// lower-numbered pins, so index 0 holds pins 0-7, index 1 holds +// pins 8-15, etc. +type Devices []*Device + +// NewI2CDevices returns a Devices slice holding the Device values +// for all the given addresses on the given bus. +// When more than one bus is in use, create the slice yourself. +func NewI2CDevices(bus I2C, addrs ...uint8) (Devices, error) { + devs := make(Devices, len(addrs)) + for i, addr := range addrs { + dev, err := NewI2C(bus, addr) + if err != nil { + // TODO return a more informative error. + return nil, err + } + devs[i] = dev + } + return devs, nil +} + +// SetModes sets the pin modes of all the pins on all the devices in devs. +// If there are less entries in modes than there are pins, the +// last entry is replicated to all of them (or PinMode(0) if modes +// is empty). +func (devs Devices) SetModes(modes []PinMode) error { + var defaultModes []PinMode + if len(modes) > 0 { + defaultModes = modes[len(modes)-1:] + } + for i, dev := range devs { + pinStart := i * PinCount + var devModes []PinMode + if pinStart < len(modes) { + devModes = modes[pinStart:] + } else { + devModes = defaultModes + } + if err := dev.SetModes(devModes); err != nil { + return err + } + } + return nil +} + +// GetModes gets the pin modes from the devices. +// It's OK if modes isn't the same length as all the pins: +// extra entries will be left unchanged. +func (devs Devices) GetModes(modes []PinMode) error { + for i, dev := range devs { + pinStart := i * PinCount + if pinStart >= len(modes) { + break + } + if err := dev.GetModes(modes[pinStart:]); err != nil { + return err + } + } + return nil +} + +// Pin returns the pin for the given number. +func (devs Devices) Pin(pin int) Pin { + if pin < 0 || pin >= len(devs)*PinCount { + panic("pin out of range") + } + return devs[pin/PinCount].Pin(pin % PinCount) +} + +// GetPins returns pin values for all the pins. +func (devs Devices) GetPins(pins PinSlice) error { + for i, dev := range devs { + if i >= len(pins) { + break + } + devPins, err := dev.GetPins() + if err != nil { + return err + } + pins[i] = devPins + } + return nil +} + +// SetPins sets all the pins for which mask is high +// to their respective values in pins. +// +// That is, it does the equivalent of: +// +// for i := 0; i < PinCount*len(devs); i++ { +// if mask.Get(i) { +// d.Pin(i).Set(pins.Get(i)) +// } +// } +func (devs Devices) SetPins(pins, mask PinSlice) error { + defaultPins := pins.extra() + defaultMask := mask.extra() + for i, dev := range devs { + devPins := defaultPins + if i < len(pins) { + devPins = pins[i] + } + devMask := defaultMask + if i < len(mask) { + devMask = mask[i] + } + if err := dev.SetPins(devPins, devMask); err != nil { + return err + } + } + return nil +} + +// PinSlice represents an arbitrary nunber of pins, each element corresponding +// to the pins for one device. The value of the highest numbered pin in the +// slice is extended to all other pins beyond the end of the slice. +type PinSlice []Pins + +// Get returns the value for the given pin. If the length of pins is too short +// for the pin number, the value of the highest available pin is returned. +// That is, the highest numbered pin in the last element of pins +// is effectively replicated to all other elements. +// +// This means that PinSlice{} means "all pins high" and +// PinSlice{0xffff} means "all pins low". +func (pins PinSlice) Get(i int) bool { + if len(pins) == 0 || i < 0 { + return false + } + if i >= len(pins)*PinCount { + return pins[len(pins)-1].Get(PinCount - 1) + } + return pins[i/PinCount].Get(i % PinCount) +} + +// Set sets the value for the given pin. +func (pins PinSlice) Set(i int, value bool) { + pins[i/PinCount].Set(i%PinCount, value) +} + +// High is short for p.Set(pin, true). +func (pins PinSlice) High(pin int) { + pins[pin/PinCount].High(pin % PinCount) +} + +// High is short for p.Set(pin, false). +func (pins PinSlice) Low(pin int) { + pins[pin/PinCount].Low(pin % PinCount) +} + +// Ensure checks that pins has enough space to store +// at least length pins. If it does, it returns pins unchanged. +// Otherwise, it returns pins with elements appended as needed, +// populating additonal elements by replicating the +// highest pin (mirroring the behavior of PinSlice.Get). +func (pins PinSlice) Ensure(length int) PinSlice { + if length == 0 { + return pins + } + n := length/PinCount + 1 + if len(pins) >= n { + return pins + } + // TODO we could potentially make use of additional + // extra capacity in pins when available instead + // of allocating a new slice always. + newPins := make(PinSlice, n) + copy(newPins, pins) + if extend := pins.extra(); extend != 0 { + for i := len(pins); i < n; i++ { + newPins[i] = extend + } + } + return newPins +} + +// extra returns the value of implied extra elements beyond +// the end of pins. +func (pins PinSlice) extra() Pins { + if len(pins) == 0 || !pins[len(pins)-1].Get(PinCount-1) { + return 0 + } + return ^Pins(0) +} diff --git a/mcp23017/multidevice_test.go b/mcp23017/multidevice_test.go new file mode 100644 index 0000000..ebe06b9 --- /dev/null +++ b/mcp23017/multidevice_test.go @@ -0,0 +1,166 @@ +package mcp23017 + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestDevicesGetPins(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev0 := bus.addDevice(0x20) + fdev1 := bus.addDevice(0x21) + fdev0.Registers[rGPIO] = 0b10101100 + fdev0.Registers[rGPIO|portB] = 0b01010011 + fdev1.Registers[rGPIO] = 0b10101101 + fdev1.Registers[rGPIO|portB] = 0b01010010 + devs, err := NewI2CDevices(bus, 0x20, 0x21) + c.Assert(err, qt.IsNil) + pins := make(PinSlice, 2) + err = devs.GetPins(pins) + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.DeepEquals, PinSlice{0b01010011_10101100, 0b01010010_10101101}) + + // It's OK to pass less elements than there are devices. + pins = make(PinSlice, 1) + err = devs.GetPins(pins) + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.DeepEquals, PinSlice{0b01010011_10101100}) +} + +func TestDevicesSetPinsAllOff(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev0 := bus.addDevice(0x20) + fdev1 := bus.addDevice(0x21) + fdev0.Registers[rGPIO] = 0b10101100 + fdev0.Registers[rGPIO|portB] = 0b01010011 + fdev1.Registers[rGPIO] = 0b10101101 + fdev1.Registers[rGPIO|portB] = 0b01010010 + devs, err := NewI2CDevices(bus, 0x20, 0x21) + c.Assert(err, qt.IsNil) + + err = devs.SetPins(nil, PinSlice{0xffff}) + c.Assert(err, qt.IsNil) + pins := make(PinSlice, 2) + err = devs.GetPins(pins) + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.DeepEquals, PinSlice{0, 0}) +} + +func TestDevicesSetPinsAllOn(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev0 := bus.addDevice(0x20) + fdev1 := bus.addDevice(0x21) + fdev0.Registers[rGPIO] = 0b10101100 + fdev0.Registers[rGPIO|portB] = 0b01010011 + fdev1.Registers[rGPIO] = 0b10101101 + fdev1.Registers[rGPIO|portB] = 0b01010010 + devs, err := NewI2CDevices(bus, 0x20, 0x21) + c.Assert(err, qt.IsNil) + + err = devs.SetPins(PinSlice{0xffff}, PinSlice{0xffff}) + c.Assert(err, qt.IsNil) + pins := make(PinSlice, 2) + err = devs.GetPins(pins) + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.DeepEquals, PinSlice{0xffff, 0xffff}) +} + +func TestDevicesSetPinsMask(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev0 := bus.addDevice(0x20) + fdev1 := bus.addDevice(0x21) + fdev0.Registers[rGPIO] = 0b10101100 + fdev0.Registers[rGPIO|portB] = 0b01010011 + fdev1.Registers[rGPIO] = 0b10101101 + fdev1.Registers[rGPIO|portB] = 0b01010010 + devs, err := NewI2CDevices(bus, 0x20, 0x21) + c.Assert(err, qt.IsNil) + + // Sanity check the original value of the pins. + pins := make(PinSlice, 2) + err = devs.GetPins(pins) + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.DeepEquals, PinSlice{0b01010011_10101100, 0b01010010_10101101}) + + pins = make(PinSlice, 2) + pins.High(0) + pins.High(1) + mask := make(PinSlice, 2) + mask.High(0) + mask.High(16) + + err = devs.SetPins(pins, mask) + c.Assert(err, qt.IsNil) + pins = make(PinSlice, 2) + err = devs.GetPins(pins) + c.Assert(err, qt.IsNil) + c.Assert(pins, qt.DeepEquals, PinSlice{0b01010011_10101101, 0b01010010_10101100}) +} + +func TestDevicesSetGetModes(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + fdev0 := bus.addDevice(0x20) + fdev1 := bus.addDevice(0x21) + devs, err := NewI2CDevices(bus, 0x20, 0x21) + c.Assert(err, qt.IsNil) + // Sanity check that IODIR registers start off all ones. + c.Assert(fdev0.Registers[rIODIR], qt.Equals, uint8(0xff)) + + // The last entry is replicated to fill them all. + err = devs.SetModes([]PinMode{Input | Pullup, Output}) + c.Assert(err, qt.IsNil) + c.Assert(fdev0.Registers[rIODIR], qt.Equals, uint8(1)) + c.Assert(fdev0.Registers[rIODIR|portB], qt.Equals, uint8(0)) + c.Assert(fdev1.Registers[rIODIR], qt.Equals, uint8(0)) + c.Assert(fdev1.Registers[rIODIR|portB], qt.Equals, uint8(0)) + + modes := make([]PinMode, 2) + err = devs.GetModes(modes) + c.Assert(err, qt.Equals, nil) + c.Assert(modes, qt.DeepEquals, []PinMode{Input | Pullup, Output}) + + // It's OK to pass a smaller slice to GetModes. + modes = make([]PinMode, 1) + err = devs.GetModes(modes) + c.Assert(err, qt.Equals, nil) + c.Assert(modes, qt.DeepEquals, []PinMode{Input | Pullup}) +} + +func TestDevicesPin(t *testing.T) { + c := qt.New(t) + bus := newBus(c) + bus.addDevice(0x20) + fdev1 := bus.addDevice(0x21) + devs, err := NewI2CDevices(bus, 0x20, 0x21) + c.Assert(err, qt.IsNil) + pin := devs.Pin(16) + v, err := pin.Get() + c.Assert(err, qt.Equals, nil) + c.Assert(v, qt.Equals, false) + err = pin.High() + c.Assert(err, qt.Equals, nil) + c.Assert(fdev1.Registers[rGPIO], qt.Equals, uint8(1)) +} + +func TestPinSlice(t *testing.T) { + c := qt.New(t) + pins := PinSlice(nil).Ensure(20) + pins.Set(16, true) + c.Assert(pins, qt.DeepEquals, PinSlice{0, 1}) + pins.Set(31, true) + c.Assert(pins, qt.DeepEquals, PinSlice{0, 0b10000000_00000001}) + c.Assert(pins.Get(0), qt.Equals, false) + c.Assert(pins.Get(16), qt.Equals, true) + pins = pins.Ensure(40) + c.Assert(pins, qt.DeepEquals, PinSlice{0, 0b10000000_00000001, 0xffff}) + pins.Low(16) + c.Assert(pins.Get(16), qt.Equals, false) + pins.High(16) + c.Assert(pins.Get(16), qt.Equals, true) +}