====== 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