There are some problems pushing the RTOS tick rate higher than the default 1000Hz. 1000Hz is already quite high for an RTOS tick rate! As well as increasing the context switch overhead for worker tasks, a lot of FreeRTOS code uses semantics like vTaskDelay(1000 / portTICK_PERIOD_MS) and there are problems if portTICK_PERIOD_MS becomes less than 1!
Timer interrupts are probably the best way to solve this problem. Here's a rough overview of how you might do this:
- Create a semaphore for signalling between the timer interrupt and a task where you'd do that actual I2C heavy lifting.
- Pin the worker task to a core at system maximum priority:
Code: Select all
xTaskCreatePinnedToCore(sample_timer_task, "sample_timer", 4096, NULL, configMAX_PRIORITIES - 1, NULL, 1);
- In the worker task, configure the timer for 400Hz and register a timer interrupt (this ensures it sits on the same CPU as the task, which isn't essential but it increases chance of shorter preemption time.)
(Please treat the following as pseudo-code rather than something you can use as-is.)
Code: Select all
static SemaphoreHandle_t timer_sem;
void sample_timer_task(void *param)
{
timer_sem = xSemaphoreCreateBinary();
// timer group init and config goes here (timer_group example gives code for doing this)
timer_isr_register(timer_group, timer_idx, timer_isr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL);
while (1) {
xSemaphoreTake(timer_sem, portMAX_DELAY);
// sample sensors via i2c here
// push sensor data to another queue, or send to a socket...
}
}
void IRAM_ATTR timer_isr_handler(void *param)
{
static BaseType_t xHigherPriorityTaskWoken = pdFALSE;
TIMERG0.hw_timer[timer_idx].update = 1;
// any other code required to reset the timer for next timeout event goes here
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
if( xHigherPriorityTaskWoken) {
portYIELD_FROM_ISR(); // this wakes up sample_timer_task immediately
}
}
The above pattern works like this:
- The sensor sampling task blocks to take the semaphore. While it's blocked, other tasks can run on this core.
- When the timer interrupt happens (configured for 400Hz), it gives the semaphore. This will wake up the highest priority task which is blocking on the semaphore (the sampling task). By calling portYIELD_FROM_ISR this task will be run immediately, no need to wait for a tick to expire.
- Sampling task runs, as it has the highest priority in the system nothing(*) can preempt it from the CPU. It may need to also block waiting for I2C interrupts, but this code will also wake the task immediately via portYIELD_FROM_ISR.
- When done, the sampling task will block on the semaphore again and allow other tasks to run on this CPU (or, if sampling was slow, it will run immediately as the semaphore has already been Given - but you obviously want to avoid that!)
This might seem like a lot of work compared to a non-RTOS approach (which you could possibly also do, if you ran the RTOS on one core and sampled sensors on the other core. Although I don't advise doing that.) The thing which is really nice is that the above pattern for real-time behaviour is all you need to guarantee those events run in real-time. Everywhere else you can do whatever you want without needing to worry about tasks which mess up the timing of your timing-critical section(*).
(*) This isn't entirely true, as interrupts and anything which disables the scheduler will still take this task off-CPU. But these are generally very quick, the only exception I can think of is anything which writes to the SPI flash.
In the long run i'm gonna be needing to connect at least 3 MPU9250 + 3 high range IMU together into one ESP32 and sample data at 400hz. Do you think this is a realistic approach with single ESP32 given i'll be using i2c mux to solve the addressing issues.
I think this is feasible, provided the total sensor sampling + i2c transfer times don't add up close to the 2.5ms timeslice you have for each sampling period (which seems unlikely).