Skip to content

Commit 5bcc204

Browse files
authored
mcp23xxx: Add GPIO Group support (#92)
* Add GPIO Group support to mcp23xxx * Add unit tests for group * Fix unit test * add file * Fix lint errors
1 parent d5aac04 commit 5bcc204

File tree

5 files changed

+525
-10
lines changed

5 files changed

+525
-10
lines changed

mcp23xxx/doc.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Use of this source code is governed under the Apache License, Version 2.0
33
// that can be found in the LICENSE file.
44

5-
// Package mcp23xxx provides driver for the MCP23 family of IO extenders
5+
// Package mcp23xxx provides drivers for the MCP23XXX family of GPIO expanders.
6+
// It's available with either I2C or SPI interfaces in 8 and 16 bit variants.
7+
// Additionally, variants are available that have Open-Drain outputs.
68
//
79
// # Datasheet
810
//

mcp23xxx/group.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2025 The Periph Authors. All rights reserved.
2+
// Use of this source code is governed under the Apache License, Version 2.0
3+
// that can be found in the LICENSE file.
4+
5+
package mcp23xxx
6+
7+
import (
8+
"fmt"
9+
"time"
10+
11+
"periph.io/x/conn/v3"
12+
"periph.io/x/conn/v3/gpio"
13+
"periph.io/x/conn/v3/pin"
14+
)
15+
16+
// The internal structure for a group of pins.
17+
type pinGroup struct {
18+
dev *Dev
19+
port int
20+
pins []*portpin
21+
defaultMask gpio.GPIOValue
22+
}
23+
24+
// Group returns a gpio.Group that is made up of the specified pins.
25+
func (dev *Dev) Group(port int, pins []int) *gpio.Group {
26+
grouppins := make([]*portpin, len(pins))
27+
for ix, number := range pins {
28+
pp, ok := dev.Pins[port][number].(*portpin)
29+
if !ok {
30+
return nil
31+
}
32+
grouppins[ix] = pp
33+
}
34+
defMask := gpio.GPIOValue((1 << len(pins)) - 1)
35+
var pgif interface{} = &pinGroup{dev: dev, port: port, pins: grouppins, defaultMask: defMask}
36+
if gpiogroup, ok := pgif.(gpio.Group); ok {
37+
return &gpiogroup
38+
}
39+
40+
return nil
41+
}
42+
43+
// Pins returns the set of pin.Pin that make up that group.
44+
func (pg *pinGroup) Pins() []pin.Pin {
45+
pins := make([]pin.Pin, len(pg.pins))
46+
47+
for ix, p := range pg.pins {
48+
pins[ix] = p
49+
}
50+
return pins
51+
}
52+
53+
// Given the offset within the group, return the corresponding GPIO pin.
54+
func (pg *pinGroup) ByOffset(offset int) pin.Pin {
55+
return pg.pins[offset]
56+
}
57+
58+
// Given the specific name of a pin, return it. If it can't be found, nil is
59+
// returned.
60+
func (pg *pinGroup) ByName(name string) pin.Pin {
61+
for _, pin := range pg.pins {
62+
if pin.Name() == name {
63+
return pin
64+
}
65+
}
66+
return nil
67+
}
68+
69+
// Given the GPIO pin number, return that pin from the set.
70+
func (pg *pinGroup) ByNumber(number int) pin.Pin {
71+
for _, pin := range pg.pins {
72+
if pin.Number() == number {
73+
return pin
74+
}
75+
}
76+
return nil
77+
}
78+
79+
// Out writes value to the specified pins of the device/port. If mask is 0,
80+
// the default mask of all pins in the group is used.
81+
func (pg *pinGroup) Out(value, mask gpio.GPIOValue) error {
82+
if mask == 0 {
83+
mask = pg.defaultMask
84+
} else {
85+
mask &= pg.defaultMask
86+
}
87+
value &= mask
88+
// Convert the write value which is relative to the pins to the
89+
// absolute value for the port.
90+
wr := uint8(0)
91+
wrMask := uint8(0)
92+
for bit := range len(pg.pins) {
93+
if (mask & (1 << bit)) > 0 {
94+
if (value & 0x01) == 0x01 {
95+
wr |= 1 << pg.pins[bit].Number()
96+
}
97+
wrMask |= 1 << pg.pins[bit].Number()
98+
}
99+
value = value >> 1
100+
}
101+
port := pg.pins[0].port
102+
// Verify pins are set for output
103+
outputPins, err := port.iodir.readValue(true)
104+
if err != nil {
105+
return err
106+
}
107+
108+
if ((outputPins ^ 0xff) & wrMask) != wrMask {
109+
outputPins &= (wrMask ^ 0xff)
110+
err = port.iodir.writeValue(outputPins, false)
111+
if err != nil {
112+
return err
113+
}
114+
}
115+
116+
// Read the current value
117+
currentValue, err := port.olat.readValue(true)
118+
if err != nil {
119+
return err
120+
}
121+
// Apply the mask to clear bits we're writing.
122+
currentValue &= (0xff ^ wrMask)
123+
// Or the value with the bits to modify
124+
currentValue |= wr
125+
// And, write the value out the port.
126+
return port.olat.writeValue(currentValue, true)
127+
}
128+
129+
// Read reads from the device and port and returns the state of the GPIO
130+
// pins in the group. If a pin specified by mask is not configured for
131+
// input, it is transparently re-configured.
132+
func (pg *pinGroup) Read(mask gpio.GPIOValue) (result gpio.GPIOValue, err error) {
133+
if mask == 0 {
134+
mask = pg.defaultMask
135+
} else {
136+
mask &= pg.defaultMask
137+
}
138+
// Compute the read mask
139+
rmask := uint8(0)
140+
for bit := range 8 {
141+
if (mask & (1 << bit)) > 0 {
142+
rmask |= (1 << pg.pins[bit].Number())
143+
}
144+
}
145+
// Make sure the direction for the pins involved in this write read is
146+
// Input.
147+
port := pg.pins[0].port
148+
currentIn, err := port.iodir.readValue(true)
149+
if err != nil {
150+
return
151+
}
152+
// We need to make some pins Input. Write the value to the iodir register.
153+
if (currentIn & rmask) != rmask {
154+
err = port.iodir.writeValue(currentIn|rmask, false)
155+
if err != nil {
156+
return
157+
}
158+
}
159+
// Now, perform the read itself.
160+
v, err := port.gpio.readValue(false)
161+
if err != nil {
162+
return
163+
}
164+
// Now convert the set pins into the Group value
165+
for ix, pin := range pg.pins {
166+
if (v & (1 << pin.Number())) > 0 {
167+
result |= 1 << ix
168+
}
169+
}
170+
return
171+
}
172+
173+
// WaitForEdge listens for a GPIO pin change event. The MCP23XXXX devices
174+
// can't directly signal an edge event. To do this, you must call
175+
// Dev.SetEdgePin() with a HOST GPIO pin configured for falling edge
176+
// detection. That pin should be connected to the MCP23XXX INT pin. When
177+
// a falling edge is detected on the supplied host GPIO pin, the code
178+
// will return the GPIO Pin number on the device that changed.
179+
//
180+
// Note that the MCP23XXX devices only detect change. You can't configure
181+
// falling or rising edge. Consequently, the returned edge will always be
182+
// gpio.NoEdge.
183+
//
184+
// For a change event to be detected, the pin must be configured for input.
185+
// This function will NOT set pins for input. Additionally, the calling
186+
// code must set the INTCON register appropriately. Refer to the datasheet.
187+
//
188+
// In the event that the changed pin is NOT part of the io group, the
189+
// triggering pin number will be returned, along with the error
190+
// ErrPinNotInGroup
191+
func (pg *pinGroup) WaitForEdge(timeout time.Duration) (number int, edge gpio.Edge, err error) {
192+
return -1, gpio.NoEdge, gpio.ErrGroupFeatureNotImplemented
193+
}
194+
195+
// Halt() interrupts a pending WaitForEdge() call if one is in process.
196+
func (pg *pinGroup) Halt() error {
197+
if pg.dev.edgePin != nil {
198+
var ifpin interface{} = pg.dev.edgePin
199+
if r, ok := ifpin.(conn.Resource); ok {
200+
return r.Halt()
201+
}
202+
}
203+
// TODO: I think we want to call Dev.Halt()
204+
return nil
205+
}
206+
207+
// String returns the device variant name and configured pins for the group.
208+
func (pg *pinGroup) String() string {
209+
s := fmt.Sprintf("%s - [ ", pg.dev)
210+
for ix := range len(pg.pins) {
211+
s += fmt.Sprintf("%d ", pg.pins[ix].Number())
212+
}
213+
s += "]"
214+
return s
215+
}
216+
217+
var _ gpio.Group = &pinGroup{}

mcp23xxx/group_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2025 The Periph Authors. All rights reserved.
2+
// Use of this source code is governed under the Apache License, Version 2.0
3+
// that can be found in the LICENSE file.
4+
5+
package mcp23xxx
6+
7+
import (
8+
"testing"
9+
"time"
10+
11+
"periph.io/x/conn/v3/gpio"
12+
"periph.io/x/conn/v3/i2c/i2ctest"
13+
)
14+
15+
const (
16+
liveTest = false
17+
)
18+
19+
func TestGroup(t *testing.T) {
20+
bus := i2ctest.Playback{Ops: recordingData["TestGroup"]}
21+
extender, err := NewI2C(&bus, MCP23008, 0x20)
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
26+
for portnum, port := range extender.Pins {
27+
for _, pin := range port {
28+
t.Logf("Port: %d Pin: %d %s", portnum, pin.Number(), pin)
29+
}
30+
}
31+
32+
// The test fixture has an led on pin 0, and also wires pin 0 -> pin 4.
33+
// If I don't set pin 4 for input, then it breaks the LED blinking.
34+
// It's not important to the test, but it's nice to have a visual indicator.
35+
reader, _ := interface{}(extender.Pins[0][4]).(gpio.PinIn)
36+
_ = reader.In(gpio.PullNoChange, gpio.NoEdge)
37+
var ifpin interface{} = extender.Pins[0][0]
38+
if writer, ok := ifpin.(gpio.PinOut); ok {
39+
l := gpio.High
40+
for range 20 {
41+
writer.Out(l)
42+
l = !l
43+
if liveTest {
44+
time.Sleep(500 * time.Millisecond)
45+
}
46+
}
47+
} else {
48+
t.Error("pin[0] not converted to gpio.PinOut!")
49+
}
50+
}
51+
52+
// TestReadWrite exercises the group Out()/Read() functions. It's expected
53+
// that pin 0 of MCP23xxx port 0 is connected to pin 4 of port 0, pin 1
54+
// is connected to pin 5, etc...
55+
func TestReadWrite(t *testing.T) {
56+
bus := i2ctest.Playback{Ops: recordingData["TestReadWrite"]}
57+
extender, err := NewI2C(&bus, MCP23008, 0x20)
58+
if err != nil {
59+
t.Fatal(err)
60+
}
61+
defMask := gpio.GPIOValue(0xf)
62+
gOut := *extender.Group(0, []int{0, 1, 2, 3})
63+
if gOut == nil {
64+
t.Error("gOut is nil!")
65+
}
66+
gRead := *extender.Group(0, []int{4, 5, 6, 7})
67+
if gRead == nil {
68+
t.Error("gRead is nil!")
69+
}
70+
// Turn off the GPIOs
71+
defer gOut.Out(0, 0)
72+
defer gRead.Out(0, 0)
73+
74+
for i := range 2 {
75+
if i == 1 {
76+
/* Now invert it. */
77+
x := gRead
78+
gRead = gOut
79+
gOut = x
80+
}
81+
for i := range gpio.GPIOValue(16) {
82+
err := gOut.Out(i, 0)
83+
if err != nil {
84+
t.Error(err)
85+
}
86+
if liveTest {
87+
time.Sleep(time.Millisecond)
88+
}
89+
r, err := gRead.Read(defMask)
90+
if err != nil {
91+
t.Error(err)
92+
}
93+
if r != i {
94+
t.Errorf("Error reading/writing GPIO Group(). Wrote 0x%x Read 0x%x", i, r)
95+
}
96+
}
97+
}
98+
99+
// For this test, write to the pins individually, and then
100+
// confirm read on the other set works as expected.
101+
gRead = gOut
102+
t.Log(gRead)
103+
pinset := extender.Pins[0][:4]
104+
for i := range gpio.GPIOValue(16) {
105+
wvalue := i
106+
for bit := range 4 {
107+
if (wvalue & (1 << bit)) == (1 << bit) {
108+
pinset[bit].Out(gpio.High)
109+
} else {
110+
pinset[bit].Out(gpio.Low)
111+
}
112+
}
113+
r, err := gRead.Read(0)
114+
if err != nil {
115+
t.Error(err)
116+
}
117+
if r != i {
118+
t.Errorf("Error writing GPIO pins and reading back result. Read 0x%x Expected 0x%x", r, i)
119+
}
120+
}
121+
}

0 commit comments

Comments
 (0)