Beware that this post is related to an SDK in maintenance mode
More Info: Consider nRF Connect SDK for new designs
This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Get SPI interrupt even when using NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER flag with nrfx_spim_xfer(). Found workaround.

My code uses the nrfx SPIM driver.  This gives you the option to use interrupts or not.  Interrupts are great for long message transfers, allowing sleep while waiting for the end.  But for very short (one or two byte messages, especially) I found it to be much faster and use less processor cycles to use the NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER flag for those messages and then just poll the END event flag.  This saves all the time to wake up and for the driver to then call my handler to clear the spi_xfer_active flag.  Polling is about 10 times less processor time for these short one or two byte messages.  This also means that I can use those messages (to check if a serial flash chip is still writing or erasing, for example) from an equal priority interrupt handler (like the timer) without worrying about a deadlock with the SPI interrupt handler.

The code I use is shown.  There was one problem that I had to work around.  Though the driver code is not supposed to enable the SPI interrupt if the NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER flag is set, one out of a thousand times the interrupt happens anyway.  So if the polling loop is only looking for the END event, that rare time that the interrupt happens when it shouldn't causes the END flag to be cleared before the loop gets to see it go high, resulting in a hang of my application.  Sometimes it would take 15-30 minutes for this to happen, but it always would eventually.  I verified that the SPI IRQ handler was always being triggered right before the hang with the Ozone trace debugger, and I could see that the spi_xfer_active flag was being cleared by the interrupt handler.  So the workaround is to check both the END register and the spi_xfer_active flag, so either the polling or the interrupt will terminate the wait for the end of the SPI transfer.

I don't understand why these rare interrupts show up, but the workaround masks the problem.

    // The method of detecting the end of the SPI transfer depends on the
    // number of bytes in the sequence.
    if ( MAX(tx_len, rx_len) > MAX_SPI_BYTES_FOR_POLLING_OF_END_EVENT )
    {
        // For longer sequences, use the default method of an SPI interrupt
        // which will wake the processor to call the interrupt handler which
        // will clear the spi_xfer_active flag to signal transmission complete.
        // This allows sleeping while we wait, saving a lot of energy.
        spi_xfer_active = true;
        err_code = nrfx_spim_xfer(&spi, &xfer_desc, 0);
        APP_ERROR_CHECK(err_code);

        if (err_code == NRF_SUCCESS)
        {
            // Sleep while we wait
            while (spi_xfer_active)
            {
                nrf_pwr_mgmt_run();
            }
        }
    } else {
        /* For short sequences it is faster, and thus more efficient, to skip
         * the interrupt and handler overhead and just directly poll the
         * EVENT_END register for this SPI instance.  This allows this routine
         * to be called by an interrupt service routine without a deadlock
         * between the two interrupts, and it is also much faster.  The very
         * frequently used 2 byte sequence of reading the serial flash chip's
         * status register (e.g.- for WIP busy flag) takes 37 usecs to be
         * recognized as done using the interrupt scheme, even though the
         * transmission only takes 4 usecs (@ 4 Mbps).  But with this scheme
         * it takes only 3.6 usecs (signaling the end of RX/TX buffers even
         * before the last bits are clocked out), thus 10 times faster and
         * less energy.
         */

        // Indicate the SPI bus activity with the flag
        spi_xfer_active = true;

        err_code = nrfx_spim_xfer(&spi, &xfer_desc, NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER);
        APP_ERROR_CHECK(err_code);

        if (err_code == NRF_SUCCESS)
        {
            // Wait for the SPI EVENT_END register to flag the end of the TX and RX buffers
            /* Check the spi_xfer_active as well.  This is a workaround for the problem with
             * the SPI interrupt still happening (very rarely) even though the 
             * NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER flag is specified.  When the IRQ happens
             * it clears both spi_xfer_active and the END event bit.  So the workaround
             * ensures we see the end of the SPI transfer via one way or the other rather
             * than hanging here forever for an END event that will never come. */
            while ( !*p_end_event_reg && spi_xfer_active )
                ;
        }
        // The driver doesn't release the ss_pin if there is an event handler
        // but we have overridden it with the NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER flag.
        // So we need to do it ourselves here to tell the chip
        // that this SPI transfer is finished.
        nrf_gpio_pin_write(SF_NRFX_SPIM_SS_PIN, SF_SS_PIN_ACTIVE_STATE ? 0 : 1);

        spi_xfer_active = false;
    }

  • Hello,

    I think it sounds reasonable to not use interrupts in this case. But, I'm not sure what could be causing the interrupt to be triggered while you initiate the transfers with the NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER flag. I tried to reproduce the same here with the SPIM driver from SDK 17.0.2 (nrfx 1.8.6), but haven't had any luck with it thus far. Maybe I'm using a newer version of the driver?

    Here's is the project I used for testing:

    4722.nrfx_spim.zip

  • I looked over the Nordic SPIM driver code.  It is pretty straightforward and I couldn't see any reason that it would cause the spurious interrupts that I was seeing.  I thought about it some more and my only theory was that the module was being reentered somehow.  I realized that I had most of the code that would call this spi_read routine running in the main thread, but there were quick checks of chip ready (through spi_read of 2 byte length, thus no interrupt) that were called from within a timer ISR.  I think that if the timing was right it the timer ISR call might have reentered the spi_read at a point where it didn't trigger the SPI driver busy flag, but did catch it when it was waiting for the interrupt after a long transfer.  I went through the code and made sure to prevent that kind of thing, so hopefully the mystery interrupt is gone.

    Thanks for looking into this.  I'm convinced now that the SPIM driver code is fine, and I must have caused a reentry.  BTW, I had problems in other parts of code with possible reentry, so I started putting a static entry counter in some of the routines, incremented on entry and decremented on exit, and then asserted that it should be 0 on entry.  If there were ever reentry then the ASSERT would catch it.

    Thanks again.

  • Thank you for the update. One thing I did notice during testing was that the transfer function didn't return the busy error even if I removed my wait loop, so maybe the busy error is not a reliable way to catch reentries when the NRFX_SPIM_FLAG_NO_XFER_EVT_HANDLER is used. It sounds like it should be caught by your ASSERTs however.

Related