Inhaltsverzeichnis

Nebenläufigkeit auf Embedded-Targets & RTOS

Auf Linux und macOS stellt std.thread POSIX-Threads bereit. Auf Bare-Metal-Targets (ARM Cortex-M, RISC-V ohne Betriebssystem) und RTOS-Umgebungen (FreeRTOS auf ESP32) gelten andere Regeln. Dieses Dokument beschreibt die Muster und Werkzeuge, die Lyx für diese Umgebungen bereitstellt.

→ POSIX-Threads (Linux/macOS): Threads & Nebenläufigkeit
→ Hardware-Pointer und @volatile: Pointer & Inlining
→ Safety-Pragmas: Attribute & Pragmas · DO-178C Compliance


1. POSIX-Threads vs. Bare-Metal

Kriterium std.thread (POSIX) Bare-Metal ohne OS RTOS (FreeRTOS)
Zielplattform Linux, macOS, BSD ARM Cortex-M, RISC-V ESP32, STM32 mit FreeRTOS
Lyx-Target x86_64, arm64 arm_cm, riscv xtensa
Scheduler Betriebssystem Kein Scheduler — manuell FreeRTOS-Scheduler (preemptiv)
Kontextwechsel OS-Kernel SysTick-ISR oder kooperativ FreeRTOS-Tick
Mutex / Sync MutexLock, CondWait Interrupts sperren FreeRTOS-Queue, Semaphore
Heap-Allokation in ISR Erlaubt (aber riskant) Verboten Verboten
std.thread nutzbar ✗ (kein pthread) ✗ (andere ABI)
Faustregel: std.thread setzt einen POSIX-kompatiblen Kernel voraus. Auf Bare-Metal und in RTOS-Tasks wird Nebenläufigkeit durch Interrupt-Service-Routinen, kooperative Zustandsautomaten und RTOS-Primitiven umgesetzt.

2. Superloop — kooperatives Multitasking

Das einfachste Nebenläufigkeitsmodell für eingebettete Systeme ohne RTOS ist der Superloop: eine Endlosschleife, die mehrere Aufgaben reihum aufruft. Jede Aufgabe muss schnell enden (nicht blockieren), damit die anderen nicht verhungern.

unit superloop;
import std.io;

// Zustandsvariablen für jede Task
var sensor_ready: bool   := false;
var display_dirty: bool  := false;
var last_tick: int64     := 0;

// Task 1: Sensor lesen (alle 10 ms)
fn TaskSensor(now_ms: int64) {
    if (now_ms - last_tick < 10) { return; }
    last_tick := now_ms;
    // ... Sensor abfragen ...
    sensor_ready := true;
}

// Task 2: Display aktualisieren (wenn Daten neu)
fn TaskDisplay() {
    if (sensor_ready = false) { return; }
    // ... Display schreiben ...
    sensor_ready  := false;
    display_dirty := false;
}

// Task 3: Watchdog zurücksetzen
@volatile
fn TaskWatchdog(wdt_base: int64) {
    var feed: ^uint32 := (wdt_base + 0x04) as ^uint32;
    feed^ := 0xFEED0000u32;   // WDT-Feed-Sequenz
}

fn main(): int64 {
    con WDT_BASE: int64 := 0x6001_5000;
    var now: int64 := 0;

    while (true) {
        now := now + 1;          // Vereinfacht — in Praxis: GetTick()
        TaskSensor(now);
        TaskDisplay();
        TaskWatchdog(WDT_BASE);
    }
    return 0;
}

Tick-Zähler aus dem Hardware-Timer

Für genaues Timing liest der Superloop einen Hardware-Tick-Zähler:

@volatile
fn GetSysTickMs(): int64 {
    // ARM Cortex-M: SysTick Current Value Register
    con SYST_CVR: ^uint32 := 0xE000E018 as ^uint32;
    return SYST_CVR^ as int64;
}


3. ISR — Interrupt Service Routinen

Eine ISR ist eine Funktion, die der Hardware-Interrupt-Controller aufruft, wenn ein Ereignis eintritt (Timer-Tick, UART-Byte empfangen, GPIO-Flanke). Sie läuft außerhalb des normalen Kontrollflusses — asynchron zur Hauptschleife.

ISR in Lyx deklarieren

ISRs werden mit @section in den Interrupt-Vektor platziert. Der Name muss mit dem Linker-Script übereinstimmen:

// ARM Cortex-M: SysTick-Handler
@section(".isr_vector")
@no_opt
fn SysTick_Handler(): void {
    // Tick-Zähler erhöhen — atomar durch @no_opt und Single-Writer-Konvention
    tick_counter := tick_counter + 1;
}

// ARM Cortex-M: USART2-Empfangs-IRQ
@section(".isr_vector")
@no_opt
fn USART2_IRQHandler(): void {
    con USART2_RDR: ^uint8 := 0x4000_4424 as ^uint8;
    var byte: uint8 := USART2_RDR^;
    RingBufferPush(byte);   // Lock-freier Push (ISR schreibt, Main liest)
}

Regeln für ISRs

Regel Begründung
Kein MutexLock / pthread Mutex kann blockieren — ISR darf nicht blockieren
Kein dynamischer Speicher (alloc) Heap-Allokation ist nicht re-entrant ohne OS-Schutz
Kein PrintStr / I/O I/O-Funktionen können intern sperren oder langsam sein
Alle geteilten Variablen als @volatile Compiler darf Zugriffe sonst wegoptimieren
Minimale Logik ISR soll schnell enden — aufwändige Arbeit in Hauptschleife auslagern
@no_opt auf die ISR-Funktion Verhindert, dass Compiler Register-State wegoptimiert

@volatile auf gemeinsame Variablen

Variablen, die sowohl im Hauptprogramm als auch in einer ISR benutzt werden, müssen @volatile sein — sonst darf der Compiler Caching-Optimierungen anwenden, die den ISR-Schreibzugriff unsichtbar machen:

@volatile var tick_counter: int64 := 0;
@volatile var uart_rx_byte: uint8  := 0;
@volatile var uart_rx_ready: bool  := false;


4. ISR ↔ Hauptschleife: Lock-freier Ring Buffer

Der häufigste Weg, Daten von einer ISR an die Hauptschleife zu übergeben, ist ein SPSC-Ring-Buffer (Single Producer, Single Consumer). Die ISR schreibt (Producer), die Hauptschleife liest (Consumer). Weil es genau einen Schreiber und einen Leser gibt, sind keine Mutexe nötig — nur korrekte Speicherreihenfolge.

unit spsc_ring;

// Ring Buffer: ISR schreibt, Hauptschleife liest
con RING_SIZE: int64 := 256;

@volatile var ring_buf:  [256]uint8 := [];
@volatile var ring_head: int64      := 0;   // Schreibzeiger (ISR)
@volatile var ring_tail: int64      := 0;   // Lesezeiger (Hauptschleife)

// Aufruf NUR aus ISR — kein Mutex nötig (Single Writer)
fn RingBufferPush(byte: uint8): bool {
    var next: int64 := (ring_head + 1) mod RING_SIZE;
    if (next = ring_tail) { return false; }   // Voll — Byte verwerfen
    ring_buf[ring_head] := byte;
    ring_head := next;
    return true;
}

// Aufruf NUR aus Hauptschleife — kein Mutex nötig (Single Reader)
fn RingBufferPop(): (uint8, bool) {
    if (ring_tail = ring_head) { return (0u8, false); }   // Leer
    var byte: uint8 := ring_buf[ring_tail];
    ring_tail := (ring_tail + 1) mod RING_SIZE;
    return (byte, true);
}

// Hauptschleife: verarbeitet empfangene UART-Bytes
fn ProcessIncoming() {
    var (byte, ok): (uint8, bool) := RingBufferPop();
    while (ok) limit(RING_SIZE) {
        // ... Byte verarbeiten ...
        var (b2, ok2) := RingBufferPop();
        byte := b2;
        ok   := ok2;
    }
}

Invariante: ring_head wird nur von der ISR geschrieben, ring_tail nur von der Hauptschleife. Solange diese Konvention eingehalten wird, ist kein Lock nötig. Würden mehrere ISRs schreiben oder mehrere Consumer lesen, wäre ein kritischer Abschnitt notwendig.

5. Kritische Abschnitte auf Bare-Metal

Wenn eine Race Condition zwischen ISR und Hauptschleife unvermeidbar ist (z.B. mehrteilige Struktur atomar lesen), müssen Interrupts kurzzeitig gesperrt werden. Das ist das Bare-Metal-Äquivalent zum Mutex.

ARM Cortex-M: PRIMASK

Auf ARM Cortex-M sperrt die CPSID i-Instruktion alle maskierbaren Interrupts. Da Lyx keinen eingebauten Assembler-Aufruf für Special-Register bietet, werden kurze Assembler-Stubs als @extern eingebunden:

// Assembler-Stubs (z.B. irq_stubs.s):
//   DisableIRQ:  CPSID i / BX lr
//   EnableIRQ:   CPSIE i / BX lr
//   GetIPSR:     MRS r0, IPSR / BX lr   (0 = kein IRQ aktiv)

@extern fn DisableIRQ(): void;
@extern fn EnableIRQ():  void;
@extern fn GetIPSR(): int64;    // > 0 wenn in ISR

// Atomares Lesen einer mehrteiligen Struktur
fn ReadSensorAtomic(out_val: ^int64, out_ts: ^int64) {
    DisableIRQ();
        out_val^ := sensor_value;
        out_ts^  := sensor_timestamp;
    EnableIRQ();
}

Achtung: Interrupts so kurz wie möglich sperren. Lange kritische Abschnitte erhöhen die Interrupt-Latenz und verletzen WCET-Garantien.

RISC-V: mstatus.MIE

Auf RISC-V (Machine Mode) wird das MIE-Bit im mstatus-Register gelöscht, um Interrupts zu sperren:

// Assembler-Stubs:
//   DisableIRQ:  CSRC mstatus, 8  / RET    (MIE-Bit löschen)
//   EnableIRQ:   CSRS mstatus, 8  / RET    (MIE-Bit setzen)

@extern fn DisableIRQ(): void;
@extern fn EnableIRQ():  void;


6. ARM Cortex-M: SysTick & NVIC

Der SysTick-Timer erzeugt den periodischen System-Tick (typisch 1 ms) — die Zeitbasis für kooperative Scheduler und Timeouts.

SysTick initialisieren

unit arm_cortexm;

// SysTick-Register (ARM Cortex-M, adressiert ab 0xE000E010)
con SYST_CSR: ^uint32  := 0xE000E010 as ^uint32;   // Control & Status
con SYST_RVR: ^uint32  := 0xE000E014 as ^uint32;   // Reload Value
con SYST_CVR: ^uint32  := 0xE000E018 as ^uint32;   // Current Value

// NVIC: Interrupt Set Enable Register (für IRQ 0..31)
con NVIC_ISER0: ^uint32 := 0xE000E100 as ^uint32;

@volatile var sys_tick_ms: int64 := 0;

// SysTick auf 1 ms konfigurieren (72 MHz Takt als Beispiel)
fn SysTickInit(cpu_hz: int64) {
    var reload: uint32 := (cpu_hz / 1000 - 1) as uint32;
    SYST_RVR^ := reload;
    SYST_CVR^ := 0u32;         // Zähler zurücksetzen
    SYST_CSR^ := 0x07u32;      // Enable | TickInt | AHB-Takt
}

// SysTick-Handler (im Interrupt-Vektor)
@section(".isr_vector")
@no_opt
fn SysTick_Handler(): void {
    sys_tick_ms := sys_tick_ms + 1;
}

// Aktuellen Tick-Wert lesen (Hauptschleife)
fn GetTickMs(): int64 {
    return sys_tick_ms;
}

// Busy-Wait für N Millisekunden
fn DelayMs(n: int64) {
    var start: int64 := GetTickMs();
    while (GetTickMs() - start < n) limit(100000) { }
}

NVIC-Priorität setzen

// NVIC Interrupt Priority Register (IPR) — 8-Bit-Priorität pro IRQ
// IPR[n] liegt bei 0xE000E400 + n
fn NvicSetPriority(irq: int64, prio: uint8) {
    var ipr: ^uint8 := (0xE000E400 + irq) as ^uint8;
    ipr^ := prio;
}

// IRQ 37 (USART2 auf STM32F4) aktivieren und Priorität setzen
fn EnableUSART2IRQ() {
    NvicSetPriority(37, 0xA0u8);       // Priorität 10 (0xA0 bei 4-Bit-Prio)
    NVIC_ISER0^ := NVIC_ISER0^ | (1u32 << 5u32);  // ISER[37/32] Bit 5
}


7. RISC-V: CLINT Timer-Interrupt

Der CLINT (Core Local Interruptor) auf RISC-V-Mikrocontrollern erzeugt den Machine-Timer-Interrupt (MTI), wenn mtime >= mtimecmp. Er ist das RISC-V-Äquivalent zum SysTick.

unit riscv_timer;

// CLINT-Register-Basis (SiFive E-Series, GD32VF103)
con CLINT_BASE:    int64  := 0x0200_0000;
con MTIME:         ^int64 := (CLINT_BASE + 0xBFF8) as ^int64;
con MTIMECMP:      ^int64 := (CLINT_BASE + 0x4000) as ^int64;

con TICKS_PER_MS:  int64  := 32768;    // 32.768 kHz CLINT-Takt (typisch)

@volatile var systick_ms: int64 := 0;

// Timer-Interrupt initialisieren: alle 1 ms auslösen
fn TimerInit() {
    MTIMECMP^ := MTIME^ + TICKS_PER_MS;
}

// Machine-Timer-Interrupt-Handler
// (Eintrag im Machine-Trap-Vector, je nach Linker-Script)
@section(".mtvec")
@no_opt
fn MachineTimerHandler(): void {
    systick_ms := systick_ms + 1;
    // Nächsten Interrupt planen
    MTIMECMP^ := MTIMECMP^ + TICKS_PER_MS;
}

fn GetTickMs(): int64 {
    return systick_ms;
}


8. ESP32 (Xtensa): FreeRTOS via FFI

Der ESP32 läuft mit FreeRTOS und bietet zwei Xtensa LX6-Kerne. Lyx bindet FreeRTOS über @extern-Deklarationen ein.

FreeRTOS-Typen und Tasks

unit esp32_rtos;
import std.io;

// FreeRTOS-Handle-Typen (intern: Pointer auf OS-Strukturen)
type TaskHandle  = int64;
type QueueHandle = int64;
type SemHandle   = int64;

// FreeRTOS Task-API
@extern fn xTaskCreate(
    pvTaskCode:    int64,    // Funktionszeiger (als int64)
    pcName:        pchar,
    usStackDepth:  int64,    // Stack-Größe in Words
    pvParameters:  int64,    // Argument (beliebiger Pointer)
    uxPriority:    int64,    // Priorität (0 = niedrigst)
    pxCreatedTask: int64     // Rückgabe-Handle (^TaskHandle oder 0)
): int64;                    // pdPASS = 1 bei Erfolg

@extern fn vTaskDelay(xTicksToDelay: int64): void;
@extern fn vTaskStartScheduler(): void;
@extern fn vTaskDelete(task: TaskHandle): void;

// FreeRTOS Queue-API
@extern fn xQueueCreate(uxQueueLength: int64, uxItemSize: int64): QueueHandle;
@extern fn xQueueSend(queue: QueueHandle, pvItemToQueue: int64, xTicksToWait: int64): int64;
@extern fn xQueueReceive(queue: QueueHandle, pvBuffer: int64, xTicksToWait: int64): int64;

con pdPASS:      int64 := 1;
con portMAX_DELAY: int64 := 0xFFFF_FFFF;

Zwei Tasks + Queue

var g_queue: QueueHandle := 0;

// Producer-Task (Kern 0)
fn ProducerTask(arg: int64): void {
    var value: int64 := 0;
    while (true) {
        value := value + 1;
        xQueueSend(g_queue, ^value as int64, portMAX_DELAY);
        vTaskDelay(100);   // 100 Ticks ≈ 100 ms warten
    }
}

// Consumer-Task (Kern 1)
fn ConsumerTask(arg: int64): void {
    var received: int64 := 0;
    while (true) {
        var ok: int64 := xQueueReceive(g_queue, ^received as int64, portMAX_DELAY);
        if (ok = pdPASS) {
            PrintStr("Empfangen: ");
            PrintInt(received);
            PrintStr("\n");
        }
    }
}

fn main(): int64 {
    g_queue := xQueueCreate(16, 8);   // 16 Elemente à 8 Byte

    xTaskCreate(ProducerTask as int64, "producer", 2048, 0, 5, 0);
    xTaskCreate(ConsumerTask as int64, "consumer", 2048, 0, 5, 0);

    vTaskStartScheduler();   // gibt nie zurück
    return 0;
}

ESP32 Dual-Core: Pinning

Auf dem ESP32 können Tasks an einen bestimmten Kern gepinnt werden:

// xTaskCreatePinnedToCore: wie xTaskCreate, letztes Argument ist Core-ID (0 oder 1)
@extern fn xTaskCreatePinnedToCore(
    pvTaskCode:    int64,
    pcName:        pchar,
    usStackDepth:  int64,
    pvParameters:  int64,
    uxPriority:    int64,
    pxCreatedTask: int64,
    xCoreID:       int64    // 0 = PRO_CPU, 1 = APP_CPU
): int64;

fn main(): int64 {
    g_queue := xQueueCreate(32, 8);

    xTaskCreatePinnedToCore(ProducerTask as int64, "prod", 2048, 0, 5, 0, 0);
    xTaskCreatePinnedToCore(ConsumerTask as int64, "cons", 2048, 0, 5, 0, 1);

    vTaskStartScheduler();
    return 0;
}


9. Allgemeines RTOS-Muster: Semaphor

Binäre Semaphore sind das RTOS-Äquivalent zur Bedingungsvariable: Eine ISR signalisiert, der Task wartet.

// FreeRTOS Binary Semaphore
@extern fn xSemaphoreCreateBinary(): SemHandle;
@extern fn xSemaphoreGive(sem: SemHandle): int64;
@extern fn xSemaphoreTake(sem: SemHandle, timeout: int64): int64;

// Aus ISR heraus signalisieren (spezielle ISR-Variante!)
@extern fn xSemaphoreGiveFromISR(sem: SemHandle, pxHigherPriorityTaskWoken: int64): int64;
@extern fn portYIELD_FROM_ISR(xHigherPriorityTaskWoken: int64): void;

var g_sem: SemHandle := 0;

// UART-ISR signalisiert Semaphor
@section(".isr_vector")
@no_opt
fn UART_RxISR(): void {
    var woken: int64 := 0;
    xSemaphoreGiveFromISR(g_sem, ^woken as int64);
    portYIELD_FROM_ISR(woken);
}

// Verarbeitungs-Task wartet auf Semaphor
fn UartProcessTask(arg: int64): void {
    while (true) {
        xSemaphoreTake(g_sem, portMAX_DELAY);
        // ... UART-Daten auslesen und verarbeiten ...
    }
}


10. Safety & DO-178C auf Embedded-Targets

Stack-Limit pro Task

Jede ISR und jede RTOS-Task hat ihren eigenen Stack. @stack_limit begrenzt den Stack-Verbrauch zur Compile-Zeit:

@stack_limit(512)
@section(".isr_vector")
@no_opt
fn SysTick_Handler(): void {
    sys_tick_ms := sys_tick_ms + 1;
}

@stack_limit(2048)
fn ControlTask(arg: int64): void {
    // ... max. 2048 Byte Stack ...
}

WCET-Annotation für ISRs

@wcet(10)           // Maximale Ausführungszeit: 10 µs
@stack_limit(128)
@section(".isr_vector")
@no_opt
fn ADC_IRQHandler(): void {
    // Liest einen Messwert und legt ihn in den Ring Buffer
    RingBufferPush(AdcRead());
}

DAL-Tabelle für Embedded-Nebenläufigkeit

Konstrukt DAL-D (Minor) DAL-C (Major) DAL-B (Gefährlich) DAL-A (Katastrophal)
Superloop Timing-Nachweis Formaler WCET-Beweis
ISR ohne @wcet Dokumentation
ISR mit @wcet + MC/DC + Lockstep
Kritischer Abschnitt + Max-Dauer dok. + Latenz-Nachweis + Formal verifiziert
FreeRTOS-Task + Stack-Nachweis @stack_limit zwingend RTOS selbst DAL-A
Shared Memory ISR↔Main SPSC-Ring SPSC + @volatile + Timing-Invarianten + TMR auf Buffer

Compile-Flags für Embedded-Safety

// DO-178C DAL-B Build für ARM Cortex-M:
// lyxc main.lyx \
//   --target=arm_cm \
//   --stack-check \
//   --wcet \
//   --call-graph \
//   --mcdc-instrument \
//   --coverage-report \
//   -O2

→ Vollständige Flag-Referenz: CLI-Referenz


11. Checkliste: Embedded-Nebenläufigkeit


Zusammenfassung

Szenario Empfohlenes Muster
Linux / macOS, mehrere Threads std.threadThreads & Nebenläufigkeit
Bare-Metal, eine Aufgabe Superloop mit Hardware-Tick
Bare-Metal, ISR → Hauptschleife SPSC-Ring-Buffer + @volatile
Bare-Metal, kritische Sektion Interrupts sperren (DisableIRQ/EnableIRQ als @extern-Stubs)
ARM Cortex-M, Zeitbasis SysTick-ISR + sys_tick_ms
RISC-V, Zeitbasis CLINT MTIMECMP + Machine-Timer-Handler
ESP32, zwei Tasks FreeRTOS via FFI + xTaskCreatePinnedToCore
ESP32, ISR → Task FreeRTOS Binary Semaphor + xSemaphoreGiveFromISR
Safety-kritisch (DAL-B/A) @stack_limit + @wcet + @section + SPSC

Letzte Aktualisierung: 2026-05-22