From 76e0d77975ed780f54773043b9d926cbe418d08f Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Fri, 15 May 2026 06:08:38 +0200 Subject: [PATCH] =?UTF-8?q?fix(sim/7segment):=20multiplex-aware=20driver?= =?UTF-8?q?=20=E2=80=94=20track=20COM/DIG=20pins,=20latch=20segments=20per?= =?UTF-8?q?=20digit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The simulator's 7-segment part used to write segments straight into element.values[0..7] regardless of how many digits the display has and without considering the COM/DIG select pins. That meant: - Multi-digit displays (digits=2/3/4) only ever lit digit 0; the other digits stayed dark even when their DIGn pin was driven. - For 1-digit displays multiplexed via shared A-G bus + per-display COM.1 transistor (the canonical Arduino clock pattern), all four displays showed the same rapidly-changing segment pattern and rendered as flickering gibberish because COM.1/COM.2 were ignored. This rewrites the part: - Per-element state: live segments[] (Arduino-driven A..DP), per- digit latched digitValues[][], and digitEnabled[] flags. - Subscribes to the right digit-select pins for the digit count (COM.1/COM.2 for digits=1, DIG1..DIGn for digits=2/3/4). - On segment-pin change: writes to segments[] AND mirrors into every currently-enabled digit's latched slot. - On digit-pin LOW->HIGH (= enable, transistor-driver convention): latches the live segments[] into that digit's slot so the first refresh after enabling reflects the current pattern. - When NO digit-select pin is wired to an Arduino pin (pure direct drive, COM tied to GND): all digits default to enabled so segment writes propagate immediately — preserves the old behaviour for the simplest single-digit case. - Rebuilds element.values as a flat array of length digits*8 (the shape wokwi-7segment-element expects: indices d*8..d*8+7 = digit d's A..DP). Result: multiplexed 4-digit clocks built with 4 separate 1-digit 7segments + transistors actually render the four digits as the user intended. Direct-drive single-digit displays still work unchanged. --- frontend/src/simulation/parts/ChipParts.ts | 198 +++++++++++++++++---- 1 file changed, 165 insertions(+), 33 deletions(-) diff --git a/frontend/src/simulation/parts/ChipParts.ts b/frontend/src/simulation/parts/ChipParts.ts index a27b5ef9..457e33e7 100644 --- a/frontend/src/simulation/parts/ChipParts.ts +++ b/frontend/src/simulation/parts/ChipParts.ts @@ -42,28 +42,118 @@ function getConnectedToPin( } /** - * Update a 7-segment display element when pin states change. - * pinName is the segment identifier (A, B, C, D, E, F, G, DP). - * state is whether the segment is lit (HIGH = lit for common-cathode). + * Per-element multiplexing state for 7-segment displays. + * + * wokwi-7segment exposes either COM.1/COM.2 (for digits=1 — same physical + * cathode, two pin positions) or DIG1..DIGn (for digits=2/3/4). Code that + * lights more than one digit via multiplexing rapidly toggles the digit- + * select pin while writing different segment patterns. We capture this: + * + * - segments[] holds the live Arduino-driven segment pin states (A-G+DP). + * - digitEnabled[d] tracks whether the d-th digit-select pin is HIGH. + * - digitValues[d][seg] is the LATCHED state of that digit's segments — + * updated continuously while digitEnabled[d] is true, frozen otherwise. + * + * The wokwi-7segment element renders `values` as a flat array of length + * `digits*8` (indices d*8..d*8+7 are digit d's A..DP). We rebuild that + * flat array from digitValues on every update. + * + * Polarity convention: digit pin HIGH = digit enabled. Matches the + * transistor-driver pattern most multiplex code uses (Arduino HIGH → + * transistor on → COM line pulled low → common-cathode digit lit). For + * direct common-cathode without a transistor (active-low COM), users + * should add a transistor — that's the wiring this simulator models. + * + * Fallback: if NO digit-select pin is wired to an Arduino pin (pure + * direct-drive single display, common cathode tied to GND), we default + * all digits to enabled so segment writes propagate immediately. That + * preserves the pre-multiplex-aware behaviour for the simplest case. */ -function set7SegPin(element: HTMLElement, pinName: string, state: boolean) { - const segmentIndex: Record = { - A: 0, - B: 1, - C: 2, - D: 3, - E: 4, - F: 5, - G: 6, - DP: 7, - }; - const idx = segmentIndex[pinName.toUpperCase()]; - if (idx === undefined) return; +const SEGMENT_INDEX: Record = { + A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, DP: 7, +}; +const SEGMENT_NAMES = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'DP'] as const; - const el = element as any; - const current: number[] = Array.isArray(el.values) ? [...el.values] : [0, 0, 0, 0, 0, 0, 0, 0]; - current[idx] = state ? 1 : 0; - el.values = current; +interface SevenSegState { + digits: number; + segments: number[]; // length 8 + digitValues: number[][]; // [digit][seg]; flattened into element.values + digitEnabled: boolean[]; // length = digits +} + +const sevenSegState = new WeakMap(); + +function getDigitsCount(element: HTMLElement): number { + const raw = (element as unknown as { digits?: unknown }).digits; + const n = typeof raw === 'number' ? raw : parseInt(String(raw ?? 1), 10); + if (!Number.isFinite(n) || n < 1) return 1; + return Math.min(8, Math.floor(n)); +} + +function get7SegState(element: HTMLElement): SevenSegState { + let s = sevenSegState.get(element); + if (!s) { + const digits = getDigitsCount(element); + s = { + digits, + segments: [0, 0, 0, 0, 0, 0, 0, 0], + digitValues: Array.from({ length: digits }, () => [0, 0, 0, 0, 0, 0, 0, 0]), + digitEnabled: Array(digits).fill(false), + }; + sevenSegState.set(element, s); + } + return s; +} + +function flush7SegValues(element: HTMLElement) { + const s = get7SegState(element); + const flat: number[] = []; + for (let d = 0; d < s.digits; d++) flat.push(...s.digitValues[d]); + (element as unknown as { values: number[] }).values = flat; +} + +function handle7SegSegment(element: HTMLElement, segIdx: number, state: boolean) { + const s = get7SegState(element); + s.segments[segIdx] = state ? 1 : 0; + // Mirror into every currently-enabled digit's latched slot, so multiplex + // sequences that change segments WHILE a digit is enabled keep working. + let any = false; + for (let d = 0; d < s.digits; d++) { + if (s.digitEnabled[d]) { + s.digitValues[d][segIdx] = s.segments[segIdx]; + any = true; + } + } + if (any) flush7SegValues(element); +} + +function handle7SegDigit(element: HTMLElement, digitIdx: number, state: boolean) { + const s = get7SegState(element); + if (digitIdx < 0 || digitIdx >= s.digits) return; + const wasEnabled = s.digitEnabled[digitIdx]; + s.digitEnabled[digitIdx] = state; + // On LOW->HIGH transition, latch the live segments into this digit's slot + // so the very first frame this digit is enabled reflects the current + // Arduino-driven pattern. Without this, multiplex code that sets segments + // BEFORE toggling the digit pin would miss the first refresh and render + // a stale value for one cycle. + if (!wasEnabled && state) { + s.digitValues[digitIdx] = [...s.segments]; + flush7SegValues(element); + } +} + +/** Legacy entry point — direct segment write with no multiplex awareness. + * Kept for the chained-via-74HC595 path which still uses the old API. */ +function set7SegPin(element: HTMLElement, pinName: string, state: boolean) { + const idx = SEGMENT_INDEX[pinName.toUpperCase()]; + if (idx === undefined) return; + // Force the legacy single-digit assumption: enable digit 0 so the segment + // write actually surfaces. Multi-digit displays driven via 74HC595 would + // need their own dedicated wiring; that path doesn't exist in the wild. + const s = get7SegState(element); + s.digitEnabled[0] = true; + handle7SegSegment(element, idx, state); } // ─── 74HC595 simulation ─────────────────────────────────────────────────────── @@ -210,25 +300,67 @@ PartSimulationRegistry.register('7segment', { if (!pinManager) return () => {}; const unsubscribers: (() => void)[] = []; + const s = get7SegState(element); - const segments = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'DP']; - for (let i = 0; i < segments.length; i++) { - const seg = segments[i]; + // Figure out which digit-select pins this display exposes and subscribe + // to whichever ones are actually wired to an Arduino pin. Polarity is + // determined by the helper's HIGH/LOW report (we treat HIGH = enabled). + const digitPinNames: string[] = s.digits === 1 + ? ['COM.1', 'COM.2'] // 1-digit: both COM pins map to digit 0 + : Array.from({ length: s.digits }, (_, i) => `DIG${i + 1}`); // 2/3/4-digit: DIG1..DIGn + let digitPinsWired = 0; + for (let d = 0; d < digitPinNames.length; d++) { + const pin = getArduinoPinHelper(digitPinNames[d]); + if (pin === null) continue; + digitPinsWired++; + const digitIdx = s.digits === 1 ? 0 : d; + unsubscribers.push( + pinManager.onPinChange(pin, (_: number, state: boolean) => { + handle7SegDigit(element, digitIdx, state); + }), + ); + } + // No digit-select pin wired → direct drive (common cathode tied to GND, + // or a single display being lit unconditionally). Enable every digit + // so segment writes propagate immediately. + if (digitPinsWired === 0) { + for (let d = 0; d < s.digits; d++) s.digitEnabled[d] = true; + } + + // Subscribe to A-G + DP segment pins. + for (let i = 0; i < SEGMENT_NAMES.length; i++) { + const seg = SEGMENT_NAMES[i]; const arduinoPin = getArduinoPinHelper(seg); - if (arduinoPin !== null) { - unsubscribers.push( - pinManager.onPinChange(arduinoPin, (_: number, state: boolean) => { - set7SegPin(element, seg, state); - }), - ); - } + if (arduinoPin === null) continue; + unsubscribers.push( + pinManager.onPinChange(arduinoPin, (_: number, state: boolean) => { + handle7SegSegment(element, i, state); + }), + ); } return () => unsubscribers.forEach((u) => u()); }, - // Called by SimulatorCanvas for boards without a local simulator (e.g. ESP32 via QEMU backend). - // pinName is the segment identifier (A, B, C, D, E, F, G, DP). + // Called by SimulatorCanvas for boards without a local simulator (e.g. + // ESP32 via QEMU backend). Dispatch by pin name. onPinStateChange: (pinName: string, state: boolean, element: HTMLElement) => { - set7SegPin(element, pinName, state); + const upper = pinName.toUpperCase(); + const segIdx = SEGMENT_INDEX[upper]; + if (segIdx !== undefined) { + // For the QEMU path we don't see the digit-select pins, so fall back + // to legacy direct-drive: ensure digit 0 is enabled. + const s = get7SegState(element); + if (!s.digitEnabled.some(Boolean)) s.digitEnabled[0] = true; + handle7SegSegment(element, segIdx, state); + return; + } + if (upper === 'COM.1' || upper === 'COM.2') { + handle7SegDigit(element, 0, state); + return; + } + const dm = upper.match(/^DIG(\d+)$/); + if (dm) { + handle7SegDigit(element, parseInt(dm[1], 10) - 1, state); + } }, });