feat(rp2040/uart): synthesize bit-level TX waveform on GP0 / GP4
Same gap as the AVR USART: rp2040js's UART fires `onByte(value)` per
transmitted byte but never toggles the corresponding GPIO, so an
oscilloscope on GP0 (UART0 TX, default for Arduino-Pico's Serial1) sees
nothing during `Serial.print`. Real silicon drives the pin with the
full UART frame at the configured baud rate, and Velxio should match.
`emitUartTxFrame(uartIdx, byte)` derives:
* `txPin` via FUNCSEL inspection: walk GP0 / GP12 / GP16 / GP28 (the
four candidates for UART0 TX per RP2040 datasheet) and pick the
first whose `functionSelect == 2` (FUNCTION_UART). Same for UART1.
Fall back to GP0 / GP4 when nothing is mapped (firmware hasn't
called `Serial1.begin()` properly).
* `baudRate` and `bitsPerChar` directly from the UART peripheral
(rp2040js already exposes these as live getters).
* Time from the RP2040 IClock's `nanos` counter, matching the
existing `setupGpioListeners` path — UART waveforms therefore stack
consistently with PIO / SIO traces on the same scope.
Both `uart[0].onByte` and `uart[1].onByte` get hooked. The seed-idle-
HIGH baseline is pushed once per UART per simulation run; `stop()`
clears the flag so a re-run gets a fresh seed (matching how the scope
buffer is cleared on restart).
This commit is contained in:
parent
b587faf1b0
commit
6584a49a8f
|
|
@ -114,6 +114,14 @@ export class RP2040Simulator {
|
|||
*/
|
||||
public onPinChangeWithTime: ((pin: number, state: boolean, timeMs: number) => void) | null = null;
|
||||
|
||||
/**
|
||||
* Track whether the first byte has been transmitted on each UART since
|
||||
* the firmware booted. Used to seed the oscilloscope baseline at idle
|
||||
* HIGH the first time a frame goes out, mirroring how real silicon
|
||||
* idles the TX line HIGH once UARTEN is asserted.
|
||||
*/
|
||||
private uartTxSeeded: [boolean, boolean] = [false, false];
|
||||
|
||||
/**
|
||||
* One `I2CBusManager` per hardware I2C controller (RP2040 has two:
|
||||
* I2C0/Wire and I2C1/Wire1). Constructed up-front in the
|
||||
|
|
@ -424,6 +432,10 @@ export class RP2040Simulator {
|
|||
if (this.onSerialData) {
|
||||
this.onSerialData(ch);
|
||||
}
|
||||
// Synthesize the bit-level waveform on the UART0 TX pin so an
|
||||
// oscilloscope on it sees a real frame — rp2040js doesn't drive the
|
||||
// GPIO when the UART transmits. See emitUartTxFrame().
|
||||
this.emitUartTxFrame(0, value);
|
||||
};
|
||||
|
||||
// ── Wire UART1 (Serial1) — also forward to onSerialData for now ──
|
||||
|
|
@ -431,6 +443,7 @@ export class RP2040Simulator {
|
|||
if (this.onSerialData) {
|
||||
this.onSerialData(String.fromCharCode(value));
|
||||
}
|
||||
this.emitUartTxFrame(1, value);
|
||||
};
|
||||
|
||||
// ── Wire I2C0 and I2C1 ───────────────────────────────────────────
|
||||
|
|
@ -489,6 +502,84 @@ export class RP2040Simulator {
|
|||
this.setupGpioListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the GPIO index currently routed to a given UART's TX line.
|
||||
*
|
||||
* The RP2040 GPIO function-select register decides which signal each pad
|
||||
* carries; UART has FUNCSEL == 2. Per datasheet, UART0_TX can land on
|
||||
* GP0 / GP12 / GP16 / GP28 and UART1_TX on GP4 / GP8 / GP20 / GP24. We
|
||||
* walk the candidates and pick the first whose function select is UART.
|
||||
* If none is mapped (rare — the firmware hasn't called `Serial.begin()`
|
||||
* properly) fall back to the default for that UART (GP0 / GP4).
|
||||
*/
|
||||
private rp2040UartTxPin(uartIdx: 0 | 1): number {
|
||||
const FUNCTION_UART = 2;
|
||||
const candidates = uartIdx === 0 ? [0, 12, 16, 28] : [4, 8, 20, 24];
|
||||
if (this.rp2040) {
|
||||
for (const g of candidates) {
|
||||
const pin = this.rp2040.gpio[g];
|
||||
if (pin && (pin as unknown as { functionSelect: number }).functionSelect === FUNCTION_UART) {
|
||||
return g;
|
||||
}
|
||||
}
|
||||
}
|
||||
return uartIdx === 0 ? 0 : 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synthesize a bit-level UART frame on the TX pin so the oscilloscope
|
||||
* sees a real waveform during `Serial.print` / `Serial1.print`.
|
||||
*
|
||||
* rp2040js's UART peripheral fires `onByte(value)` per transmitted byte
|
||||
* but never toggles the corresponding GPIO — the same gap closed in
|
||||
* AVRSimulator.emitUartTxFrame(). Here we do the same: build the frame
|
||||
* (start LOW + data LSB-first + stop HIGH) using the UART's live
|
||||
* `baudRate` and `bitsPerChar`, then push one transition per bit-change
|
||||
* through `onPinChangeWithTime` so the scope draws the waveform at the
|
||||
* actual silicon-equivalent baud rate.
|
||||
*
|
||||
* Time is taken from the RP2040 clock (nanos counter), matching the
|
||||
* existing GPIO-listener path in `setupGpioListeners()` — UART
|
||||
* waveforms therefore stack consistently with any other pin trace.
|
||||
*/
|
||||
private emitUartTxFrame(uartIdx: 0 | 1, byte: number): void {
|
||||
if (!this.rp2040 || !this.onPinChangeWithTime) return;
|
||||
const uart = this.rp2040.uart[uartIdx];
|
||||
if (!uart) return;
|
||||
const baud = uart.baudRate;
|
||||
if (!baud || baud <= 0) return;
|
||||
|
||||
const txPin = this.rp2040UartTxPin(uartIdx);
|
||||
const dataBits = uart.bitsPerChar;
|
||||
const clk = (this.rp2040 as unknown as { clock?: { nanos: number } }).clock;
|
||||
const startMs = clk ? clk.nanos / 1_000_000 : 0;
|
||||
const bitMs = 1000 / baud;
|
||||
|
||||
// First frame after boot: seed an explicit idle HIGH one bit-period
|
||||
// before the start bit so the scope has a HIGH baseline to draw the
|
||||
// start-bit transition against. Subsequent frames inherit the HIGH
|
||||
// baseline from the previous frame's stop bit.
|
||||
if (!this.uartTxSeeded[uartIdx]) {
|
||||
this.onPinChangeWithTime(txPin, true, Math.max(0, startMs - bitMs));
|
||||
this.uartTxSeeded[uartIdx] = true;
|
||||
}
|
||||
|
||||
const bits: boolean[] = [false]; // start bit
|
||||
for (let i = 0; i < dataBits; i++) {
|
||||
bits.push(((byte >> i) & 1) !== 0);
|
||||
}
|
||||
bits.push(true); // stop bit (rp2040js doesn't expose 2-stop-bit selection
|
||||
// cleanly; default to 1 — same behaviour as 8N1 sketches)
|
||||
|
||||
let prevState = true;
|
||||
for (let i = 0; i < bits.length; i++) {
|
||||
if (bits[i] !== prevState) {
|
||||
this.onPinChangeWithTime(txPin, bits[i], startMs + i * bitMs);
|
||||
prevState = bits[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private wireI2C(bus: 0 | 1): void {
|
||||
if (!this.rp2040) return;
|
||||
const i2c: RPI2C = this.rp2040.i2c[bus];
|
||||
|
|
@ -615,6 +706,10 @@ export class RP2040Simulator {
|
|||
cancelAnimationFrame(this.animationFrame);
|
||||
this.animationFrame = null;
|
||||
}
|
||||
// Force a new idle-HIGH seed on the next byte: the scope buffer is
|
||||
// typically cleared on stop/start, so the previous run's "seeded"
|
||||
// flag would suppress the baseline sample for the next session.
|
||||
this.uartTxSeeded = [false, false];
|
||||
console.log('[RP2040] Simulation stopped');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue