SPI and I2C communication in background

ghost07
Posts: 36
Joined: Mon Oct 03, 2022 11:47 am

SPI and I2C communication in background

Postby ghost07 » Wed Aug 23, 2023 1:34 pm

Hello,
I would like to know more about SPI and I2C peripherals design.
Are they capable to work simultaneously in background?

In the project I collect samples from 2 devices:
1. ADC (AD7124-4) connected via SPI
2. Accelerometer (ADXL345) connected via I2C
Samples will be processed by FFT later, so there mustn't be any sample misses.

I've pinned all tasks to CPU0 and only two tasks are pinned to CPU1 (ADC task and Acc task).
Lets call ADC task a "Task A" and Acc. task a "Task B".
For now I focus to optimize Task A, so the Task B is suspended and the only task running on CPU1 should be Task A.
And yet, when I measure time delay between receiving interrupt from ADC, and finished reading of sample, it is typically 170 us (which is OK), but sometimes it is up to 4000 us (4 ms), which means that we've already missed at least 4 samples (sampling frequency is 1200 Hz).
So it seems something else is running on CPU1 besides Task A.
This longer delay usually happens after printing task stats to console (vTaskList). But that print is called from app_main(), which should be pinned to CPU0, at least it is set so in sdkconfig.

How I can check which tasks are actually running (pinned or unpinned) on particular core?

Does SPI and UART peripheral share some resources? (spi_device_transmit() takes longer when something is printed to console)

Is it safe to call "spi_device_queue_trans()" in ISR handler?

And regarding I2C I would like to know, if the transaction is blocking CPU or not. Since the Task B is going to spend 99% of time on I2C communication, will be the CPU able to switch to Task A while Task B is waiting for next bit or byte?

Is I2C peripheral able to handle transaction in the background after setting up the "link", or it needs CPU's attention for each bit?

MicroController
Posts: 1696
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: SPI and I2C communication in background

Postby MicroController » Thu Aug 24, 2023 9:42 am

ghost07 wrote:
Wed Aug 23, 2023 1:34 pm
when I measure time delay between receiving interrupt from ADC...
How do you measure that? 4ms latency would be very unsusual.
Does SPI and UART peripheral share some resources? (spi_device_transmit() takes longer when something is printed to console)
No. But formatting an output string and sending it to the console is relatively "expensive" (not "4ms expensive" however); depending on the priority of the task printing, it may rob other tasks of CPU time for a moment.
Is I2C peripheral able to handle transaction in the background after setting up the "link", or it needs CPU's attention for each bit?
Yes, I2C is handled "in the background" via interrupts; not much CPU required during the transaction. The task executing the transaction is blocked while waiting for it to finish, releasing the CPU to other tasks.

For SPI, you can choose to use DMA transfer which, once started, handles the whole transfer without any involvement of the CPU.

ghost07
Posts: 36
Joined: Mon Oct 03, 2022 11:47 am

Re: SPI and I2C communication in background

Postby ghost07 » Thu Aug 24, 2023 3:43 pm

MicroController wrote: How do you measure that? 4ms latency would be very unsusual.

I get value of "esp_timer_get_time()" in ISR handler, then in the task function and calculate difference.

Code: Select all

static void IRAM_ATTR gpio_isr_handler(void *arg) 
{
    BaseType_t xHigherPriorityTaskWoken;

    isr_time = esp_timer_get_time();

    if (task_id >= TASK_COUNT || _task_handles[task_id] == NULL) return;

    xTaskNotifyFromISR(adc_task_handle,  1UL << ADC_VALUE_RECEIVED, eSetBits, &xHigherPriorityTaskWoken);

    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
The task's loop has "xTaskNotifyWait()" that waits for any notification and then calls "onResume" function.

Code: Select all

static void onResume(uint32_t event)
{
    int32_t data;
    int8_t channel;
    float value;

    // check event id
    if (!(event & 1UL << ADC_VALUE_RECEIVED)) return;

    int64_t curr_time = esp_timer_get_time();
    uint32_t duration_us = curr_time - isr_time;
    if (duration_us > max_duration_us) max_duration_us = duration_us;
    if (duration_us < min_duration_us) min_duration_us = duration_us;
    
    // read new sample
    gpio_intr_disable(pinout->miso);
    ad7124_read_bipolar(&data, &channel);
    gpio_intr_enable(pinout->miso);

    // Some processing...
}
Print of these stats is done in another task pinned to Core 0:

Code: Select all

void print_isr_stats(void *pvParameters) 
{
    while (1) 
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
        ESP_LOGD(LOG_TAG, "Sample processed in (min: %d us, max: %d us)", min_duration_us, max_duration_us);
        max_duration_us = 0;
        min_duration_us = 999999;
    }
}
Output sample:

