diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index e175f37..f9c895e 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -700,6 +700,33 @@ "segment" ] }, + { + "id": "74hc595", + "tagName": "wokwi-74hc595", + "name": "74HC595 Shift Register", + "category": "other", + "thumbnail": "74HC595", + "properties": [ + { + "name": "values", + "type": "number", + "defaultValue": [0, 0, 0, 0, 0, 0, 0, 0], + "control": "range" + } + ], + "defaultValues": { + "values": [0, 0, 0, 0, 0, 0, 0, 0] + }, + "pinCount": 16, + "tags": [ + "74hc595", + "shift register", + "ic", + "chip", + "74", + "595" + ] + }, { "id": "analog-joystick", "tagName": "wokwi-analog-joystick", diff --git a/frontend/src/components/components-wokwi/IC74HC595.ts b/frontend/src/components/components-wokwi/IC74HC595.ts new file mode 100644 index 0000000..578e02c --- /dev/null +++ b/frontend/src/components/components-wokwi/IC74HC595.ts @@ -0,0 +1,127 @@ +/** + * wokwi-74hc595 — 8-bit Serial-to-Parallel Shift Register (SN74HC595) + * + * DIP-16 package custom element for use in Velxio/Wokwi simulations. + */ + +class IC74HC595Element extends HTMLElement { + private _values: number[] = [0, 0, 0, 0, 0, 0, 0, 0]; // Q0-Q7 + private _shadow: ShadowRoot; + + // ── Pin layout (DIP-16, matching wokwi-elements coordinate style) ───────── + // Top row (y=3): pins 16→9, left to right + // Bottom row (y=51): pins 1→8, left to right + readonly pinInfo = [ + // Bottom row: pins 1-8 (Q1–Q7, GND) + { name: 'Q1', number: 1, x: 8.1, y: 51.3, signals: [] }, + { name: 'Q2', number: 2, x: 17.7, y: 51.3, signals: [] }, + { name: 'Q3', number: 3, x: 27.3, y: 51.3, signals: [] }, + { name: 'Q4', number: 4, x: 36.9, y: 51.3, signals: [] }, + { name: 'Q5', number: 5, x: 46.5, y: 51.3, signals: [] }, + { name: 'Q6', number: 6, x: 56.1, y: 51.3, signals: [] }, + { name: 'Q7', number: 7, x: 65.7, y: 51.3, signals: [] }, + { name: 'GND', number: 8, x: 75.3, y: 51.3, signals: [] }, + // Top row: pins 9-16 (Q7S, MR, SHCP, STCP, OE, DS, Q0, VCC) right to left + { name: 'Q7S', number: 9, x: 75.3, y: 3, signals: [] }, + { name: 'MR', number: 10, x: 65.7, y: 3, signals: [] }, + { name: 'SHCP', number: 11, x: 56.1, y: 3, signals: [] }, + { name: 'STCP', number: 12, x: 46.5, y: 3, signals: [] }, + { name: 'OE', number: 13, x: 36.9, y: 3, signals: [] }, + { name: 'DS', number: 14, x: 27.3, y: 3, signals: [] }, + { name: 'Q0', number: 15, x: 17.7, y: 3, signals: [] }, + { name: 'VCC', number: 16, x: 8.1, y: 3, signals: [] }, + ]; + + constructor() { + super(); + this._shadow = this.attachShadow({ mode: 'open' }); + this._render(); + } + + get values() { + return this._values; + } + + set values(v: number[]) { + this._values = Array.isArray(v) ? [...v] : [0, 0, 0, 0, 0, 0, 0, 0]; + this._renderOutputDots(); + } + + private _render() { + const PIN_X = [8.1, 17.7, 27.3, 36.9, 46.5, 56.1, 65.7, 75.3]; + const TOP_Y = 3; + const BOT_Y = 51.3; + const CHIP_Y1 = 9; + const CHIP_H = 37; + + const topLabels = ['VCC', 'Q0', 'DS', 'OE', 'SCP', 'HCP', 'MR', 'Q7S']; + const botLabels = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q6', 'Q7', 'GND']; + + const pinLines = PIN_X.map((x) => ` + + + + + `).join(''); + + const topText = PIN_X.map((x, i) => ` + ${topLabels[i]} + `).join(''); + + const botText = PIN_X.map((x, i) => ` + ${botLabels[i]} + `).join(''); + + const outputDots = PIN_X.map((x, i) => ` + + `).join(''); + + this._shadow.innerHTML = ` + + + ${pinLines} + + + + + + 74HC595 + + ${topText} + + ${botText} + + ${outputDots} + + + + `; + } + + private _renderOutputDots() { + if (!this._shadow) return; + // Update Q1-Q7 output dots (bottom, indices 1-7 of values) + for (let i = 1; i <= 7; i++) { + const dot = this._shadow.getElementById(`dot-q${i}`) as SVGCircleElement | null; + if (dot) { + dot.setAttribute('fill', this._values[i] ? '#00ff88' : '#333'); + dot.setAttribute('opacity', this._values[i] ? '1' : '0.5'); + } + } + // Q0 on top side + const dotQ0 = this._shadow.getElementById('dot-q0') as SVGCircleElement | null; + if (dotQ0) { + dotQ0.setAttribute('fill', this._values[0] ? '#00ff88' : '#333'); + dotQ0.setAttribute('opacity', this._values[0] ? '1' : '0.5'); + } + } +} + +if (!customElements.get('wokwi-74hc595')) { + customElements.define('wokwi-74hc595', IC74HC595Element); +} + +export { IC74HC595Element }; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 65cf9e4..a1f9ecc 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,5 +1,6 @@ import { createRoot } from 'react-dom/client' import './index.css' +import './components/components-wokwi/IC74HC595' import App from './App.tsx' createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/simulation/parts/ChipParts.ts b/frontend/src/simulation/parts/ChipParts.ts new file mode 100644 index 0000000..4307595 --- /dev/null +++ b/frontend/src/simulation/parts/ChipParts.ts @@ -0,0 +1,210 @@ +/** + * 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; +} + +/** + * 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). + */ +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 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; +} + +// ─── 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 === 'wokwi-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 segments = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'DP']; + for (let i = 0; i < segments.length; i++) { + const seg = segments[i]; + const arduinoPin = getArduinoPinHelper(seg); + if (arduinoPin !== null) { + unsubscribers.push(pinManager.onPinChange(arduinoPin, (_: number, state: boolean) => { + set7SegPin(element, seg, state); + })); + } + } + + return () => unsubscribers.forEach(u => u()); + }, +}); diff --git a/frontend/src/simulation/parts/index.ts b/frontend/src/simulation/parts/index.ts index e7a1d14..c7f2b58 100644 --- a/frontend/src/simulation/parts/index.ts +++ b/frontend/src/simulation/parts/index.ts @@ -1,3 +1,4 @@ export * from './PartSimulationRegistry'; import './BasicParts'; import './ComplexParts'; +import './ChipParts'; diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 650a52f..553d84a 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -23,5 +23,6 @@ declare namespace JSX { 'wokwi-ir-receiver': any; 'wokwi-pir-motion-sensor': any; 'wokwi-photoresistor-sensor': any; + 'wokwi-74hc595': any; } }