Migrate from FreeRTOS to Zephyr with AI
By Embedder ·

Moving from FreeRTOS to Zephyr changes how firmware describes hardware, schedules work, manages dependencies, and interacts with drivers. AI can help investigate the existing application, propose changes, and repeat build and test steps. The engineering work is deciding which behavior must survive the migration and collecting evidence that it does.
You can do this with a general-purpose AI coding assistant connected to your repository and development tools. A firmware-focused option such as Embedder may make the process easier by bringing hardware documentation, debugging, and measurements into the same workflow. Either approach needs a reproducible environment, explicit requirements, and tests on the target hardware.
Understand what the migration should accomplish
Start with a product requirement: support a new board, consolidate middleware, add connectivity, or reduce the cost of maintaining several hardware variants. If the existing FreeRTOS application meets its requirements and has well-supported drivers, retaining it may be the better engineering decision.
Published migrations illustrate different reasons to make the change. Boston Engineering moved an STM32 application to Zephyr for CANopen integration and future connectivity. During the port, the team replaced custom SPI flash code and an untested ringbuffer with Zephyr facilities, but also encountered an ISR-related pipe bug that required an upstream fix. Reusing a subsystem can reduce maintenance while still introducing behavior that needs testing. See Boston Engineering’s migration account.
Atym’s Ocre runtime had a broader platform requirement: consistent hardware interfaces and fewer independently managed libraries and middleware components. Its account also describes variation in driver and board-support quality. That makes support for the exact peripheral features you use an early investigation, even when the board already appears in Zephyr’s catalog. See Atym’s FreeRTOS-to-Zephyr migration.
Define the first milestone narrowly: the current application’s essential behavior running on Zephyr. Record optional changes—such as adopting a new bootloader or replacing an application messaging framework—as separate decisions with their own tests.
1. Map the application and record a baseline
Ask the AI assistant to trace RTOS dependencies through application code, wrappers, interrupt handlers, middleware, and build variants. A list of xTaskCreate() calls is only a starting point. The useful output explains how work moves through the system and which assumptions connect the components.
For each task, record priority, stack allocation, activation source, deadlines, shared resources, and shutdown behavior. For each communication path, record whether it carries copied data, pointers, event bits, or counts. Include queue capacity, buffer ownership, timeout behavior, and what happens when the consumer falls behind.
Jacob Beningo’s migration guide recommends analyzing the architecture before porting primitives and considering an OS abstraction layer. A small abstraction can let you test FreeRTOS and Zephyr implementations against the same application contract. Its value depends on whether you need both implementations long term. If the application already uses CMSIS-RTOS v2, review Zephyr’s documented compatibility limitations before assuming the port will be transparent.
Capture the source SDK, FreeRTOS version, compiler, board revision, build options, and firmware artifact. Run representative workloads and save measurements with their method and tolerances.
| Area | Baseline to capture |
|---|---|
| Timing | Sampling period, jitter, interrupt-to-thread latency, deadline misses |
| Data flow | Throughput, ordering, queue occupancy, dropped or overwritten messages |
| Memory | Flash and RAM usage, task stacks, heap behavior under sustained load |
| Hardware | Bus settings, transaction traces, peripheral error and recovery behavior |
| Power | Sleep current, wake latency, energy over a representative duty cycle |
| Persistent state | Calibration, credentials, settings, and behavior after interrupted writes |
A useful planning request is specific about both evidence and constraints:
Plan a FreeRTOS-to-Zephyr migration for the current board. Inventory RTOS
dependencies, including wrappers, middleware, and ISR interactions. Cite
the files and functions supporting each finding, and flag unknowns.
Preserve the sensor API, 10 ms sampling period, sample ordering, and UART
packet format. Use the recorded baseline for timing and memory limits.
Check board and driver support in the selected Zephyr release. Propose
incremental changes and tests before editing. Identify any assumption
that requires a schematic, datasheet, or measurement to confirm.
The example’s 10 ms period is a requirement to replace with your own, not a default for the migration.
2. Establish a reproducible Zephyr build
Choose the destination release before generating code. Give the assistant that release’s headers, bindings, examples, and documentation. Vendor SDKs can carry different driver implementations and configuration options from upstream Zephyr; mixing examples across versions can produce plausible code that does not build.
Create the smallest application that boots and produces observable output. Establish its CMakeLists.txt, prj.conf, board target, and required overlays before moving application services. Zephyr’s application development guide explains this structure.
Once the toolchain and workspace are configured, a basic sequence is:
# Replace YOUR_BOARD_TARGET with the target for the selected SDK release.
west build -p always -b YOUR_BOARD_TARGET -d build/zephyr-port path/to/app
west flash -d build/zephyr-port
The board’s supported runner and probe setup determine how flashing works. A pristine build helps eliminate stale configuration when changing boards or overlays. See Zephyr’s build, flash, and debug commands.
Record dependency revisions as well as the application commit:
west manifest --freeze -o west-frozen.yml
The frozen manifest records project revisions as commit SHAs. Preserve local patches, the manifest repository’s revision, toolchain versions, and build commands separately. Together, these let another engineer reproduce the result.
Ask the assistant to explain each build failure against the selected source tree. A missing driver, an unmet Kconfig dependency, and an obsolete API call require different fixes.
3. Bring up hardware one peripheral at a time
Zephyr divides configuration between devicetree, Kconfig, and runtime APIs. Devicetree describes hardware and its boot-time configuration. Kconfig selects software features and drivers. Some settings can still change at runtime. See devicetree versus Kconfig.
For a sensor on an I2C bus, trace the bus controller, pinctrl configuration, device address, bus frequency, interrupt pin, power supply, and driver binding. Give the AI assistant the schematic and existing initialization code, then ask it to justify each proposed setting against those sources. Do not let a development-board example silently determine the custom board’s wiring.
Inspect the generated configuration after building. In a typical single-image build, build/zephyr-port/zephyr/zephyr.dts shows the resolved hardware description and build/zephyr-port/zephyr/.config shows the selected software configuration. An undefined reference to __device_dts_ord_<N> error commonly means a referenced device has no allocated driver instance; check whether the node and its driver are enabled. Zephyr’s devicetree troubleshooting guide walks through that diagnosis.
Remember that prj.conf requests settings; .config contains the resolved result. An assignment can be ignored when its dependencies are unmet. Ask the assistant to trace those dependencies instead of repeatedly adding the same flag. See Kconfig configuration values.
For each peripheral, verify the features the application actually needs: DMA, asynchronous transfers, chip-select behavior, interrupt mode, sleep states, and error recovery. If retaining vendor HAL code, assign ownership explicitly so it does not compete with a Zephyr driver for the same peripheral, interrupt, DMA channel, or clocks.
NXP’s Platform Accelerator port shows the potential benefit of completing this work: its team replaced board-specific integration code with common code using Zephyr’s driver interfaces, with board configuration handling differences such as display support and heap size. That portability depended on the required hardware and runtime support being present. See NXP’s porting presentation.
Make the first hardware milestone concrete: one valid sensor transaction reaching the output. Add periodic acquisition, buffering, and concurrent activity afterward.
4. Port kernel behavior explicitly
For each conversion, ask the assistant to document the old behavior, the proposed implementation, and a test that would expose a mismatch. Several differences deserve particular attention.
Priorities, stacks, and startup order
FreeRTOS assigns higher task priority to larger numbers. Zephyr assigns higher thread priority to smaller numbers: 0 is the highest preemptible priority, while negative values select cooperative priorities. Preserve the intended ordering while accounting for system threads and time slicing. An arithmetic conversion that produces negative priorities can change the scheduling model. See FreeRTOS scheduling and Zephyr threads.
Upstream FreeRTOS specifies xTaskCreate() stack depth in StackType_t elements. With four-byte elements, a depth of 512 represents 2,048 bytes. Zephyr’s K_THREAD_DEFINE() takes a size in bytes. Check the source SDK: ESP-IDF already uses bytes for task stack sizes. This conversion gives a starting allocation; different call paths and libraries can change actual stack demand. See the FreeRTOS API, Zephyr thread API, and ESP-IDF documentation.
Initialization order also changes. Zephyr is already scheduling when application main() runs; a FreeRTOS sequence that creates objects and then calls vTaskStartScheduler() cannot be copied unchanged. AMD’s producer/consumer example creates Zephyr threads with K_FOREVER as their initial delay, initializes shared objects, then calls k_thread_start(). The example also distinguishes allocating thread storage from initializing a thread at runtime. See AMD’s FreeRTOS-to-Zephyr porting example.
Queues, ownership, and interrupt context
A fixed-size FreeRTOS queue often maps to k_msgq, which copies each message. If the message contains a pointer, only the pointer is copied; the referenced buffer still needs an ownership and lifetime rule. Pointer-based queues and FIFOs have different requirements and should be evaluated separately. See Zephyr message queues.
For an already available sample, a minimal enqueue helper could look like this:
#include <stdint.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/atomic.h>
struct sample {
uint32_t sequence;
int32_t value;
};
K_MSGQ_DEFINE(samples, sizeof(struct sample), 16, 4);
static atomic_t dropped_samples;
/* Called from a regular ISR after the sample is available. */
void enqueue_sample_from_isr(uint32_t sequence, int32_t value)
{
const struct sample sample = {
.sequence = sequence,
.value = value,
};
if (k_msgq_put(&samples, &sample, K_NO_WAIT) != 0) {
atomic_inc(&dropped_samples);
}
}
This illustrative fragment implements an explicit drop-newest policy and records unsuccessful submissions. A consumer and the hardware-specific interrupt handler are still required. Test whether dropping is acceptable, whether capacity is sufficient, and whether the consumer can sustain the input rate. For lossless acquisition, use an appropriate buffering or flow-control design and verify it under overload.
Review return-value tests during conversion: k_msgq_put() returns zero on success, unlike FreeRTOS queue-send checks against pdPASS. In ISR context, use a nonblocking timeout. See the message queue API.
The fragment is unsuitable for a zero-latency interrupt: Zephyr defines kernel API use from that context as undefined behavior. Removing a FreeRTOS FromISR suffix is therefore insufficient; verify every operation’s permitted context against Zephyr’s interrupt rules.
Notifications, event flags, and locking
FreeRTOS task notifications can represent a wakeup, a count, event bits, or a value with overwrite semantics. Choose the replacement from the producer and consumer behavior. A binary semaphore can represent “work is available,” but it cannot preserve an arbitrary number of pending occurrences. A counting semaphore retains counts within its limit; a queue retains individual payloads. See FreeRTOS task notifications.
Event flags need special care. In k_event_wait(), reset=true clears existing events before waiting. It does not mean FreeRTOS-style clear-on-exit. Zephyr 4.3 provides k_event_wait_safe() for atomic consumption of received bits; verify availability in the pinned SDK and check multi-waiter behavior. Also distinguish k_event_post(), which merges event bits, from k_event_set(), which replaces the current set. Test events posted before the wait, concurrent posts, wait-any versus wait-all, and multiple consumers. See Zephyr’s event API.
Keep mutex ownership distinct from semaphore signaling. Zephyr mutexes support recursive locking and priority inheritance but are not intended for ISR use. Check lock ordering and timeout paths, especially where a task holds one lock while waiting for another. See Zephyr mutexes.
If moving to multiple cores, review critical sections separately. Preventing scheduling on one CPU does not establish mutual exclusion against another CPU. Choose the appropriate Zephyr lock or atomic operation for the shared state, and keep spinlock-protected sections bounded and nonblocking. See Zephyr’s SMP guidance.
Timers, deferred work, and periodic execution
FreeRTOS software timer callbacks execute in the timer service task and must not block. Zephyr k_timer expiry callbacks execute in interrupt context. A callback that accesses a driver or performs substantial processing may need to signal a thread or submit work, with the added scheduling delay included in its timing budget. See FreeRTOS software timers and Zephyr timers.
A work item is not an event counter. Submitting a k_work item that is already queued does not add another copy, and a busy shared workqueue can delay its execution. Keep individual samples in a queue or buffer when every occurrence matters. A dedicated workqueue can isolate workloads, but adds a thread and stack that must be budgeted. See Zephyr workqueues.
Review periodic loops independently. Processing a sample and then sleeping for 10 ms produces a period longer than 10 ms. When the source uses a delay-until schedule, preserve its reference timeline and define the overrun policy: catch up, skip missed periods, or report a fault. Zephyr supports absolute timeouts, but clock resolution and scheduling latency still affect execution. See Zephyr’s timing documentation.
5. Include the rest of the product
A working sensor loop leaves several migration boundaries to check:
- Networking and Bluetooth. Inventory connection-state callbacks, buffer ownership, retry behavior, and persistent identities or bonds. Test reconnects, interrupted transfers, and operation under concurrent radio traffic. Treat a vendor-stack replacement as its own integration effort.
- Stored data. Preserve flash partitions, calibration, credentials, and settings formats deliberately. Zephyr’s settings subsystem supports multiple storage backends, which does not establish compatibility with the source firmware’s bytes. As a concrete example, enabling
CONFIG_NVS_DATA_CRCon existing NVS data written without that option invalidates that data. Plan conversion and interrupted-conversion tests. See settings and NVS. - Boot and updates. If introducing MCUboot, review image layout, signing, confirmation, and recovery alongside the installed bootloader. Test an upgrade from an actual source-firmware device, including its stored state. Booting a freshly erased board exercises a different path. See MCUboot’s design.
- Power and recovery. Check peripheral suspend/resume, wake sources, watchdog feeding, and behavior after a timeout. A sleeping application thread does not establish that every device has entered a low-power state. See Zephyr’s power management documentation.
Ask the assistant to produce a separate change and validation plan for each boundary. This makes intentional redesigns visible and keeps failures traceable.
6. Validate in layers, then combine workloads
Use small tests to check parsers, state machines, buffer ownership, and synchronization edge cases. Zephyr’s Ztest framework provides test facilities, while Twister can organize builds and test runs across configurations and targets. Inspect the reports to distinguish tests that executed from cases that were only built, skipped, or filtered out.
Run suitable logic tests on a host or simulator for fast feedback. The native simulator is useful for functional testing, but it does not reproduce the target MCU’s instruction timing or physical peripherals. Timing, electrical behavior, and power require target measurements.
| Check | Evidence to collect |
|---|---|
| Acquisition under peak load | Timestamp or GPIO traces showing period, jitter, and missed deadlines |
| Interrupt-to-consumer latency | Correlated interrupt and processing markers using a consistent timebase |
| Queue saturation | Sequence numbers, occupancy, and drop counters under a deliberately slowed consumer |
| Peripheral recovery | Results from induced timeouts, disconnects, and repeated recovery attempts |
| Memory headroom | Build reports and runtime stack measurements during worst observed workloads |
| Sleep and wake behavior | Current captures, wake latency, and successful transactions after resume |
| Updates and retained state | Upgrade, interruption, reboot, and applicable recovery tests on populated devices |
Use the thread analyzer to inspect runtime stack usage, including system threads and workqueues. Exercise error paths and logging-heavy workloads as well as normal operation. An observed stack margin is evidence from those runs, not proof of the maximum possible demand.
Have the AI assistant compare results with the baseline and report failures, intentional differences, and untested conditions separately. Save commands, firmware identifiers, test inputs, and raw captures. Logging and debugger halts can affect measurements; repeat timing-sensitive checks under representative operating conditions. Hard deadlines may also require worst-case analysis beyond observed test results.
Why choose Embedder for this migration
A general-purpose AI coding agent can help inventory dependencies, edit code, and run builds. With suitable scripts and integrations, it can also work with debuggers and bench instruments. That remains a viable approach, especially if your team already maintains that automation. You will need to connect those tools and make their results available to the agent.
For teams that want code changes and hardware validation in one workflow, Embedder is our recommended starting point. Its RTOS migration workflow follows the work this migration requires: establish the baseline, account for kernel differences, and validate the application on hardware. That gives the team a documented process to follow from the initial port through runtime investigation.
The practical advantage becomes clear when the firmware builds but a sensor stops responding under radio load. Embedder brings serial output, debugger state, and supported instrument captures alongside the code and conversation. You can investigate the failure using UART history, inspect execution with GDB, and use a supported logic or power analyzer to collect the next piece of evidence. Its monitoring and debugging documentation covers the integrations and setup for your equipment.
That combination makes Embedder a compelling choice for the repeated edit, build, flash, and measure cycle of an RTOS migration. Start by checking support for your probe and instruments, then use the migration guide to organize the first subsystem port. Keep the acceptance tests defined above: as Zephyr’s practical migration session emphasizes, AI assistance works best alongside a clear engineering strategy and validation.
Keep the source build until the migration is verified
Retain a reproducible FreeRTOS build and its baseline captures until the Zephyr application passes the agreed checks. Finish with pinned dependencies, reviewed hardware configuration, documented API mappings, regression tests, and measurements tied to a specific firmware revision.
The final review should answer what behavior was preserved, what changed intentionally, and what remains unverified. That record gives the team a defensible migration decision and a useful baseline for the next firmware change.
