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:
parent
1e4d78fda5
commit
76e0d77975
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue