Skip to content

Commit

Permalink
Add PIO-based SoftwareSPI enabling SPI on any pins (#2778)
Browse files Browse the repository at this point in the history
* Add PIO-based SoftwareSPI enabling SPI on any pins

The Raspberry Pi team has a working PIO-based SPI interface.  Wrap it
to work like a hardware SPI interface, allowing SPI on any pin
combination.

Tested reading and writing an SD card using unmodified SD library.

* Add W5500 example

Good for testing, shows non-contiguous pin outs.
earlephilhower authored Jan 27, 2025
1 parent a426fbf commit acf81f4
Showing 11 changed files with 1,110 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -148,7 +148,7 @@ The RP2040 PIO state machines (SMs) are used to generate jitter-free:
* I2S Input
* I2S Output
* Software UARTs (Serial ports)

* Software SPIs

# Installing via Arduino Boards Manager
## Windows-specific Notes
13 changes: 13 additions & 0 deletions docs/spi.rst
Original file line number Diff line number Diff line change
@@ -28,6 +28,19 @@ pin itself, as is the standard way in Arduino.

* The interrupt calls (``attachInterrupt``, and ``detachInterrpt``) are not implemented.

Software SPI (Master Only)
==========================

Similar to ``SoftwareSerial``, ``SoftwareSPI`` creates a PIO based SPI interface that
can be used in the same manner as the hardware SPI devices. The constructor takes the
pins desired, which can be any GPIO pins with the rule that if hardware CS is used then
it must be on pin ``SCK + 1``. Construct a ``SoftwareSPI`` object in your code as
follows and use it as needed (i.e. pass it into ``SD.begin(_CS, softwareSPI);``

.. code:: cpp
#include <SoftwareSPI.h>
SoftwareSPI softSPI(_sck, _miso, _mosi); // no HW CS support, any selection of pins can be used
SPI Slave (SPISlave)
====================
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
SD card basic file example with Software SPI
This example shows how to create and destroy an SD card file
The circuit:
SD card attached to Pico as follows:
** SCK - GPIO0
** CS - GPIO1
** MISO (AKA RX) - GPIO2
** MOSI (AKA TX) - GPIO3
created Nov 2010
by David A. Mellis
modified 9 Apr 2012
by Tom Igoe
This example code is in the public domain.
*/

#include <SoftwareSPI.h>

const int _SCK = 0;
const int _CS = 1; // Must be SCK+1 for HW CS support
const int _MISO = 2;
const int _MOSI = 3;
SoftwareSPI softSPI(_SCK, _MISO, _MOSI, _CS);

#include <SD.h>

File myFile;

void setup() {
// Open serial communications and wait for port to open:
Serial.begin(115200);

do {
delay(100); // wait for serial port to connect. Needed for native USB port only
} while (!Serial);

Serial.print("Initializing SD card...");

bool sdInitialized = false;
sdInitialized = SD.begin(_CS, softSPI);
if (!sdInitialized) {
Serial.println("initialization failed!");
return;
}
Serial.println("initialization done.");

if (SD.exists("example.txt")) {
Serial.println("example.txt exists.");
} else {
Serial.println("example.txt doesn't exist.");
}

// open a new file and immediately close it:
Serial.println("Creating example.txt...");
myFile = SD.open("example.txt", FILE_WRITE);
myFile.close();

// Check to see if the file exists:
if (SD.exists("example.txt")) {
Serial.println("example.txt exists.");
} else {
Serial.println("example.txt doesn't exist.");
}

// delete the file:
Serial.println("Removing example.txt...");
SD.remove("example.txt");

if (SD.exists("example.txt")) {
Serial.println("example.txt exists.");
} else {
Serial.println("example.txt doesn't exist.");
}
}

void loop() {
// nothing happens after setup finishes.
}



Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
This sketch establishes a TCP connection to a "quote of the day" service.
It sends a "hello" message, and then prints received data.
*/

#include <W5500lwIP.h>

const char* host = "djxmmx.net";
const uint16_t port = 17;

#include <SoftwareSPI.h>
const int _SCK = 0; // Any pin allowed
const int _CS = 1; // Must be SCK+1 for HW CS support
const int _MISO = 28; // Note that MOSI and MISO don't need to be contiguous. Any pins allowed
const int _MOSI = 3; // Any pin not used elsewhere
const int _INT = 4; // W5500 IRQ line

SoftwareSPI softSPI(_SCK, _MISO, _MOSI, _CS);

Wiznet5500lwIP eth(_CS, softSPI, _INT);

void setup() {
Serial.begin(115200);
delay(5000);
Serial.println();
Serial.println();
Serial.println("Starting Ethernet port");

// Start the Ethernet port
if (!eth.begin()) {
Serial.println("No wired Ethernet hardware detected. Check pinouts, wiring.");
while (1) {
delay(1000);
}
}

while (!eth.connected()) {
Serial.print(".");
delay(500);
}

Serial.println("");
Serial.println("Ethernet connected");
Serial.println("IP address: ");
Serial.println(eth.localIP());
}

void loop() {
static bool wait = false;

Serial.print("connecting to ");
Serial.print(host);
Serial.print(':');
Serial.println(port);

// Use WiFiClient class to create TCP connections
WiFiClient client;
if (!client.connect(host, port)) {
Serial.println("connection failed");
delay(5000);
return;
}

// This will send a string to the server
Serial.println("sending data to server");
if (client.connected()) {
client.println("hello from RP2040");
}

// wait for data to be available
unsigned long timeout = millis();
while (client.available() == 0) {
if (millis() - timeout > 5000) {
Serial.println(">>> Client Timeout !");
client.stop();
delay(60000);
return;
}
}

// Read all the lines of the reply from server and print them to Serial
Serial.println("receiving from remote server");
// not testing 'client.connected()' since we do not need to send data here
while (client.available()) {
char ch = static_cast<char>(client.read());
Serial.print(ch);
}

// Close the connection
Serial.println();
Serial.println("closing connection");
client.stop();

if (wait) {
delay(300000); // execute once every 5 minutes, don't flood remote service
}
wait = true;
}
43 changes: 43 additions & 0 deletions libraries/SoftwareSPI/keywords.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#######################################
# Syntax Coloring Map SPI
#######################################

#######################################
# Instances (KEYWORD2)
#######################################

SoftwareSPI KEYWORD1

#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
end KEYWORD2
beginTransaction KEYWORD2
endTransaction KEYWORD2
SPISettings KEYWORD2
transfer KEYWORD2
transfer16 KEYWORD2
setBitOrder KEYWORD2
setDataMode KEYWORD2
setClockDivider KEYWORD2
setSCK KEYWORD2
setMOSI KEYWORD2
setMISO KEYWORD2
setCS 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
10 changes: 10 additions & 0 deletions libraries/SoftwareSPI/library.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name=SoftwareSPI
version=1.0
author=Earle F. Philhower, III <earlephilhower@yahoo.com>
maintainer=Earle F. Philhower, III <earlephilhower@yahoo.com>
sentence=Uses the PIO to provide an SPI interface on any pin.
paragraph=
category=Signal Input/Output
url=http://arduino.cc/en/Reference/SPI
architectures=rp2040
dot_a_linkage=true
365 changes: 365 additions & 0 deletions libraries/SoftwareSPI/src/SoftwareSPI.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,365 @@
/*
PIO-based SPI Master library for the Raspberry Pi Pico RP2040
Copyright (c) 2025 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 "SoftwareSPI.h"
#include <hardware/gpio.h>
#include <hardware/structs/iobank0.h>
#include <hardware/irq.h>
#include "spi.pio.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

SoftwareSPI::SoftwareSPI(pin_size_t sck, pin_size_t miso, pin_size_t mosi, pin_size_t cs) {
_running = false;
_initted = false;
_spis = SPISettings(1, LSBFIRST, SPI_MODE0); // Ensure spi_init called by setting current freq to 0
_sck = sck;
_miso = miso;
_mosi = mosi;
_cs = cs;
}

inline spi_cpol_t SoftwareSPI::cpol() {
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 SoftwareSPI::cpha() {
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;
}

inline uint8_t SoftwareSPI::reverseByte(uint8_t b) {
b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
return b;
}

inline uint16_t SoftwareSPI::reverse16Bit(uint16_t w) {
return (reverseByte(w & 0xff) << 8) | (reverseByte(w >> 8));
}

// The HW can't do LSB first, only MSB first, so need to bitreverse
void SoftwareSPI::adjustBuffer(const void *s, void *d, size_t cnt, bool by16) {
if (_spis.getBitOrder() == MSBFIRST) {
memcpy(d, s, cnt * (by16 ? 2 : 1));
} else if (!by16) {
const uint8_t *src = (const uint8_t *)s;
uint8_t *dst = (uint8_t *)d;
for (size_t i = 0; i < cnt; i++) {
*(dst++) = reverseByte(*(src++));
}
} else { /* by16 */
const uint16_t *src = (const uint16_t *)s;
uint16_t *dst = (uint16_t *)d;
for (size_t i = 0; i < cnt; i++) {
*(dst++) = reverse16Bit(*(src++));
}
}
}

