1. Today's topic

Three main mechanisms for communication between FreeRTOS tasks:

text
Queue             - transfer data
Event Group       - report a set of state flags
Task Notification - quickly wake one specific task or transfer a 32-bit signal

The central question: how can tasks communicate without global variables, races, hangs or lost events?

2. Why this matters

Typical data flows:

text
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/logs

Global variables:

c
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

text
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 buffer

4. Queue

A queue transfers messages. Examples:

text
input_task -> input_snapshot_t -> phase_task
phase_task -> phase_event_t -> transport_task
transport_task -> modem_request_t -> modem_task

Structures:

c
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:

text
input_queue length        = 4..16
phase_event_queue length  = 8..32
modem_request_queue       = 8..32

A small queue reveals overload earlier. A large queue can hide the problem.

6. Handling overflow

Different data requires different policies:

text
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:

c
if (xQueueSend(queue, &event, 0) != pdTRUE) {
    diag.queue_drops++;
}

7. Event Group

An event group holds state bits:

text
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_DEGRADED

Example:

c
#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:

c
xEventGroupSetBits(modem_event_group, MODEM_BIT_AT_READY);

When the network is lost:

c
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.

Bits and masks

Value: 15 · 00001111 · 0x0F
Mask: 14 · 00001110 · 0x0E

00001111 AND 00001110 = 00001110 (14)
Hexadecimal: 0x0F AND 0x0E = 0x0E

Toggle bits with Enter or Space. AND keeps shared bits, OR combines them, and XOR keeps differing bits.

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.

c
static TaskHandle_t transport_task_handle;
xTaskNotifyGive(transport_task_handle);

Receiver:

c
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:

c
#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.

text
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 Group

10. 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:

markdown
# 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:

c
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

text
Queue             -> data
Event Group       -> state
Task Notification -> fast wakeup
Mutex             -> resource protection

A basic arrangement:

text
input_task
  -> input_snapshot_queue
  -> phase_task
  -> phase_event_queue
  -> transport_task
-> modem_request_queue
-> modem_task / EC25
You must transfer phase_event_t with timestamp, old_phase and new_phase while preserving the order of several transitions. What should you use?

Exercise

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.