Skip to content

4. RTOS and asynchronous

Nathan zhou edited this page Nov 10, 2025 · 8 revisions

Why We Need RTOS

Because CAN is asynchronous, CAN frames can arrive at any time from the roboRIO or other nodes on the network.

Without RTOS

  • You would need to constantly poll for incoming messages.
  • Sensor readings and CAN transmissions would have to be manually interleaved using delays or fixed scheduling loops.
  • This leads to timing jitter, dropped messages, and unreliable synchronization between CAN communication and sensor sampling.

With RTOS

  • A dedicated TaskCANRx can block on receive using twai_receive() or wait on a FreeRTOS queue.
  • It can immediately handle configuration or control frames without interfering with ongoing sensor sampling or periodic transmission tasks.
  • Other tasks (e.g. sensor read, LED control, NFC handling) can run concurrently and deterministically, each with its own timing and priority.

How to Create a Task on Arduino ESP32

ESP32 uses FreeRTOS under the Arduino framework. You can create a new concurrent task with xTaskCreatePinnedToCore(), which allows you to specify the function, name, stack size, priority, and which CPU core the task should run on.

Example

void TaskCANRx(void* pvParameters) {
  for (;;) {
    // Wait for CAN frame
    twai_message_t message;
    if (twai_receive(&message, pdMS_TO_TICKS(1000)) == ESP_OK) {
      // Process incoming CAN message
    }
  }
}

void setup() {
  Serial.begin(115200);
  // Create the CAN RX task
  xTaskCreatePinnedToCore(
    TaskCANRx,           // Task function
    "CAN RX",            // Name
    4096,                // Stack size (bytes)
    NULL,                // Parameter
    1,                   // Priority (1 = low, 5 = high)
    NULL,                // Task handle (optional)
    0                    // Core 0 or 1
  );
}

Notes

  • Each task runs independently and can block or delay without affecting others.
  • Use vTaskDelay(ms / portTICK_PERIOD_MS) instead of delay() to yield to other tasks.
  • Tasks can communicate using queues, semaphores, or shared variables (with mutex protection if needed).

Handling CAN Reception with twai_receive() and Internal Dispatch

On Arduino ESP32, the TWAI driver (twai_receive()) is responsible for reading frames from the hardware CAN controller. Because CAN traffic is asynchronous, messages may arrive at any time — but only one function should directly access twai_receive() to prevent concurrency and buffer conflicts.

twai_receive() Fundamentals

twai_message_t msg;
if (twai_receive(&msg, pdMS_TO_TICKS(1000)) == ESP_OK) {
  // A valid CAN frame was received
}
  • twai_receive() blocks until a frame is available or the timeout expires.

  • It returns ESP_OK when a message is successfully read.

  • Each message is stored in a twai_message_t structure containing:

    • identifier (the 29-bit CAN ID)
    • data_length_code (number of bytes, 0–8)
    • data[8] (payload bytes)

Why Only One Caller

The TWAI driver has a single internal receive buffer. If multiple tasks try to call twai_receive() at the same time, they can:

  • Compete for the same frame
  • Cause data corruption
  • Lead to unpredictable blocking and timing behavior

Therefore, only one task — typically TaskCANRx — should call twai_receive().

Recommended Architecture: Internal Dispatch

Instead of creating another dispatcher task (which adds memory and scheduling overhead), TaskCANRx itself should perform the dispatching by calling sub-functions for each message type:

void TaskCANRx(void* pvParameters) {
  twai_message_t msg;

  for (;;) {
    if (twai_receive(&msg, portMAX_DELAY) == ESP_OK) {
      uint32_t apiId = msg.identifier & 0xFFF;

      // Internal dispatch
      switch (apiId) {
        case 0x135:
          handleSomething(msg);
          break;

        case 0x136:
          handleSomethingElse(msg);
          break;

        case 0x187:
          handleJokes(msg);
          break;

        default:
          handleUnknown(msg);
          break;
      }
    }
  }
}

Benefits of Internal Dispatch

  • No extra task or queue — conserves memory and stack usage.
  • Single access point for all incoming CAN frames (safe and deterministic).
  • No blocking or scheduling delay between frame arrival and handler execution.
  • Easier debugging — all CAN receive logic is centralized in one function.

Best Practices

  • Keep each handler (handleRoboRIOHeartbeat, etc.) non-blocking and short.
  • Use shared data structures or flags for communication between tasks (e.g. CAN → NFC writer).
  • Use mutexes or atomic updates if handlers modify shared variables.
  • Avoid long vTaskDelay() or Serial prints inside handlers — delegate to other tasks if needed.

Clone this wiki locally