SPI and I2C communication in background
SPI and I2C communication in background
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?
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?
-
- Posts: 1750
- Joined: Mon Oct 17, 2022 7:38 pm
- Location: Europe, Germany
Re: SPI and I2C communication in background
How do you measure that? 4ms latency would be very unsusual.
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.Does SPI and UART peripheral share some resources? (spi_device_transmit() takes longer when something is printed to console)
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.Is I2C peripheral able to handle transaction in the background after setting up the "link", or it needs CPU's attention for each bit?
For SPI, you can choose to use DMA transfer which, once started, handles the whole transfer without any involvement of the CPU.
Re: SPI and I2C communication in background
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);
}
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...
}
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;
}
}
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)
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.
-
- Posts: 9772
- Joined: Thu Nov 26, 2015 4:08 am
Re: SPI and I2C communication in background
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.
-
- Posts: 1750
- Joined: Mon Oct 17, 2022 7:38 pm
- Location: Europe, Germany
Re: SPI and I2C communication in background
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.The task's loop has "xTaskNotifyWait()" that waits for any notification and then calls "onResume" function.
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.
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.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.
Re: SPI and I2C communication in background
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?)
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?)
Re: SPI and I2C communication in background
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.
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?
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();
}
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?
-
- Posts: 1750
- Joined: Mon Oct 17, 2022 7:38 pm
- Location: Europe, Germany
Re: SPI and I2C communication in background
Probably not directly via IDF.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?
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.
Re: SPI and I2C communication in background
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.
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: Bing [Bot] and 86 guests