CodeRigor

FREE LESSON

CMSIS-RTOS2 on FreeRTOS: threads, queues & API contracts

Run the CMSIS-RTOS2 API on the real FreeRTOS kernel: byte-sized thread stacks, priorities, copied messages, bounded waits, flags and adapter limits.

Begin after FreeRTOS tasks and queues and CMSIS-Core. You will express familiar scheduling and communication operations through cmsis_os2.h, while retaining an explicit contract for the backend that implements them.

The header describes; the kernel schedules

This runner uses CMSIS 6.4.0, the CMSIS-FreeRTOS 11.3.0 adapter, and the existing FreeRTOS-Kernel 11.3.2 on Cortex-M4F. Including cmsis_os2.h automatically selects that support. It does not replace the kernel with RTX5. The adapter owns SysTick dispatch; startup still provides the vector table and FreeRTOS still owns its SVC/PendSV context switching. Do not add competing handlers or reconfigure SysTick while the scheduler is active.

One scheduler underneath the standardized API
The RTOS2 API maps to the existing kernel. Code portability still depends on backend features, allocation rules and interrupt constraints.
One scheduler underneath the standardized API
The RTOS2 API maps to the existing kernel. Code portability still depends on backend features, allocation rules and interrupt constraints.

A standalone program calls osKernelInitialize(), creates its objects and threads, then calls osKernelStart(). Creation can also happen later from a running thread. Check every handle and status. Threads use a void(void*) entry point and finish with osThreadExit() when their bounded work is done. The graded harness already initializes and starts the kernel, and runs TEST bodies at osPriorityNormal. A helper must not start it again.

Units and priority directions are part of the API

osThreadAttr_t attr{} initializes defaults safely. Its stack_size is in bytes; native xTaskCreate on this Cortex-M4 takes four-byte stack words. A native depth of 512 corresponds to a CMSIS byte count of 2048. A name pointer is borrowed: keep its storage alive for the thread's lifetime. Default dynamic allocation is useful for these small exercises, not a bound on production allocation latency.

A stack budget must keep its unit across an API boundary
CMSIS expresses a byte budget; the native port expresses stack words. Blindly copying the native depth into stack_size requests a quarter of the memory.
A stack budget must keep its unit across an API boundary
CMSIS expresses a byte budget; the native port expresses stack words. Blindly copying the native depth into stack_size requests a quarter of the memory.

CMSIS thread priorities such as osPriorityNormal and osPriorityAboveNormal use larger values for higher priority. NVIC IRQ priorities use smaller values for greater urgency. Do not interchange those numbers. This adapter requires a 56-priority FreeRTOS configuration and maps CMSIS thread priority p to native priority p-1. Native-only FreeRTOS exercises retain their separate eight-priority configuration; the test runner uses native 23 to represent CMSIS Normal (24).

Messages copy bytes; statuses describe the attempt

osMessageQueueNew(count,sizeof(sample),nullptr) fixes both capacity and copied-item size. osMessageQueuePut(q,&sample,0,timeout) copies that many bytes; changing the sender's scalar values later cannot change an already queued sample. A pointer field would copy only the pointer, leaving its pointee's ownership and lifetime unresolved. The receiving helper validates message size and uses a temporary so it changes the caller's output only on success.

Distinguish a poll from an expired bounded wait
Queue APIs return signed osStatus_t values. Preserve the status instead of reducing every attempt to true or false.
Distinguish a poll from an expired bounded wait
Queue APIs return signed osStatus_t values. Preserve the status instead of reducing every attempt to true or false.

Timeouts and osDelay use kernel ticks, not an API-wide assumption of milliseconds. This runner is 1 kHz, but portable conversion queries or accepts the frequency and rounds positive durations up. Compute in 64 bits, then clamp finite waits below the osWaitForever sentinel. Every wait in a test must be bounded so a broken solution produces a failure instead of hanging.

C++
#include "cmsis_os2.h"
#include <cassert>
#include <cstdio>
#include <cstdlib>
static void demo(void*) {
    auto q=osMessageQueueNew(1,sizeof(int),nullptr); assert(q);
    int sent=7, received=0;
    assert(osMessageQueuePut(q,&sent,0,0)==osOK); sent=99;
    assert(osMessageQueueGet(q,&received,nullptr,0)==osOK);
    std::printf("copied value = %d; tick Hz = %lu\n",received,(unsigned long)osKernelGetTickFreq());
    assert(osMessageQueueDelete(q)==osOK);
    std::exit(0);
}
int main() {
    assert(osKernelInitialize()==osOK);
    osThreadAttr_t attr{}; attr.stack_size=4096; attr.priority=osPriorityNormal;
    assert(osThreadNew(demo,nullptr,&attr));
    osKernelStart();
}

The backend ignores message priority; queues here are FIFO. An exposed parameter is not evidence that every backend implements its generic behavior. Read the pinned adapter and its documented limitations before porting software.

Flags, interrupts and object retirement

Thread flags belong to a particular thread. Event flags are a separate shared object. Repeated sets coalesce; use a queue or counter if every occurrence matters. Flag APIs return uint32_t with failures encoded through the osFlagsError bit: test that bit, rather than treating nonzero as success. This backend offers 31 thread-flag bits and 24 event-flag bits, and uses native notification slot 0 for thread flags. Do not share that slot with unrelated native notifications or stream/message-buffer waits in the same thread.

CMSIS internally selects ISR operations only for the documented ISR-safe calls. Queue put/get from an ISR require timeout zero and a priority legal for the FreeRTOS port (logical IRQ priorities 5–7 here). Thread creation, blocking waits, mutexes and object deletion are not arbitrary ISR operations. The build tests pend a real priority-6 IRQ to check the queue's polling path and its rejection of a positive timeout.

The adapter also supplies mutexes, semaphores, memory pools and software timers; runtime tests exercise those objects. CMSIS does not remove their ownership rules. Timer callbacks run in the shared daemon, must not block, and must retain their argument storage until callbacks/queued deletion have retired. See timers and event coordination. An accepted delete command is not a general completion handshake.

Static allocation needs backend-specific control-block sizes and alignment, plus correctly sized backing memory; providing arbitrary byte arrays does not make it portable or correct. This adapter rejects joinable threads and robust mutex behavior. It does not implement the full newer RTOS2 process-isolation, TrustZone or SMP interfaces. These exercises claim this tested single-core FreeRTOS backend, not universal CMSIS conformance. There is no physical-board latency or hardware-in-the-loop evidence in an emulated run.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Uses the CMSIS RTOS/V2 abstraction over FreeRTOS. Focus on generated thread attributes and queue calls; our target is MPS2/QEMU, without STM32 HAL or CubeMX.