diff --git a/README.md b/README.md index fc3326d..77d7d0d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/spi.rst b/docs/spi.rst index 8b87220..c9ee67a 100644 --- a/docs/spi.rst +++ b/docs/spi.rst @@ -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 softSPI(_sck, _miso, _mosi); // no HW CS support, any selection of pins can be used SPI Slave (SPISlave) ==================== diff --git a/libraries/SoftwareSPI/examples/FilesSoftwareSPI/FilesSoftwareSPI.ino b/libraries/SoftwareSPI/examples/FilesSoftwareSPI/FilesSoftwareSPI.ino new file mode 100644 index 0000000..21d0dc4 --- /dev/null +++ b/libraries/SoftwareSPI/examples/FilesSoftwareSPI/FilesSoftwareSPI.ino @@ -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 + +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 + +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. +} + + + diff --git a/libraries/SoftwareSPI/examples/W5500SoftwareSPI/W5500SoftwareSPI.ino b/libraries/SoftwareSPI/examples/W5500SoftwareSPI/W5500SoftwareSPI.ino new file mode 100644 index 0000000..249229f --- /dev/null +++ b/libraries/SoftwareSPI/examples/W5500SoftwareSPI/W5500SoftwareSPI.ino @@ -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 + +const char* host = "djxmmx.net"; +const uint16_t port = 17; + +#include +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(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; +} diff --git a/libraries/SoftwareSPI/keywords.txt b/libraries/SoftwareSPI/keywords.txt new file mode 100644 index 0000000..9820e3d --- /dev/null +++ b/libraries/SoftwareSPI/keywords.txt @@ -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 diff --git a/libraries/SoftwareSPI/library.properties b/libraries/SoftwareSPI/library.properties new file mode 100644 index 0000000..3a071e5 --- /dev/null +++ b/libraries/SoftwareSPI/library.properties @@ -0,0 +1,10 @@ +name=SoftwareSPI +version=1.0 +author=Earle F. Philhower, III +maintainer=Earle F. Philhower, III +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 diff --git a/libraries/SoftwareSPI/src/SoftwareSPI.cpp b/libraries/SoftwareSPI/src/SoftwareSPI.cpp new file mode 100644 index 0000000..9b9619d --- /dev/null +++ b/libraries/SoftwareSPI/src/SoftwareSPI.cpp @@ -0,0 +1,365 @@ +/* + PIO-based SPI Master library for the Raspberry Pi Pico RP2040 + + Copyright (c) 2025 Earle F. Philhower, III + + 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 +#include +#include +#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 +#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(csrc); + uint8_t *dest = reinterpret_cast(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 +} diff --git a/libraries/SoftwareSPI/src/SoftwareSPI.h b/libraries/SoftwareSPI/src/SoftwareSPI.h new file mode 100644 index 0000000..f3543a6 --- /dev/null +++ b/libraries/SoftwareSPI/src/SoftwareSPI.h @@ -0,0 +1,109 @@ +/* + PIO-based SPI Master library for the Raspberry Pi Pico RP2040 + + Copyright (c) 2025 Earle F. Philhower, III + + 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 +#include +#include +#include + +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 _usingIRQs; +}; diff --git a/libraries/SoftwareSPI/src/spi.pio b/libraries/SoftwareSPI/src/spi.pio new file mode 100644 index 0000000..4033a74 --- /dev/null +++ b/libraries/SoftwareSPI/src/spi.pio @@ -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); +} +%} diff --git a/libraries/SoftwareSPI/src/spi.pio.h b/libraries/SoftwareSPI/src/spi.pio.h new file mode 100644 index 0000000..557446a --- /dev/null +++ b/libraries/SoftwareSPI/src/spi.pio.h @@ -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 + diff --git a/tests/restyle.sh b/tests/restyle.sh index 27dea5b..e1709e3 100755 --- a/tests/restyle.sh +++ b/tests/restyle.sh @@ -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