velxio/frontend/src/__tests__/avr-uart-tx-waveform.test.ts

149 lines
5.4 KiB
TypeScript
Raw Normal View History

feat(avr/uart): synthesize bit-level TX waveform on PD1/PE1 avr8js intercepts the transmitted byte at the UDR0 register and never toggles the corresponding GPIO. Real ATmega328P / ATmega2560 hardware drives PD1 / PE1 with a start bit, 8 data bits LSB-first, and a stop bit at the configured baud rate the moment TXEN is set. An oscilloscope probe on D1 therefore showed nothing in Velxio while the same probe in the real world would resolve the UART frame. Synthesize the frame from the inside of `onByteTransmit`: * Read `usart.baudRate`, `usart.bitsPerChar`, `usart.parityEnabled`, `usart.parityOdd`, `usart.stopBits` so unusual configurations stay accurate (avr8js already exposes these as public getters). * Build the bit list start + data(LSB first) + parity? + stopBit(s). * For each transition vs. previous state (initial = idle HIGH), call `onPinChangeWithTime(1, state, timeMs)` where `timeMs = (cpu.cycles + i * cyclesPerBit) / 16_000`. Same simulator-time clock the existing port-listener path uses, so the scope draws the UART waveform cycle-accurately alongside other GPIO activity. Also hook `onConfigurationChange` to detect TXEN flipping 0→1 and seed the scope baseline at idle HIGH; without that, the very first byte's start bit transition would be invisible because the scope's pre-first- sample default is LOW. Both USART construction sites (initial setupSimulation around line 423, re-init after stop around line 749) get the same hook. Covered by `__tests__/avr-uart-tx-waveform.test.ts` (5 cases): idle seed, byte with internal transitions, 0xFF edge case, TXEN-disabled no-op, bit-period timing.
2026-05-23 00:49:43 +07:00
/**
* AVR UART TX pin waveform synthesis
*
* avr8js's USART peripheral only intercepts the transmitted byte at the
* UDR0 data register it never toggles PD1 (Uno/Nano) / PE1 (Mega). The
* oscilloscope and any other GPIO consumer therefore see a flat line on
* the TX pin during Serial.print, which doesn't match real hardware.
*
* AVRSimulator.emitUartTxFrame() is the shim that closes that gap: when
* onByteTransmit fires it derives the 10-bit UART frame from the byte and
* the current USART config, then emits each bit transition through
* onPinChangeWithTime so the scope sees the same waveform a real ATmega328P
* would put on PD1.
*
* These tests assert that:
* - The TX pin is seeded HIGH (idle) when TXEN flips on.
* - Each byte produces a properly-timed start/data(LSB-first)/stop sequence
* on pin 1 at the configured baud rate.
* - Bytes that need no internal transitions (e.g. 0xFF) still emit the
* start-bit drop and the stop-bit rise.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { AVRSimulator } from '../simulation/AVRSimulator';
import { PinManager } from '../simulation/PinManager';
// ATmega328P USART0 register addresses
const UCSRA = 0xc0;
const UCSRB = 0xc1;
const UCSRC = 0xc2;
const UBRRL = 0xc4;
const UBRRH = 0xc5;
const UCSRB_RXEN = 0x10;
const UCSRB_TXEN = 0x08;
const UCSRC_UCSZ1 = 0x04;
const UCSRC_UCSZ0 = 0x02;
const EMPTY_HEX = ':00000001FF\n';
type PinEvent = { pin: number; state: boolean; timeMs: number };
function configureUsartFor115200(sim: AVRSimulator): void {
const cpu = (sim as unknown as { cpu: { data: Uint8Array } }).cpu;
cpu.data[UBRRH] = 0;
cpu.data[UBRRL] = 8; // 16M / (16*9) = 111111 baud (Arduino's actual 115200 setting)
cpu.data[UCSRC] = UCSRC_UCSZ1 | UCSRC_UCSZ0; // 8 data bits, no parity, 1 stop bit
cpu.data[UCSRA] = 0; // U2X=0 → multiplier 16
// Trigger the configuration-change hook by simulating a UCSRB write
cpu.data[UCSRB] = UCSRB_RXEN | UCSRB_TXEN;
// avr8js's writeHook for UCSRB updates internal state; the cleanest way to
// trigger it without running the firmware is to call onConfigurationChange
// directly (it's the callback we registered, so it's safe to invoke).
sim.usart!.onConfigurationChange?.();
}
beforeEach(() => {
let counter = 0;
let depth = 0;
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
if (depth === 0) {
depth++;
cb(0);
depth--;
}
return ++counter;
});
vi.stubGlobal('cancelAnimationFrame', vi.fn());
});
afterEach(() => vi.unstubAllGlobals());
describe('AVR USART → TX pin waveform synthesis', () => {
let pm: PinManager;
let sim: AVRSimulator;
let events: PinEvent[];
beforeEach(() => {
pm = new PinManager();
sim = new AVRSimulator(pm);
events = [];
sim.onPinChangeWithTime = (pin, state, timeMs) => {
events.push({ pin, state, timeMs });
};
sim.loadHex(EMPTY_HEX);
});
afterEach(() => sim.stop());
it('seeds the TX pin HIGH (idle) when TXEN flips 0 → 1', () => {
configureUsartFor115200(sim);
// The first thing the scope should see on PD1 is an idle-HIGH sample.
const txEvents = events.filter((e) => e.pin === 1);
expect(txEvents.length).toBeGreaterThanOrEqual(1);
expect(txEvents[0].state).toBe(true);
});
it('emits a complete 10-bit UART frame for a byte with internal transitions', () => {
configureUsartFor115200(sim);
events = []; // discard the idle-seed event so we only inspect the frame
// 'a' = 0x61 = 0b01100001 → LSB-first bits: 1, 0, 0, 0, 0, 1, 1, 0
// start bit0 bit1 bit2 bit3 bit4 bit5 bit6 bit7 stop
// LOW HIGH LOW LOW LOW LOW HIGH HIGH LOW HIGH
// Transitions vs. prev (starting from idle HIGH):
// t0 LOW (start), t1 HIGH (b0), t2 LOW (b1), t6 HIGH (b5),
// t8 LOW (b7), t9 HIGH (stop)
sim.usart!.onByteTransmit!(0x61);
const txEvents = events.filter((e) => e.pin === 1);
const states = txEvents.map((e) => e.state);
expect(states).toEqual([false, true, false, true, false, true]);
});
it('handles 0xFF (all ones) — only start bit drop, then stop-bit rise', () => {
configureUsartFor115200(sim);
events = [];
// 0xFF: start LOW, then 8x HIGH (no internal transitions), then stop HIGH.
// Only 1 LOW (start) and 1 HIGH (first data bit, which is also the rest).
sim.usart!.onByteTransmit!(0xff);
const txEvents = events.filter((e) => e.pin === 1);
expect(txEvents.map((e) => e.state)).toEqual([false, true]);
});
it('does not emit anything when TXEN is disabled', () => {
// Don't configure UCSRB — TXEN remains 0.
events = [];
sim.usart!.onByteTransmit!(0x61);
const txEvents = events.filter((e) => e.pin === 1);
expect(txEvents).toHaveLength(0);
});
it('uses the configured baud rate for bit timing (1 bit ≈ 1/baud seconds)', () => {
configureUsartFor115200(sim);
events = [];
// 0x00 produces transitions at: t0 LOW (start) and t9 HIGH (stop only).
sim.usart!.onByteTransmit!(0x00);
const txEvents = events.filter((e) => e.pin === 1);
expect(txEvents).toHaveLength(2);
const dtMs = txEvents[1].timeMs - txEvents[0].timeMs;
// 9 bit periods between start LOW and stop HIGH at 16M/(16*9) = 111111 baud:
// bitMs = 1000 / 111111 ≈ 0.009 ms, 9 * 0.009 ≈ 0.081 ms
expect(dtMs).toBeCloseTo((9 * 1000) / 111111, 3);
});
});