Skip to content

0007: FreeRTOS Task Architecture

Date: 2025-10-15 Phase: 0.1 Status: Accepted Type: Architecture


Summary (Y-Statement)

In the context of implementing concurrent firmware operations (motor control, BLE communication, battery monitoring), facing real-time requirements for bilateral timing and emergency response, we decided for priority-based multi-task FreeRTOS architecture with statically allocated stacks, and neglected single-threaded event loop or dynamic task creation, to achieve deterministic real-time scheduling with emergency response (<50ms) and guaranteed memory allocation, accepting increased complexity of inter-task communication and mutex synchronization.


Problem Statement

The firmware must handle multiple concurrent operations: - Motor control: Precise bilateral timing (±10ms precision) - BLE communication: Device-to-device coordination + mobile app - Button monitoring: Emergency shutdown (<50ms response) - Battery monitoring: Periodic voltage checks without blocking motor timing - NVS persistence: Background data storage

Requirements: - Real-time response for emergency button (highest priority) - Bilateral timing cannot be interrupted by BLE operations - No dynamic memory allocation (JPL compliance) - Watchdog feeding from all critical tasks - Thread-safe resource sharing (GPIO, LEDC, ADC)


Context

Real-Time Requirements

Priority Hierarchy: 1. Emergency shutdown: <50ms button response (highest) 2. Motor timing: ±10ms bilateral precision (high) 3. BLE communication: <100ms command latency (medium) 4. Battery monitoring: 1-second sampling period (low) 5. NVS persistence: Background writes (lowest)

FreeRTOS Capabilities

ESP-IDF v5.5.0 includes: - FreeRTOS v10.5.1 with priority-based preemptive scheduling - Static task creation (xTaskCreateStatic()) - Mutexes for thread-safe resource sharing - Task Watchdog Timer (TWDT) integration - Message queues for inter-task communication

Memory Constraints

ESP32-C6: - 512 KB SRAM total - Stack allocated from SRAM - Must reserve memory for heap (ESP-IDF + NimBLE)


Decision

We will use priority-based multi-task FreeRTOS architecture with static stack allocation and mutex-protected shared resources.

Task Priorities and Stack Sizes

// Task priority definitions (higher number = higher priority)
#define TASK_PRIORITY_BUTTON_ISR        25  // Highest - emergency response
#define TASK_PRIORITY_MOTOR_CONTROL     15  // High - bilateral timing critical
#define TASK_PRIORITY_BLE_MANAGER       10  // Medium - communication
#define TASK_PRIORITY_BATTERY_MONITOR    5  // Low - background monitoring
#define TASK_PRIORITY_NVS_MANAGER        1  // Lowest - data persistence

// Stack sizes (optimized for ESP32-C6's 512KB SRAM)
#define STACK_SIZE_BUTTON_ISR       1024    // Simple ISR handling
#define STACK_SIZE_MOTOR_CONTROL    2048    // PWM + timing calculations
#define STACK_SIZE_BLE_MANAGER      4096    // NimBLE stack requirements
#define STACK_SIZE_BATTERY_MONITOR  1024    // ADC reading + calculations
#define STACK_SIZE_NVS_MANAGER      1024    // NVS operations

// Total static stack: ~9 KB of 512 KB SRAM (1.8%)

Task Responsibilities

1. Button ISR Task (Priority 25) - Monitor GPIO1 button state via ISR - Detect emergency shutdown (5s hold) - Detect factory reset (10s hold, first 30s only) - Send shutdown messages to all tasks - Feed watchdog during purple blink loops

2. Motor Control Task (Priority 15) - Execute bilateral half-cycle timing - Control H-bridge via LEDC PWM - Maintain WS2812B therapy LED - Check message queue every 50ms for mode changes - Feed watchdog during 1ms dead time

3. BLE Manager Task (Priority 10) - Handle NimBLE stack events - Peer device discovery and connection - Process BLE characteristic reads/writes - Coordinate bilateral timing commands - Battery level notifications

4. Battery Monitor Task (Priority 5) - Periodic voltage sampling (1 Hz) - Battery percentage calculation - Send battery updates to BLE task - Low-voltage warnings

5. NVS Manager Task (Priority 1) - Background session data persistence - Device pairing information storage - Configuration settings save/restore - Avoid NVS writes during critical motor timing

Task Creation Requirements

Static Allocation Pattern:

static StackType_t motor_task_stack[STACK_SIZE_MOTOR_CONTROL];
static StaticTask_t motor_task_buffer;

TaskHandle_t motor_task_handle = xTaskCreateStatic(
    motor_task_function,
    "motor_task",
    STACK_SIZE_MOTOR_CONTROL,
    NULL,
    TASK_PRIORITY_MOTOR_CONTROL,
    motor_task_stack,
    &motor_task_buffer
);

Watchdog Registration:

void motor_task_function(void *arg) {
    ESP_ERROR_CHECK(esp_task_wdt_add(NULL));  // Subscribe to TWDT

    while (session_active) {
        // Task work...
        esp_task_wdt_reset();  // Feed watchdog regularly
        vTaskDelay(pdMS_TO_TICKS(50));
    }

    esp_task_wdt_delete(NULL);  // Unsubscribe before exit
    vTaskDelete(NULL);  // Self-delete
}

Shared Resource Protection

Mutex-Protected Resources: - LEDC PWM: Motor control exclusive access - ADC: Battery monitor vs. back-EMF sampling - GPIO: Status LED vs. WS2812B - NVS: Configuration writes serialized

Example Mutex Usage:

static SemaphoreHandle_t ledc_mutex;

void motor_set_direction_intensity(motor_direction_t dir, uint8_t intensity) {
    xSemaphoreTake(ledc_mutex, portMAX_DELAY);
    // LEDC configuration...
    xSemaphoreGive(ledc_mutex);
}


Consequences

Benefits

  • Emergency response: Button ISR task pre-empts all other tasks (<50ms shutdown)
  • Real-time motor control: High priority prevents BLE/battery interruptions
  • Concurrent operations: Motor timing + BLE + battery monitoring without blocking
  • Thread safety: Mutex protection prevents race conditions on shared peripherals
  • Watchdog compliance: All critical tasks feed TWDT (2000ms timeout, 2× safety margin)
  • Static memory: JPL-compliant allocation, no heap fragmentation
  • Stack analysis: uxTaskGetStackHighWaterMark() enables worst-case verification
  • Deterministic behavior: Priority-based scheduling guarantees response times

Drawbacks

  • Increased complexity: Multi-task coordination harder than single-threaded
  • Inter-task communication: Message queues and mutexes add overhead
  • Stack tuning required: Must profile stack usage to avoid overflow
  • Priority inversion risk: Lower-priority tasks holding mutexes can block higher-priority
  • Debugging difficulty: Race conditions and timing issues harder to reproduce

Options Considered

Option A: Priority-Based Multi-Task (Selected)

Pros: - Real-time response for emergency shutdown - Concurrent operations without blocking - Deterministic scheduling with priorities - Thread-safe resource sharing via mutexes - Static allocation for JPL compliance

Cons: - Increased complexity - Mutex overhead - Priority tuning required

Selected: YES Rationale: Only option meeting real-time emergency response requirement

Option B: Single-Threaded Event Loop (Rejected)

Pros: - Simpler code (no mutexes or inter-task communication) - No priority inversion risk - Easier debugging

Cons: - Cannot guarantee <50ms emergency response (motor delay may be 999ms) - BLE operations could block motor timing - No concurrent battery monitoring during motor operation

Selected: NO Rationale: Cannot meet emergency shutdown requirement (<50ms response)

Option C: Dynamic Task Creation (Rejected)

Pros: - More flexible (create tasks as needed) - Lower static memory footprint

Cons: - ❌ Violates JPL Rule #1 (no dynamic memory allocation) - Heap fragmentation risk during long sessions - Harder to analyze worst-case memory usage

Selected: NO Rationale: JPL compliance requires static allocation

Option D: Bare-Metal Interrupt-Driven (Rejected)

Pros: - Lowest overhead (no RTOS scheduler) - Maximum performance

Cons: - No FreeRTOS primitives (vTaskDelay, mutexes, queues) - Much harder to implement bilateral timing coordination - Violates JPL requirement for bounded waits (hard to avoid busy-wait) - Loses ESP-IDF ecosystem benefits

Selected: NO Rationale: Incompatible with JPL coding standard and ESP-IDF framework



Implementation Notes

Code References

  • src/motor_task.c - Motor control task (priority 15)
  • src/ble_task.c - BLE manager task (priority 10)
  • src/button_task.c - Button ISR task (priority 25)
  • test/single_device_demo_jpl_queued.c - Phase 0.4 task architecture

Build Environment

All environments use FreeRTOS multi-task architecture.

Task Creation Example

Motor Task Creation:

// Static allocation (JPL-compliant)
static StackType_t motor_task_stack[STACK_SIZE_MOTOR_CONTROL];
static StaticTask_t motor_task_buffer;

void create_motor_task(void) {
    motor_task_handle = xTaskCreateStatic(
        motor_task_function,
        "motor_task",
        STACK_SIZE_MOTOR_CONTROL,
        NULL,
        TASK_PRIORITY_MOTOR_CONTROL,
        motor_task_stack,
        &motor_task_buffer
    );

    if (motor_task_handle == NULL) {
        ESP_LOGE(TAG, "Failed to create motor task");
        esp_restart();  // Fail-safe restart
    }
}

Testing & Verification

Stack Usage Analysis:

UBaseType_t high_water_mark = uxTaskGetStackHighWaterMark(motor_task_handle);
ESP_LOGI(TAG, "Motor task stack: %u bytes remaining", high_water_mark * sizeof(StackType_t));

Verified Stack High Water Marks (Phase 0.4): - Button task: 512 bytes remaining (50% utilization) - Motor task: 896 bytes remaining (56% utilization) - BLE task: 1024 bytes remaining (75% utilization)

Real-Time Response Measured: - Emergency shutdown: 15-25ms (button press → motor coast) - Mode change: <100ms (BLE write → motor update) - Battery update: <50ms (ADC read → BLE notify)

Known Issues: - None - task architecture stable and verified


JPL Coding Standards Compliance

  • ✅ Rule #1: No dynamic memory - xTaskCreateStatic() for all tasks
  • ✅ Rule #2: Fixed loop bounds - Task loops bounded by shutdown flag
  • ✅ Rule #5: Return value checking - Task creation results checked
  • ✅ Rule #6: No unbounded waits - vTaskDelay() and xSemaphoreTake() have timeouts
  • ✅ Rule #7: Watchdog compliance - All critical tasks subscribe to TWDT
  • ✅ Rule #8: Defensive logging - Task state changes logged

Migration Notes

Migrated from docs/architecture_decisions.md on 2025-11-21 Original location: AD007 (Software Architecture Decisions) Git commit: Current working tree


Template Version: MADR 4.0.0 (Customized for EMDR Pulser Project) Last Updated: 2025-11-21