diff --git a/README.md b/README.md index 9bf9f4c..26e4dfe 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,18 @@ Der geführte Modus ist ein didaktisches Angebot, keine Speicherung oder Benotun Im **freien Labor** erscheinen Registersteuerung, CNT-Zähler, Zeitdiagramm und Kennwerte zuerst. Danach folgen die vollständigen Vertiefungen für **RCC-Taktbaum** und **Prescaler**; ihre Rechen- und Eingabefunktionen bleiben unverändert. Die Sprungnavigation bedient die Bereiche ohne Routenwechsel (die Anwendung nutzt Hash-Routing) und bewegt den Tastaturfokus zum gewählten Abschnitt. Aus den Vertiefungen gibt es einen direkten Rückweg zu den Registern. Register-Eingaben referenzieren ihre Erklärtexte über `aria-describedby`; mobile Layouts, Textgrößen und Tasten sind auf **320 bis 1280 px** ausgerichtet. E2E prüft die Navigation, die unabhängige ARR/CCR2-Bearbeitung und die horizontale Breite in beiden Modi. Diese Änderungen betreffen Layout/Bedienbarkeit, **nicht** die Timer-Berechnungen. + +## Virtuelles DOT an PE11: Signalweg (Issue #5) + +Der Abschnitt „Virtuelles DOT · PE11“ besitzt ein **standardmäßig geschlossenes, per Tastatur zugängliches Ausklappfeld** mit einem dynamischen **Signalweg und einer Zustandstabelle**. Er trennt das **interne OC2REF-Signal**, die Ausgangspolarität `TIM_OCPOLARITY_LOW`, den **elektrischen PE11-Pegel** und die **low-aktive DOT-Anzeige**. + +- PWM Mode 1: `OC2REF = 1` für `CNT < CCR2` (gemeinsamer Berechnungspfad `pwmActive`). +- Bei `TIM1_CH2` mit `OCPolarity LOW` entspricht OC2REF=1 einem **LOW-Pegel an PE11**; eine low-aktive DOT-Leitung lässt das gewählte Segment dann leuchten. +- Die physikalische Darstellung gilt nur bei **TIM1** und unter Annahme aktivierter Ausgangsfreigaben **CC2E/MOE**, AF1-Konfiguration sowie einer aktivierten Anzeigenstelle über **CNTL1…CNTL4**. Diese Voraussetzungen sind in der Website nicht dynamisch modelliert. **TIM2_CH2 ist nicht mit DOT/PE11 verbunden.** +- Das Blockschaltbild ist eine **didaktische Funktionsdarstellung, kein elektrischer Stromlaufplan** und behauptet keine zusätzlichen physischen Inverter. Der tatsächliche Treiber-/Multiplex-Strompfad des 8-Segment-Adapterboards ist nicht rekonstruiert. +- **Primärquelle der Praktikumshardware:** *Praktikum Eingebettete Systeme – Praktikumsanleitung*, Technische Hochschule Mittelhessen, **Version 1.6 vom 13.10.2026**, Kap. **7.1.1**, Druckseiten **28–29/79** (low-aktive Segmentleitungen, CNTL1…CNTL4, Verweis auf das 8-Segment-Board-Schematic), sowie Kap. **7.6.3**, Druckseiten **51–52/79** (TIM1_CH2 → AF1 → PE11 → DOT). Die konkrete Kurs-PDF liegt in den Praktikumsressourcen, **nicht als öffentlich verlinkte Kopie** in diesem Repository. +- **Offizielle Hardwaredokumente:** [ST STM32F429-Datenblatt, Pin-/AF-Tabelle (PE11 / TIM1_CH2)](https://www.st.com/resource/en/datasheet/stm32f429zg.pdf), [Waveshare Open429Z-D – Ressourcen inkl. Basisschaltplan](https://www.waveshare.com/wiki/Open429Z-D) und [Waveshare 8 SEG LED Board – Ressourcen inkl. Adapterschaltplan](https://www.waveshare.com/wiki/8_SEG_LED_Board). Das vorliegende Open429Z-D-Basisschaltbild von 2014 belegt **nicht** den vollständigen Strompfad des Adapterboards; dieser wurde für die Demo nicht elektrisch rekonstruiert. + +Das vorhandene OC2REF-Zeitdiagramm und die Simulations-Engine bleiben unverändert; die Tabelle bietet die alternative synchrone Darstellung der drei Pegel-/LED-Ebenen und reagiert auf CNT, CCR2 und Timerwechsel. + +**TIM2-Darstellung:** Das geschlossene Ausklappfeld und beide LED-Widgets weisen ausdrücklich auf die fehlende Verbindung zu DOT/PE11 hin. Die TIM1-Pegeltabelle wird bei ausgewähltem TIM2 nicht angezeigt, damit keine hypothetischen PE11-HIGH-/LOW-Werte als aktueller TIM2-Pegel missverstanden werden. diff --git a/e2e/visual.spec.ts b/e2e/visual.spec.ts index 1073b63..37625af 100644 --- a/e2e/visual.spec.ts +++ b/e2e/visual.spec.ts @@ -478,3 +478,102 @@ test("Sprungziele bleiben bei Desktop und 320 px unterhalb des Sticky Headers si } } }); + +test("DOT-Signalweg: interne Referenz, PE11-Pegel und low-aktive LED bleiben synchron", async ({ page }, info) => { + await page.goto("/PES-Demos/#/timer-pwm"); + await page.getByRole("button", { name: "Freies Labor", exact: true }).click(); + const details = page.getByTestId("dot-signal-path"); + const summary = details.locator("summary"); + await expect(details).not.toHaveAttribute("open", ""); + await expect(summary).toContainText("Warum leuchtet DOT bei OC2REF = 1 (im TIM1-Modell)?"); + await summary.click(); + await expect(details).toHaveAttribute("open", ""); + await expect(details.getByTestId("path-oc2ref")).toContainText("OC2REF = 1"); + await expect(details.getByTestId("path-pe11")).toContainText("PE11: LOW (0)"); + await expect(details.getByTestId("path-dot")).toContainText("DOT: AN"); + await expect(details.locator(".dot-path-row-active")).toContainText("LOW (0)"); + await expect(details).toContainText("CC2E und MOE für TIM1"); + await expect(details).toContainText("kein verifizierter elektrischer Schaltplan"); + await details.scrollIntoViewIfNeeded(); + await capture(page, info.project.name, "dot-path-tim1-on"); + + const counterStep = page.locator(".transport-buttons") + .getByRole("button", { name: "+1 CNT-Zählschritt" }); + for (let i = 0; i < 3; i++) await counterStep.click(); + await expect(page.locator(".counter-number")).toHaveText("3"); + await expect(details).toHaveAttribute("open", ""); + await expect(details.getByTestId("path-oc2ref")).toContainText("OC2REF = 0"); + await expect(details.getByTestId("path-pe11")).toContainText("PE11: HIGH (1)"); + await expect(details.getByTestId("path-dot")).toContainText("DOT: AUS"); + await expect(details.locator(".dot-path-row-active")).toContainText("HIGH (1)"); + await details.scrollIntoViewIfNeeded(); + await capture(page, info.project.name, "dot-path-tim1-off"); + + await page.locator(".transport-buttons").getByRole("button", { name: /Zum Rücksprung/ }).click(); + await expect(page.locator(".counter-number")).toHaveText("0"); + await expect(details.getByTestId("path-dot")).toContainText("DOT: AN"); + + // Boundary cases: no active step at CCR2=0 and always active when CCR2>ARR. + await page.locator("#ccr").fill("0"); + await expect(details.getByTestId("path-dot")).toContainText("DOT: AUS"); + await expect(details.getByTestId("path-pe11")).toContainText("PE11: HIGH (1)"); + await page.locator("#ccr").fill("12"); + await expect(details.getByTestId("path-dot")).toContainText("DOT: AN"); + await expect(details.getByTestId("path-pe11")).toContainText("PE11: LOW (0)"); + + await page.locator("#timer").selectOption("TIM2"); + await expect(details.getByTestId("path-oc2ref")).toContainText("OC2REF = 1"); + await expect(details.getByTestId("path-pe11")).toContainText("PE11: nicht verbunden"); + await expect(details.getByTestId("path-dot")).toContainText("DOT: nicht verbunden"); + await expect(details.getByTestId("path-current-state")).toContainText("keine Verbindung"); + await expect(details.locator(".dot-path-table")).toHaveCount(0); + await expect(details.locator(".dot-path-row-active")).toHaveCount(0); + await expect(details.getByTestId("tim2-table-explanation")).toContainText("ausschließlich für das TIM1-Beispiel"); + await expect(page.locator(".led-visual").first().getByRole("img")).toHaveAttribute( + "aria-label", "DOT nicht verbunden: TIM2_CH2 hat keinen Signalpfad über PE11"); + await expect(page.locator(".led-visual").nth(1).getByRole("img")).toHaveAttribute( + "aria-label", "Kein Helligkeitsmittelwert: TIM2_CH2 ist nicht mit dem DOT an PE11 verbunden"); + await expect(page.locator(".led-visual").first()).toContainText("Nicht verbunden"); + await expect(page.locator(".led-visual").nth(1)).toContainText("Kein Mittelwert"); + + // Auch wenn das interne Referenzsignal auf 0 wechselt, bleibt der PE11-Pfad getrennt. + await page.locator("#ccr").fill("0"); + await expect(details.getByTestId("path-oc2ref")).toContainText("OC2REF = 0"); + await expect(details.getByTestId("path-pe11")).toContainText("PE11: nicht verbunden"); + await expect(details.getByTestId("path-dot")).toContainText("DOT: nicht verbunden"); + await expect(details.locator(".dot-path-table")).toHaveCount(0); + await expect(page.locator(".led-visual").first().getByRole("img")).toHaveAttribute( + "aria-label", "DOT nicht verbunden: TIM2_CH2 hat keinen Signalpfad über PE11"); + + // Zusammengeklappt ist bereits der erste Erklärungssatz timerabhängig. + await summary.focus(); + await page.keyboard.press("Enter"); + await expect(details).not.toHaveAttribute("open", ""); + await expect(summary).toContainText("Warum hat TIM2_CH2 keinen DOT-Pfad über PE11?"); + await details.scrollIntoViewIfNeeded(); + await capture(page, info.project.name, "dot-path-tim2-disconnected"); + + // A native details/summary is operable with a keyboard in both directions. + await page.keyboard.press("Enter"); + await expect(details).toHaveAttribute("open", ""); +}); + +test("DOT-Signalweg ist mobil aufklappbar ohne horizontale Überbreite", async ({ page }, info) => { + await page.goto("/PES-Demos/#/timer-pwm"); + await page.getByRole("button", { name: "Freies Labor", exact: true }).click(); + const details = page.getByTestId("dot-signal-path"); + await details.locator("summary").click(); + for (const width of [320, 360, 393]) { + await page.setViewportSize({ width, height: 800 }); + await expect(details.getByTestId("path-dot")).toBeVisible(); + const dimensions = await page.evaluate(() => ({ + documentOverflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, + stageOverflow: [...document.querySelectorAll(".dot-path-stage")] + .map(el => el.scrollWidth - el.clientWidth), + })); + expect(dimensions.documentOverflow, "Dokument bei " + width + "px").toBeLessThanOrEqual(2); + expect(Math.max(...dimensions.stageOverflow), "Signalweg-Knoten bei " + width + "px").toBeLessThanOrEqual(2); + await details.scrollIntoViewIfNeeded(); + await capture(page, info.project.name, "dot-path-mobile-" + width); + } +}); diff --git a/src/demos/timer-pwm/TimerPwmDemo.tsx b/src/demos/timer-pwm/TimerPwmDemo.tsx index a933496..015b934 100644 --- a/src/demos/timer-pwm/TimerPwmDemo.tsx +++ b/src/demos/timer-pwm/TimerPwmDemo.tsx @@ -3,6 +3,7 @@ import Waveform from "./components/Waveform"; import ClockTree from "./components/ClockTree"; import PrescalerPipeline from "./components/PrescalerPipeline"; import GuidedTimerLesson from "./components/GuidedTimerLesson"; +import DotSignalPath from "./components/DotSignalPath"; import { advanceTimerClocks, INITIAL_CLOCKED_COUNTER, type ClockedCounterState, } from "./model/prescaler"; @@ -455,28 +456,36 @@ export default function TimerPwmDemo() {
-
-
05 / HARDWAREBEZUG
+
+
+
05 / HARDWAREBEZUG

Virtuelles DOT · PE11

{pe11Connected ? "TIM1_CH2 → AF1 → PE11. Die LED ist low-aktiv. Wir setzen eine aktivierte Anzeigenstelle und freigeschalteten TIM1-Ausgang voraus (CC2E/MOE)." : "TIM2_CH2 liegt nicht auf PE11. Deshalb bleibt das virtuelle DOT-Segment unverbunden; oben wird trotzdem das interne PWM-Signal simuliert."}

-

Links: momentaner LED-Zustand gemäß CNT. Rechts: gemitteltes Helligkeitsmodell, nur bei ausreichend hoher PWM-Frequenz sinnvoll.

+

{pe11Connected + ? "Links: momentaner LED-Zustand gemäß CNT. Rechts: gemitteltes Helligkeitsmodell, nur bei ausreichend hoher PWM-Frequenz sinnvoll." + : "Beide LED-Anzeigen sind für TIM2 nicht verbunden; es wird kein PE11-Pegel und kein LED-Mittelwert simuliert."}

-
+
- Momentan + {pe11Connected ? "Momentan" : "Nicht verbunden"}
-
+
- Mittelwert + {pe11Connected ? "Mittelwert" : "Kein Mittelwert"}
+
+
diff --git a/src/demos/timer-pwm/components/DotSignalPath.tsx b/src/demos/timer-pwm/components/DotSignalPath.tsx new file mode 100644 index 0000000..0b63843 --- /dev/null +++ b/src/demos/timer-pwm/components/DotSignalPath.tsx @@ -0,0 +1,130 @@ +import type { TimerId } from "../model/timer"; + +interface DotSignalPathProps { + timer: TimerId; + cnt: number; + arr: number; + psc: number; + ccr: number; + /** Der Zustand stammt aus dem gemeinsamen pwmActive-Modell des Parents. */ + oc2refActive: boolean; +} + +export default function DotSignalPath({ + timer, cnt, arr, psc, ccr, oc2refActive, +}: DotSignalPathProps) { + const connected = timer === "TIM1"; + // Die Ausgangspolarität LOW invertiert den logischen OC2REF-Zustand. + // PE11 und DOT werden nur bei TIM1 unter den dokumentierten Annahmen modelliert. + const pe11Level = !connected ? "nicht verbunden" : oc2refActive ? "LOW (0)" : "HIGH (1)"; + const dotState = !connected ? "nicht verbunden" : oc2refActive ? "AN" : "AUS"; + + return ( +
+ + + Signalweg verstehen + {connected ? "Warum leuchtet DOT bei OC2REF = 1 (im TIM1-Modell)?" : "Warum hat TIM2_CH2 keinen DOT-Pfad über PE11?"} + + + +
+

+ Das Zeitdiagramm zeigt OC2REF – ein internes logisches Signal, + nicht den elektrischen Pegel an PE11. Im dargestellten PWM-Modus 1 gilt + OC2REF = 1 bei CNT < CCR2. +

+ +
+ Timer-Taktweg + {timer}CLK → PSC = {psc.toLocaleString("de-DE")} (÷{(psc + 1).toLocaleString("de-DE")}) → CNT = {cnt.toLocaleString("de-DE")} (0 … {arr.toLocaleString("de-DE")}) +
+ +
+
+ 1 · PWM-Vergleich + OC2REF = {oc2refActive ? "1" : "0"} + CNT = {cnt.toLocaleString("de-DE")} · CCR2 = {ccr.toLocaleString("de-DE")} + {oc2refActive ? "intern aktiv" : "intern inaktiv"} +
+ +
+ 2 · {connected ? "Ausgangsstufe" : "Keine PE11-Ausgabe"} + {connected ? "OCPolarity LOW" : "TIM2_CH2 intern"} + {connected ? "Invertiert den OC2REF-Pegel" : "Kein Pegel am PE11-DOT-Anschluss"} + {connected ? "CC2E und MOE für TIM1 als freigegeben angenommen" : "TIM2_CH2 ist nicht nach PE11 geroutet"} +
+ +
+ 3 · {connected ? "TIM1_CH2 / AF1" : "PE11-Anschluss"} + PE11: {pe11Level} + {connected ? "physikalischer Pinpegel im Modell" : "TIM2_CH2 liegt nicht auf PE11"} +
+ +
+ 4 · low-aktive Last + DOT: {dotState} + {connected ? "Segment leuchtet bei PE11 = LOW" : "kein DOT-Signalpfad über PE11"} +
+
+ +
+ {connected ? ( + <>Aktuell: OC2REF = {oc2refActive ? "1" : "0"} →{" "} + PE11 = {pe11Level} → DOT {dotState}. + ) : ( + <>Aktuell: OC2REF = {oc2refActive ? "1" : "0"} für TIM2, + aber keine Verbindung von TIM2_CH2 zum DOT an PE11. + )} +
+ + {connected ?
+ + + + + + + + + + + + + + + + + + + + + + + + +
Signalzustände für TIM1_CH2 mit Ausgangspolarität LOW und aktivierter DOT-Anzeigenstelle
OC2REF (intern)PE11 (Pin)DOT (low-aktiv)Zustand
1 · aktivLOW (0)AN{connected && oc2refActive ? "← aktuell" : "–"}
0 · inaktivHIGH (1)AUS{connected && !oc2refActive ? "← aktuell" : "–"}
+
:

+ Die Zuordnung OC2REF → PE11 → DOT gilt ausschließlich für das TIM1-Beispiel. + Für TIM2 existieren hier weder ein modellierter PE11-Pegel noch eine + DOT-LED-Zustandstabelle. +

} + + {connected &&

+ Warum zwei Umkehrungen? Ausgangspolarität LOW erzeugt bei + OC2REF = 1 einen LOW-Pegel an PE11. Die low-aktive DOT-Beschaltung + interpretiert gerade diesen LOW-Pegel als „Segment AN“. Das sind zwei + logische Zuordnungen – keine Behauptung über zwei reale Inverterbausteine. +

} +

+ Modellannahmen: TIM1_CH2 ist über AF1 auf PE11 geschaltet, + der Timer-Ausgang ist freigegeben (CC2E/MOE), und eine DOT-Anzeigenstelle + wird über CNTL1…CNTL4 aktiviert. Diese Freigaben und der reale + Treiber-/Multiplex-Strompfad werden hier nicht simuliert. Dies ist ein + didaktisches Blockschaltbild, kein verifizierter elektrischer Schaltplan. + Bei TIM2 bleibt die virtuelle PE11-Verbindung unterbrochen. +

+
+
+ ); +} diff --git a/src/styles.css b/src/styles.css index a0cff38..ac04741 100644 --- a/src/styles.css +++ b/src/styles.css @@ -499,3 +499,67 @@ button:disabled { opacity:.45; cursor:not-allowed; } background:#1a4955; border:1px solid #4c9c9b; border-radius:8px; cursor:pointer; } .lab-local-jumps button:hover { background:#26606b; } + +/* Issue #5: expandable, model-based OC2REF → polarity → PE11 → DOT explanation. */ +.physical-panel { flex-direction:column; align-items:stretch; } +.physical-summary { display:flex; align-items:center; justify-content:space-between; gap:20px; } +.dot-signal-path { border:1px solid #36596a; background:#0c1e2f; border-radius:12px; min-width:0; } +.dot-signal-path summary { + display:flex; align-items:center; justify-content:space-between; gap:12px; + cursor:pointer; list-style:none; padding:15px 17px; min-height:54px; +} +.dot-signal-path summary::-webkit-details-marker { display:none; } +.dot-signal-path summary:focus-visible { outline:2px solid #6de1d0; outline-offset:3px; border-radius:9px; } +.dot-signal-path summary > span:first-child { display:flex; flex-direction:column; gap:4px; min-width:0; } +.dot-signal-path summary strong { font-size:14px; color:#d5f5ec; } +.dot-signal-path summary small { font-size:12px; color:#b7cdd8; } +.dot-signal-path-toggle { color:#8ef1dc; font-weight:800; font-size:23px; transition:transform .15s; } +.dot-signal-path[open] .dot-signal-path-toggle { transform:rotate(180deg); } +.dot-signal-path-body { border-top:1px solid #315166; padding:17px; min-width:0; } +.physical-panel .dot-signal-path-body p { max-width:none; font-size:13px; color:#c0d4df; line-height:1.65; margin:0 0 14px; } +.dot-path-source { + display:flex; flex-wrap:wrap; gap:6px 12px; align-items:baseline; + padding:10px 13px; border:1px solid #345467; background:#122c3c; + border-radius:9px; margin:10px 0 16px; font-size:13px; +} +.dot-path-source strong { color:#83edda; } +.dot-path-source span { color:#e0f4f7; overflow-wrap:anywhere; } +.dot-path-stages { display:flex; align-items:stretch; gap:8px; min-width:0; } +.dot-path-stage { + flex:1 1 0; min-width:0; display:flex; flex-direction:column; gap:8px; + padding:13px 10px; background:#142c3d; border:1px solid #3a5970; border-radius:10px; +} +.dot-path-stage-label { font-size:11px; color:#a2c9d2; font-weight:800; line-height:1.4; } +.dot-path-stage strong { font-size:15px; color:#77e7d5; overflow-wrap:anywhere; } +.dot-path-stage small,.dot-path-stage-description { color:#bacdd9; font-size:12px; line-height:1.5; overflow-wrap:anywhere; } +.dot-path-arrow { align-self:center; flex:0 0 auto; color:#71cbbb; font-size:18px; font-weight:800; } +.dot-path-disconnected { border-style:dashed; opacity:.8; } +.dot-path-disconnected strong { color:#cfdeec; } +.dot-path-lit { background:#214344; border-color:#68dcc0; } +.dot-path-current { + background:#16353f; border-left:3px solid #64dcc9; color:#cfe8eb; + border-radius:5px; padding:11px 13px; margin:15px 0; font-size:13px; line-height:1.6; +} +.dot-path-current strong { color:#9df8e3; } +.dot-path-table-wrap { max-width:100%; overflow-x:auto; } +.dot-path-table { width:100%; border-collapse:collapse; text-align:left; font-size:12px; line-height:1.55; } +.dot-path-table caption { text-align:left; color:#c5dce7; padding:0 0 8px; font-weight:700; } +.dot-path-table th,.dot-path-table td { padding:9px 10px; border-bottom:1px solid #385264; } +.dot-path-table thead th { background:#183344; color:#d5edf0; } +.dot-path-table tbody th { color:#d3e9ef; font-weight:650; } +.dot-path-row-active { background:#22504a; } +.dot-path-row-active td:last-child { color:#96f5d6; font-weight:800; } +.physical-panel .dot-path-assumptions { color:#adbfcd; font-size:12px; margin-top:14px; } +@media(max-width:1000px) { + .dot-path-stages { flex-direction:column; } + .dot-path-arrow { transform:rotate(90deg); padding:0; } + .dot-path-stage { flex:0 0 auto; } +} +@media(max-width:700px) { + .physical-summary { flex-direction:column; align-items:stretch; } + .dot-signal-path-body { padding:12px; } + .dot-path-table th,.dot-path-table td { padding:8px 5px; } +} +@media(prefers-reduced-motion:reduce) { + .dot-signal-path-toggle { transition:none; } +}