Running an AUTOSAR Project on ThreadX, Part 1: The OS and the Execution Model
This is Part 1 of a two-part post on running AUTOSAR components on ThreadX. This part covers the OS side: runnables, tasks, resources, alarms, interrupts, hooks, and everything multicore. Part 2 covers RTE communication, basic software, and what the whole exercise costs you.
Sooner or later, every embedded architect hears some version of this sentence: “We have all these AUTOSAR components already — can’t we just run them on ThreadX?” Sometimes it’s a QM core that doesn’t justify another AUTOSAR license, sometimes a prototype that has to run next week, sometimes a customer who wants an AUTOSAR architecture but a different OS underneath. The question is always the same: how much of AUTOSAR can you rebuild on a plain RTOS, and what does it cost?
Short answer: almost all of it. Long answer: this post and the next one. I went through the AUTOSAR Classic mechanisms one by one — runnables and RTE events, OS tasks and resources, alarms, IOC, partitions, timing protection in this part; sender-receiver in all its flavors, client-server and modes in Part 2 — and mapped each one onto ThreadX. Every mapping gets a rating:
- Native — ThreadX has a primitive that does the job.
- Shim — a thin layer of your code on top of ThreadX primitives.
- Build — ThreadX has nothing close. You design and implement it, and for safety-relevant parts, you also argue it.
The example project is the BCU from the foundation series: six Cortex-M7 cores, one ASIL B core, five QM cores.
The idea: keep the contract, swap the engine
The trick that makes this work is not to port the components. You keep them exactly as they are, including every Rte_Read and Rte_Call, and replace what sits underneath:
| Layer | In AUTOSAR | On ThreadX |
|---|---|---|
| Software components | Runnables calling Rte_* |
Unchanged |
| RTE API | Generated Rte_<Swc>.h headers |
Unchanged — the contract stays |
| RTE implementation | Generated by the RTE generator | The shim: buffers, queues, flags, dispatchers |
| Basic software scheduling | SchM, generated | Periodic threads calling the BSW main functions |
| OS | AUTOSAR OS (OSEK-based) | ThreadX, plus whatever the shim has to add |
| Memory protection | OS-Applications, generated MPU tables | ThreadX Modules or your own MPU handling |
The RTE API is the contract between the components and the platform. As long as that contract holds, components don’t care what fulfills it. That’s also the first rule of doing this in practice: generate the shim, don’t write it by hand. An ECU has hundreds of ports. Nobody writes 300 Rte_Read functions by hand and gets all of them right — but a small script that reads your port description (the ARXML you already have, or a YAML file like the memory map from Part 2 of the foundation series) gets them right every time, or fails loudly.
The execution model
Runnables and RTE events
In AUTOSAR, you don’t write tasks. You write runnables, and RTE events decide when they run: every 10 ms, when data arrives, when someone calls an operation. The RTE generator then maps runnables into OS tasks. On ThreadX, the shim does the same with one thread per OS task and one event flag per RTE event:
#define EV_10MS (1u << 0) /* TimingEvent 10 ms */
#define EV_SPEED_RX (1u << 1) /* DataReceivedEvent */
#define EV_SERVE_OP (1u << 2) /* OperationInvokedEvent */
static TX_EVENT_FLAGS_GROUP task10ms_ev;
static TX_TIMER task10ms_tmr;
static void tick_10ms(ULONG arg) /* runs in the ThreadX timer thread */
{
(void)arg;
tx_event_flags_set(&task10ms_ev, EV_10MS, TX_OR);
}
static void Task_10ms_entry(ULONG arg)
{
ULONG ev;
(void)arg;
LightCtrl_Init(); /* InitEvent */
for (;;) {
tx_event_flags_get(&task10ms_ev, EV_10MS | EV_SPEED_RX | EV_SERVE_OP,
TX_OR_CLEAR, &ev, TX_WAIT_FOREVER);
rte_task10ms_copy_in(); /* implicit inputs, see Part 2 */
if (ev & EV_10MS) { R_Control(); }
if (ev & EV_SPEED_RX) { R_OnSpeed(); }
if (ev & EV_SERVE_OP) { R_ServeOp(); }
rte_task10ms_copy_out(); /* implicit outputs */
}
}
/* at init, with a 1 ms tick: */
/* tx_timer_create(&task10ms_tmr, "10ms", tick_10ms, 0, 10, 10, TX_AUTO_ACTIVATE); */
Two catches. First, event flags don’t count. If the 10 ms timer fires twice before the thread gets to run, the thread sees one flag, not two — the activation is gone, silently. AUTOSAR basic tasks can count multiple activations and report E_OS_LIMIT when they overflow. If you care (and for anything timing-supervised, you do), use a counting semaphore for the timing event, or at least count overruns in the timer callback. Second, ThreadX timer callbacks run in the timer thread (or in the tick interrupt, depending on configuration), so they set a flag and leave. Nothing else.
The full list of RTE events, what they mean in AUTOSAR, and where they come from on ThreadX:
| RTE event | When it fires in AUTOSAR | Triggered on ThreadX by | Watch out for | Rating |
|---|---|---|---|---|
TimingEvent |
Periodically, with the configured period (and an offset, if the RTE generator staggers runnables) | tx_timer expiration function setting the task’s flag; offsets via the timer’s initial ticks |
Flags don’t count missed activations (see above). Period must be a multiple of the tick. Two timers with the same period drift apart only if they are created at different ticks — create them together | Native |
InitEvent |
Once, after Rte_Start, before any other runnable of the component |
Call before the thread’s for (;;) loop |
Order across threads: a component’s init must not read data that another thread hasn’t initialized yet. Either init everything in tx_application_define or hold all threads behind a start barrier |
Native |
BackgroundEvent |
Whenever nothing else runs, in the lowest-priority (background) task | Lowest-priority thread calling the runnables in an endless loop | Never blocks, so it must be the only thread on that priority, and below the idle-time measurement if you have one. Keep it preemptible: no interrupt locks inside | Native |
DataReceivedEvent |
When a new value arrives on an R-port data element (queued or unqueued) | The shim’s Rte_Write/Rte_Send (or the COM Rx callback for bus data) setting the receiver’s flag |
For queued data, one flag for several messages: the runnable must drain the queue in a loop, not read one element. For unqueued data, several writes collapse into one activation — that’s last-is-best, and correct | Shim |
DataReceiveErrorEvent |
When a received value is invalidated, or its alive timeout expires (unqueued data) | Shim: Rte_Invalidate path, or a supervision timer that checks the timestamp of the buffer |
Needs a periodic check, because a timeout is the absence of an event — some thread has to look at the clock | Shim |
DataSendCompletedEvent |
When an explicit Rte_Write/Rte_Send has been transmitted (or failed) |
Shim: immediately for local data, from the COM Tx confirmation callback for bus data | The confirmation callback runs in interrupt or BSW context: set a flag and leave | Shim |
DataWriteCompletedEvent |
When an implicit Rte_IWrite has been published, after the task ends |
Shim: in copy_out, after the task-level copies are written back |
Fires once per task activation, not once per Rte_IWrite |
Shim |
OperationInvokedEvent |
When a client calls the server’s operation (client-server) | Server thread waiting on its request queue (or a flag set by the shim next to the queue) | Several clients share one queue: the request needs a client ID and a sequence number, so the reply finds its way back (see Part 2) | Shim |
AsynchronousServerCallReturnsEvent |
When the result of an asynchronous Rte_Call is available |
Server setting the client’s flag after writing the reply slot | The flag only says “something is ready” — Rte_Result must check the sequence number and drop stale answers |
Shim |
SwcModeSwitchEvent |
On entry to, exit from or transition between modes of a mode-switch port | The mode shim: a flag per mode user, the transition in the dispatcher (see Part 2) | Mode-disabled runnables must not start during the transition — the dispatcher checks the mode before every runnable, not once per loop | Shim |
ModeSwitchedAckEvent |
At the mode manager, when all mode users have completed the switch | The mode shim, after the last user has finished its transition | “All users” across several threads or cores needs a counter or a barrier, not a single flag | Shim |
SwcModeManagerErrorEvent |
At the mode manager, when a mode switch could not be performed (e.g. the switch queue overflowed) | The mode shim, from Rte_Switch |
Rarely used — and therefore rarely tested. Decide whether you need it at all | Shim |
ExternalTriggerOccurredEvent |
When another component (or the BSW) raises a trigger on a trigger port | Rte_Trigger → tx_event_flags_set on every triggered task |
One trigger can activate runnables in several tasks: the shim sets one flag per task. Queued triggers (with a queue length) need a counter | Native |
InternalTriggerOccurredEvent |
When a runnable triggers another runnable of the same component (Rte_IrTrigger) |
tx_event_flags_set, or a direct call if both runnables are in the same task |
Same as above; the direct call is only allowed if the configuration doesn’t require a separate activation | Native |
TransformerErrorEvent |
When a transformer (E2E, SOME/IP serializer) reports a hard error on the server side of a call | E2E/transformer wrapper in the shim setting the server’s flag | The error has to be stored next to the flag, so the runnable can tell which request failed and why | Shim |
OsTaskExecutionEvent |
Every time the OS task the runnable is mapped to executes, independent of why | Runnable called on every pass of the thread loop | Runs whenever any flag of the thread is set — including the ones you didn’t think of | Native |
OS tasks: basic, extended, activation
AUTOSAR OS is OSEK underneath, and OSEK knows two kinds of tasks. Basic tasks run to completion: ActivateTask starts them, they end with TerminateTask. Extended tasks can also wait for events. On ThreadX, both become threads in an endless loop:
- Basic task → thread blocking on a counting semaphore.
ActivateTaskistx_semaphore_put, the semaphore count is the number of pending activations, and the shim returnsE_OS_LIMITwhen it exceeds the configured maximum.TerminateTaskandChainTaskbecome “return to the top of the loop” (plus an activation of the next task forChainTask). Rating: Shim. - Extended task → thread with an event flag group.
SetEvent,WaitEvent,ClearEventandGetEventmap almost one to one ontotx_event_flags_setandtx_event_flags_get, with 32 flags per group. Rating: Native. - Priorities → careful: in OSEK, a higher number means a higher priority. In ThreadX, 0 is the highest. The shim generator inverts the numbers exactly once, in one place, and nobody ever touches priorities by hand again. This is not a hypothetical bug. It is the bug.
- Non-preemptive tasks and internal resources → ThreadX’s preemption-threshold. A thread with priority 10 and a preemption threshold of 5 can only be preempted by threads above priority 5 — exactly the “group of tasks that don’t preempt each other” that OSEK builds with internal resources. Rating: Native, and one of the nicest matches in this whole post.
Schedule()(a non-preemptive task offering a rescheduling point) → temporarily lowering the preemption threshold.tx_thread_relinquishonly yields to threads of the same priority, so it is not the same thing. Rating: Shim.
Resources and exclusive areas
OSEK resources use the immediate priority ceiling protocol: when a task takes a resource, it immediately runs at the highest priority of all tasks that use that resource. Nobody else who needs the resource can run, so nobody can block on it, and deadlocks are impossible by design. ThreadX mutexes use priority inheritance instead: the holder is boosted only once a higher-priority thread actually blocks. Both bound the blocking time, but they behave differently:
The good news: preemption-threshold gives you the ceiling protocol on ThreadX almost for free.
/* GetResource(RES_SPEED) -- ceiling = highest-priority user of RES_SPEED */
UINT old;
tx_thread_preemption_change(tx_thread_identify(), CEILING_RES_SPEED, &old);
/* ... critical section ... */
/* ReleaseResource(RES_SPEED) */
tx_thread_preemption_change(tx_thread_identify(), old, &old);
For nested resources, the shim keeps a small stack of old thresholds. For resources shared with interrupts (OSEK allows Category 2 ISRs to use resources), preemption-threshold isn’t enough, because it only affects threads — the shim has to mask the interrupt instead. Rating: Shim.
Exclusive areas (Rte_Enter/Rte_Exit for components, SchM_Enter/SchM_Exit for basic software) are the same story with one more twist: AUTOSAR lets you configure how each exclusive area is implemented — interrupt lock, OS resource, or no protection at all if the configuration proves it’s unnecessary. The shim needs the same switch per exclusive area: tx_interrupt_control(TX_INT_DISABLE) for short copies, preemption-threshold for longer sections, a mutex only where you really want inheritance. Rating: Shim.
Alarms, counters and schedule tables
- Alarms (
SetRelAlarm,SetAbsAlarm,CancelAlarm, with the actions “activate task”, “set event” or “callback”) →tx_timerwith an expiration function that performs the action. Rating: Native. - Counters → the ThreadX tick (
tx_time_get) covers the system counter. Additional counters driven by hardware (an angle counter, a second time base) are yours. Rating: Shim. - Schedule tables (expiry points that activate tasks and set events at fixed offsets, optionally synchronized to a global time) → a table plus one timer that walks through the expiry points is easy. Synchronization to an external time base, with drift correction, is not. Rating: Shim without synchronization, Build with it.
Interrupts
- Category 1 vs. Category 2 ISRs → ThreadX doesn’t make that distinction; on the Cortex-M ports, an ISR can call the ThreadX services that are allowed from interrupt context (with
TX_NO_WAIT). “This ISR never calls the OS” is a rule you enforce in reviews. Rating: Shim (a naming convention and a review rule). DisableAllInterrupts,SuspendAllInterrupts→tx_interrupt_control(TX_INT_DISABLE)and restore. Nesting counters forSuspend/Resumeare a few lines in the shim. Rating: Native.SuspendOSInterrupts(block only interrupts that may call the OS, let the fast Category 1 ISRs through) → on Cortex-M that’s a BASEPRI-based lock instead of PRIMASK. Check whether your ThreadX port can be configured that way; if not, it’s a port change. Rating: Shim or Build.
Hooks and error handling
- StartupHook, ShutdownHook →
tx_application_defineand your own shutdown path. Rating: Native. - ErrorHook → ThreadX returns error codes from every service (with error checking enabled). The shim wraps the calls and funnels errors into one handler. Rating: Shim.
- Stack monitoring →
tx_thread_stack_error_notify, with stack checking enabled. Rating: Native. - ProtectionHook → the MemManage fault handler, plus the module manager’s fault handling if you use ThreadX Modules. Rating: Shim.
Multicore and partitioning
Cross-core communication
AUTOSAR OS generates the IOC (Inter-OS-Application Communicator) for anything that crosses cores or partitions: IocWrite/IocRead for last-is-best, IocSend/IocReceive for queued data. Cross-core ActivateTask and SetEvent work too.
ThreadX on a multicore microcontroller like ours usually runs as one independent instance per core (AMP). There is no shared kernel, so no queue, flag or semaphore spans two cores. The bridge is shared memory and a protocol you define:
typedef struct { volatile uint32_t seq; Speed_T data; } mbox_speed_t;
extern mbox_speed_t mbox_speed; /* shared SRAM, mapped non-cacheable */
void mbox_speed_write(Speed_T v) /* the single writer, Core 1 */
{
mbox_speed.seq++; __DMB(); /* odd: writing */
mbox_speed.data = v;
__DMB(); mbox_speed.seq++; /* even: done */
}
bool mbox_speed_read(Speed_T *out) /* reader, Core 0: bounded, never waits on the writer */
{
for (int tries = 0; tries < 3; tries++) {
uint32_t s1 = mbox_speed.seq; __DMB();
*out = mbox_speed.data; __DMB();
if ((s1 == mbox_speed.seq) && ((s1 & 1u) == 0u)) { return true; }
}
return false; /* stale: use the fallback */
}
Three things carry this, and the IOC would have handled every one of them:
- Caches. A Cortex-M7 has a data cache, and the caches of different cores are not coherent. Shared data lives in a region the MPU marks non-cacheable, or both sides do explicit cache maintenance. Forget this and the reader processes a value that’s been stale for minutes, with a perfectly consistent sequence counter.
- Bounded retries. An unbounded seqlock loop lets a misbehaving QM writer stall the ASIL reader — interference through time.
- One writer per mailbox. Two writers need two mailboxes.
For queued data, the same idea becomes a single-producer, single-consumer ring buffer, plus an inter-core interrupt so the receiving core can wake its thread with tx_semaphore_put instead of polling. Cross-core ActivateTask/SetEvent work the same way: an inter-core interrupt, and a handler on the target core that sets the flag. Rating: Build.
Spinlocks
GetSpinlock/ReleaseSpinlock protect data that several cores modify. On ThreadX, you build them from the chip’s hardware semaphore unit if it has one, or from exclusive load/store instructions — but only if the interconnect actually supports exclusive accesses to the shared SRAM across cores. Many don’t, and an LDREX/STREX spinlock on memory without a global exclusive monitor locks nothing at all. Check the reference manual before you trust it. And the rule from Part 4 of the foundation series still applies: no spinlock is ever shared between ASIL and QM. Rating: Build.
OS-Applications, memory protection and trusted functions
AUTOSAR groups tasks, ISRs and data into OS-Applications, marks them trusted or non-trusted, and generates the MPU configuration per partition. On ThreadX, the closest thing is ThreadX Modules: separately linked application modules that the module manager loads, starts and stops, each running unprivileged in its own MPU-protected memory. It’s a different model — modules are separate binaries — but the result is the same: QM code that can’t touch ASIL memory.
- Partitioning and the MPU → ThreadX Modules, or a hand-made scheme that reprograms the MPU on every context switch. Rating: Shim with Modules, Build without.
CallTrustedFunction(a QM partition asks the ASIL partition for a service) → a request queue to a thread inside the ASIL partition, exactly like a client-server call. Rating: Shim.CallNonTrustedFunction(the call gate from the ASIL partition into QM code) → a request to a thread in the QM partition, with a timeout, and validation of the result. Rating: Shim.- Terminating and restarting a partition (
TerminateApplication) → the module manager can stop and restart a module. Rating: Native with Modules.
Timing protection
AUTOSAR OS can monitor an execution budget per task, a time frame between activations, and lock budgets for resources and interrupt locks. When a task exceeds them, the ProtectionHook decides what happens. ThreadX has nothing equivalent built in. Its execution profiling can measure how long threads run, and from there you build a monitor: per-activation runtime accounting in the dispatcher, a deadline check, and a watchdog supervisor that escalates. For ASIL software, that monitor is a safety mechanism — and has to be developed and argued like one. Rating: Build.
Service protection
AUTOSAR OS rejects calls from the wrong context (WaitEvent from an ISR, TerminateTask with a resource still held). ThreadX’s error checking catches the first kind (TX_CALLER_ERROR); the second kind is the shim’s job. Rating: Native plus Shim.
The summary table, part 1
| Mechanism | ThreadX building blocks | Rating |
|---|---|---|
| Runnables + TimingEvent | Thread, tx_timer, event flags |
Native |
| Other RTE events | Event flags set by the shim | Shim |
Basic tasks, ActivateTask, multiple activation |
Thread + counting semaphore | Shim |
Extended tasks, SetEvent/WaitEvent |
Thread + event flags | Native |
| Non-preemptive tasks, internal resources | Preemption-threshold | Native |
| OS resources (priority ceiling) | tx_thread_preemption_change |
Shim |
Exclusive areas (Rte_/SchM_Enter) |
Interrupt lock, preemption-threshold or mutex | Shim |
| Alarms | tx_timer |
Native |
| Schedule tables | Table + timer; synchronization | Shim / Build |
| Interrupt locks | tx_interrupt_control |
Native |
SuspendOSInterrupts (Category 1 stays alive) |
BASEPRI-based port | Shim / Build |
| Hooks, stack monitoring | tx_application_define, tx_thread_stack_error_notify |
Native |
| IOC, cross-core events | Shared memory protocol, inter-core interrupts | Build |
| Spinlocks | Hardware semaphores or exclusive accesses | Build |
| OS-Applications, MPU partitioning | ThreadX Modules | Shim |
| Trusted / non-trusted function calls | Request queues across partitions | Shim |
| Timing protection | Runtime accounting + monitor + watchdog | Build |
The OS side holds up well: most of OSEK maps onto ThreadX natively or with a thin shim, and preemption-threshold turns out to be the secret weapon for internal resources and the priority ceiling. What’s left — cross-core communication, spinlocks, timing protection — is exactly the part that’s hard everywhere.
Next up
So far, the components have a place to run and a reason to wake up. What they don’t have yet is anybody to talk to. Part 2 builds the RTE communication on top of this execution model — sender-receiver, implicit access, client-server, modes — brings in the basic software, and ends with the honest bill: what you lose without the AUTOSAR toolchain, and how to win some of it back.
Sources and further reading
Standards and specifications
- AUTOSAR Classic Platform: Specification of Operating System, R23-11 — tasks, resources, alarms, schedule tables, spinlocks, OS-Applications, timing and service protection: the contract this post rebuilds.
- AUTOSAR Classic Platform: Specification of RTE, R23-11 — runnables, RTE events and exclusive areas.
- ISO 17356-3:2005 — Road vehicles — Open interface for embedded automotive applications — Part 3: OSEK/VDX Operating System — the OSEK base AUTOSAR OS is built on.
- Eclipse ThreadX source code and ThreadX documentation — the API (threads, event flags, mutexes with priority inheritance, timers) used for the mapping.
Books
- Edward L. Lamie: Real-Time Embedded Multithreading Using ThreadX, 2nd ed., Newnes, 2009 — the ThreadX book, written by the people who built it.
- Edward Row: Mastering Modern RTOS: Zephyr and ThreadX Internals, APIs, and Advanced Development — ThreadX next to Zephyr, good for seeing which design choices are ThreadX-specific.
- Olaf Kindel, Mario Friedrich: Softwareentwicklung mit AUTOSAR, dpunkt.verlag, 2009 (German) — a practical walkthrough of the AUTOSAR OS and RTE concepts.
- Giorgio C. Buttazzo: Hard Real-Time Computing Systems, 3rd ed., Springer, 2011 — priority ceiling and priority inheritance, the theory behind resources and mutexes.
The animations in this post are rendered with manim from scripts/manim/autosar_threadx_scenes.py.