feat: Enhance PinManager with PWM and Analog support

- Added PWM duty cycle tracking and callback registration to PinManager.
- Introduced methods for handling analog voltage injection and callbacks.
- Updated updatePort method to notify digital pin listeners.
- Improved listener management with clearAllListeners method.

feat: Expand BasicParts with new components

- Registered new components: 6mm Pushbutton, Slide Switch, DIP Switch 8, LED Bar Graph, and 7-Segment Display.
- Implemented event handling for each component to interact with the AVR simulator.

feat: Introduce ComplexParts with advanced components

- Added RGB LED with PWM support for color mixing.
- Implemented Potentiometer and Slide Potentiometer for analog input.
- Created Photoresistor Sensor to simulate light levels.
- Developed Analog Joystick for two-axis control and button press.
- Added Servo motor simulation with pulse width modulation.
- Implemented Buzzer using Web Audio API for sound generation.
- Created LCD 1602 and 2004 simulations with command/data processing.
pull/10/head
David Montero Crespo 2026-03-04 18:27:14 -03:00
parent c2f07665b4
commit 5ca8a82985
7 changed files with 889 additions and 357 deletions

248
README.md
View File

@ -1,6 +1,6 @@
# Arduino Emulator - Wokwi Clone
# OpenWokwi - Arduino Emulator
Local Arduino emulator with code editor and visual simulator.
A fully local, open-source Arduino emulator inspired by [Wokwi](https://wokwi.com). Write Arduino code, compile it, and simulate it with real AVR8 CPU emulation and 48+ interactive electronic components — all running in your browser.
## Support This Project
@ -10,18 +10,6 @@ If you find this project helpful, please consider giving it a star! Your support
Every star counts and helps make this project better!
## Features
- ✅ Code editor with syntax highlighting (Monaco Editor)
- ✅ Arduino code compilation with arduino-cli
- ✅ **Official Wokwi repositories cloned locally**
- ✅ **wokwi-elements** - Electronic web components
- ✅ **avr8js** - AVR8 emulator
- ✅ **rp2040js** - RP2040 emulator (future)
- ✅ Visual components using wokwi-elements (Arduino Uno, LEDs, etc.)
- ⏳ Full emulation with avr8js (in progress)
- ⏳ SQLite persistence (coming soon)
## Screenshots
![Arduino Emulator - Editor and Simulator](doc/img1.png)
@ -32,6 +20,82 @@ Arduino emulator with Monaco code editor and visual simulator with wokwi-element
Interactive component properties dialog and segment-based wire editing
## Features
### Code Editing
- ✅ **Monaco Editor** — Full C++ editor with syntax highlighting, autocomplete, minimap, and dark theme
- ✅ **Arduino compilation** via `arduino-cli` backend — compile sketches to `.hex` files
- ✅ **Compile / Run / Stop / Reset** toolbar buttons with status messages
### AVR8 Simulation (avr8js)
- ✅ **Real ATmega328p emulation** at 16 MHz using avr8js
- ✅ **Intel HEX parser** with checksum verification
- ✅ **Full GPIO support** — PORTB (pins 8-13), PORTC (A0-A5), PORTD (pins 0-7)
- ✅ **Timer0 peripheral** support
- ✅ **USART (Serial)** support
- ✅ **~60 FPS simulation loop** with `requestAnimationFrame` (~267k cycles/frame)
- ✅ **Speed control** — adjustable from 0.1x to 10x
- ✅ **Single-step debugging** API
- ✅ **External pin state injection** for input components (buttons, potentiometers)
### Component System (48+ Components)
- ✅ **48 electronic components** auto-discovered from wokwi-elements source code
- ✅ **Component picker modal** with search bar, category filtering, and live wokwi-element previews as thumbnails
- ✅ **9 component categories**: Boards (4), Sensors (6), Displays (3), Input (5), Output (5), Motors (2), Passive (4), Other (19)
- ✅ **Dynamic component rendering** from build-time metadata (TypeScript AST parser extracts `@customElement` tags, `@property` decorators, and pin counts)
- ✅ **Drag-and-drop repositioning** on the simulation canvas
- ✅ **Component rotation** in 90° increments
- ✅ **Property dialog** (single-click) — shows pin roles, Arduino pin assignment, rotate & delete actions
- ✅ **Pin selector** (double-click) — assign Arduino pins D0-D13 and A0-A5 to component pins
- ✅ **Pin overlay system** — clickable cyan dots on each component pin with hover animation
- ✅ **Keyboard shortcuts** — Delete/Backspace to remove, Escape to cancel
### Part Simulation Behaviors
- ✅ **LED** — pin state drives LED on/off
- ✅ **RGB LED** — digital HIGH/LOW mapped to individual R/G/B channels
- ✅ **Pushbutton** — press/release events inject active-LOW pin state into simulation
- ✅ **Potentiometer** — reads element value (0-1023), converts to voltage, injects into ADC channel
- ✅ **LCD 1602 & LCD 2004** — Full HD44780 controller emulation:
- 4-bit mode protocol (high nibble first, then low nibble)
- DDRAM with proper line address mapping
- Commands: Clear Display, Return Home, Entry Mode Set, Display On/Off, Cursor/Display Shift, Function Set
- Initialization sequence handling
- Enable pin falling-edge detection for data latching
### Wire System
- ✅ **Wire creation** — click a pin to start, click another pin to connect
- ✅ **Real-time preview** — dashed green wire with L-shaped orthogonal routing while creating
- ✅ **Orthogonal wire rendering** — no diagonal paths
- ✅ **Segment-based wire editing** — hover to highlight, drag segments perpendicular to their orientation
- ✅ **Smooth dragging** with `requestAnimationFrame`
- ✅ **8 signal-type wire colors**: Red (VCC), Black (GND), Blue (Analog), Green (Digital), Purple (PWM), Gold (I2C), Orange (SPI), Cyan (USART)
- ✅ **Automatic overlap offset** — parallel wires are offset symmetrically (6px spacing)
- ✅ **Auto-update positions** — wire endpoints recalculate when components move
- ✅ **Grid snapping** (20px grid)
### Example Projects
- ✅ **8 built-in example projects** with full code, components, and wire definitions:
| Example | Category | Difficulty |
|---------|----------|------------|
| Blink LED | Basics | Beginner |
| Traffic Light | Basics | Beginner |
| Button Control | Basics | Beginner |
| Fade LED (PWM) | Basics | Beginner |
| Serial Hello World | Communication | Beginner |
| RGB LED Colors | Basics | Intermediate |
| Simon Says Game | Games | Advanced |
| LCD 20x4 Display | Displays | Intermediate |
- ✅ **Examples gallery** with category and difficulty filters
- ✅ **One-click loading** — loads code, components, and wires into the editor and simulator
### Wokwi Libraries (Local Clones)
- ✅ **wokwi-elements** — 48+ electronic web components (Lit-based Web Components)
- ✅ **avr8js** — AVR8 CPU emulator
- ✅ **rp2040js** — RP2040 emulator (cloned, for future use)
- ✅ **Build-time metadata generation** — TypeScript AST parser reads wokwi-elements source to generate component metadata automatically
## Prerequisites
### 1. Node.js
@ -69,7 +133,8 @@ arduino-cli core install arduino:avr
### 1. Clone the repository
```bash
cd e:\Hardware\wokwi_clon
git clone https://github.com/davidmonterocrespo24/openwokwi.git
cd openwokwi
```
### 2. Setup Backend
@ -123,92 +188,89 @@ The frontend will be available at:
## Usage
1. Open http://localhost:5173 in your browser
2. Write Arduino code in the editor (there's a Blink example by default)
3. Click "Compile" to compile the code
4. If compilation is successful, click "Run" to start the simulation
5. Watch the simulated LED blinking
2. Write Arduino code in the editor (a Blink example is loaded by default)
3. Click **Compile** to compile the code via the backend
4. Click **Run** to start real AVR8 CPU simulation
5. Watch LEDs, LCDs, and other components react in real time
6. Click on components to view properties or assign pin mappings
7. Double-click components to open the pin selector
8. Click pins to create wires connecting components
9. Browse **Examples** to load pre-built projects (Blink, Traffic Light, Simon Says, LCD, etc.)
## Project Structure
```
wokwi_clon/
├── frontend/ # React + Vite
openwokwi/
├── frontend/ # React + Vite + TypeScript
│ ├── src/
│ │ ├── components/ # React components
│ │ │ ├── components-wokwi/ # wokwi-elements wrappers
│ │ │ ├── editor/ # Monaco Editor
│ │ │ └── simulator/ # Simulation canvas
│ │ ├── store/ # Global state (Zustand)
│ │ ├── services/ # API clients
│ │ └── App.tsx # Main component
│ └── package.json
│ │ ├── components/
│ │ │ ├── ComponentPickerModal.tsx # Component search & picker
│ │ │ ├── DynamicComponent.tsx # Generic wokwi component renderer
│ │ │ ├── components-wokwi/ # Legacy React wrappers
│ │ │ ├── editor/ # Monaco Editor + toolbar
│ │ │ ├── examples/ # Examples gallery
│ │ │ └── simulator/ # Canvas, wires, pins, dialogs
│ │ ├── simulation/
│ │ │ ├── AVRSimulator.ts # AVR8 CPU emulator wrapper
│ │ │ ├── PinManager.ts # Pin-to-component mapping
│ │ │ └── parts/ # Part behaviors (LED, LCD, etc.)
│ │ ├── store/ # Zustand state management
│ │ ├── services/ # API clients & ComponentRegistry
│ │ ├── types/ # TypeScript types (wires, components)
│ │ ├── utils/ # Hex parser, wire routing, pin calc
│ │ └── pages/ # EditorPage, ExamplesPage
│ └── public/
│ └── components-metadata.json # Auto-generated component metadata
├── backend/ # FastAPI + Python
│ ├── app/
│ │ ├── api/routes/ # REST endpoints
│ │ ├── services/ # Business logic
│ │ └── main.py # Entry point
│ └── requirements.txt
├── backend/ # FastAPI + Python
│ └── app/
│ ├── main.py # Entry point, CORS config
│ ├── api/routes/compile.py # POST /api/compile, GET /api/compile/boards
│ └── services/arduino_cli.py # arduino-cli subprocess wrapper
├── wokwi-libs/ # Cloned Wokwi repositories
│ ├── wokwi-elements/ # Web Components
│ ├── avr8js/ # AVR8 Emulator
│ ├── rp2040js/ # RP2040 Emulator
│ └── wokwi-features/ # Features and documentation
├── wokwi-libs/ # Cloned Wokwi repositories
│ ├── wokwi-elements/ # 48+ Web Components (Lit)
│ ├── avr8js/ # AVR8 CPU Emulator
│ ├── rp2040js/ # RP2040 Emulator (future)
│ └── wokwi-features/ # Features and documentation
├── README.md
├── WOKWI_LIBS.md # Wokwi integration documentation
└── update-wokwi-libs.bat # Update script
├── scripts/
│ └── generate-component-metadata.ts # AST parser for component discovery
├── ARCHITECTURE.md # Detailed architecture documentation
├── WOKWI_LIBS.md # Wokwi integration documentation
└── update-wokwi-libs.bat # Update local Wokwi libraries
```
## Technologies Used
### Frontend
- **React** 18 - UI framework
- **Vite** 5 - Build tool
- **TypeScript** - Static typing
- **Monaco Editor** - Code editor (VSCode)
- **Zustand** - State management
- **Axios** - HTTP client
- **React** 19 — UI framework
- **Vite** 7 — Build tool with local library aliases
- **TypeScript** 5.9 — Static typing
- **Monaco Editor** — Code editor (VS Code engine)
- **Zustand** 5 — State management
- **React Router** 7 — Client-side routing
- **Axios** — HTTP client
### Backend
- **FastAPI** - Python web framework
- **uvicorn** - ASGI server
- **arduino-cli** - Arduino compiler
- **SQLAlchemy** - ORM (coming soon)
- **SQLite** - Database (coming soon)
- **FastAPI** — Python web framework
- **uvicorn** — ASGI server
- **arduino-cli** — Arduino compiler (subprocess)
### Simulation
- **avr8js** - AVR8 emulator (coming soon)
- **@wokwi/elements** - Electronic components (coming soon)
### Simulation & Components
- **avr8js** — Real AVR8 ATmega328p emulator (local clone)
- **wokwi-elements** — 48+ electronic web components built with Lit (local clone)
- **rp2040js** — RP2040 emulator (local clone, for future use)
## Upcoming Features
## Planned Features
### Phase 2: Real Emulation with avr8js
- [ ] Integrate avr8js for real ATmega328p emulation
- [ ] .hex file parser
- [ ] PinManager for pin management
- [ ] Real-time execution
### Phase 3: Visual Components
- [ ] Integrate @wokwi/elements
- [ ] LED component with real state
- [ ] Resistor component
- [ ] Component drag & drop
- [ ] Visual connections (wires)
### Phase 4: Persistence
- [ ] SQLite database
- [ ] Project CRUD
- [ ] Save/load code and circuit
- [ ] Project history
### Phase 5: Advanced Features
- [ ] More components (buttons, potentiometers, sensors)
- [ ] Serial monitor
- [ ] Simulation speed control
- [ ] Example projects
- [ ] Export/import projects
- 📋 **Serial Monitor** — UI for reading USART output from the simulation
- 📋 **Project Persistence** — Save/load projects with SQLite
- 📋 **Undo/Redo** — Edit history for code and circuit changes
- 📋 **Multi-board Support** — Runtime board switching (Mega, Nano, ESP32)
- 📋 **Wire Validation** — Electrical validation and error highlighting
- 📋 **Export/Import** — Share projects as files
## Update Wokwi Libraries
@ -248,9 +310,17 @@ See [WOKWI_LIBS.md](WOKWI_LIBS.md) for more details about Wokwi integration.
- Make sure Arduino code is valid
- Verify you have the `arduino:avr` core installed
### LED doesn't blink
- Check port listeners are firing (browser console logs)
- Verify pin mapping in the component property dialog
### CPU stuck at PC=0
- Ensure `avrInstruction()` is being called in the execution loop
- Check hex file was loaded correctly
## Contributing
This is an educational project. Suggestions and improvements are welcome!
This is an open-source project. Suggestions, bug reports, and pull requests are welcome!
## License
@ -258,9 +328,9 @@ MIT
## References
- [Wokwi](https://wokwi.com) - Project inspiration
- [avr8js](https://github.com/wokwi/avr8js) - AVR8 emulator
- [wokwi-elements](https://github.com/wokwi/wokwi-elements) - Web components
- [arduino-cli](https://github.com/arduino/arduino-cli) - Arduino compiler
- [Monaco Editor](https://microsoft.github.io/monaco-editor/) - Code editor
- [Wokwi](https://wokwi.com) Project inspiration
- [avr8js](https://github.com/wokwi/avr8js) AVR8 emulator
- [wokwi-elements](https://github.com/wokwi/wokwi-elements) — Electronic web components
- [arduino-cli](https://github.com/arduino/arduino-cli) Arduino compiler
- [Monaco Editor](https://microsoft.github.io/monaco-editor/) Code editor

View File

@ -49,6 +49,7 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
const handleComponentEvent = useSimulatorStore((s) => s.handleComponentEvent);
const running = useSimulatorStore((s) => s.running);
const simulator = useSimulatorStore((s) => s.simulator);
// Check if component is interactive (has simulation logic with attachEvents)
const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]);
@ -182,43 +183,38 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
el.addEventListener('button-press', onButtonPress);
el.addEventListener('button-release', onButtonRelease);
const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]); // Fallback if id is like led-1
const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]);
let cleanupSimulationEvents: (() => void) | undefined;
if (logic && logic.attachEvents) {
// We need AVRSimulator instance. We can grab it from store.
const simulator = useSimulatorStore.getState().simulator;
if (simulator) {
// Helper to find Arduino pin connected to a component pin
const getArduinoPin = (componentPinName: string): number | null => {
const wires = useSimulatorStore.getState().wires.filter(
w => (w.start.componentId === id && w.start.pinName === componentPinName) ||
(w.end.componentId === id && w.end.pinName === componentPinName)
);
if (logic && logic.attachEvents && simulator) {
// Helper to find Arduino pin connected to a component pin
const getArduinoPin = (componentPinName: string): number | null => {
const wires = useSimulatorStore.getState().wires.filter(
w => (w.start.componentId === id && w.start.pinName === componentPinName) ||
(w.end.componentId === id && w.end.pinName === componentPinName)
);
for (const w of wires) {
const arduinoEndpoint = w.start.componentId === 'arduino-uno' ? w.start :
w.end.componentId === 'arduino-uno' ? w.end : null;
if (arduinoEndpoint) {
const pin = parseInt(arduinoEndpoint.pinName, 10);
if (!isNaN(pin)) return pin;
}
for (const w of wires) {
const arduinoEndpoint = w.start.componentId === 'arduino-uno' ? w.start :
w.end.componentId === 'arduino-uno' ? w.end : null;
if (arduinoEndpoint) {
const pin = parseInt(arduinoEndpoint.pinName, 10);
if (!isNaN(pin)) return pin;
}
return null;
};
}
return null;
};
cleanupSimulationEvents = logic.attachEvents(el, simulator, getArduinoPin);
}
cleanupSimulationEvents = logic.attachEvents(el, simulator, getArduinoPin);
}
return () => {
if (cleanupSimulationEvents) cleanupSimulationEvents();
// Old hardcoded events (to be removed in future if Pushbutton registry works fully)
el.removeEventListener('button-press', onButtonPress);
el.removeEventListener('button-release', onButtonRelease);
};
}, [id, handleComponentEvent, metadata.id]);
}, [id, handleComponentEvent, metadata.id, simulator]);
return (
<div

View File

@ -415,7 +415,7 @@ void setup() {
pinMode(BUTTON_PINS[i], INPUT_PULLUP);
}
randomSeed(analogRead(A0));
randomSeed(millis());
newGame();
}

View File

@ -1,4 +1,4 @@
import { CPU, AVRTimer, timer0Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, PinState } from 'avr8js';
import { CPU, AVRTimer, timer0Config, timer1Config, timer2Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, PinState, AVRADC, adcConfig } from 'avr8js';
import { PinManager } from './PinManager';
import { hexToUint8Array } from '../utils/hexParser';
@ -7,18 +7,34 @@ import { hexToUint8Array } from '../utils/hexParser';
*
* Features:
* - CPU emulation at 16MHz
* - Timer0 support
* - Timer0/Timer1/Timer2 support (enables millis(), delay(), PWM)
* - USART support (Serial)
* - GPIO ports (PORTB, PORTC, PORTD)
* - ADC support (analogRead())
* - PWM monitoring via OCR register polling
* - Pin state tracking via PinManager
*/
// OCR register addresses → Arduino pin mapping for PWM
const PWM_PINS = [
{ ocrAddr: 0x47, pin: 6, label: 'OCR0A' }, // Timer0A → D6
{ ocrAddr: 0x48, pin: 5, label: 'OCR0B' }, // Timer0B → D5
{ ocrAddr: 0x88, pin: 9, label: 'OCR1AL' }, // Timer1A low byte → D9
{ ocrAddr: 0x8A, pin: 10, label: 'OCR1BL' }, // Timer1B low byte → D10
{ ocrAddr: 0xB3, pin: 11, label: 'OCR2A' }, // Timer2A → D11
{ ocrAddr: 0xB4, pin: 3, label: 'OCR2B' }, // Timer2B → D3
];
export class AVRSimulator {
private cpu: CPU | null = null;
private timer0: AVRTimer | null = null;
private timer1: AVRTimer | null = null;
private timer2: AVRTimer | null = null;
private usart: AVRUSART | null = null;
private portB: AVRIOPort | null = null;
private portC: AVRIOPort | null = null;
private portD: AVRIOPort | null = null;
private adc: AVRADC | null = null;
private program: Uint16Array | null = null;
private running = false;
private animationFrame: number | null = null;
@ -27,6 +43,7 @@ export class AVRSimulator {
private lastPortBValue = 0;
private lastPortCValue = 0;
private lastPortDValue = 0;
private lastOcrValues: number[] = new Array(PWM_PINS.length).fill(-1);
constructor(pinManager: PinManager) {
this.pinManager = pinManager;
@ -58,17 +75,32 @@ export class AVRSimulator {
// Initialize peripherals
this.timer0 = new AVRTimer(this.cpu, timer0Config);
this.timer1 = new AVRTimer(this.cpu, timer1Config);
this.timer2 = new AVRTimer(this.cpu, timer2Config);
this.usart = new AVRUSART(this.cpu, usart0Config, 16000000); // 16MHz
// Initialize ADC (analogRead support)
this.adc = new AVRADC(this.cpu, adcConfig);
// Initialize IO ports
this.portB = new AVRIOPort(this.cpu, portBConfig);
this.portC = new AVRIOPort(this.cpu, portCConfig);
this.portD = new AVRIOPort(this.cpu, portDConfig);
// Reset OCR tracking
this.lastOcrValues = new Array(PWM_PINS.length).fill(-1);
// Set up pin change hooks
this.setupPinHooks();
console.log('AVR CPU initialized successfully');
console.log('AVR CPU initialized successfully (ADC + Timer1/Timer2 enabled)');
}
/**
* Expose ADC instance so components (potentiometer, etc.) can inject voltages
*/
getADC(): AVRADC | null {
return this.adc;
}
/**
@ -78,17 +110,9 @@ export class AVRSimulator {
if (!this.cpu) return;
console.log('Setting up pin hooks...');
console.log('Initial PORTB:', this.portB);
console.log('Initial PORTC:', this.portC);
console.log('Initial PORTD:', this.portD);
// PORTB (Digital pins 8-13)
// Pin 13 (LED_BUILTIN) = PORTB5
this.portB!.addListener((value, _oldValue) => {
console.log(`[PORTB LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`);
console.log(` Binary: ${value.toString(2).padStart(8, '0')}`);
console.log(` Pin 13 (bit 5) state: ${this.portB!.pinState(5) === PinState.High ? 'HIGH' : 'LOW'}`);
if (value !== this.lastPortBValue) {
this.pinManager.updatePort('PORTB', value, this.lastPortBValue);
this.lastPortBValue = value;
@ -97,8 +121,6 @@ export class AVRSimulator {
// PORTC (Analog pins A0-A5)
this.portC!.addListener((value, _oldValue) => {
console.log(`[PORTC LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`);
if (value !== this.lastPortCValue) {
this.pinManager.updatePort('PORTC', value, this.lastPortCValue);
this.lastPortCValue = value;
@ -107,8 +129,6 @@ export class AVRSimulator {
// PORTD (Digital pins 0-7)
this.portD!.addListener((value, _oldValue) => {
console.log(`[PORTD LISTENER CALLED] register value: 0x${value.toString(16).padStart(2, '0')}`);
if (value !== this.lastPortDValue) {
this.pinManager.updatePort('PORTD', value, this.lastPortDValue);
this.lastPortDValue = value;
@ -118,6 +138,23 @@ export class AVRSimulator {
console.log('Pin hooks configured successfully');
}
/**
* Poll OCR registers and notify PinManager of PWM duty cycle changes
*/
private pollPwmRegisters(): void {
if (!this.cpu) return;
for (let i = 0; i < PWM_PINS.length; i++) {
const { ocrAddr, pin } = PWM_PINS[i];
const ocrValue = this.cpu.data[ocrAddr];
if (ocrValue !== this.lastOcrValues[i]) {
this.lastOcrValues[i] = ocrValue;
const dutyCycle = ocrValue / 255;
this.pinManager.updatePwm(pin, dutyCycle);
}
}
}
/**
* Start simulation loop
*/
@ -129,34 +166,27 @@ export class AVRSimulator {
this.running = true;
console.log('Starting AVR simulation...');
console.log('CPU state:', {
pc: this.cpu.pc,
cycles: this.cpu.cycles,
data: this.cpu.data.slice(0, 10) // First 10 bytes of RAM
});
console.log('Program loaded:', this.program?.length, 'words');
let frameCount = 0;
const execute = (timestamp: number) => {
const execute = (_timestamp: number) => {
if (!this.running || !this.cpu) return;
// Execute instructions in batches for performance
// ATmega328p @ 16MHz = 16M cycles/sec
// At 60fps: 16,000,000 / 60 ≈ 267,000 cycles per frame
const cyclesPerFrame = Math.floor(267000 * this.speed);
try {
for (let i = 0; i < cyclesPerFrame; i++) {
// Execute one AVR instruction and update peripherals
avrInstruction(this.cpu); // CRITICAL: Execute the actual instruction
avrInstruction(this.cpu); // Execute the AVR instruction
this.cpu.tick(); // Update peripheral timers and cycles
}
// Log every 60 frames (once per second at 60fps)
// Poll PWM registers every frame
this.pollPwmRegisters();
frameCount++;
if (frameCount % 60 === 0) {
console.log(`[CPU] Frame ${frameCount}, PC: ${this.cpu.pc}, Cycles: ${this.cpu.cycles}`);
console.log(`[CPU] PORTB register value: 0x${this.cpu.data[0x25].toString(16).padStart(2, '0')}`);
}
} catch (error) {
console.error('Simulation error:', error);
@ -164,7 +194,6 @@ export class AVRSimulator {
return;
}
// Schedule next frame
this.animationFrame = requestAnimationFrame(execute);
};
@ -195,20 +224,21 @@ export class AVRSimulator {
if (this.cpu && this.program) {
console.log('Resetting AVR CPU...');
// Reinitialize CPU
this.cpu = new CPU(this.program);
this.timer0 = new AVRTimer(this.cpu, timer0Config);
this.timer1 = new AVRTimer(this.cpu, timer1Config);
this.timer2 = new AVRTimer(this.cpu, timer2Config);
this.usart = new AVRUSART(this.cpu, usart0Config, 16000000);
this.adc = new AVRADC(this.cpu, adcConfig);
// Reinitialize ports
this.portB = new AVRIOPort(this.cpu, portBConfig);
this.portC = new AVRIOPort(this.cpu, portCConfig);
this.portD = new AVRIOPort(this.cpu, portDConfig);
// Reset port values
this.lastPortBValue = 0;
this.lastPortCValue = 0;
this.lastPortDValue = 0;
this.lastOcrValues = new Array(PWM_PINS.length).fill(-1);
this.setupPinHooks();
@ -216,36 +246,23 @@ export class AVRSimulator {
}
}
/**
* Check if simulator is running
*/
isRunning(): boolean {
return this.running;
}
/**
* Set simulation speed (1.0 = normal, 0.5 = half speed, 2.0 = double speed)
*/
setSpeed(speed: number): void {
this.speed = Math.max(0.1, Math.min(10.0, speed));
console.log(`Simulation speed set to ${this.speed}x`);
}
/**
* Get current simulation speed
*/
getSpeed(): number {
return this.speed;
}
/**
* Execute a single instruction (for step-by-step debugging)
*/
step(): void {
if (!this.cpu) return;
avrInstruction(this.cpu); // Execute the instruction
this.cpu.tick(); // Update peripherals
avrInstruction(this.cpu);
this.cpu.tick();
}
/**

View File

@ -5,44 +5,50 @@
* - PORTB (0x25) Digital pins 8-13
* - PORTC (0x28) Analog pins A0-A5 (14-19)
* - PORTD (0x2B) Digital pins 0-7
*
* Also supports:
* - Analog voltage injection (for potentiometers, sensors)
* - PWM duty cycle tracking (for servos, RGB LEDs, buzzers)
*/
export type PinState = boolean;
export type PinChangeCallback = (pin: number, state: PinState) => void;
export type AnalogCallback = (pin: number, voltage: number) => void;
export type PwmCallback = (pin: number, dutyCycle: number) => void;
export class PinManager {
private listeners: Map<number, Set<PinChangeCallback>> = new Map();
private pwmListeners: Map<number, Set<PwmCallback>> = new Map();
private analogListeners: Map<number, Set<AnalogCallback>> = new Map();
private pinStates: Map<number, boolean> = new Map();
private pwmValues: Map<number, number> = new Map();
// ── Digital pin API ──────────────────────────────────────────────────────
/**
* Register callback for pin state changes
* Returns unsubscribe function
* Note: Does NOT call callback immediately to avoid infinite loops
* Register callback for digital pin state changes.
* Returns unsubscribe function.
*/
onPinChange(arduinoPin: number, callback: PinChangeCallback): () => void {
if (!this.listeners.has(arduinoPin)) {
this.listeners.set(arduinoPin, new Set());
}
this.listeners.get(arduinoPin)!.add(callback);
// Return unsubscribe function
return () => {
this.listeners.get(arduinoPin)?.delete(callback);
};
}
/**
* Update port register and notify listeners
* Update port register and notify digital pin listeners.
*/
updatePort(portName: 'PORTB' | 'PORTC' | 'PORTD', newValue: number, oldValue: number = 0) {
// Map AVR ports to Arduino pin numbers
const pinOffset = {
'PORTB': 8, // PORTB0-7 → Arduino D8-D13 (only D8-D13 are used)
'PORTC': 14, // PORTC0-5 → Arduino A0-A5 (14-19)
'PORTD': 0, // PORTD0-7 → Arduino D0-D7
'PORTB': 8,
'PORTC': 14,
'PORTD': 0,
}[portName];
// Check each bit
for (let bit = 0; bit < 8; bit++) {
const mask = 1 << bit;
const oldState = (oldValue & mask) !== 0;
@ -50,11 +56,8 @@ export class PinManager {
if (oldState !== newState) {
const arduinoPin = pinOffset + bit;
// Update internal state
this.pinStates.set(arduinoPin, newState);
// Notify listeners
const callbacks = this.listeners.get(arduinoPin);
if (callbacks) {
callbacks.forEach(cb => cb(arduinoPin, newState));
@ -65,26 +68,78 @@ export class PinManager {
}
}
/**
* Get current state of a pin
*/
getPinState(arduinoPin: number): boolean {
return this.pinStates.get(arduinoPin) || false;
}
// ── PWM duty cycle API ───────────────────────────────────────────────────
/**
* Get all listeners count (for debugging)
* Register callback for PWM duty cycle changes on a pin.
* dutyCycle is 0.01.0.
*/
onPwmChange(pin: number, callback: PwmCallback): () => void {
if (!this.pwmListeners.has(pin)) {
this.pwmListeners.set(pin, new Set());
}
this.pwmListeners.get(pin)!.add(callback);
return () => {
this.pwmListeners.get(pin)?.delete(callback);
};
}
/**
* Called by AVRSimulator each frame when an OCR register changes.
*/
updatePwm(pin: number, dutyCycle: number): void {
this.pwmValues.set(pin, dutyCycle);
const callbacks = this.pwmListeners.get(pin);
if (callbacks) {
callbacks.forEach(cb => cb(pin, dutyCycle));
}
}
getPwmValue(pin: number): number {
return this.pwmValues.get(pin) ?? 0;
}
// ── Analog voltage API ───────────────────────────────────────────────────
/**
* Register callback when external code sets an analog voltage on a pin.
*/
onAnalogChange(pin: number, callback: AnalogCallback): () => void {
if (!this.analogListeners.has(pin)) {
this.analogListeners.set(pin, new Set());
}
this.analogListeners.get(pin)!.add(callback);
return () => {
this.analogListeners.get(pin)?.delete(callback);
};
}
/**
* Inject a simulated analog voltage (05V) on an Arduino pin.
* Notifies any registered analog listeners.
*/
setAnalogVoltage(arduinoPin: number, voltage: number): void {
const callbacks = this.analogListeners.get(arduinoPin);
if (callbacks) {
callbacks.forEach(cb => cb(arduinoPin, voltage));
}
}
// ── Utility ──────────────────────────────────────────────────────────────
getListenersCount(): number {
let count = 0;
this.listeners.forEach(set => count += set.size);
return count;
}
/**
* Clear all listeners
*/
clearAllListeners() {
this.listeners.clear();
this.pwmListeners.clear();
this.analogListeners.clear();
}
}

View File

@ -2,34 +2,27 @@ import { PartSimulationRegistry } from './PartSimulationRegistry';
import type { AVRSimulator } from '../AVRSimulator';
/**
* Basic Pushbutton implementation
* Basic Pushbutton implementation (full-size)
*/
PartSimulationRegistry.register('pushbutton', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
// 1. Find which Arduino pin is connected to terminal '1.L' or '2.L'
const arduinoPin =
getArduinoPinHelper('1.l') ?? getArduinoPinHelper('2.l') ??
getArduinoPinHelper('1.r') ?? getArduinoPinHelper('2.r');
if (arduinoPin === null) {
return () => { }; // no-op if unconnected
}
if (arduinoPin === null) return () => { };
const onButtonPress = () => {
// By default wokwi pushbuttons are active LOW (connected to GND)
avrSimulator.setPinState(arduinoPin, false);
avrSimulator.setPinState(arduinoPin, false); // Active LOW
(element as any).pressed = true;
};
const onButtonRelease = () => {
// Release lets the internal pull-up pull it HIGH
avrSimulator.setPinState(arduinoPin, true);
(element as any).pressed = false;
};
element.addEventListener('button-press', onButtonPress);
element.addEventListener('button-release', onButtonRelease);
return () => {
element.removeEventListener('button-press', onButtonPress);
element.removeEventListener('button-release', onButtonRelease);
@ -37,6 +30,101 @@ PartSimulationRegistry.register('pushbutton', {
},
});
/**
* 6mm Pushbutton same behaviour as the full-size pushbutton
*/
PartSimulationRegistry.register('pushbutton-6mm', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const arduinoPin =
getArduinoPinHelper('1.l') ?? getArduinoPinHelper('2.l') ??
getArduinoPinHelper('1.r') ?? getArduinoPinHelper('2.r');
if (arduinoPin === null) return () => { };
const onPress = () => {
avrSimulator.setPinState(arduinoPin, false);
(element as any).pressed = true;
};
const onRelease = () => {
avrSimulator.setPinState(arduinoPin, true);
(element as any).pressed = false;
};
element.addEventListener('button-press', onPress);
element.addEventListener('button-release', onRelease);
return () => {
element.removeEventListener('button-press', onPress);
element.removeEventListener('button-release', onRelease);
};
},
});
/**
* Slide Switch toggles between HIGH and LOW on each click
*/
PartSimulationRegistry.register('slide-switch', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
// Slide switch has pins: 1, 2, 3 — middle pin (2) is the common output
const arduinoPin = getArduinoPinHelper('2') ?? getArduinoPinHelper('1');
if (arduinoPin === null) return () => { };
// Read initial value from element (0 or 1)
let state = (element as any).value === 1;
avrSimulator.setPinState(arduinoPin, state);
const onChange = () => {
state = (element as any).value === 1;
avrSimulator.setPinState(arduinoPin, state);
console.log(`[SlideSwitch] pin ${arduinoPin}${state ? 'HIGH' : 'LOW'}`);
};
element.addEventListener('change', onChange);
// The slide-switch element fires a 'change' event when clicked
element.addEventListener('input', onChange);
return () => {
element.removeEventListener('change', onChange);
element.removeEventListener('input', onChange);
};
},
});
/**
* DIP Switch 8 8 independent toggle switches
* Pin layout: 1A-8A on one side, 1B-8B on the other
*/
PartSimulationRegistry.register('dip-switch-8', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
// Each switch i has pins (i+1)A and (i+1)B; we use the A side as output
const pins: (number | null)[] = [];
for (let i = 1; i <= 8; i++) {
pins.push(getArduinoPinHelper(`${i}A`) ?? getArduinoPinHelper(`${i}a`));
}
// Sync initial states
const values: number[] = (element as any).values || new Array(8).fill(0);
pins.forEach((pin, i) => {
if (pin !== null) avrSimulator.setPinState(pin, values[i] === 1);
});
const onChange = () => {
const newValues: number[] = (element as any).values || new Array(8).fill(0);
pins.forEach((pin, i) => {
if (pin !== null) {
const state = newValues[i] === 1;
avrSimulator.setPinState(pin, state);
}
});
};
element.addEventListener('change', onChange);
element.addEventListener('input', onChange);
return () => {
element.removeEventListener('change', onChange);
element.removeEventListener('input', onChange);
};
},
});
/**
* Basic LED implementation
*/
@ -45,6 +133,65 @@ PartSimulationRegistry.register('led', {
if (pinName === 'A') { // Anode
(element as any).value = state;
}
// We ignore cathode 'C' in this simple digital model
}
});
/**
* LED Bar Graph 10 LEDs, each driven by one pin
* Wokwi pin names: A1-A10
*/
PartSimulationRegistry.register('led-bar-graph', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const pinManager = (avrSimulator as any).pinManager;
if (!pinManager) return () => { };
const values = new Array(10).fill(0);
const unsubscribers: (() => void)[] = [];
for (let i = 1; i <= 10; i++) {
const pin = getArduinoPinHelper(`A${i}`);
if (pin !== null) {
const idx = i - 1;
unsubscribers.push(
pinManager.onPinChange(pin, (_p: number, state: boolean) => {
values[idx] = state ? 1 : 0;
(element as any).values = [...values];
})
);
}
}
return () => unsubscribers.forEach(u => u());
},
});
/**
* 7-Segment Display
* Pins: A, B, C, D, E, F, G, DP (common cathode segments light when HIGH)
* The wokwi-7segment 'values' property is an array of 8 values (A B C D E F G DP)
*/
PartSimulationRegistry.register('7segment', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const pinManager = (avrSimulator as any).pinManager;
if (!pinManager) return () => { };
// Order matches wokwi-elements values array: [A, B, C, D, E, F, G, DP]
const segmentPinNames = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'DP'];
const values = new Array(8).fill(0);
const unsubscribers: (() => void)[] = [];
segmentPinNames.forEach((segName, idx) => {
const pin = getArduinoPinHelper(segName);
if (pin !== null) {
unsubscribers.push(
pinManager.onPinChange(pin, (_p: number, state: boolean) => {
values[idx] = state ? 1 : 0;
(element as any).values = [...values];
})
);
}
});
return () => unsubscribers.forEach(u => u());
},
});

View File

@ -1,96 +1,423 @@
import { PartSimulationRegistry } from './PartSimulationRegistry';
import type { AVRSimulator } from '../AVRSimulator';
// ─── Helpers ────────────────────────────────────────────────────────────────
/** Read the ADC instance from the simulator (returns null if not initialized) */
function getADC(avrSimulator: AVRSimulator): any | null {
return (avrSimulator as any).getADC?.() ?? null;
}
/** Write an analog voltage (0-5V) to an ADC channel derived from an Arduino pin (A0-A5 = 14-19) */
function setAdcVoltage(avrSimulator: AVRSimulator, arduinoPin: number, voltage: number): boolean {
if (arduinoPin < 14 || arduinoPin > 19) return false;
const channel = arduinoPin - 14;
const adc = getADC(avrSimulator);
if (!adc) return false;
adc.channelValues[channel] = voltage;
return true;
}
// ─── RGB LED (PWM-aware) ─────────────────────────────────────────────────────
/**
* RGB LED implementation
* Translates digital HIGH/LOW to corresponding 255/0 values on RGB channels
* RGB LED implementation supports both digital and PWM (analogWrite) output.
* Falls back to digital mode if no PWM is detected.
*/
PartSimulationRegistry.register('rgb-led', {
onPinStateChange: (pinName: string, state: boolean, element: HTMLElement) => {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const pinManager = (avrSimulator as any).pinManager;
if (!pinManager) return () => { };
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;
const unsubscribers: (() => void)[] = [];
const pinR = getArduinoPinHelper('R');
const pinG = getArduinoPinHelper('G');
const pinB = getArduinoPinHelper('B');
// Digital fallback
if (pinR !== null) {
unsubscribers.push(pinManager.onPinChange(pinR, (_: number, state: boolean) => {
el.ledRed = state ? 255 : 0;
}));
}
}
if (pinG !== null) {
unsubscribers.push(pinManager.onPinChange(pinG, (_: number, state: boolean) => {
el.ledGreen = state ? 255 : 0;
}));
}
if (pinB !== null) {
unsubscribers.push(pinManager.onPinChange(pinB, (_: number, state: boolean) => {
el.ledBlue = state ? 255 : 0;
}));
}
// PWM override — when analogWrite() is used the OCR value supersedes digital
const pwmPins = [
{ pin: pinR, prop: 'ledRed' },
{ pin: pinG, prop: 'ledGreen' },
{ pin: pinB, prop: 'ledBlue' },
];
for (const { pin, prop } of pwmPins) {
if (pin !== null) {
unsubscribers.push(pinManager.onPwmChange(pin, (_: number, dc: number) => {
el[prop] = Math.round(dc * 255);
}));
}
}
return () => unsubscribers.forEach(u => u());
},
});
/**
* Analog Potentiometer implementation
*/
// ─── Potentiometer (rotary) ──────────────────────────────────────────────────
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
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const arduinoPin = getArduinoPinHelper('SIG');
if (arduinoPin === null) return () => { };
// 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;
}
const onInput = () => {
const raw = parseInt((element as any).value || '0', 10);
const volts = (raw / 1023.0) * 5.0;
if (!setAdcVoltage(avrSimulator, arduinoPin, volts)) {
console.warn('[Potentiometer] ADC not available — is AVRADC initialized?');
}
};
element.addEventListener('input', onInput);
return () => element.removeEventListener('input', onInput);
},
});
// ─── Slide Potentiometer ─────────────────────────────────────────────────────
PartSimulationRegistry.register('slide-potentiometer', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const arduinoPin = getArduinoPinHelper('SIG') ?? getArduinoPinHelper('OUT');
if (arduinoPin === null) return () => { };
const el = element as any;
const onInput = () => {
const min = el.min ?? 0;
const max = el.max ?? 1023;
const value = el.value ?? 0;
const normalized = (value - min) / (max - min || 1);
const volts = normalized * 5.0;
setAdcVoltage(avrSimulator, arduinoPin, volts);
};
element.addEventListener('input', onInput);
return () => element.removeEventListener('input', onInput);
},
});
// ─── Photoresistor Sensor ────────────────────────────────────────────────────
/**
* Photoresistor sensor the wokwi element does not emit input events,
* so we simulate light level with a slider drawn via the component's
* luminance property when available, or simply set a mid-range voltage.
*
* The element exposes `ledDO` and `ledPower` for display only.
* We inject a static mid-range voltage on the AO pin so analogRead()
* returns a valid value. Users can modify the element's `value` attribute.
*/
PartSimulationRegistry.register('photoresistor-sensor', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const pinAO = getArduinoPinHelper('AO') ?? getArduinoPinHelper('A0');
const pinDO = getArduinoPinHelper('DO') ?? getArduinoPinHelper('D0');
const pinManager = (avrSimulator as any).pinManager;
const unsubscribers: (() => void)[] = [];
// Inject initial mid-range voltage (simulate moderate light)
if (pinAO !== null) {
setAdcVoltage(avrSimulator, pinAO, 2.5);
}
// Watch element's 'input' events in case the element supports it
const onInput = () => {
const val = (element as any).value;
if (val !== undefined && pinAO !== null) {
const volts = (val / 1023.0) * 5.0;
setAdcVoltage(avrSimulator, pinAO, volts);
}
};
element.addEventListener('input', onInput);
unsubscribers.push(() => element.removeEventListener('input', onInput));
// DO (digital output) — if connected, update element's LED indicator
if (pinDO !== null && pinManager) {
unsubscribers.push(pinManager.onPinChange(pinDO, (_: number, state: boolean) => {
(element as any).ledDO = state;
}));
}
return () => unsubscribers.forEach(u => u());
},
});
// ─── Analog Joystick ─────────────────────────────────────────────────────────
/**
* Analog Joystick two axes (xValue/yValue 0-1023) + button press
* Wokwi pins: VRX (X axis), VRY (Y axis), SW (button)
*/
PartSimulationRegistry.register('analog-joystick', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const pinX = getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT');
const pinY = getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT');
const pinSW = getArduinoPinHelper('SW');
const el = element as any;
// Center position is mid-range (~2.5V)
if (pinX !== null) setAdcVoltage(avrSimulator, pinX, 2.5);
if (pinY !== null) setAdcVoltage(avrSimulator, pinY, 2.5);
const onMove = () => {
// xValue / yValue are 0-1023
if (pinX !== null) {
const vx = ((el.xValue ?? 512) / 1023.0) * 5.0;
setAdcVoltage(avrSimulator, pinX, vx);
}
if (pinY !== null) {
const vy = ((el.yValue ?? 512) / 1023.0) * 5.0;
setAdcVoltage(avrSimulator, pinY, vy);
}
};
const onPress = () => {
if (pinSW !== null) avrSimulator.setPinState(pinSW, false); // Active LOW
el.pressed = true;
};
const onRelease = () => {
if (pinSW !== null) avrSimulator.setPinState(pinSW, true);
el.pressed = false;
};
element.addEventListener('input', onMove);
element.addEventListener('joystick-move', onMove);
element.addEventListener('button-press', onPress);
element.addEventListener('button-release', onRelease);
return () => {
element.removeEventListener('input', onInput);
element.removeEventListener('input', onMove);
element.removeEventListener('joystick-move', onMove);
element.removeEventListener('button-press', onPress);
element.removeEventListener('button-release', onRelease);
};
},
});
// ─── Servo ───────────────────────────────────────────────────────────────────
/**
* 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 (HIGHLOW)
* 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)
* Servo motor reads OCR1A and ICR1 to calculate pulse width and angle.
*
* Standard RC servo protocol:
* - 50 Hz signal (20 ms period)
* - Pulse width 1 ms 0°, 1.5 ms 90°, 2 ms 180°
*
* With Timer1, prescaler=8, F_CPU=16MHz:
* - ICR1 = 20000 for 50Hz
* - OCR1A = 1000 0°, 1500 90°, 2000 180°
*
* We poll these registers every animation frame via a requestAnimationFrame loop.
*/
PartSimulationRegistry.register('servo', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const pinSIG = getArduinoPinHelper('PWM') ?? getArduinoPinHelper('SIG') ?? getArduinoPinHelper('1');
const el = element as any;
// OCR1A low byte = 0x88, OCR1A high byte = 0x89
// ICR1L = 0x86, ICR1H = 0x87
const OCR1AL = 0x88;
const OCR1AH = 0x89;
const ICR1L = 0x86;
const ICR1H = 0x87;
let rafId: number | null = null;
let lastOcr1a = -1;
const poll = () => {
const cpu = (avrSimulator as any).cpu;
if (!cpu) { rafId = requestAnimationFrame(poll); return; }
const ocr1a = cpu.data[OCR1AL] | (cpu.data[OCR1AH] << 8);
if (ocr1a !== lastOcr1a) {
lastOcr1a = ocr1a;
const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8);
// Calculate pulse width in microseconds
// prescaler 8, F_CPU 16MHz → 1 tick = 0.5µs
// pulse_us = ocr1a * 0.5
// But also handle prescaler 64 (1 tick = 4µs) and default ICR1 detection
let pulseUs: number;
if (icr1 > 0) {
// Proportional to ICR1 period (assume 20ms period)
pulseUs = 1000 + (ocr1a / icr1) * 1000;
} else {
// Fallback: prescaler 8
pulseUs = ocr1a * 0.5;
}
// Clamp to 1000-2000µs and map to 0-180°
const clamped = Math.max(1000, Math.min(2000, pulseUs));
const angle = Math.round(((clamped - 1000) / 1000) * 180);
el.angle = angle;
}
// Also support PWM duty cycle approach via PinManager
if (pinSIG !== null) {
const pinManager = (avrSimulator as any).pinManager;
// Only override angle if cpu-based approach doesn't work
// (ICR1 = 0 means Timer1 not configured as servo)
const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8);
if (icr1 === 0 && pinManager) {
const dc = pinManager.getPwmValue(pinSIG);
if (dc > 0) {
el.angle = Math.round(dc * 180);
}
}
}
rafId = requestAnimationFrame(poll);
};
rafId = requestAnimationFrame(poll);
return () => {
if (rafId !== null) cancelAnimationFrame(rafId);
};
},
});
// ─── Buzzer ──────────────────────────────────────────────────────────────────
/**
* Buzzer uses Web Audio API to generate a tone.
*
* Reads OCR2A (Timer2 CTC mode) to determine frequency:
* f = F_CPU / (2 × prescaler × (OCR2A + 1))
*
* Prescaler detected from TCCR2B[2:0] bits.
* Activates when duty cycle > 0 (pin is driven HIGH).
*/
PartSimulationRegistry.register('buzzer', {
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
const pinSIG = getArduinoPinHelper('1') ?? getArduinoPinHelper('+') ?? getArduinoPinHelper('POS');
const pinManager = (avrSimulator as any).pinManager;
let audioCtx: AudioContext | null = null;
let oscillator: OscillatorNode | null = null;
let gainNode: GainNode | null = null;
let isSounding = false;
const el = element as any;
// Timer2 register addresses
const OCR2A = 0xB3;
const TCCR2B = 0xB1;
const F_CPU = 16_000_000;
const prescalerTable: Record<number, number> = {
1: 1, 2: 8, 3: 32, 4: 64, 5: 128, 6: 256, 7: 1024,
};
function getFrequency(cpu: any): number {
const ocr2a = cpu.data[OCR2A] ?? 0;
const tccr2b = cpu.data[TCCR2B] ?? 0;
const csField = tccr2b & 0x07;
const prescaler = prescalerTable[csField] ?? 64;
// CTC mode: f = F_CPU / (2 × prescaler × (OCR2A + 1))
return F_CPU / (2 * prescaler * (ocr2a + 1));
}
function startTone(freq: number) {
if (!audioCtx) {
audioCtx = new AudioContext();
gainNode = audioCtx.createGain();
gainNode.gain.value = 0.1;
gainNode.connect(audioCtx.destination);
}
if (oscillator) {
oscillator.frequency.setTargetAtTime(freq, audioCtx.currentTime, 0.01);
return;
}
oscillator = audioCtx.createOscillator();
oscillator.type = 'square';
oscillator.frequency.value = freq;
oscillator.connect(gainNode!);
oscillator.start();
isSounding = true;
if (el.playing !== undefined) el.playing = true;
}
function stopTone() {
if (oscillator) {
oscillator.stop();
oscillator.disconnect();
oscillator = null;
}
isSounding = false;
if (el.playing !== undefined) el.playing = false;
}
// Poll via PWM duty cycle on the buzzer pin
const unsubscribers: (() => void)[] = [];
if (pinSIG !== null && pinManager) {
unsubscribers.push(pinManager.onPwmChange(pinSIG, (_: number, dc: number) => {
const cpu = (avrSimulator as any).cpu;
if (dc > 0) {
const freq = cpu ? getFrequency(cpu) : 440;
startTone(Math.max(20, Math.min(20000, freq)));
} else {
stopTone();
}
}));
// Also respond to digital HIGH/LOW (tone() toggles the pin)
unsubscribers.push(pinManager.onPinChange(pinSIG, (_: number, state: boolean) => {
if (!isSounding && state) {
const cpu = (avrSimulator as any).cpu;
const freq = cpu ? getFrequency(cpu) : 440;
startTone(Math.max(20, Math.min(20000, freq)));
} else if (isSounding && !state) {
// Don't stop on every LOW — tone() generates a square wave
// We stop only when duty cycle drops to 0 via onPwmChange
}
}));
}
return () => {
stopTone();
if (audioCtx) { audioCtx.close(); audioCtx = null; }
unsubscribers.forEach(u => u());
};
},
});
// ─── LCD 1602 / 2004 ─────────────────────────────────────────────────────────
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
const ddram = new Uint8Array(128).fill(0x20);
let ddramAddress = 0;
let entryIncrement = true;
let displayOn = true;
let cursorOn = false;
let blinkOn = false;
let nibbleState: 'high' | 'low' = 'high';
let highNibble = 0;
let initialized = false;
let initCount = 0;
// Pin states tracked locally
let rsState = false;
let eState = false;
let d4State = false;
@ -98,12 +425,10 @@ function createLcdSimulation(cols: number, rows: number) {
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
? [0x00, 0x40, 0x14, 0x54]
: [0x00, 0x40];
// 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];
@ -111,17 +436,14 @@ function createLcdSimulation(cols: number, rows: number) {
return row * cols + (addr - offset);
}
}
return -1; // Address not visible
return -1;
}
// 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];
@ -132,8 +454,6 @@ function createLcdSimulation(cols: number, rows: number) {
el.characters = chars;
el.cursor = cursorOn;
el.blink = blinkOn;
// Set cursor position
const cursorLinear = ddramToLinear(ddramAddress);
if (cursorLinear >= 0) {
el.cursorX = cursorLinear % cols;
@ -141,169 +461,96 @@ function createLcdSimulation(cols: number, rows: number) {
}
}
// 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
// CGRAM — not implemented
} 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;
}
if (!sc) { ddramAddress = (ddramAddress + (rl ? 1 : -1)) & 0x7F; }
} else if (data & 0x08) {
// Display On/Off Control
displayOn = !!(data & 0x04);
cursorOn = !!(data & 0x02);
blinkOn = !!(data & 0x01);
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;
}
ddramAddress = entryIncrement
? (ddramAddress + 1) & 0x7F
: (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';
}
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;
processByte(rsState, highNibble | nibble);
nibbleState = 'high';
processByte(rsState, fullByte);
}
}
// Get Arduino pin numbers for LCD pins
const pinRS = getArduinoPinHelper('RS');
const pinE = getArduinoPinHelper('E');
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 () => { };
}
if (!pinManager) return () => { };
const unsubscribers: (() => void)[] = [];
if (pinRS !== null) {
unsubscribers.push(pinManager.onPinChange(pinRS, (_p: number, state: boolean) => {
rsState = state;
}));
}
if (pinRS !== null) unsubscribers.push(pinManager.onPinChange(pinRS, (_: number, s: boolean) => { rsState = s; }));
if (pinD4 !== null) unsubscribers.push(pinManager.onPinChange(pinD4, (_: number, s: boolean) => { d4State = s; }));
if (pinD5 !== null) unsubscribers.push(pinManager.onPinChange(pinD5, (_: number, s: boolean) => { d5State = s; }));
if (pinD6 !== null) unsubscribers.push(pinManager.onPinChange(pinD6, (_: number, s: boolean) => { d6State = s; }));
if (pinD7 !== null) unsubscribers.push(pinManager.onPinChange(pinD7, (_: number, s: boolean) => { d7State = s; }));
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) => {
unsubscribers.push(pinManager.onPinChange(pinE, (_: number, s: boolean) => {
const wasHigh = eState;
eState = state;
// Falling edge: data is latched
if (wasHigh && !state) {
onEnableFallingEdge();
}
eState = s;
if (wasHigh && !s) 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));