====== 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:sprache:threads|Threads & Nebenläufigkeit]]\\
→ Hardware-Pointer und @volatile: [[lyx_-_programmiersprache:sprache:pointer-inlining|Low-Level: Speicherzugriff & Pragmas]]\\
→ Safety-Pragmas: [[lyx_-_programmiersprache:sprache:attributes-pragmas|Attribute & Pragmas]] · [[lyx_-_programmiersprache:guides: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 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 [[https://github.com/SEOLizer/LyX-Compiler/issues/1099|#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("<reg>", <Ausdruck>)'' | wird **vor** dem Block ausgewertet und ins Register gelegt |
| ''out("<reg>", <Variable>)'' | 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 ([[lyx_-_programmiersprache:sprache:ffi|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: [[lyx_-_programmiersprache:tools:compiler-parameter|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.thread'' — [[lyx_-_programmiersprache:sprache:threads|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 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).