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

149 lines
5.4 KiB
TypeScript

/**
* 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);
});
});