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