feat: enhance DHT22 and HC-SR04 simulations with cycle-accurate pin changes

pull/47/head
David Montero Crespo 2026-03-19 08:22:36 -03:00
parent 8ba11800e1
commit f6fdae6b0e
4 changed files with 107 additions and 32 deletions

View File

@ -262,8 +262,8 @@ export const SimulatorCanvas = () => {
const componentWrapper = target?.closest('[data-component-id]') as HTMLElement | null;
const boardOverlay = target?.closest('[data-board-overlay]') as HTMLElement | null;
if (componentWrapper && !runningRef.current) {
// ── Single finger on a component: start drag ──
if (componentWrapper) {
// ── Single finger on a component: track for click/drag ──
const componentId = componentWrapper.getAttribute('data-component-id');
if (componentId) {
const component = componentsRef.current.find((c) => c.id === componentId);
@ -985,10 +985,7 @@ export const SimulatorCanvas = () => {
y={component.y}
isSelected={isSelected}
onMouseDown={(e) => {
// Only handle UI events when simulation is NOT running
if (!running) {
handleComponentMouseDown(component.id, e);
}
handleComponentMouseDown(component.id, e);
}}
onDoubleClick={(e) => {
// Only handle UI events when simulation is NOT running

View File

@ -152,6 +152,9 @@ export class AVRSimulator {
/** 'uno' for ATmega328P boards (Uno, Nano); 'mega' for ATmega2560; 'tiny85' for ATtiny85 */
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 */
public onSerialData: ((char: string) => void) | null = null;
/** Fires whenever the sketch changes Serial baud rate (Serial.begin) */
@ -306,6 +309,37 @@ export class AVRSimulator {
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.
* @param pinMap Optional explicit per-bit Arduino pin numbers (Mega).
@ -444,6 +478,7 @@ export class AVRSimulator {
for (let i = 0; i < cyclesPerFrame; i++) {
avrInstruction(this.cpu); // Execute the AVR instruction
this.cpu.tick(); // Update peripheral timers and cycles
if (this.scheduledPinChanges.length > 0) this.flushScheduledPinChanges();
}
// Poll PWM registers every frame
@ -476,6 +511,7 @@ export class AVRSimulator {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
this.scheduledPinChanges = [];
console.log('AVR simulation stopped');
}

View File

@ -384,31 +384,68 @@ function buildDHT22Payload(element: HTMLElement): Uint8Array {
}
/**
* Drive 40 bits on the DATA pin as fast as synchronous setPinState allows.
* Each bit produces: LOW then HIGH if bit=1, LOW if bit=0.
* This saturates the timing but ensures the pin is toggled correctly.
* Schedule the full DHT22 waveform on DATA using cycle-accurate pin changes.
*
* 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);
// Response preamble: drive LOW (response start)
simulator.setPinState(pin, false);
// Then HIGH (ready)
simulator.setPinState(pin, true);
// Transmit 40 bits MSB first
const now = simulator.getCurrentCycles() as number;
// Timing constants at 16 MHz (cycles per µs = 16)
const LOW80 = 1280; // 80 µs LOW preamble
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 (let b = 7; b >= 0; b--) {
const bit = (byte >> b) & 1;
simulator.setPinState(pin, false); // 50 µs LOW marker
simulator.setPinState(pin, !!bit); // HIGH duration encodes 0 or 1
t += LOW50; simulator.schedulePinChange(pin, false, t);
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', {
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 () => {};
let wasLow = false;
@ -424,7 +461,7 @@ PartSimulationRegistry.register('dht22', {
if (wasLow) {
// MCU released DATA HIGH — begin DHT22 response
wasLow = false;
driveDHT22Response(simulator, pin, element);
scheduleDHT22Response(simulator, pin, element);
}
},
);

View File

@ -634,20 +634,26 @@ PartSimulationRegistry.register('hc-sr04', {
simulator.setPinState(echoPin, false); // ECHO LOW initially
let distanceCm = 10; // default distance in cm
let echoTimer: ReturnType<typeof setTimeout> | null = null;
const cleanup = simulator.pinManager.onPinChange(trigPin, (_: number, state: boolean) => {
if (state) {
if (echoTimer !== null) clearTimeout(echoTimer);
// echoMs = distanceCm / 17.15 (min 1 ms for setTimeout reliability)
if (!state) return; // only react on TRIG HIGH
// HC-SR04 timing (at 16 MHz):
// - 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);
echoTimer = setTimeout(() => {
setTimeout(() => {
simulator.setPinState(echoPin, true);
console.log(`[HC-SR04] ECHO HIGH (${distanceCm} cm)`);
echoTimer = setTimeout(() => {
simulator.setPinState(echoPin, false);
echoTimer = null;
}, echoMs);
setTimeout(() => { simulator.setPinState(echoPin, false); }, echoMs);
}, 1);
}
});
@ -660,7 +666,6 @@ PartSimulationRegistry.register('hc-sr04', {
return () => {
cleanup();
if (echoTimer !== null) clearTimeout(echoTimer);
unregisterSensorUpdate(componentId);
};
},