void SoftwareSPI::_adjustPIO(int bits) {
if (_bits == bits) {
return; // Nothing to do!
}
// Manually set the shiftctl and possibly Y for 8 bits
pio_sm_set_enabled(_pio, _sm, false);
uint32_t v = _pio->sm[_sm].shiftctrl ;
v &= ~0x3e000000 | ~0x01f00000;
if (bits == 8) {
v |= 0x108 << 20; // Hardcode push/pull threshold 0'b0100001000, there is no simple accessor I can find
} else {
v |= 0x210 << 20; // 0b'1000010000
}
_pio->sm[_sm].shiftctrl = v;
if (_hwCS) {
pio_sm_exec(_pio, _sm, pio_encode_set(pio_x, bits - 2));
pio_sm_exec(_pio, _sm, pio_encode_set(pio_y, bits - 2));
}
pio_sm_set_enabled(_pio, _sm, true);
_bits = bits;
}

byte SoftwareSPI::transfer(uint8_t data) {
uint8_t ret;
if (!_initted) {
return 0;
}
data = (_spis.getBitOrder() == MSBFIRST) ? data : reverseByte(data);
DEBUGSPI("SPI::transfer(%02x), cpol=%d, cpha=%d\n", data, cpol(), cpha());
_adjustPIO(8);
io_rw_8 *txfifo = (io_rw_8 *) &_pio->txf[_sm];
io_rw_8 *rxfifo = (io_rw_8 *) &_pio->rxf[_sm];
while (pio_sm_is_tx_fifo_full(_pio, _sm)) { /* noop wait */ }
*txfifo = data;
while (pio_sm_is_rx_fifo_empty(_pio, _sm)) { /* noop wait for in data */ }
ret = *rxfifo;
ret = (_spis.getBitOrder() == MSBFIRST) ? ret : reverseByte(ret);
DEBUGSPI("SPI: read back %02x\n", ret);
return ret;
}

uint16_t SoftwareSPI::transfer16(uint16_t data) {
uint16_t ret;
if (!_initted) {
return 0;
}
data = (_spis.getBitOrder() == MSBFIRST) ? data : reverse16Bit(data);
DEBUGSPI("SPI::transfer16(%04x), cpol=%d, cpha=%d\n", data, cpol(), cpha());
_adjustPIO(16);
io_rw_16 *txfifo = (io_rw_16 *) &_pio->txf[_sm];
io_rw_16 *rxfifo = (io_rw_16 *) &_pio->rxf[_sm];
while (pio_sm_is_tx_fifo_full(_pio, _sm)) { /* noop wait */ }
*txfifo = data;
while (pio_sm_is_rx_fifo_empty(_pio, _sm)) { /* noop wait for in data */ }
ret = *rxfifo;
ret = (_spis.getBitOrder() == MSBFIRST) ? ret : reverse16Bit(ret);
DEBUGSPI("SPI: read back %04x\n", ret);
return ret;
}

void SoftwareSPI::transfer(void *buf, size_t count) {
transfer(buf, buf, count);
}

void SoftwareSPI::transfer(const void *csrc, void *cdest, size_t count) {
if (!_initted) {
return;
}
DEBUGSPI("SPI::transfer(%p, %p %d)\n", csrc, cdest, count);
const uint8_t *src = reinterpret_cast<const uint8_t *>(csrc);
uint8_t *dest = reinterpret_cast<uint8_t *>(cdest);
_adjustPIO(8);
io_rw_8 *txfifo = (io_rw_8 *) &_pio->txf[_sm];
io_rw_8 *rxfifo = (io_rw_8 *) &_pio->rxf[_sm];
int txleft = count;
int rxleft = count;

if (_spis.getBitOrder() == !MSBFIRST) {
// We're going to hack like heck here and reverse the txbuf into the receive buff (because txbuff is const
// Then by construction SPI will send before it received, we can use the rx buff to trans and recv
for (size_t i = 0; i < count; i++) {
dest[i] = reverseByte(src[i]);
}
src = dest; // We'll transmit the flipped data...
}

while (txleft || rxleft) {
while (txleft && !pio_sm_is_tx_fifo_full(_pio, _sm)) {
*txfifo = *src++;
txleft--;
}
while (rxleft && !pio_sm_is_rx_fifo_empty(_pio, _sm)) {
*dest++ = *rxfifo;
rxleft--;
}
}

if (_spis.getBitOrder() == !MSBFIRST) {
// Now we have data in recv but also need to flip it before returning to the app
for (size_t i = 0; i < count; i++) {
dest[i] = reverseByte(dest[i]);
}
}
DEBUGSPI("SPI::transfer completed\n");
}

#ifdef PICO_RP2350B
#define GPIOIRQREGS 6
#else
#define GPIOIRQREGS 4
#endif

void SoftwareSPI::beginTransaction(SPISettings settings) {
noInterrupts(); // Avoid possible race conditions if IRQ comes in while main app is in middle of this
DEBUGSPI("SPI::beginTransaction(clk=%lu, bo=%s)\n", settings.getClockFreq(), (settings.getBitOrder() == MSBFIRST) ? "MSB" : "LSB");
if (_initted && settings == _spis) {
DEBUGSPI("SPI: Reusing existing initted SPI\n");
} else {
/* Only de-init if the clock changes frequency */
if (settings.getClockFreq() != _spis.getClockFreq()) {
DEBUGSPI("SPI: initting SPI\n");
float divider = (float)rp2040.f_cpu() / (float)settings.getClockFreq();
divider /= _hwCS ? 4.0f : 4.0f;
pio_sm_set_clkdiv(_pio, _sm, divider);
DEBUGSPI("SPI: divider=%f\n", divider);
}
_spis = settings;
// Note we can only change frequency, not CPOL/CPHA (which would be physically not too useful anyway)
_initted = true;
}
// Disable any IRQs that are being used for SPI
io_bank0_irq_ctrl_hw_t *irq_ctrl_base = get_core_num() ? &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
DEBUGSPI("SPI: IRQ masks before = %08x %08x %08x %08x %08x %08x\n", (unsigned)irq_ctrl_base->inte[0],
(unsigned)irq_ctrl_base->inte[1], (unsigned)irq_ctrl_base->inte[2], (unsigned)irq_ctrl_base->inte[3],
(GPIOIRQREGS > 4) ? (unsigned)irq_ctrl_base->inte[4] : 0, (GPIOIRQREGS > 5) ? (unsigned)irq_ctrl_base->inte[5] : 0);
for (auto entry : _usingIRQs) {
int gpio = entry.first;

// There is no gpio_get_irq, so manually twiddle the register
io_rw_32 *en_reg = &irq_ctrl_base->inte[gpio / 8];
uint32_t val = ((*en_reg) >> (4 * (gpio % 8))) & 0xf;
_usingIRQs.insert_or_assign(gpio, val);
DEBUGSPI("SPI: GPIO %d = %lu\n", gpio, val);
(*en_reg) ^= val << (4 * (gpio % 8));
}
DEBUGSPI("SPI: IRQ masks after = %08x %08x %08x %08x %08x %08x\n", (unsigned)irq_ctrl_base->inte[0],
(unsigned)irq_ctrl_base->inte[1], (unsigned)irq_ctrl_base->inte[2], (unsigned)irq_ctrl_base->inte[3],
(GPIOIRQREGS > 4) ? (unsigned)irq_ctrl_base->inte[4] : 0, (GPIOIRQREGS > 5) ? (unsigned)irq_ctrl_base->inte[5] : 0);
interrupts();
}

void SoftwareSPI::endTransaction(void) {
noInterrupts(); // Avoid race condition so the GPIO IRQs won't come back until all state is restored
DEBUGSPI("SPI::endTransaction()\n");
// Re-enable IRQs
for (auto entry : _usingIRQs) {
int gpio = entry.first;
int mode = entry.second;
gpio_set_irq_enabled(gpio, mode, true);
}
io_bank0_irq_ctrl_hw_t *irq_ctrl_base = get_core_num() ? &iobank0_hw->proc1_irq_ctrl : &iobank0_hw->proc0_irq_ctrl;
(void) irq_ctrl_base;
DEBUGSPI("SPI: IRQ masks = %08x %08x %08x %08x %08x %08x\n", (unsigned)irq_ctrl_base->inte[0], (unsigned)irq_ctrl_base->inte[1],
(unsigned)irq_ctrl_base->inte[2], (unsigned)irq_ctrl_base->inte[3], (GPIOIRQREGS > 4) ? (unsigned)irq_ctrl_base->inte[4] : 0,
(GPIOIRQREGS > 5) ? (unsigned)irq_ctrl_base->inte[5] : 0);
interrupts();
}

bool SoftwareSPI::setCS(pin_size_t pin) {
if (pin < 1) {
// CS is SCK+1, so has to be at least GPIO1
return false;
}
if (!_running || (_cs == pin)) {
_cs = pin;
_sck = _cs - 1;
return true;
}
return false;
}

bool SoftwareSPI::setSCK(pin_size_t pin) {
if (!_running || (_sck == pin)) {
_sck = pin;
_cs = pin + 1;
return true;
}
return false;
}

bool SoftwareSPI::setMISO(pin_size_t pin) {
if (!_running || (_miso == pin)) {
_miso = pin;
return true;
}
return false;
}

bool SoftwareSPI::setMOSI(pin_size_t pin) {
if (!_running || (_mosi == pin)) {
_mosi = pin;
return true;
}
return false;
}

void SoftwareSPI::begin(bool hwCS) {
DEBUGSPI("SPI::begin(%d), rx=%d, cs=%d, sck=%d, tx=%d\n", hwCS, _miso, _cs, _sck, _mosi);
float divider = (float)rp2040.f_cpu() / (float)_spis.getClockFreq();
DEBUGSPI("SPI: divider=%f\n", divider);
if (!hwCS) {
_spi = new PIOProgram(cpha() == SPI_CPHA_0 ? &spi_cpha0_program : &spi_cpha1_program);
if (!_spi->prepare(&_pio, &_sm, &_off, _sck, 1)) {
_running = false;
delete _spi;
_spi = nullptr;
return;
}
pio_spi_init(_pio, _sm, _off, 8, divider / 4.0f, cpha(), cpol(), _sck, _mosi, _miso);
} else {
_spi = new PIOProgram(cpha() == SPI_CPHA_0 ? &spi_cpha0_cs_program : &spi_cpha1_cs_program);
if (!_spi->prepare(&_pio, &_sm, &_off, _sck, 2)) {
_running = false;
delete _spi;
_spi = nullptr;
return;
}
pio_spi_cs_init(_pio, _sm, _off, 8, divider / 4.0f, cpha(), cpol(), _sck, _mosi, _miso);
}
_hwCS = hwCS;
_bits = 8;
// Give a default config in case user doesn't use beginTransaction
beginTransaction(_spis);
endTransaction();
}

void SoftwareSPI::end() {
DEBUGSPI("SPI::end()\n");
if (_initted) {
DEBUGSPI("SPI: deinitting currently active SPI\n");
_initted = false;
}
_spis = SPISettings(0, LSBFIRST, SPI_MODE0);
}

void SoftwareSPI::setBitOrder(BitOrder order) {
_spis = SPISettings(_spis.getClockFreq(), order, _spis.getDataMode());
beginTransaction(_spis);
endTransaction();
}

void SoftwareSPI::setDataMode(uint8_t uc_mode) {
_spis = SPISettings(_spis.getClockFreq(), _spis.getBitOrder(), uc_mode);
beginTransaction(_spis);
endTransaction();
}

void SoftwareSPI::setClockDivider(uint8_t uc_div) {
(void) uc_div; // no-op
}
109 changes: 109 additions & 0 deletions libraries/SoftwareSPI/src/SoftwareSPI.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
PIO-based SPI Master library for the Raspberry Pi Pico RP2040
Copyright (c) 2025 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 <map>

class SoftwareSPI : public arduino::HardwareSPI {
public:
/**
@brief Create a PIO-based SPI instance
@param [in] sck SCK GPIO
@param [in] miso MISO GPIO
@param [in] mosi MOSI GPIO
@param [in] cs Optional CS pin for HW CS, must be SCK+1
*/
SoftwareSPI(pin_size_t sck, pin_size_t miso, pin_size_t mosi, pin_size_t cs = -1);

// Send or receive 8- or 16-bit data. Returns read back value
byte transfer(uint8_t data) override;
uint16_t transfer16(uint16_t data) override;

// Sends buffer in 8 bit chunks. Overwrites buffer with read data
void transfer(void *buf, size_t count) override;

// Sends one buffer and receives into another, much faster! can set rx or txbuf to nullptr
void transfer(const void *txbuf, void *rxbuf, size_t count) override;

// Call before/after every complete transaction
void beginTransaction(SPISettings settings) override;
void endTransaction(void) override;

// Assign pins, call before begin()
bool setMISO(pin_size_t pin);
inline bool setRX(pin_size_t pin) {
return setMISO(pin);
}
bool setCS(pin_size_t pin);
bool setSCK(pin_size_t pin);
bool setMOSI(pin_size_t pin);
inline bool setTX(pin_size_t pin) {
return setMOSI(pin);
}

// Call once to init/deinit SPI class, select pins, etc.
virtual void begin() override {
begin(false);
}
void begin(bool hwCS);
void end() override;

// Deprecated - do not use!
void setBitOrder(BitOrder order) __attribute__((deprecated));
void setDataMode(uint8_t uc_mode) __attribute__((deprecated));
void setClockDivider(uint8_t uc_div) __attribute__((deprecated));

// List of GPIO IRQs to disable during a transaction
virtual void usingInterrupt(int interruptNumber) override {
_usingIRQs.insert({interruptNumber, 0});
}
virtual void notUsingInterrupt(int interruptNumber) override {
_usingIRQs.erase(interruptNumber);
}
virtual void attachInterrupt() override { /* noop */ }
virtual void detachInterrupt() override { /* noop */ }

private:
spi_cpol_t cpol();
spi_cpha_t cpha();
uint8_t reverseByte(uint8_t b);
uint16_t reverse16Bit(uint16_t w);
void adjustBuffer(const void *s, void *d, size_t cnt, bool by16);
void _adjustPIO(int bits);

PIOProgram *_spi;
PIO _pio;
int _sm;
int _off;

SPISettings _spis;
pin_size_t _sck, _miso, _mosi, _cs;
bool _hwCS;
bool _running; // SPI port active
bool _initted; // Transaction begun
int _bits;

std::map<int, int> _usingIRQs;
};
168 changes: 168 additions & 0 deletions libraries/SoftwareSPI/src/spi.pio
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
;
; Copyright (c) 2020 Raspberry Pi (Trading) Ltd.
;
; SPDX-License-Identifier: BSD-3-Clause
;

; These programs implement full-duplex SPI, with a SCK period of 4 clock
; cycles. A different program is provided for each value of CPHA, and CPOL is
; achieved using the hardware GPIO inversion available in the IO controls.
;
; Transmit-only SPI can go twice as fast -- see the ST7789 example!
.pio_version 0 // only requires PIO version 0

.program spi_cpha0
.side_set 1

; Pin assignments:
; - SCK is side-set pin 0
; - MOSI is OUT pin 0
; - MISO is IN pin 0
;
; Autopush and autopull must be enabled, and the serial frame size is set by
; configuring the push/pull threshold. Shift left/right is fine, but you must
; justify the data yourself. This is done most conveniently for frame sizes of
; 8 or 16 bits by using the narrow store replication and narrow load byte
; picking behaviour of RP2040's IO fabric.

; Clock phase = 0: data is captured on the leading edge of each SCK pulse, and
; transitions on the trailing edge, or some time before the first leading edge.

out pins, 1 side 0 [1] ; Stall here on empty (sideset proceeds even if
in pins, 1 side 1 [1] ; instruction stalls, so we stall with SCK low)

.program spi_cpha1
.side_set 1

; Clock phase = 1: data transitions on the leading edge of each SCK pulse, and
; is captured on the trailing edge.

out x, 1 side 0 ; Stall here on empty (keep SCK deasserted)
mov pins, x side 1 [1] ; Output data, assert SCK (mov pins uses OUT mapping)
in pins, 1 side 0 ; Input data, deassert SCK

% c-sdk {
#include "hardware/gpio.h"
static inline void pio_spi_init(PIO pio, uint sm, uint prog_offs, uint n_bits,
float clkdiv, bool cpha, bool cpol, uint pin_sck, uint pin_mosi, uint pin_miso) {
pio_sm_config c = cpha ? spi_cpha1_program_get_default_config(prog_offs) : spi_cpha0_program_get_default_config(prog_offs);
sm_config_set_out_pins(&c, pin_mosi, 1);
sm_config_set_in_pins(&c, pin_miso);
sm_config_set_sideset_pins(&c, pin_sck);
// Only support MSB-first in this example code (shift to left, auto push/pull, threshold=nbits)
sm_config_set_out_shift(&c, false, true, n_bits);
sm_config_set_in_shift(&c, false, true, n_bits);
sm_config_set_clkdiv(&c, clkdiv);

// MOSI, SCK output are low, MISO is input
pio_sm_set_pins_with_mask(pio, sm, 0, (1u << pin_sck) | (1u << pin_mosi));
pio_sm_set_pindirs_with_mask(pio, sm, (1u << pin_sck) | (1u << pin_mosi), (1u << pin_sck) | (1u << pin_mosi) | (1u << pin_miso));
pio_gpio_init(pio, pin_mosi);
pio_gpio_init(pio, pin_miso);
pio_gpio_init(pio, pin_sck);

// The pin muxes can be configured to invert the output (among other things
// and this is a cheesy way to get CPOL=1
gpio_set_outover(pin_sck, cpol ? GPIO_OVERRIDE_INVERT : GPIO_OVERRIDE_NORMAL);
// SPI is synchronous, so bypass input synchroniser to reduce input delay.
hw_set_bits(&pio->input_sync_bypass, 1u << pin_miso);

pio_sm_init(pio, sm, prog_offs, &c);
pio_sm_set_enabled(pio, sm, true);
}
%}

; SPI with Chip Select
; -----------------------------------------------------------------------------
;
; For your amusement, here are some SPI programs with an automatic chip select
; (asserted once data appears in TX FIFO, deasserts when FIFO bottoms out, has
; a nice front/back porch).
;
; The number of bits per FIFO entry is configured via the Y register
; and the autopush/pull threshold. From 2 to 32 bits.
;
; Pin assignments:
; - SCK is side-set bit 0
; - CSn is side-set bit 1
; - MOSI is OUT bit 0 (host-to-device)
; - MISO is IN bit 0 (device-to-host)
;
; This program only supports one chip select -- use GPIO if more are needed
;
; Provide a variation for each possibility of CPHA; for CPOL we can just
; invert SCK in the IO muxing controls (downstream from PIO)


; CPHA=0: data is captured on the leading edge of each SCK pulse (including
; the first pulse), and transitions on the trailing edge

.program spi_cpha0_cs
.side_set 2

.wrap_target
bitloop:
out pins, 1 side 0x0 [1]
in pins, 1 side 0x1
jmp x-- bitloop side 0x1

out pins, 1 side 0x0
mov x, y side 0x0 ; Reload bit counter from Y
in pins, 1 side 0x1
jmp !osre bitloop side 0x1 ; Fall-through if TXF empties

nop side 0x0 [1] ; CSn back porch
public entry_point: ; Must set X,Y to n-2 before starting!
pull ifempty side 0x2 [1] ; Block with CSn high (minimum 2 cycles)
.wrap ; Note ifempty to avoid time-of-check race

; CPHA=1: data transitions on the leading edge of each SCK pulse, and is
; captured on the trailing edge

.program spi_cpha1_cs
.side_set 2

.wrap_target
bitloop:
out pins, 1 side 0x1 [1]
in pins, 1 side 0x0
jmp x-- bitloop side 0x0

out pins, 1 side 0x1
mov x, y side 0x1
in pins, 1 side 0x0
jmp !osre bitloop side 0x0

public entry_point: ; Must set X,Y to n-2 before starting!
pull ifempty side 0x2 [1] ; Block with CSn high (minimum 2 cycles)
nop side 0x0 [1]; CSn front porch
.wrap

% c-sdk {
#include "hardware/gpio.h"
static inline void pio_spi_cs_init(PIO pio, uint sm, uint prog_offs, uint n_bits, float clkdiv, bool cpha, bool cpol,
uint pin_sck, uint pin_mosi, uint pin_miso) {
pio_sm_config c = cpha ? spi_cpha1_cs_program_get_default_config(prog_offs) : spi_cpha0_cs_program_get_default_config(prog_offs);
sm_config_set_out_pins(&c, pin_mosi, 1);
sm_config_set_in_pins(&c, pin_miso);
sm_config_set_sideset_pins(&c, pin_sck);
sm_config_set_out_shift(&c, false, true, n_bits);
sm_config_set_in_shift(&c, false, true, n_bits);
sm_config_set_clkdiv(&c, clkdiv);

pio_sm_set_pins_with_mask(pio, sm, (2u << pin_sck), (3u << pin_sck) | (1u << pin_mosi));
pio_sm_set_pindirs_with_mask(pio, sm, (3u << pin_sck) | (1u << pin_mosi), (3u << pin_sck) | (1u << pin_mosi) | (1u << pin_miso));
pio_gpio_init(pio, pin_mosi);
pio_gpio_init(pio, pin_miso);
pio_gpio_init(pio, pin_sck);
pio_gpio_init(pio, pin_sck + 1);
gpio_set_outover(pin_sck, cpol ? GPIO_OVERRIDE_INVERT : GPIO_OVERRIDE_NORMAL);
hw_set_bits(&pio->input_sync_bypass, 1u << pin_miso);

uint entry_point = prog_offs + (cpha ? spi_cpha1_cs_offset_entry_point : spi_cpha0_cs_offset_entry_point);
pio_sm_init(pio, sm, entry_point, &c);
pio_sm_exec(pio, sm, pio_encode_set(pio_x, n_bits - 2));
pio_sm_exec(pio, sm, pio_encode_set(pio_y, n_bits - 2));
pio_sm_set_enabled(pio, sm, true);
}
%}
218 changes: 218 additions & 0 deletions libraries/SoftwareSPI/src/spi.pio.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// -------------------------------------------------- //
// This file is autogenerated by pioasm; do not edit! //
// -------------------------------------------------- //

#pragma once

#if !PICO_NO_HARDWARE
#include "hardware/pio.h"
#endif

// --------- //
// spi_cpha0 //
// --------- //

#define spi_cpha0_wrap_target 0
#define spi_cpha0_wrap 1
#define spi_cpha0_pio_version 0

static const uint16_t spi_cpha0_program_instructions[] = {
// .wrap_target
0x6101, // 0: out pins, 1 side 0 [1]
0x5101, // 1: in pins, 1 side 1 [1]
// .wrap
};

#if !PICO_NO_HARDWARE
static const struct pio_program spi_cpha0_program = {
.instructions = spi_cpha0_program_instructions,
.length = 2,
.origin = -1,
.pio_version = 0,
#if PICO_PIO_VERSION > 0
.used_gpio_ranges = 0x0
#endif
};

static inline pio_sm_config spi_cpha0_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + spi_cpha0_wrap_target, offset + spi_cpha0_wrap);
sm_config_set_sideset(&c, 1, false, false);
return c;
}
#endif

// --------- //
// spi_cpha1 //
// --------- //

#define spi_cpha1_wrap_target 0
#define spi_cpha1_wrap 2
#define spi_cpha1_pio_version 0

static const uint16_t spi_cpha1_program_instructions[] = {
// .wrap_target
0x6021, // 0: out x, 1 side 0
0xb101, // 1: mov pins, x side 1 [1]
0x4001, // 2: in pins, 1 side 0
// .wrap
};

#if !PICO_NO_HARDWARE
static const struct pio_program spi_cpha1_program = {
.instructions = spi_cpha1_program_instructions,
.length = 3,
.origin = -1,
.pio_version = 0,
#if PICO_PIO_VERSION > 0
.used_gpio_ranges = 0x0
#endif
};

static inline pio_sm_config spi_cpha1_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + spi_cpha1_wrap_target, offset + spi_cpha1_wrap);
sm_config_set_sideset(&c, 1, false, false);
return c;
}

#include "hardware/gpio.h"
static inline void pio_spi_init(PIO pio, uint sm, uint prog_offs, uint n_bits,
float clkdiv, bool cpha, bool cpol, uint pin_sck, uint pin_mosi, uint pin_miso) {
pio_sm_config c = cpha ? spi_cpha1_program_get_default_config(prog_offs) : spi_cpha0_program_get_default_config(prog_offs);
sm_config_set_out_pins(&c, pin_mosi, 1);
sm_config_set_in_pins(&c, pin_miso);
sm_config_set_sideset_pins(&c, pin_sck);
// Only support MSB-first in this example code (shift to left, auto push/pull, threshold=nbits)
sm_config_set_out_shift(&c, false, true, n_bits);
sm_config_set_in_shift(&c, false, true, n_bits);
sm_config_set_clkdiv(&c, clkdiv);
// MOSI, SCK output are low, MISO is input
pio_sm_set_pins_with_mask(pio, sm, 0, (1u << pin_sck) | (1u << pin_mosi));
pio_sm_set_pindirs_with_mask(pio, sm, (1u << pin_sck) | (1u << pin_mosi), (1u << pin_sck) | (1u << pin_mosi) | (1u << pin_miso));
pio_gpio_init(pio, pin_mosi);
pio_gpio_init(pio, pin_miso);
pio_gpio_init(pio, pin_sck);
// The pin muxes can be configured to invert the output (among other things
// and this is a cheesy way to get CPOL=1
gpio_set_outover(pin_sck, cpol ? GPIO_OVERRIDE_INVERT : GPIO_OVERRIDE_NORMAL);
// SPI is synchronous, so bypass input synchroniser to reduce input delay.
hw_set_bits(&pio->input_sync_bypass, 1u << pin_miso);
pio_sm_init(pio, sm, prog_offs, &c);
pio_sm_set_enabled(pio, sm, true);
}

#endif

// ------------ //
// spi_cpha0_cs //
// ------------ //

#define spi_cpha0_cs_wrap_target 0
#define spi_cpha0_cs_wrap 8
#define spi_cpha0_cs_pio_version 0

#define spi_cpha0_cs_offset_entry_point 8u

