How to stop SPI with PPI when using EasyDMA Arraylist feature

Hi,

On a NRF52840, I try to perform `x` SPI transactions of 3 bytes each using EasyDMA Arraylist feature fully controlled by hardware. The CS must be toggled between each block of 3 bytes so I use CS HW of SPIM3.

Example of a transaction:

I need everything to be hardware controlled as SoftDevice is running and has higher interrupts priority than my app, I do not control the software timing precisely. My slot window is very tiny and a precise timing is required.

The start is not a problem, triggered by a GPIOTE but I don't know how to stop the SPI in hardware.

Here is the code I tried:

#define NUM_SPI_TRANSACTION 10
#define SPI_CMD_SIZE 3

uint8_t spi_buf[NUM_SPI_TRANSACTION * SPI_CMD_SIZE];

void configure_timer_ppi(void) {
  /* Configure timer1 in counter mode */
  NRF_TIMER1->TASKS_STOP = 1;
  NRF_TIMER1->TASKS_CLEAR = 1;
  NRF_TIMER1->MODE = TIMER_MODE_MODE_LowPowerCounter;
  NRF_TIMER1->BITMODE = TIMER_BITMODE_BITMODE_08Bit;
  /* Stop timer when it reaches the expected value */
  NRF_TIMER1->SHORTS = TIMER_SHORTS_COMPARE1_STOP_Enabled
                       << TIMER_SHORTS_COMPARE1_STOP_Pos;

  /* Configure number of SPI transactions to trigger */
  NRF_TIMER1->CC[0] = NUM_SPI_TRANSACTION;
  NRF_TIMER1->CC[1] = NRF_TIMER1->CC[0];

  NRF_TIMER1->TASKS_START = 1;
}

void configure_ppi(void) {
  /* Increment timer counter when SPI transaction is done */
  sd_ppi_channel_assign(0, &NRF_SPIM3->EVENTS_END, &NRF_TIMER1->TASKS_COUNT);
  /* Stop SPI when counter reaches the right amount of SPI transactions */
  sd_ppi_channel_assign(1, &NRF_TIMER1->EVENTS_COMPARE[0],
                        &NRF_SPIM3->TASKS_STOP);

  sd_ppi_channel_enable_set(0);
  sd_ppi_channel_enable_set(1);
}

void configure_spi_dma(void) {
  NRF_SPIM3->TASKS_STOP = 1;
  NRF_SPIM3->TXD.MAXCNT = SPI_CMD_SIZE;
  NRF_SPIM3->RXD.MAXCNT = 0;
  /* Configure DMA in array_list */
  NRF_SPIM3->TXD.LIST = SPIM_TXD_LIST_LIST_ArrayList << SPIM_TXD_LIST_LIST_Pos;
  NRF_SPIM3->TXD.PTR = (uint32_t)(spi_buf);
  NRF_SPIM3->EVENTS_END = 0;
  /* When a SPI transacation is done, starts SPI again */
  NRF_SPIM3->SHORTS = SPIM_SHORTS_END_START_Enabled
                      << SPIM_SHORTS_END_START_Pos;

  /* Also configure the rest of SPI including the CS HW */
}

int main(void) {
  /* Init */
  ble_stack_init();
  gap_params_init();
  gatt_init();
  advertising_init();
  services_init();
  conn_params_init();

  configure_timer_ppi();
  configure_ppi();
  configure_spi_dma();

  /* Trigger spi start */
  NRF_SPIM3->TASKS_START = 1;

  /* Enter main loop */
  for (;;) {
  }
  return 0;
}

With this code, the SPI never stops, I guess this is due to the short of the SPI, it has to be disabled before stopping it but this is not mentioned in the datasheet. How to deactivate shorts with hardware? Why the SPI does not stop? I also tried to increment counter timer on `NRF_SPIM3->EVENTS_STARTED` and `NRF_SPIM3->EVENTS_STOPPED`, same result.

Then I tried to pause the SPI with PPI instead of stopping it `NRF_SPIM3->TASKS_SUSPEND`

void SWI0_IRQHandler(void) {
  /* Clear SWI event */
  NRF_EGU0->EVENTS_TRIGGERED[0] = 0;

  /* Disable short before stopping SPI */
  NRF_SPIM3->SHORTS = SPIM_SHORTS_END_START_Disabled;
  NRF_SPIM3->TASKS_STOP;
}

void configure_ppi(void) {
  /* Increment timer counter when SPI transaction is done */
  sd_ppi_channel_assign(0, &NRF_SPIM3->EVENTS_END, &NRF_TIMER1->TASKS_COUNT);
  /* Stop SPI when counter reaches the right amount of SPI transactions */
  sd_ppi_channel_assign(1, &NRF_TIMER1->EVENTS_COMPARE[0],
                        &NRF_SPIM3->TASKS_SUSPEND);

  /* Generate a software interrupt when counter reaches the right amount of SPI
   * transactions */
  NRF_EGU0->INTENSET = EGU_INTENSET_TRIGGERED0_Enabled
                       << EGU_INTENSET_TRIGGERED0_Pos;

  NVIC_SetPriority(SWI0_EGU0_IRQn, IRQ_PRIO_LOWEST);
  NVIC_ClearPendingIRQ(SWI0_EGU0_IRQn);
  NVIC_EnableIRQ(SWI0_EGU0_IRQn);
  NRF_PPI->FORK[1].TEP = (unsigned int)&NRF_EGU0->TASKS_TRIGGER[0];

  sd_ppi_channel_enable_set(0);
  sd_ppi_channel_enable_set(1);
}

It does not work as expected, it pauses but after sending a byte. I think this is due to short of SPI being executed before PPI, right? From datasheet, chapter 6.1.7 Shortcuts:
> [...] However, the propagation delay through the shortcut is usually shorter than the propagation delay through the PPI.

Transaction example:

Then, the SPI block seems stuck, calling `NRF_SPIM3->TASKS_STOP` in the app has no effect, the CS remains asserted. Also I cannot start another SPI transaction. Why calling `NRF_SPIM3->TASKS_STOP` or `NRF_SPIM3->TASKS_RESUME` has no effect, how to unlock the SPI block?

I tried another solution involving software interrupt to stop the SPI but this is not accurate, the handle timing of the interrupt by the app is not predictable.

I also tried without short by connecting the `NRF_SPIM3->EVENTS_END` to `NRF_SPIM3->EVENTS_START` in PPI but the result is the same, SPI block seems stuck at some point.

Is it possible to perform multiple SPI transaction fully hardware controlled, without involving software interrupt?

  • Hello,

    Sorry for the late reply. I needed to do some testing, and the more I tested, the more issues I found, so now I am at the bottom of this rabbit hole. Let me explain:

    First of all, one reason your original snippet isn't working is because of the 

      sd_ppi_channel_enable_set(0);
      sd_ppi_channel_enable_set(1);

    These are channel maps, so sd_ppi_channel_enable_set(0) doesn't do anything, and sd_ppi_channel_enable_set(1) enables channel 0. Also, it is a good idea to check the return values.

    ret_code_t err_code;
    uint32_t ppi_channel_map;
    err_code = sd_ppi_channel_enable_set(1<<0);
    APP_ERROR_CHECK(err_code);
    err_code = sd_ppi_channel_enable_set(1<<1);
    APP_ERROR_CHECK(err_code);
    
    err_code = sd_ppi_channel_enable_get(&ppi_channel_map);
    APP_ERROR_CHECK(err_code);
    
    NRF_LOG_INFO("PPI channel map 0x%08x", ppi_channel_map);

    Also, check the return values from sd_ppi_channel_assign().

    The reason why NRF_SPIM3->TASKS_STOP doesn't work in your application is that this task also generates the EVENTS_END event, which will trigger the next start task for the SPIM3. Using PPI to toggle a GPIO on the events end looks like this after you trigger the TASKS STOP:

    Where channel 4 is the pin that toggles on the EVENTS_END.

    It is a tricky task to do what you request without the additional byte. You can use the TASKS_SUSPEND (via TIMER and PPI), but I found that when I tried to do TASKS_RESUME, the SPIM3 got a STALLSTAT (the register NRF_SPIM3->STALLSTAT triggered). This is related to an errata in SPIM3. It does not happen in SPIM0-2, which I tested, but those doesn't support HW CSN signal, so I guess that is not an option. so either you need to apply a workaround for the errata:

    Errata 198

    It is a bit cryptic, but search for errata 198 here on DevZone, and if you are stuck, please let me know what IDE you are using.

    Another possible workaround is to either try to implement the CS gpio using PPI as well, and then use SPIM0-2 instead of SPIM3. Alternatively, you can call NRF_SPIM3->TASKS_STOP =1; to restart the SPI transmissions. However, before you do this you should reset the counter (NRF_TIMER1->TASKS_CLEAR = 1, and please note that calling TASKS_STOP will trigger the first count). This will also not solve the issue that one extra byte will be send, because the NRF_SPIM3->TASKS_SUSPEND will finish the ongoing byte before stopping the transaction.

    Best regards,

    Edvin

Related