# Optimal Task Priority and Timing Configurations for Deterministic Performance in ESP32 Sim Racing Pedals

> Achieve sub-millisecond deterministic latency for ESP32 sim racing pedals. Optimize task priority and timing with round-robin scheduling and RUN_IN_CACHE for ultimate control.

- Repository: [chrgri/diy-sim-racing-ffb-pedal](https://github.com/chrgri/diy-sim-racing-ffb-pedal)
- Tags: best-practices
- Published: 2026-02-27

---

**Configure equal-priority round-robin scheduling with a 300 µs base tick and `RUN_IN_CACHE` mode to achieve sub-millisecond deterministic latency across all real-time control loops.**

The `chrgri/diy-sim-racing-ffb-pedal` firmware relies on precise timing for force-feedback calculations, load-cell sampling, and joystick output. Achieving deterministic performance requires careful configuration of the embedded **TaskScheduler** library, FreeRTOS task priorities, and hardware timer settings. This guide explains the optimal task priority and timing configurations drawn directly from the source code to eliminate jitter and guarantee predictable execution.

## Understanding the TaskScheduler Architecture

The project uses a lightweight cooperative scheduler located in `Common_Libs/TaskScheduler`. Unlike standard FreeRTOS preemptive scheduling, this implementation drives all real-time tasks from a single **ESP-IDF `esp_timer`** interrupt running at a configurable base frequency.

In [`TaskScheduler.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/TaskScheduler.h), the fundamental time quantum is defined as:

```cpp
#define BASE_TICK_US 300  // 300 µs ≈ 3.33 kHz base frequency

```

All task periods are expressed as integer multiples of this `BASE_TICK_US`. This fixed-resolution time base prevents drift and ensures that the interrupt fires exactly when a task is due, regardless of other system activity.

## Configuring the Base Tick and Timer Source

Deterministic behavior starts with the hardware timer configuration. The `TaskScheduler::begin()` method in [`TaskScheduler.cpp`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/TaskScheduler.cpp) creates a periodic `esp_timer` that runs independently of the FreeRTOS system tick.

Key configuration points:

- **Timer source**: Use `esp_timer` (hardware-backed) rather than software delays or the RTOS tick. This isolates the scheduler from scheduler-level jitter caused by Wi-Fi or Bluetooth interrupts.
- **Base tick**: Keep `BASE_TICK_US` at **300 µs**. This provides sufficient resolution for 1 kHz load-cell sampling while leaving CPU headroom for force-feedback calculations.

## Setting Optimal Task Priorities with RUN_IN_CACHE

The most critical setting for deterministic performance is the **`RUN_IN_CACHE`** flag defined in [`ESP32/include/Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/ESP32/include/Main.h). When enabled, this macro forces **all real-time tasks to run at equal priority** (`(UBaseType_t)1`), creating a round-robin scheduling pattern that eliminates priority inversion.

Priority configuration from [`Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Main.h) (lines 61-74):

```cpp
#define TASK_PRIORITY_PEDAL_UPDATE_TASK        (UBaseType_t)1
#define TASK_PRIORITY_LOADCELL_READING_TASK    (UBaseType_t)1
#define TASK_PRIORITY_JOYSTICKOUTPUT_TASK      (UBaseType_t)1
#define TASK_PRIORITY_ESP_NOW_TASK             (UBaseType_t)1
// All real-time tasks share priority 1 when RUN_IN_CACHE is defined

```

**Why equal priority matters**: When every task runs at the same priority level, FreeRTOS schedules them round-robin within that priority. The `TaskScheduler` ISR simply calls `vTaskNotifyGiveFromISR()` for the next due task. This creates a fully deterministic cycle: **timer interrupt → task notification → task execution → back to timer**, with no preemption jitter.

## Defining Task Periods and Core Affinity

Task periods must be integer multiples of the 300 µs base tick. The macros in [`Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Main.h) convert microseconds to scheduler ticks:

```cpp
#define REPETITION_INTERVAL_PEDAL_UPDATE_TASK_IN_US      (int64_t)600   // 600 µs
#define REPETITION_INTERVAL_LOADCELL_READING_TASK_IN_US  (int64_t)1000  // 1 ms

```

**Core affinity** is equally important. The ESP32 runs Wi-Fi and Bluetooth stacks on **core 0**. To prevent network ISR latency from interfering with control loops, pin all real-time tasks to **core 1**:

```cpp
#define CORE_ID_PEDAL_UPDATE_TASK        (uint8_t)1
#define CORE_ID_LOADCELLREADING_TASK     (uint8_t)1
#define CORE_ID_JOYSTICKOUTPUT_TASK      (uint8_t)1

```

**Stack sizes** should remain at the defaults (e.g., `STACK_SIZE_PEDAL_UPDATE_TASK = 7000` bytes). These are large enough to prevent overflow—which would cause a reset and break determinism—without being so large that memory allocation becomes nondeterministic.

## Implementation Example

### Initialising the Scheduler

In [`Main.cpp`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Main.cpp), instantiate the scheduler and register tasks:

```cpp
#include "TaskScheduler.h"

TaskScheduler taskScheduler;

void setup()
{
    // Hardware initialisation omitted ...
    
    // Start scheduler with timer 0
    taskScheduler.begin(0);
    
    // Register pedal update task: 600 µs period, priority 1, core 1
    taskScheduler.addScheduledTask(
        pedalUpdateTask,
        "Pedal Update",
        REPETITION_INTERVAL_PEDAL_UPDATE_TASK_IN_US,
        TASK_PRIORITY_PEDAL_UPDATE_TASK,
        CORE_ID_PEDAL_UPDATE_TASK,
        STACK_SIZE_PEDAL_UPDATE_TASK
    );
    
    // Register load-cell task: 1 ms period, priority 1, core 1
    taskScheduler.addScheduledTask(
        loadCellTask,
        "Loadcell Read",
        REPETITION_INTERVAL_LOADCELL_READING_TASK_IN_US,
        TASK_PRIORITY_LOADCELL_READING_TASK,
        CORE_ID_LOADCELLREADING_TASK,
        STACK_SIZE_LOADCELL_READING_TASK
    );
}

```

### Writing a Deterministic Task

Tasks must use `ulTaskNotifyTake` to synchronise with the scheduler:

```cpp
void pedalUpdateTask(void *pvParameters)
{
    while (true)
    {
        // Block until scheduler notification arrives
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        // Deterministic work must complete before next tick
        readPedalADC();
        applyFIRFilter();
        computeForceFeedback();
        
        // Task yields automatically when loop ends
    }
}

```

### Adding Custom Tasks

Maintain determinism by keeping priority at 1 and using integer multiples of 300 µs:

```cpp
#define REPETITION_INTERVAL_MYTASK_IN_US (int64_t)3000  // 3 ms = 10 ticks
#define TASK_PRIORITY_MYTASK (UBaseType_t)1
#define CORE_ID_MYTASK (uint8_t)1

taskScheduler.addScheduledTask(
    myTask,
    "Custom Task",
    REPETITION_INTERVAL_MYTASK_IN_US,
    TASK_PRIORITY_MYTASK,
    CORE_ID_MYTASK,
    4000
);

```

## Summary

- **Use a 300 µs base tick** (`BASE_TICK_US` in [`TaskScheduler.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/TaskScheduler.h)) to provide fixed-resolution timing for all control loops.
- **Enable `RUN_IN_CACHE`** in [`Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Main.h) to force equal priority (`(UBaseType_t)1`) across all real-time tasks, creating deterministic round-robin scheduling.
- **Pin tasks to core 1** using the `CORE_ID_*` macros to isolate them from Wi-Fi and Bluetooth interrupts running on core 0.
- **Express periods as integer multiples** of the base tick (e.g., 600 µs for pedal updates, 1000 µs for load-cell reads) to prevent timing drift.
- **Use `ulTaskNotifyTake`** in task loops to synchronise exactly with the `esp_timer` ISR, ensuring one execution per tick with minimal jitter.

## Frequently Asked Questions

### What is the optimal base tick interval for the DIY Sim Racing FFB Pedal firmware?

The optimal base tick is **300 microseconds** (approximately 3.33 kHz), defined as `BASE_TICK_US` in [`Common_Libs/TaskScheduler/src/TaskScheduler.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Common_Libs/TaskScheduler/src/TaskScheduler.h). This value provides sufficient resolution for 1 kHz load-cell sampling and force-feedback calculations while maintaining CPU headroom for other tasks. All task periods must be configured as integer multiples of this 300 µs quantum to ensure deterministic timing.

### Why should all real-time tasks use the same priority level?

Equal priority (`(UBaseType_t)1`) enables **round-robin scheduling** that eliminates priority inversion and preemption jitter. When the `RUN_IN_CACHE` flag is defined in [`ESP32/include/Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/ESP32/include/Main.h), all tasks run at priority 1, allowing the `TaskScheduler` to dispatch them sequentially via `vTaskNotifyGiveFromISR()` without scheduler-level conflicts. This creates a predictable execution cycle where each task runs exactly once per scheduled tick.

### How do I prevent Wi-Fi and Bluetooth interrupts from affecting pedal latency?

Pin all time-critical tasks to **core 1** using the `CORE_ID_*` macros defined in [`Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Main.h). The ESP32 runs Wi-Fi and Bluetooth protocol stacks on core 0, which generates frequent interrupts. By setting `CORE_ID_PEDAL_UPDATE_TASK`, `CORE_ID_LOADCELLREADING_TASK`, and similar macros to `(uint8_t)1`, you isolate the control loops from network-related latency spikes.

### What happens if I choose task periods that are not multiples of 300 µs?

Non-integer multiples of the base tick cause **timing drift** and jitter. The `TaskScheduler` counts ticks using the `BASE_TICK_US` quantum (300 µs). If you specify a period of, for example, 500 µs, the scheduler cannot represent this exactly as an integer number of ticks, leading to inconsistent intervals between task executions. Always define periods in [`Main.h`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Main.h) using values like 600, 1000, or 3000 µs that divide evenly by 300.