diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 68c0260..8d32fd7 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "generatedAt": "2026-03-20T17:33:38.631Z", + "generatedAt": "2026-03-21T20:08:10.580Z", "components": [ { "id": "arduino-mega", diff --git a/frontend/src/simulation/RP2040Simulator.ts b/frontend/src/simulation/RP2040Simulator.ts index 83be0b1..28d5a11 100644 --- a/frontend/src/simulation/RP2040Simulator.ts +++ b/frontend/src/simulation/RP2040Simulator.ts @@ -294,13 +294,26 @@ export class RP2040Simulator { this.stepPIO(); break; } - clock.tick(jump); const jumped = Math.ceil(jump / CYCLE_NANOS); - cyclesDone += jumped; - this.totalCycles += jumped; - // Step PIO proportionally during the jump const pioSteps = Math.floor(jumped / pioDiv); - for (let i = 0; i < pioSteps && i < 2000; i++) this.stepPIO(); + // Advance clock incrementally per PIO step so GPIO transitions + // get accurate timestamps (not all lumped at the end of the jump). + const nanoPerPioStep = pioDiv * CYCLE_NANOS; + const maxSteps = Math.min(pioSteps, 50000); + let nanosStepped = 0; + for (let i = 0; i < maxSteps; i++) { + clock.tick(nanoPerPioStep); + nanosStepped += nanoPerPioStep; + this.totalCycles += pioDiv; + this.stepPIO(); + } + // Tick any remaining nanoseconds not covered by PIO steps + const remaining = jump - nanosStepped; + if (remaining > 0) { + clock.tick(remaining); + this.totalCycles += Math.ceil(remaining / CYCLE_NANOS); + } + cyclesDone += jumped; this.flushScheduledPinChanges(); } else { break; @@ -312,7 +325,7 @@ export class RP2040Simulator { this.totalCycles += cycles; // Step PIO synchronously at the PIO clock rate this.pioStepAccum += cycles; - if (this.pioStepAccum >= pioDiv) { + while (this.pioStepAccum >= pioDiv) { this.pioStepAccum -= pioDiv; this.stepPIO(); } @@ -323,20 +336,31 @@ export class RP2040Simulator { frameCount++; if (frameCount % 60 === 0) { console.log(`[RP2040] Frame ${frameCount}, PC: 0x${core.PC.toString(16)}`); - // PIO diagnostic: check GPIO 15 state and PIO status + // PIO diagnostic: check GPIO 15 state and PIO1 status (servo uses PIO1) // eslint-disable-next-line @typescript-eslint/no-explicit-any const rp = this.rp2040 as any; if (rp && frameCount <= 300) { const gpio15 = rp.gpio[15]; - const pio0 = rp.pio[0]; + const pio1 = rp.pio[1]; const funcSel = gpio15 ? (gpio15.ctrl & 0x1f) : -1; - const pioStopped = pio0 ? pio0.stopped : true; - const pioPinVal = pio0 ? ((pio0.pinValues >> 15) & 1) : -1; - const pioPinDir = pio0 ? ((pio0.pinDirections >> 15) & 1) : -1; + const pio1Stopped = pio1 ? pio1.stopped : true; + const pio1PinVal = pio1 ? ((pio1.pinValues >> 15) & 1) : -1; + const pio1PinDir = pio1 ? ((pio1.pinDirections >> 15) & 1) : -1; + // Find clockDivInt for first enabled machine in PIO1 + let pio1ClkDiv = 'N/A'; + if (pio1?.machines) { + for (const m of pio1.machines) { + if (m.enabled) { + pio1ClkDiv = `${m.clockDivInt}.${m.clockDivFrac || 0}`; + break; + } + } + } console.log( - `[RP2040 PIO diag] gpio15.funcSel=${funcSel} (6=PIO0)` + - ` pio0.stopped=${pioStopped} pinVal[15]=${pioPinVal} pinDir[15]=${pioPinDir}` + - ` onPinChangeWithTime=${!!this.onPinChangeWithTime}` + `[RP2040 PIO diag] pioDiv=${pioDiv}` + + ` gpio15.funcSel=${funcSel} (7=PIO1)` + + ` pio1.stopped=${pio1Stopped} pinVal[15]=${pio1PinVal} pinDir[15]=${pio1PinDir}` + + ` pio1.clkDiv=${pio1ClkDiv}` ); } } diff --git a/frontend/src/simulation/parts/ComplexParts.ts b/frontend/src/simulation/parts/ComplexParts.ts index 7158fa3..73edc9f 100644 --- a/frontend/src/simulation/parts/ComplexParts.ts +++ b/frontend/src/simulation/parts/ComplexParts.ts @@ -283,6 +283,13 @@ PartSimulationRegistry.register('servo', { let riseTimeMs = -1; let logCount = 0; + // Self-calibrating pulse range: the PIO clock divider may not match + // exactly, producing pulses offset from the standard 544-2400µs range. + // Track the minimum observed pulse (= 0° reference) and map using the + // known standard spread (MAX_PULSE_US - MIN_PULSE_US = 1856µs). + let observedMin = Infinity; + const EXPECTED_SPREAD = MAX_PULSE_US - MIN_PULSE_US; // 1856 + avrSimulator.onPinChangeWithTime = (pin, state, timeMs) => { if (pin !== pinSIG) return; if (logCount < 10) { @@ -294,14 +301,32 @@ PartSimulationRegistry.register('servo', { } else if (riseTimeMs >= 0) { const pulseUs = (timeMs - riseTimeMs) * 1000; riseTimeMs = -1; + + // Reject noise: only consider pulses in a reasonable servo range + if (pulseUs < 100 || pulseUs > 25000) return; + if (logCount <= 12) { - console.log(`[Servo RP2040] pulseUs=${pulseUs.toFixed(1)}`); + console.log(`[Servo RP2040] pulseUs=${pulseUs.toFixed(1)} observedMin=${observedMin.toFixed(1)}`); } + + // Update calibration baseline + if (pulseUs < observedMin) observedMin = pulseUs; + + // Try standard range first if (pulseUs >= MIN_PULSE_US && pulseUs <= MAX_PULSE_US) { const angle = Math.round( - ((pulseUs - MIN_PULSE_US) / (MAX_PULSE_US - MIN_PULSE_US)) * 180 + ((pulseUs - MIN_PULSE_US) / EXPECTED_SPREAD) * 180 ); - el.angle = angle; + el.angle = Math.max(0, Math.min(180, angle)); + } else if (observedMin < Infinity) { + // Self-calibrated range: use observedMin as 0° reference + const rangeMax = observedMin + EXPECTED_SPREAD; + if (pulseUs >= observedMin - 50 && pulseUs <= rangeMax + 200) { + const angle = Math.round( + ((pulseUs - observedMin) / EXPECTED_SPREAD) * 180 + ); + el.angle = Math.max(0, Math.min(180, angle)); + } } } };