mcp23xxx: support for Microchip MCP23 family of IO extenders (#433)

Initial code for MCP23xxx support.
pull/1/head
Balázs Grill 6 years ago committed by GitHub
parent 1f4a49516e
commit dffddf2e7e

@ -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

@ -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)
}
}

@ -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")
}
}

@ -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),
}}
}

@ -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}

@ -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
}
Loading…
Cancel
Save