hybridgroup.gobot/system/spi_gpio.go

177 lines
4.1 KiB
Go

package system
import (
"fmt"
"time"
"github.com/hashicorp/go-multierror"
"gobot.io/x/gobot/v2"
)
type spiGpioConfig struct {
pinProvider gobot.DigitalPinnerProvider
sclkPinID string
nssPinID string
mosiPinID string
misoPinID string
}
// spiGpio is the implementation of the SPI interface using GPIO's.
type spiGpio struct {
cfg spiGpioConfig
// time between clock edges (i.e. half the cycle time)
tclk time.Duration
sclkPin gobot.DigitalPinner
nssPin gobot.DigitalPinner
mosiPin gobot.DigitalPinner
misoPin gobot.DigitalPinner
}
// newSpiGpio creates and returns a new SPI connection based on given GPIO's.
func newSpiGpio(cfg spiGpioConfig, maxSpeed int64) (*spiGpio, error) {
spi := &spiGpio{cfg: cfg}
spi.initializeTime(maxSpeed)
return spi, spi.initializeGpios()
}
func (s *spiGpio) initializeTime(maxSpeed int64) {
// maxSpeed is given in Hz, tclk is half the cycle time, tclk=1/(2*f), tclk[ns]=1 000 000 000/(2*maxSpeed)
// but with gpio's a speed of more than ~15kHz is most likely not possible, so we limit to 10kHz
if maxSpeed > 10000 {
if systemDebug {
fmt.Printf("reduce SPI speed for GPIO usage to 10Khz")
}
maxSpeed = 10000
}
tclk := time.Duration(1000000000/2/maxSpeed) * time.Nanosecond
if systemDebug {
fmt.Println("clk", tclk)
}
}
// TxRx uses the SPI device to send/receive data. Implements gobot.SpiSystemDevicer.
func (s *spiGpio) TxRx(tx []byte, rx []byte) error {
var doRx bool
if rx != nil {
doRx = true
if len(tx) != len(rx) {
return fmt.Errorf("length of tx (%d) must be the same as length of rx (%d)", len(tx), len(rx))
}
}
if err := s.nssPin.Write(0); err != nil {
return err
}
for idx, b := range tx {
val, err := s.transferByte(b)
if err != nil {
return err
}
if doRx {
rx[idx] = val
}
}
return s.nssPin.Write(1)
}
// Close the SPI connection. Implements gobot.SpiSystemDevicer.
func (s *spiGpio) Close() error {
var err error
if s.sclkPin != nil {
if e := s.sclkPin.Unexport(); e != nil {
err = multierror.Append(err, e)
}
}
if s.mosiPin != nil {
if e := s.mosiPin.Unexport(); e != nil {
err = multierror.Append(err, e)
}
}
if s.misoPin != nil {
if e := s.misoPin.Unexport(); e != nil {
err = multierror.Append(err, e)
}
}
if s.nssPin != nil {
if e := s.nssPin.Unexport(); e != nil {
err = multierror.Append(err, e)
}
}
return err
}
func (cfg *spiGpioConfig) String() string {
return fmt.Sprintf("sclk: %s, nss: %s, mosi: %s, miso: %s", cfg.sclkPinID, cfg.nssPinID, cfg.mosiPinID, cfg.misoPinID)
}
// transferByte simultaneously transmit and receive a byte
// polarity and phase are assumed to be both 0 (CPOL=0, CPHA=0), so:
// * input data is captured on rising edge of SCLK
// * output data is propagated on falling edge of SCLK
func (s *spiGpio) transferByte(txByte uint8) (uint8, error) {
rxByte := uint8(0)
bitMask := uint8(0x80) // start at MSBit
for i := 0; i < 8; i++ {
if err := s.mosiPin.Write(int(txByte & bitMask)); err != nil {
return 0, err
}
time.Sleep(s.tclk)
if err := s.sclkPin.Write(1); err != nil {
return 0, err
}
v, err := s.misoPin.Read()
if err != nil {
return 0, err
}
if v != 0 {
rxByte |= bitMask
}
time.Sleep(s.tclk)
if err := s.sclkPin.Write(0); err != nil {
return 0, err
}
bitMask = bitMask >> 1 // next lower bit
}
return rxByte, nil
}
func (s *spiGpio) initializeGpios() error {
var err error
// nss is an output, negated (currently not implemented at pin level)
s.nssPin, err = s.cfg.pinProvider.DigitalPin(s.cfg.nssPinID)
if err != nil {
return err
}
if err := s.nssPin.ApplyOptions(WithPinDirectionOutput(1)); err != nil {
return err
}
// sclk is an output, CPOL = 0
s.sclkPin, err = s.cfg.pinProvider.DigitalPin(s.cfg.sclkPinID)
if err != nil {
return err
}
if err := s.sclkPin.ApplyOptions(WithPinDirectionOutput(0)); err != nil {
return err
}
// miso is an input
s.misoPin, err = s.cfg.pinProvider.DigitalPin(s.cfg.misoPinID)
if err != nil {
return err
}
// mosi is an output
s.mosiPin, err = s.cfg.pinProvider.DigitalPin(s.cfg.mosiPinID)
if err != nil {
return err
}
return s.mosiPin.ApplyOptions(WithPinDirectionOutput(0))
}