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: Low-Level: Speicherzugriff & Pragmas
→ 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 gilt fuer VARIABLEN; der Registerzugriff selbst laeuft ueber
//  poke32 und wird nicht wegoptimiert.)
fn TaskWatchdog(wdt_base: int64) {
    var feed: int64 := (wdt_base + 0x04);
    poke32(feed, 0xFEED0000);   // 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: int64 := 0xE000E018;
    return peek32(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

 
Die Platzierung im Interrupt-Vektor ist derzeit nicht möglich. Die Attribute @section(„name“) und @no_opt gibt es nicht — der Compiler weist beide mit unbekanntes Attribut ab. Das ist seit #1099 (erledigt) gewolltes Verhalten: erfundene Attributnamen werden abgewiesen statt stillschweigend angenommen. Ein Issue für die beiden Attribute selbst gibt es bislang nicht. Ohne sie lässt sich eine Funktion keinem ELF-Abschnitt zuordnen, und der Interrupt-Vektor braucht genau das.
Die Handler selbst sind gewöhnliche Funktionen und übersetzen; die Verdrahtung mit dem Vektor muss bis auf Weiteres über ein externes Assembler-Stub oder das Linker-Skript erfolgen.

Der Rumpf eines Handlers ist gewöhnlicher Lyx-Code:

// ARM Cortex-M: SysTick-Handler
fn SysTick_Handler(): int64 {
    tick_counter := tick_counter + 1;
    return 0;
}

// ARM Cortex-M: USART2-Empfangs-IRQ
fn USART2_IRQHandler(): int64 {
    con USART2_RDR: int64 := 0x4000_4424;
    var byte: int64 := peek8(USART2_RDR);
    RingBufferPush(byte);   // Lock-freier Push (ISR schreibt, Main liest)
    return 0;
}

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 Print / 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
Geteilte Variablen als @volatile Einziges verfügbares Mittel gegen Wegoptimieren — @no_opt gibt es nicht

@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) % 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 (0, false); }   // Leer
    var byte: uint8 := ring_buf[ring_tail];
    ring_tail := (ring_tail + 1) % RING_SIZE;
    return (byte, true);
}

// Hauptschleife: verarbeitet empfangene UART-Bytes
fn ProcessIncoming() {
    var byte, ok := 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. Lyx kennt dafür Inline-Assembly: Ein asm-Block nimmt je Zeichenkette eine Instruktion auf. Der Vorrat ist architekturabhängig und wird über –target gewählt; ein fremdes Mnemonic ist ein harter Übersetzungsfehler, kein stilles NOP.

fn DisableIRQ(): int64 { asm { "cpsid i" } return 0; }
fn EnableIRQ():  int64 { asm { "cpsie i" } return 0; }

Übersetzt mit –target=arm-cm4. Für arm64 stehen unter anderem wfi, isb, dsb und dmb bereit, für RISC-V wfi, fence und mret.

Werte hinein- und herausreichen

Seit 1.0.20A kann ein asm-Block Operanden binden (#1324) — GCC-artig, aber mit ausgeschriebenem Registernamen statt Constraint-Buchstaben. Erst die Ausgaben, dann die Eingaben:

asm { "in eax, dx" : out("eax", wert) : in("dx", port) }

Form Bedeutung
in(„&lt;reg&gt;“, &lt;Ausdruck&gt;) wird vor dem Block ausgewertet und ins Register gelegt
out(„&lt;reg&gt;“, &lt;Variable&gt;) wird nach dem Block in die Variable geschrieben — lokal oder global

Erkannt werden rax, rcx, rdx, rbx, rsi und rdi samt ihren 32-, 16- und 8-Bit-Namen. Ein unbekannter Name ist ein Übersetzungsfehler, kein stiller Fehlgriff:

error: asm: unbekanntes Register in out(...)

Neu in der Mnemonic-Tabelle sind die Portbefehle in eax, dx, in al, dx, out dx, eax und out dx, al — damit ist der Zugriff auf den x86-Portadressraum möglich.

 
Nur auf dem x86-64-Weg (ELF, macOS, Windows). Auf den IR-Zielen weist ein Block mit Portbefehl ab — für LyxOS und bare metal bleibt der Legacy-Zugriff auf den PCI-Konfigurationsraum damit verschlossen.

Die Grammatik steht in ebnf.md unter AsmStmt, AsmOperandList und AsmOperand.

Alternativ lassen sich Assembler-Stubs als externe Funktionen einbinden:

// 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)

@cap(system.exit)
extern fn DisableIRQ(): void link "libfreertos.a";
@cap(system.exit)
extern fn EnableIRQ():  void link "libfreertos.a";
@cap(system.exit)
extern fn GetIPSR(): int64 link "libfreertos.a";    // > 0 wenn in ISR

// Atomares Lesen einer mehrteiligen Struktur
fn ReadSensorAtomic(out_val: int64, out_ts: int64) {
    DisableIRQ();
        poke64(out_val, sensor_value);
        poke64(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)

@cap(system.exit)
extern fn DisableIRQ(): void link "libfreertos.a";
@cap(system.exit)
extern fn EnableIRQ():  void link "libfreertos.a";


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: int64 := 0xE000E010;   // Control & Status
con SYST_RVR: int64 := 0xE000E014;   // Reload Value
con SYST_CVR: int64 := 0xE000E018;   // Current Value

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

@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;
    poke32(SYST_RVR, reload);
    poke32(SYST_CVR, 0);         // Zähler zurücksetzen
    poke32(SYST_CSR, 0x07);      // Enable | TickInt | AHB-Takt
}

// SysTick-Handler (im Interrupt-Vektor)
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: int64 := (0xE000E400 + irq);
    poke8(ipr, prio);
}

// IRQ 37 (USART2 auf STM32F4) aktivieren und Priorität setzen
fn EnableUSART2IRQ() {
    NvicSetPriority(37, 0xA0);       // Priorität 10 (0xA0 bei 4-Bit-Prio)
    poke32(NVIC_ISER0, peek32(NVIC_ISER0) | (1 << 5));  // 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);
con MTIMECMP:      int64 := (CLINT_BASE + 0x4000);

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() {
    poke64(MTIMECMP, peek64(MTIME) + TICKS_PER_MS);
}

// Machine-Timer-Interrupt-Handler
// (Eintrag im Machine-Trap-Vector, je nach Linker-Script)
fn MachineTimerHandler(): void {
    systick_ms := systick_ms + 1;
    // Nächsten Interrupt planen
    poke64(MTIMECMP, peek64(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 fn-Deklarationen ein (FFI) — mit link-Klausel, die die FreeRTOS-Bibliothek benennt.

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
@cap(system.exit)
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 (Adresse oder 0)
): int64 link "libfreertos.a";                    // pdPASS = 1 bei Erfolg

@cap(system.exit)
extern fn vTaskDelay(xTicksToDelay: int64): void link "libfreertos.a";
@cap(system.exit)
extern fn vTaskStartScheduler(): void link "libfreertos.a";
@cap(system.exit)
extern fn vTaskDelete(task: TaskHandle): void link "libfreertos.a";

// FreeRTOS Queue-API
@cap(system.exit)
extern fn xQueueCreate(uxQueueLength: int64, uxItemSize: int64): QueueHandle link "libfreertos.a";
@cap(system.exit)
extern fn xQueueSend(queue: QueueHandle, pvItemToQueue: int64, xTicksToWait: int64): int64 link "libfreertos.a";
@cap(system.exit)
extern fn xQueueReceive(queue: QueueHandle, pvBuffer: int64, xTicksToWait: int64): int64 link "libfreertos.a";

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

Zwei Tasks + Queue

import std.alloc;   // alloc()

var g_queue: QueueHandle := 0;

// Producer-Task (Kern 0)
fn ProducerTask(arg: int64): void {
    var zelle: int64 := alloc(8);      // Adresse einer Zelle statt Adresse einer Variablen
    var value: int64 := 0;
    while (true) {
        value := value + 1;
        poke64(zelle, value);
        xQueueSend(g_queue, zelle, portMAX_DELAY);
        vTaskDelay(100);   // 100 Ticks ≈ 100 ms warten
    }
}

// Consumer-Task (Kern 1)
fn ConsumerTask(arg: int64): void {
    var zelle: int64 := alloc(8);
    while (true) {
        var ok: int64 := xQueueReceive(g_queue, zelle, portMAX_DELAY);
        if (ok == pdPASS) {
            PrintLn(StrConcat("Empfangen: ", IntToStr(peek64(zelle))));
        }
    }
}

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)
@cap(system.exit)
extern fn xTaskCreatePinnedToCore(
    pvTaskCode:    int64,
    pcName:        pchar,
    usStackDepth:  int64,
    pvParameters:  int64,
    uxPriority:    int64,
    pxCreatedTask: int64,
    xCoreID:       int64    // 0 = PRO_CPU, 1 = APP_CPU
): int64 link "libfreertos.a";

var g_queue: QueueHandle := 0;   // wie im Block oben

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
@cap(system.exit)
extern fn xSemaphoreCreateBinary(): SemHandle link "libfreertos.a";
@cap(system.exit)
extern fn xSemaphoreGive(sem: SemHandle): int64 link "libfreertos.a";
@cap(system.exit)
extern fn xSemaphoreTake(sem: SemHandle, timeout: int64): int64 link "libfreertos.a";

// Aus ISR heraus signalisieren (spezielle ISR-Variante!)
@cap(system.exit)
extern fn xSemaphoreGiveFromISR(sem: SemHandle, pxHigherPriorityTaskWoken: int64): int64 link "libfreertos.a";
@cap(system.exit)
extern fn portYIELD_FROM_ISR(xHigherPriorityTaskWoken: int64): void link "libfreertos.a";

var g_sem: SemHandle := 0;

// UART-ISR signalisiert Semaphor
fn UART_RxISR(): void {
    var woken: int64 := alloc(8);
    poke64(woken, 0);
    xSemaphoreGiveFromISR(g_sem, woken);
    portYIELD_FROM_ISR(peek64(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

Safety-Anforderungen für Embedded-Targets nach DO-178C:

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)
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)
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 \
//   --call-graph \
//   --static-analysis \
//   --runtime-checks \
//   --lint \
//   --mcdc-report \
//   --target-energy=1 \
//   -o firmware.elf
//
// Nicht vorhanden: --stack-check, --wcet, --coverage-report, -O2.
// @stack_limit und @wcet werden ohne Flag bei jeder Uebersetzung geprueft;
// -O0..-O3 und --no-opt sind wirkungslos (#1525). Fuer die Codeerzeugung
// wirkt allein --target-energy.

→ Vollständige Flag-Referenz: CLI-Referenz


11. Checkliste: Embedded-Nebenläufigkeit

Checkliste für sicheren und korrekten Einsatz:

  • @volatile auf alle Variablen, die ISR und Hauptschleife teilen
  • Interrupt-Vektor über Linker-Skript oder Assembler-Stub verdrahtet (@section gibt es nicht)
  • SPSC-Ring-Buffer: sicherstellen, dass nur ein Schreiber und ein Leser existieren
  • Kein malloc / std.alloc in ISRs
  • Kritische Abschnitte so kurz wie möglich halten
  • @stack_limit für alle ISRs und RTOS-Tasks gesetzt
  • @wcet für zeitkritische ISRs gesetzt und vom Compiler verifiziert
  • FreeRTOS-Task-Stack-Größe großzügig bemessen (Überläufe sind schwer zu debuggen)
  • Watchdog-Reset in der Hauptschleife — nie vergessen

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 per asm-Block (cpsid i / cpsie i)
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 + SPSC

Letzte Aktualisierung: 2026-05-22

Codebeispiele geprüft: gegen lyxc 1.2.5C übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die pub fn-Signaturen in aurum/std gehalten, 0 Abweichungen).