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
parent
c2f07665b4
commit
5ca8a82985
248
README.md
248
README.md
|
|
@ -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
|
||||
|
||||

|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ void setup() {
|
|||
pinMode(BUTTON_PINS[i], INPUT_PULLUP);
|
||||
}
|
||||
|
||||
randomSeed(analogRead(A0));
|
||||
randomSeed(millis());
|
||||
newGame();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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.0–1.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 (0–5V) 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (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)
|
||||
* 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));
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue