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 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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue