feat: Add 74HC595 Shift Register component and simulation logic

pull/10/head
David Montero Crespo 2026-03-07 23:24:23 -03:00
parent 41dfd20583
commit 318305bac4
6 changed files with 367 additions and 0 deletions

View File

@ -700,6 +700,33 @@
"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",
"tagName": "wokwi-analog-joystick",

View File

@ -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 (Q1Q7, 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 };

View File

@ -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(

View File

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

View File

@ -1,3 +1,4 @@
export * from './PartSimulationRegistry';
import './BasicParts';
import './ComplexParts';
import './ChipParts';

View File

@ -23,5 +23,6 @@ declare namespace JSX {
'wokwi-ir-receiver': any;
'wokwi-pir-motion-sensor': any;
'wokwi-photoresistor-sensor': any;
'wokwi-74hc595': any;
}
}