From fa6b145c8c886648ba3612393615739f2ad8c641 Mon Sep 17 00:00:00 2001 From: Phil Rittenhouse Date: Sat, 8 Aug 2026 23:57:55 -0400 Subject: [PATCH] Add support register-based SPI slave By adding a method to read and write one byte at a time, the slave's code can implement a complete register-based SPI slave. The new method includes timeouts so that the slave does not get stuck in an infinite while loop if something goes wrong. An additional reset() method can be used to reset the spi interface This allows the slave to recover if it falls behind or detects other errors that cause it to fall out sync with the master. Example code showing a simple register-based slave is included in the SPI/examples/RegisterSlave folder along with a python script that runs on a RPi and acts as the master. --- .../examples/RegisterSlave/SPI_slave_demo.ino | 316 ++++++++++++++++++ .../SPI/examples/RegisterSlave/spi_demo.py | 75 +++++ libraries/SPI/src/SPI.cpp | 23 ++ libraries/SPI/src/SPI.h | 2 + libraries/SPI/src/utility/spi_com.c | 140 ++++++++ libraries/SPI/src/utility/spi_com.h | 2 + 6 files changed, 558 insertions(+) create mode 100644 libraries/SPI/examples/RegisterSlave/SPI_slave_demo.ino create mode 100644 libraries/SPI/examples/RegisterSlave/spi_demo.py diff --git a/libraries/SPI/examples/RegisterSlave/SPI_slave_demo.ino b/libraries/SPI/examples/RegisterSlave/SPI_slave_demo.ino new file mode 100644 index 0000000000..a479255123 --- /dev/null +++ b/libraries/SPI/examples/RegisterSlave/SPI_slave_demo.ino @@ -0,0 +1,316 @@ +/* + * This code demonstrates register-based SPI slave support in the stm32 arduino library. + * An example python script is included to show the master side running on a RPi. + * Since this is a software implementation of a SPI slave the max data rate will be + * significantly slower than a typical hardware-based SPI slave. I have found 100kHz + * to be a reliable max clock speed. + * + * You must install STM32duino FreeRTOS library (Sketch -> Include Library -> Manage Libraries) + * + * This code was tested on a NUCLEO-F401RE dev board using the Arduino IDE. + * Set Board to Nucleo-64 and board number to F401RE + * Set USART support to Enable (generic Serial) + * Set USB support to None + * Upload method STM32CubeProgrammer (SWD) -> Requires installing STM32_Programmer_CLI.exe and adding it to your PATH + * Optimize for smallest + * + * The following pins should be conneced to the corresponding pins on the master + * PA4 - NSS + * PA5 - SCK + * PA6 - MISO + * PA7 - MOSI + * GND - GND + * + * In this example we implement a register-based SPI slave. Twenty 16-bit registers + * (addressed 0 through 19) are implemented. All registers are read/write except as noted. + * The following registers have special functions: + * Address Function + * ------- -------- + * 0 Returns the elapsed time in millis (read only) + * 1 Returns the state of the blue User button on the Nucleo (read only) + * 10 The LSB controls a GPIO on D4 + * 11 Returns the inverse of the value written + * + * the remaining registers simply read back whatever value was written. + */ +#include +//#define SPI_TRANSFER_TIMEOUT HAL_MAX_DELAY // Disable SPI timeouts +#include + +#define NUM_REG 20 +#define DEBUG_GPIO D4 + +// USE TASK NOTIFICATION INSTEAD OF SEMAPHORE FOR SPEED +/* Store the handle of the task that will be notified when the ISR fires */ +static TaskHandle_t xTaskToNotify = NULL; + +// Shadow storage for 16 bit register values +uint16_t Registers[NUM_REG]; + +// Globals used by ISRs +volatile int xTaskErr = 0; +volatile int CatchupCnt = 0; + + +void SPI_ISR() { + BaseType_t xHigherPriorityTaskWoken = pdFALSE; + + SPI.reset(); // Reset the SPI hardware interface to flush any stale data: + + // At this point xTaskToNotify should not be NULL + // If this happens it means we are falling behind. + // Increment an error cound so the main task can take action to get back in sync + if (xTaskToNotify == NULL) + { + xTaskErr++; + return; + } + + // Signal SPI thread + vTaskNotifyGiveFromISR( xTaskToNotify, &xHigherPriorityTaskWoken ); + + xTaskToNotify = NULL; + + /* If xHigherPriorityTaskWoken is now set to pdTRUE then a context switch + should be performed to ensure the interrupt returns directly to the highest + priority task. The macro used for this purpose is dependent on the port in + use and may be called portEND_SWITCHING_ISR(). */ + portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); +} + +// Handle the 3-byte transaction from master +// *read - True for read transactions, False for writes +// *address - Returns the register address +// *value - returns the value sent by a write +// Returns false on error. +bool process_transaction(bool *read, uint8_t *address, uint16_t *value) { + uint8_t rx, tx; + + // Read the first byte from master to get the address and read/write bit + tx = 0xBB; // Send dummy 0xBB since the master will ignore the first byte we send + if (!SPI.read_write_byte(tx, &rx)) { + return(false); + } + + *read = (rx & 0x80) == 0x80; + *address = rx & 0x7F; + if (*address >= NUM_REG) { + return(false); + } + + // if the read bit is set, look up the value and send it to the master + if (*read) { + *value = Registers[*address]; + // Return value in the next two bytes of the transaction + tx = (*value >> 8) & 0xFF; + if (!SPI.read_write_byte(tx, &rx)) { + return(false); + } + + tx = *value & 0xFF; + if (!SPI.read_write_byte(tx, &rx)) { + return(false); + } + } + else { + // If this is a write, read the next two bytes to get the value to write to the register + tx = 0xFF; // Don't care + if (!SPI.read_write_byte(tx, &rx)) { + return(false); + } + *value = rx << 8; + + tx = 0xFF; + if (!SPI.read_write_byte(tx, &rx)) { + return(false); + } + *value |= rx; + } + return(true); +} + + +/* + * SPI Thread, process SPI when triggered by ISR + * + * This function is used to send/receive data as a slave + * It implements a register set with 16-bit registers/words + * It supports reading or writing one word per 3-byte SPI transaction. + * It will automatically check the R/W bit in the first word and act accordingly. + * For read, the address will be used as an + * index into the register array and the corresponding value will be returned to the + * master. For writes the address is used to select an appropriate action. + */ +static void SPIThread(void* arg) { + UNUSED(arg); + bool read; + uint8_t address; + uint16_t value; + bool rval; + + Serial.println("SPI Thread started"); + + while (1) { + // Wait for signal from ISR. + // At this point xTaskToNotify should be NULL + // If it's no try to reset the interface instead + if (xTaskToNotify != NULL) + { + Serial.println("SPI notification out of sync. Resetting..."); + Serial.flush(); + // Wait for transaction to finish before resetting. SS is active low so wait until it goes high + unsigned long end_time = millis() + 100; + while (digitalRead(SS) == LOW) { + if (millis() > end_time) { + Serial.println("Timed out waiting for SS to go high"); + Serial.flush(); + break; + } + } + SPI.reset(); // Reset the SPI hardware interface + } + + // Store this task's handle so the ISR knows who to signal + xTaskToNotify = xTaskGetCurrentTaskHandle(); + + // Block until ISR signals us. This is faster than using a semaphore + ulTaskNotifyTake( pdFALSE, portMAX_DELAY ); // Block without timeout, decrement (don't clear) + + if (xTaskErr > CatchupCnt) { + Serial.println("Fell behind"); + Serial.flush(); + // Wait for transaction to finish before resetting. SS is active low so wait until it goes high + unsigned long end_time = millis() + 100; + while (digitalRead(SS) == LOW) { + if (millis() > end_time) { + Serial.println("Timed out waiting for SS to go high"); + Serial.flush(); + break; + } + } + SPI.reset(); // Reset the SPI hardware interface + CatchupCnt = xTaskErr; // Don't block to process the missed interrupt. + continue; + } + + // We must process the transaction as quickly as possible in order to keep up with the master. + // If you have other interrupts in your system, you can use a critical section to prevent + // the transsaction getting interrupted, but you must disable the timeouts in the SPI library + // by defining the SPI_TRANSFER_TIMEOUT macro to HAL_MAX_DELAY before including the SPI library + // Be cafeful though as, without timeouts, the code can get stuck in an infinite loop under certain + // error conditions. + //taskENTER_CRITICAL(); + rval = process_transaction(&read, &address, &value); + //taskEXIT_CRITICAL(); + + if (!rval) { + Serial.println("Error in SPI transfer. Resetting SPI hardware..."); + SPI.reset(); // Reset the SPI hardware interface + continue; + } + + if (!read) { // Handle Write actions outside of the critical section + switch(address) { + case 0: + case 1: + // Read only, no action + break; + case 10: + if (value & 0x0001) { + Serial.println("GPIO on"); + digitalWrite(DEBUG_GPIO, LOW); // turn the LED on + } + else { + Serial.println("GPIO off"); + digitalWrite(DEBUG_GPIO, HIGH); // turn the LED off + } + Registers[10] = value; + break; + case 11: + Registers[11] = ~value; + break; + default: + Registers[address] = value; + } + } // write + } // while forever +} + + +/* + * Thread 2, Just a busy loop representing lower priority worker task. + */ +static void Thread2(void* arg) { + UNUSED(arg); + static int old_err = 0; + static int old_cnt = 0; + + Serial.println("low priority task started"); + + while(1) + { + // Load Reg 0 with the value from millis() + Registers[0] = millis() & 0xFFFF; + + // Load Reg 1 with the state of the blue User button + Registers[1] = digitalRead(PC13); + + // Check for error from SPI ISR - only print on change to avoid collision with other prints + if (xTaskErr != old_err || CatchupCnt != old_cnt) + { + old_err = xTaskErr; + old_cnt = CatchupCnt; + Serial.printf("xTaskErr = %d, CatchupCnt = %d\n", xTaskErr, CatchupCnt); + Serial.flush(); + } + delay(100); + } +} + + +void setup() { + portBASE_TYPE s1, s2; + + Serial.begin(115200); + // Wait for the serial port to actually open + while (!Serial) { + delay(10); + } + Serial.println("In setup"); + + // initialize digital pin DEBUG_GPIO as an output. + pinMode(DEBUG_GPIO, OUTPUT); + pinMode(PC13, INPUT_PULLUP); // Configure the blue button as an input with internal pull-up resistor + + SPI.begin(SPI_PERIPHERAL); + // Add an ISR to the SS pin to detect device selection by SPI master + SPI.attachSlaveInterrupt(SS, SPI_ISR); + + // create task at priority two (higher priority) + s1 = xTaskCreate(SPIThread, NULL, 1024, NULL, 2, NULL); + // create task at priority one (lower priority) + s2 = xTaskCreate(Thread2, NULL, 1024, NULL, 1, NULL); + // check for creation errors + if (s1 != pdPASS || s2 != pdPASS ) { + Serial.println("Task Creation problem! Halting."); + Serial.flush(); + while(1); + } + Serial.println("Threads started"); + + Serial.println("Done setup"); + // start scheduler + vTaskStartScheduler(); + Serial.println("Insufficient RAM"); + Serial.flush(); + while(1); +} + + +//------------------------------------------------------------------------------ +// WARNING idle loop has a very small stack (configMINIMAL_STACK_SIZE) +// loop must never block +void loop() { + // Not used. +} + diff --git a/libraries/SPI/examples/RegisterSlave/spi_demo.py b/libraries/SPI/examples/RegisterSlave/spi_demo.py new file mode 100644 index 0000000000..c546e9d064 --- /dev/null +++ b/libraries/SPI/examples/RegisterSlave/spi_demo.py @@ -0,0 +1,75 @@ +""" +To access SPI bus 1 you need to +Edit the /boot/firmware/config.txt +Add the following lines to enable SPI1: +dtoverlay=spi1-1cs,cs0_pin=22 +""" +import spidev +import time +import sys + + +def write_register(register_address, data): + """Writes 16 bits to a specified register.""" + # The first byte sent contains the register address and a write bit + command_byte = register_address & 0x7F # Clear MSB for write + + # Send command byte and data bytes (MSB first) + spi.xfer2([command_byte, (data >> 8) & 0xff, data & 0xff]) + + +def read_register(register_address): + """Reads a 16-bit word from a register.""" + # The first byte sent contains the register address and a read bit + command_byte = register_address | 0x80 # Set MSB for read + + # Send command byte and dummy bytes for reading + # The received data will be in the response list + response = spi.xfer2([command_byte] + [0x00] * 2) + + # Return the relevant data from the response (excluding the command byte) + return response[1] << 8 | response[2] + + +spi_speed = 50000 +if (len(sys.argv) > 1): + spi_speed = int(sys.argv[1]) + +bus = 1 +device = 0 +# Enable SPI +spi = spidev.SpiDev() + +# Open a connection to a specific bus and device (chip select pin) +spi.open(bus, device) + +# Set SPI speed and mode +print("Set SPI bus speed to %d kHz\n" % (spi_speed/1000)) +spi.max_speed_hz = spi_speed +spi.mode = 0 + +while(1): + test = 0xA55A + write_register(11, test) + time.sleep(.05) + val = read_register(11); + print("Wrote 0x%x in reg 11, read back 0x%x" % (test, val)) + + val = read_register(0); + print("reg 0 millis = %d" % val) + + val = read_register(1); + print("User button = %d" % val) + + test = 0x0F0F + write_register(11, test) + time.sleep(.05) + val = read_register(11); + print("Wrote 0x%x in reg 11, read back 0x%x" % (test, val)) + + + time.sleep(.5) + print("\n") + + + diff --git a/libraries/SPI/src/SPI.cpp b/libraries/SPI/src/SPI.cpp index 2a23c0ef31..fcea741f34 100644 --- a/libraries/SPI/src/SPI.cpp +++ b/libraries/SPI/src/SPI.cpp @@ -100,6 +100,16 @@ void SPIClass::endTransaction(void) } +/** + * @brief Reset the SPI interface. + */ +void SPIClass::reset(void) +{ + _spi.handle.State = HAL_SPI_STATE_RESET; + spi_reset(&_spi); +} + + /** * @brief Deinitialize the SPI instance and stop it. */ @@ -182,6 +192,19 @@ void SPIClass::transfer(const void *tx_buf, void *rx_buf, size_t count) spi_transfer(&_spi, ((const uint8_t *)tx_buf), ((uint8_t *)rx_buf), count); } +/** + * @brief Helper to perform a single byte transaction (read and write). + * begin() or beginTransaction() must be called at least once before. + * @param + * @param + * @param tx: byte to send + * @param *rx: byte received. If NULL the received byte will be discarded. + * @return true on success. + */ +bool SPIClass::read_write_byte(uint8_t tx, uint8_t *rx) +{ + return(spi_read_write_byte(&_spi, tx, rx) == SPI_OK); +} /** * @brief Not implemented. diff --git a/libraries/SPI/src/SPI.h b/libraries/SPI/src/SPI.h index 0560a0b7f0..e78f9a0c0d 100644 --- a/libraries/SPI/src/SPI.h +++ b/libraries/SPI/src/SPI.h @@ -68,6 +68,7 @@ class SPIClass : public HardwareSPI { { begin(SPI_CONTROLLER); } + void reset(void); void end(void) override ; /* This function should be used to configure the SPI instance in case you @@ -100,6 +101,7 @@ class SPIClass : public HardwareSPI { */ void transfer(const void *tx_buf, void *rx_buf, size_t count); + bool read_write_byte(uint8_t tx, uint8_t *rx); // Not implemented functions. Kept for compatibility. void usingInterrupt(int interruptNumber) override; void notUsingInterrupt(int interruptNumber) override; diff --git a/libraries/SPI/src/utility/spi_com.c b/libraries/SPI/src/utility/spi_com.c index 1778e06d80..5f37ce4379 100644 --- a/libraries/SPI/src/utility/spi_com.c +++ b/libraries/SPI/src/utility/spi_com.c @@ -550,6 +550,77 @@ void spi_deinit(spi_t *obj) } } + + +/** + * @brief This function is implemented to reset the SPI interface + * @param obj : pointer to spi_t structure + * @retval None + */ +void spi_reset(spi_t *obj) +{ + if (obj == NULL) { + return; + } + + SPI_HandleTypeDef *handle = &(obj->handle); + +#if defined SPI1_BASE + // Reset SPI + if (handle->Instance == SPI1) { + __HAL_RCC_SPI1_FORCE_RESET(); + __HAL_RCC_SPI1_RELEASE_RESET(); + } +#endif +#if defined SPI2_BASE + if (handle->Instance == SPI2) { + __HAL_RCC_SPI2_FORCE_RESET(); + __HAL_RCC_SPI2_RELEASE_RESET(); + } +#endif + +#if defined SPI3_BASE + if (handle->Instance == SPI3) { + __HAL_RCC_SPI3_FORCE_RESET(); + __HAL_RCC_SPI3_RELEASE_RESET(); + } +#endif + +#if defined SPI4_BASE + if (handle->Instance == SPI4) { + __HAL_RCC_SPI4_FORCE_RESET(); + __HAL_RCC_SPI4_RELEASE_RESET(); + } +#endif + +#if defined SPI5_BASE + if (handle->Instance == SPI5) { + __HAL_RCC_SPI5_FORCE_RESET(); + __HAL_RCC_SPI5_RELEASE_RESET(); + } +#endif + +#if defined SPI6_BASE + if (handle->Instance == SPI6) { + __HAL_RCC_SPI6_FORCE_RESET(); + __HAL_RCC_SPI6_RELEASE_RESET(); + } +#endif + +#if defined SUBGHZSPI_BASE + if (handle->Instance == SUBGHZSPI) { + __HAL_RCC_SUBGHZSPI_FORCE_RESET(); + __HAL_RCC_SUBGHZSPI_RELEASE_RESET(); + } +#endif + + HAL_SPI_Init(handle); + + /* In order to correctly set the SPI polarity we need to enable the peripheral */ + __HAL_SPI_ENABLE(handle); +} + + /** * @brief This function is implemented by user to send/receive data over * SPI interface @@ -624,6 +695,75 @@ spi_status_e spi_transfer(spi_t *obj, const uint8_t *tx_buffer, uint8_t *rx_buff return ret; } + +/** + * @brief This function is used to send/receive one byte on SPI + * @param + * @param obj : pointer to spi_t structure + * @param tx: byte to send + * @param rx: pointer to byte received. If NULL the received byte will be discarded + * @param + * @retval status. SPI_OK = 0 + */ +spi_status_e spi_read_write_byte(spi_t *obj, uint8_t tx, uint8_t *rx) +{ + spi_status_e ret = SPI_OK; + int8_t tmp; + uint32_t tickstart; + SPI_TypeDef *_SPI = obj->handle.Instance; + + tickstart = HAL_GetTick(); + +#if defined(SPI_SR_TXP) // True if we have fifo threshold support + while (!LL_SPI_IsActiveFlag_TXP(_SPI)) { + if ((SPI_TRANSFER_TIMEOUT != HAL_MAX_DELAY) && + (HAL_GetTick() - tickstart >= SPI_TRANSFER_TIMEOUT)) { + core_debug("SPI Active flag timed out\n"); + return(SPI_TIMEOUT); + } + } +#else + while (!LL_SPI_IsActiveFlag_TXE(_SPI)) { // Wait for transmit empty before sending + if ((SPI_TRANSFER_TIMEOUT != HAL_MAX_DELAY) && + (HAL_GetTick() - tickstart >= SPI_TRANSFER_TIMEOUT)) { + core_debug("SPI Active flag timed out\n"); + return(SPI_TIMEOUT); + } + } +#endif + LL_SPI_TransmitData8(_SPI, tx); + +#if defined(SPI_SR_RXP) + while (!LL_SPI_IsActiveFlag_RXP(_SPI)) { + if ((SPI_TRANSFER_TIMEOUT != HAL_MAX_DELAY) && + (HAL_GetTick() - tickstart >= SPI_TRANSFER_TIMEOUT)) { + core_debug("SPI Rx timed out\n"); + return(SPI_TIMEOUT); + } + } +#else + while (!LL_SPI_IsActiveFlag_RXNE(_SPI)) { // Wait for Rx not empty before read + if ((SPI_TRANSFER_TIMEOUT != HAL_MAX_DELAY) && + (HAL_GetTick() - tickstart >= SPI_TRANSFER_TIMEOUT)) { + core_debug("SPI Rx timed out\n"); + return(SPI_TIMEOUT); + } + } +#endif + tmp = LL_SPI_ReceiveData8(_SPI); + if (rx != NULL) *rx = (uint8_t)tmp; + + if ((SPI_TRANSFER_TIMEOUT != HAL_MAX_DELAY) && + (HAL_GetTick() - tickstart >= SPI_TRANSFER_TIMEOUT)) { + core_debug("SPI Transfer timed out\n"); + return(SPI_TIMEOUT); + } + + return ret; +} + + + #ifdef __cplusplus } #endif diff --git a/libraries/SPI/src/utility/spi_com.h b/libraries/SPI/src/utility/spi_com.h index 8e4fcf844d..c7f9457153 100644 --- a/libraries/SPI/src/utility/spi_com.h +++ b/libraries/SPI/src/utility/spi_com.h @@ -94,7 +94,9 @@ typedef enum { /* Exported functions ------------------------------------------------------- */ void spi_init(spi_t *obj, uint32_t speed, spi_mode_e dataMode, bool msbFirst, spi_busmode_e busMode); void spi_deinit(spi_t *obj); +void spi_reset(spi_t *obj); spi_status_e spi_transfer(spi_t *obj, const uint8_t *tx_buffer, uint8_t *rx_buffer, uint16_t len); +spi_status_e spi_read_write_byte(spi_t *obj, uint8_t tx, uint8_t *rx); uint32_t spi_getClkFreq(spi_t *obj); #ifdef __cplusplus