Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions nxp74hc595/74hc595.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// 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.

// The 74HC595 is a serial shift register. It converts a serial stream to a
// parallel output. For example, you can use it as an SPI => Parallel
// converter.
//
// # Datasheet
//
// https://www.nexperia.com/product/74HC595D
//
// There's a nice tutorial on the device here:
//
// https://docs.arduino.cc/tutorials/communication/guide-to-shift-out/
package nxp74hc595

import (
"errors"
"fmt"
"sync"
"time"

"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/pin"
"periph.io/x/conn/v3/spi"
)

const (
devMask = 0xff
devName = "74HC595"
numPins = 8
)

var (
ErrNotImplemented = errors.New("nxp74hc595: not implemented")
)

// Dev represents a 74hc595 device.
type Dev struct {
Pins []gpio.PinOut

mu sync.Mutex
conn spi.Conn
value gpio.GPIOValue
}

// Group implements gpio.Group and provides a way to write to multiple GPO pins
// in a single transaction.
type Group struct {
dev *Dev
pins []Pin
}

// New accepts an spi.Conn and returns a new HC74595 device.
func New(conn spi.Conn) (*Dev, error) {
// setting value to an invalid initial state forces the first write to
// happen, even if it's 0.
dev := Dev{conn: conn, value: gpio.GPIOValue(1 << 9), Pins: make([]gpio.PinOut, numPins)}
for ix := range numPins {
dev.Pins[ix] = &Pin{number: ix, name: fmt.Sprintf("%s_GPO%d", devName, ix), dev: &dev}
}
return &dev, nil
}

// write does the low-level write to the device.
func (dev *Dev) write(value, mask gpio.GPIOValue) error {

dev.mu.Lock()
defer dev.mu.Unlock()
newValue := (dev.value & (devMask ^ mask)) | (value & mask)
if dev.value == newValue {
return nil
}
var err error
var w = []byte{byte(newValue)}
err = dev.conn.Tx(w, nil)
if err == nil {
dev.value = newValue
}
return err
}

// Group returns a subset of pins on the device as a gpio.Group. A Group
// allows you to write to multiple pins in a single transaction.
func (dev *Dev) Group(pins ...int) (gpio.Group, error) {
gr := Group{dev: dev, pins: make([]Pin, len(pins))}
for ix, pinNumber := range pins {
if p, ok := dev.Pins[pinNumber].(*Pin); ok {
gr.pins[ix] = *p
}
}
return &gr, nil
}

// Halt disables the device
func (dev *Dev) Halt() (err error) {
dev.mu.Lock()
defer dev.mu.Unlock()
dev.Pins = make([]gpio.PinOut, 0)
dev.conn = nil
return

Check warning on line 102 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L97-L102

Added lines #L97 - L102 were not covered by tests
}

func (dev *Dev) String() string {
return devName

Check warning on line 106 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L105-L106

Added lines #L105 - L106 were not covered by tests
}

// Return the set of GPO Pins that are associated with this group.
func (gr *Group) Pins() []pin.Pin {
result := make([]pin.Pin, len(gr.pins))
for ix, p := range gr.pins {
result[ix] = &p
}
return result

Check warning on line 115 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L110-L115

Added lines #L110 - L115 were not covered by tests

}

// Given an offset of a pin into the group, return that pin.
func (gr *Group) ByOffset(offset int) pin.Pin {
return &gr.pins[offset]

Check warning on line 121 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L120-L121

Added lines #L120 - L121 were not covered by tests
}

// Given a name of a pin in the group, return that pin.
func (gr *Group) ByName(name string) pin.Pin {
for _, p := range gr.pins {
if p.name == name {
return &p
}

Check warning on line 129 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L125-L129

Added lines #L125 - L129 were not covered by tests
}
return nil

Check warning on line 131 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L131

Added line #L131 was not covered by tests
}

// Given the pin number of a pin within the group, return that pin.
func (gr *Group) ByNumber(number int) pin.Pin {
for _, p := range gr.pins {
if p.number == number {
return &p
}

Check warning on line 139 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L135-L139

Added lines #L135 - L139 were not covered by tests
}
return nil

Check warning on line 141 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L141

Added line #L141 was not covered by tests
}

// Out writes the value to the device. Only pins identified by mask are
// modified.
func (gr *Group) Out(value, mask gpio.GPIOValue) error {
if mask == 0 {
mask = gpio.GPIOValue(1<<len(gr.pins)) - 1
}
wrMask := gpio.GPIOValue(0)
wrValue := gpio.GPIOValue(0)
for ix := range len(gr.pins) {
currentBit := gpio.GPIOValue(1 << ix)
if (mask & currentBit) == currentBit {
wrMask |= gpio.GPIOValue(1 << gr.pins[ix].number)
}
if (value & currentBit) == currentBit {
wrValue |= gpio.GPIOValue(1 << gr.pins[ix].number)
}
}
return gr.dev.write(wrValue, wrMask)
}

// Read is not available for this device.
func (gr *Group) Read(mask gpio.GPIOValue) (gpio.GPIOValue, error) {
return 0, ErrNotImplemented

Check warning on line 166 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L165-L166

Added lines #L165 - L166 were not covered by tests
}

// WaitForEdge is not available for this device.
func (gr *Group) WaitForEdge(timeout time.Duration) (int, gpio.Edge, error) {
return 0, gpio.NoEdge, ErrNotImplemented

Check warning on line 171 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L170-L171

Added lines #L170 - L171 were not covered by tests
}

// Halt frees the group's resources and prevents it from being used again.
func (gr *Group) Halt() error {
gr.pins = nil
return nil

Check warning on line 177 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L175-L177

Added lines #L175 - L177 were not covered by tests
}

func (gr *Group) String() string {
s := gr.dev.String() + "[ "
for ix := range len(gr.pins) {
s += fmt.Sprintf("%d ", gr.pins[ix].number)
}
s += "]"
return s

Check warning on line 186 in nxp74hc595/74hc595.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/74hc595.go#L180-L186

Added lines #L180 - L186 were not covered by tests
}

var _ gpio.PinOut = &Pin{}
var _ gpio.Group = &Group{}
54 changes: 54 additions & 0 deletions nxp74hc595/74hc595_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 nxp74hc595

import (
"log"
"testing"

"periph.io/x/conn/v3/conntest"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/physic"
"periph.io/x/conn/v3/spi"
"periph.io/x/conn/v3/spi/spitest"
)

func TestBasic(t *testing.T) {
pb := &spitest.Record{Ops: make([]conntest.IO, 0)}
defer pb.Close()
conn, err := pb.Connect(physic.MegaHertz, spi.Mode1, 8)
if err != nil {
log.Fatal(err)
}

dev, err := New(conn)
if err != nil {
log.Fatal(err)
}

gr, _ := dev.Group(6, 5, 4, 3)
for i := range 16 {
gr.Out(gpio.GPIOValue(i), 0)
}
singlePin := dev.Pins[7]
for i := range 20 {
err = singlePin.Out(gpio.Level(i%2 == 0))
if err != nil {
t.Error(err)
}
err = dev.Pins[0].Out(i%2 != 0)
if err != nil {
t.Error(err)
}
}
err = dev.Pins[0].Out(gpio.Low)
if err != nil {
t.Error(err)
}
err = singlePin.Out(gpio.High)
if err != nil {
t.Error(err)
}
}
41 changes: 41 additions & 0 deletions nxp74hc595/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 nxp74hc595

import (
"log"

"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/physic"
"periph.io/x/conn/v3/spi"
"periph.io/x/conn/v3/spi/spireg"
"periph.io/x/host/v3"
)

func Example() {
if _, err := host.Init(); err != nil {
log.Fatal(err)
}
// Open the SPI Bus
pc, err := spireg.Open("")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
conn, err := pc.Connect(physic.MegaHertz, spi.Mode1, 8)
if err != nil {
log.Fatal(err)
}
// Create a new 74HC595 device conn that bus.
dev, err := New(conn)
if err != nil {
log.Fatal(err)
}
// Get a GPIO group, and write values to it.
gr, _ := dev.Group(0, 1, 2, 3, 4, 5, 6, 7, 8)
for i := range 256 {
gr.Out(gpio.GPIOValue(i), 0)
}
}
55 changes: 55 additions & 0 deletions nxp74hc595/pin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 nxp74hc595

import (
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/physic"
)

type Pin struct {
dev *Dev
name string
number int
}

// Halt implements conn.Resource.
func (pin *Pin) Halt() error {
return nil

Check warning on line 20 in nxp74hc595/pin.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/pin.go#L19-L20

Added lines #L19 - L20 were not covered by tests
}

// Name returns the name of the GPIO pin.
func (pin *Pin) Name() string {
return pin.name

Check warning on line 25 in nxp74hc595/pin.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/pin.go#L24-L25

Added lines #L24 - L25 were not covered by tests
}

// Number returns the number of the GPIO pin.
func (pin *Pin) Number() int {
return pin.number

Check warning on line 30 in nxp74hc595/pin.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/pin.go#L29-L30

Added lines #L29 - L30 were not covered by tests
}

// Deprecated: returns "Out"
func (pin *Pin) Function() string {
return "Out"

Check warning on line 35 in nxp74hc595/pin.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/pin.go#L34-L35

Added lines #L34 - L35 were not covered by tests
}

// Write the specified gpio.Level to the pin.
func (pin *Pin) Out(l gpio.Level) error {
mask := gpio.GPIOValue(1 << pin.number)
v := gpio.GPIOValue(0)
if l {
v = mask
}
return pin.dev.write(v, mask)
}

// Not implemented.
func (pin *Pin) PWM(duty gpio.Duty, f physic.Frequency) error {
return ErrNotImplemented

Check warning on line 50 in nxp74hc595/pin.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/pin.go#L49-L50

Added lines #L49 - L50 were not covered by tests
}

func (pin *Pin) String() string {
return pin.name

Check warning on line 54 in nxp74hc595/pin.go

View check run for this annotation

Codecov / codecov/patch

nxp74hc595/pin.go#L53-L54

Added lines #L53 - L54 were not covered by tests
}