SPI1 issues when used as slave on teensy4.1

Hi all,

I'm using a teensy 4.1 as an SPI slave to sniff SPI transactions. The same master writes to two different slaves in two different moments:
1734708135152.png


I want to sniff the two transactions from the master. CS1 is connected to SPI CS (pin 10) and it works fine. CS2 is connected to SPI1 CS (LPSPI3, pin 0). In order to configure the peripherals as SPI slaves, I'm using a modified version of this fork of the SPISlave_T4 library. I put here my changes, together with a minimum reproducible example.

In the example below, you can switch between SPI or SPI1 by uncommenting the commented line in main.ino. Below you can find my issue.

C++:
// SPISlave_T4.h
#if !defined(_SPISlave_T4_H_)
#define _SPISlave_T4_H_
#include <SPI.h>
typedef enum SPI_BITS {
  SPI_8_BITS = 8,
  SPI_16_BITS = 16,
  SPI_32_BITS = 32,
} SPI_BITS;
typedef void (*_SPI_ptr)();
class SPISlave_T4 {
  public:
    SPISlave_T4(unsigned portnum, SPI_BITS bits);
    void begin(uint8_t bitOrder = MSBFIRST, uint8_t dataMode = SPI_MODE0);
    uint32_t transmitErrors() { return transmit_errors; }
    void onReceive(_SPI_ptr handler) { _spihandler = handler; }
    bool active();
    bool available();
    void pushr(uint32_t data);
    uint32_t popr();
  protected:
    IMXRT_LPSPI_t *_lpspi; // Book chapter 48; p2799
    SPI_BITS _bits;
    _SPI_ptr _spihandler = nullptr;
    virtual void SLAVE_ISR();
    IRQ_NUMBER_t nvic_irq;
    uint32_t transmit_errors = 0;
    bool sniffer_enabled = 0;
  friend void lpspi4_slave_isr();
  friend void lpspi3_slave_isr();
  friend void lpspi1_slave_isr();
};
#endif

C++:
// SPISlave.cpp

#include "imxrt.h"
#include <Arduino.h>
#include "SPISlave_T4.h"
// Static pointers to the instances. The numbers are the LPSPI port numbers,
// not the Arduino / Teensy port numbers that are printed on the pinout
// card from PJRC. The static pointers are needed so the interrupt service
// routines know which instance of our class to call.
// Note: LPSPI 2 is not available.
static SPISlave_T4* _LPSPI1 = nullptr; // Arduino / Teensy port 2
static SPISlave_T4* _LPSPI3 = nullptr; // Arduino / Teensy port 1
static SPISlave_T4* _LPSPI4 = nullptr; // Arduino / Teensy port 0
void lpspi1_slave_isr() {
  _LPSPI1->SLAVE_ISR();
}
void lpspi3_slave_isr() {
  _LPSPI3->SLAVE_ISR();
}
void lpspi4_slave_isr() {
  _LPSPI4->SLAVE_ISR();
}
SPISlave_T4::SPISlave_T4(unsigned portnum, SPI_BITS bits) {
  _SPI_ptr isr = nullptr; // Interrupt service routine address
  uint32_t cg = 0; // Clock gate value
  volatile uint32_t *iomuxc_base = nullptr; // IO multiplexer registers for SCK/SDI/SDO/PCS0
  _bits = bits;
  switch(portnum)
  {
    // The order of the Arduino SPI port numbers doesn't correspond to the
    // LPSPI port numbers
    // See e.g.: https://forum.pjrc.com/threads/61234-Teensy-4-1-and-SPI2
    case 0: _LPSPI4 = this; _lpspi = &IMXRT_LPSPI4_S; nvic_irq = IRQ_LPSPI4; isr = lpspi4_slave_isr; cg = CCM_CCGR1_LPSPI4(CCM_CCGR_ON); iomuxc_base = &IOMUXC_LPSPI4_PCS0_SELECT_INPUT; break;
    case 1: _LPSPI3 = this; _lpspi = &IMXRT_LPSPI3_S; nvic_irq = IRQ_LPSPI3; isr = lpspi3_slave_isr; cg = CCM_CCGR1_LPSPI3(CCM_CCGR_ON); iomuxc_base = &IOMUXC_LPSPI3_PCS0_SELECT_INPUT; break;
    case 2: _LPSPI1 = this; _lpspi = &IMXRT_LPSPI1_S; nvic_irq = IRQ_LPSPI1; isr = lpspi1_slave_isr; cg = CCM_CCGR1_LPSPI1(CCM_CCGR_ON); iomuxc_base = &IOMUXC_LPSPI1_PCS0_SELECT_INPUT; break;
  }
  // Clock gating register, see 14.7.22 p1085
  // Oddly, changing from CG3 to CG2 causes hang on init.
  // There appears to be a need for coordination between setting this register and CCM_CBCMR, as per the thread:
  // https://forum.pjrc.com/threads/59254-SPI-Slave-Mode-on-Teensy-4
  // "CCM_CBCMR |= CCM_CCGR1_LPSPI4(CCM_CCGR_ON); //Clock reaktivieren (reactivate)". Note he also appears to
  // set SION on the CS pin as we also did below.
  CCM_CCGR1 |= cg;
  attachInterruptVector(nvic_irq, isr);
  /* Alternate pins not broken out on Teensy 4.0/4.1 for LPSPI4 */
  // 11.7.323 LPSPI4_PCS0_SELECT_INPUT DAISY Register (IOMUXC_LPSPI4_PCS0_SELECT_INPUT) 401F_851Ch (Baseaddr + (_portnum * 0x10))
  iomuxc_base[0] = 0; // IOMUXC_LPSPIx_PCS0_SELECT_INPUT. For LSPCI4: (401F_851Ch) 0=GPIO_B0_00_ALT3 1=GPIO_B1_04_ALT1
  iomuxc_base[1] = 0; // IOMUXC_LPSPIx_SCK_SELECT_INPUT . For LSPCI4: (401F_8520h) 0=GPIO_B0_03_ALT3 1=GPIO_B1_07_ALT1
  iomuxc_base[2] = 0; // IOMUXC_LPSPIx_SDI_SELECT_INPUT . For LSPCI4: (401F_8524h) 0=GPIO_B0_02_ALT3 1=GPIO_B1_05_ALT1
  iomuxc_base[3] = 0; // IOMUXC_LPSPIx_SDO_SELECT_INPUT . For LSPCI4: (401F_8528h) 0=GPIO_B0_02_ALT3 1=GPIO_B1_06_ALT1
  switch (portnum) {
    case 0:
      // These are the primary SPI mux control registers.
      IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_03 = 0x3; // LPSPI4 SCK (CLK) 13 ALT3
      IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_01 = 0x3; // LPSPI4 SDI (MISO) 12 ALT3
      IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_02 = 0x3; // LPSPI4 SDO (MOSI) 11 ALT3
      IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_00 = 0x3; // LPSPI4 PCS0 (CS) 10 ALT3
      break;
    case 1:
      // These are the primary SPI2 mux control registers (unverified -- JG).
      IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_15 = 0x12; // LPSPI3_SCK1 (CLK) 27 ALT2
      IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B0_02 = 0x17; // LPSPI3_SDI (MISO1) 1 ALT7
      IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_14 = 0x12; // LPSPI3_SDO (MOSI1) 26 ALT2
      IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B0_03 = 0x17; // LPSPI3_PCS0 (CS1) 0 ALT7 + SION. See: https://forum.pjrc.com/archive/index.php/t-59893.html
      break;
    case 2:
      // These are the primary SPI3 mux control registers (unverified -- JG).
      IOMUXC_SW_MUX_CTL_PAD_GPIO_SD_B0_00 = 0x14; // LPSPI2_SCK1 (CLK2) 37 ALT4
      IOMUXC_SW_MUX_CTL_PAD_GPIO_SD_B0_03 = 0x14; // LPSPI2_SDI (MISO2) 34 ALT4
      IOMUXC_SW_MUX_CTL_PAD_GPIO_SD_B0_02 = 0x14; // LPSPI2_SDO (MOSI2) 35 ALT4
      IOMUXC_SW_MUX_CTL_PAD_GPIO_SD_B0_01 = 0x14; // LPSPI2_PCS0 (CS2) 36 ALT4 + SION. See: https://forum.pjrc.com/archive/index.php/t-59893.html
      break;
  }
}
bool SPISlave_T4::active() {
  return !(_lpspi->SR & LPSPI_SR_FCF); // returns 1 if frame transfer not completed
}

bool SPISlave_T4::available() {
  return !(_lpspi->RSR & LPSPI_RSR_RXEMPTY); // returns 1 if Rx FIFO is not empty
}

void SPISlave_T4::pushr(uint32_t data) {
  _lpspi->TDR = data;
}

uint32_t SPISlave_T4::popr() {
  uint32_t data = _lpspi->RDR;
  //_lpspi->SR = LPSPI_SR_WCF; /* Clear WCF */
  return data;
}

void SPISlave_T4::SLAVE_ISR() {
  if ( _spihandler ) {
    _spihandler();
    _lpspi->SR = LPSPI_SR_DMF | LPSPI_SR_REF | LPSPI_SR_TEF | LPSPI_SR_TCF | LPSPI_SR_FCF | LPSPI_SR_WCF;
    asm volatile ("dsb");
    return;
  }
}

void SPISlave_T4::begin(uint8_t bitOrder, uint8_t dataMode) {
  _lpspi->CR = LPSPI_CR_RST;// Reset Module
  _lpspi->CR = 0;           // Disable Module
  _lpspi->FCR = LPSPI_FCR_RXWATER(0) | LPSPI_FCR_TXWATER(0); // x10001; // Watermark for RX and TX
  _lpspi->IER = LPSPI_IER_RDIE; // RX Interrupt
  _lpspi->CFGR0 = 0;        // Verify HRSEL. Should be 1?
  _lpspi->CFGR1 = 0;        // slave, sample on SCK rising edge, !autoPCS (must raise CS between frames), FIFO will stall, CS active low, match disabled,
  _lpspi->CR |= LPSPI_CR_MEN /*| LPSPI_CR_DBGEN*/; /* Enable Module, Debug Mode */
  _lpspi->SR = LPSPI_SR_DMF | LPSPI_SR_REF | LPSPI_SR_TEF | LPSPI_SR_TCF | LPSPI_SR_FCF | LPSPI_SR_WCF; // Clear status register
  _lpspi->TCR = LPSPI_TCR_FRAMESZ(_bits - 1)
    | (!!(dataMode & 2) ? LPSPI_TCR_CPOL : 0)
    | (!!(dataMode & 1) ? LPSPI_TCR_CPHA : 0)
    | (!!(bitOrder == LSBFIRST) ? LPSPI_TCR_LSBF : 0);
  _lpspi->TDR = 0x0;        // dummy data, must populate initial TX slot
  NVIC_ENABLE_IRQ(nvic_irq);
  NVIC_SET_PRIORITY(nvic_irq, 1);
}

C++:
// main.ino

#include "SPISlave_T4.h"

SPISlave_T4 my_spi(0, SPI_8_BITS);
// SPISlave_T4 my_spi(1, SPI_8_BITS);

void setup() {
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Hello World!");
  my_spi.begin();
  my_spi.onReceive(myHandler);
}

void loop() {
  delay(5000); Serial.print("millis: "); Serial.println(millis());
}

void myHandler() {
  while ( my_spi.available() ) {
    Serial.print(my_spi.popr(), HEX);
    Serial.print(" ");
  }
}

Main comments:
  • SPI peripheral works as expected, for example, an output is the following:
    Code:
    Hello World!
    millis: 6528
    millis: 11528
    millis: 16528
    A4 A4 F 80 0 40 0 55 2C 2F
  • SPI1 peripheral does not work as expected. It seems the interrupt is not raised correctly. In order to fix this issue, this post suggests to set SION bit to 1 when setting IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B0_03, which is done in the SPISlave_T4 class constructor (SPISlave_T4.cpp). AD_B0_03, according to teensy pinout diagram is pin 0, CS of SPI1 peripheral.
  • In the case of SPI1 peripheral used, if IER (Interrupt Enable Register) is set to LPSPI_IER_FCIE (Frame Complete Interrupt Enable), it seems that CS is read correctly and the interrupt is raised but data on SDI line are not read, for example:
    Code:
    Hello World!
    ISR. Data read:
I can't make it work. Is there anything I'm missing? Has anyone else already struggled with SPI1 peripheral in slave mode and found a solution?

Thanks,
Luca

EDIT:
The solution is reported here.
 
Last edited:
The _SELECT_INPUT settings are not correct for the LPSPI3 pins you are trying to use.
This is the solution! Not knowing where the error was, I didn't check those values. Now, I can use SPI1:

This is the trick.
Code:
switch (portnum) {
  case 0:
    iomuxc_base[0] = 0; // IOMUXC_LPSPI4_PCS0_SELECT_INPUT. For LSPCI4: (401F_851Ch) 0=GPIO_B0_00_ALT3 1=GPIO_B1_04_ALT1
    iomuxc_base[1] = 0; // IOMUXC_LPSPI4_SCK_SELECT_INPUT . For LSPCI4: (401F_8520h) 0=GPIO_B0_03_ALT3 1=GPIO_B1_07_ALT1
    iomuxc_base[2] = 0; // IOMUXC_LPSPI4_SDI_SELECT_INPUT . For LSPCI4: (401F_8524h) 0=GPIO_B0_02_ALT3 1=GPIO_B1_05_ALT1
    iomuxc_base[3] = 0; // IOMUXC_LPSPI4_SDO_SELECT_INPUT . For LSPCI4: (401F_8528h) 0=GPIO_B0_02_ALT3 1=GPIO_B1_06_ALT1
    break;
  case 1:
    iomuxc_base[0] = 0; // IOMUXC_LPSPI3_PCS0_SELECT_INPUT. For LSPCI3: (401F_850Ch) 0=GPIO_AD_B0_03_ALT7 1=GPIO_AD_B1_12_ALT2
    iomuxc_base[1] = 1; // IOMUXC_LPSPI3_SCK_SELECT_INPUT . For LSPCI3: (401F_8510h) 0=GPIO_AD_B0_00_ALT7 1=GPIO_AD_B1_15_ALT2
    iomuxc_base[2] = 0; // IOMUXC_LPSPI3_SDI_SELECT_INPUT . For LSPCI3: (401F_8514h) 0=GPIO_AD_B0_02_ALT7 1=GPIO_AD_B1_13_ALT2
    iomuxc_base[3] = 1; // IOMUXC_LPSPI3_SDO_SELECT_INPUT . For LSPCI3: (401F_8518h) 0=GPIO_AD_B0_01_ALT7 1=GPIO_AD_B1_14_ALT2
    break;
  }

Thanks again :)
Luca
 
Back
Top