static const uint16_t spi_cpha0_cs_program_instructions[] = {
// .wrap_target
0x6101, // 0: out pins, 1 side 0 [1]
0x4801, // 1: in pins, 1 side 1
0x0840, // 2: jmp x--, 0 side 1
0x6001, // 3: out pins, 1 side 0
0xa022, // 4: mov x, y side 0
0x4801, // 5: in pins, 1 side 1
0x08e0, // 6: jmp !osre, 0 side 1
0xa142, // 7: nop side 0 [1]
0x91e0, // 8: pull ifempty block side 2 [1]
// .wrap
};

#if !PICO_NO_HARDWARE
static const struct pio_program spi_cpha0_cs_program = {
.instructions = spi_cpha0_cs_program_instructions,
.length = 9,
.origin = -1,
.pio_version = 0,
#if PICO_PIO_VERSION > 0
.used_gpio_ranges = 0x0
#endif
};

static inline pio_sm_config spi_cpha0_cs_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + spi_cpha0_cs_wrap_target, offset + spi_cpha0_cs_wrap);
sm_config_set_sideset(&c, 2, false, false);
return c;
}
#endif

// ------------ //
// spi_cpha1_cs //
// ------------ //

#define spi_cpha1_cs_wrap_target 0
#define spi_cpha1_cs_wrap 8
#define spi_cpha1_cs_pio_version 0

#define spi_cpha1_cs_offset_entry_point 7u

static const uint16_t spi_cpha1_cs_program_instructions[] = {
// .wrap_target
0x6901, // 0: out pins, 1 side 1 [1]
0x4001, // 1: in pins, 1 side 0
0x0040, // 2: jmp x--, 0 side 0
0x6801, // 3: out pins, 1 side 1
0xa822, // 4: mov x, y side 1
0x4001, // 5: in pins, 1 side 0
0x00e0, // 6: jmp !osre, 0 side 0
0x91e0, // 7: pull ifempty block side 2 [1]
0xa142, // 8: nop side 0 [1]
// .wrap
};

#if !PICO_NO_HARDWARE
static const struct pio_program spi_cpha1_cs_program = {
.instructions = spi_cpha1_cs_program_instructions,
.length = 9,
.origin = -1,
.pio_version = 0,
#if PICO_PIO_VERSION > 0
.used_gpio_ranges = 0x0
#endif
};

static inline pio_sm_config spi_cpha1_cs_program_get_default_config(uint offset) {
pio_sm_config c = pio_get_default_sm_config();
sm_config_set_wrap(&c, offset + spi_cpha1_cs_wrap_target, offset + spi_cpha1_cs_wrap);
sm_config_set_sideset(&c, 2, false, false);
return c;
}

#include "hardware/gpio.h"
static inline void pio_spi_cs_init(PIO pio, uint sm, uint prog_offs, uint n_bits, float clkdiv, bool cpha, bool cpol,
uint pin_sck, uint pin_mosi, uint pin_miso) {
pio_sm_config c = cpha ? spi_cpha1_cs_program_get_default_config(prog_offs) : spi_cpha0_cs_program_get_default_config(prog_offs);
sm_config_set_out_pins(&c, pin_mosi, 1);
sm_config_set_in_pins(&c, pin_miso);
sm_config_set_sideset_pins(&c, pin_sck);
sm_config_set_out_shift(&c, false, true, n_bits);
sm_config_set_in_shift(&c, false, true, n_bits);
sm_config_set_clkdiv(&c, clkdiv);
pio_sm_set_pins_with_mask(pio, sm, (2u << pin_sck), (3u << pin_sck) | (1u << pin_mosi));
pio_sm_set_pindirs_with_mask(pio, sm, (3u << pin_sck) | (1u << pin_mosi), (3u << pin_sck) | (1u << pin_mosi) | (1u << pin_miso));
pio_gpio_init(pio, pin_mosi);
pio_gpio_init(pio, pin_miso);
pio_gpio_init(pio, pin_sck);
pio_gpio_init(pio, pin_sck + 1);
gpio_set_outover(pin_sck, cpol ? GPIO_OVERRIDE_INVERT : GPIO_OVERRIDE_NORMAL);
hw_set_bits(&pio->input_sync_bypass, 1u << pin_miso);
uint entry_point = prog_offs + (cpha ? spi_cpha1_cs_offset_entry_point : spi_cpha0_cs_offset_entry_point);
pio_sm_init(pio, sm, entry_point, &c);
pio_sm_exec(pio, sm, pio_encode_set(pio_x, n_bits - 2));
pio_sm_exec(pio, sm, pio_encode_set(pio_y, n_bits - 2));
pio_sm_set_enabled(pio, sm, true);
}

#endif

2 changes: 1 addition & 1 deletion tests/restyle.sh
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ for dir in ./cores/rp2040 ./libraries/EEPROM ./libraries/I2S ./libraries/SingleF
./libraries/SPISlave ./libraries/lwIP_ESPHost ./libraries/FatFS\
./libraries/FatFSUSB ./libraries/BluetoothAudio ./libraries/BluetoothHCI \
./libraries/BluetoothHIDMaster ./libraries/NetBIOS ./libraries/Ticker \
./libraries/VFS ./libraries/rp2350 ./libraries/SimpleMDNS ; do
./libraries/VFS ./libraries/rp2350 ./libraries/SimpleMDNS ./libraries/SoftwareSPI ; 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

0 comments on commit acf81f4

Please sign in to comment.