// Dokumentation Übung 1 RTO // Kompilieren: typst compile --root .. u01_dokumentation.typ (im Ordner doc/; --root für den Quellcode aus ../src) #let todo(body) = box( fill: rgb("#ffe0e0"), inset: (x: 3pt, y: 2pt), radius: 2pt, )[#text(fill: rgb("#b00000"))[*TODO:* #body]] #let fehlt(name) = rect( width: 100%, height: 4.5cm, stroke: (paint: rgb("#b00000"), thickness: 0.8pt, dash: "dashed"), )[#align(center + horizon)[#text(fill: rgb("#b00000"))[Screenshot fehlt: #raw(name)]]] #set document(title: "Übung 1 RTO – Dokumentation") #set page(paper: "a4", margin: 2.2cm, numbering: "1") #set text(lang: "de", size: 11pt) #set par(justify: true) #set heading(numbering: "1.1") #show raw.where(block: true): set text(size: 8.5pt) #show raw.where(block: true): block.with(fill: luma(245), inset: 8pt, radius: 3pt, width: 100%) #show figure.where(kind: table): set figure.caption(position: top) #set table(stroke: 0.5pt + luma(160), inset: 5pt, fill: (x, y) => if y == 0 { luma(230) }) #align(center)[ #text(size: 20pt, weight: "bold")[Übung 1 – RTO] #v(2pt) #text(size: 13pt)[Vorläufer von Betriebssystemen, kooperative Task-Weitergabe] #v(10pt) Markus Neumann s2610567004, Christian Gamsjäger s2610567014 \ #datetime.today().display("[day].[month].[year]") ] #v(1em) #outline() #pagebreak(weak: true) = Überblick *Programmstruktur:* Kooperative Superloop in `main.c`. Jeder Task ist eine Funktion, die pro Aufruf nur einen kurzen Arbeitsschritt erledigt und sofort zurückkehrt; der Zustand zwischen zwei Aufrufen liegt in `static`-Variablen. Gemeinsame Zeitbasis ist der SysTick-Interrupt mit 1 ms Periode. ```c while (1) { Debug_Set(counter); TaskCounter(); Debug_Unset(counter); Debug_Set(key); TaskKey(); Debug_Unset(key); Debug_Set(led); TaskLed(); Debug_Unset(led); Debug_Set(watch); TaskWatch(); Debug_Unset(watch); Debug_Set(poti); TaskPoti(); Debug_Unset(poti); Debug_Set(mandelbrot); TaskMandelbrot(); Debug_Unset(mandelbrot); } ``` #pagebreak(weak: true) = Aufgabe A – Debug-Unit zur Timing-Messung == Implementierung Jedem Task und dem SysTick-Interrupt ist ein Ausgang von Port B zugeordnet (@tab-pins). Beim Start des Tasks wird die Leitung auf High, beim Ende auf Low gesetzt; die Pulsbreite am Logic-Analyzer entspricht damit der Laufzeit. #figure( table( columns: 3, table.header([*Pin*], [*Signal*], [*Enum-Wert in* `Debug.h`]), [PB0], [SystemTick], [`systick`], [PB1], [TaskCounter], [`counter`], [PB3], [TaskKey], [`key`], [PB12], [TaskLed], [`led`], [PB13], [TaskWatch], [`watch`], [PB14], [TaskPoti], [`poti`], [PB15], [TaskMandelbrot], [`mandelbrot`], ), caption: [Zuordnung der Debug-Pins], ) `Debug_Init()` aktiviert den Takt für GPIOB und konfiguriert die sieben Pins als Push-Pull-Ausgänge. Die Tasks werden in `main.c` von außen eingerahmt, sodass der Task-Code selbst frei von Messcode bleibt. Nur der SysTick-Handler setzt seinen Pin intern: ```c void SysTick_Handler (void){ Debug_Set(systick); msTicks++; Debug_Unset(systick); } ``` == Messergebnisse im Ausgangszustand Die folgenden Messungen zeigen den Ausgangszustand der Vorlage: Die Tasks geben nur ihre Beschriftung aus, Mandelbrot zeichnet das komplette Bild in einem Aufruf. #figure( table( columns: (auto, auto, 1fr), align: (left, right, left), table.header([*Signal*], [*Laufzeit*], [*Bemerkung*]), [SysTick], [0,956 µs], [alle 1 ms], [Counter], [2,864 ms], [„Cnt “ + Zählerwert], [Key], [1,887 ms], [„Key “ (4 Zeichen)], [Led], [1,889 ms], [„Led “ (4 Zeichen)], [Watch], [2,832 ms], [„Watch “ (6 Zeichen)], [Poti], [2,362 ms], [„Poti “ (5 Zeichen)], [Mandelbrot], [16,164 s], [komplettes Bild, blockierend], ), caption: [Gemessene Laufzeiten im Ausgangszustand], ) #grid( columns: 1, gutter: 10pt, [#figure(image("img/counter.png", width: 100%), caption: [TaskCounter: 2,864 ms]) ], [#figure(image("img/key.png", width: 100%), caption: [TaskKey: 1,887 ms]) ], [#figure(image("img/led.png", width: 100%), caption: [TaskLed: 1,889 ms]) ], [#figure(image("img/watch.png", width: 100%), caption: [TaskWatch: 2,832 ms]) ], [#figure(image("img/poti.png", width: 100%), caption: [TaskPoti: 2,362 ms]) ], [#figure(image("img/mandelbrot.png", width: 100%), caption: [TaskMandelbrot: 16,164 s]) ], ) #figure(image("img/systick.png", width: 75%), caption: [SysTick-Interrupt: Pulsbreite 0,956 µs]) *Auswertung:* - Die Laufzeit der Tasks wird fast vollständig von der Textausgabe bestimmt. Aus Led (4 Zeichen, 1,889 ms), Poti (5 Zeichen, 2,362 ms) und Watch (6 Zeichen, 2,832 ms) ergibt sich übereinstimmend *≈ 0,472 ms pro Zeichen*. Ein Zeichen des 16×24-Fonts hat 384 Pixel, also ≈ 1,23 µs bzw. ≈ 59 Takte pro Pixel, da jedes Pixel per Software über GPIO-Zugriffe an das Display übertragen wird. - Mandelbrot blockiert die Superloop für 16,16 s. Damit beträgt die Zykluszeit aller Tasks ≈ 16,2 s; keine der Zeitanforderungen aus Aufgabe B ist so erfüllt. - Der SysTick-Puls ist 0,956 µs breit. Davon sind 0,748 µs Messoverhead (@sec-overhead), die eigentliche Arbeit (`msTicks++`) dauert also nur ≈ 0,21 µs ≈ 10 Takte. Hinzu kommen Ein- und Austritt in den Interrupt (Cortex-M0: 16 Takte Latenz), die am Pin nicht sichtbar sind. == Overhead der Messroutine *Methode:* In `main.c` wurden `Debug_Set(mandelbrot)` und `Debug_Unset(mandelbrot)` ohne Code dazwischen direkt hintereinander aufgerufen. Die gemessene Pulsbreite entspricht dem Overhead, den eine Messung zur Laufzeit eines Tasks hinzufügt (@fig-overhead). #figure(image("img/overhead.png", width: 75%), caption: [Overhead-Messung: Pulsbreite 748 ns]) $ t_"Overhead" = 748 "ns" quad arrow.r.double quad 748 "ns" dot 48 "MHz" approx 36 "Takte" $ *Bedeutung:* Jede gemessene Pulsbreite ist um ≈ 0,75 µs zu lang. Für Tasks im Millisekundenbereich ist das vernachlässigbar, für sehr kurze Tasks (z. B. Led mit 1,9 µs in Aufgabe B) dagegen nicht; dort gilt $t_"netto" approx t_"gemessen" - "0,75 µs"$. #pagebreak(weak: true) = Aufgabe B – Reaktionszeit in einer Superloop-Architektur == Zeitbasis Alle zeitabhängigen Tasks verwenden den Millisekundenzähler des SysTick. Dafür wurde `systick.c` um eine Abfragefunktion erweitert; `msTicks` ist `volatile`, da er in der ISR verändert wird: ```c uint32_t Tick_GetMs(void){ return msTicks; } ``` Ein 32-Bit-Lesezugriff ist auf dem Cortex-M0 atomar. Zeitvergleiche erfolgen immer als vorzeichenlose Differenz (`Tick_GetMs() - start >= dauer`); diese bleibt auch beim Überlauf des Zählers nach ≈ 49,7 Tagen korrekt. == Implementierung der Tasks / Counter: Erhöht den Zähler bei jedem Aufruf um 1 und gibt ihn aus. / Key: Liest die drei Tasten ein (+Flankenerkennung), USER-Taste ändert die Farbe (Schwarz → Blau → Grün → Schwarz) und gilt für alle danach gezeichneten Ausgaben. ```c static BOOL Key_GetDebounced (KeyType key, uint32_t now) { ... BOOL raw = Key_GetState(key); if (raw != lastRaw[key]) { lastRaw[key] = raw; lastChange[key] = now; } else if (now - lastChange[key] >= DEBOUNCE_MS) { stable[key] = raw; } return stable[key]; } ``` / Led: Lauflicht über Led3, Led4 und Led5 als Zustandsautomat. Bei jedem Aufruf wird nur geprüft, ob seit dem letzten Wechsel 100 ms vergangen sind. Einzelne Umschaltungen können sich um einen Schleifendurchlauf verspäten, die mittlere Periode bleibt exakt 100 ms. ```c else if (Tick_GetMs() - lastSwitch >= LED_PERIOD_MS) { Led_TurnOff(ledSeq[idx]); idx = (idx + 1u) % LED_COUNT; Led_TurnOn(ledSeq[idx]); lastSwitch += LED_PERIOD_MS; } ``` / Poti: Liest den Potentiometer-Wert über den ADC (in mV) und gibt ihn mit fester Breite (`%4d`) aus, damit beim Kürzerwerden des Werts keine alten Ziffern stehen bleiben. / Watch: Berechnet die Betriebszeit aus `Tick_GetMs()` und gibt sie im Format mm:ss aus. / Mandelbrot: Die Berechnung wurde zerlegt: Pro Aufruf werden Pixel berechnet, bis ein Zeitbudget von 5 ms verbraucht ist; die aktuelle Position (`curX`, `curY`) bleibt bis zum nächsten Aufruf erhalten. Nach dem letzten Pixel wird der Bildbereich zeilenweise (ebenfalls innerhalb des Zeitbudgets) gelöscht und das Bild neu gezeichnet. Dafür wurde der Display-Treiber um `Tft_ClearArea()` erweitert, das ein Rechteck in der Hintergrundfarbe füllt. ```c while (Tick_GetMs() - start < TIME_BUDGET_MS) { if (clearing) { /* delete one row, after the last one: clearing = FALSE */ } else { MandelBrotPixel(curX, curY); if (++curX >= ImageWidth) { curX = 0; if (++curY >= ImageHeight) { curY = 0; clearing = TRUE; } } } } ``` == Optimierung der Superloop Nach der Zerlegung des Mandelbrot-Tasks betrug die Zykluszeit noch 26–27 ms. Ursache war, dass in jedem Durchlauf alle Beschriftungen und Werte neu gezeichnet wurden. + *Beschriftungen nur einmal zeichnen:* „Cnt“, „Key“, „Led“, „Watch“, „Poti“ und „ManBr“ werden nur beim ersten Aufruf des jeweiligen Tasks ausgegeben. + *Werte nur bei Änderung zeichnen:* Key bei Änderung eines entprellten Tastenzustands, Watch beim Sekundenwechsel, Poti bei einer Änderung von mehr als 10 mV gegenüber dem zuletzt angezeigten Wert (unterdrückt das Rauschen des ADC). Der Counter ändert sich bei jedem Aufruf und wird daher immer gezeichnet. + *Kein Löschen des ganzen Bildschirms:* `Tft_ClearScreen()` wird nur noch einmal vor der Superloop aufgerufen. #figure( table( columns: (1fr, auto), align: (left, right), table.header([*Stand*], [*Zykluszeit*]), [Vorlage (Mandelbrot blockierend)], [≈ 16,2 s], [Mandelbrot zerlegt, alle Ausgaben in jedem Durchlauf], [26–27 ms], [Endstand (Maßnahmen 1–3)], [7,184 ms], [Endstand, Durchlauf mit Sekundenwechsel (Watch)], [≈ 9,6 ms], ), caption: [Zykluszeit der Superloop], ) == Messergebnisse im Endstand #figure(image("img/task_runtimes.png", width: 100%), caption: [Laufzeiten aller Tasks im Endstand]) #figure( table( columns: (auto, auto, 1fr), align: (left, right, left), table.header([*Task*], [*Laufzeit*], [*Bemerkung*]), [Counter], [1,949 ms], [Wert wird in jedem Durchlauf gezeichnet], [Key], [7,7 µs], [ohne Änderung; mit Neuzeichnen ≈ 2,4 ms (5 Zeichen, abgeschätzt)], [Led], [1,9 µs], [davon ≈ 0,75 µs Messoverhead], [Watch], [2,439 ms], [nur beim Sekundenwechsel, sonst wenige µs], [Poti], [18,0 µs], [ADC-Wandlung; mit Neuzeichnen ≈ 1,9 ms (4 Zeichen, abgeschätzt)], [Mandelbrot], [5,035 ms], [Zeitbudget 5 ms], ), caption: [Gemessene Laufzeiten im Endstand (@fig-b)], ) Die Zykluszeit der Superloop entspricht dem Abstand zweier steigender Flanken von PB1 (Counter) und beträgt 7,184 ms bzw. 139,2 Hz (@fig-cycle). In Durchläufen mit Sekundenwechsel kommt die Laufzeit von Watch hinzu, die Zykluszeit steigt dann auf ≈ 9,6 ms. #figure(image("img/cycle_runtime.png", width: 100%), caption: [Zykluszeit der Superloop: 7,184 ms zwischen zwei steigenden Flanken von PB1]) == Nachweis der Zeitanforderungen Der ungünstigste Durchlauf entsteht, wenn zum gemessenen Durchlauf (7,18 ms) hinzukommt, dass Key, Watch und Poti im selben Durchlauf neu zeichnen und Mandelbrot nach dem Löschen des Bildes zusätzlich die Beschriftung (6 Zeichen ≈ 2,8 ms) ausgibt: $ T_"Loop,max" approx ("7,18" + "2,4" + "2,44" + "1,9" + "2,83") "ms" approx "16,8 ms" $ Für die Reaktionszeit auf einen Tastendruck gilt: Der Druck wird spätestens nach einem Durchlauf erkannt, nach 20 ms Entprellzeit (+1 ms Auflösung des SysTick) beim nächsten Aufruf übernommen und im selben Aufruf angezeigt: $ t_"Reaktion,Key" <= T_"Loop,1" + 21 "ms" + T_"Loop,2" + t_"Anzeige" $ Da Watch nur einmal pro Sekunde und die Mandelbrot-Beschriftung nur einmal pro Bild neu gezeichnet werden, können zwei aufeinanderfolgende Durchläufe nicht beide den Maximalwert erreichen. Realistisch ungünstig ist $T_"Loop,1" approx "14,4 ms"$ (Watch, Poti, Beschriftung) und $T_"Loop,2" approx "9,1 ms"$ (Poti). #figure( table( columns: (auto, auto, 1fr, auto), align: (left, right, left, center), table.header([*Anforderung*], [*Grenze*], [*Ergebnis*], [*erfüllt*]), [Counter Zykluszeit], [≤ 20 ms], [$T_"Loop"$: gemessen 7,18 ms, max. ≈ 16,8 ms], [✓], [Key Reaktionszeit], [≤ 50 ms], [typ. 7,2 + 21 + 7,2 + 2,4 ≈ 38 ms; ungünstig 14,4 + 21 + 9,1 + 2,4 ≈ 47 ms], [✓], [Poti Reaktionszeit], [≤ 50 ms], [Abtastung in jedem Durchlauf: ≤ $T_"Loop,max"$ ≈ 17 ms], [✓], [Watch Aktualisierung], [≤ 1 s], [jede Sekunde, Verzögerung ≤ $T_"Loop"$], [✓], [Led Periode], [100 ms], [Mittelwert exakt, Jitter ≤ $T_"Loop"$], [✓], ), caption: [Vergleich mit den Zeitanforderungen], ) #pagebreak(weak: true) = Aufgabe C – Process-Stack (PSP) und Main-Stack (MSP) == Implementierung Die Umschaltung erfolgt am Beginn von `main()`. Die Initialisierung und die Superloop liegen in einer eigenen Funktion, die erst nach der Umschaltung aufgerufen wird: ```c #define PS_SIZE 1024 static char processStack[PS_SIZE] __attribute__((aligned(8))); static void Superloop(void) __attribute__((noinline, noreturn)); int main(void) { __set_PSP((uint32_t)processStack + PS_SIZE); __set_CONTROL(2); __ISB(); Superloop(); } ``` - `__set_PSP()` lädt den Process Stack Pointer mit dem Ende von `processStack` (0x20000478). Die Stacks des Cortex-M0 wachsen zu kleineren Adressen, und der Stack-Pointer zeigt auf das zuletzt belegte Wort (full descending); der erste `PUSH` schreibt daher auf 0x20000474. Das Array ist auf 8 Byte ausgerichtet, damit der Stack die von der ARM-Aufrufkonvention (AAPCS) geforderte 8-Byte-Ausrichtung hat. - `__set_CONTROL(2)` setzt Bit 1 (SPSEL) im CONTROL-Register: Im Thread-Modus wird ab jetzt der PSP verwendet. Beim Cortex-M0 läuft der Thread-Modus immer privilegiert, nur SPSEL ist relevant. - `__ISB()` (Instruction Synchronization Barrier) stellt sicher, dass alle folgenden Befehle bereits mit dem neuen Stack-Pointer ausgeführt werden. - Im Handler-Modus (Interrupts) verwendet der Prozessor unabhängig von SPSEL immer den MSP. Der Compiler legt den Stackframe einer Funktion beim Eintritt an. In der ersten Version lagen Umschaltung und Superloop direkt in `main`: `main` reservierte beim Eintritt mit `SUB sp,sp,#8` einen Platz auf dem *MSP*, schaltete dann auf den PSP um und griff danach mit `[sp,#4]` auf diesen Platz zu, der nun über den PSP adressiert wurde. Das Ergebnis lag außerhalb von `processStack` im Ausgabepuffer von TaskCounter und wurde von diesem überschrieben. Im Debugger (@fig-psp-alt) ist das sichtbar: R4 enthält 0x30343432, also den Text „2440“ des Zählers, und wurde als Pin-Maske an `Debug_Set(led)` übergeben (ODR von GPIOB = 0x3432). In der korrigierten Version hat `main` keinen Stackframe mehr; der Stackframe von `Superloop` entsteht bereits auf dem PSP. `noinline` verhindert, dass der Compiler `Superloop` wieder in `main` einfügt. #figure(image("img/psp_1.PNG", width: 100%), caption: [Erste Version: Zugriff von `main` über den falschen Stack (R4 = 0x30343432, ODR = 0x3432)]) == Stack-Layout Der SRAM liegt bei 0x20000000–0x20003FFF (16 KB). Die Lage der Bereiche stammt aus dem Map-File des Linkers (`listing/Apus.map`): #figure( table( columns: (auto, auto, 1fr), align: (left, right, left), table.header([*Adressbereich*], [*Größe*], [*Inhalt*]), [0x20000000–0x20000077], [120 B], [globale/statische Variablen (u. a. `msTicks` bei 0x20000070), 4 B Füllbytes für die Ausrichtung], [0x20000078–0x20000477], [1024 B], [`processStack` → *PSP*, Startwert 0x20000478], [0x20000478–0x20000497], [32 B], [Ausgabepuffer von TaskCounter und TaskPoti, Füllbytes], [0x20000498–0x20000897], [1024 B], [`STACK` aus der Startup-Datei → *MSP*, Startwert 0x20000898], [0x20000898–0x20003FFF], [≈ 13,9 KB], [frei], ), caption: [Belegung des SRAM], ) Der Heap (512 B in der Startup-Datei) wird nicht verwendet und vom Linker entfernt. *Begründung der Aufteilung:* - *Trennung:* Der Stackbedarf der Interrupts muss nicht im Task-Stack eingeplant werden, und ein Überlauf des Task-Stacks beschädigt nicht den Stack der Interrupts. In einem späteren RTOS mit eigenem Stack pro Task muss die Interrupt-Reserve so nur einmal (im MSP) statt in jedem Task-Stack vorgehalten werden. - *MSP, 1 KB* (Vorgabe `Stack_Size EQU 0x400` der Startup-Datei): Vor der Umschaltung belegt der Startup-Code der C-Bibliothek 16 B (`PUSH {r2-r4,lr}` in `__scatterload`), deshalb steht der MSP in der Superloop auf 0x20000888. Danach verwendet ihn nur noch der SysTick-Handler: `PUSH {r4,lr}` (8 B), `Debug_Set` (8 B) und `GPIO_WriteBit`. Der Bedarf liegt deutlich unter 100 B. Da nur eine Interruptquelle ohne Verschachtelung aktiv ist, ist 1 KB großzügig und bietet Reserve für weitere Interrupts in den folgenden Übungen. - *PSP, 1 KB:* Der gesamte Anwendungscode läuft auf dem PSP, darunter `snprintf` aus der C-Bibliothek und die Software-Gleitkommaroutinen für `double` im Mandelbrot-Task. Zusätzlich werden pro Interrupt 32 B für den Exception-Frame auf dem PSP abgelegt. - *Risiko:* Der Cortex-M0 besitzt keine MPU. Ein Überlauf des PSP würde unbemerkt die darunterliegenden Variablen (u. a. `msTicks`) überschreiben, ein Überlauf des MSP die Ausgabepuffer. Abhilfe wären Stacks am Ende des RAM mit freiem Abstand oder ein Füllmuster, das zur Laufzeit geprüft wird. == Ablauf bei einem Interrupt Tritt der SysTick-Interrupt auf, während die Superloop im Thread-Modus mit PSP läuft, geschieht Folgendes: + *Stacking (Hardware):* Der Prozessor legt 8 Register (32 Byte) auf dem *aktuellen* Stack ab, also auf dem PSP (@tab-frame). Der Frame wird immer auf 8 Byte ausgerichtet; falls nötig, wird ein Füllwort eingefügt und dies in Bit 9 des gesicherten xPSR vermerkt. + *Moduswechsel:* Der Prozessor wechselt in den Handler-Modus und verwendet ab jetzt den *MSP*. Das IPSR enthält die Exception-Nummer (SysTick = 15). + *Rücksprungwert:* Das LR wird mit dem Wert 0xFFFFFFFD geladen. Dieser `EXC_RETURN`-Wert kodiert, wohin und mit welchem Stack zurückgekehrt wird (@tab-excret). + *Handler (Software):* Der Compiler sichert im Prolog nur die vom Handler verwendeten Callee-saved-Register und das LR auf dem *MSP*, hier `PUSH {r4,lr}`. + *Rückkehr:* `POP {r4,pc}` lädt 0xFFFFFFFD in den PC. Der Prozessor erkennt den `EXC_RETURN`-Wert, holt die 8 Register vom PSP zurück (Unstacking, PSP + 32) und setzt das unterbrochene Programm im Thread-Modus fort. ``` SysTick_Handler: PUSH {r4,lr} ; R4 und EXC_RETURN auf den MSP MOVS r4,#1 MOV r0,r4 BL Debug_Set LDR r0,=msTicks LDR r1,[r0,#0] ADDS r1,r1,#1 STR r1,[r0,#0] MOV r0,r4 BL Debug_Unset POP {r4,pc} ; PC = 0xFFFFFFFD -> Exception Return, Unstacking vom PSP ``` #grid( columns: 2, gutter: 14pt, [#figure( table( columns: 2, table.header([*Adresse*], [*Inhalt*]), [PSP + 0x00], [R0], [PSP + 0x04], [R1], [PSP + 0x08], [R2], [PSP + 0x0C], [R3], [PSP + 0x10], [R12], [PSP + 0x14], [LR (R14)], [PSP + 0x18], [Rücksprungadresse (PC)], [PSP + 0x1C], [xPSR], ), caption: [Exception-Frame auf dem PSP], ) ], [#figure( table( columns: 2, table.header([`EXC_RETURN`], [*Rückkehr in*]), [0xFFFFFFF1], [Handler-Modus, MSP], [0xFFFFFFF9], [Thread-Modus, MSP], [0xFFFFFFFD], [Thread-Modus, PSP], ), caption: [`EXC_RETURN`-Werte], ) ], ) Die Hardware sichert nur die Register, die laut ARM-Aufrufkonvention (AAPCS) eine aufgerufene Funktion verändern darf (R0–R3, R12, LR) sowie PC und xPSR. R4–R11 sichert der Handler bei Bedarf selbst, wie jede andere C-Funktion. Dadurch können Interrupt-Handler als normale C-Funktionen geschrieben werden. == Analyse im µVision-Debugger Für die Analyse wurden Breakpoints in `Debug_Set`, in `TaskCounter` und im `SysTick_Handler` gesetzt. Im Register-Fenster stehen unter „Banked“ MSP und PSP, R13 (SP) zeigt den gerade aktiven Stack-Pointer. Das Memory-Fenster zeigt den Stack byteweise in Dezimaldarstellung; Wörter liegen im Little-Endian-Format im Speicher. #figure(image("img/psp_debug.jpeg", width: 100%), caption: [Breakpoint in `Debug_Set`, aufgerufen aus `Superloop`: SP = PSP = 0x20000470]) *Aufruf aus der Superloop (@fig-dbg-superloop):* `Superloop` ruft `Debug_Set(counter)` vor TaskCounter auf (R0 = 2 = Pin PB1). Der Prozessor läuft im Thread-Modus (xPSR = 0x21000000: IPSR = 0, Thumb-Bit gesetzt), R13 (SP) ist gleich dem PSP = 0x20000470. Das ist der Startwert 0x20000478 minus 8 B Stackframe von `Superloop` (`SUB sp,#8`). In diesem Stackframe liegt bei 0x20000474 der Wert 0x00001000 (Bytes 000 016 000 000), die Pin-Maske von `led` (PB12), die `Superloop` mit `STR r1,[sp,#4]` dort ablegt. Das ist genau der Platz, der in der ersten Version (@fig-psp-alt) außerhalb des Stacks lag. Direkt oberhalb des Stacks beginnt bei 0x20000478 der Ausgabepuffer von TaskCounter mit dem Text „1434“ (049 052 051 052), wie in @tab-ram angegeben. Der MSP steht auf 0x20000888. #figure(image("img/psp_superloop.jpeg", width: 100%), caption: [Breakpoint in `TaskCounter` nach `PUSH {r4,lr}`: SP = PSP = 0x20000468]) *In einem Task (@fig-dbg-task):* TaskCounter hat mit `PUSH {r4,lr}` 8 B auf dem PSP abgelegt, der PSP ist auf 0x20000468 gesunken. Im Memory-Fenster liegen dort das gesicherte R4 = 0x00000002 und die Rücksprungadresse 0x0800086F (Bytes 111 008 000 008), die mit R14 übereinstimmt und hinter den Aufruf von TaskCounter in `Superloop` zeigt. Der MSP ist unverändert 0x20000888, der Anwendungscode verwendet nur den PSP. #figure(image("img/msp_irqhandler.jpeg", width: 100%), caption: [Breakpoint im `SysTick_Handler` nach `PUSH {r4,lr}`: SP = MSP = 0x20000880, LR = 0xFFFFFFFD]) *Im SysTick-Handler (@fig-dbg-isr):* Der Breakpoint liegt auf `Debug_Set(systick)`, also nach dem Prolog `PUSH {r4,lr}`. Der Prozessor ist im Handler-Modus (xPSR = 0x0100000F: IPSR = 15 = SysTick), R13 (SP) ist jetzt gleich dem MSP, und LR enthält 0xFFFFFFFD (Rückkehr in den Thread-Modus mit PSP, @tab-excret). - *MSP:* Er ist nur um die 8 B des `PUSH` auf 0x20000880 gesunken. Im Memory-Fenster liegen dort das R4 des unterbrochenen Codes (0xFFFEE127) und der `EXC_RETURN`-Wert 0xFFFFFFFD (Bytes 253 255 255 255). Das Hardware-Stacking hat auf dem MSP nichts abgelegt. - *PSP:* Er steht auf 0x20000440. Die Hardware hat den Exception-Frame (32 B, @tab-frame) bei 0x20000440–0x2000045F auf dem Stack des unterbrochenen Codes abgelegt; dieser lief also mit PSP = 0x20000460 (bzw. 0x20000464, falls zur Ausrichtung ein Füllwort eingefügt wurde). #figure( { set par(justify: false) table( columns: (auto, 1fr, 1fr, 1fr), table.header([*Register*], [*Thread-Modus gemessen*], [*Handler erwartet*], [*Handler gemessen*]), [R13 (SP)], [0x20000470 (= PSP)], [\= MSP], [0x20000880 (= MSP)], [PSP], [0x20000470], [$p - "0x20"$ (Exception-Frame)], [0x20000440], [MSP], [0x20000888], [0x20000880 (nach `PUSH`)], [0x20000880], [LR], [0x0800086B (Rücksprung in `Superloop`)], [0xFFFFFFFD], [0xFFFFFFFD], [xPSR], [0x21000000 (IPSR = 0)], [IPSR = 15 (SysTick)], [0x0100000F (IPSR = 15)], ) }, caption: [Registerwerte im Thread-Modus (@fig-dbg-superloop) und im SysTick-Handler (@fig-dbg-isr); $p$: PSP des unterbrochenen Codes], ) == Funktion von PSP und MSP - *MSP:* Nach dem Reset aktiv; der Startwert wird aus dem ersten Eintrag der Vektortabelle geladen (`__initial_sp` = 0x20000898). Der Handler-Modus verwendet immer den MSP, also alle Interrupts und Exceptions und in einem RTOS auch der Kernel und der Scheduler. - *PSP:* Wird nur im Thread-Modus verwendet, wenn SPSEL = 1 ist, also für den Anwendungs- bzw. Task-Code. In einem RTOS erhält jeder Task einen eigenen Stack. Der Kontextwechsel besteht dann im Wesentlichen darin, den PSP auf den Stack des nächsten Tasks umzusetzen: Die Hardware legt beim Interrupt R0–R3, R12, LR, PC und xPSR auf dem Stack des unterbrochenen Tasks ab, der Scheduler sichert R4–R11 zusätzlich dort. Damit liegt der komplette Kontext eines Tasks in seinem eigenen Stack. - *Vorteil der Trennung:* Interrupts haben einen eigenen, zuverlässig verfügbaren Stack, dessen Bedarf nur einmal eingeplant werden muss, und Anwendungs- und Systemspeicher sind sauber getrennt. #pagebreak(weak: true) #set heading(numbering: "A.1") #counter(heading).update(0) = Quellcode #[ #show raw.where(block: true): set text(size: 7.5pt) #show raw.where(block: true): it => block(fill: luma(245), inset: 8pt, radius: 3pt, width: 100%, grid( columns: (1.8em, 1fr), column-gutter: 0.8em, row-gutter: 0.65em, ..it.lines.map(l => (align(right, text(fill: luma(140), str(l.number))), l.body)).flatten(), )) #for pfad in ( "main.c", "Debug.h", "Debug.c", "Tasks/TaskCounter.c", "Tasks/TaskKey.c", "Tasks/TaskLed.c", "Tasks/TaskWatch.c", "Tasks/TaskPoti.c", "Tasks/TaskMandelbrot.c", ) [ == #raw("src/" + pfad) #raw(read("../src/" + pfad).trim(at: end), lang: "c", block: true) ] ]