velxio/frontend/src/simulation/parts/ChipParts.ts

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);
}
},
});