import { PartSimulationRegistry } from './PartSimulationRegistry'; import type { AVRSimulator } from '../AVRSimulator'; /** * RGB LED implementation * Translates digital HIGH/LOW to corresponding 255/0 values on RGB channels */ PartSimulationRegistry.register('rgb-led', { onPinStateChange: (pinName: string, state: boolean, element: HTMLElement) => { const el = element as any; if (pinName === 'R') { el.ledRed = state ? 255 : 0; } else if (pinName === 'G') { el.ledGreen = state ? 255 : 0; } else if (pinName === 'B') { el.ledBlue = state ? 255 : 0; } } }); /** * Analog Potentiometer implementation */ PartSimulationRegistry.register('potentiometer', { attachEvents: (element: HTMLElement, avrSimulator: AVRSimulator, getArduinoPinHelper: (pin: string) => number | null) => { // A potentiometer's 'SIG' pin goes to an Analog In (A0-A5). // We map generic pin integers back to our ADC logic. // E.g., A0 = pin 14 on UNO // Potentiometer emits 'input' events when dragged const onInput = (_e: Event) => { const arduinoPin = getArduinoPinHelper('SIG'); // If connected to Analog Pin (14-19 is A0-A5 on Uno) if (arduinoPin !== null && arduinoPin >= 14 && arduinoPin <= 19) { // Find the analog channel (0-5) const channel = arduinoPin - 14; // Element's value is between 0-1023 const value = parseInt((element as any).value || '0', 10); // Access avr8js ADC to inject analog voltages // (Assuming getADC is implemented in AVRSimulator) const adc: any = (avrSimulator as any).getADC?.(); if (adc) { // ADC wants a float voltage from 0 to 5V. // Potentiometer is linearly 0-1023 -> 5V max const volts = (value / 1023.0) * 5.0; adc.channelValues[channel] = volts; } } }; element.addEventListener('input', onInput); return () => { element.removeEventListener('input', onInput); }; }, }); /** * HD44780 LCD Controller Simulation (for LCD 1602 and LCD 2004) * * Implements the 4-bit mode protocol used by the Arduino LiquidCrystal library. * The HD44780 controller uses 6 signal lines in 4-bit mode: * RS (Register Select): 0 = command, 1 = data * E (Enable): Data is latched on falling edge (HIGH→LOW) * D4-D7: 4 data bits (high nibble first, then low nibble) * * DDRAM address mapping for multi-line displays: * Line 0: 0x00-0x13 (or 0x00-0x0F for 16x2) * Line 1: 0x40-0x53 (or 0x40-0x4F for 16x2) * Line 2: 0x14-0x27 (20x4 only) * Line 3: 0x54-0x67 (20x4 only) */ function createLcdSimulation(cols: number, rows: number) { return { attachEvents: (element: HTMLElement, avrSimulator: AVRSimulator, getArduinoPinHelper: (pin: string) => number | null) => { const el = element as any; // HD44780 internal state const ddram = new Uint8Array(128).fill(0x20); // Display Data RAM (space = 0x20) let ddramAddress = 0; // Current DDRAM address let entryIncrement = true; // true = increment, false = decrement let displayOn = true; // Is display on? let cursorOn = false; // Underline cursor visible? let blinkOn = false; // Blinking block cursor? let nibbleState: 'high' | 'low' = 'high'; // 4-bit mode nibble tracking let highNibble = 0; // Stored high nibble let initialized = false; // Has initialization sequence completed? let initCount = 0; // Count initialization nibbles // Pin states tracked locally let rsState = false; let eState = false; let d4State = false; let d5State = false; let d6State = false; let d7State = false; // DDRAM line offsets for the HD44780 const lineOffsets = rows >= 4 ? [0x00, 0x40, 0x14, 0x54] // 20x4 LCD : [0x00, 0x40]; // 16x2 LCD // Convert DDRAM address to linear buffer index for the element function ddramToLinear(addr: number): number { for (let row = 0; row < rows; row++) { const offset = lineOffsets[row]; if (addr >= offset && addr < offset + cols) { return row * cols + (addr - offset); } } return -1; // Address not visible } // Refresh the wokwi-element's characters from our DDRAM function refreshDisplay() { if (!displayOn) { // Blank display el.characters = new Uint8Array(cols * rows).fill(0x20); return; } const chars = new Uint8Array(cols * rows); for (let row = 0; row < rows; row++) { const offset = lineOffsets[row]; for (let col = 0; col < cols; col++) { chars[row * cols + col] = ddram[offset + col]; } } el.characters = chars; el.cursor = cursorOn; el.blink = blinkOn; // Set cursor position const cursorLinear = ddramToLinear(ddramAddress); if (cursorLinear >= 0) { el.cursorX = cursorLinear % cols; el.cursorY = Math.floor(cursorLinear / cols); } } // Process a complete byte (command or data) function processByte(rs: boolean, data: number) { if (!rs) { // === COMMAND === if (data & 0x80) { // Set DDRAM Address (bit 7 = 1) ddramAddress = data & 0x7F; } else if (data & 0x40) { // Set CGRAM Address - not implemented for display } else if (data & 0x20) { // Function Set (usually during init) // DL=0 means 4-bit mode (already assumed) initialized = true; } else if (data & 0x10) { // Cursor/Display Shift const sc = (data >> 3) & 1; const rl = (data >> 2) & 1; if (!sc) { // Move cursor ddramAddress += rl ? 1 : -1; ddramAddress &= 0x7F; } } else if (data & 0x08) { // Display On/Off Control displayOn = !!(data & 0x04); cursorOn = !!(data & 0x02); blinkOn = !!(data & 0x01); } else if (data & 0x04) { // Entry Mode Set entryIncrement = !!(data & 0x02); } else if (data & 0x02) { // Return Home ddramAddress = 0; } else if (data & 0x01) { // Clear Display ddram.fill(0x20); ddramAddress = 0; } } else { // === DATA (character write) === ddram[ddramAddress & 0x7F] = data; // Auto-increment/decrement address if (entryIncrement) { ddramAddress = (ddramAddress + 1) & 0x7F; } else { ddramAddress = (ddramAddress - 1) & 0x7F; } } refreshDisplay(); } // Handle Enable pin falling edge - this is where data is latched function onEnableFallingEdge() { // Read D4-D7 to form a nibble const nibble = (d4State ? 0x01 : 0) | (d5State ? 0x02 : 0) | (d6State ? 0x04 : 0) | (d7State ? 0x08 : 0); // During initialization, the LiquidCrystal library sends // several single-nibble commands (0x03, 0x03, 0x03, 0x02) // before switching to 4-bit mode proper if (!initialized) { initCount++; if (initCount >= 4) { initialized = true; nibbleState = 'high'; } return; } if (nibbleState === 'high') { // First nibble (high 4 bits) highNibble = nibble << 4; nibbleState = 'low'; } else { // Second nibble (low 4 bits) → combine and process const fullByte = highNibble | nibble; nibbleState = 'high'; processByte(rsState, fullByte); } } // Get Arduino pin numbers for LCD pins const pinRS = getArduinoPinHelper('RS'); const pinE = getArduinoPinHelper('E'); const pinD4 = getArduinoPinHelper('D4'); const pinD5 = getArduinoPinHelper('D5'); const pinD6 = getArduinoPinHelper('D6'); const pinD7 = getArduinoPinHelper('D7'); console.log(`[LCD] Pin mapping: RS=${pinRS}, E=${pinE}, D4=${pinD4}, D5=${pinD5}, D6=${pinD6}, D7=${pinD7}`); // Subscribe to pin changes via PinManager const pinManager = (avrSimulator as any).pinManager; if (!pinManager) { console.warn('[LCD] No pinManager found on AVRSimulator'); return () => { }; } const unsubscribers: (() => void)[] = []; if (pinRS !== null) { unsubscribers.push(pinManager.onPinChange(pinRS, (_p: number, state: boolean) => { rsState = state; })); } if (pinD4 !== null) { unsubscribers.push(pinManager.onPinChange(pinD4, (_p: number, state: boolean) => { d4State = state; })); } if (pinD5 !== null) { unsubscribers.push(pinManager.onPinChange(pinD5, (_p: number, state: boolean) => { d5State = state; })); } if (pinD6 !== null) { unsubscribers.push(pinManager.onPinChange(pinD6, (_p: number, state: boolean) => { d6State = state; })); } if (pinD7 !== null) { unsubscribers.push(pinManager.onPinChange(pinD7, (_p: number, state: boolean) => { d7State = state; })); } // Enable pin: watch for falling edge (HIGH → LOW) if (pinE !== null) { unsubscribers.push(pinManager.onPinChange(pinE, (_p: number, state: boolean) => { const wasHigh = eState; eState = state; // Falling edge: data is latched if (wasHigh && !state) { onEnableFallingEdge(); } })); } // Initialize display as blank refreshDisplay(); console.log(`[LCD] ${cols}x${rows} simulation initialized`); return () => { unsubscribers.forEach(u => u()); console.log(`[LCD] ${cols}x${rows} simulation cleaned up`); }; }, }; } // Register LCD 1602 (16x2) PartSimulationRegistry.register('lcd1602', createLcdSimulation(16, 2)); // Register LCD 2004 (20x4) PartSimulationRegistry.register('lcd2004', createLcdSimulation(20, 4));