1. Today's topic
Three main mechanisms for communication between FreeRTOS tasks:
Queue - transfer data
Event Group - report a set of state flags
Task Notification - quickly wake one specific task or transfer a 32-bit signalThe central question: how can tasks communicate without global variables, races, hangs or lost events?
2. Why this matters
Typical data flows:
1. Traffic-light inputs
input_task -> phase_task -> transport_task
2. EC25 modem
UART RX -> modem_task -> transport_task / health_task / CLI
3. Network events
transport_task -> modem_task -> retry/ACK state
4. Diagnostics
services -> health_task -> CLI
5. HIL testing
Raspberry Pi -> UART CLI -> services -> responses/logsGlobal variables:
extern bool g_modem_ready;
extern int g_current_phase;
extern bool g_send_now;
extern uint32_t g_error_flags;They seem convenient at first, but later cause races, lost events and unstable HIL tests.
3. Choosing a mechanism: quick reference
Need to transfer a data structure? -> Queue
Need to report state flags? -> Event Group
Need to wake one task quickly? -> Task Notification
Need to protect a shared resource? -> Mutex
Need to transfer a UART byte stream? -> Stream/ring buffer/UART driver buffer4. Queue
A queue transfers messages. Examples:
input_task -> input_snapshot_t -> phase_task
phase_task -> phase_event_t -> transport_task
transport_task -> modem_request_t -> modem_taskStructures:
typedef struct {
uint32_t raw_mask;
int64_t timestamp_us;
} input_snapshot_t;
typedef struct {
uint32_t seq;
uint8_t old_phase;
uint8_t new_phase;
uint32_t raw_mask;
int64_t timestamp_us;
} phase_event_t;A queue answers the question: what specific data arrived?
5. Choosing queue length
Do not arbitrarily choose a very large queue. Illustrative starting sizes:
input_queue length = 4..16
phase_event_queue length = 8..32
modem_request_queue = 8..32A small queue reveals overload earlier. A large queue can hide the problem.
6. Handling overflow
Different data requires different policies:
Phase-change event:
losing it is undesirable; use a drops counter and diagnostics.
Periodic telemetry:
old samples may be discarded; fresh data matters more.
Current modem state:
keep the latest value.
CLI response:
return BUSY or QUEUE_FULL where appropriate.
Modem UART RX:
losing bytes is undesirable; provide a suitable RX buffer/parser.Code:
if (xQueueSend(queue, &event, 0) != pdTRUE) {
diag.queue_drops++;
}7. Event Group
An event group holds state bits:
bit 0: MODEM_AT_READY
bit 1: MODEM_REGISTERED
bit 2: PDP_ACTIVE
bit 3: MQTT_CONNECTED
bit 4: UDP_READY
bit 5: TIME_SYNCED
bit 6: INPUT_FAULT
bit 7: TRANSPORT_DEGRADEDExample:
#define MODEM_BIT_AT_READY (1 << 0)
#define MODEM_BIT_REGISTERED (1 << 1)
#define MODEM_BIT_PDP_ACTIVE (1 << 2)
#define MODEM_BIT_MQTT_CONNECTED (1 << 3)
#define MODEM_BIT_TIME_SYNCED (1 << 4)
static EventGroupHandle_t modem_event_group;When the modem becomes ready:
xEventGroupSetBits(modem_event_group, MODEM_BIT_AT_READY);When the network is lost:
xEventGroupClearBits(modem_event_group,
MODEM_BIT_REGISTERED |
MODEM_BIT_PDP_ACTIVE |
MODEM_BIT_MQTT_CONNECTED);An event group is useful for state, but unsuitable for event history.
Check your reasoning about flag readiness
Reveal solution
Initially, 00001111 AND 00001110 = 00001110 (14): the result matches the mask. Turning off bit 2 changes the value to 11; 11 AND 14 = 10. MODEM_BIT_PDP_ACTIVE is absent, so all three flags are not ready even though the result is nonzero. Turning off only bit 0 gives value 14; 14 AND 14 = 14. MODEM_BIT_AT_READY is outside the selected mask and does not affect this criterion. Mask 14 is the specific criterion for this practice, not a universal modem readiness policy.
8. Task Notification
A task notification is a direct signal to a specific task.
static TaskHandle_t transport_task_handle;
xTaskNotifyGive(transport_task_handle);Receiver:
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);This is useful when data already lives elsewhere and the notification only acts as a doorbell. Notification bits can also be used:
#define TRANSPORT_NOTIFY_NEW_EVENT (1 << 0)
#define TRANSPORT_NOTIFY_MODEM_UP (1 << 1)
#define TRANSPORT_NOTIFY_MODEM_DOWN (1 << 2)
#define TRANSPORT_NOTIFY_ACK_TIMEOUT (1 << 3)
xTaskNotify(transport_task_handle,
TRANSPORT_NOTIFY_NEW_EVENT,
eSetBits);9. Notification limitations
Task notifications are convenient, but do not replace queues.
Transfer phase_event_t with all its fields? -> Queue
Wake a task? -> Task Notification
Transfer bit flags to one task? -> Task Notification bits
Share state read by many tasks? -> Event Group10. Common mistakes
- using a global variable instead of a queue;
- using an event group for events carrying data;
- ignoring the result of xQueueSend();
- blocking the fast path with portMAX_DELAY;
- confusing mutexes with semaphores;
- using one queue for everything;
- making one task wait on too many different objects.
11. Practical assignment
Create RTOS_COMMUNICATION.md:
# RTOS communication map
| From | To | Mechanism | Data | Overflow policy |
|---|---|---|---|---|
| input_task | phase_task | Queue | input_snapshot_t | drop newest + counter |
| phase_task | transport_task | Queue | phase_event_t | drop newest + critical counter |
| transport_task | modem_task | Queue | modem_request_t | reject low-priority telemetry first |
| modem_task | transport_task | Event Group / Notification | modem ready/lost flags | state only, no history |
| cli_task | services | direct API / Queue | command-specific | return BUSY if unavailable |
| services | health_task | counters / Event Group | fault bits | persistent until cleared |Describe the message structures:
typedef enum {
MODEM_REQ_MQTT_PUBLISH,
MODEM_REQ_UDP_SEND,
MODEM_REQ_TCP_SEND,
MODEM_REQ_RECONNECT,
MODEM_REQ_STATUS
} modem_request_type_t;
typedef struct {
modem_request_type_t type;
uint8_t priority;
uint32_t seq;
uint8_t payload[128];
uint16_t payload_len;
int64_t created_at_us;
} modem_request_t;Add overflow counters.
12. Brief recap
Queue -> data
Event Group -> state
Task Notification -> fast wakeup
Mutex -> resource protectionA basic arrangement:
input_task
-> input_snapshot_queue
-> phase_task
-> phase_event_queue
-> transport_task
-> modem_request_queue
-> modem_task / EC25Exercise
Choose a mechanism for four cases: phase-transition history with timestamps; current modem readiness read by several tasks; waking one consumer of an already populated buffer; protecting a shared resource. State which losses must not be silent.
Self-check criteria: Identify four different mechanisms, where notification payload data lives, and overflow consequences.
Show the supplied answer
Phase history uses a queue with payload and overflow accounting. Shared readiness uses an Event Group. Waking one consumer uses a Task Notification; data remains in a buffer with its own ownership contract. A shared resource uses a mutex in a valid task context. Phase transitions and UART bytes must not be lost silently: define policy, diagnostics and failure handling.