This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Unexpected EVENTS_ENDRX/EVENTS_ENDTX SPIM interrupts when only EVENTS_END interrupt enabled

There is an SPIM unexpected interrupt issue which I have replicated in a totally bare-bones piece of code using no Nordic library drivers. In my bare-bones SPI code I get 2 interrupts instead of the expected 1 at the end of every SPI transaction when only the EVENTS_END interrupt is enabled. This is checked by having a volatile counter which never resets; the count should match the expected number of SPI interrupts but in fact shows double (2x) the expected number. I suspect this to be an SPI errata, but have been too busy to chase it down.
With the same setup and function code, the following handler results in double interrupts for every transaction:

volatile uint32_t mSpiInterruptCounter = 0UL;
uint32_t mSpiCorrectResponseCounter = 0UL;

void SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler(void)
{
  mSpiInterruptCounter++;
  NRF_SPIM0->EVENTS_END = 0;
}

Modifying the interrupt handler to this code removes the problem, keep in mind EVENTS_ENDRX and EVENTS_ENDTX are not enabled::

void SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler(void)
{
  mSpiInterruptCounter++;
  if (NRF_SPIM0->EVENTS_END) NRF_SPIM0->EVENTS_END = 0;
  if (NRF_SPIM0->EVENTS_ENDRX) NRF_SPIM0->EVENTS_ENDRX = 0;
  if (NRF_SPIM0->EVENTS_ENDTX) NRF_SPIM0->EVENTS_ENDTX = 0;
}

The other odd thing is that either clearing ENDRX or clearing ENDTX fixes the issue; both do not need to be cleared.

This is the initialisation and repeated transfer code in both the above cases:

#define LIS2DH12_WHO_AM_I  0x0F  // Read as 0x33
#define I_AM_LIS3DH        0x33

uint8_t spiXBuf[] = {0x80|LIS2DH12_WHO_AM_I, 0xFF};
uint8_t spiRXBuf[sizeof(spiTXBuf)] = {0,0};
static const uint8_t mRxTxBufLength = sizeof(spiTXBuf);

static void Test4WireSPI(void)
{
  NRF_GPIO->PIN_CNF[CSPIN]   = 0x301;  // output, high drive high and low H0H1
  NRF_GPIO->PIN_CNF[SCKPIN]  = 0x301;  // output, high drive high and low H0H1
  NRF_GPIO->PIN_CNF[MOSIPIN] = 1;      // output, standard drive S0S1
  NRF_GPIO->PIN_CNF[MISOPIN] = 0;      // input pin, input buffer connected, no pull, S0S1, sense disabled
  NRF_GPIO->OUTSET = 1 << CSPIN;       // deactivate by setting chip select high
  NRF_SPIM0->PSEL.SCK = SCKPIN;
  NRF_SPIM0->PSEL.MOSI = MOSIPIN;
  NRF_SPIM0->PSEL.MISO = MISOPIN;
  NRF_SPIM0->CONFIG = 0;               // CPOL 0 -- clock polarity active high, CPHA 1 -- sample on trailing clock edge, send Msb first
  NRF_SPIM0->FREQUENCY = 0x80000000UL; // 8 Mbps
  NRF_SPIM0->ORC =0;                   // Unused Tx bytes, set all low

  NRF_SPIM0->EVENTS_ENDTX = 0;
  NRF_SPIM0->EVENTS_ENDRX = 0;
  // Disable all interrupts
  NRF_SPIM0->INTENCLR = 0xFFFFFFFFUL;
  // Enable selected interrupts
  NRF_SPIM0->INTENSET = 0x040;   // END
  //NRF_SPIM0->INTENSET = 0x010;   // END_RX - not enabled!
  //NRF_SPIM0->INTENSET = 0x100;   // END_TX - not enabled!
  
  // Set interrupt priority and enable interrupt
  NVIC_SetPriority(SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQn, 6);
  NVIC_ClearPendingIRQ(SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQn);
  NVIC_EnableIRQ(SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQn);

  NRF_SPIM0->ENABLE = 7;               // enable SPI
  mSpiInterruptCounter = 0UL;
  // Test some arbitrary number of SPI command requests
  for (uint32_t i = 0; i<40; i++)
  {
    // Clear response byte
    spiRXBuf[1] == '?';
    NRF_SPIM0->TXD.PTR = (uint32_t)spiTXBuf;
    NRF_SPIM0->TXD.MAXCNT = mRxTxBufLength;
    NRF_SPIM0->RXD.PTR = (uint32_t)spiRXBuf;
    NRF_SPIM0->RXD.MAXCNT = mRxTxBufLength;
    NRF_GPIO->OUTCLR = 1 << CSPIN;  // drive cs low to initiate spi comm
    NRF_SPIM0->TASKS_START = 1;
    while(!NRF_SPIM0->EVENTS_ENDTX);  //last byte transmitted
    while(!NRF_SPIM0->EVENTS_ENDRX);  //last byte received
    NRF_SPIM0->TASKS_STOP = 1;
    NRF_GPIO->OUTSET = 1 << CSPIN;  // drive cs high to end spi comm
    // Ensure transfer has completed and SPI stopped
    while(!NRF_SPIM0->EVENTS_STOPPED);
    // Check result
    if (spiRXBuf[1] == I_AM_LIS3DH) mSpiCorrectResponseCounter++;
  }
  if (mSpiInterruptCounter == mSpiCorrectResponseCounter)
  {
    // test pass
  }
}

