fix(sim/7segment): multiplex-aware driver — track COM/DIG pins, latch segments per digit

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.
This commit is contained in:
davidmonterocrespo24 2026-05-15 06:08:38 +02:00
parent 1e4d78fda5
commit 76e0d77975
1 changed files with 165 additions and 33 deletions

View File

@ -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<string, number> = {
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<string, number> = {
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<HTMLElement, SevenSegState>();
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);
}
},
});