feat: Add 74HC595 Shift Register component and simulation logic
parent
41dfd20583
commit
318305bac4
|
|
@ -700,6 +700,33 @@
|
||||||
"segment"
|
"segment"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "74hc595",
|
||||||
|
"tagName": "wokwi-74hc595",
|
||||||
|
"name": "74HC595 Shift Register",
|
||||||
|
"category": "other",
|
||||||
|
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\"><rect width=\"64\" height=\"64\" fill=\"#1a1a2e\" rx=\"4\"/><rect x=\"4\" y=\"16\" width=\"56\" height=\"30\" rx=\"2\" fill=\"#222\" stroke=\"#555\" stroke-width=\"0.8\"/><text x=\"32\" y=\"34\" text-anchor=\"middle\" font-size=\"7\" font-weight=\"bold\" fill=\"#eee\" font-family=\"monospace\">74HC595</text></svg>",
|
||||||
|
"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",
|
"id": "analog-joystick",
|
||||||
"tagName": "wokwi-analog-joystick",
|
"tagName": "wokwi-analog-joystick",
|
||||||
|
|
|
||||||
|
|
@ -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) => `
|
||||||
|
<line x1="${x}" y1="${TOP_Y}" x2="${x}" y2="${CHIP_Y1}" stroke="#aaa" stroke-width="1.5"/>
|
||||||
|
<circle cx="${x}" cy="${TOP_Y}" r="2" fill="#aaa"/>
|
||||||
|
<line x1="${x}" y1="${BOT_Y}" x2="${x}" y2="${CHIP_Y1 + CHIP_H}" stroke="#aaa" stroke-width="1.5"/>
|
||||||
|
<circle cx="${x}" cy="${BOT_Y}" r="2" fill="#aaa"/>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const topText = PIN_X.map((x, i) => `
|
||||||
|
<text x="${x}" y="${CHIP_Y1 + 7}" text-anchor="middle" font-size="3.5" fill="#0ff">${topLabels[i]}</text>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const botText = PIN_X.map((x, i) => `
|
||||||
|
<text x="${x}" y="${CHIP_Y1 + CHIP_H - 3}" text-anchor="middle" font-size="3.5" fill="#0ff">${botLabels[i]}</text>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const outputDots = PIN_X.map((x, i) => `
|
||||||
|
<circle id="dot-q${i + 1}" cx="${x}" cy="${BOT_Y - 6}" r="2" fill="#333" opacity="0.6"/>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
this._shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: inline-block; }
|
||||||
|
svg { overflow: visible; }
|
||||||
|
</style>
|
||||||
|
<svg width="84" height="55" viewBox="0 0 84 55" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
${pinLines}
|
||||||
|
<!-- Chip body -->
|
||||||
|
<rect x="5" y="${CHIP_Y1}" width="74" height="${CHIP_H}" rx="2" fill="#1a1a2e" stroke="#555" stroke-width="0.8"/>
|
||||||
|
<!-- Notch (pin 1 indicator) -->
|
||||||
|
<ellipse cx="5" cy="${CHIP_Y1 + CHIP_H / 2}" rx="3" ry="4" fill="#111" stroke="#555" stroke-width="0.5"/>
|
||||||
|
<!-- Chip label -->
|
||||||
|
<text x="42" y="${CHIP_Y1 + 14}" text-anchor="middle" font-size="6" font-weight="bold" fill="#eee" font-family="monospace">74HC595</text>
|
||||||
|
<!-- Pin labels top -->
|
||||||
|
${topText}
|
||||||
|
<!-- Pin labels bottom -->
|
||||||
|
${botText}
|
||||||
|
<!-- Q1-Q7 output state dots (bottom side, animated) -->
|
||||||
|
${outputDots}
|
||||||
|
<!-- Q0 dot on top side -->
|
||||||
|
<circle id="dot-q0" cx="${PIN_X[1]}" cy="${CHIP_Y1 + 6}" r="2" fill="#333" opacity="0.6"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 };
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
import './components/components-wokwi/IC74HC595'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
|
|
||||||
|
|
@ -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<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 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());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './PartSimulationRegistry';
|
export * from './PartSimulationRegistry';
|
||||||
import './BasicParts';
|
import './BasicParts';
|
||||||
import './ComplexParts';
|
import './ComplexParts';
|
||||||
|
import './ChipParts';
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,6 @@ declare namespace JSX {
|
||||||
'wokwi-ir-receiver': any;
|
'wokwi-ir-receiver': any;
|
||||||
'wokwi-pir-motion-sensor': any;
|
'wokwi-pir-motion-sensor': any;
|
||||||
'wokwi-photoresistor-sensor': any;
|
'wokwi-photoresistor-sensor': any;
|
||||||
|
'wokwi-74hc595': any;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue