This episode covers C++ for embedded and real-time applications: memory constraints and deterministic behavior, RTOS integration, embedded code profiling and low-level optimization, as well as firmware deployment and hardware considerations.

Car controllers, medical devices, and industrial control systems run on devices with very limited resources — kilobytes of RAM, no filesystem, no full OS. C++ is increasingly dominant in the embedded world because it delivers performance and full control with safer abstractions than pure C.
Episode 21 takes you into embedded and high-performance systems: C++ principles for real-time systems, living with hard memory constraints, RTOS integration, how to profile embedded code that's hard to observe, low-level optimization, and the firmware deployment process to hardware.
A real-time system guarantees task completion within a certain time limit. Failing a time limit can mean disaster — a car not stopping in time, a medical device giving the wrong dose. That's why embedded C++ demands:
new and malloc can be very slow or fail.constexpr and metaprogramming to move work to compile time.An embedded-style example that meets these rules:
#include <cstdint>
#include <array>
constexpr std::uint16_t maksimum(const std::array<std::uint16_t, 4>& a) {
std::uint16_t m = 0;
for (auto v : a) {
if (v > m) m = v;
}
return m;
}
int main() {
constexpr std::array<std::uint16_t, 4> sampel{10, 40, 25, 18};
constexpr auto hasil = maksimum(sampel);
return hasil;
}
EOF
g++ -std=c++20 -Os embedded.cpp -o embeddedconstexpr std::array<std::uint16_t, 4> and constexpr auto hasil make all computations happen at compile time — no heap allocation, no runtime variables, the result is already fixed in the binary. This style is ideal for devices with limited RAM.
In embedded, the heap is often avoided entirely. All buffers are allocated statically or on the stack with sizes known at compile time. std::array replaces std::vector, and fixed-size types replace platform-dependent ones:
g++ -std=c++20 -fno-exceptions -fno-rtti \
-ffreestanding -fstack-usage -Os app.cppThe flags -fno-exceptions and -fno-rtti turn off features that need a large runtime, and -ffreestanding marks the program as freestanding — without assuming a full standard library. -fstack-usage reports stack usage per function, important for designing a memory budget.
Deterministic behavior also means avoiding operations with uncertain timing: algorithms whose cost depends on the data (like hash maps) can be replaced with sorted arrays or lookup tables. Measure execution time with a hardware counter or a toggled GPIO pin — episode 15 covers profiling tools for desktop systems, and the embedded version uses tools closer to the hardware.
RTOS (Real-Time Operating System) like FreeRTOS and Zephyr provide multitasking with priorities and time guarantees. The concepts an RTOS uses are very similar to the threads from episode 12: tasks, mutexes, semaphores, and queues. The difference: everything is based on fixed priorities and there's no heap on critical paths:
#include "FreeRTOS.h"
#include "task.h"
void sensor_task(void* param) {
(void)param;
for (;;) {
// baca sensor lalu proses
vTaskDelay(pdMS_TO_TICKS(10));
}
}
int main() {
xTaskCreate(sensor_task, "sensor", 256, nullptr, 1, nullptr);
vTaskStartScheduler();
}xTaskCreate(sensor_task, "sensor", 256, nullptr, 1, nullptr) creates a task with a 256-word stack and priority 1, then vTaskStartScheduler() hands control to the scheduler. The task runs periodically with vTaskDelay(pdMS_TO_TICKS(10)).
Inter-task communication uses RTOS queues instead of raw shared memory. Queues copy data with a deterministic blocking mechanism — safe without a manual mutex. Semaphores limit access to shared resources, and event groups wait for combinations of events. These patterns avoid the data races covered in episode 12.
Embedded devices often have no screen or filesystem. Profiling uses simple tools: toggle a GPIO pin around the measured function and observe it with a logic analyzer or oscilloscope. FreeRTOS also has tracing tools for viewing the task timeline.
Embedded optimization focuses on binary size and speed with conscious tradeoffs:
-Os: size optimization for chips with small flash.-ffunction-sections -fdata-sections: makes it easier for the linker to drop dead code.g++ -std=c++20 -Os -ffunction-sections -fdata-sections \
-Wl,--gc-sections -fno-exceptions -fno-rtti app.cpp-Wl,--gc-sections tells the linker to drop unused sections, and -Os optimizes for size. This combination is the standard for shrinking firmware.
Firmware is an embedded executable flashed into a device's storage. The deployment process: compile, produce a raw binary file with objcopy, then write it to the device with a flasher or debug probe:
arm-none-eabi-objcopy -O binary app.elf app.bin
arm-none-eabi-size app.elfarm-none-eabi-objcopy -O binary app.elf app.bin converts ELF into a raw binary for flashing, and arm-none-eabi-size app.elf shows flash and RAM usage per section. This size is the budget you must maintain throughout development.
The embedded toolchain uses compilers with a target prefix like arm-none-eabi-. Debugging runs through OpenOCD and GDB, connected to a debug probe on the device:
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg &
gdb -ex 'target remote localhost:3333' -ex load app.elfgdb -ex 'target remote localhost:3333' -ex load app.elf connects GDB to OpenOCD, which talks to the probe, then loads the firmware into RAM or flash. From there you use GDB as usual — breakpoints, stepping, and inspecting registers.
Info
Always compile with -Wall -Wextra in embedded and enable -fstack-usage. Stack overflows on devices without an OS are hard to detect and can corrupt data unpredictably.
Here's what to take away:
constexpr, std::array, and fixed-size types are the foundation of embedded code.-Os, --gc-sections, -fno-exceptions shrink firmware.In the next episode, episode 22, we'll discuss observability and production support — logging, tracing, and monitoring for C++ applications, crash dump analysis and post-mortem debugging, release engineering with versioning and packaging, as well as maintenance and documentation.