Add GPIO Group support to mcp23xxx

pull/92/head
George Sexton 1 year ago
parent cad22759dd
commit 743985e052

@ -2,7 +2,9 @@
// Use of this source code is governed under the Apache License, Version 2.0 // Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file. // that can be found in the LICENSE file.
// Package mcp23xxx provides driver for the MCP23 family of IO extenders // Package mcp23xxx provides drivers for the MCP23XXX family of GPIO expanders.
// It's availble with either I2C or SPI interfaces in 8 and 16 bit variants.
// Additionally, variants are available that have Open-Drain outputs.
// //
// # Datasheet // # Datasheet
// //

@ -0,0 +1,214 @@
// Copyright 2025 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"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/pin"
)
// The internal structure for a group of pins.
type pinGroup struct {
dev *Dev
port int
pins []*portpin
defaultMask gpio.GPIOValue
}
// Group returns a gpio.Group that is made up of the specified pins.
func (dev *Dev) Group(port int, pins []int) *gpio.Group {
grouppins := make([]*portpin, len(pins))
for ix, number := range pins {
pp, ok := dev.Pins[port][number].(*portpin)
if !ok {
return nil
}
grouppins[ix] = pp
}
defMask := gpio.GPIOValue((1 << len(pins)) - 1)
var pgif interface{} = &pinGroup{dev: dev, port: port, pins: grouppins, defaultMask: defMask}
if gpiogroup, ok := pgif.(gpio.Group); ok {
return &gpiogroup
}
return nil
}
// Pins returns the set of pin.Pin that make up that group.
func (pg *pinGroup) Pins() []pin.Pin {
pins := make([]pin.Pin, len(pg.pins))
for ix, p := range pg.pins {
pins[ix] = p
}
return pins
}
// Given the offset within the group, return the corresponding GPIO pin.
func (pg *pinGroup) ByOffset(offset int) pin.Pin {
return pg.pins[offset]
}
// Given the specific name of a pin, return it. If it can't be found, nil is
// returned.
func (pg *pinGroup) ByName(name string) pin.Pin {
for _, pin := range pg.pins {
if pin.Name() == name {
return pin
}
}
return nil
}
// Given the GPIO pin number, return that pin from the set.
func (pg *pinGroup) ByNumber(number int) pin.Pin {
for _, pin := range pg.pins {
if pin.Number() == number {
return pin
}
}
return nil
}
// Out writes value to the specified pins of the device/port. If mask is 0,
// the default mask of all pins in the group is used.
func (pg *pinGroup) Out(value, mask gpio.GPIOValue) error {
if mask == 0 {
mask = pg.defaultMask
} else {
mask &= pg.defaultMask
}
value &= mask
// Convert the write value which is relative to the pins to the
// absolute value for the port.
wr := uint8(0)
wrMask := uint8(0)
for bit := range len(pg.pins) {
if (mask & (1 << bit)) > 0 {
if (value & 0x01) == 0x01 {
wr |= 1 << pg.pins[bit].Number()
}
wrMask |= 1 << pg.pins[bit].Number()
}
value = value >> 1
}
port := pg.pins[0].port
// Verify pins are set for output
outputPins, err := port.iodir.readValue(true)
if err != nil {
return err
}
if ((outputPins ^ 0xff) & wrMask) != wrMask {
outputPins &= (wrMask ^ 0xff)
err = port.iodir.writeValue(outputPins, false)
if err != nil {
return err
}
}
// Read the current value
currentValue, err := port.olat.readValue(true)
// Apply the mask to clear bits we're writing.
currentValue &= (0xff ^ wrMask)
// Or the value with the bits to modify
currentValue |= wr
// And, write the value out the port.
return port.olat.writeValue(currentValue, true)
}
// Read reads from the device and port and returns the state of the GPIO
// pins in the group. If a pin specified by mask is not configured for
// input, it is transparently re-configured.
func (pg *pinGroup) Read(mask gpio.GPIOValue) (result gpio.GPIOValue, err error) {
if mask == 0 {
mask = pg.defaultMask
} else {
mask &= pg.defaultMask
}
// Compute the read mask
rmask := uint8(0)
for bit := range 8 {
if (mask & (1 << bit)) > 0 {
rmask |= (1 << pg.pins[bit].Number())
}
}
// Make sure the direction for the pins involved in this write read is
// Input.
port := pg.pins[0].port
currentIn, err := port.iodir.readValue(true)
if err != nil {
return
}
// We need to make some pins Input. Write the value to the iodir register.
if (currentIn & rmask) != rmask {
err = port.iodir.writeValue(currentIn|rmask, false)
if err != nil {
return
}
}
// Now, perform the read itself.
v, err := port.gpio.readValue(false)
if err != nil {
return
}
// Now convert the set pins into the Group value
for ix, pin := range pg.pins {
if (v & (1 << pin.Number())) > 0 {
result |= 1 << ix
}
}
return
}
// WaitForEdge listens for a GPIO pin change event. The MCP23XXXX devices
// can't directly signal an edge event. To do this, you must call
// Dev.SetEdgePin() with a HOST GPIO pin configured for falling edge
// detection. That pin should be connected to the MCP23XXX INT pin. When
// a falling edge is detected on the supplied host GPIO pin, the code
// will return the GPIO Pin number on the device that changed.
//
// Note that the MCP23XXX devices only detect change. You can't configure
// falling or rising edge. Consequently, the returned edge will always be
// gpio.NoEdge.
//
// For a change event to be detected, the pin must be configured for input.
// This function will NOT set pins for input. Additionally, the calling
// code must set the INTCON register appropriately. Refer to the datasheet.
//
// In the event that the changed pin is NOT part of the io group, the
// triggering pin number will be returned, along with the error
// ErrPinNotInGroup
func (pg *pinGroup) WaitForEdge(timeout time.Duration) (number int, edge gpio.Edge, err error) {
return -1, gpio.NoEdge, gpio.ErrGroupFeatureNotImplemented
}
// Halt() interrupts a pending WaitForEdge() call if one is in process.
func (pg *pinGroup) Halt() error {
if pg.dev.edgePin != nil {
var ifpin interface{} = pg.dev.edgePin
if r, ok := ifpin.(conn.Resource); ok {
return r.Halt()
}
}
// TODO: I think we want to call Dev.Halt()
return nil
}
// String returns the device variant name and configured pins for the group.
func (pg *pinGroup) String() string {
s := fmt.Sprintf("%s - [ ", pg.dev)
for ix := range len(pg.pins) {
s += fmt.Sprintf("%d ", pg.pins[ix].Number())
}
s += "]"
return s
}
var _ gpio.Group = &pinGroup{}

@ -0,0 +1,132 @@
// Copyright 2025 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 (
"log"
"testing"
"time"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/host/v3"
)
func init() {
// Make sure periph is initialized.
if _, err := host.Init(); err != nil {
log.Fatal(err)
}
}
func TestGroup(t *testing.T) {
// Open default I²C bus.
bus, err := i2creg.Open("")
if err != nil {
log.Fatalf("failed to open I²C: %v", err)
}
defer bus.Close()
extender, err := NewI2C(bus, MCP23008, 0x20)
if err != nil {
t.Fatal(err)
}
for portnum, port := range extender.Pins {
for _, pin := range port {
t.Logf("Port: %d Pin: %d %s", portnum, pin.Number(), pin)
}
}
// My Test fixture has an led on pin 0, and also wires pin 0 -> pin 4.
// If I don't set pin 4 for input, then it breaks the LED blinking.
reader, _ := interface{}(extender.Pins[0][4]).(gpio.PinIn)
_ = reader.In(gpio.PullNoChange, gpio.NoEdge)
var ifpin interface{} = extender.Pins[0][0]
if writer, ok := ifpin.(gpio.PinOut); ok {
l := gpio.High
for range 20 {
writer.Out(l)
l = !l
time.Sleep(500 * time.Millisecond)
}
} else {
t.Error("pin[0] not converted to gpio.PinOut!")
}
}
func TestReadWrite(t *testing.T) {
// Open default I²C bus.
bus, err := i2creg.Open("")
if err != nil {
log.Fatalf("failed to open I²C: %v", err)
}
defer bus.Close()
extender, err := NewI2C(bus, MCP23008, 0x20)
if err != nil {
t.Fatal(err)
}
defMask := gpio.GPIOValue(0xf)
gOut := *extender.Group(0, []int{0, 1, 2, 3})
if gOut == nil {
t.Error("gOut is nil!")
}
gRead := *extender.Group(0, []int{4, 5, 6, 7})
if gRead == nil {
t.Error("gIn is nil!")
}
// Turn off the GPIOs
defer gOut.Out(0, 0)
defer gRead.Out(0, 0)
for i := range 2 {
if i == 1 {
/* Now invert it. */
x := gRead
gRead = gOut
gOut = x
}
for i := range gpio.GPIOValue(16) {
err := gOut.Out(i, 0)
if err != nil {
t.Error(err)
}
time.Sleep(time.Millisecond)
r, err := gRead.Read(defMask)
if err != nil {
t.Error(err)
}
if r != i {
t.Errorf("Error reading/writing GPIO Group(). Wrote 0x%x Read 0x%x", i, r)
}
}
}
// For this test, write to the pins individually, and then
// confirm read on the other set works as expected.
x := gRead
gRead = gOut
gOut = x
t.Log(gRead)
pinset := extender.Pins[0][:4]
for i := range gpio.GPIOValue(16) {
wvalue := i
for bit := range 4 {
if (wvalue & (1 << bit)) == (1 << bit) {
pinset[bit].Out(gpio.High)
} else {
pinset[bit].Out(gpio.Low)
}
}
r, err := gRead.Read(0)
if err != nil {
t.Error(err)
}
if r != i {
t.Errorf("Error writing GPIO pins and reading back result. Read 0x%x Expected 0x%x", r, i)
}
}
}

@ -8,15 +8,21 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/gpio/gpioreg" "periph.io/x/conn/v3/gpio/gpioreg"
"periph.io/x/conn/v3/i2c" "periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/spi" "periph.io/x/conn/v3/spi"
) )
// Dev his a handle for a configured MCP23xxx device. // Dev is a handle for a configured MCP23xxx device.
type Dev struct { type Dev struct {
// Pins provide access to extender pins. // For all variants, Pins exposes a two dimensional slice of pins. For a
// MCP23X08/X09, [][]Pins would have one row with 8 pins, while the 16
// pin variants would have two rows with 8 pins each.
Pins [][]Pin Pins [][]Pin
edgePin *gpio.PinIn
variant Variant
} }
// Variant is the type denoting a specific variant of the family. // Variant is the type denoting a specific variant of the family.
@ -29,25 +35,25 @@ const (
// MCP23S08 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S08 // MCP23S08 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S08
MCP23S08 Variant = "MCP23S08" MCP23S08 Variant = "MCP23S08"
// MCP23009 8-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23009 // MCP23009 8-bit I2C extender w/ Open-Drain Output. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23009
MCP23009 Variant = "MCP23009" MCP23009 Variant = "MCP23009"
// MCP23S09 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S09 // MCP23S09 8-bit SPI extender w/ Open-Drain Output. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S09
MCP23S09 Variant = "MCP23S09" MCP23S09 Variant = "MCP23S09"
// MCP23016 16-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23016 // MCP23016 16-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23016
MCP23016 Variant = "MCP23016" MCP23016 Variant = "MCP23016"
// MCP23017 8-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23017 // MCP23017 16-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23017
MCP23017 Variant = "MCP23017" MCP23017 Variant = "MCP23017"
// MCP23S17 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S17 // MCP23S17 16-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S17
MCP23S17 Variant = "MCP23S17" MCP23S17 Variant = "MCP23S17"
// MCP23018 8-bit I2C extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23018 // MCP23018 16-bit I2C extender w/ Open-Drain Output. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23018
MCP23018 Variant = "MCP23018" MCP23018 Variant = "MCP23018"
// MCP23S18 8-bit SPI extender. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S18 // MCP23S18 16-bit SPI extender w/ Open-Drain Output. Datasheet: https://www.microchip.com/wwwproducts/en/MCP23S18
MCP23S18 Variant = "MCP23S18" MCP23S18 Variant = "MCP23S18"
) )
@ -112,10 +118,20 @@ func makeDev(ra registerAccess, variant Variant, devicename string) (*Dev, error
} }
} }
return &Dev{ return &Dev{
Pins: pins, Pins: pins,
variant: variant,
}, nil }, nil
} }
// SetEdgePin supplies a configured GPIO pin
func (dev *Dev) SetEdgePin(pin *gpio.PinIn) {
dev.edgePin = pin
}
func (dev *Dev) String() string {
return string(dev.variant)
}
func mcp23x178ports(devicename string, ra registerAccess) []port { func mcp23x178ports(devicename string, ra registerAccess) []port {
return []port{{ return []port{{
name: devicename + "_PORTA", name: devicename + "_PORTA",

Loading…
Cancel
Save