176 lines
4.1 KiB
Go
176 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))
|
|
}
|