diff --git a/experimental/devices/pca9548/doc.go b/experimental/devices/pca9548/doc.go new file mode 100644 index 0000000..5ef3049 --- /dev/null +++ b/experimental/devices/pca9548/doc.go @@ -0,0 +1,19 @@ +// Copyright 2018 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 pca9548 is a driver for an 8 port I²C multiplexer that is available +// from multiple vendors. The main features of this multiplexer is that its has +// 8 channels and is capable of voltage level translation. +// +// +// Adjusting the Bus CLK +// +// The bus clock is slaved to the master bus clock, different clock for each +// port is currently not supported. The Maximum clock for this device is 400kHz. +// +// +// Datasheet +// +// https://www.nxp.com/docs/en/data-sheet/PCA9548A.pdf +package pca9548 diff --git a/experimental/devices/pca9548/pca9548.go b/experimental/devices/pca9548/pca9548.go new file mode 100644 index 0000000..98bb0d9 --- /dev/null +++ b/experimental/devices/pca9548/pca9548.go @@ -0,0 +1,158 @@ +// Copyright 2018 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 pca9548 + +import ( + "errors" + "strconv" + "sync" + + "periph.io/x/periph/conn" + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/conn/physic" +) + +// DefaultOpts is the recommended default options. +var DefaultOpts = Opts{Addr: 0x70} + +// Opts is the pca9548 configuration. +type Opts struct { + // Addr is the pca9548 I²C Address. Valid addresses for the NXP pca9548 are + // 0x70 to 0x77. The address is set by pulling A0~A2 low or high. Please + // refer to the datasheet. + Addr int +} + +// Dev is handle to a pca9548 I²C Multiplexer. +type Dev struct { + // Immutable. + c i2c.Bus + address uint16 + name string + numPorts uint8 + + // Mutable. + mu sync.Mutex + activePort uint8 +} + +// New creates a new handle to a pca9548 I²C multiplexer. +func New(bus i2c.Bus, opts *Opts) (*Dev, error) { + if opts.Addr < 0x70 || opts.Addr > 0x77 { + return nil, errors.New("Address outside valid range of 0x70-0x77") + } + d := &Dev{ + c: bus, + activePort: 0xFF, + address: uint16(opts.Addr), + numPorts: 8, + name: "pca9548-" + strconv.FormatUint(uint64(opts.Addr), 16), + } + r := make([]byte, 1) + err := bus.Tx(uint16(opts.Addr), nil, r) + if err != nil { + return nil, errors.New("could not establish communicate with multiplexer: " + err.Error()) + } + return d, nil +} + +// RegisterPorts registers multiplexer ports with the host. These ports can +// then be used as any other i2c.Bus. Busses will be named "alias0", "alias1" +// etc. If using more than one multiplexer note that the alias must be unique. +// Returns slice of ports names registered and error. +func (d *Dev) RegisterPorts(alias string) ([]string, error) { + var portNames []string + for i := uint8(0); i < d.numPorts; i++ { + portStr := strconv.Itoa(int(i)) + addrStr := strconv.FormatUint(uint64(d.address), 16) + portName := d.c.String() + "-pca9548-" + addrStr + "-" + portStr + opener := newOpener(d, i, alias+portStr, portName) + if err := i2creg.Register(portName, []string{alias + portStr}, -1, opener); err != nil { + return portNames, err + } + portNames = append(portNames, portName) + } + return portNames, nil +} + +// Halt does nothing. +func (d *Dev) Halt() error { + return nil +} + +// String returns the bus base name for multiplexer ports. +func (d *Dev) String() string { + return d.name +} + +// tx wraps the master bus tx, maintains which port that each bus is registered +// on so that communication from the master is always on the right port. +func (d *Dev) tx(port uint8, address uint16, w, r []byte) error { + if address == d.address { + return errors.New("device address conflicts with multiplexer address") + } + d.mu.Lock() + defer d.mu.Unlock() + // Change active port if needed. + if port != d.activePort { + if err := d.c.Tx(d.address, []byte{1 << port}, nil); err != nil { + return errors.New("failed to change active port on multiplexer: " + err.Error()) + } + d.activePort = port + } + return d.c.Tx(address, w, r) +} + +// newOpener is a helper for creating an opener func. +func newOpener(d *Dev, portNumber uint8, alias string, name string) i2creg.Opener { + return func() (i2c.BusCloser, error) { + return &port{ + name: name + "(" + alias + ")", + mux: d, + number: portNumber, + }, nil + } +} + +// port is a i2c.BusCloser. +type port struct { + // Immutable. + name string + number uint8 + + // Mutable. + mu sync.Mutex + mux *Dev +} + +// String gets the port number of the bus on the multiplexer. +func (p *port) String() string { return "Port:" + p.name } + +// SetSpeed is no implemented as the port slaves the master port clock. +func (p *port) SetSpeed(f physic.Frequency) error { + return errors.New("SetSpeed is not impelmented on a port by port basis") +} + +// Tx does a transaction on the multiplexer port it is register to. +func (p *port) Tx(addr uint16, w, r []byte) error { + p.mu.Lock() + defer p.mu.Unlock() + if p.mux == nil { + return errors.New(p.String() + " has been closed") + } + return p.mux.tx(p.number, addr, w, r) +} + +// Close closes a port. +func (p *port) Close() error { + p.mu.Lock() + p.mux = nil + p.mu.Unlock() + return nil +} + +var _ conn.Resource = &Dev{} +var _ i2c.Bus = &port{} diff --git a/experimental/devices/pca9548/pca9548_test.go b/experimental/devices/pca9548/pca9548_test.go new file mode 100644 index 0000000..f8eaa9a --- /dev/null +++ b/experimental/devices/pca9548/pca9548_test.go @@ -0,0 +1,339 @@ +// Copyright 2018 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 pca9548 + +import ( + "strconv" + "testing" + + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/physic" + "periph.io/x/periph/host" +) + +func TestNew(t *testing.T) { + tests := []struct { + tx []i2ctest.IO + address int + wantErr bool + }{ + { + tx: []i2ctest.IO{{Addr: 0x70, W: nil, R: []byte{0xFF}}}, + address: 0x70, + wantErr: false, + }, + { + tx: []i2ctest.IO{{Addr: 0x70, W: nil, R: nil}}, + address: 0x70, + wantErr: true, + }, + { + tx: []i2ctest.IO{{Addr: 0x50, W: nil, R: nil}}, + address: 0x50, + wantErr: true, + }, + } + + for _, tt := range tests { + bus := &i2ctest.Playback{Ops: tt.tx, DontPanic: true} + _, err := New(bus, &Opts{Addr: tt.address}) + + if err != nil && !tt.wantErr { + t.Errorf("got unexpected error %v", err) + } + + if err == nil && tt.wantErr { + t.Errorf("expected error but got none") + } + } +} + +func TestRegisterPorts(t *testing.T) { + tests := []struct { + alias string + expect []string + }{ + { + "mux", + []string{ + "playback-pca9548-70-0", + "playback-pca9548-70-1", + "playback-pca9548-70-2", + "playback-pca9548-70-3", + "playback-pca9548-70-4", + "playback-pca9548-70-5", + "playback-pca9548-70-6", + "playback-pca9548-70-7", + }, + }, + } + + for _, tt := range tests { + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x70, W: nil, R: []byte{0xFF}}, + }, + DontPanic: true, + } + host.Init() + + mux, err := New(bus, &DefaultOpts) + if err != nil { + t.Fatalf("failed to create I²C mux: %v", err) + } + + portNames, err := mux.RegisterPorts(tt.alias) + if err != nil { + t.Fatalf("failed to create I²C mux: %v", err) + } + for i, port := range tt.expect { + if portNames[i] != port { + t.Errorf("expected port name %v but got %v", portNames[i], port) + } else if err := i2creg.Unregister(port); err != nil { + t.Errorf("failed to Unregister port %d: %v", i, err) + } + } + } + // Failing Case. + for _, tt := range tests { + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: 0x70, W: nil, R: []byte{0xFF}}, + }, + DontPanic: true, + } + host.Init() + + mux, err := New(bus, &DefaultOpts) + if err != nil { + t.Fatalf("failed to create I²C mux: %v", err) + } + + if _, err := mux.RegisterPorts(tt.alias); err != nil { + t.Fatalf("failed to create I²C mux: %v", err) + } + if _, err := mux.RegisterPorts(tt.alias); err == nil { + t.Fatal("expected second registraion to fail", err) + } + for i, port := range tt.expect { + if err := i2creg.Unregister(port); err != nil { + t.Errorf("failed to Unregister port %d: %v", i, err) + } + } + } +} + +func TestDev_Halt(t *testing.T) { + d := &Dev{} + err := d.Halt() + if err != nil { + t.Errorf("expected error but got none") + } +} + +func TestDev_String(t *testing.T) { + tests := []struct { + address uint16 + want string + }{ + {address: 0x70, want: "pca9548-70"}, + {address: 0x71, want: "pca9548-71"}, + {address: 0x72, want: "pca9548-72"}, + {address: 0x73, want: "pca9548-73"}, + {address: 0x74, want: "pca9548-74"}, + } + for _, tt := range tests { + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{ + {Addr: tt.address, W: nil, R: []byte{0xFF}}, + }, + DontPanic: false, + } + host.Init() + + mux, err := New(bus, &Opts{Addr: int(tt.address)}) + if err != nil { + t.Fatalf("failed to create I²C mux: %v", err) + } + + if got := mux.String(); got != tt.want { + t.Errorf("Dev.String() = %v, want %v", got, tt.want) + } + } +} + +func TestDev_Tx(t *testing.T) { + var tests = []struct { + alias string + openPort string + initialPort int + address uint16 + tx []i2ctest.IO + wantErr bool + }{ + { + alias: "mux", + openPort: "mux0", + initialPort: 0, + address: 0x30, + tx: []i2ctest.IO{ + {Addr: 0x70, W: nil, R: []byte{0xFF}}, + {Addr: 0x70, W: []byte{0x01}, R: []byte{}}, + {Addr: 0x30, W: []byte{0xAA}, R: []byte{0xBB}}, + }, + }, + { + alias: "mux", + openPort: "mux0", + initialPort: 0, + address: 0x70, + tx: []i2ctest.IO{ + {Addr: 0x70, W: nil, R: []byte{0xFF}}, + {Addr: 0x70, W: []byte{0x01}, R: []byte{}}, + {Addr: 0x70, W: []byte{0xAA}, R: []byte{0xBB}}, + }, + wantErr: true, + }, + { + alias: "mux", + openPort: "mux0", + initialPort: 0, + address: 0x30, + tx: []i2ctest.IO{ + {Addr: 0x70, W: nil, R: []byte{0xFF}}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + + bus := &i2ctest.Playback{ + Ops: tt.tx, + DontPanic: true, + } + host.Init() + + mux, err := New(bus, &DefaultOpts) + if err != nil { + t.Fatalf("failed to open I²C: %v", err) + } + + _, err = mux.RegisterPorts(tt.alias) + if err != nil { + t.Fatalf("failed to open I²C: %v", err) + } + + muxbus, err := i2creg.Open(tt.openPort) + if err != nil { + t.Fatalf("failed to open I²C: %v", err) + } + defer muxbus.Close() + err = muxbus.Tx(tt.address, []byte{0xAA}, []byte{0xBB}) + + if err != nil && !tt.wantErr { + t.Errorf("expected no error but got: %v", err) + } + if err == nil && tt.wantErr { + t.Errorf("expected error") + } + + // Cleanup + for i := 0; i < 8; i++ { + if err := i2creg.Unregister("playback-" + mux.String() + "-" + strconv.Itoa(i)); err != nil { + t.Errorf("failed to Unregister port %d: %v", i, err) + } + } + } +} + +func Test_port_Tx(t *testing.T) { + tests := []struct { + p *port + wantErr bool + }{ + { + p: &port{number: 6}, + wantErr: true, + }, + { + p: &port{number: 0x6, mux: &Dev{address: 0x70, activePort: 6, + c: &i2ctest.Playback{ + Ops: []i2ctest.IO{{Addr: 0x1, W: nil, R: nil}}, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + err := tt.p.Tx(0x01, nil, nil) + + if err != nil && !tt.wantErr { + t.Errorf("got unexpected error %v", err) + } + + if err == nil && tt.wantErr { + t.Errorf("expected error but got none") + } + } + +} + +func Test_port_Close(t *testing.T) { + + p := &port{number: 6} + + p.Close() + if err := p.Tx(0x01, nil, nil); err == nil { + t.Errorf("expected error but got none") + } + +} + +func Test_port_String(t *testing.T) { + bus := &i2ctest.Playback{ + Ops: []i2ctest.IO{{Addr: 0x70, W: nil, R: []byte{0xFF}}}, + DontPanic: true, + } + host.Init() + + mux, err := New(bus, &DefaultOpts) + if err != nil { + t.Fatalf("failed to open I²C: %v", err) + } + + _, err = mux.RegisterPorts("mux") + if err != nil { + t.Fatalf("failed to open I²C: %v", err) + } + + muxbus, err := i2creg.Open("mux0") + if err != nil { + t.Fatalf("failed to open I²C: %v", err) + } + + expected := "Port:playback-pca9548-70-0(mux0)" + got := muxbus.String() + if got != expected { + t.Errorf("expected: \n%v but got: \n%v", expected, got) + } + + // Cleanup + for i := 0; i < 8; i++ { + if err := i2creg.Unregister("playback-" + mux.String() + "-" + strconv.Itoa(i)); err != nil { + t.Errorf("failed to Unregister port %d: %v", i, err) + } + } +} + +func Test_port_SetSpeed(t *testing.T) { + p := &port{} + err := p.SetSpeed(400 * physic.KiloHertz) + if err == nil { + t.Errorf("expected error but got none") + } +}