feat: enhance DHT22 and HC-SR04 simulations with cycle-accurate pin changes
parent
8ba11800e1
commit
f6fdae6b0e
|
|
@ -262,8 +262,8 @@ export const SimulatorCanvas = () => {
|
||||||
const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
|
const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
|
||||||
const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;
|
const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;
|
||||||
|
|
||||||
if (componentWrapper && !runningRef.current) {
|
if (componentWrapper) {
|
||||||
// ── Single finger on a component: start drag ──
|
// ── Single finger on a component: track for click/drag ──
|
||||||
const componentId = componentWrapper.getAttribute('data-component-id');
|
const componentId = componentWrapper.getAttribute('data-component-id');
|
||||||
if (componentId) {
|
if (componentId) {
|
||||||
const component = componentsRef.current.find((c) => c.id === componentId);
|
const component = componentsRef.current.find((c) => c.id === componentId);
|
||||||
|
|
@ -985,10 +985,7 @@ export const SimulatorCanvas = () => {
|
||||||
y={component.y}
|
y={component.y}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
// Only handle UI events when simulation is NOT running
|
handleComponentMouseDown(component.id, e);
|
||||||
if (!running) {
|
|
||||||
handleComponentMouseDown(component.id, e);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
// Only handle UI events when simulation is NOT running
|
// Only handle UI events when simulation is NOT running
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,9 @@ export class AVRSimulator {
|
||||||
/** 'uno' for ATmega328P boards (Uno, Nano); 'mega' for ATmega2560; 'tiny85' for ATtiny85 */
|
/** 'uno' for ATmega328P boards (Uno, Nano); 'mega' for ATmega2560; 'tiny85' for ATtiny85 */
|
||||||
private boardVariant: 'uno' | 'mega' | 'tiny85';
|
private boardVariant: 'uno' | 'mega' | 'tiny85';
|
||||||
|
|
||||||
|
/** Cycle-accurate pin change queue — used by timing-sensitive peripherals (e.g. DHT22). */
|
||||||
|
private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = [];
|
||||||
|
|
||||||
/** Serial output buffer — subscribers receive each byte or line */
|
/** Serial output buffer — subscribers receive each byte or line */
|
||||||
public onSerialData: ((char: string) => void) | null = null;
|
public onSerialData: ((char: string) => void) | null = null;
|
||||||
/** Fires whenever the sketch changes Serial baud rate (Serial.begin) */
|
/** Fires whenever the sketch changes Serial baud rate (Serial.begin) */
|
||||||
|
|
@ -306,6 +309,37 @@ export class AVRSimulator {
|
||||||
return this.adc;
|
return this.adc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current CPU cycle count.
|
||||||
|
* Used by timing-sensitive peripherals to schedule future pin changes.
|
||||||
|
*/
|
||||||
|
getCurrentCycles(): number {
|
||||||
|
return this.cpu?.cycles ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a pin state change at a specific future CPU cycle count.
|
||||||
|
* The change fires between AVR instructions, enabling cycle-accurate protocol simulation.
|
||||||
|
* Used by DHT22 and other timing-sensitive single-wire peripherals.
|
||||||
|
*/
|
||||||
|
schedulePinChange(pin: number, state: boolean, atCycle: number): void {
|
||||||
|
// Callers are expected to push entries in ascending cycle order.
|
||||||
|
// Insert at the correct position to maintain sort (linear scan from end, O(1) for ordered pushes).
|
||||||
|
let i = this.scheduledPinChanges.length;
|
||||||
|
while (i > 0 && this.scheduledPinChanges[i - 1].cycle > atCycle) i--;
|
||||||
|
this.scheduledPinChanges.splice(i, 0, { cycle: atCycle, pin, state });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Flush all scheduled pin changes whose target cycle has been reached. */
|
||||||
|
private flushScheduledPinChanges(): void {
|
||||||
|
if (this.scheduledPinChanges.length === 0 || !this.cpu) return;
|
||||||
|
const now = this.cpu.cycles;
|
||||||
|
while (this.scheduledPinChanges.length > 0 && this.scheduledPinChanges[0].cycle <= now) {
|
||||||
|
const { pin, state } = this.scheduledPinChanges.shift()!;
|
||||||
|
this.setPinState(pin, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fire onPinChangeWithTime for every bit that differs between newVal and oldVal.
|
* Fire onPinChangeWithTime for every bit that differs between newVal and oldVal.
|
||||||
* @param pinMap Optional explicit per-bit Arduino pin numbers (Mega).
|
* @param pinMap Optional explicit per-bit Arduino pin numbers (Mega).
|
||||||
|
|
@ -444,6 +478,7 @@ export class AVRSimulator {
|
||||||
for (let i = 0; i < cyclesPerFrame; i++) {
|
for (let i = 0; i < cyclesPerFrame; i++) {
|
||||||
avrInstruction(this.cpu); // Execute the AVR instruction
|
avrInstruction(this.cpu); // Execute the AVR instruction
|
||||||
this.cpu.tick(); // Update peripheral timers and cycles
|
this.cpu.tick(); // Update peripheral timers and cycles
|
||||||
|
if (this.scheduledPinChanges.length > 0) this.flushScheduledPinChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll PWM registers every frame
|
// Poll PWM registers every frame
|
||||||
|
|
@ -476,6 +511,7 @@ export class AVRSimulator {
|
||||||
cancelAnimationFrame(this.animationFrame);
|
cancelAnimationFrame(this.animationFrame);
|
||||||
this.animationFrame = null;
|
this.animationFrame = null;
|
||||||
}
|
}
|
||||||
|
this.scheduledPinChanges = [];
|
||||||
|
|
||||||
console.log('AVR simulation stopped');
|
console.log('AVR simulation stopped');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -384,31 +384,68 @@ function buildDHT22Payload(element: HTMLElement): Uint8Array {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drive 40 bits on the DATA pin as fast as synchronous setPinState allows.
|
* Schedule the full DHT22 waveform on DATA using cycle-accurate pin changes.
|
||||||
* Each bit produces: LOW → then HIGH if bit=1, LOW if bit=0.
|
*
|
||||||
* This saturates the timing but ensures the pin is toggled correctly.
|
* DHT22 protocol (after MCU releases DATA HIGH):
|
||||||
|
* - 80 µs LOW → 80 µs HIGH (response preamble)
|
||||||
|
* - 40 bits, each: 50 µs LOW + (26 µs HIGH = '0', 70 µs HIGH = '1')
|
||||||
|
* - Line released HIGH after last bit
|
||||||
|
*
|
||||||
|
* At 16 MHz: 1 µs = 16 cycles
|
||||||
|
* - 80 µs = 1280 cycles, 50 µs = 800 cycles, 26 µs = 416 cycles, 70 µs = 1120 cycles
|
||||||
*/
|
*/
|
||||||
function driveDHT22Response(simulator: any, pin: number, element: HTMLElement): void {
|
function scheduleDHT22Response(simulator: any, pin: number, element: HTMLElement): void {
|
||||||
|
if (typeof simulator.schedulePinChange !== 'function') {
|
||||||
|
// Fallback: synchronous drive (legacy / non-AVR simulators)
|
||||||
|
const payload = buildDHT22Payload(element);
|
||||||
|
simulator.setPinState(pin, false);
|
||||||
|
simulator.setPinState(pin, true);
|
||||||
|
for (const byte of payload) {
|
||||||
|
for (let b = 7; b >= 0; b--) {
|
||||||
|
const bit = (byte >> b) & 1;
|
||||||
|
simulator.setPinState(pin, false);
|
||||||
|
simulator.setPinState(pin, !!bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
simulator.setPinState(pin, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const payload = buildDHT22Payload(element);
|
const payload = buildDHT22Payload(element);
|
||||||
// Response preamble: drive LOW (response start)
|
const now = simulator.getCurrentCycles() as number;
|
||||||
simulator.setPinState(pin, false);
|
|
||||||
// Then HIGH (ready)
|
// Timing constants at 16 MHz (cycles per µs = 16)
|
||||||
simulator.setPinState(pin, true);
|
const LOW80 = 1280; // 80 µs LOW preamble
|
||||||
// Transmit 40 bits MSB first
|
const HIGH80 = 1280; // 80 µs HIGH preamble
|
||||||
|
const LOW50 = 800; // 50 µs LOW marker before each bit
|
||||||
|
const HIGH0 = 416; // 26 µs HIGH → bit '0'
|
||||||
|
const HIGH1 = 1120; // 70 µs HIGH → bit '1'
|
||||||
|
|
||||||
|
let t = now;
|
||||||
|
|
||||||
|
// Preamble: 80 µs LOW then 80 µs HIGH
|
||||||
|
t += LOW80; simulator.schedulePinChange(pin, false, t);
|
||||||
|
t += HIGH80; simulator.schedulePinChange(pin, true, t);
|
||||||
|
|
||||||
|
// 40 data bits, MSB first
|
||||||
for (const byte of payload) {
|
for (const byte of payload) {
|
||||||
for (let b = 7; b >= 0; b--) {
|
for (let b = 7; b >= 0; b--) {
|
||||||
const bit = (byte >> b) & 1;
|
const bit = (byte >> b) & 1;
|
||||||
simulator.setPinState(pin, false); // 50 µs LOW marker
|
t += LOW50; simulator.schedulePinChange(pin, false, t);
|
||||||
simulator.setPinState(pin, !!bit); // HIGH duration encodes 0 or 1
|
t += bit ? HIGH1 : HIGH0;
|
||||||
|
simulator.schedulePinChange(pin, true, t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Line idle HIGH
|
|
||||||
simulator.setPinState(pin, true);
|
// Release line HIGH (it already is, but explicit for clarity)
|
||||||
|
t += LOW50; simulator.schedulePinChange(pin, false, t);
|
||||||
|
t += HIGH0; simulator.schedulePinChange(pin, true, t);
|
||||||
}
|
}
|
||||||
|
|
||||||
PartSimulationRegistry.register('dht22', {
|
PartSimulationRegistry.register('dht22', {
|
||||||
attachEvents: (element, simulator, getPin, componentId) => {
|
attachEvents: (element, simulator, getPin, componentId) => {
|
||||||
const pin = getPin('DATA');
|
// wokwi-dht22 element uses 'SDA' as the data pin name (not 'DATA')
|
||||||
|
const pin = getPin('SDA') ?? getPin('DATA');
|
||||||
if (pin === null) return () => {};
|
if (pin === null) return () => {};
|
||||||
|
|
||||||
let wasLow = false;
|
let wasLow = false;
|
||||||
|
|
@ -424,7 +461,7 @@ PartSimulationRegistry.register('dht22', {
|
||||||
if (wasLow) {
|
if (wasLow) {
|
||||||
// MCU released DATA HIGH — begin DHT22 response
|
// MCU released DATA HIGH — begin DHT22 response
|
||||||
wasLow = false;
|
wasLow = false;
|
||||||
driveDHT22Response(simulator, pin, element);
|
scheduleDHT22Response(simulator, pin, element);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -634,20 +634,26 @@ PartSimulationRegistry.register('hc-sr04', {
|
||||||
simulator.setPinState(echoPin, false); // ECHO LOW initially
|
simulator.setPinState(echoPin, false); // ECHO LOW initially
|
||||||
|
|
||||||
let distanceCm = 10; // default distance in cm
|
let distanceCm = 10; // default distance in cm
|
||||||
let echoTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
const cleanup = simulator.pinManager.onPinChange(trigPin, (_: number, state: boolean) => {
|
const cleanup = simulator.pinManager.onPinChange(trigPin, (_: number, state: boolean) => {
|
||||||
if (state) {
|
if (!state) return; // only react on TRIG HIGH
|
||||||
if (echoTimer !== null) clearTimeout(echoTimer);
|
// HC-SR04 timing (at 16 MHz):
|
||||||
// echoMs = distanceCm / 17.15 (min 1 ms for setTimeout reliability)
|
// - Sensor processing delay after TRIG: ~600 µs = 9600 cycles
|
||||||
|
// - Echo duration = distanceCm / 17150 s × 16 000 000 cycles/s
|
||||||
|
// (17150 cm/s = speed of sound, one-way = round-trip/2)
|
||||||
|
if (typeof simulator.schedulePinChange === 'function') {
|
||||||
|
const now = simulator.getCurrentCycles() as number;
|
||||||
|
const processingCycles = 9600; // ~600 µs sensor overhead
|
||||||
|
const echoCycles = Math.round((distanceCm / 17150) * 16_000_000);
|
||||||
|
simulator.schedulePinChange(echoPin, true, now + processingCycles);
|
||||||
|
simulator.schedulePinChange(echoPin, false, now + processingCycles + echoCycles);
|
||||||
|
console.log(`[HC-SR04] Scheduled ECHO (${distanceCm} cm, echo=${(echoCycles/16000).toFixed(1)} µs)`);
|
||||||
|
} else {
|
||||||
|
// Fallback: best-effort async (works with delay()-based sketches, not pulseIn)
|
||||||
const echoMs = Math.max(1, distanceCm / 17.15);
|
const echoMs = Math.max(1, distanceCm / 17.15);
|
||||||
echoTimer = setTimeout(() => {
|
setTimeout(() => {
|
||||||
simulator.setPinState(echoPin, true);
|
simulator.setPinState(echoPin, true);
|
||||||
console.log(`[HC-SR04] ECHO HIGH (${distanceCm} cm)`);
|
setTimeout(() => { simulator.setPinState(echoPin, false); }, echoMs);
|
||||||
echoTimer = setTimeout(() => {
|
|
||||||
simulator.setPinState(echoPin, false);
|
|
||||||
echoTimer = null;
|
|
||||||
}, echoMs);
|
|
||||||
}, 1);
|
}, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -660,7 +666,6 @@ PartSimulationRegistry.register('hc-sr04', {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cleanup();
|
cleanup();
|
||||||
if (echoTimer !== null) clearTimeout(echoTimer);
|
|
||||||
unregisterSensorUpdate(componentId);
|
unregisterSensorUpdate(componentId);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue