From dffddf2e7e4da105378093b735edb20499f81909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Grill?= Date: Tue, 3 Mar 2020 14:29:37 +0100 Subject: [PATCH] mcp23xxx: support for Microchip MCP23 family of IO extenders (#433) Initial code for MCP23xxx support. --- experimental/devices/mcp23xxx/doc.go | 6 + experimental/devices/mcp23xxx/example_test.go | 50 ++++ .../devices/mcp23xxx/mcp23017_test.go | 176 ++++++++++++++ experimental/devices/mcp23xxx/mcp23xxx.go | 220 ++++++++++++++++++ experimental/devices/mcp23xxx/pins.go | 197 ++++++++++++++++ experimental/devices/mcp23xxx/registers.go | 111 +++++++++ 6 files changed, 760 insertions(+) create mode 100644 experimental/devices/mcp23xxx/doc.go create mode 100644 experimental/devices/mcp23xxx/example_test.go create mode 100644 experimental/devices/mcp23xxx/mcp23017_test.go create mode 100644 experimental/devices/mcp23xxx/mcp23xxx.go create mode 100644 experimental/devices/mcp23xxx/pins.go create mode 100644 experimental/devices/mcp23xxx/registers.go diff --git a/experimental/devices/mcp23xxx/doc.go b/experimental/devices/mcp23xxx/doc.go new file mode 100644 index 0000000..3f9b6ba --- /dev/null +++ b/experimental/devices/mcp23xxx/doc.go @@ -0,0 +1,6 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +// Package mcp23xxx provides driver for the MCP23 family of IO extenders +package mcp23xxx diff --git a/experimental/devices/mcp23xxx/example_test.go b/experimental/devices/mcp23xxx/example_test.go new file mode 100644 index 0000000..85041c0 --- /dev/null +++ b/experimental/devices/mcp23xxx/example_test.go @@ -0,0 +1,50 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp23xxx_test + +import ( + "fmt" + "log" + + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/experimental/devices/mcp23xxx" + "periph.io/x/periph/host" +) + +func Example() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Open default I²C bus. + bus, err := i2creg.Open("") + if err != nil { + log.Fatalf("failed to open I²C: %v", err) + } + defer bus.Close() + + // Create a new I2C IO extender + extender, err := mcp23xxx.NewI2C(bus, mcp23xxx.MCP23017, 0x20) + if err != nil { + log.Fatalln(err) + } + + for _, port := range extender.Pins { + for _, pin := range port { + err = pin.In(gpio.Float, gpio.NoEdge) + if err != nil { + log.Fatalln(err) + } + level := pin.Read() + fmt.Printf("%s\t%s\n", pin.Name(), level.String()) + } + } + + if err != nil { + log.Fatalln(err) + } +} diff --git a/experimental/devices/mcp23xxx/mcp23017_test.go b/experimental/devices/mcp23xxx/mcp23017_test.go new file mode 100644 index 0000000..c8096f6 --- /dev/null +++ b/experimental/devices/mcp23xxx/mcp23017_test.go @@ -0,0 +1,176 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp23xxx + +import ( + "testing" + + "periph.io/x/periph/conn/conntest" + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/gpio/gpioreg" + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/spi" + "periph.io/x/periph/conn/spi/spitest" +) + +func TestMCP23017_out(t *testing.T) { + const address uint16 = 0x20 + scenario := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + // iodir is read on creation + {Addr: address, W: []byte{0x00}, R: []byte{0xFF}}, + {Addr: address, W: []byte{0x01}, R: []byte{0xFF}}, + // iodira is set to output + {Addr: address, W: []byte{0x00, 0xFE}, R: nil}, + // olata is read + {Addr: address, W: []byte{0x14}, R: []byte{0x00}}, + // writing back unchanged value is omitted + // writing high output + {Addr: address, W: []byte{0x14, 0x01}, R: nil}, + }, + } + + dev, err := NewI2C(scenario, MCP23017, address) + if err != nil { + t.Fatal(err) + } + defer dev.Close() + + pA0 := gpioreg.ByName("MCP23017_20_PORTA_0") + pA0.Out(gpio.Low) + pA0.Out(gpio.High) +} + +func TestMCP23S17_out(t *testing.T) { + const address uint16 = 0x20 + scenario := &spitest.Playback{ + Playback: conntest.Playback{ + Ops: []conntest.IO{ + // iodira is read + {W: []byte{0x41, 0x00}, R: []byte{0xFF}}, + {W: []byte{0x41, 0x01}, R: []byte{0xFF}}, + // iodira is set to output + {W: []byte{0x40, 0x00, 0xFE}, R: nil}, + // olata is read + {W: []byte{0x41, 0x14}, R: []byte{0x00}}, + // writing back unchanged value is omitted + // writing high output + {W: []byte{0x40, 0x14, 0x01}, R: nil}, + }, + }, + } + + conn, err := scenario.Connect(1, spi.Mode0, 8) + if err != nil { + t.Fatal(err) + } + dev, err := NewSPI(conn, MCP23S17) + if err != nil { + t.Fatal(err) + } + defer dev.Close() + + pA0 := gpioreg.ByName("MCP23S17_PORTA_0") + + pA0.Out(gpio.Low) + pA0.Out(gpio.High) +} + +func TestMCP23017_in(t *testing.T) { + const address uint16 = 0x20 + scenario := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + // iodir is read on creation + {Addr: address, W: []byte{0x00}, R: []byte{0xFF}}, + {Addr: address, W: []byte{0x01}, R: []byte{0xFF}}, + // not written, since it didn't change + // gppua is read + {Addr: address, W: []byte{0x0C}, R: []byte{0x00}}, + // not written, since it didn't change + // gpio is read + {Addr: address, W: []byte{0x12}, R: []byte{0x01}}, + }, + } + + dev, err := NewI2C(scenario, MCP23017, address) + if err != nil { + t.Fatal(err) + } + defer dev.Close() + + pA0 := gpioreg.ByName("MCP23017_20_PORTA_0") + + pA0.In(gpio.Float, gpio.NoEdge) + l := pA0.Read() + if l != gpio.High { + t.Errorf("Input should be High") + } +} + +func TestMCP23017_inInverted(t *testing.T) { + const address uint16 = 0x20 + scenario := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + // iodir is read on creation + {Addr: address, W: []byte{0x00}, R: []byte{0xFF}}, + {Addr: address, W: []byte{0x01}, R: []byte{0xFF}}, + // not written, since it didn't change + // gppua is read + {Addr: address, W: []byte{0x0C}, R: []byte{0x00}}, + // not written, since it didn't change + // polarity is set + {Addr: address, W: []byte{0x02}, R: []byte{0x01}}, + // gpio is read + {Addr: address, W: []byte{0x12}, R: []byte{0x01}}, + }, + } + + dev, err := NewI2C(scenario, MCP23017, address) + if err != nil { + t.Fatal(err) + } + defer dev.Close() + + pA0 := gpioreg.ByName("MCP23017_20_PORTA_0").(Pin) + + pA0.In(gpio.Float, gpio.NoEdge) + pA0.SetPolarityInverted(true) + l := pA0.Read() + if l != gpio.High { + t.Errorf("Input should be High") + } +} + +func TestMCP23017_inPullUp(t *testing.T) { + const address uint16 = 0x20 + scenario := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + // iodir is read on creation + {Addr: address, W: []byte{0x00}, R: []byte{0xFF}}, + {Addr: address, W: []byte{0x01}, R: []byte{0xFF}}, + // not written, since it didn't change + // gppua is read and written + {Addr: address, W: []byte{0x0C}, R: []byte{0x00}}, + {Addr: address, W: []byte{0x0C, 0x01}, R: nil}, + // not written, since it didn't change + // gpio is read + {Addr: address, W: []byte{0x12}, R: []byte{0x01}}, + }, + } + + dev, err := NewI2C(scenario, MCP23017, address) + if err != nil { + t.Fatal(err) + } + defer dev.Close() + + pA0 := gpioreg.ByName("MCP23017_20_PORTA_0") + + pA0.In(gpio.PullUp, gpio.NoEdge) + l := pA0.Read() + if l != gpio.High { + t.Errorf("Input should be High") + } +} diff --git a/experimental/devices/mcp23xxx/mcp23xxx.go b/experimental/devices/mcp23xxx/mcp23xxx.go new file mode 100644 index 0000000..5a75117 --- /dev/null +++ b/experimental/devices/mcp23xxx/mcp23xxx.go @@ -0,0 +1,220 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp23xxx + +import ( + "fmt" + "strconv" + + "periph.io/x/periph/conn/gpio/gpioreg" + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/spi" +) + +// Dev his a handle for a configured MCP23xxx device. +type Dev struct { + // Pins provide access to extender pins. + Pins [][]Pin +} + +// Variant is the type denoting a specific variant of the family. +type Variant string + +const ( + // MCP23008 8-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23008 + MCP23008 Variant = "MCP23008" + + // MCP23S08 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S08 + MCP23S08 Variant = "MCP23S08" + + // MCP23009 8-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23009 + MCP23009 Variant = "MCP23009" + + // MCP23S09 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S09 + MCP23S09 Variant = "MCP23S09" + + // MCP23016 16-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23016 + MCP23016 Variant = "MCP23016" + + // MCP23017 8-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23017 + MCP23017 Variant = "MCP23017" + + // MCP23S17 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S17 + MCP23S17 Variant = "MCP23S17" + + // MCP23018 8-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23018 + MCP23018 Variant = "MCP23018" + + // MCP23S18 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S18 + MCP23S18 Variant = "MCP23S18" +) + +// NewI2C initializes an IO extender through I2C connection. +func NewI2C(b i2c.Bus, variant Variant, addr uint16) (*Dev, error) { + if addr&0xFFF8 != 0x20 { + return nil, fmt.Errorf("%s: Supported address range is 0x20 - 0x27", variant) + } + devicename := string(variant) + "_" + strconv.FormatInt(int64(addr), 16) + ra := &i2cRegisterAccess{ + Dev: &i2c.Dev{Bus: b, Addr: addr}, + } + return makeDev(ra, variant, devicename) +} + +// NewSPI initializes an IO extender through I2C connection. +func NewSPI(b spi.Conn, variant Variant) (*Dev, error) { + devicename := string(variant) + ra := &spiRegisterAccess{ + Conn: b, + } + return makeDev(ra, variant, devicename) +} + +// Close removes any registration to the device. +func (d *Dev) Close() error { + for _, port := range d.Pins { + for _, pin := range port { + err := gpioreg.Unregister(pin.Name()) + if err != nil { + return err + } + } + } + return nil +} + +func makeDev(ra registerAccess, variant Variant, devicename string) (*Dev, error) { + var ports []port + switch variant { + case MCP23008, MCP23009, MCP23S08, MCP23S09: + ports = mcp23x089port(devicename, ra) + case MCP23016: + ports = mcp23x16ports(devicename, ra) + case MCP23017, MCP23S17, MCP23018, MCP23S18: + ports = mcp23x178ports(devicename, ra) + default: + return nil, fmt.Errorf("%s: Unsupported variant", devicename) + } + + pins := make([][]Pin, len(ports)) + for i := range ports { + // pre-cache iodir + _, err := ports[i].iodir.readValue(false) + if err != nil { + return nil, err + } + pins[i] = ports[i].pins() + for _, pin := range pins[i] { + gpioreg.Register(pin) + } + } + return &Dev{ + Pins: pins, + }, nil +} + +func mcp23x178ports(devicename string, ra registerAccess) []port { + return []port{{ + name: devicename + "_PORTA", + // GPIO basic registers + iodir: ra.define(0x00), + gpio: ra.define(0x12), + olat: ra.define(0x14), + + // polarity setting + ipol: ra.define(0x02), + + // pull-up control register + gppu: ra.define(0x0C), + supportPullup: true, + + // interrupt handling registers + gpinten: ra.define(0x04), + intcon: ra.define(0x08), + intf: ra.define(0x0E), + intcap: ra.define(0x10), + supportInterrupt: true, + }, { + name: devicename + "_PORTB", + // GPIO basic registers + iodir: ra.define(0x01), + gpio: ra.define(0x13), + olat: ra.define(0x15), + + // polarity setting + ipol: ra.define(0x03), + supportPullup: true, + + // pull-up control register + gppu: ra.define(0x0D), + + // interrupt handling registers + gpinten: ra.define(0x05), + intcon: ra.define(0x0B), + intf: ra.define(0x0F), + intcap: ra.define(0x11), + supportInterrupt: true, + }} +} + +func mcp23x089port(devicename string, ra registerAccess) []port { + return []port{{ + name: devicename, + // GPIO basic registers + iodir: ra.define(0x00), + gpio: ra.define(0x09), + olat: ra.define(0x0A), + + // polarity setting + ipol: ra.define(0x01), + + // pull-up control register + gppu: ra.define(0x06), + supportPullup: true, + + // interrupt handling registers + gpinten: ra.define(0x02), + intcon: ra.define(0x04), + intf: ra.define(0x07), + intcap: ra.define(0x08), + supportInterrupt: true, + }} +} + +func mcp23x16ports(devicename string, ra registerAccess) []port { + return []port{{ + name: devicename + "_PORT0", + // GPIO basic registers + iodir: ra.define(0x06), + gpio: ra.define(0x00), + olat: ra.define(0x02), + + // polarity setting + ipol: ra.define(0x04), + + // pull-up control register + supportPullup: false, + + // interrupt handling registers + supportInterrupt: false, + intcap: ra.define(0x08), + }, { + name: devicename + "_PORT1", + // GPIO basic registers + iodir: ra.define(0x07), + gpio: ra.define(0x01), + olat: ra.define(0x03), + + // polarity setting + ipol: ra.define(0x05), + + // pull-up control register + supportPullup: false, + + // interrupt handling registers + supportInterrupt: false, + intcap: ra.define(0x09), + }} +} diff --git a/experimental/devices/mcp23xxx/pins.go b/experimental/devices/mcp23xxx/pins.go new file mode 100644 index 0000000..92f213b --- /dev/null +++ b/experimental/devices/mcp23xxx/pins.go @@ -0,0 +1,197 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp23xxx + +import ( + "errors" + "strconv" + "time" + + "periph.io/x/periph/conn/gpio" + "periph.io/x/periph/conn/physic" + "periph.io/x/periph/conn/pin" +) + +// Pin extends gpio.PinIO interface with features supported by MCP23xxx devices. +type Pin interface { + gpio.PinIO + // SetPolarityInverted if set to true, GPIO register bit reflects the same logic state of the input pin. + SetPolarityInverted(p bool) error + // IsPolarityInverted returns true if the value of the input pin reflects inverted logic state. + IsPolarityInverted() (bool, error) +} + +type port struct { + name string + + // GPIO basic registers + iodir registerCache + gpio registerCache + olat registerCache + + // polarity setting + ipol registerCache + + // pull-up control register + // Not present in all devices + gppu registerCache + supportPullup bool + + // interrupt handling registers + supportInterrupt bool + gpinten registerCache + intcon registerCache + intf registerCache + intcap registerCache +} + +type portpin struct { + port *port + pinbit uint8 +} + +func (p *port) pins() []Pin { + result := make([]Pin, 8) + var i uint8 + for i = 0; i < 8; i++ { + result[i] = &portpin{ + port: p, + pinbit: i, + } + } + return result +} + +func (p *portpin) String() string { + return p.Name() +} + +func (p *portpin) Halt() error { + // To halt all drive, set to high-impedance input + return p.In(gpio.Float, gpio.NoEdge) +} + +func (p *portpin) Name() string { + return p.port.name + "_" + strconv.Itoa(int(p.pinbit)) +} + +func (p *portpin) Number() int { + return int(p.pinbit) +} + +func (p *portpin) Function() string { + return string(p.Func()) +} + +func (p *portpin) In(pull gpio.Pull, edge gpio.Edge) error { + // Set pin to input + err := p.port.iodir.getAndSetBit(p.pinbit, true, true) + if err != nil { + return err + } + // Set pullup + switch pull { + case gpio.PullNoChange: + // don't check, don't change + case gpio.PullDown: + // pull down is not supported by any device + return errors.New("MCP23xxx: PullDown is not supported") + case gpio.PullUp: + if !p.port.supportPullup { + return errors.New("MCP23xxx: PullUp is not supported by this device") + } + err = p.port.gppu.getAndSetBit(p.pinbit, true, true) + if err != nil { + return err + } + case gpio.Float: + if p.port.supportPullup { + err = p.port.gppu.getAndSetBit(p.pinbit, false, true) + if err != nil { + return err + } + } + } + // check edge detection + // TODO interrupt support + return nil +} + +func (p *portpin) Read() gpio.Level { + v, _ := p.port.gpio.getBit(p.pinbit, false) + if v { + return gpio.High + } + return gpio.Low +} + +func (p *portpin) WaitForEdge(timeout time.Duration) bool { + // TODO interrupt handling + return false +} + +func (p *portpin) Pull() gpio.Pull { + if !p.port.supportPullup { + return gpio.Float + } + v, err := p.port.gppu.getBit(p.pinbit, true) + if err != nil { + return gpio.PullNoChange + } + if v { + return gpio.PullUp + } + return gpio.Float +} + +func (p *portpin) DefaultPull() gpio.Pull { + return gpio.Float +} + +func (p *portpin) Out(l gpio.Level) error { + err := p.port.iodir.getAndSetBit(p.pinbit, false, true) + if err != nil { + return err + } + return p.port.olat.getAndSetBit(p.pinbit, l == gpio.High, true) +} + +func (p *portpin) PWM(duty gpio.Duty, f physic.Frequency) error { + return errors.New("MCP23xxx: PWM is not supported") +} + +func (p *portpin) Func() pin.Func { + v, _ := p.port.iodir.getBit(p.pinbit, true) + if v { + return gpio.IN + } + return gpio.OUT +} + +func (p *portpin) SupportedFuncs() []pin.Func { + return supportedFuncs[:] +} + +func (p *portpin) SetFunc(f pin.Func) error { + var v bool + switch f { + case gpio.IN: + v = true + case gpio.OUT: + v = false + default: + return errors.New("MCP23xxx: Function not supported: " + string(f)) + } + return p.port.iodir.getAndSetBit(p.pinbit, v, true) +} + +func (p *portpin) SetPolarityInverted(pol bool) error { + return p.port.ipol.getAndSetBit(p.pinbit, pol, true) +} +func (p *portpin) IsPolarityInverted() (bool, error) { + return p.port.ipol.getBit(p.pinbit, true) +} + +var supportedFuncs = [...]pin.Func{gpio.IN, gpio.OUT} diff --git a/experimental/devices/mcp23xxx/registers.go b/experimental/devices/mcp23xxx/registers.go new file mode 100644 index 0000000..9195616 --- /dev/null +++ b/experimental/devices/mcp23xxx/registers.go @@ -0,0 +1,111 @@ +// Copyright 2020 The Periph Authors. All rights reserved. +// Use of this source code is governed under the Apache License, Version 2.0 +// that can be found in the LICENSE file. + +package mcp23xxx + +import ( + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/spi" +) + +type registerAccess interface { + define(address uint8) registerCache + readRegister(address uint8) (uint8, error) + writeRegister(address uint8, value uint8) error +} + +type i2cRegisterAccess struct { + *i2c.Dev +} + +func (ra *i2cRegisterAccess) readRegister(address uint8) (uint8, error) { + r := make([]byte, 1) + err := ra.Tx([]byte{address}, r) + return r[0], err +} + +func (ra *i2cRegisterAccess) writeRegister(address uint8, value uint8) error { + return ra.Tx([]byte{address, value}, nil) +} + +func (ra *i2cRegisterAccess) define(address uint8) registerCache { + return newRegister(ra, address) +} + +type spiRegisterAccess struct { + spi.Conn +} + +func (ra *spiRegisterAccess) readRegister(address uint8) (uint8, error) { + r := make([]byte, 1) + err := ra.Tx([]byte{0x41, address}, r) + return r[0], err +} + +func (ra *spiRegisterAccess) writeRegister(address uint8, value uint8) error { + return ra.Tx([]byte{0x40, address, value}, nil) +} + +func (ra *spiRegisterAccess) define(address uint8) registerCache { + return newRegister(ra, address) +} + +type registerCache struct { + registerAccess + address uint8 + got bool + cache uint8 +} + +func newRegister(ra registerAccess, address uint8) registerCache { + return registerCache{ + registerAccess: ra, + address: address, + got: false, + } +} + +func (r *registerCache) readValue(cached bool) (uint8, error) { + if cached && r.got { + return r.cache, nil + } + v, err := r.readRegister(r.address) + if err == nil { + r.got = true + r.cache = v + } + return v, err +} + +func (r *registerCache) writeValue(value uint8, cached bool) error { + if cached && r.got && value == r.cache { + return nil + } + + err := r.writeRegister(r.address, value) + if err != nil { + return err + } + r.got = true + r.cache = value + return nil +} + +func (r *registerCache) getAndSetBit(bit uint8, value bool, cached bool) error { + v, err := r.readValue(cached) + if err != nil { + return err + } + if value { + v |= 1 << bit + } else { + v &= ^(1 << bit) + } + return r.writeValue(v, cached) +} + +func (r *registerCache) getBit(bit uint8, cached bool) (bool, error) { + v, err := r.readValue(cached) + return 0 != (v & (1 << bit)), err +}