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.threadsetzt 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_optgibt es nicht — der Compiler weist beide mitunbekanntes Attributab. 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_headwird nur von der ISR geschrieben,ring_tailnur 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 inebnf.mdunterAsmStmt,AsmOperandListundAsmOperand.
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:
@volatileauf alle Variablen, die ISR und Hauptschleife teilen- Interrupt-Vektor über Linker-Skript oder Assembler-Stub verdrahtet (
@sectiongibt es nicht) - SPSC-Ring-Buffer: sicherstellen, dass nur ein Schreiber und ein Leser existieren
- Kein
malloc/std.allocin ISRs - Kritische Abschnitte so kurz wie möglich halten
@stack_limitfür alle ISRs und RTOS-Tasks gesetzt@wcetfü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 — 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).