Code: Select all

D (37269) ADC_TASK: Sample processed in (min: 15 us, max: 2341 us)
D (38269) ADC_TASK: Sample processed in (min: 15 us, max: 46 us)
D (39269) ADC_TASK: Sample processed in (min: 15 us, max: 37 us)
D (40269) ADC_TASK: Sample processed in (min: 15 us, max: 31 us)
D (41269) ADC_TASK: Sample processed in (min: 15 us, max: 31 us)
D (41480) MAIN: 
main            X       1       1908    4       0
IDLE            R       0       1020    6       1
IDLE            R       0       1008    5       0
adc_aux_task    B       3       356     21      0
sntp_task       B       10      324     12      0
adc_task        B       22      2188    20      1
tiT             B       18      2432    9       0
ipc1            B       22      1004    2       1
ipc0            B       24      1080    1       0
Tmr Svc         B       1       1592    7       0
sys_evt         B       20      908     10      0
httpd           B       5       3276    15      0
esp_timer       S       22      3320    3       0
wifi            B       23      4504    11      0
modbus_tcp_srv  B       5       2276    18      0
modbus_rtu      B       5       2148    19      0

D (42269) ADC_TASK: Sample processed in (min: 15 us, max: 2585 us)
D (43269) ADC_TASK: Sample processed in (min: 15 us, max: 31 us)
D (44269) ADC_TASK: Sample processed in (min: 15 us, max: 31 us)
D (45269) ADC_TASK: Sample processed in (min: 15 us, max: 31 us)
D (46269) ADC_TASK: Sample processed in (min: 15 us, max: 31 us)
That lag spike is always after a MAIN print.
On Core 1 is now running only IDLE, ipc1 and adc_task, so I have no idea what could be causing that lag. Something is maybe trying to access a shared resource? But which one and how to avoid it?

Btw. why does it take at least 15 us to jump into task function? There is just a few "if" statements and CPU is running at 160 MHz, so in 15 us it has to be 2400 instructions.

ESP_Sprite
Posts: 9709
Joined: Thu Nov 26, 2015 4:08 am

Re: SPI and I2C communication in background

Postby ESP_Sprite » Fri Aug 25, 2023 12:51 am

Do you also use WiFi? If so, it may do a write to flash, which hangs up everything that's not specifically marked to be running from IRAM.

MicroController
Posts: 1696
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: SPI and I2C communication in background

Postby MicroController » Fri Aug 25, 2023 9:00 am

The task's loop has "xTaskNotifyWait()" that waits for any notification and then calls "onResume" function.
Note that the lag you observe may also be caused by the time the task needs for the processing between its calls to xTaskNotifyWait, which may be delayed for a number of reasons.

Also, you may want to check that the ISR is registered to run on the same core as the task.

You can explore the option of polling the ADC state via I2C instead of using its interrupt.
Btw. why does it take at least 15 us to jump into task function? There is just a few "if" statements and CPU is running at 160 MHz, so in 15 us it has to be 2400 instructions.
15µs is not unreasonable here. Note that the ISR doesn't "jump" to a task but rather causes FreeRTOS, after the ISR is done, to find the new highest-priority runnable task and perform a context switch from the fomerly running (idle) task to the task woken up.

ghost07
Posts: 36
Joined: Mon Oct 03, 2022 11:47 am

Re: SPI and I2C communication in background

Postby ghost07 » Tue Aug 29, 2023 5:25 pm

So far, I've found out that delay is caused by vTaskList(), which calls uxTaskGetSystemState() which uses vTaskSuspendAll().

However, vTaskSuspendAll() is supposed:
1) to suspend scheduler only on the core it was called from
2) not to disable interrupts

But it blocks ISR handler on the other core for about 2ms (depends on how many tasks is created).
So, is it possible that my ISR handler is actually called from Core 0, prehaps isr0 task?
Even tho ISR handler itself reports that is running on Core 1 (xPortGetCoreID()).

By ISR handler I mean function that I passed to gpio_isr_handler_add().

And one more question - is is possible to monitor how often was context switched to IDLE task?
(Or how many times was context switched in general?)

ghost07
Posts: 36
Joined: Mon Oct 03, 2022 11:47 am

Re: SPI and I2C communication in background

Postby ghost07 » Mon Sep 04, 2023 3:38 pm

Is it possible to setup auto-triggered repetitive SPI transactions?
When there is a new sample, ADC sets DOUT/^RDY pin to low as a form of interrupt. DOUT/^RDY is actually MISO line.
Can be a SPI instructed to wait for this interrupt and then read 32-bits, store it to DMA, and wait for another such interrupt?
So in my application code I can just simply fetch buffered samples?

Right now I attach GPIO interrupt to MISO pin.
Then the ISR handler unblocks a task which:
1. disables GPIO interrupt
2. calls spi_device_transmit()
3. re-enables that GPIO interrupt

However this approach is not suitable for >1000 Hz and having second task that reads data from another device over I2C at 800 Hz.
Either first task misses some samples or the other one - depends which one has set higher priority.

I've tried to start prepared SPI transaction in ISR handler as was suggested here: https://esp32.com/viewtopic.php?t=1383#p6271
However I can't get post callback to work.

Code: Select all

#include "soc/spi_struct.h" // spi_dev_t struct
static spi_dev_t *spi3 = 0x3FF65000;

void IRAM_ATTR ad7124_start_spi_transaction(void)
{
    /* Set read-data phase */
    spi3->user.usr_mosi_highpart = 0;
    spi3->mosi_dlen.usr_mosi_dbitlen=0;
    spi3->miso_dlen.usr_miso_dbitlen=32-1;
    spi3->user.usr_mosi=0;
    spi3->user.usr_miso=1;
    // Start transfer
    spi3->cmd.usr = 1;
}

Code: Select all

void IRAM_ATTR post_cb_handler()
{
    tr_counter++;
    // TODO: copy received bytes from SPI registers
    gpio_intr_enable(pinout->miso);
}

void IRAM_ATTR gpio_isr_handler(void *arg)
{
    isr_counter++;
    gpio_intr_disable(pinout->miso);
    ad7124_start_spi_transaction();
}
In console print (every second) I can see this all the time:
isr_counter: 1
tr_counter: 0

So it processed gpio_isr_handler once, but not post_cb_handler to re-activate GPIO interrupts.

But if I call "spi_device_transmit()" from task, the tr_counter counts up - so it is assigned to spi device struct properly.
Do I have to enable interrupt per each transaction? Which HW register in spi3 struct it would be?

MicroController
Posts: 1696
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: SPI and I2C communication in background

Postby MicroController » Mon Sep 04, 2023 6:03 pm

Can be a SPI instructed to wait for this interrupt and then read 32-bits, store it to DMA, and wait for another such interrupt?
Probably not directly via IDF.
32 bits is very little data though, and the SPI peripheral has 15x32 bits of memory. The cost of setting up a DMA transaction for one word of data is not worth it.
You could manually pre-configure the SPI peripheral to execute one "user" transaction of a single 32-bit MISO phase. Then, starting the transaction, e.g. from the GPIO ISR, is only a matter of setting SPI_USR in SPI_CMD_REG. After the transaction (SPI ISR?), you can read the single word of data from SPI_W0_REG or SPI_W8_REG.

ghost07
Posts: 36
Joined: Mon Oct 03, 2022 11:47 am

Re: SPI and I2C communication in background

Postby ghost07 » Wed Sep 06, 2023 9:57 am

Yeah, I don't need DMA if I have to trigger each transaction from CPU.
DMA would be useful if SPI peripheral could automatically trigger prepared transaction whenever MISO goes low, read 32-bit word, store to DMA, wait for another MISO -> low level.

Actually, I am basically doing what you suggest - trigger SPI transaction from GPIO ISR by setting SPI_USR to 1.
But prior to start the SPI transaction I need to disable GPIO interrupts, otherwise it will fire multiple times while reading 32-bit word (as the "sample ready" interrupt is shared on the MISO line), and then re-enable GPIO interrupts after transaction is done.

However, the "SPI transaction done" callback (post_cb_handler) isn't called if I don't start transaction a standard way - using "spi_device_transmit()" or "spi_device_queue_trans()", which I can't use from GPIO ISR because both call xQueueSend() - a blocking function.

SPI_TRANS_INTEN is enabled all the time, so the driver has to disable IRQ listener between transactions or something.

Who is online

Users browsing this forum: Gaston1980, Majestic-12 [Bot] and 129 guests