I will post this code in the bare-bones Git repo as I'm sure others will find it useful. Meanwhile I would like to not have the extra instructions in the interrupt handler in the interests of reducing cpu cycles

Parents
  • Hi,

    This is odd. There are no errata that should explain this, and if you look at the nrfx_spim implementation, you will see that it also use the END event only, and does not touch ENDRX or ENDTX. Are you by any chance using an old engineering sample of the nRF52832, or are you using a production version (rev 1 or rev 2)? Does it make a difference if you clear the event before incrementing mSpiInterruptCounter?

Reply
  • Hi,

    This is odd. There are no errata that should explain this, and if you look at the nrfx_spim implementation, you will see that it also use the END event only, and does not touch ENDRX or ENDTX. Are you by any chance using an old engineering sample of the nRF52832, or are you using a production version (rev 1 or rev 2)? Does it make a difference if you clear the event before incrementing mSpiInterruptCounter?

Children
  • Thanks for the response. We have Rev 1, I think, in a WLCSP package within a module:

    // 13.1.17 INFO.VARIANT - Address offset: 0x104
    // Part Variant, Hardware version and Production configuration
    // Bit number 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
    //         Id  A  A  A  A  A  A  A  A  A  A  A  A  A  A  A  A  A  A  A  A  A  A A A A A A A A A A A
    // Reset 0x41414142 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 1 0 1 0 0 0 0 1 0
    //
    // Hardware version and Production configuration, encoded as ASCII
    //   AAAA 0x41414141 AAAA
    //   AAAB 0x41414142 AAAB
    //   AABA 0x41414241 AABA
    //   AABB 0x41414242 AABB
    //   AAB0 0x41414230 AAB0
    //   AAE0 0x41414530 AAE0
    //        0xFFFFFFFF Unspecified
    // We have 41414230 = "AAB0"

    I will try a Rev.2 if we have one here. Interestingly your suggestion of moving the event clear within the interrupt to before the ++ fixes the problem. Looking at the generated assembly for the two instructions shows no difference when changing order. How does this work? Clearly this could be an issue within other interrupts.

  • Hi,

    Revision 1 and 2 should be the same, so that is OK. It looks like it was just a timing issue so that by clearing the event on the last line it was not actually cleared by the time the ISR exited, and therefor the interrupt was still active, and the ISR was called again. So you should make sure to clear the event earlier in the ISR, or read it back or similar to ensure that it is actually cleared before returning from the ISR.

  • Is the Data Synchronization Barrier instruction "dsb" implemented in the nRF52832? This code generates the interrupt 12 times instead of once:

    void SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler(void)
    {
      // Clearing ->EVENTS_END MUST be first or add void read or barrier else get double interrupt ..
      pSPIM->EVENTS_END = 0;
      // Data Synchronization Barrier: completes when all explicit memory accesses before this instruction complete
      __DSB();
    }

    In debug mode, the following works ok and onlt generates a single interrupt:

    void SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0_IRQHandler(void)
    {
      // Clearing ->EVENTS_END MUST be first or add void read or barrier else get double interrupt ..
      pSPIM->EVENTS_END = 0;
      // Data Synchronization Barrier: completes when all explicit memory accesses before this instruction complete
      __ASM volatile ("dsb 0xF":::"memory");
    }

    My understanding is that "dsb" ensures all bus activity completes before the instruction terminates and continues with the next; is that incorrect? See cmsis_gcc.h in .\components\toolchain\cmsis\include for definition

    I see that the instruction is used in nrf_log_frontend.c:

    bool nrf_log_frontend_dequeue(void)
    {
    
        if (buffer_is_empty())
        {
            return false;
        }
        m_log_data.log_skipped      = 0;
        //It has to be ensured that reading rd_idx occurs after skipped flag is cleared.
        __DSB();

    Maybe this needs to be added to sample interrupt example code ..

  • Hi,

    First of all, the DSB instruction is implmented in the nRF52832. Secondly, "__DSB();" and "__ASM volatile ("dsb 0xF":::"memory");" should be equivalen.

    I am having some problems understanding this. Even if _DSB() was not working, clearing the event with "pSPIM->EVENTS_END = 0;" only takes a few cycles, so it should not be possible. Therefore I wonder how you can know that the interrupt is generated 12 times when not debugging using this code? Is something else happening simultaneously?

Related