193 lines
8.3 KiB
TypeScript
193 lines
8.3 KiB
TypeScript
/**
|
|
* ESP32-C3 blink integration test.
|
|
*
|
|
* Compiles a bare-metal C program with riscv32-esp-elf-gcc (the toolchain
|
|
* bundled with arduino-cli's ESP32 package), loads the raw binary into
|
|
* Esp32C3Simulator, and verifies that GPIO 8 toggles as expected.
|
|
*
|
|
* Why bare-metal and not a full Arduino sketch?
|
|
* The full ESP-IDF/Arduino framework requires dozens of peripherals
|
|
* (cache controller, interrupt matrix, RTC, FreeRTOS scheduler, etc.) that
|
|
* the browser-side emulator does not implement. A bare-metal program that
|
|
* writes directly to GPIO MMIO registers tests exactly what the emulator
|
|
* is built for: pure RV32IMC instruction execution with GPIO/UART MMIO.
|
|
*
|
|
* Source: frontend/src/__tests__/fixtures/esp32c3-blink/blink.c
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
|
import { execSync } from 'child_process';
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { Esp32C3Simulator } from '../simulation/Esp32C3Simulator';
|
|
import type { PinManager } from '../simulation/PinManager';
|
|
|
|
// ── Mocks (Node has no requestAnimationFrame) ─────────────────────────────────
|
|
vi.stubGlobal('requestAnimationFrame', (cb: () => void) => { cb(); return 0; });
|
|
vi.stubGlobal('cancelAnimationFrame', () => {});
|
|
|
|
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
const FIXTURE_DIR = join(__dirname, 'fixtures/esp32c3-blink');
|
|
const BIN_PATH = join(FIXTURE_DIR, 'blink.bin');
|
|
const DIS_PATH = join(FIXTURE_DIR, 'blink.dis');
|
|
const BUILD_SH = join(FIXTURE_DIR, 'build.sh');
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
function mockPinManager(): PinManager {
|
|
return {
|
|
setPinState: vi.fn(),
|
|
getPinState: vi.fn(() => false),
|
|
} as unknown as PinManager;
|
|
}
|
|
|
|
/** Run N CPU steps on the simulator's internal core */
|
|
function runSteps(sim: Esp32C3Simulator, n: number): void {
|
|
const core = (sim as unknown as { core: { step(): void } }).core;
|
|
for (let i = 0; i < n; i++) core.step();
|
|
}
|
|
|
|
// ── Build the binary once before all tests ────────────────────────────────────
|
|
let buildError: string | null = null;
|
|
let binData: Uint8Array;
|
|
|
|
beforeAll(() => {
|
|
// Rebuild whenever the source changes (or if binary is missing)
|
|
try {
|
|
execSync(`bash "${BUILD_SH}"`, { stdio: 'pipe' });
|
|
} catch (err: unknown) {
|
|
buildError = String((err as NodeJS.ErrnoException).stderr ?? err);
|
|
if (!existsSync(BIN_PATH)) return; // binary missing AND build failed
|
|
// Binary exists from a previous build — use it but warn
|
|
console.warn('[esp32c3-blink] Build script failed; using existing binary.\n' + buildError);
|
|
buildError = null;
|
|
}
|
|
binData = new Uint8Array(readFileSync(BIN_PATH));
|
|
});
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
describe('ESP32-C3 bare-metal blink (compiled with riscv32-esp-elf-gcc)', () => {
|
|
|
|
it('build.sh produces a non-empty blink.bin', () => {
|
|
if (buildError) throw new Error('Build failed:\n' + buildError);
|
|
expect(binData.byteLength).toBeGreaterThan(0);
|
|
expect(binData.byteLength).toBeLessThan(4096); // sanity: tiny bare-metal binary
|
|
console.log(`Binary size: ${binData.byteLength} bytes`);
|
|
});
|
|
|
|
it('binary starts with a valid RV32 instruction at 0x42000000', () => {
|
|
if (buildError) throw new Error('Build failed:\n' + buildError);
|
|
// First 4 bytes must decode as a valid 32-bit or 16-bit RISC-V instruction
|
|
const b0 = binData[0], b1 = binData[1];
|
|
const half = b0 | (b1 << 8);
|
|
const is32bit = (half & 0x3) === 0x3;
|
|
const is16bit = (half & 0x3) !== 0x3;
|
|
expect(is32bit || is16bit).toBe(true);
|
|
|
|
// First instruction in the disassembly should be _start at 0x42000000
|
|
if (existsSync(DIS_PATH)) {
|
|
const dis = readFileSync(DIS_PATH, 'utf8');
|
|
expect(dis).toContain('42000000 <_start>');
|
|
console.log('\nDisassembly (first 20 lines):\n' + dis.split('\n').slice(0, 20).join('\n'));
|
|
}
|
|
});
|
|
|
|
it('loadBin() loads the binary and resets PC to 0x42000000', () => {
|
|
if (buildError) throw new Error('Build failed:\n' + buildError);
|
|
const sim = new Esp32C3Simulator(mockPinManager());
|
|
sim.loadBin(binData);
|
|
const core = (sim as unknown as { core: { pc: number } }).core;
|
|
expect(core.pc).toBe(0x42000000);
|
|
});
|
|
|
|
it('GPIO 8 goes HIGH after the first SW to GPIO_W1TS', () => {
|
|
if (buildError) throw new Error('Build failed:\n' + buildError);
|
|
const pm = mockPinManager();
|
|
const sim = new Esp32C3Simulator(pm);
|
|
const pinEvents: Array<{ pin: number; state: boolean; timeMs: number }> = [];
|
|
sim.onPinChangeWithTime = (pin, state, timeMs) => pinEvents.push({ pin, state, timeMs });
|
|
|
|
sim.loadBin(binData);
|
|
// The blink program writes GPIO_W1TS within the first ~10 instructions
|
|
runSteps(sim, 20);
|
|
|
|
const gpio8Events = pinEvents.filter(e => e.pin === 8);
|
|
expect(gpio8Events.length).toBeGreaterThan(0);
|
|
expect(gpio8Events[0].state).toBe(true); // first event is LED ON
|
|
expect(gpio8Events[0].timeMs).toBeGreaterThan(0); // has a meaningful timestamp
|
|
|
|
console.log(`GPIO 8 went HIGH at ${gpio8Events[0].timeMs.toFixed(4)} ms (simulated)`);
|
|
});
|
|
|
|
it('GPIO 8 toggles ON and OFF within a full blink cycle', () => {
|
|
if (buildError) throw new Error('Build failed:\n' + buildError);
|
|
const pm = mockPinManager();
|
|
const sim = new Esp32C3Simulator(pm);
|
|
const pinEvents: Array<{ pin: number; state: boolean }> = [];
|
|
sim.onPinChangeWithTime = (pin, state) => pinEvents.push({ pin, state });
|
|
|
|
sim.loadBin(binData);
|
|
// delay(50) = ~200 instructions; two delays + GPIO writes ≈ 410 instructions/cycle
|
|
// Run 2000 steps → ~4 complete blink cycles
|
|
runSteps(sim, 2000);
|
|
|
|
const gpio8 = pinEvents.filter(e => e.pin === 8);
|
|
const highEvents = gpio8.filter(e => e.state === true);
|
|
const lowEvents = gpio8.filter(e => e.state === false);
|
|
|
|
expect(highEvents.length).toBeGreaterThanOrEqual(2); // at least 2 ON events
|
|
expect(lowEvents.length).toBeGreaterThanOrEqual(2); // at least 2 OFF events
|
|
|
|
// Events should strictly alternate: HIGH, LOW, HIGH, LOW ...
|
|
for (let i = 0; i < gpio8.length - 1; i++) {
|
|
expect(gpio8[i].state).not.toBe(gpio8[i + 1].state);
|
|
}
|
|
|
|
console.log(`GPIO 8 toggled ${gpio8.length} times in 2000 steps (${highEvents.length} ON, ${lowEvents.length} OFF)`);
|
|
});
|
|
|
|
it('PinManager.setPinState is called with correct pin and state', () => {
|
|
if (buildError) throw new Error('Build failed:\n' + buildError);
|
|
const pm = mockPinManager();
|
|
const sim = new Esp32C3Simulator(pm);
|
|
sim.loadBin(binData);
|
|
runSteps(sim, 2000);
|
|
|
|
// PinManager must have received GPIO 8 state changes
|
|
expect(pm.setPinState).toHaveBeenCalledWith(8, true);
|
|
expect(pm.setPinState).toHaveBeenCalledWith(8, false);
|
|
});
|
|
|
|
it('timestamps increase monotonically across blink events', () => {
|
|
if (buildError) throw new Error('Build failed:\n' + buildError);
|
|
const sim = new Esp32C3Simulator(mockPinManager());
|
|
const times: number[] = [];
|
|
sim.onPinChangeWithTime = (pin, _state, timeMs) => {
|
|
if (pin === 8) times.push(timeMs);
|
|
};
|
|
|
|
sim.loadBin(binData);
|
|
runSteps(sim, 2000);
|
|
|
|
expect(times.length).toBeGreaterThan(2);
|
|
for (let i = 1; i < times.length; i++) {
|
|
expect(times[i]).toBeGreaterThan(times[i - 1]);
|
|
}
|
|
const totalSimMs = times[times.length - 1];
|
|
console.log(`Simulated ${totalSimMs.toFixed(4)} ms over 2000 steps`);
|
|
});
|
|
|
|
it('reset() after run clears GPIO state', () => {
|
|
if (buildError) throw new Error('Build failed:\n' + buildError);
|
|
const sim = new Esp32C3Simulator(mockPinManager());
|
|
sim.loadBin(binData);
|
|
runSteps(sim, 500);
|
|
|
|
sim.reset();
|
|
expect(sim.isRunning()).toBe(false);
|
|
|
|
const core = (sim as unknown as { core: { pc: number }; gpioOut: number }).core;
|
|
expect(core.pc).toBe(0x42000000);
|
|
});
|
|
});
|