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

SPI asynchronoues usage

I am currently working on nRF52DK and TCAN4550 using SPI communication. Regardless the SPI slave which I am using, I have some doubts with SPI Nordic library (I am using 17.0.2 SDK).

It I provided three ways to implement SPI communications:

  • SPI Master driver
  • SPI driver
  • SPI HAL

I am using SPI Master Driver, even if it is based on legacy driver “SPI/SPIM peripheral driver”. Therefore, is it better to use SPI HAL?? I tried without success….

I have implemented my read/write functions using a non-blocking transfer function “nrf_drv_spi_transfer”. It works and I am able to read/write CAN frames by sending and receiving SPI commands.

My other doubt begin now, I have configured a GPIO HW interrupt in order to know when I receive a new CAN frame, and afterwards read using SPI library to get that frame. If I use a non-blocking transfer inside of a GPIO handler, my program breaks due to a handler inside of another handler… It is normal. I saw this other topic Is nrf_drv_spi_transfer interrupt safe? In which Nordic support recommended “SPI transaction manager library” but is a post 3 years old… so… I am not sure if it continues being the best answer.

You can find the related code below.

main.c

 

/**
 * @file main.c
 * @date 30 april 2021
 * @brief Main file
 */

#include "nrf_drv_spi.h"
#include "app_util_platform.h"
#include "nrf_gpio.h"
#include "nrf_delay.h"
#include "boards.h"
#include "app_error.h"
#include <string.h>
#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"
#include "nrf_drv_gpiote.h"
#include "SPI.h"
#include "tcan4x5x/TCAN4550.h"
#include "EBM_TCAN4550.h"

#ifdef ARDUINO_0_PIN
    #define CAN_INT ARDUINO_0_PIN
#endif

extern void TCAN4550_device_init(void);
extern void send_CAN_frame(uint16_t ID, uint8_t DLC, uint8_t* data);
extern void read_CAN_frame_due_interrupt(uint16_t* ID, uint8_t* DLC, uint8_t* data);

static volatile bool frame_received; 

/**
 * @brief GPIO interrupt handler
 * @param pin interrupt number
 * @param action polarity
*/
void canIntHandler(nrf_drv_gpiote_pin_t pin, nrf_gpiote_polarity_t action)
{
    NRF_LOG_INFO("[main::canIntHandler] GPIO interrupt: %d.", pin);
    uint16_t ID;
    uint8_t DLC;
    uint8_t data[8];
    read_CAN_frame_due_interrupt(&ID, &DLC, data);
    if (ID == 0x0AA)		// Example of how you can do an action based off a received address
    {
      // Do something
      bsp_board_led_invert(BSP_BOARD_LED_1);
    }
    bsp_board_led_invert(BSP_BOARD_LED_1);
}

/**
 * @brief GPIO initial configuration
 */
void gpio_init(void)
{
    ret_code_t err_code;

    err_code = nrf_drv_gpiote_init();
    APP_ERROR_CHECK(err_code);

    nrf_drv_gpiote_in_config_t in_config = GPIOTE_CONFIG_IN_SENSE_HITOLO(true);
    in_config.pull = NRF_GPIO_PIN_PULLUP;

    err_code = nrf_drv_gpiote_in_init(CAN_INT, &in_config, canIntHandler);
    APP_ERROR_CHECK(err_code);

    nrf_drv_gpiote_in_event_enable(CAN_INT, true);
}

/**
 * @brief Main function
 * @return value
 */
int main(void)
{
    bsp_board_init(BSP_INIT_LEDS);
    APP_ERROR_CHECK(NRF_LOG_INIT(NULL));
    NRF_LOG_DEFAULT_BACKENDS_INIT();

		spi_init();
    gpio_init();

		TCAN4550_device_init();
	
    uint8_t data[8] = {0,1,2,3,4,5,6,7};
		TCAN4x5x_MCAN_ClearInterruptsAll();
       
    //testTCAN4550();
    
		while (1)
    {            
				send_CAN_frame(111, 8, data);
        bsp_board_led_invert(BSP_BOARD_LED_0);
        nrf_delay_ms(200);
        NRF_LOG_FLUSH();
    }
}

#define WAIT_FOR_TRANSMIT   while (!spi_xfer_done) {  __WFE(); }

void spi_event_handler(nrf_drv_spi_evt_t const * p_event, void * p_context)
{
    spi_xfer_done = true;    
}

void spi_init(void)
{
		nrf_drv_spi_config_t spi_config;
    spi_config.sck_pin      = SPI_SCK_PIN,          							     \
    spi_config.mosi_pin     = SPI_MOSI_PIN,                						 \
    spi_config.miso_pin     = SPI_MISO_PIN,                						 \
    spi_config.ss_pin       = NRF_DRV_SPI_PIN_NOT_USED,                \
    spi_config.irq_priority = SPI_DEFAULT_CONFIG_IRQ_PRIORITY,         \
    spi_config.orc          = 0xFF,                                    \
    spi_config.frequency    = NRF_DRV_SPI_FREQ_1M,                     \
    spi_config.mode         = NRF_DRV_SPI_MODE_0,                      // It is the only mode which is supported by TCAN4550
    spi_config.bit_order    = NRF_DRV_SPI_BIT_ORDER_MSB_FIRST,         \
    
		nrf_drv_spi_init(&spi0, &spi_config, spi_event_handler, NULL); 
    //nrf_drv_spi_init(&spi0, &spi_config, NULL, NULL); 
    nrf_gpio_cfg_output(SPI_SS_PIN);
}

void spi_transfer(uint8_t *tx_data, unsigned tx_len, uint8_t *rx_data, unsigned rx_len)
{
  spi_xfer_done = false;
  memset(rx_data, 0, m_length);
  memset(m_rx_buf, 0, m_length);
	ret_code_t ret = nrf_drv_spi_transfer(&spi0, tx_data, tx_len, m_rx_buf, rx_len);
  WAIT_FOR_TRANSMIT
  if(ret != NRF_SUCCESS) {
    NRF_LOG_INFO("[SPI::spi_transfer] Transfer fail.");
  }
  memcpy(rx_data, m_rx_buf,8);
}

Which is the best way to manage the SPI transfer asynchronously? I have to configure the "nrf_drv_spi_transfer" as blocking transfer?

Many thanks in advanced and regards,

Javier

Parents
  • Hi Javier, 

    I am using SPI Master Driver, even if it is based on legacy driver “SPI/SPIM peripheral driver”. Therefore, is it better to use SPI HAL??

    The Hardware Abstraction Layer (HAL) ensures safe register access to a peripheral but is often limited in features. A driver sits atop the HAL and expands on its feature set. Whether you want to use the HAL or driver depends on your specific use-case, many drivers ensure reliable operation of the peripherals that might otherwise be tricky to do by writing a driver from scratch.  

    My other doubt begin now, I have configured a GPIO HW interrupt in order to know when I receive a new CAN frame, and afterwards read using SPI library to get that frame. If I use a non-blocking transfer inside of a GPIO handler, my program breaks due to a handler inside of another handler… It is normal.

    You can do the transfers outside of the GPIOTE handler, by setting a flag and start the transfer in main-loop if the flag is set. Alternatively, you can run SPI at a higher priority than the GPIOTE, to allow the SPI handler to preempt the current interrupt context, to handle the events and set the trasnfer-done flag.

    -Amanda H.

  • Hello Amanda,

    First, thanks for your support. Regarding the first question, now it is clearer.

    Considering the second question, I have just changed the priorities, giving to SPI priority 3 and GPIOTE keep in 6. If I keep the SPI transfer function inside of GPIO handler it now works, but I am not sure if I am missing received CAN frames.

    Now I want to ask you for your opinion /recommendation for one of the following solutions:

    • Blocking SPI transfer function inside of GPIO handler.
    • Non-blocking SPI transfer function inside of GPIO handler.
    • Non-blocking SPI transfer outside GPIO handler, using a flag in main-loop.

    Which one is better in terms of robustness??

    I have also performed a test using a flag and checking it inside main-loop, but it exits also the possibility to lose some CAN frame due to I have a sleep inside the loop. Any ideas to solve this?

    Many thanks and best regards,

    Javier

  • Hi Javier, 

    In general, it is best to not spend long periods of time inside the interrupt handler, as you will be blocking other events with same or lower priority from being handled. Blocking transfer will most likely take the same time or longer than non-blocking, so no difference there. If they should do it inside the handler or outside depends on what else is running in their application, and how long time the transfer will take.

    Not sure I understand the concern about missing CAN frames due to sleep in the loop. The GPIOTE handler will still run in interrupt context and will wake the chip from sleep and jump to the handler before executing the rest of the code in the main-loop. The rest of the code in main-loop will be executed before going back to sleep (unless they have multiple sleep statements in the loop.

    If they expect more than one CAN frame to arrive before they are able to read out the frames over SPI, they may need some scheduling mechanism that indicate how many frames should be read.

    Another option is to use the Advanced usage features in the SPI driver to trigger the SPI transfer directly over PPI from the GPIOTE event. This only works if the transfer can be completed and processed before the next CAN frame arrives, as PPI does not have any ways to check if a transfer is already ongoing.

    -Amanda H.

Reply
  • Hi Javier, 

    In general, it is best to not spend long periods of time inside the interrupt handler, as you will be blocking other events with same or lower priority from being handled. Blocking transfer will most likely take the same time or longer than non-blocking, so no difference there. If they should do it inside the handler or outside depends on what else is running in their application, and how long time the transfer will take.

    Not sure I understand the concern about missing CAN frames due to sleep in the loop. The GPIOTE handler will still run in interrupt context and will wake the chip from sleep and jump to the handler before executing the rest of the code in the main-loop. The rest of the code in main-loop will be executed before going back to sleep (unless they have multiple sleep statements in the loop.

    If they expect more than one CAN frame to arrive before they are able to read out the frames over SPI, they may need some scheduling mechanism that indicate how many frames should be read.

    Another option is to use the Advanced usage features in the SPI driver to trigger the SPI transfer directly over PPI from the GPIOTE event. This only works if the transfer can be completed and processed before the next CAN frame arrives, as PPI does not have any ways to check if a transfer is already ongoing.

    -Amanda H.

Children
  • Hello Amanda,

     

    Many thanks for your explanation, now I understand better how to manage the interrupts. Moreover, I did not know this:

    The GPIOTE handler will still run in interrupt context and will wake the chip from sleep and jump to the handler before executing the rest of the code in the main-loop. The rest of the code in main-loop will be executed before going back to sleep (unless they have multiple sleep statements in the loop.

    Also, I think that I have to implement some scheduling mechanism as you said, in order to be sure that I am not missing CAN frames.

    Thanks again for the support, I think that we can tick as close this topic.

    Best regards,

    Javier

Related