feat: add ILI9341 TFT display simulation and enhance component registry loading
parent
da47f69cb2
commit
efd4c11e03
|
|
@ -214,7 +214,7 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
el.removeEventListener('button-press', onButtonPress);
|
||||
el.removeEventListener('button-release', onButtonRelease);
|
||||
};
|
||||
}, [id, handleComponentEvent, metadata.id, simulator]);
|
||||
}, [id, handleComponentEvent, metadata.id, simulator, running]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ export const SimulatorCanvas = () => {
|
|||
// Component picker modal
|
||||
const [showComponentPicker, setShowComponentPicker] = useState(false);
|
||||
const [registry] = useState(() => ComponentRegistry.getInstance());
|
||||
const [registryLoaded, setRegistryLoaded] = useState(registry.isLoaded);
|
||||
|
||||
// Wait for registry to finish loading before rendering components
|
||||
useEffect(() => {
|
||||
if (!registryLoaded) {
|
||||
registry.loadPromise.then(() => setRegistryLoaded(true));
|
||||
}
|
||||
}, [registry, registryLoaded]);
|
||||
|
||||
// Component selection
|
||||
const [selectedComponentId, setSelectedComponentId] = useState<string | null>(null);
|
||||
|
|
@ -454,7 +462,7 @@ export const SimulatorCanvas = () => {
|
|||
/>
|
||||
|
||||
{/* Components using wokwi-elements */}
|
||||
<div className="components-area">{components.map(renderComponent)}</div>
|
||||
<div className="components-area">{registryLoaded && components.map(renderComponent)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -593,6 +593,113 @@ void loop() {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tft-display',
|
||||
title: 'TFT ILI9341 Display',
|
||||
description: 'Color TFT display demo: fills, text, and a bouncing ball animation using the Arduino TFT library',
|
||||
category: 'displays',
|
||||
difficulty: 'intermediate',
|
||||
code: `// TFT ILI9341 Display Demo
|
||||
// Library: TFT by Arduino, Adafruit
|
||||
// Connect: CS=10, DC/RS=9, RST=8, LED=7, MOSI=11(SPI), SCK=13(SPI)
|
||||
|
||||
#include <TFT.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#define TFT_CS 10
|
||||
#define TFT_DC 9
|
||||
#define TFT_RST 8
|
||||
#define TFT_LED 7
|
||||
|
||||
TFT TFTscreen = TFT(TFT_CS, TFT_DC, TFT_RST);
|
||||
|
||||
int ballX = 64, ballY = 90;
|
||||
int ballDX = 3, ballDY = 2;
|
||||
const int BALL_R = 8;
|
||||
|
||||
void drawStaticUI() {
|
||||
// Background
|
||||
TFTscreen.background(0, 0, 50);
|
||||
|
||||
// Title
|
||||
TFTscreen.setTextSize(2);
|
||||
TFTscreen.stroke(255, 220, 0);
|
||||
TFTscreen.text("WOKWI TFT", 5, 5);
|
||||
|
||||
// Subtitle
|
||||
TFTscreen.setTextSize(1);
|
||||
TFTscreen.stroke(180, 180, 255);
|
||||
TFTscreen.text("ILI9341 Demo", 12, 28);
|
||||
|
||||
// Color palette bars
|
||||
TFTscreen.noStroke();
|
||||
TFTscreen.fill(220, 50, 50);
|
||||
TFTscreen.rect(5, 45, 36, 14);
|
||||
TFTscreen.fill(50, 200, 50);
|
||||
TFTscreen.rect(45, 45, 36, 14);
|
||||
TFTscreen.fill(50, 100, 240);
|
||||
TFTscreen.rect(85, 45, 36, 14);
|
||||
|
||||
// Play-field border
|
||||
TFTscreen.stroke(80, 80, 130);
|
||||
TFTscreen.noFill();
|
||||
TFTscreen.rect(2, 68, 124, 88);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
pinMode(TFT_LED, OUTPUT);
|
||||
digitalWrite(TFT_LED, HIGH); // Backlight on
|
||||
|
||||
TFTscreen.begin();
|
||||
drawStaticUI();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Erase old ball
|
||||
TFTscreen.noStroke();
|
||||
TFTscreen.fill(0, 0, 50);
|
||||
TFTscreen.circle(ballX, ballY, BALL_R);
|
||||
|
||||
// Update position
|
||||
ballX += ballDX;
|
||||
ballY += ballDY;
|
||||
|
||||
// Bounce off field borders
|
||||
if (ballX < 3 + BALL_R || ballX > 124 - BALL_R) ballDX = -ballDX;
|
||||
if (ballY < 69 + BALL_R || ballY > 155 - BALL_R) ballDY = -ballDY;
|
||||
|
||||
// Draw ball
|
||||
TFTscreen.fill(255, 140, 0);
|
||||
TFTscreen.circle(ballX, ballY, BALL_R);
|
||||
|
||||
delay(30);
|
||||
}
|
||||
`,
|
||||
components: [
|
||||
{
|
||||
type: 'wokwi-arduino-uno',
|
||||
id: 'arduino-uno',
|
||||
x: 80,
|
||||
y: 220,
|
||||
properties: {},
|
||||
},
|
||||
{
|
||||
type: 'wokwi-ili9341',
|
||||
id: 'tft1',
|
||||
x: 480,
|
||||
y: 60,
|
||||
properties: {},
|
||||
},
|
||||
],
|
||||
wires: [
|
||||
{ id: 'w-sck', start: { componentId: 'arduino-uno', pinName: '13' }, end: { componentId: 'tft1', pinName: 'SCK' }, color: '#ff8800' },
|
||||
{ id: 'w-mosi', start: { componentId: 'arduino-uno', pinName: '11' }, end: { componentId: 'tft1', pinName: 'MOSI' }, color: '#ff8800' },
|
||||
{ id: 'w-cs', start: { componentId: 'arduino-uno', pinName: '10' }, end: { componentId: 'tft1', pinName: 'CS' }, color: '#00aaff' },
|
||||
{ id: 'w-dc', start: { componentId: 'arduino-uno', pinName: '9' }, end: { componentId: 'tft1', pinName: 'D/C' }, color: '#00cc00' },
|
||||
{ id: 'w-rst', start: { componentId: 'arduino-uno', pinName: '8' }, end: { componentId: 'tft1', pinName: 'RST' }, color: '#cc0000' },
|
||||
{ id: 'w-led', start: { componentId: 'arduino-uno', pinName: '7' }, end: { componentId: 'tft1', pinName: 'LED' }, color: '#ffffff' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'lcd-hello',
|
||||
title: 'LCD 20x4 Display',
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export class ComponentRegistry {
|
|||
private categories: Map<ComponentCategory, ComponentMetadata[]> = new Map();
|
||||
private allComponents: ComponentMetadata[] = [];
|
||||
private loaded = false;
|
||||
private _loadPromise: Promise<void> | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
|
|
@ -35,6 +36,23 @@ export class ComponentRegistry {
|
|||
*/
|
||||
async load(): Promise<void> {
|
||||
if (this.loaded) return;
|
||||
if (this._loadPromise) return this._loadPromise;
|
||||
this._loadPromise = this._doLoad();
|
||||
return this._loadPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the load promise so consumers can await registry readiness
|
||||
*/
|
||||
get loadPromise(): Promise<void> {
|
||||
return this._loadPromise ?? this.load();
|
||||
}
|
||||
|
||||
get isLoaded(): boolean {
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
private async _doLoad(): Promise<void> {
|
||||
|
||||
try {
|
||||
const response = await fetch('/components-metadata.json');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CPU, AVRTimer, timer0Config, timer1Config, timer2Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, AVRADC, adcConfig } from 'avr8js';
|
||||
import { CPU, AVRTimer, timer0Config, timer1Config, timer2Config, AVRUSART, usart0Config, AVRIOPort, portBConfig, portCConfig, portDConfig, avrInstruction, AVRADC, adcConfig, AVRSPI, spiConfig } from 'avr8js';
|
||||
import { PinManager } from './PinManager';
|
||||
import { hexToUint8Array } from '../utils/hexParser';
|
||||
|
||||
|
|
@ -33,6 +33,7 @@ export class AVRSimulator {
|
|||
private portC: AVRIOPort | null = null;
|
||||
private portD: AVRIOPort | null = null;
|
||||
private adc: AVRADC | null = null;
|
||||
public spi: AVRSPI | null = null;
|
||||
private program: Uint16Array | null = null;
|
||||
private running = false;
|
||||
private animationFrame: number | null = null;
|
||||
|
|
@ -72,11 +73,18 @@ export class AVRSimulator {
|
|||
this.cpu = new CPU(this.program);
|
||||
|
||||
// Initialize peripherals (kept alive so their CPU hooks are not GC'd)
|
||||
this.spi = new AVRSPI(this.cpu, spiConfig, 16000000);
|
||||
// Default onByte: complete transfer immediately (no external device)
|
||||
this.spi.onByte = (value) => {
|
||||
this.spi!.completeTransfer(value);
|
||||
};
|
||||
|
||||
this.peripherals = [
|
||||
new AVRTimer(this.cpu, timer0Config),
|
||||
new AVRTimer(this.cpu, timer1Config),
|
||||
new AVRTimer(this.cpu, timer2Config),
|
||||
new AVRUSART(this.cpu, usart0Config, 16000000),
|
||||
this.spi,
|
||||
];
|
||||
|
||||
// Initialize ADC (analogRead support)
|
||||
|
|
|
|||
|
|
@ -554,3 +554,185 @@ function createLcdSimulation(cols: number, rows: number) {
|
|||
|
||||
PartSimulationRegistry.register('lcd1602', createLcdSimulation(16, 2));
|
||||
PartSimulationRegistry.register('lcd2004', createLcdSimulation(20, 4));
|
||||
|
||||
// ─── ILI9341 TFT Display (SPI) ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* ILI9341 TFT display simulation via hardware SPI.
|
||||
*
|
||||
* Intercepts writes to SPDR (via AVRSPI) and decodes ILI9341 commands:
|
||||
* - 0x2A CASET – set column address window
|
||||
* - 0x2B PASET – set page (row) address window
|
||||
* - 0x2C RAMWR – stream RGB-565 pixel data
|
||||
* - 0x01 SWRESET – clear display
|
||||
* - All others are silently accepted (init sequences, DISPON, MADCTL…)
|
||||
*
|
||||
* DC/RS pin: LOW = command byte, HIGH = data bytes.
|
||||
*/
|
||||
PartSimulationRegistry.register('ili9341', {
|
||||
attachEvents: (element, avrSimulator, getArduinoPinHelper) => {
|
||||
const el = element as any;
|
||||
const pinManager = (avrSimulator as any).pinManager;
|
||||
const spi = (avrSimulator as any).spi;
|
||||
|
||||
if (!pinManager || !spi) {
|
||||
console.warn('[ILI9341] pinManager or SPI peripheral not available');
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// ── Canvas setup ──────────────────────────────────────────────────
|
||||
const SCREEN_W = 240;
|
||||
const SCREEN_H = 320;
|
||||
|
||||
const initCanvas = (): CanvasRenderingContext2D | null => {
|
||||
// el.canvas is the getter defined in ili9341-element.ts:
|
||||
// get canvas() { return this.shadowRoot?.querySelector('canvas'); }
|
||||
// The element already sets width=240 height=320 in its LitElement template.
|
||||
const canvas = el.canvas as HTMLCanvasElement | null;
|
||||
if (!canvas) return null;
|
||||
return canvas.getContext('2d');
|
||||
};
|
||||
|
||||
let ctx = initCanvas();
|
||||
|
||||
const onCanvasReady = () => { ctx = initCanvas(); };
|
||||
el.addEventListener('canvas-ready', onCanvasReady);
|
||||
|
||||
// ── Shared ImageData buffer ───────────────────────────────────────
|
||||
// Accumulate pixels here; flush to canvas once per animation frame.
|
||||
let imageData: ImageData | null = null;
|
||||
|
||||
const getOrCreateImageData = (): ImageData => {
|
||||
if (!ctx) ctx = initCanvas();
|
||||
if (!imageData && ctx) imageData = ctx.createImageData(SCREEN_W, SCREEN_H);
|
||||
return imageData!;
|
||||
};
|
||||
|
||||
let pendingFlush = false;
|
||||
let rafId: number | null = null;
|
||||
|
||||
const scheduleFlush = () => {
|
||||
if (rafId !== null) return;
|
||||
rafId = requestAnimationFrame(() => {
|
||||
rafId = null;
|
||||
if (pendingFlush && ctx && imageData) {
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
pendingFlush = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ── ILI9341 state ─────────────────────────────────────────────────
|
||||
let colStart = 0, colEnd = SCREEN_W - 1;
|
||||
let rowStart = 0, rowEnd = SCREEN_H - 1;
|
||||
let curX = 0, curY = 0;
|
||||
|
||||
let currentCmd = -1;
|
||||
let dataBytes: number[] = [];
|
||||
let inRamWrite = false;
|
||||
let pixelHiByte = 0;
|
||||
let pixelByteCount = 0;
|
||||
|
||||
// ── DC pin tracking ───────────────────────────────────────────────
|
||||
let dcState = false; // LOW = command, HIGH = data
|
||||
const pinDC = getArduinoPinHelper('D/C');
|
||||
|
||||
const unsubscribers: (() => void)[] = [];
|
||||
|
||||
if (pinDC !== null) {
|
||||
unsubscribers.push(
|
||||
pinManager.onPinChange(pinDC, (_: number, s: boolean) => { dcState = s; })
|
||||
);
|
||||
}
|
||||
|
||||
// ── Pixel writer ──────────────────────────────────────────────────
|
||||
const writePixel = (hi: number, lo: number) => {
|
||||
if (curX > colEnd || curY > rowEnd || curY >= SCREEN_H || curX >= SCREEN_W) return;
|
||||
|
||||
const id = getOrCreateImageData();
|
||||
const color = (hi << 8) | lo;
|
||||
const r = ((color >> 11) & 0x1F) * 8;
|
||||
const g = ((color >> 5) & 0x3F) * 4;
|
||||
const b = ( color & 0x1F) * 8;
|
||||
|
||||
const idx = (curY * SCREEN_W + curX) * 4;
|
||||
id.data[idx] = r;
|
||||
id.data[idx + 1] = g;
|
||||
id.data[idx + 2] = b;
|
||||
id.data[idx + 3] = 255;
|
||||
|
||||
pendingFlush = true;
|
||||
curX++;
|
||||
if (curX > colEnd) {
|
||||
curX = colStart;
|
||||
curY++;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Command / data processing ─────────────────────────────────────
|
||||
const processCommand = (cmd: number) => {
|
||||
currentCmd = cmd;
|
||||
dataBytes = [];
|
||||
inRamWrite = (cmd === 0x2C);
|
||||
pixelByteCount = 0;
|
||||
|
||||
if (cmd === 0x01) { // SWRESET – clear framebuffer
|
||||
colStart = 0; colEnd = SCREEN_W - 1;
|
||||
rowStart = 0; rowEnd = SCREEN_H - 1;
|
||||
curX = 0; curY = 0;
|
||||
imageData = null;
|
||||
if (ctx) ctx.clearRect(0, 0, SCREEN_W, SCREEN_H);
|
||||
}
|
||||
};
|
||||
|
||||
const processData = (value: number) => {
|
||||
if (inRamWrite) {
|
||||
// RGB-565: two bytes per pixel
|
||||
if (pixelByteCount === 0) {
|
||||
pixelHiByte = value;
|
||||
pixelByteCount = 1;
|
||||
} else {
|
||||
writePixel(pixelHiByte, value);
|
||||
scheduleFlush();
|
||||
pixelByteCount = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
dataBytes.push(value);
|
||||
switch (currentCmd) {
|
||||
case 0x2A: // CASET – column address set
|
||||
if (dataBytes.length === 2) colStart = (dataBytes[0] << 8) | dataBytes[1];
|
||||
if (dataBytes.length === 4) { colEnd = (dataBytes[2] << 8) | dataBytes[3]; curX = colStart; }
|
||||
break;
|
||||
case 0x2B: // PASET – page address set
|
||||
if (dataBytes.length === 2) rowStart = (dataBytes[0] << 8) | dataBytes[1];
|
||||
if (dataBytes.length === 4) { rowEnd = (dataBytes[2] << 8) | dataBytes[3]; curY = rowStart; }
|
||||
break;
|
||||
// All other commands (DISPON, MADCTL, COLMOD…) just buffer data
|
||||
}
|
||||
};
|
||||
|
||||
// ── Intercept SPI ─────────────────────────────────────────────────
|
||||
const prevOnByte = spi.onByte.bind(spi);
|
||||
|
||||
spi.onByte = (value: number) => {
|
||||
if (!dcState) {
|
||||
processCommand(value);
|
||||
} else {
|
||||
processData(value);
|
||||
}
|
||||
spi.completeTransfer(0xFF); // Unblock CPU immediately
|
||||
};
|
||||
|
||||
console.log(`[ILI9341] SPI simulation ready. DC→pin${pinDC}`);
|
||||
|
||||
// ── Cleanup ───────────────────────────────────────────────────────
|
||||
return () => {
|
||||
spi.onByte = prevOnByte;
|
||||
if (rafId !== null) cancelAnimationFrame(rafId);
|
||||
el.removeEventListener('canvas-ready', onCanvasReady);
|
||||
unsubscribers.forEach(u => u());
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue