-
Notifications
You must be signed in to change notification settings - Fork 2
4. RTOS and asynchronous
Because CAN is asynchronous, CAN frames can arrive at any time from the roboRIO or other nodes on the network.
- 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.
- A dedicated
TaskCANRxcan block on receive usingtwai_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.
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.
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
);
}- Each task runs independently and can block or delay without affecting others.
- Use
vTaskDelay(ms / portTICK_PERIOD_MS)instead ofdelay()to yield to other tasks. - Tasks can communicate using queues, semaphores, or shared variables (with mutex protection if needed).
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_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_OKwhen a message is successfully read. -
Each message is stored in a
twai_message_tstructure containing:-
identifier(the 29-bit CAN ID) -
data_length_code(number of bytes, 0–8) -
data[8](payload bytes)
-
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().
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;
}
}
}
}- 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.
- 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.