Unverified Commit f60b7831 authored by Earle F. Philhower, III's avatar Earle F. Philhower, III Committed by GitHub

Add SPISlave class (#1717)

Allows the Pico to behave as an SPI slave and allows apps to respond
with appropriate data through callbacks.

Fixes #1680
parent c48cdeee
......@@ -214,7 +214,7 @@ The installed tools include a version of OpenOCD (in the pqt-openocd directory)
* Analog stereo audio in using DMA and the built-in ADC
* Analog stereo audio out using PWM hardware
* USB drive mode for data loggers (SingleFileDrive)
* Peripherals: SPI master, Wire(I2C) master/slave, dual UART, emulated EEPROM, I2S audio input, I2S audio output, Servo
* Peripherals: SPI master/slave, Wire(I2C) master/slave, dual UART, emulated EEPROM, I2S audio input/output, Servo
* printf (i.e. debug) output over USB serial
The RP2040 PIO state machines (SMs) are used to generate jitter-free:
......
SPI (Serial Peripheral Interface)
=================================
SPI Master (Serial Peripheral Interface)
========================================
The RP2040 has two hardware SPI interfaces, ``spi0 (SPI)`` and ``spi1 (SPI1)``.
These interfaces are supported by the ``SPI`` library in master mode.
......@@ -20,5 +20,26 @@ The Arduino `SPI documentation <https://www.arduino.cc/en/reference/SPI>`_ gives
a detailed overview of the library, except for the following RP2040-specific
changes:
* ``SPI.begin(bool hwCS)`` can take an options ``hwCS`` parameter. By passing in ``true`` for ``hwCS`` the sketch does not need to worry about asserting and deasserting the ``CS`` pin between transactions. The default is ``false`` and requires the sketch to handle the CS pin itself, as is the standard way in Arduino.
* The interrupt calls (``usingInterrupt``, ``notUsingInterrupt``, ``attachInterrupt``, and ``detachInterrpt``) are not implemented.
* ``SPI.begin(bool hwCS)`` can take an options ``hwCS`` parameter.
By passing in ``true`` for ``hwCS`` the sketch does not need to worry
about asserting and deasserting the ``CS`` pin between transactions.
The default is ``false`` and requires the sketch to handle the CS
pin itself, as is the standard way in Arduino.
* The interrupt calls (``attachInterrupt``, and ``detachInterrpt``) are not implemented.
SPI Slave (SPISlave)
====================
Slave mode operation is also supported on either SPI interface. Two callbacks are
needed in your app, set through ``SPISlave.onDataRecv`` and ``SPISlave.onDataSent``,
in order to consunme the received data and provide data to transmit.
* The callbacks operate at IRQ time and may be called very frequently at high SPI frequencies. So, make then small, fast, and with no memory allocations or locking.
Examples
========
See the SPItoMyself example for a complete Master and Slave application.
......@@ -61,7 +61,7 @@ public:
void setDataMode(uint8_t uc_mode) __attribute__((deprecated));
void setClockDivider(uint8_t uc_div) __attribute__((deprecated));
// Unimplemented
// List of GPIO IRQs to disable during a transaction
virtual void usingInterrupt(int interruptNumber) override {
_usingIRQs.insert({interruptNumber, 0});
}
......
// Shows how to use SPISlave on a single device.
// Core0 runs as an SPI master and initiates a transmission to the slave
// Core1 runs the SPI Slave mode and provides a unique reply to messages from the master
//
// Released to the public domain 2023 by Earle F. Philhower, III <earlephilhower@yahoo.com>
#include <SPI.h>
#include <SPISlave.h>
// Wiring:
// Master RX GP0 <-> GP11 Slave TX
// Master CS GP1 <-> GP9 Slave CS
// Master CK GP2 <-> GP10 Slave CK
// Master TX GP3 <-> GP8 Slave RX
SPISettings spisettings(1000000, MSBFIRST, SPI_MODE0);
// Core 0 will be SPI master
void setup() {
SPI.setRX(0);
SPI.setCS(1);
SPI.setSCK(2);
SPI.setTX(3);
SPI.begin(true);
delay(5000);
}
int transmits = 0;
void loop() {
char msg[42];
memset(msg, 0, sizeof(msg));
sprintf(msg, "What's up? This is transmission %d", transmits);
Serial.printf("\n\nM-SEND: '%s'\n", msg);
SPI.beginTransaction(spisettings);
SPI.transfer(msg, sizeof(msg));
SPI.endTransaction();
Serial.printf("M-RECV: '%s'\n", msg);
transmits++;
delay(5000);
}
// Core 1 will be SPI slave
volatile bool recvBuffReady = false;
char recvBuff[42] = "";
int recvIdx = 0;
void recvCallback(uint8_t *data, size_t len) {
memcpy(recvBuff + recvIdx, data, len);
recvIdx += len;
if (recvIdx == sizeof(recvBuff)) {
recvBuffReady = true;
recvIdx = 0;
}
}
int sendcbs = 0;
// Note that the buffer needs to be long lived, the SPISlave doesn't copy it. So no local stack variables, only globals or heap(malloc/new) allocations.
char sendBuff[42];
void sentCallback() {
memset(sendBuff, 0, sizeof(sendBuff));
sprintf(sendBuff, "Slave to Master Xmission %d", sendcbs++);
SPISlave1.setData((uint8_t*)sendBuff, sizeof(sendBuff));
}
// Note that we use SPISlave1 here **not** because we're running on
// Core 1, but because SPI0 is being used already. You can use
// SPISlave or SPISlave1 on any core.
void setup1() {
SPISlave1.setRX(8);
SPISlave1.setCS(9);
SPISlave1.setSCK(10);
SPISlave1.setTX(11);
// Ensure we start with something to send...
sentCallback();
// Hook our callbacks into the slave
SPISlave1.onDataRecv(recvCallback);
SPISlave1.onDataSent(sentCallback);
SPISlave1.begin(spisettings);
delay(3000);
Serial.println("S-INFO: SPISlave started");
}
void loop1() {
if (recvBuffReady) {
Serial.printf("S-RECV: '%s'\n", recvBuff);
recvBuffReady = false;
}
}
#######################################
# Syntax Coloring Map SPI
#######################################
#######################################
# Instances (KEYWORD2)
#######################################
SPISlave KEYWORD1
SPISlave1 KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
end KEYWORD2
SPISettings KEYWORD2
setRX KEYWORD2
setTX KEYWORD2
setSCK KEYWORD2
setCS KEYWORD2
setData KEYWORD2
onDataRecv KEYWORD2
onDataSent KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
SPI_CLOCK_DIV4 LITERAL1
SPI_CLOCK_DIV16 LITERAL1
SPI_CLOCK_DIV64 LITERAL1
SPI_CLOCK_DIV128 LITERAL1
SPI_CLOCK_DIV2 LITERAL1
SPI_CLOCK_DIV8 LITERAL1
SPI_CLOCK_DIV32 LITERAL1
SPI_CLOCK_DIV64 LITERAL1
SPI_MODE0 LITERAL1
SPI_MODE1 LITERAL1
SPI_MODE2 LITERAL1
SPI_MODE3 LITERAL1
name=SPISlave
version=1.0
author=Earle F. Philhower, III <earlephilhower@yahoo.com>
maintainer=Earle F. Philhower, III <earlephilhower@yahoo.com>
sentence=Enables the communication as a slave SPI devices.
paragraph=
category=Signal Input/Output
url=https://github.com/earlephilhower/arduino-pico
architectures=rp2040
dot_a_linkage=true
/*
SPI Slave library for the Raspberry Pi Pico RP2040
Copyright (c) 2023 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "SPISlave.h"
#include <hardware/spi.h>
#include <hardware/gpio.h>
#include <hardware/structs/iobank0.h>
#include <hardware/irq.h>
#ifdef USE_TINYUSB
// For Serial when selecting TinyUSB. Can't include in the core because Arduino IDE
// will not link in libraries called from the core. Instead, add the header to all
// the standard libraries in the hope it will still catch some user cases where they
// use these libraries.
// See https://github.com/earlephilhower/arduino-pico/issues/167#issuecomment-848622174
#include <Adafruit_TinyUSB.h>
#endif
SPISlaveClass::SPISlaveClass(spi_inst_t *spi, pin_size_t rx, pin_size_t cs, pin_size_t sck, pin_size_t tx) {
_spi = spi;
_running = false;
_initted = false;
_spis = SPISettings();
_RX = rx;
_TX = tx;
_SCK = sck;
_CS = cs;
_recvCB = nullptr;
_sentCB = nullptr;
_dataOut = nullptr;
_dataLeft = 0;
}
inline spi_cpol_t SPISlaveClass::cpol(SPISettings _spis) {
switch (_spis.getDataMode()) {
case SPI_MODE0:
return SPI_CPOL_0;
case SPI_MODE1:
return SPI_CPOL_0;
case SPI_MODE2:
return SPI_CPOL_1;
case SPI_MODE3:
return SPI_CPOL_1;
}
// Error
return SPI_CPOL_0;
}
inline spi_cpha_t SPISlaveClass::cpha(SPISettings _spis) {
switch (_spis.getDataMode()) {
case SPI_MODE0:
return SPI_CPHA_0;
case SPI_MODE1:
return SPI_CPHA_1;
case SPI_MODE2:
return SPI_CPHA_0;
case SPI_MODE3:
return SPI_CPHA_1;
}
// Error
return SPI_CPHA_0;
}
bool SPISlaveClass::setRX(pin_size_t pin) {
constexpr uint32_t valid[2] = { __bitset({0, 4, 16, 20}) /* SPI0 */,
__bitset({8, 12, 24, 28}) /* SPI1 */
};
if ((!_running) && ((1 << pin) & valid[spi_get_index(_spi)])) {
_RX = pin;
return true;
}
if (_RX == pin) {
return true;
}
if (_running) {
panic("FATAL: Attempting to set SPI%s.RX while running", spi_get_index(_spi) ? "1" : "");
} else {
panic("FATAL: Attempting to set SPI%s.RX to illegal pin %d", spi_get_index(_spi) ? "1" : "", pin);
}
return false;
}
bool SPISlaveClass::setCS(pin_size_t pin) {
constexpr uint32_t valid[2] = { __bitset({1, 5, 17, 21}) /* SPI0 */,
__bitset({9, 13, 25, 29}) /* SPI1 */
};
if ((!_running) && ((1 << pin) & valid[spi_get_index(_spi)])) {
_CS = pin;
return true;
}
if (_CS == pin) {
return true;
}
if (_running) {
panic("FATAL: Attempting to set SPI%s.CS while running", spi_get_index(_spi) ? "1" : "");
} else {
panic("FATAL: Attempting to set SPI%s.CS to illegal pin %d", spi_get_index(_spi) ? "1" : "", pin);
}
return false;
}
bool SPISlaveClass::setSCK(pin_size_t pin) {
constexpr uint32_t valid[2] = { __bitset({2, 6, 18, 22}) /* SPI0 */,
__bitset({10, 14, 26}) /* SPI1 */
};
if ((!_running) && ((1 << pin) & valid[spi_get_index(_spi)])) {
_SCK = pin;
return true;
}
if (_SCK == pin) {
return true;
}
if (_running) {
panic("FATAL: Attempting to set SPI%s.SCK while running", spi_get_index(_spi) ? "1" : "");
} else {
panic("FATAL: Attempting to set SPI%s.SCK to illegal pin %d", spi_get_index(_spi) ? "1" : "", pin);
}
return false;
}
bool SPISlaveClass::setTX(pin_size_t pin) {
constexpr uint32_t valid[2] = { __bitset({3, 7, 19, 23}) /* SPI0 */,
__bitset({11, 15, 27}) /* SPI1 */
};
if ((!_running) && ((1 << pin) & valid[spi_get_index(_spi)])) {
_TX = pin;
return true;
}
if (_TX == pin) {
return true;
}
if (_running) {
panic("FATAL: Attempting to set SPI%s.TX while running", spi_get_index(_spi) ? "1" : "");
} else {
panic("FATAL: Attempting to set SPI%s.TX to illegal pin %d", spi_get_index(_spi) ? "1" : "", pin);
}
return false;
}
void SPISlaveClass::_handleIRQ() {
// Attempt to read out all RX FIFO datra and return to callback
uint8_t buff[8]; // SPI FIFO 8 deep max
int cnt;
for (cnt = 0; (cnt < 8) && spi_is_readable(_spi); cnt++) {
buff[cnt] = spi_get_hw(_spi)->dr;
}
if (cnt && _recvCB) {
_recvCB(buff, cnt);
}
// Attempt to send as many ytes to the TX FIFO as we have/are free
while (spi_is_writable(_spi)) {
for (; _dataLeft && spi_is_writable(_spi); _dataLeft--) {
spi_get_hw(_spi)->dr = *(_dataOut++);
}
if (!_dataLeft && _sentCB) {
_sentCB();
}
}
// Disable the TX FIFO IRQ if there is still no data to send or we'd always be stuck in an IRQ
// Will be re-enabled once user does a setData
if (!_dataLeft) {
spi_get_hw(_spi)->imsc = 2 | 4; // RTIM + RXIM
}
}
void SPISlaveClass::_irq0() {
SPISlave._handleIRQ();
}
void SPISlaveClass::_irq1() {
SPISlave1._handleIRQ();
}
void SPISlaveClass::setData(const uint8_t *data, size_t len) {
_dataOut = data;
_dataLeft = len;
if (_initted) {
spi_get_hw(_spi)->imsc = 2 | 4 | 8;
}
}
void SPISlaveClass::begin(SPISettings spis) {
DEBUGSPI("SPISlave::begin(%d), rx=%d, cs=%d, sck=%d, tx=%d\n", hwCS, _RX, _CS, _SCK, _TX);
gpio_set_function(_RX, GPIO_FUNC_SPI);
gpio_set_function(_CS, GPIO_FUNC_SPI);
gpio_set_function(_SCK, GPIO_FUNC_SPI);
gpio_set_function(_TX, GPIO_FUNC_SPI);
if (_initted) {
DEBUGSPI("SPISlave: deinitting currently active SPI\n");
spi_deinit(_spi);
}
DEBUGSPI("SPISlave: initting SPI\n");
spi_init(_spi, _spis.getClockFreq());
DEBUGSPI("SPISlave: actual baudrate=%u\n", spi_get_baudrate(_spi));
spi_set_slave(_spi, true);
spi_set_format(_spi, 8, cpol(spis), cpha(spis), SPI_MSB_FIRST);
// Install our IRQ handler
if (_spi == spi0) {
irq_set_exclusive_handler(SPI0_IRQ, _irq0);
} else {
irq_set_exclusive_handler(SPI1_IRQ, _irq1);
}
// Set to get IRQs on transmit and receive
spi_get_hw(_spi)->imsc = 2 | 4 | 8 ; // RTIM + RXIM + TXIM (not RORIM)
_initted = true;
irq_set_enabled(_spi == spi0 ? SPI0_IRQ : SPI1_IRQ, true);
}
void SPISlaveClass::end() {
DEBUGSPI("SPISlave::end()\n");
if (_initted) {
DEBUGSPI("SPISlave: deinitting currently active SPI\n");
if (_spi == spi0) {
irq_remove_handler(SPI0_IRQ, _irq0);
} else {
irq_remove_handler(SPI1_IRQ, _irq1);
}
spi_deinit(_spi);
_initted = false;
}
gpio_set_function(_RX, GPIO_FUNC_SIO);
gpio_set_function(_CS, GPIO_FUNC_SIO);
gpio_set_function(_SCK, GPIO_FUNC_SIO);
gpio_set_function(_TX, GPIO_FUNC_SIO);
}
#ifndef __SPI0_DEVICE
#define __SPI0_DEVICE spi0
#endif
#ifndef __SPI1_DEVICE
#define __SPI1_DEVICE spi1
#endif
SPISlaveClass SPISlave(__SPI0_DEVICE, PIN_SPI0_MISO, PIN_SPI0_SS, PIN_SPI0_SCK, PIN_SPI0_MOSI);
SPISlaveClass SPISlave1(__SPI1_DEVICE, PIN_SPI1_MISO, PIN_SPI1_SS, PIN_SPI1_SCK, PIN_SPI1_MOSI);
/*
SPI Slave library for the Raspberry Pi Pico RP2040
Copyright (c) 2023 Earle F. Philhower, III <earlephilhower@yahoo.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#pragma once
#include <Arduino.h>
#include <api/HardwareSPI.h>
#include <hardware/spi.h>
#include <functional>
typedef std::function<void(uint8_t *data, size_t len)> SPISlaveRecvHandler;
typedef std::function<void(void)> SPISlaveSentHandler;
class SPISlaveClass {
public:
SPISlaveClass(spi_inst_t *spi, pin_size_t rx, pin_size_t cs, pin_size_t sck, pin_size_t tx);
// Assign pins, call before begin()
bool setRX(pin_size_t pin);
bool setCS(pin_size_t pin);
bool setSCK(pin_size_t pin);
bool setTX(pin_size_t pin);
void begin(SPISettings spis);
void end();
// May be set before the initial begin(). If not, then as soon as
// begin() is called you will get a callback.
void setData(const uint8_t * data, size_t len);
inline void setData(const char * data) {
setData((const uint8_t *)data, strlen(data));
}
// NOTE: These two callbacks are called from an IRQ context
// NOTE: They should also be called before begin()
// Called when bytes are available to be read from the SPI slave reception buffer
void onDataRecv(SPISlaveRecvHandler cb) {
_recvCB = cb;
}
// Called when there is space in the SPI transmission buffer
void onDataSent(SPISlaveSentHandler cb) {
_sentCB = cb;
}
private:
// Naked IRQ callbacks, will thunk to real object ones below
static void _irq0();
static void _irq1();
public:
void _handleIRQ();
private:
spi_cpol_t cpol(SPISettings _spis);
spi_cpha_t cpha(SPISettings _spis);
uint8_t reverseByte(uint8_t b);
uint16_t reverse16Bit(uint16_t w);
void adjustBuffer(const void *s, void *d, size_t cnt, bool by16);
spi_inst_t *_spi;
SPISettings _spis;
pin_size_t _RX, _TX, _SCK, _CS;
bool _running; // SPI port active
bool _initted; // Transaction begun
SPISlaveRecvHandler _recvCB;
SPISlaveSentHandler _sentCB;
// The current data to be pumped into the transmit FIFO
const uint8_t *_dataOut;
size_t _dataLeft;
// Received data will be returned in small chunks directly from a local buffer in _handleIRQ()
};
extern SPISlaveClass SPISlave;
extern SPISlaveClass SPISlave1;
......@@ -13,7 +13,8 @@ for dir in ./cores/rp2040 ./libraries/EEPROM ./libraries/I2S ./libraries/SingleF
./libraries/JoystickBT ./libraries/KeyboardBT ./variants ./libraries/BTstackLib \
./libraries/MouseBT ./libraries/SerialBT ./libraries/HID_Bluetooth \
./libraries/JoystickBLE ./libraries/KeyboardBLE ./libraries/MouseBLE \
./libraries/lwIP_w5500 ./libraries/lwIP_w5100 ./libraries/lwIP_enc28j60; do
./libraries/lwIP_w5500 ./libraries/lwIP_w5100 ./libraries/lwIP_enc28j60 \
./libraries/SPISlave ; do
find $dir -type f \( -name "*.c" -o -name "*.h" -o -name "*.cpp" \) -a \! -path '*api*' -exec astyle --suffix=none --options=./tests/astyle_core.conf \{\} \;
find $dir -type f -name "*.ino" -exec astyle --suffix=none --options=./tests/astyle_examples.conf \{\} \;
done
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment