====== 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): [[lyx_-_programmiersprache:threads-nebenlaeufikeit|Threads & Nebenläufigkeit]]\\
→ Hardware-Pointer und @volatile: [[lyx_-_programmiersprache:pointer-inlining|Pointer & Inlining]]\\
→ Safety-Pragmas: [[lyx_-_programmiersprache:attributes-pragmas|Attribute & Pragmas]] · [[lyx_-_programmiersprache:do-178c|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: [[lyx_-_programmiersprache:compiler-parameter|CLI-Referenz]]
----
===== 11. Checkliste: Embedded-Nebenläufigkeit =====
* ''@volatile'' auf alle Variablen, die ISR und Hauptschleife teilen
* ''@no_opt'' auf jede ISR-Funktion
* ''@section(".isr_vector")'' korrekt gesetzt — mit Linker-Script abgeglichen
* 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.thread'' — [[lyx_-_programmiersprache:threads-nebenlaeufikeit|Threads & 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