367 lines
14 KiB
TypeScript
367 lines
14 KiB
TypeScript
/**
|
|
* ChipParts.ts — Simulation logic for complex IC chips
|
|
*
|
|
* Implements:
|
|
* - 74HC595 8-bit Serial-to-Parallel Shift Register
|
|
* - wokwi-7segment display (driven by 74HC595 outputs)
|
|
*/
|
|
|
|
import { PartSimulationRegistry } from './PartSimulationRegistry';
|
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Given a 74HC595 component ID and a pin name (e.g. 'Q0'), find the DOM element
|
|
* of whatever component is connected on the other side of that wire, plus the
|
|
* pin name on that component.
|
|
*/
|
|
function getConnectedToPin(
|
|
componentId: string,
|
|
pinName: string,
|
|
): { element: HTMLElement; pinName: string } | null {
|
|
const { wires } = useSimulatorStore.getState();
|
|
for (const wire of wires) {
|
|
let otherCompId: string | null = null;
|
|
let otherPin: string | null = null;
|
|
|
|
if (wire.start.componentId === componentId && wire.start.pinName === pinName) {
|
|
otherCompId = wire.end.componentId;
|
|
otherPin = wire.end.pinName;
|
|
} else if (wire.end.componentId === componentId && wire.end.pinName === pinName) {
|
|
otherCompId = wire.start.componentId;
|
|
otherPin = wire.start.pinName;
|
|
}
|
|
|
|
if (otherCompId && otherPin) {
|
|
const el = document.getElementById(otherCompId);
|
|
if (el) return { element: el as HTMLElement, pinName: otherPin };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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;
|
|
|
|
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 ───────────────────────────────────────────────────────
|
|
|
|
PartSimulationRegistry.register('74hc595', {
|
|
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
|
const pinManager = (simulator as any).pinManager;
|
|
if (!pinManager) return () => {};
|
|
|
|
// Internal state
|
|
let shiftReg = 0; // 8-bit shift register
|
|
let storageReg = 0; // 8-bit storage register (output)
|
|
let oeActive = false; // output enable (active low)
|
|
let mrActive = true; // master reset (active low — HIGH = not reset)
|
|
|
|
let prevShcp = false;
|
|
let prevStcp = false;
|
|
|
|
// Resolve connected Arduino pins
|
|
const pinDS = getArduinoPinHelper('DS');
|
|
const pinSHCP = getArduinoPinHelper('SHCP');
|
|
const pinSTCP = getArduinoPinHelper('STCP');
|
|
const pinMR = getArduinoPinHelper('MR');
|
|
const pinOE = getArduinoPinHelper('OE');
|
|
|
|
const unsubscribers: (() => void)[] = [];
|
|
|
|
// Helper: propagate current storage reg outputs to connected components
|
|
const propagateOutputs = () => {
|
|
if (!oeActive) return; // outputs disabled (OE high = disabled)
|
|
const compId = element.id;
|
|
|
|
// Q0-Q7 maps to bits 0-7 of storageReg
|
|
const outputPins = ['Q0', 'Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q6', 'Q7'];
|
|
for (let i = 0; i < 8; i++) {
|
|
const state = ((storageReg >> i) & 1) === 1;
|
|
const connected = getConnectedToPin(compId, outputPins[i]);
|
|
if (connected) {
|
|
// Update 7-segment or LED or any other component
|
|
const tagName = connected.element.tagName.toLowerCase();
|
|
if (tagName === 'wokwi-7segment') {
|
|
set7SegPin(connected.element, connected.pinName, state);
|
|
} else if (tagName === 'wokwi-led') {
|
|
(connected.element as any).value = state ? 1 : 0;
|
|
}
|
|
// Update 74HC595 chained via Q7S
|
|
if (tagName === 'velxio-74hc595' && outputPins[i] === 'Q7S') {
|
|
// Q7S is serial out — drives DS of next chip (handled via wire logic)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update this element's visual (Q0-Q7 output dots)
|
|
const el = element as any;
|
|
el.values = outputPins.map((_, i) => (storageReg >> i) & 1);
|
|
|
|
// Also propagate Q7S (serial output = bit 7 of shift register, not storage)
|
|
const q7sConn = getConnectedToPin(element.id, 'Q7S');
|
|
if (q7sConn) {
|
|
// Q7S is used to chain to the DS pin of next 74HC595 — this is handled
|
|
// by the DS monitoring of the downstream chip
|
|
}
|
|
};
|
|
|
|
// OE (active low — LOW enables outputs)
|
|
if (pinOE !== null) {
|
|
pinManager.triggerPinChange(pinOE, true); // default HIGH = disabled
|
|
unsubscribers.push(
|
|
pinManager.onPinChange(pinOE, (_: number, state: boolean) => {
|
|
oeActive = !state; // OE low = active
|
|
propagateOutputs();
|
|
}),
|
|
);
|
|
} else {
|
|
oeActive = true; // assume OE tied to GND (always enabled)
|
|
}
|
|
|
|
// MR (active low — LOW resets shift register)
|
|
if (pinMR !== null) {
|
|
unsubscribers.push(
|
|
pinManager.onPinChange(pinMR, (_: number, state: boolean) => {
|
|
mrActive = state; // MR high = no reset, low = reset
|
|
if (!mrActive) {
|
|
shiftReg = 0;
|
|
}
|
|
}),
|
|
);
|
|
} else {
|
|
mrActive = true; // assume MR tied high
|
|
}
|
|
|
|
// DS — latched on SHCP rising edge; just track current value
|
|
let dsState = false;
|
|
if (pinDS !== null) {
|
|
unsubscribers.push(
|
|
pinManager.onPinChange(pinDS, (_: number, state: boolean) => {
|
|
dsState = state;
|
|
}),
|
|
);
|
|
}
|
|
|
|
// SHCP — rising edge shifts DS into shift register
|
|
if (pinSHCP !== null) {
|
|
unsubscribers.push(
|
|
pinManager.onPinChange(pinSHCP, (_: number, state: boolean) => {
|
|
if (state && !prevShcp) {
|
|
// Rising edge
|
|
if (mrActive) {
|
|
// Shift: MSB shifts out via Q7S, DS enters at LSB
|
|
shiftReg = ((shiftReg << 1) | (dsState ? 1 : 0)) & 0xff;
|
|
}
|
|
}
|
|
prevShcp = state;
|
|
}),
|
|
);
|
|
}
|
|
|
|
// STCP — rising edge latches shift register to storage register
|
|
if (pinSTCP !== null) {
|
|
unsubscribers.push(
|
|
pinManager.onPinChange(pinSTCP, (_: number, state: boolean) => {
|
|
if (state && !prevStcp) {
|
|
// Rising edge — latch
|
|
storageReg = shiftReg;
|
|
propagateOutputs();
|
|
}
|
|
prevStcp = state;
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Initial state propagation
|
|
propagateOutputs();
|
|
|
|
return () => unsubscribers.forEach((u) => u());
|
|
},
|
|
});
|
|
|
|
// ─── 7-segment display (direct-drive, when connected directly to Arduino) ────
|
|
|
|
PartSimulationRegistry.register('7segment', {
|
|
attachEvents: (element, simulator, getArduinoPinHelper) => {
|
|
const pinManager = (simulator as any).pinManager;
|
|
if (!pinManager) return () => {};
|
|
|
|
const unsubscribers: (() => void)[] = [];
|
|
const s = get7SegState(element);
|
|
|
|
// 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) 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). Dispatch by pin name.
|
|
onPinStateChange: (pinName: string, state: boolean, element: HTMLElement) => {
|
|
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);
|
|
}
|
|
},
|
|
});
|