diff --git a/Makefile b/Makefile index 1a73800d2..a048705d6 100644 --- a/Makefile +++ b/Makefile @@ -57,4 +57,5 @@ deps: go.bug.st/serial.v1 \ github.com/veandco/go-sdl2/sdl \ golang.org/x/net/websocket \ + golang.org/x/exp/io/spi \ golang.org/x/sys/unix diff --git a/drivers/spi/README.MD b/drivers/spi/README.MD new file mode 100644 index 000000000..a8f558804 --- /dev/null +++ b/drivers/spi/README.MD @@ -0,0 +1,25 @@ +# SPI + +This package provides drivers for [spi](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface_Bus) devices. It must be used along with the [raspberry pi](https://gobot.io/documentation/platforms/raspi) adaptor that supports the needed interfaces for spi devices. This uses the experimental [spi package](https://github.com/golang/exp/tree/master/io/spi) which only works on linux systems. + +## Getting Started + +## Installing +``` +go get -d -u gobot.io/x/gobot/... +``` + +## Hardware Support +Gobot has a extensible system for connecting to hardware devices. The following spi devices are currently supported: + +- GoPiGo3 robotics board (built for Rapsberry Pi, but in theory it could be used in another platform) + +More drivers are coming soon... + +## Using A Different Bus or Address + +You can set a different SPI address or SPI bus than the default when initializing your SPI drivers by using optional parameters. Here is an example: + +```go +blinkm := spi.NewGoPiGo3DriverDriver(e, spi.WithBus(0), spi.WithAddress(0x10), spi.With) +``` \ No newline at end of file diff --git a/drivers/spi/doc.go b/drivers/spi/doc.go new file mode 100644 index 000000000..cc4d10ea8 --- /dev/null +++ b/drivers/spi/doc.go @@ -0,0 +1,9 @@ +/* +Package spi provides Gobot drivers for spi devices. +Uses "golang.org/x/exp/io/spi" for spi +Installing: + go get -d -u gobot.io/x/gobot +For further information refer to spi README: +https://github.com/hybridgroup/gobot/blob/master/drivers/spi/README.md +*/ +package spi // import "gobot.io/x/gobot/drivers/spi" diff --git a/drivers/spi/gopigo3_driver.go b/drivers/spi/gopigo3_driver.go new file mode 100644 index 000000000..15e4d154f --- /dev/null +++ b/drivers/spi/gopigo3_driver.go @@ -0,0 +1,410 @@ +// based on https://github.com/DexterInd/GoPiGo3/blob/master/Software/Python/gopigo3.py +// you will need to run the following if using a stock raspbian image before this library will work: +// https://www.dexterindustries.com/GoPiGo/get-started-with-the-gopigo3-raspberry-pi-robot/3-program-your-raspberry-pi-robot/python-programming-language/ +package spi + +import ( + "gobot.io/x/gobot" + + "fmt" + "math" +) + +// spi address for gopigo3 +const goPiGo3Address = 0x08 + +// register addresses for gopigo3 +const ( + NONE byte = iota + GET_MANUFACTURER + GET_NAME + GET_HARDWARE_VERSION + GET_FIRMWARE_VERSION + GET_ID + SET_LED + GET_VOLTAGE_5V + GET_VOLTAGE_VCC + SET_SERVO + SET_MOTOR_PWM + SET_MOTOR_POSITION + SET_MOTOR_POSITION_KP + SET_MOTOR_POSITION_KD + SET_MOTOR_DPS + SET_MOTOR_LIMITS + OFFSET_MOTOR_ENCODER + GET_MOTOR_ENCODER_LEFT + GET_MOTOR_ENCODER_RIGHT + GET_MOTOR_STATUS_LEFT + GET_MOTOR_STATUS_RIGHT + SET_GROVE_TYPE + SET_GROVE_MODE + SET_GROVE_STATE + SET_GROVE_PWM_DUTY + SET_GROVE_PWM_FREQUENCY + GET_GROVE_VALUE_1 + GET_GROVE_VALUE_2 + GET_GROVE_STATE_1_1 + GET_GROVE_STATE_1_2 + GET_GROVE_STATE_2_1 + GET_GROVE_STATE_2_2 + GET_GROVE_VOLTAGE_1_1 + GET_GROVE_VOLTAGE_1_2 + GET_GROVE_VOLTAGE_2_1 + GET_GROVE_VOLTAGE_2_2 + GET_GROVE_ANALOG_1_1 + GET_GROVE_ANALOG_1_2 + GET_GROVE_ANALOG_2_1 + GET_GROVE_ANALOG_2_2 + START_GROVE_I2C_1 + START_GROVE_I2C_2 +) + +const ( + WHEEL_BASE_WIDTH = 117 // distance (mm) from left wheel to right wheel. This works with the initial GPG3 prototype. Will need to be adjusted. + WHEEL_DIAMETER = 66.5 // wheel diameter (mm) + WHEEL_BASE_CIRCUMFERENCE = WHEEL_BASE_WIDTH * math.Pi // circumference of the circle the wheels will trace while turning (mm) + WHEEL_CIRCUMFERENCE = WHEEL_DIAMETER * math.Pi // circumference of the wheels (mm) + MOTOR_GEAR_RATIO = 120 // motor gear ratio + ENCODER_TICKS_PER_ROTATION = 6 // encoder ticks per motor rotation (number of magnet positions) + MOTOR_TICKS_PER_DEGREE = ((MOTOR_GEAR_RATIO * ENCODER_TICKS_PER_ROTATION) / 360.0) // encoder ticks per output shaft rotation degree + GROVE_I2C_LENGTH_LIMIT = 16 + MOTOR_FLOAT = -128 + GROVE_INPUT_DIGITAL = 0 + GROVE_OUTPUT_DIGITAL = 1 + GROVE_INPUT_DIGITAL_PULLUP = 2 + GROVE_INPUT_DIGITAL_PULLDOWN = 3 + GROVE_INPUT_ANALOG = 4 + GROVE_OUTPUT_PWM = 5 + GROVE_INPUT_ANALOG_PULLUP = 6 + GROVE_INPUT_ANALOG_PULLDOWN = 7 + GROVE_LOW = 0 + GROVE_HIGH = 1 +) + +// Servo contains the address for the 2 servo ports. +type Servo byte + +const ( + SERVO_1 = 0x01 + SERVO_2 = 0x02 +) + +// Motor contains the address for the left and right motors. +type Motor byte + +const ( + MOTOR_LEFT Motor = 0x01 + MOTOR_RIGHT Motor = 0x02 +) + +// Led contains the addresses for all leds on the board. +type Led byte + +const ( + LED_EYE_RIGHT Led = 0x01 + LED_EYE_LEFT Led = 0x02 + LED_BLINKER_LEFT Led = 0x04 + LED_BLINKER_RIGHT Led = 0x08 + LED_WIFI Led = 0x80 +) + +// Grove contains the addresses for the optional grove devices. +type Grove byte + +const ( + GROVE_1_1 = 0x01 + GROVE_1_2 = 0x02 + GROVE_2_1 = 0x04 + GROVE_2_2 = 0x08 + GROVE_1 = GROVE_1_1 + GROVE_1_2 + GROVE_2 = GROVE_2_1 + GROVE_2_2 +) + +// GroveType represents the type of a grove device. +type GroveType int + +const ( + CUSTOM GroveType = iota + IR_DI_REMOTE + IR_EV3_REMOTE + US + I2C +) + +// GroveState contains the state of a grove device. +type GroveState int + +const ( + VALID_DATA GroveState = iota + NOT_CONFIGURED + CONFIGURING + NO_DATA + I2C_ERROR +) + +// GoPiGo3Driver is a Gobot Driver for the GoPiGo3 board. +type GoPiGo3Driver struct { + name string + connector Connector + connection Connection + Config + sCon *SpiConnection +} + +// NewGoPiGo3Driver creates a new Gobot Driver for the GoPiGo3 board. +// +// Params: +// conn Connector - the Adaptor to use with this Driver +// +// Optional params: +// spi.WithBus(int): bus to use with this driver +// spi.WithAddress(int): address to use with this driver +// +func NewGoPiGo3Driver(conn Connector, options ...func(Config)) *GoPiGo3Driver { + g := &GoPiGo3Driver{ + name: gobot.DefaultName("GoPiGo3"), + connector: conn, + Config: NewConfig(), + } + for _, option := range options { + option(g) + } + return g +} + +func (g *GoPiGo3Driver) initialization() (err error) { + bus := g.GetBusOrDefault(g.connector.GetSpiDefaultBus()) + address := g.GetAddressOrDefault(goPiGo3Address) + mode := g.connector.GetSpiDefaultMode() + maxSpeed := g.connector.GetSpiDefaultMaxSpeed() + g.connection, err = g.connector.GetSpiConnection(bus, address, mode, maxSpeed) + g.sCon = g.connection.(*SpiConnection) + return err +} + +// Name returns the name of the device. +func (g *GoPiGo3Driver) Name() string { return g.name } + +// SetName sets the name of the device. +func (g *GoPiGo3Driver) SetName(n string) { g.name = n } + +// Connection returns the Connection of the device. +func (g *GoPiGo3Driver) Connection() gobot.Connection { return g.connector.(gobot.Connection) } + +// Halt stops the driver. +func (g *GoPiGo3Driver) Halt() (err error) { return } + +// Start initializes the GoPiGo3 +func (g *GoPiGo3Driver) Start() (err error) { + return g.initialization() +} + +// GetManufacturerName returns the manufacturer from the firmware. +func (g *GoPiGo3Driver) GetManufacturerName() (mName string, err error) { + // read 24 bytes to get manufacturer name + response, err := g.sCon.ReadBytes(goPiGo3Address, GET_MANUFACTURER, 24) + if err != nil { + return mName, err + } + if response[3] == 0xA5 { + mf := response[4:23] + return fmt.Sprintf("%s", string(mf)), nil + } + return mName, nil +} + +// GetBoardName returns the board name from the firmware. +func (g *GoPiGo3Driver) GetBoardName() (bName string, err error) { + // read 24 bytes to get board name + response, err := g.sCon.ReadBytes(goPiGo3Address, GET_NAME, 24) + if err != nil { + return bName, err + } + if response[3] == 0xA5 { + mf := response[4:23] + return fmt.Sprintf("%s", string(mf)), nil + } + return bName, nil +} + +// GetHardwareVersion returns the hardware version from the firmware. +func (g *GoPiGo3Driver) GetHardwareVersion() (hVer string, err error) { + response, err := g.sCon.ReadUint32(goPiGo3Address, GET_HARDWARE_VERSION) + if err != nil { + return hVer, err + } + major := response / 1000000 + minor := response / 1000 % 1000 + patch := response % 1000 + return fmt.Sprintf("%d.%d.%d", major, minor, patch), nil +} + +// GetFirmwareVersion returns the current firmware version. +func (g *GoPiGo3Driver) GetFirmwareVersion() (fVer string, err error) { + response, err := g.sCon.ReadUint32(goPiGo3Address, GET_FIRMWARE_VERSION) + if err != nil { + return fVer, err + } + major := response / 1000000 + minor := response / 1000 % 1000 + patch := response % 1000 + return fmt.Sprintf("%d.%d.%d", major, minor, patch), nil +} + +// GetSerialNumber returns the 128-bit hardware serial number of the board. +func (g *GoPiGo3Driver) GetSerialNumber() (sNum string, err error) { + // read 20 bytes to get the serial number + response, err := g.sCon.ReadBytes(goPiGo3Address, GET_ID, 20) + if err != nil { + return sNum, err + } + if response[3] == 0xA5 { + return fmt.Sprintf("%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", response[4], response[5], response[6], response[7], response[8], response[9], response[10], response[11], response[12], response[13], response[14], response[15], response[16], response[17], response[18], response[19]), nil + + } + return sNum, nil +} + +// Get5vVoltage returns the current voltage on the 5v line. +func (g *GoPiGo3Driver) Get5vVoltage() (voltage float32, err error) { + val, err := g.sCon.ReadUint16(goPiGo3Address, GET_VOLTAGE_5V) + return (float32(val) / 1000.0), err +} + +// GetBatteryVoltage gets the battery voltage from the main battery pack (7v-12v). +func (g *GoPiGo3Driver) GetBatteryVoltage() (voltage float32, err error) { + val, err := g.sCon.ReadUint16(goPiGo3Address, GET_VOLTAGE_VCC) + return (float32(val) / 1000.0), err +} + +// SetLED sets rgb values from 0 to 255. +func (g *GoPiGo3Driver) SetLED(led Led, red, green, blue uint8) error { + return g.sCon.WriteBytes([]byte{ + goPiGo3Address, + SET_LED, + byte(led), + byte(red), + byte(green), + byte(blue), + }) +} + +// SetServo sets a servo's position in microseconds (0-16666). +func (g *GoPiGo3Driver) SetServo(servo Servo, us uint16) error { + return g.sCon.WriteBytes([]byte{ + goPiGo3Address, + SET_SERVO, + byte(servo), + byte(us), + }) +} + +// SetMotorPower from -128 to 127. +func (g *GoPiGo3Driver) SetMotorPower(motor Motor, power int8) error { + return g.sCon.WriteBytes([]byte{ + goPiGo3Address, + SET_MOTOR_PWM, + byte(motor), + byte(power), + }) +} + +// SetMotorPosition sets the motor's position in degrees. +func (g *GoPiGo3Driver) SetMotorPosition(motor Motor, position float64) error { + positionRaw := math.Float64bits(position * MOTOR_TICKS_PER_DEGREE) + return g.sCon.WriteBytes([]byte{ + goPiGo3Address, + SET_MOTOR_POSITION, + byte(motor), + byte((positionRaw >> 24) & 0xFF), + byte((positionRaw >> 16) & 0xFF), + byte((positionRaw >> 8) & 0xFF), + byte(positionRaw & 0xFF), + }) +} + +// SetMotorDps sets the motor target speed in degrees per second. +func (g *GoPiGo3Driver) SetMotorDps(motor Motor, dps float64) error { + dpsUint := math.Float64bits(dps * MOTOR_TICKS_PER_DEGREE) + return g.sCon.WriteBytes([]byte{ + goPiGo3Address, + SET_MOTOR_DPS, + byte(motor), + byte((dpsUint >> 8) & 0xFF), + byte(dpsUint & 0xFF), + }) +} + +// SetMotorLimits sets the speed limits for a motor. +func (g *GoPiGo3Driver) SetMotorLimits(motor Motor, power int8, dps float64) error { + dpsUint := math.Float64bits(dps * MOTOR_TICKS_PER_DEGREE) + return g.sCon.WriteBytes([]byte{ + goPiGo3Address, + SET_MOTOR_LIMITS, + byte(motor), + byte(power), + byte((dpsUint >> 8) & 0xFF), + byte(dpsUint & 0xFF), + }) +} + +// GetMotorStatus returns the status for the given motor. +func (g *GoPiGo3Driver) GetMotorStatus(motor Motor) (flags uint8, power uint16, encoder, dps float64, err error) { + message := GET_MOTOR_STATUS_RIGHT + if motor == MOTOR_LEFT { + message = GET_MOTOR_STATUS_LEFT + } + response, err := g.sCon.ReadBytes(goPiGo3Address, message, 12) + if err != nil { + return flags, power, encoder, dps, err + } + if response[3] == 0xA5 { + flags = uint8(response[4]) + power = uint16(response[5]) + if power&0x80 == 0x80 { + power = power - 0x100 + } + enc := uint64(response[6]<<24 | response[7]<<16 | response[8]<<8 | response[9]) + if enc&0x80000000 == 0x80000000 { + encoder = float64(enc - 0x100000000) + } + d := uint64(response[10]<<8 | response[11]) + if d&0x8000 == 0x8000 { + dps = float64(d - 0x10000) + } + return flags, power, encoder / MOTOR_TICKS_PER_DEGREE, dps / MOTOR_TICKS_PER_DEGREE, nil + } + return flags, power, encoder, dps, nil +} + +// GetMotorEncoder reads a motor's encoder in degrees. +func (g *GoPiGo3Driver) GetMotorEncoder(motor Motor) (encoder float64, err error) { + message := GET_MOTOR_ENCODER_RIGHT + if motor == MOTOR_LEFT { + message = GET_MOTOR_ENCODER_LEFT + } + response, err := g.sCon.ReadUint32(goPiGo3Address, message) + if err != nil { + return encoder, err + } + if response&0x80000000 == 0x80000000 { + encoder = float64(encoder - 0x100000000) + } + return encoder, nil +} + +// OffsetMotorEncoder offsets a motor's encoder for calibration purposes. +func (g *GoPiGo3Driver) OffsetMotorEncoder(motor Motor, offset float64) error { + offsetUint := math.Float64bits(offset * MOTOR_TICKS_PER_DEGREE) + return g.sCon.WriteBytes([]byte{ + goPiGo3Address, + OFFSET_MOTOR_ENCODER, + byte(motor), + byte((offsetUint >> 24) & 0xFF), + byte((offsetUint >> 16) & 0xFF), + byte((offsetUint >> 8) & 0xFF), + byte(offsetUint & 0xFF), + }) +} + +//TODO: add grove functions diff --git a/drivers/spi/spi.go b/drivers/spi/spi.go new file mode 100644 index 000000000..9e419c7e1 --- /dev/null +++ b/drivers/spi/spi.go @@ -0,0 +1,141 @@ +package spi + +import ( + xspi "golang.org/x/exp/io/spi" + "time" +) + +const ( + // BusNotInitialized is the initial value for a bus + BusNotInitialized = -1 + + // AddressNotInitialized is the initial value for an address + AddressNotInitialized = -1 +) + +type SPIOperations interface { + Close() error + SetBitOrder(o xspi.Order) error + SetBitsPerWord(bits int) error + SetCSChange(leaveEnabled bool) error + SetDelay(t time.Duration) error + SetMaxSpeed(speed int) error + SetMode(mode xspi.Mode) error + Tx(w, r []byte) error +} + +// SPIDevice is the interface to a specific spi bus +type SPIDevice interface { + SPIOperations + // SetAddress(int) error +} + +// Connector lets Adaptors provide the interface for Drivers +// to get access to the SPI buses on platforms that support SPI. +type Connector interface { + // GetConnection returns a connection to device at the specified address + // and bus. Bus numbering starts at index 0, the range of valid buses is + // platform specific. + GetSpiConnection(busNum, address, mode int, maxSpeed int64) (device Connection, err error) + + // GetDefaultBus returns the default SPI bus index + GetSpiDefaultBus() int + + // GetDefaultMode returns the default SPI mode (0/1/2/3) + GetSpiDefaultMode() int + + // GetSpiDefaultMaxSpeed returns the max SPI speed + GetSpiDefaultMaxSpeed() int64 +} + +// Connection is a connection to an SPI device with a specified address +// on a specific bus. Used as an alternative to the SPI interface. +// Implements SPIOperations to talk to the device, wrapping the +// calls in SetAddress to always target the specified device. +// Provided by an Adaptor by implementing the SPIConnector interface. +type Connection SPIOperations + +type SpiConnection struct { + bus SPIDevice + mode int + maxSpeed int64 + address int +} + +// NewConnection creates and returns a new connection to a specific +// spi device on a bus and address +func NewConnection(bus SPIDevice, address int) (connection *SpiConnection) { + return &SpiConnection{bus: bus, address: address} +} + +func (c *SpiConnection) Close() error { + return c.bus.Close() +} + +func (c *SpiConnection) SetBitOrder(o xspi.Order) error { + return c.bus.SetBitOrder(o) +} + +func (c *SpiConnection) SetBitsPerWord(bits int) error { + return c.bus.SetBitsPerWord(bits) +} + +func (c *SpiConnection) SetCSChange(leaveEnabled bool) error { + return c.bus.SetCSChange(leaveEnabled) +} + +func (c *SpiConnection) SetDelay(t time.Duration) error { + return c.bus.SetDelay(t) +} + +func (c *SpiConnection) SetMaxSpeed(speed int) error { + return c.bus.SetMaxSpeed(speed) +} + +func (c *SpiConnection) SetMode(mode xspi.Mode) error { + return c.bus.SetMode(mode) +} + +func (c *SpiConnection) Tx(w, r []byte) error { + return c.bus.Tx(w, r) +} + +func (c *SpiConnection) ReadBytes(address byte, msg byte, numBytes int) (val []byte, err error) { + w := make([]byte, numBytes) + w[0] = address + w[1] = msg + r := make([]byte, len(w)) + err = c.Tx(w, r) + if err != nil { + return val, err + } + return r, nil +} + +func (c *SpiConnection) ReadUint8(address, msg byte) (val uint8, err error) { + r, err := c.ReadBytes(address, msg, 8) + if err != nil { + return val, err + } + return uint8(r[4]) << 8, nil +} + +func (c *SpiConnection) ReadUint16(address, msg byte) (val uint16, err error) { + r, err := c.ReadBytes(address, msg, 8) + if err != nil { + return val, err + } + return uint16(r[4])<<8 | uint16(r[5]), nil +} + +func (c *SpiConnection) ReadUint32(address, msg byte) (val uint32, err error) { + r, err := c.ReadBytes(address, msg, 8) + if err != nil { + return val, err + } + return uint32(r[4])<<24 | uint32(r[5])<<16 | uint32(r[6])<<8 | uint32(r[7]), nil +} + +func (c *SpiConnection) WriteBytes(w []byte) (err error) { + return c.Tx(w, nil) +} diff --git a/drivers/spi/spi_config.go b/drivers/spi/spi_config.go new file mode 100644 index 000000000..bf6cac652 --- /dev/null +++ b/drivers/spi/spi_config.go @@ -0,0 +1,70 @@ +package spi + +type spiConfig struct { + bus int + address int +} + +// Config is the interface which describes how a Driver can specify +// optional SPI params such as which SPI bus it wants to use. +type Config interface { + // WithBus sets which bus to use + WithBus(bus int) + + // GetBusOrDefault gets which bus to use + GetBusOrDefault(def int) int + + // WithAddress sets which address to use + WithAddress(address int) + + // GetAddressOrDefault gets which address to use + GetAddressOrDefault(def int) int +} + +// NewConfig returns a new SPI Config. +func NewConfig() Config { + return &spiConfig{bus: BusNotInitialized, address: AddressNotInitialized} +} + +// WithBus sets preferred bus to use. +func (s *spiConfig) WithBus(bus int) { + s.bus = bus +} + +// GetBusOrDefault returns which bus to use, either the one set using WithBus(), +// or the default value which is passed in as the one param. +func (s *spiConfig) GetBusOrDefault(d int) int { + if s.bus == BusNotInitialized { + return d + } + return s.bus +} + +// WithBus sets which bus to use as a optional param. +func WithBus(bus int) func(Config) { + return func(s Config) { + s.WithBus(bus) + } +} + +// WithAddress sets which address to use. +func (s *spiConfig) WithAddress(address int) { + s.address = address +} + +// GetAddressOrDefault returns which address to use, either +// the one set using WithBus(), or the default value which +// is passed in as the param. +func (s *spiConfig) GetAddressOrDefault(a int) int { + if s.address == AddressNotInitialized { + return a + } + return s.address +} + +// WithAddress sets which address to use as a optional param. +func WithAddress(address int) func(Config) { + return func(s Config) { + s.WithAddress(address) + } +} diff --git a/examples/raspi_gopigo3.go b/examples/raspi_gopigo3.go new file mode 100644 index 000000000..99ac7e9b7 --- /dev/null +++ b/examples/raspi_gopigo3.go @@ -0,0 +1,32 @@ +// +build example +// +// Do not build by default. + +package main + +import ( + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/drivers/spi" + "gobot.io/x/gobot/platforms/raspi" +) + +func main() { + r := raspi.NewAdaptor() + g := spi.NewGoPiGo3Driver(r) + g.Start() + work := func() { + g.SetLED(spi.LED_EYE_LEFT, 255, 0, 0) + g.SetLED(spi.LED_EYE_RIGHT, 0, 255, 0) + time.Sleep(1 * time.Second) + g.SetLED(spi.LED_EYE_LEFT, 0, 255, 0) + g.SetLED(spi.LED_EYE_RIGHT, 255, 0, 0) + } + robot := gobot.NewRobot("gopigo3-bot", + []gobot.Connection{r}, + []gobot.Device{g}, + work, + ) + robot.Start() +} diff --git a/examples/raspi_ssd1306.go b/examples/raspi_ssd1306.go index c5fa5078e..da6c8ef2e 100644 --- a/examples/raspi_ssd1306.go +++ b/examples/raspi_ssd1306.go @@ -1,3 +1,7 @@ +// +build example +// +// Do not build by default. + package main import ( diff --git a/platforms/raspi/raspi_adaptor.go b/platforms/raspi/raspi_adaptor.go index 07004ed5f..e9cebed8f 100644 --- a/platforms/raspi/raspi_adaptor.go +++ b/platforms/raspi/raspi_adaptor.go @@ -12,7 +12,9 @@ import ( multierror "github.com/hashicorp/go-multierror" "gobot.io/x/gobot" "gobot.io/x/gobot/drivers/i2c" + "gobot.io/x/gobot/drivers/spi" "gobot.io/x/gobot/sysfs" + xspi "golang.org/x/exp/io/spi" ) var readFile = func() ([]byte, error) { @@ -21,13 +23,17 @@ var readFile = func() ([]byte, error) { // Adaptor is the Gobot Adaptor for the Raspberry Pi type Adaptor struct { - mutex *sync.Mutex - name string - revision string - digitalPins map[int]*sysfs.DigitalPin - pwmPins map[int]*PWMPin - i2cDefaultBus int - i2cBuses [2]i2c.I2cDevice + mutex *sync.Mutex + name string + revision string + digitalPins map[int]*sysfs.DigitalPin + pwmPins map[int]*PWMPin + i2cDefaultBus int + i2cBuses [2]i2c.I2cDevice + spiDefaultBus int + spiBuses [2]spi.SPIDevice + spiDefaultMode int + spiDefaultMaxSpeed int64 } // NewAdaptor creates a Raspi Adaptor @@ -44,6 +50,9 @@ func NewAdaptor() *Adaptor { s := strings.Split(string(v), " ") version, _ := strconv.ParseInt("0x"+s[len(s)-1], 0, 64) r.i2cDefaultBus = 1 + r.spiDefaultBus = 1 + r.spiDefaultMode = 0 + r.spiDefaultMaxSpeed = 500000 if version <= 3 { r.revision = "1" r.i2cDefaultBus = 0 @@ -106,6 +115,13 @@ func (r *Adaptor) Finalize() (err error) { } } } + for _, bus := range r.spiBuses { + if bus != nil { + if e := bus.Close(); e != nil { + err = multierror.Append(err, e) + } + } + } return } @@ -190,6 +206,66 @@ func (r *Adaptor) GetDefaultBus() int { return r.i2cDefaultBus } +// GetSpiConnection returns an spi connection to a device on a specified bus. +// Valid bus number is [0..1] which corresponds to /dev/spidev0.0 through /dev/spidev0.1. +func (r *Adaptor) GetSpiConnection(busNum, address, mode int, maxSpeed int64) (connection spi.Connection, err error) { + if (busNum < 0) || (busNum > 1) { + return nil, fmt.Errorf("Bus number %d out of range", busNum) + } + device, err := r.getSpiBus(busNum, address, mode, maxSpeed) + return spi.NewConnection(device, address), err +} + +func (r *Adaptor) getSpiBus(busNum, address, mode int, maxSpeed int64) (_ spi.SPIDevice, err error) { + r.mutex.Lock() + defer r.mutex.Unlock() + + if r.spiBuses[busNum] == nil { + var spiMode xspi.Mode + switch mode { + case 0: + spiMode = xspi.Mode0 + case 1: + spiMode = xspi.Mode1 + case 2: + spiMode = xspi.Mode2 + case 3: + spiMode = xspi.Mode3 + default: + spiMode = xspi.Mode0 + } + dev := fmt.Sprintf("/dev/spidev0.%d", busNum) + devfs := &xspi.Devfs{ + Dev: dev, + Mode: spiMode, + MaxSpeed: maxSpeed, + } + if r.spiBuses[busNum] == nil { + bus, err := xspi.Open(devfs) + if err != nil { + return nil, err + } + r.spiBuses[busNum] = spi.NewConnection(bus, address) + } + } + return r.spiBuses[busNum], err +} + +// GetSpiDefaultBus returns the default spi bus for this platform. +func (r *Adaptor) GetSpiDefaultBus() int { + return r.spiDefaultBus +} + +// GetSpiDefaultMode returns the default spi mode for this platform. +func (r *Adaptor) GetSpiDefaultMode() int { + return r.spiDefaultMode +} + +// GetDefaultMaxSpeed returns the default spi bus for this platform. +func (r *Adaptor) GetSpiDefaultMaxSpeed() int64 { + return r.spiDefaultMaxSpeed +} + // PWMPin returns a raspi.PWMPin which provides the sysfs.PWMPinner interface func (r *Adaptor) PWMPin(pin string) (raspiPWMPin sysfs.PWMPinner, err error) { i, err := r.translatePin(pin) diff --git a/platforms/raspi/raspi_adaptor_test.go b/platforms/raspi/raspi_adaptor_test.go index b92149880..a0067d6d5 100644 --- a/platforms/raspi/raspi_adaptor_test.go +++ b/platforms/raspi/raspi_adaptor_test.go @@ -8,6 +8,7 @@ import ( "gobot.io/x/gobot" "gobot.io/x/gobot/drivers/gpio" "gobot.io/x/gobot/drivers/i2c" + "gobot.io/x/gobot/drivers/spi" "gobot.io/x/gobot/gobottest" "gobot.io/x/gobot/sysfs" "runtime" @@ -24,6 +25,7 @@ var _ gpio.ServoWriter = (*Adaptor)(nil) var _ sysfs.DigitalPinnerProvider = (*Adaptor)(nil) var _ sysfs.PWMPinnerProvider = (*Adaptor)(nil) var _ i2c.Connector = (*Adaptor)(nil) +var _ spi.Connector = (*Adaptor)(nil) func initTestAdaptor() *Adaptor { readFile = func() ([]byte, error) { @@ -91,6 +93,8 @@ func TestAdaptorFinalize(t *testing.T) { "/dev/pi-blaster", "/dev/i2c-1", "/dev/i2c-0", + "/dev/spidev0.0", + "/dev/spidev0.1", }) sysfs.SetFilesystem(fs) @@ -178,6 +182,27 @@ func TestAdaptorI2c(t *testing.T) { gobottest.Assert(t, a.GetDefaultBus(), 1) } +func TestAdaptorSPI(t *testing.T) { + a := initTestAdaptor() + fs := sysfs.NewMockFilesystem([]string{ + "/dev/spidev0.1", + }) + sysfs.SetFilesystem(fs) + sysfs.SetSyscall(&sysfs.MockSyscall{}) + // TODO: find a better way to test this + _, err := a.GetSpiConnection(1, 0xC0, 0, 500000) + gobottest.Assert(t, err, err) + gobottest.Assert(t, a.GetSpiDefaultBus(), 1) + gobottest.Assert(t, a.GetSpiDefaultMode(), 0) + gobottest.Assert(t, a.GetSpiDefaultMaxSpeed(), int64(500000)) + + _, err = a.GetSpiConnection(1, 0xC0, 1, 500000) + _, err = a.GetSpiConnection(1, 0xC0, 2, 500000) + _, err = a.GetSpiConnection(1, 0xC0, 3, 500000) + _, err = a.GetSpiConnection(1, 0xC0, 5, 500000) + _, err = a.GetSpiConnection(4, 0xC0, 0, 500000) +} + func TestAdaptorDigitalPinConcurrency(t *testing.T) { oldProcs := runtime.GOMAXPROCS(0)