Add bare-metal ESP32-C3 blink example with simulator support
- Implemented a simple LED blink program in C for the ESP32-C3, targeting RV32IMC architecture. - Created a linker script to define memory layout for the ESP32-C3. - Added build script to compile the blink program using the riscv32-esp-elf-gcc toolchain. - Developed Esp32C3Simulator to emulate ESP32-C3 behavior in the browser, including GPIO and UART functionality. - Implemented utility functions to parse ESP32 flash images and handle loading of binary data. - Included disassembly output for debugging purposes. - Added necessary test fixtures for the blink example.
This commit is contained in:
parent
7ab0f132a3
commit
74e7ec58c1
|
|
@ -293,14 +293,12 @@ describe('Esp32Bridge — WebSocket protocol', () => {
|
|||
s3Bridge.disconnect();
|
||||
});
|
||||
|
||||
it('ESP32-C3 bridge sends esp32-c3 in start_esp32', () => {
|
||||
it('ESP32-C3 no longer uses Esp32Bridge — uses browser-side Esp32C3Simulator', () => {
|
||||
// ESP32-C3 was moved from QEMU to the browser RV32IMC emulator.
|
||||
// Creating an Esp32Bridge manually still works (the class is not removed),
|
||||
// but addBoard('esp32-c3') will now create an Esp32C3Simulator instead.
|
||||
const c3Bridge = new Esp32Bridge('test-esp32-c3', 'esp32-c3');
|
||||
c3Bridge.connect();
|
||||
const c3Ws = (c3Bridge as any).socket as MockWebSocket;
|
||||
c3Ws.open();
|
||||
const msg = JSON.parse(c3Ws.sent[0]);
|
||||
expect(msg.data.board).toBe('esp32-c3');
|
||||
c3Bridge.disconnect();
|
||||
expect(c3Bridge.boardKind).toBe('esp32-c3'); // bridge still instantiatable
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -330,10 +328,12 @@ describe('useSimulatorStore — ESP32 boards', () => {
|
|||
expect(getEsp32Bridge(id)?.boardKind).toBe('esp32-s3');
|
||||
});
|
||||
|
||||
it('addBoard("esp32-c3") creates bridge with correct boardKind', () => {
|
||||
it('addBoard("esp32-c3") creates an Esp32C3Simulator, not an Esp32Bridge', () => {
|
||||
const { addBoard } = useSimulatorStore.getState();
|
||||
const id = addBoard('esp32-c3', 300, 100);
|
||||
expect(getEsp32Bridge(id)?.boardKind).toBe('esp32-c3');
|
||||
// ESP32-C3 uses the browser-side RV32IMC emulator — no QEMU bridge
|
||||
expect(getEsp32Bridge(id)).toBeUndefined();
|
||||
expect(getBoardSimulator(id)).toBeDefined();
|
||||
});
|
||||
|
||||
it('addBoard creates a file group with sketch.ino (not script.py)', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,192 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,543 @@
|
|||
/**
|
||||
* ESP32-C3 Browser Emulation Tests
|
||||
*
|
||||
* Tests the RV32IMC (Integer + Multiply + Compressed) emulator used
|
||||
* for browser-side ESP32-C3 simulation without a QEMU backend.
|
||||
*
|
||||
* Test groups:
|
||||
* 1. RV32M — multiply/divide instructions (via RiscVCore directly)
|
||||
* 2. RV32C — 16-bit compressed instructions (via RiscVCore directly)
|
||||
* 3. Esp32C3Simulator — UART0 serial output
|
||||
* 4. Esp32C3Simulator — GPIO pin toggling
|
||||
* 5. Lifecycle — start/stop/reset
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { RiscVCore } from '../simulation/RiscVCore';
|
||||
import { Esp32C3Simulator } from '../simulation/Esp32C3Simulator';
|
||||
import type { PinManager } from '../simulation/PinManager';
|
||||
|
||||
// ── Node environment stubs ───────────────────────────────────────────────────
|
||||
|
||||
let rafDepth = 0;
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
// Allow a bounded number of recursive RAF calls to test the loop
|
||||
if (rafDepth < 2) { rafDepth++; cb(0); rafDepth--; }
|
||||
return 1;
|
||||
});
|
||||
vi.stubGlobal('cancelAnimationFrame', () => {});
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function writeWord(mem: Uint8Array, offset: number, val: number): void {
|
||||
mem[offset] = val & 0xFF;
|
||||
mem[offset + 1] = (val >> 8) & 0xFF;
|
||||
mem[offset + 2] = (val >> 16) & 0xFF;
|
||||
mem[offset + 3] = (val >> 24) & 0xFF;
|
||||
}
|
||||
|
||||
function writeHalf(mem: Uint8Array, offset: number, val: number): void {
|
||||
mem[offset] = val & 0xFF;
|
||||
mem[offset + 1] = (val >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
/** Run exactly n steps on a core */
|
||||
function runSteps(core: RiscVCore, n: number): void {
|
||||
for (let i = 0; i < n; i++) core.step();
|
||||
}
|
||||
|
||||
/** Create a minimal mock PinManager */
|
||||
function mockPinManager(): PinManager {
|
||||
return {
|
||||
setPinState: vi.fn(),
|
||||
getPinState: vi.fn(() => false),
|
||||
registerPin: vi.fn(),
|
||||
unregisterPin: vi.fn(),
|
||||
} as unknown as PinManager;
|
||||
}
|
||||
|
||||
/** Access the simulator's internal RiscVCore for direct testing */
|
||||
function getCore(sim: Esp32C3Simulator): RiscVCore {
|
||||
return (sim as unknown as { core: RiscVCore }).core;
|
||||
}
|
||||
|
||||
/** Access the simulator's internal flash buffer for direct programming */
|
||||
function getFlash(sim: Esp32C3Simulator): Uint8Array {
|
||||
return (sim as unknown as { flash: Uint8Array }).flash;
|
||||
}
|
||||
|
||||
// ── Test Group 1: RV32M (multiply/divide) ────────────────────────────────────
|
||||
|
||||
describe('RV32M — multiply/divide extension', () => {
|
||||
let mem: Uint8Array;
|
||||
let core: RiscVCore;
|
||||
|
||||
beforeEach(() => {
|
||||
mem = new Uint8Array(64);
|
||||
core = new RiscVCore(mem, 0);
|
||||
core.reset(0);
|
||||
});
|
||||
|
||||
it('MUL: 6 × 7 = 42', () => {
|
||||
writeWord(mem, 0, 0x00600093); // ADDI x1, x0, 6
|
||||
writeWord(mem, 4, 0x00700113); // ADDI x2, x0, 7
|
||||
writeWord(mem, 8, 0x022081B3); // MUL x3, x1, x2
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3]).toBe(42);
|
||||
});
|
||||
|
||||
it('MUL: negative × positive = negative', () => {
|
||||
writeWord(mem, 0, 0xFFF00093); // ADDI x1, x0, -1
|
||||
writeWord(mem, 4, 0x00300113); // ADDI x2, x0, 3
|
||||
writeWord(mem, 8, 0x022081B3); // MUL x3, x1, x2
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3]).toBe(-3);
|
||||
});
|
||||
|
||||
it('MULH: signed upper — (-1) × (-1) upper 32 bits = 0', () => {
|
||||
// (-1) * (-1) = 1; upper 32 bits of 64-bit result = 0
|
||||
writeWord(mem, 0, 0xFFF00093); // ADDI x1, x0, -1
|
||||
writeWord(mem, 4, 0xFFF00113); // ADDI x2, x0, -1
|
||||
writeWord(mem, 8, 0x022091B3); // MULH x3, x1, x2
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3]).toBe(0);
|
||||
});
|
||||
|
||||
it('MULHU: unsigned upper — 0xFFFFFFFF × 0xFFFFFFFF upper 32 bits', () => {
|
||||
// 0xFFFFFFFF * 0xFFFFFFFF = 0xFFFFFFFE_00000001; upper = 0xFFFFFFFE
|
||||
writeWord(mem, 0, 0xFFF00093); // ADDI x1, x0, -1 (= 0xFFFFFFFF unsigned)
|
||||
writeWord(mem, 4, 0xFFF00113); // ADDI x2, x0, -1
|
||||
writeWord(mem, 8, 0x022081B3 | (3 << 12)); // MULHU x3, x1, x2 (funct3=3)
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3] >>> 0).toBe(0xFFFFFFFE);
|
||||
});
|
||||
|
||||
it('DIV: 42 / 7 = 6', () => {
|
||||
writeWord(mem, 0, 0x02A00093); // ADDI x1, x0, 42
|
||||
writeWord(mem, 4, 0x00700113); // ADDI x2, x0, 7
|
||||
// DIV x3, x1, x2: opcode=0x33, rd=3, funct3=4, rs1=1, rs2=2, funct7=1
|
||||
// = (1<<25)|(2<<20)|(1<<15)|(4<<12)|(3<<7)|0x33 = 0x0220C1B3
|
||||
writeWord(mem, 8, 0x0220C1B3); // DIV x3, x1, x2
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3]).toBe(6);
|
||||
});
|
||||
|
||||
it('DIV: signed — -7 / 2 = -3 (truncate toward zero)', () => {
|
||||
writeWord(mem, 0, 0xFF900093); // ADDI x1, x0, -7
|
||||
writeWord(mem, 4, 0x00200113); // ADDI x2, x0, 2
|
||||
writeWord(mem, 8, 0x0220C1B3); // DIV x3, x1, x2
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3]).toBe(-3);
|
||||
});
|
||||
|
||||
it('DIV: divide by zero returns -1 (0xFFFFFFFF)', () => {
|
||||
writeWord(mem, 0, 0x00500093); // ADDI x1, x0, 5
|
||||
writeWord(mem, 4, 0x00000113); // ADDI x2, x0, 0
|
||||
writeWord(mem, 8, 0x0220C1B3); // DIV x3, x1, x2
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3]).toBe(-1);
|
||||
});
|
||||
|
||||
it('REM: 10 % 3 = 1', () => {
|
||||
writeWord(mem, 0, 0x00A00093); // ADDI x1, x0, 10
|
||||
writeWord(mem, 4, 0x00300113); // ADDI x2, x0, 3
|
||||
// REM x3, x1, x2: funct3=6 → (1<<25)|(2<<20)|(1<<15)|(6<<12)|(3<<7)|0x33 = 0x0220E1B3
|
||||
writeWord(mem, 8, 0x0220E1B3); // REM x3, x1, x2
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3]).toBe(1);
|
||||
});
|
||||
|
||||
it('REM: divide by zero returns dividend', () => {
|
||||
writeWord(mem, 0, 0x00700093); // ADDI x1, x0, 7
|
||||
writeWord(mem, 4, 0x00000113); // ADDI x2, x0, 0
|
||||
writeWord(mem, 8, 0x0220E1B3); // REM x3, x1, x2
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[3]).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Test Group 2: RV32C (compressed instructions) ───────────────────────────
|
||||
|
||||
describe('RV32C — 16-bit compressed instruction extension', () => {
|
||||
let mem: Uint8Array;
|
||||
let core: RiscVCore;
|
||||
|
||||
beforeEach(() => {
|
||||
mem = new Uint8Array(64);
|
||||
core = new RiscVCore(mem, 0);
|
||||
core.reset(0);
|
||||
});
|
||||
|
||||
it('C.LI x1, 5: loads immediate 5 into x1 and advances PC by 2', () => {
|
||||
// C.LI: funct3=010, imm[5]=0, rd=x1(00001), imm[4:0]=00101, op=01
|
||||
// Encoding: bit15..0 = 0_100_0_00001_00101_01 = 0x4095
|
||||
writeHalf(mem, 0, 0x4095);
|
||||
core.step();
|
||||
expect(core.regs[1]).toBe(5);
|
||||
expect(core.pc).toBe(2);
|
||||
});
|
||||
|
||||
it('C.LI x2, -1: sign-extends negative immediate', () => {
|
||||
// C.LI: funct3=010, imm[5]=1, rd=x2(00010), imm[4:0]=11111, op=01
|
||||
// Encoding: bit15..0 = 0_100_1_00010_11111_01 = 0x5105
|
||||
// Checking: bit12=1, bits[11:7]=00010=2, bits[6:2]=11111=31, bits[1:0]=01
|
||||
// Value: 0_1_0_0 | 1_0_0_0 | 1_0_1_1 | 1_1_1_0_1 ... let me compute:
|
||||
// op=01: bit1=0,bit0=1; imm[4:0]=11111: bit6=1,bit5=1,bit4=1,bit3=1,bit2=1
|
||||
// rd=00010: bit11=0,bit10=0,bit9=0,bit8=1,bit7=0
|
||||
// imm[5]=1: bit12=1; funct3=010: bit15=0,bit14=1,bit13=0
|
||||
// = 0100 1000 1111 1101 wait...
|
||||
// bit15=0,14=1,13=0,12=1,11=0,10=0,9=0,8=1,7=0,6=1,5=1,4=1,3=1,2=1,1=0,0=1
|
||||
// = 0101 0001 0111 1101 = 0x517D? Let me just compute the halfword value:
|
||||
// Bits (from bit15 to bit0):
|
||||
// 0,1,0,1 | 0,0,0,1 | 0,1,1,1 | 1,1,0,1
|
||||
// wait: funct3=010 → bits[15:13]=010: bit15=0,bit14=1,bit13=0
|
||||
// bit12=1 (imm[5]=1)
|
||||
// rd=2=00010: bit11=0,bit10=0,bit9=0,bit8=1,bit7=0
|
||||
// imm[4:0]=11111: bit6=1,bit5=1,bit4=1,bit3=1,bit2=1
|
||||
// op=01: bit1=0,bit0=1
|
||||
// = 0100_1000_1111_1101 = 0x48FD? Wait let me re-group:
|
||||
// bits 15..12: 0,1,0,1 = 0x5... no: bit15=0,bit14=1,bit13=0,bit12=1 → 0101 = 5? That's nibble 0101=5
|
||||
// bits 11..8: 0,0,0,1 → 0001 = 1
|
||||
// bits 7..4: 0,1,1,1 → 0111 = 7
|
||||
// bits 3..0: 1,1,0,1 → 1101 = D
|
||||
// So 0x517D. Let's verify the C.LI decode:
|
||||
// half=0x517D: op=0x1, funct3=(0x517D>>13)&7=(0x28)&7=... 0x517D=20861, 20861>>13=2, 2&7=2 ✓ (funct3=2=C.LI)
|
||||
// bit12=(0x517D>>12)&1=5&1=1 ✓
|
||||
// rd=(0x517D>>7)&31=(0xA2)&31... 0x517D>>7=163, 163&31=3? Hmm that gives rd=3 not rd=2...
|
||||
// 0x517D in binary: 0101 0001 0111 1101
|
||||
// bits[11:7]: bit11=0,bit10=0,bit9=0,bit8=1,bit7=0 = 00010 = 2 ✓
|
||||
// But (0x517D>>7) = 0101 0001 0 = 162, 162&31=162-160=2 ✓ (I miscalculated before)
|
||||
// bits[6:2]: bit6=1,bit5=1,bit4=1,bit3=1,bit2=1 = 11111 = 31 ✓
|
||||
// imm6 = sext((1<<5)|(31), 6) = sext(63, 6) = sext(0b111111, 6) = -1 ✓
|
||||
writeHalf(mem, 0, 0x517D);
|
||||
core.step();
|
||||
expect(core.regs[2]).toBe(-1);
|
||||
expect(core.pc).toBe(2);
|
||||
});
|
||||
|
||||
it('C.ADDI x1, 3: adds immediate to register', () => {
|
||||
// Preset x1=10
|
||||
writeWord(mem, 0, 0x00A00093); // ADDI x1, x0, 10 (32-bit)
|
||||
// C.ADDI x1, 3: funct3=000, imm[5]=0, rd=x1, imm[4:0]=00011, op=01
|
||||
// bit12=0, bits[11:7]=00001, bits[6:2]=00011, bits[1:0]=01
|
||||
// = 0000 0000 1000 1101 = 0x008D
|
||||
writeHalf(mem, 4, 0x008D);
|
||||
runSteps(core, 2);
|
||||
expect(core.regs[1]).toBe(13);
|
||||
expect(core.pc).toBe(6);
|
||||
});
|
||||
|
||||
it('C.MV x5, x1: copies register (ADD x5, x0, x1)', () => {
|
||||
// Preset x1=42
|
||||
writeWord(mem, 0, 0x02A00093); // ADDI x1, x0, 42
|
||||
// C.MV x5, x1: funct3=100, bit12=0, rd=x5(00101), rs2=x1(00001), op=10
|
||||
// bit15=1,14=0,13=0,12=0,bits[11:7]=00101=5,bits[6:2]=00001=1,bits[1:0]=10
|
||||
// = 1000 0010 1000 0110 = 0x8286
|
||||
writeHalf(mem, 4, 0x8286);
|
||||
runSteps(core, 2);
|
||||
expect(core.regs[5]).toBe(42);
|
||||
});
|
||||
|
||||
it('C.ADD x1, x2: adds two registers', () => {
|
||||
writeWord(mem, 0, 0x00300093); // ADDI x1, x0, 3
|
||||
writeWord(mem, 4, 0x00400113); // ADDI x2, x0, 4
|
||||
// C.ADD x1, x2: funct3=100, bit12=1, rd=x1(00001), rs2=x2(00010), op=10
|
||||
// bit15=1,14=0,13=0,12=1,bits[11:7]=00001,bits[6:2]=00010,bits[1:0]=10
|
||||
// = 1001 0000 1000 1010 = 0x908A
|
||||
writeHalf(mem, 8, 0x908A);
|
||||
runSteps(core, 3);
|
||||
expect(core.regs[1]).toBe(7);
|
||||
});
|
||||
|
||||
it('C.J +4: jumps forward 4 bytes from compressed instruction', () => {
|
||||
// C.J with offset=4, starting at PC=0
|
||||
// CJ format: funct3=101, imm[3:1]=010 → bits[5:3]=010 → bit4=1, bits[1:0]=01
|
||||
// = 1010_0000_0001_0001 = 0xA011
|
||||
writeHalf(mem, 0, 0xA011);
|
||||
core.step();
|
||||
expect(core.pc).toBe(4); // 0 + 4
|
||||
});
|
||||
|
||||
it('C.BEQZ x8, offset: branch taken when register is zero', () => {
|
||||
// x8 is 0 (default), so branch should be taken
|
||||
// C.BEQZ x8, +4: rs1'=x8(=0 encoded as 0b000), offset=4
|
||||
// CB format: funct3=110, imm[8]=0, rs1'=000, imm[7:6]=00, imm[2:1]=10, imm[5]=0, op=01
|
||||
// offset=4: imm[2:1]=10 → bits[4:3]=10=2, other imm bits=0
|
||||
// bit15=0,14=1,13=1,12=0,bit11=0,bit10=0,bits[9:7]=000,bit6=0,bit5=0,bits[4:3]=10,bit2=0,bit1=0,bit0=1
|
||||
// = 0110 0000 0001 0001 ... let me compute more carefully
|
||||
// CB: bits[15:13]=110, bit[12]=imm[8]=0, bits[11:10]=imm[4:3]=00, bits[9:7]=rs1'=000
|
||||
// bits[6:5]=imm[7:6]=00, bits[4:3]=imm[2:1]=10, bit[2]=imm[5]=0, bits[1:0]=01
|
||||
// For offset=4: imm[2:1]=10 → bits[4:3]=10 → bit4=1, bit3=0
|
||||
// = 1100_0000_0001_0001 = 0xC011
|
||||
writeHalf(mem, 0, 0xC011);
|
||||
core.step();
|
||||
expect(core.pc).toBe(4);
|
||||
});
|
||||
|
||||
it('C.BEQZ x8, offset: branch NOT taken when register is non-zero', () => {
|
||||
core.regs[8] = 5;
|
||||
writeHalf(mem, 0, 0xC011); // C.BEQZ x8, +4
|
||||
core.step();
|
||||
expect(core.pc).toBe(2); // falls through
|
||||
});
|
||||
|
||||
it('C.SWSP + C.LWSP: stack round-trip', () => {
|
||||
// Set sp (x2) to offset 32 within our buffer (so stack writes stay in bounds)
|
||||
core.regs[2] = 32;
|
||||
core.regs[1] = 0xDEAD;
|
||||
|
||||
// C.SWSP rs2=x1, offset=0:
|
||||
// CSS: funct3=110, uimm[5:2]=bits[12:9]=0000, uimm[7:6]=bits[8:7]=00, rs2=bits[6:2]=00001, op=10
|
||||
// = 1101 0000 0000 0110 = 0xD006? let me compute:
|
||||
// bit15=1,14=1,13=0,12=0,bits[11:10]=uimm[5:4]=00,bits[9:7]=uimm[3:1]=000,bits[6:2]=rs2=00001,bits[1:0]=10
|
||||
// Wait the spec says: bits[12:9]=uimm[5:2], bits[8:7]=uimm[7:6]
|
||||
// For offset=0: all uimm bits=0 → bits[12:9]=0000, bits[8:7]=00
|
||||
// = 1101 0000 0000 0110 = 0xD006?
|
||||
// bit15=1,bit14=1,bit13=0,bit12=0 → 1100
|
||||
// bits[11:10]=00, bits[9:8]=00 → 0000
|
||||
// bits[7]=0 → 0
|
||||
// bits[6:2]=00001 → bit6=0,bit5=0,bit4=0,bit3=0,bit2=1
|
||||
// bits[1:0]=10
|
||||
// = 1100 0000 0000 0110 = 0xC006
|
||||
writeHalf(mem, 0, 0xC006); // C.SWSP x1, 0(sp)
|
||||
|
||||
// C.LWSP rd=x3, offset=0:
|
||||
// CI: funct3=010, bit12=uimm[5]=0, rd=x3=00011, bits[6:4]=uimm[4:2]=000, bits[3:2]=uimm[7:6]=00, op=10
|
||||
// = 0100 0001 1000 0010 = 0x4182
|
||||
writeHalf(mem, 2, 0x4182); // C.LWSP x3, 0(sp)
|
||||
|
||||
runSteps(core, 2);
|
||||
expect(core.regs[3]).toBe(0xDEAD);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Test Group 3: Esp32C3Simulator — UART ───────────────────────────────────
|
||||
|
||||
describe('Esp32C3Simulator — UART0 serial output', () => {
|
||||
let sim: Esp32C3Simulator;
|
||||
|
||||
beforeEach(() => {
|
||||
sim = new Esp32C3Simulator(mockPinManager());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sim.stop();
|
||||
});
|
||||
|
||||
it('writing to UART0 FIFO (0x60000000) triggers onSerialData', () => {
|
||||
const received: string[] = [];
|
||||
sim.onSerialData = (ch) => received.push(ch);
|
||||
|
||||
const flash = getFlash(sim);
|
||||
const core = getCore(sim);
|
||||
|
||||
// Program at IROM offset 0 (= address 0x42000000):
|
||||
// LUI a1, 0x60000 → a1 = 0x60000000 (UART0_BASE)
|
||||
// ADDI a0, x0, 72 → a0 = 72 = 'H'
|
||||
// SB a0, 0(a1) → write byte to UART0 FIFO
|
||||
writeWord(flash, 0, 0x600005B7); // LUI a1, 0x60000
|
||||
writeWord(flash, 4, 0x04800513); // ADDI a0, x0, 72
|
||||
writeWord(flash, 8, 0x00A58023); // SB a0, 0(a1)
|
||||
|
||||
core.reset(0x42000000);
|
||||
runSteps(core, 3);
|
||||
|
||||
expect(received).toEqual(['H']);
|
||||
});
|
||||
|
||||
it('writing multiple bytes emits each character', () => {
|
||||
const received: string[] = [];
|
||||
sim.onSerialData = (ch) => received.push(ch);
|
||||
|
||||
const flash = getFlash(sim);
|
||||
const core = getCore(sim);
|
||||
|
||||
// LUI a1, 0x60000 → a1 = UART0_BASE
|
||||
// ADDI a0, x0, 65 ('A')
|
||||
// SB a0, 0(a1)
|
||||
// ADDI a0, x0, 66 ('B')
|
||||
// SB a0, 0(a1)
|
||||
writeWord(flash, 0, 0x600005B7); // LUI a1, 0x60000
|
||||
writeWord(flash, 4, 0x04100513); // ADDI a0, x0, 65 ('A')
|
||||
writeWord(flash, 8, 0x00A58023); // SB a0, 0(a1)
|
||||
writeWord(flash, 12, 0x04200513); // ADDI a0, x0, 66 ('B')
|
||||
writeWord(flash, 16, 0x00A58023); // SB a0, 0(a1)
|
||||
|
||||
core.reset(0x42000000);
|
||||
runSteps(core, 5);
|
||||
|
||||
expect(received).toEqual(['A', 'B']);
|
||||
});
|
||||
|
||||
it('serialWrite injects bytes into RX FIFO, firmware can read them', () => {
|
||||
const flash = getFlash(sim);
|
||||
const core = getCore(sim);
|
||||
|
||||
sim.serialWrite('X');
|
||||
|
||||
// Program: LB a0, 0(a1) — reads from UART0_FIFO
|
||||
// LUI a1, 0x60000 → a1 = 0x60000000
|
||||
// LBU a0, 0(a1) → a0 = UART0_FIFO read
|
||||
writeWord(flash, 0, 0x600005B7); // LUI a1, 0x60000
|
||||
writeWord(flash, 4, 0x00058503); // LBU a0, 0(a1)
|
||||
|
||||
core.reset(0x42000000);
|
||||
runSteps(core, 2);
|
||||
|
||||
expect(core.regs[10]).toBe('X'.charCodeAt(0)); // a0 = 88 = 'X'
|
||||
});
|
||||
});
|
||||
|
||||
// ── Test Group 4: Esp32C3Simulator — GPIO ────────────────────────────────────
|
||||
|
||||
describe('Esp32C3Simulator — GPIO pin toggling', () => {
|
||||
let sim: Esp32C3Simulator;
|
||||
|
||||
beforeEach(() => {
|
||||
sim = new Esp32C3Simulator(mockPinManager());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sim.stop();
|
||||
});
|
||||
|
||||
it('SW to GPIO_OUT_W1TS (offset +8) sets GPIO0 high', () => {
|
||||
const pinChanges: Array<{ pin: number; state: boolean }> = [];
|
||||
sim.onPinChangeWithTime = (pin, state) => pinChanges.push({ pin, state });
|
||||
|
||||
const flash = getFlash(sim);
|
||||
const core = getCore(sim);
|
||||
|
||||
// LUI t1, 0x60004 → t1 = 0x60004000 (GPIO_BASE)
|
||||
// ADDI t0, x0, 1 → t0 = 1 (bit 0 = GPIO0)
|
||||
// SW t0, 8(t1) → write to GPIO_OUT_W1TS
|
||||
writeWord(flash, 0, 0x60004337); // LUI t1, 0x60004
|
||||
writeWord(flash, 4, 0x00100293); // ADDI t0, x0, 1
|
||||
writeWord(flash, 8, 0x00532423); // SW t0, 8(t1) [offset 8 = W1TS]
|
||||
|
||||
core.reset(0x42000000);
|
||||
runSteps(core, 3);
|
||||
|
||||
expect(pinChanges).toContainEqual({ pin: 0, state: true });
|
||||
});
|
||||
|
||||
it('SW to GPIO_OUT_W1TC (offset +12) clears GPIO0', () => {
|
||||
const pinChanges: Array<{ pin: number; state: boolean }> = [];
|
||||
sim.onPinChangeWithTime = (pin, state) => pinChanges.push({ pin, state });
|
||||
|
||||
const flash = getFlash(sim);
|
||||
const core = getCore(sim);
|
||||
|
||||
// First set GPIO0 high via W1TS, then clear via W1TC
|
||||
writeWord(flash, 0, 0x60004337); // LUI t1, 0x60004
|
||||
writeWord(flash, 4, 0x00100293); // ADDI t0, x0, 1
|
||||
writeWord(flash, 8, 0x00532423); // SW t0, 8(t1) — set bit 0 (W1TS)
|
||||
writeWord(flash, 12, 0x00532623); // SW t0, 12(t1) — clear bit 0 (W1TC)
|
||||
|
||||
core.reset(0x42000000);
|
||||
runSteps(core, 4);
|
||||
|
||||
expect(pinChanges).toContainEqual({ pin: 0, state: true });
|
||||
expect(pinChanges).toContainEqual({ pin: 0, state: false });
|
||||
});
|
||||
|
||||
it('SW to GPIO_OUT sets multiple pins via direct write', () => {
|
||||
const setPins: number[] = [];
|
||||
sim.onPinChangeWithTime = (pin, state) => { if (state) setPins.push(pin); };
|
||||
|
||||
const flash = getFlash(sim);
|
||||
const core = getCore(sim);
|
||||
|
||||
// Write 0b101 (bits 0 and 2) to GPIO_OUT (offset +4)
|
||||
writeWord(flash, 0, 0x60004337); // LUI t1, 0x60004
|
||||
writeWord(flash, 4, 0x00500293); // ADDI t0, x0, 5 (0b101)
|
||||
writeWord(flash, 8, 0x00532223); // SW t0, 4(t1) — GPIO_OUT
|
||||
|
||||
core.reset(0x42000000);
|
||||
runSteps(core, 3);
|
||||
|
||||
expect(setPins).toContain(0);
|
||||
expect(setPins).toContain(2);
|
||||
expect(setPins).not.toContain(1);
|
||||
});
|
||||
|
||||
it('pinManager.setPinState is called on GPIO change', () => {
|
||||
const pm = mockPinManager();
|
||||
const s = new Esp32C3Simulator(pm);
|
||||
const flash = getFlash(s);
|
||||
const core = getCore(s);
|
||||
|
||||
writeWord(flash, 0, 0x60004337); // LUI t1, 0x60004
|
||||
writeWord(flash, 4, 0x00100293); // ADDI t0, x0, 1
|
||||
writeWord(flash, 8, 0x00532423); // SW t0, 8(t1)
|
||||
|
||||
core.reset(0x42000000);
|
||||
runSteps(core, 3);
|
||||
|
||||
expect(pm.setPinState).toHaveBeenCalledWith(0, true);
|
||||
s.stop();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Test Group 5: Lifecycle ──────────────────────────────────────────────────
|
||||
|
||||
describe('Esp32C3Simulator — lifecycle', () => {
|
||||
it('starts not running', () => {
|
||||
const sim = new Esp32C3Simulator(mockPinManager());
|
||||
expect(sim.isRunning()).toBe(false);
|
||||
sim.stop();
|
||||
});
|
||||
|
||||
it('start() sets running, stop() clears it', () => {
|
||||
const sim = new Esp32C3Simulator(mockPinManager());
|
||||
sim.start();
|
||||
expect(sim.isRunning()).toBe(true);
|
||||
sim.stop();
|
||||
expect(sim.isRunning()).toBe(false);
|
||||
});
|
||||
|
||||
it('reset() stops simulator and clears register state', () => {
|
||||
const sim = new Esp32C3Simulator(mockPinManager());
|
||||
const core = getCore(sim);
|
||||
core.regs[1] = 999;
|
||||
sim.start();
|
||||
sim.reset();
|
||||
expect(sim.isRunning()).toBe(false);
|
||||
expect(core.regs[1]).toBe(0);
|
||||
expect(core.pc).toBe(0x42000000);
|
||||
});
|
||||
|
||||
it('reset() clears GPIO output state', () => {
|
||||
const pinChanges: Array<{ pin: number; state: boolean }> = [];
|
||||
const sim = new Esp32C3Simulator(mockPinManager());
|
||||
sim.onPinChangeWithTime = (pin, state) => pinChanges.push({ pin, state });
|
||||
|
||||
const flash = getFlash(sim);
|
||||
const core = getCore(sim);
|
||||
writeWord(flash, 0, 0x60004337);
|
||||
writeWord(flash, 4, 0x00100293);
|
||||
writeWord(flash, 8, 0x00532423);
|
||||
core.reset(0x42000000);
|
||||
runSteps(core, 3);
|
||||
|
||||
sim.reset();
|
||||
|
||||
const gpioOut = (sim as unknown as { gpioOut: number }).gpioOut;
|
||||
expect(gpioOut).toBe(0);
|
||||
});
|
||||
|
||||
it('double start() is a no-op (does not create duplicate loops)', () => {
|
||||
let rafCalls = 0;
|
||||
vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
|
||||
rafCalls++;
|
||||
// Don't recurse
|
||||
return rafCalls;
|
||||
});
|
||||
|
||||
const sim = new Esp32C3Simulator(mockPinManager());
|
||||
sim.start();
|
||||
sim.start(); // second call should be ignored
|
||||
expect(rafCalls).toBe(1);
|
||||
sim.stop();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* ESP32-C3 bare-metal LED blink — no ESP-IDF, no FreeRTOS.
|
||||
*
|
||||
* Targets RV32IMC directly. Compiled with the riscv32-esp-elf-gcc toolchain
|
||||
* bundled inside the arduino-cli ESP32 package.
|
||||
*
|
||||
* Memory map (matches Esp32C3Simulator):
|
||||
* Flash (IROM) @ 0x42000000 — code lands here
|
||||
* DRAM @ 0x3FC80000 — stack/data
|
||||
* GPIO_W1TS @ 0x60004008 — set pin high
|
||||
* GPIO_W1TC @ 0x6000400C — set pin low
|
||||
*
|
||||
* LED: GPIO 8 (ESP32-C3-DevKit onboard LED)
|
||||
*/
|
||||
|
||||
#define GPIO_W1TS (*(volatile unsigned int *)0x60004008u)
|
||||
#define GPIO_W1TC (*(volatile unsigned int *)0x6000400Cu)
|
||||
|
||||
#define LED_PIN 8u
|
||||
#define LED_BIT (1u << LED_PIN)
|
||||
|
||||
/* Short delay — small enough for the test to run fast */
|
||||
static void delay(int n) {
|
||||
for (volatile int i = 0; i < n; i++);
|
||||
}
|
||||
|
||||
/* Entry point — the linker script places _start at 0x42000000 */
|
||||
void _start(void) {
|
||||
while (1) {
|
||||
GPIO_W1TS = LED_BIT; /* LED ON — sets bit 8 of gpioOut */
|
||||
delay(50);
|
||||
GPIO_W1TC = LED_BIT; /* LED OFF — clears bit 8 of gpioOut */
|
||||
delay(50);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
E:/Hardware/wokwi_clon/frontend/src/__tests__/fixtures/esp32c3-blink/blink.elf: file format elf32-littleriscv
|
||||
|
||||
|
||||
Disassembly of section .text:
|
||||
|
||||
42000000 <_start>:
|
||||
42000000: 60004737 lui x14,0x60004
|
||||
42000004: 1141 addi x2,x2,-16
|
||||
42000006: 00870613 addi x12,x14,8 # 60004008 <_start+0x1e004008>
|
||||
4200000a: 10000593 li x11,256
|
||||
4200000e: 10000513 li x10,256
|
||||
42000012: 03100693 li x13,49
|
||||
42000016: c208 sw x10,0(x12)
|
||||
42000018: c602 sw x0,12(x2)
|
||||
4200001a: 47b2 lw x15,12(x2)
|
||||
4200001c: 00f6db63 bge x13,x15,42000032 <_start+0x32>
|
||||
42000020: c74c sw x11,12(x14)
|
||||
42000022: c402 sw x0,8(x2)
|
||||
42000024: 47a2 lw x15,8(x2)
|
||||
42000026: fef6c8e3 blt x13,x15,42000016 <_start+0x16>
|
||||
4200002a: 47a2 lw x15,8(x2)
|
||||
4200002c: 0785 addi x15,x15,1
|
||||
4200002e: c43e sw x15,8(x2)
|
||||
42000030: bfd5 j 42000024 <_start+0x24>
|
||||
42000032: 47b2 lw x15,12(x2)
|
||||
42000034: 0785 addi x15,x15,1
|
||||
42000036: c63e sw x15,12(x2)
|
||||
42000038: b7cd j 4200001a <_start+0x1a>
|
||||
Binary file not shown.
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env bash
|
||||
# Compile the bare-metal ESP32-C3 blink program using the riscv32-esp-elf-gcc
|
||||
# toolchain that ships with the arduino-cli ESP32 package.
|
||||
#
|
||||
# Output:
|
||||
# blink.elf — ELF with debug info
|
||||
# blink.bin — raw binary, loaded directly into Esp32C3Simulator flash buffer
|
||||
# blink.dis — disassembly for debugging
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ── Locate toolchain ──────────────────────────────────────────────────────────
|
||||
ARDUINO15="${LOCALAPPDATA:-$HOME/.arduino15}/Arduino15"
|
||||
TOOLCHAIN_BASE="$ARDUINO15/packages/esp32/tools/riscv32-esp-elf-gcc"
|
||||
|
||||
if [ ! -d "$TOOLCHAIN_BASE" ]; then
|
||||
echo "ERROR: ESP32 RISC-V toolchain not found at $TOOLCHAIN_BASE"
|
||||
echo "Install with: arduino-cli core install esp32:esp32"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pick the newest version directory
|
||||
TOOLCHAIN_VER=$(ls "$TOOLCHAIN_BASE" | sort -V | tail -1)
|
||||
TOOLCHAIN_BIN="$TOOLCHAIN_BASE/$TOOLCHAIN_VER/bin"
|
||||
|
||||
GCC="$TOOLCHAIN_BIN/riscv32-esp-elf-gcc"
|
||||
OBJCOPY="$TOOLCHAIN_BIN/riscv32-esp-elf-objcopy"
|
||||
OBJDUMP="$TOOLCHAIN_BIN/riscv32-esp-elf-objdump"
|
||||
|
||||
if [ ! -f "$GCC.exe" ] && [ ! -f "$GCC" ]; then
|
||||
echo "ERROR: riscv32-esp-elf-gcc not found in $TOOLCHAIN_BIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# On Windows inside Git Bash, .exe is appended automatically
|
||||
GCC="${GCC}.exe" 2>/dev/null || GCC="$GCC"
|
||||
OBJCOPY="${OBJCOPY}.exe" 2>/dev/null || OBJCOPY="$OBJCOPY"
|
||||
OBJDUMP="${OBJDUMP}.exe" 2>/dev/null || OBJDUMP="$OBJDUMP"
|
||||
|
||||
echo "Toolchain: $TOOLCHAIN_BIN"
|
||||
echo "Compiler: $GCC"
|
||||
|
||||
# ── Compile ───────────────────────────────────────────────────────────────────
|
||||
"$GCC" \
|
||||
-march=rv32imc \
|
||||
-mabi=ilp32 \
|
||||
-Os \
|
||||
-ffunction-sections \
|
||||
-fdata-sections \
|
||||
-nostdlib \
|
||||
-nostartfiles \
|
||||
-T "$SCRIPT_DIR/link.ld" \
|
||||
-Wl,--gc-sections \
|
||||
-o "$SCRIPT_DIR/blink.elf" \
|
||||
"$SCRIPT_DIR/blink.c"
|
||||
|
||||
# ── Extract raw binary (flash offset 0 → loads at IROM_BASE 0x42000000) ───────
|
||||
"$OBJCOPY" -O binary "$SCRIPT_DIR/blink.elf" "$SCRIPT_DIR/blink.bin"
|
||||
|
||||
# ── Disassembly for inspection ─────────────────────────────────────────────────
|
||||
"$OBJDUMP" -d -M numeric "$SCRIPT_DIR/blink.elf" > "$SCRIPT_DIR/blink.dis"
|
||||
|
||||
BIN_SIZE=$(wc -c < "$SCRIPT_DIR/blink.bin" | tr -d ' ')
|
||||
echo ""
|
||||
echo "✓ blink.bin — ${BIN_SIZE} bytes"
|
||||
echo "✓ blink.dis — disassembly"
|
||||
echo ""
|
||||
echo "Entry point: 0x42000000 (first instruction in flash)"
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Linker script for ESP32-C3 bare-metal blink.
|
||||
*
|
||||
* Places .text at 0x42000000 (IROM_BASE — the flash instruction region
|
||||
* that Esp32C3Simulator maps as its primary memory).
|
||||
* Stack lives at the top of DRAM (0x3FC80000 + 384KB).
|
||||
*/
|
||||
OUTPUT_ARCH(riscv)
|
||||
ENTRY(_start)
|
||||
|
||||
MEMORY {
|
||||
IROM (rx) : ORIGIN = 0x42000000, LENGTH = 4M
|
||||
DRAM (rwx) : ORIGIN = 0x3FC80000, LENGTH = 384K
|
||||
}
|
||||
|
||||
SECTIONS {
|
||||
.text 0x42000000 : {
|
||||
KEEP(*(.text.startup))
|
||||
*(.text*)
|
||||
*(.rodata*)
|
||||
} > IROM
|
||||
|
||||
.data : {
|
||||
*(.data*)
|
||||
} > DRAM AT > IROM
|
||||
|
||||
.bss : {
|
||||
*(.bss*)
|
||||
*(COMMON)
|
||||
} > DRAM
|
||||
|
||||
/DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) }
|
||||
}
|
||||
|
|
@ -131,7 +131,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
const handleRun = () => {
|
||||
if (activeBoardId) {
|
||||
const board = boards.find((b) => b.id === activeBoardId);
|
||||
const isQemuBoard = board?.boardKind === 'raspberry-pi-3' || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3' || board?.boardKind === 'esp32-c3';
|
||||
const isQemuBoard = board?.boardKind === 'raspberry-pi-3' || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3';
|
||||
if (isQemuBoard || board?.compiledProgram) {
|
||||
startBoard(activeBoardId);
|
||||
setMessage(null);
|
||||
|
|
@ -213,7 +213,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
const boardsList = useSimulatorStore.getState().boards;
|
||||
for (const board of boardsList) {
|
||||
const isQemu = board.boardKind === 'raspberry-pi-3' ||
|
||||
board.boardKind === 'esp32' || board.boardKind === 'esp32-s3' || board.boardKind === 'esp32-c3';
|
||||
board.boardKind === 'esp32' || board.boardKind === 'esp32-s3';
|
||||
if (!board.running && (isQemu || board.compiledProgram)) {
|
||||
startBoard(board.id);
|
||||
}
|
||||
|
|
@ -306,7 +306,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
{/* Run */}
|
||||
<button
|
||||
onClick={handleRun}
|
||||
disabled={running || (!['raspberry-pi-3','esp32','esp32-s3','esp32-c3'].includes(activeBoard?.boardKind ?? '') && !compiledHex && !activeBoard?.compiledProgram)}
|
||||
disabled={running || (!['raspberry-pi-3','esp32','esp32-s3'].includes(activeBoard?.boardKind ?? '') && !compiledHex && !activeBoard?.compiledProgram)}
|
||||
className="tb-btn tb-btn-run"
|
||||
title="Run"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,327 @@
|
|||
/**
|
||||
* Esp32C3Simulator — Browser-side ESP32-C3 emulator.
|
||||
*
|
||||
* Wraps RiscVCore (RV32IMC) with:
|
||||
* - ESP32-C3 memory map: Flash IROM/DROM @ 0x42000000/0x3C000000,
|
||||
* DRAM @ 0x3FC80000, IRAM @ 0x4037C000
|
||||
* - UART0 MMIO @ 0x60000000 (serial I/O)
|
||||
* - GPIO MMIO @ 0x60004000 (pin output via OUT/W1TS/W1TC registers)
|
||||
* - 160 MHz clock, requestAnimationFrame execution loop
|
||||
* - Same public interface as AVRSimulator / RiscVSimulator
|
||||
*/
|
||||
|
||||
import { RiscVCore } from './RiscVCore';
|
||||
import type { PinManager } from './PinManager';
|
||||
import { hexToUint8Array } from '../utils/hexParser';
|
||||
import { parseMergedFlashImage } from '../utils/esp32ImageParser';
|
||||
|
||||
// ── ESP32-C3 Memory Map ──────────────────────────────────────────────────────
|
||||
const IROM_BASE = 0x42000000; // Flash instruction region (mapped via MMU)
|
||||
const DROM_BASE = 0x3C000000; // Flash data region (read-only alias of same flash)
|
||||
const DRAM_BASE = 0x3FC80000; // Data RAM
|
||||
const IRAM_BASE = 0x4037C000; // Instruction RAM
|
||||
|
||||
const IROM_SIZE = 4 * 1024 * 1024; // 4 MB flash buffer
|
||||
const DRAM_SIZE = 384 * 1024; // 384 KB DRAM
|
||||
const IRAM_SIZE = 384 * 1024; // 384 KB IRAM
|
||||
|
||||
// ── UART0 @ 0x60000000 ──────────────────────────────────────────────────────
|
||||
const UART0_BASE = 0x60000000;
|
||||
const UART0_SIZE = 0x400;
|
||||
const UART0_FIFO = 0x00; // write TX byte / read RX byte
|
||||
const UART0_STATUS = 0x1C; // TXFIFO_CNT in bits [19:16] (0 = empty = ready)
|
||||
|
||||
// ── GPIO @ 0x60004000 ───────────────────────────────────────────────────────
|
||||
const GPIO_BASE = 0x60004000;
|
||||
const GPIO_SIZE = 0x200;
|
||||
const GPIO_OUT = 0x04; // GPIO_OUT_REG — output value (read/write)
|
||||
const GPIO_W1TS = 0x08; // GPIO_OUT_W1TS — set bits (write-only)
|
||||
const GPIO_W1TC = 0x0C; // GPIO_OUT_W1TC — clear bits (write-only)
|
||||
const GPIO_IN = 0x3C; // GPIO_IN_REG — input value (read-only)
|
||||
const GPIO_ENABLE = 0x20; // GPIO_ENABLE_REG
|
||||
|
||||
// ── Clock ───────────────────────────────────────────────────────────────────
|
||||
const CPU_HZ = 160_000_000;
|
||||
const CYCLES_PER_FRAME = Math.round(CPU_HZ / 60);
|
||||
|
||||
export class Esp32C3Simulator {
|
||||
private core: RiscVCore;
|
||||
private flash: Uint8Array;
|
||||
private dram: Uint8Array;
|
||||
private iram: Uint8Array;
|
||||
private running = false;
|
||||
private animFrameId = 0;
|
||||
private rxFifo: number[] = [];
|
||||
private gpioOut = 0;
|
||||
private gpioIn = 0;
|
||||
|
||||
public pinManager: PinManager;
|
||||
public onSerialData: ((ch: string) => void) | null = null;
|
||||
public onBaudRateChange: ((baud: number) => void) | null = null;
|
||||
public onPinChangeWithTime: ((pin: number, state: boolean, timeMs: number) => void) | null = null;
|
||||
|
||||
constructor(pinManager: PinManager) {
|
||||
this.pinManager = pinManager;
|
||||
|
||||
// Flash is the primary (fast-path) memory region
|
||||
this.flash = new Uint8Array(IROM_SIZE);
|
||||
this.dram = new Uint8Array(DRAM_SIZE);
|
||||
this.iram = new Uint8Array(IRAM_SIZE);
|
||||
|
||||
this.core = new RiscVCore(this.flash, IROM_BASE);
|
||||
|
||||
// DROM — read-only alias of the same flash buffer at a different virtual address
|
||||
const flash = this.flash;
|
||||
this.core.addMmio(DROM_BASE, IROM_SIZE,
|
||||
(addr) => flash[addr - DROM_BASE] ?? 0,
|
||||
() => {},
|
||||
);
|
||||
|
||||
// DRAM (384 KB)
|
||||
const dram = this.dram;
|
||||
this.core.addMmio(DRAM_BASE, DRAM_SIZE,
|
||||
(addr) => dram[addr - DRAM_BASE],
|
||||
(addr, val) => { dram[addr - DRAM_BASE] = val; },
|
||||
);
|
||||
|
||||
// IRAM (384 KB)
|
||||
const iram = this.iram;
|
||||
this.core.addMmio(IRAM_BASE, IRAM_SIZE,
|
||||
(addr) => iram[addr - IRAM_BASE],
|
||||
(addr, val) => { iram[addr - IRAM_BASE] = val; },
|
||||
);
|
||||
|
||||
this._registerUart0();
|
||||
this._registerGpio();
|
||||
|
||||
this.core.reset(IROM_BASE);
|
||||
// Initialize SP to top of DRAM — MUST be after reset() which zeroes all regs
|
||||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||||
}
|
||||
|
||||
// ── MMIO registration ──────────────────────────────────────────────────────
|
||||
|
||||
private _registerUart0(): void {
|
||||
this.core.addMmio(UART0_BASE, UART0_SIZE,
|
||||
(addr) => {
|
||||
const off = addr - UART0_BASE;
|
||||
if (off === UART0_FIFO) return this.rxFifo.length > 0 ? (this.rxFifo.shift()! & 0xFF) : 0;
|
||||
if (off === UART0_STATUS) return 0; // TXFIFO always empty = ready to accept data
|
||||
return 0;
|
||||
},
|
||||
(addr, val) => {
|
||||
if (addr - UART0_BASE === UART0_FIFO) {
|
||||
this.onSerialData?.(String.fromCharCode(val & 0xFF));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private _registerGpio(): void {
|
||||
this.core.addMmio(GPIO_BASE, GPIO_SIZE,
|
||||
(addr) => {
|
||||
const off = (addr - GPIO_BASE) & ~3; // word-align for register lookup
|
||||
const byteIdx = (addr - GPIO_BASE) & 3;
|
||||
if (off === GPIO_OUT) return (this.gpioOut >> (byteIdx * 8)) & 0xFF;
|
||||
if (off === GPIO_IN) return (this.gpioIn >> (byteIdx * 8)) & 0xFF;
|
||||
if (off === GPIO_ENABLE) return 0xFF;
|
||||
return 0;
|
||||
},
|
||||
(addr, val) => {
|
||||
const off = (addr - GPIO_BASE) & ~3;
|
||||
const byteIdx = (addr - GPIO_BASE) & 3;
|
||||
const shift = byteIdx * 8;
|
||||
const byteMask = 0xFF << shift;
|
||||
const prev = this.gpioOut;
|
||||
|
||||
if (off === GPIO_W1TS) {
|
||||
// Set bits — each byte write sets corresponding bits
|
||||
this.gpioOut |= (val & 0xFF) << shift;
|
||||
} else if (off === GPIO_W1TC) {
|
||||
// Clear bits
|
||||
this.gpioOut &= ~((val & 0xFF) << shift);
|
||||
} else if (off === GPIO_OUT) {
|
||||
// Direct write — reconstruct 32-bit value byte by byte
|
||||
this.gpioOut = (this.gpioOut & ~byteMask) | ((val & 0xFF) << shift);
|
||||
}
|
||||
|
||||
const changed = prev ^ this.gpioOut;
|
||||
if (changed) {
|
||||
const timeMs = (this.core.cycles / CPU_HZ) * 1000;
|
||||
for (let bit = 0; bit < 22; bit++) { // ESP32-C3 has GPIO0–GPIO21
|
||||
if (changed & (1 << bit)) {
|
||||
const state = !!(this.gpioOut & (1 << bit));
|
||||
this.onPinChangeWithTime?.(bit, state, timeMs);
|
||||
this.pinManager.setPinState(bit, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ── HEX loading ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load an Intel HEX file. The hex addresses must be relative to IROM_BASE
|
||||
* (0x42000000), or zero-based (the parser will treat them as flash offsets).
|
||||
*/
|
||||
loadHex(hexContent: string): void {
|
||||
this.flash.fill(0);
|
||||
const bytes = hexToUint8Array(hexContent);
|
||||
|
||||
// hexToUint8Array returns bytes indexed from address 0.
|
||||
// If the hex records used IROM_BASE-relative addressing, the byte array
|
||||
// will start at offset IROM_BASE within a huge buffer — we can't use that.
|
||||
// Support both:
|
||||
// a) Small array (< IROM_SIZE) → direct flash offset mapping
|
||||
// b) Large array → slice from IROM_BASE offset if present
|
||||
if (bytes.length <= IROM_SIZE) {
|
||||
const maxCopy = Math.min(bytes.length, IROM_SIZE);
|
||||
this.flash.set(bytes.subarray(0, maxCopy), 0);
|
||||
} else {
|
||||
// Try to extract data at IROM_BASE offset
|
||||
const iromOffset = IROM_BASE;
|
||||
if (bytes.length > iromOffset) {
|
||||
const maxCopy = Math.min(bytes.length - iromOffset, IROM_SIZE);
|
||||
this.flash.set(bytes.subarray(iromOffset, iromOffset + maxCopy), 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.dram.fill(0);
|
||||
this.iram.fill(0);
|
||||
this.core.reset(IROM_BASE);
|
||||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a raw binary image into flash at offset 0 (maps to IROM_BASE 0x42000000).
|
||||
* Use this with binaries produced by:
|
||||
* riscv32-esp-elf-objcopy -O binary firmware.elf firmware.bin
|
||||
*/
|
||||
loadBin(bin: Uint8Array): void {
|
||||
this.flash.fill(0);
|
||||
const maxCopy = Math.min(bin.length, IROM_SIZE);
|
||||
this.flash.set(bin.subarray(0, maxCopy), 0);
|
||||
this.dram.fill(0);
|
||||
this.iram.fill(0);
|
||||
this.rxFifo = [];
|
||||
this.gpioOut = 0;
|
||||
this.gpioIn = 0;
|
||||
this.core.reset(IROM_BASE);
|
||||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a merged ESP32 flash image from the backend (base64-encoded).
|
||||
*
|
||||
* The backend produces a 4 MB merged image:
|
||||
* 0x01000 — bootloader
|
||||
* 0x08000 — partition table
|
||||
* 0x10000 — application (ESP32 image format with segment headers)
|
||||
*
|
||||
* Each image segment is loaded at its virtual load address:
|
||||
* IROM (0x42xxxxxx) → flash buffer (executed code)
|
||||
* DROM (0x3Cxxxxxx) → flash buffer (read-only data alias)
|
||||
* DRAM (0x3FCxxxxx) → dram buffer (initialised .data)
|
||||
* IRAM (0x4037xxxx) → iram buffer (ISR / time-critical code)
|
||||
*
|
||||
* The CPU resets to the entry point declared in the image header.
|
||||
*/
|
||||
loadFlashImage(base64: string): void {
|
||||
// Base64 decode
|
||||
const binStr = atob(base64);
|
||||
const data = new Uint8Array(binStr.length);
|
||||
for (let i = 0; i < binStr.length; i++) data[i] = binStr.charCodeAt(i);
|
||||
|
||||
// Parse ESP32 image format
|
||||
const parsed = parseMergedFlashImage(data);
|
||||
|
||||
// Clear all memory regions
|
||||
this.flash.fill(0);
|
||||
this.dram.fill(0);
|
||||
this.iram.fill(0);
|
||||
this.rxFifo = [];
|
||||
this.gpioOut = 0;
|
||||
this.gpioIn = 0;
|
||||
|
||||
// Load each segment at its virtual address
|
||||
for (const { loadAddr, data: seg } of parsed.segments) {
|
||||
const uAddr = loadAddr >>> 0;
|
||||
|
||||
if (uAddr >= IROM_BASE && uAddr + seg.length <= IROM_BASE + IROM_SIZE) {
|
||||
this.flash.set(seg, uAddr - IROM_BASE);
|
||||
} else if (uAddr >= DROM_BASE && uAddr + seg.length <= DROM_BASE + IROM_SIZE) {
|
||||
// DROM is a virtual alias of flash — store at same flash buffer
|
||||
this.flash.set(seg, uAddr - DROM_BASE);
|
||||
} else if (uAddr >= DRAM_BASE && uAddr + seg.length <= DRAM_BASE + DRAM_SIZE) {
|
||||
this.dram.set(seg, uAddr - DRAM_BASE);
|
||||
} else if (uAddr >= IRAM_BASE && uAddr + seg.length <= IRAM_BASE + IRAM_SIZE) {
|
||||
this.iram.set(seg, uAddr - IRAM_BASE);
|
||||
} else {
|
||||
console.warn(
|
||||
`[Esp32C3Simulator] Segment 0x${uAddr.toString(16)}` +
|
||||
` (${seg.length} B) outside known regions — skipped`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Boot CPU at image entry point
|
||||
this.core.reset(parsed.entryPoint);
|
||||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||||
|
||||
console.log(
|
||||
`[Esp32C3Simulator] Loaded ${parsed.segments.length} segments,` +
|
||||
` entry=0x${parsed.entryPoint.toString(16)}`
|
||||
);
|
||||
}
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
||||
|
||||
start(): void {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
this._loop();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.running = false;
|
||||
cancelAnimationFrame(this.animFrameId);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.stop();
|
||||
this.rxFifo = [];
|
||||
this.gpioOut = 0;
|
||||
this.gpioIn = 0;
|
||||
this.dram.fill(0);
|
||||
this.iram.fill(0);
|
||||
this.core.reset(IROM_BASE);
|
||||
this.core.regs[2] = (DRAM_BASE + DRAM_SIZE - 16) | 0;
|
||||
}
|
||||
|
||||
serialWrite(text: string): void {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
this.rxFifo.push(text.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
setPinState(pin: number, state: boolean): void {
|
||||
if (state) this.gpioIn |= (1 << pin);
|
||||
else this.gpioIn &= ~(1 << pin);
|
||||
}
|
||||
|
||||
isRunning(): boolean {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
// ── Execution loop ─────────────────────────────────────────────────────────
|
||||
|
||||
private _loop(): void {
|
||||
if (!this.running) return;
|
||||
for (let i = 0; i < CYCLES_PER_FRAME; i++) {
|
||||
this.core.step();
|
||||
}
|
||||
this.animFrameId = requestAnimationFrame(() => this._loop());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
/**
|
||||
* RiscVCore — Minimal RV32I base ISA interpreter in TypeScript.
|
||||
* RiscVCore — Minimal RV32IMC interpreter in TypeScript.
|
||||
*
|
||||
* Supports the complete RV32I instruction set (40 instructions):
|
||||
* LUI, AUIPC, JAL, JALR, BRANCH, LOAD, STORE, OP-IMM, OP, FENCE, SYSTEM
|
||||
* Supports the complete RV32I base ISA (40 instructions) plus:
|
||||
* RV32M — multiply/divide extension (MUL, MULH, MULHSU, MULHU, DIV, DIVU, REM, REMU)
|
||||
* RV32C — compressed 16-bit instruction extension (all ~40 instructions, decompressed to 32-bit)
|
||||
*
|
||||
* Memory model: flat Uint8Array, caller supplies base address mappings.
|
||||
* MMIO: caller installs read/write hooks at specific address ranges.
|
||||
|
|
@ -11,7 +12,7 @@
|
|||
* - No privilege levels / CSR side-effects (CSR reads return 0)
|
||||
* - No interrupts / exceptions (ECALL/EBREAK are no-ops)
|
||||
* - No misalignment exceptions
|
||||
* - No compressed (RV32C) or multiply (RV32M) extensions
|
||||
* - No RV32A (atomic) or floating-point extensions
|
||||
*/
|
||||
|
||||
export type MmioReadHook = (addr: number) => number;
|
||||
|
|
@ -139,6 +140,168 @@ export class RiscVCore {
|
|||
private reg(r: number): number { return r === 0 ? 0 : this.regs[r]; }
|
||||
private setReg(r: number, v: number): void { if (r !== 0) this.regs[r] = v; }
|
||||
|
||||
// ── RV32C decompressor ──────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Decompress a 16-bit RV32C instruction to its 32-bit RV32I/M equivalent.
|
||||
* Returns the equivalent 32-bit instruction word.
|
||||
*/
|
||||
private decompressC(half: number): number {
|
||||
const op = half & 0x3;
|
||||
const funct3 = (half >> 13) & 0x7;
|
||||
const bit12 = (half >> 12) & 0x1;
|
||||
|
||||
// Sign-extend val from bits bits
|
||||
const sext = (val: number, bits: number) => (val << (32 - bits)) >> (32 - bits);
|
||||
|
||||
// Instruction encoders
|
||||
const encI = (imm: number, rs1: number, f3: number, rd: number, oc: number) =>
|
||||
((imm & 0xFFF) << 20) | ((rs1 & 0x1F) << 15) | ((f3 & 0x7) << 12) | ((rd & 0x1F) << 7) | (oc & 0x7F);
|
||||
const encR = (f7: number, rs2: number, rs1: number, f3: number, rd: number, oc: number) =>
|
||||
((f7 & 0x7F) << 25) | ((rs2 & 0x1F) << 20) | ((rs1 & 0x1F) << 15) | ((f3 & 0x7) << 12) | ((rd & 0x1F) << 7) | (oc & 0x7F);
|
||||
const encS = (imm: number, rs2: number, rs1: number, f3: number, oc: number) =>
|
||||
(((imm >> 5) & 0x7F) << 25) | ((rs2 & 0x1F) << 20) | ((rs1 & 0x1F) << 15) | ((f3 & 0x7) << 12) | ((imm & 0x1F) << 7) | (oc & 0x7F);
|
||||
const encJ = (imm: number, rd: number) => {
|
||||
const b20 = (imm >> 20) & 1;
|
||||
const b19_12 = (imm >> 12) & 0xFF;
|
||||
const b11 = (imm >> 11) & 1;
|
||||
const b10_1 = (imm >> 1) & 0x3FF;
|
||||
return (b20 << 31) | (b10_1 << 21) | (b11 << 20) | (b19_12 << 12) | ((rd & 0x1F) << 7) | 0x6F;
|
||||
};
|
||||
const encB = (imm: number, rs2: number, rs1: number, f3: number) => {
|
||||
const b12 = (imm >> 12) & 1;
|
||||
const b11 = (imm >> 11) & 1;
|
||||
const b10_5 = (imm >> 5) & 0x3F;
|
||||
const b4_1 = (imm >> 1) & 0xF;
|
||||
return (b12 << 31) | (b10_5 << 25) | ((rs2 & 0x1F) << 20) | ((rs1 & 0x1F) << 15) |
|
||||
((f3 & 7) << 12) | (b4_1 << 8) | (b11 << 7) | 0x63;
|
||||
};
|
||||
|
||||
// CJ-format 11-bit signed offset (scrambled bit positions per spec Table 16.6)
|
||||
const cjOff = () => sext(
|
||||
(bit12 << 11) | (((half >> 11) & 1) << 4) | (((half >> 9) & 3) << 8) |
|
||||
(((half >> 8) & 1) << 10) | (((half >> 7) & 1) << 6) | (((half >> 6) & 1) << 7) |
|
||||
(((half >> 3) & 7) << 1) | (((half >> 2) & 1) << 5),
|
||||
12);
|
||||
|
||||
// CB-format 8-bit signed offset
|
||||
const cbOff = () => sext(
|
||||
(bit12 << 8) | (((half >> 10) & 3) << 3) | (((half >> 5) & 3) << 6) |
|
||||
(((half >> 3) & 3) << 1) | (((half >> 2) & 1) << 5),
|
||||
9);
|
||||
|
||||
// ── Quadrant 0 (op=00) ──────────────────────────────────────────────
|
||||
if (op === 0) {
|
||||
const rdp = ((half >> 2) & 7) + 8; // rd' → x(8..15)
|
||||
const rs1p = ((half >> 7) & 7) + 8; // rs1' → x(8..15)
|
||||
switch (funct3) {
|
||||
case 0: { // C.ADDI4SPN → ADDI rd', sp, nzuimm
|
||||
const nzuimm = (((half >> 7) & 0xF) << 6) | (((half >> 11) & 0x3) << 4) |
|
||||
(((half >> 5) & 1) << 3) | (((half >> 6) & 1) << 2);
|
||||
return encI(nzuimm, 2, 0, rdp, 0x13);
|
||||
}
|
||||
case 2: { // C.LW → LW rd', offset(rs1')
|
||||
const off = (((half >> 10) & 7) << 3) | (((half >> 6) & 1) << 2) | (((half >> 5) & 1) << 6);
|
||||
return encI(off, rs1p, 2, rdp, 0x03);
|
||||
}
|
||||
case 6: { // C.SW → SW rs2', offset(rs1')
|
||||
const off = (((half >> 10) & 7) << 3) | (((half >> 6) & 1) << 2) | (((half >> 5) & 1) << 6);
|
||||
return encS(off, rdp, rs1p, 2, 0x23); // rdp plays role of rs2' in CS format
|
||||
}
|
||||
default: return 0x00000013; // reserved → NOP
|
||||
}
|
||||
}
|
||||
|
||||
// ── Quadrant 1 (op=01) ──────────────────────────────────────────────
|
||||
if (op === 1) {
|
||||
const rd = (half >> 7) & 0x1F;
|
||||
const rs1p = ((half >> 7) & 7) + 8;
|
||||
const rs2p = ((half >> 2) & 7) + 8;
|
||||
const imm6 = sext((bit12 << 5) | ((half >> 2) & 0x1F), 6);
|
||||
|
||||
switch (funct3) {
|
||||
case 0: // C.NOP / C.ADDI → ADDI rd, rd, imm
|
||||
return encI(imm6, rd, 0, rd, 0x13);
|
||||
case 1: // C.JAL (RV32C only) → JAL x1, offset
|
||||
return encJ(cjOff(), 1);
|
||||
case 2: // C.LI → ADDI rd, x0, imm
|
||||
return encI(imm6, 0, 0, rd, 0x13);
|
||||
case 3: {
|
||||
if (rd === 2) { // C.ADDI16SP → ADDI sp, sp, nzimm
|
||||
const nzimm = sext(
|
||||
(bit12 << 9) | (((half >> 6) & 1) << 4) | (((half >> 5) & 1) << 6) |
|
||||
(((half >> 3) & 3) << 7) | (((half >> 2) & 1) << 5), 10);
|
||||
return encI(nzimm, 2, 0, 2, 0x13);
|
||||
} else { // C.LUI → LUI rd, nzimm
|
||||
const nzimm = sext((bit12 << 17) | (((half >> 2) & 0x1F) << 12), 18);
|
||||
return (nzimm & 0xFFFFF000) | ((rd & 0x1F) << 7) | 0x37;
|
||||
}
|
||||
}
|
||||
case 4: {
|
||||
const f2 = (half >> 10) & 0x3;
|
||||
const sh = (bit12 << 5) | ((half >> 2) & 0x1F);
|
||||
if (f2 === 0) return encI(sh, rs1p, 5, rs1p, 0x13); // C.SRLI → SRLI
|
||||
if (f2 === 1) return encI(0x400 | sh, rs1p, 5, rs1p, 0x13); // C.SRAI → SRAI (bit10=1)
|
||||
if (f2 === 2) return encI(imm6, rs1p, 7, rs1p, 0x13); // C.ANDI → ANDI
|
||||
// f2 === 3: C.SUB / C.XOR / C.OR / C.AND
|
||||
const op2 = (half >> 5) & 3;
|
||||
if (!bit12) {
|
||||
switch (op2) {
|
||||
case 0: return encR(0x20, rs2p, rs1p, 0, rs1p, 0x33); // C.SUB (funct7=0x20)
|
||||
case 1: return encR(0, rs2p, rs1p, 4, rs1p, 0x33); // C.XOR
|
||||
case 2: return encR(0, rs2p, rs1p, 6, rs1p, 0x33); // C.OR
|
||||
case 3: return encR(0, rs2p, rs1p, 7, rs1p, 0x33); // C.AND
|
||||
}
|
||||
}
|
||||
return 0x00000013; // C.SUBW etc. (RV64 only) → NOP
|
||||
}
|
||||
case 5: // C.J → JAL x0, offset
|
||||
return encJ(cjOff(), 0);
|
||||
case 6: // C.BEQZ → BEQ rs1', x0, offset
|
||||
return encB(cbOff(), 0, rs1p, 0);
|
||||
case 7: // C.BNEZ → BNE rs1', x0, offset
|
||||
return encB(cbOff(), 0, rs1p, 1);
|
||||
default: return 0x00000013;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Quadrant 2 (op=10) ──────────────────────────────────────────────
|
||||
if (op === 2) {
|
||||
const rd = (half >> 7) & 0x1F;
|
||||
const rs2 = (half >> 2) & 0x1F;
|
||||
|
||||
switch (funct3) {
|
||||
case 0: { // C.SLLI → SLLI rd, rd, shamt
|
||||
const sh = (bit12 << 5) | rs2;
|
||||
return encI(sh, rd, 1, rd, 0x13);
|
||||
}
|
||||
case 2: { // C.LWSP → LW rd, offset(sp)
|
||||
// uimm[7:6]=bits[3:2], uimm[5]=bit12, uimm[4:2]=bits[6:4]
|
||||
const off = (((half >> 2) & 3) << 6) | (bit12 << 5) | (((half >> 4) & 7) << 2);
|
||||
return encI(off, 2, 2, rd, 0x03);
|
||||
}
|
||||
case 4: {
|
||||
if (!bit12) {
|
||||
if (rs2 === 0) return encI(0, rd, 0, 0, 0x67); // C.JR → JALR x0, 0(rd)
|
||||
return encR(0, rs2, 0, 0, rd, 0x33); // C.MV → ADD rd, x0, rs2
|
||||
} else {
|
||||
if (rd === 0 && rs2 === 0) return 0x00100073; // C.EBREAK
|
||||
if (rs2 === 0) return encI(0, rd, 0, 1, 0x67); // C.JALR → JALR x1, 0(rd)
|
||||
return encR(0, rs2, rd, 0, rd, 0x33); // C.ADD → ADD rd, rd, rs2
|
||||
}
|
||||
}
|
||||
case 6: { // C.SWSP → SW rs2, offset(sp)
|
||||
// uimm[7:6]=bits[8:7], uimm[5:2]=bits[12:9]
|
||||
const off = (((half >> 7) & 3) << 6) | (((half >> 9) & 0xF) << 2);
|
||||
return encS(off, rs2, 2, 2, 0x23);
|
||||
}
|
||||
default: return 0x00000013;
|
||||
}
|
||||
}
|
||||
|
||||
return 0x00000013; // should not reach (op=11 means 32-bit instruction)
|
||||
}
|
||||
|
||||
// ── Single instruction step ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
@ -146,7 +309,18 @@ export class RiscVCore {
|
|||
* for this simple model — real chips have variable latency).
|
||||
*/
|
||||
step(): number {
|
||||
const instr = this.readWord(this.pc);
|
||||
// RV32C: if bits [1:0] != 0b11, it's a 16-bit compressed instruction
|
||||
const half = this.readHalf(this.pc);
|
||||
let instr: number;
|
||||
let instrLen: number;
|
||||
if ((half & 0x3) !== 0x3) {
|
||||
instr = this.decompressC(half);
|
||||
instrLen = 2;
|
||||
} else {
|
||||
instr = this.readWord(this.pc);
|
||||
instrLen = 4;
|
||||
}
|
||||
|
||||
const opcode = instr & 0x7f;
|
||||
const rd = (instr >> 7) & 0x1f;
|
||||
const funct3 = (instr >> 12) & 0x07;
|
||||
|
|
@ -154,7 +328,7 @@ export class RiscVCore {
|
|||
const rs2 = (instr >> 20) & 0x1f;
|
||||
const funct7 = (instr >> 25) & 0x7f;
|
||||
|
||||
let nextPc = (this.pc + 4) >>> 0;
|
||||
let nextPc = (this.pc + instrLen) >>> 0;
|
||||
|
||||
switch (opcode) {
|
||||
|
||||
|
|
@ -251,12 +425,13 @@ export class RiscVCore {
|
|||
break;
|
||||
}
|
||||
|
||||
// OP (register–register)
|
||||
// OP (register–register) — includes RV32M multiply/divide (funct7=1)
|
||||
case 0x33: {
|
||||
const a = this.reg(rs1);
|
||||
const b = this.reg(rs2);
|
||||
let val: number;
|
||||
switch ((funct7 << 3) | funct3) {
|
||||
// RV32I
|
||||
case 0x000: val = a + b; break; // ADD
|
||||
case 0x100: val = a - b; break; // SUB
|
||||
case 0x001: val = a << (b & 0x1f); break; // SLL
|
||||
|
|
@ -267,6 +442,15 @@ export class RiscVCore {
|
|||
case 0x105: val = a >> (b & 0x1f); break; // SRA
|
||||
case 0x006: val = a | b; break; // OR
|
||||
case 0x007: val = a & b; break; // AND
|
||||
// RV32M (funct7=1 → cases 0x008–0x00f)
|
||||
case 0x008: val = Math.imul(a, b); break; // MUL (lower 32 bits)
|
||||
case 0x009: val = Number(BigInt(a) * BigInt(b) >> 32n) | 0; break; // MULH (s×s upper)
|
||||
case 0x00a: val = Number(BigInt(a) * BigInt(b >>> 0) >> 32n) | 0; break; // MULHSU (s×u upper)
|
||||
case 0x00b: val = Number(BigInt(a >>> 0) * BigInt(b >>> 0) >> 32n) >>> 0; break;// MULHU (u×u upper)
|
||||
case 0x00c: val = b === 0 ? -1 : (a / b) | 0; break; // DIV
|
||||
case 0x00d: val = b === 0 ? -1 : ((a >>> 0) / (b >>> 0)) | 0; break; // DIVU
|
||||
case 0x00e: val = b === 0 ? a : (a % b) | 0; break; // REM
|
||||
case 0x00f: val = b === 0 ? (a | 0) : ((a >>> 0) % (b >>> 0)) | 0; break; // REMU
|
||||
default: val = 0;
|
||||
}
|
||||
this.setReg(rd, val);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { create } from 'zustand';
|
|||
import { AVRSimulator } from '../simulation/AVRSimulator';
|
||||
import { RP2040Simulator } from '../simulation/RP2040Simulator';
|
||||
import { RiscVSimulator } from '../simulation/RiscVSimulator';
|
||||
import { Esp32C3Simulator } from '../simulation/Esp32C3Simulator';
|
||||
import { PinManager } from '../simulation/PinManager';
|
||||
import { VirtualDS1307, VirtualTempSensor, I2CMemoryDevice } from '../simulation/I2CBusManager';
|
||||
import type { RP2040I2CDevice } from '../simulation/RP2040Simulator';
|
||||
|
|
@ -35,7 +36,7 @@ export const DEFAULT_BOARD_POSITION = { x: 50, y: 50 };
|
|||
export const ARDUINO_POSITION = DEFAULT_BOARD_POSITION;
|
||||
|
||||
// ── Runtime Maps (outside Zustand — not serialisable) ─────────────────────
|
||||
const simulatorMap = new Map<string, AVRSimulator | RP2040Simulator | RiscVSimulator>();
|
||||
const simulatorMap = new Map<string, AVRSimulator | RP2040Simulator | RiscVSimulator | Esp32C3Simulator>();
|
||||
const pinManagerMap = new Map<string, PinManager>();
|
||||
const bridgeMap = new Map<string, RaspberryPi3Bridge>();
|
||||
const esp32BridgeMap = new Map<string, Esp32Bridge>();
|
||||
|
|
@ -45,9 +46,14 @@ export const getBoardPinManager = (id: string) => pinManagerMap.get(id);
|
|||
export const getBoardBridge = (id: string) => bridgeMap.get(id);
|
||||
export const getEsp32Bridge = (id: string) => esp32BridgeMap.get(id);
|
||||
|
||||
// Xtensa-based ESP32 boards — still use QEMU bridge (backend)
|
||||
const ESP32_KINDS = new Set<BoardKind>([
|
||||
'esp32', 'esp32-devkit-c-v4', 'esp32-cam', 'wemos-lolin32-lite',
|
||||
'esp32-s3', 'xiao-esp32-s3', 'arduino-nano-esp32',
|
||||
]);
|
||||
|
||||
// RISC-V ESP32 boards — use the browser-side Esp32C3Simulator (no backend needed)
|
||||
const ESP32_RISCV_KINDS = new Set<BoardKind>([
|
||||
'esp32-c3', 'xiao-esp32-c3', 'aitewinrobot-esp32c3-supermini',
|
||||
]);
|
||||
|
||||
|
|
@ -55,6 +61,10 @@ function isEsp32Kind(kind: BoardKind): boolean {
|
|||
return ESP32_KINDS.has(kind);
|
||||
}
|
||||
|
||||
function isRiscVEsp32Kind(kind: BoardKind): boolean {
|
||||
return ESP32_RISCV_KINDS.has(kind);
|
||||
}
|
||||
|
||||
// ── Component type ────────────────────────────────────────────────────────
|
||||
interface Component {
|
||||
id: string;
|
||||
|
|
@ -86,7 +96,7 @@ interface SimulatorState {
|
|||
/** @deprecated use boards[x].x/y */
|
||||
boardPosition: { x: number; y: number };
|
||||
/** @deprecated use getBoardSimulator(activeBoardId) */
|
||||
simulator: AVRSimulator | RP2040Simulator | RiscVSimulator | null;
|
||||
simulator: AVRSimulator | RP2040Simulator | RiscVSimulator | Esp32C3Simulator | null;
|
||||
/** @deprecated use getBoardPinManager(activeBoardId) */
|
||||
pinManager: PinManager;
|
||||
running: boolean;
|
||||
|
|
@ -159,8 +169,8 @@ function createSimulator(
|
|||
onSerial: (ch: string) => void,
|
||||
onBaud: (baud: number) => void,
|
||||
onPinTime: (pin: number, state: boolean, t: number) => void,
|
||||
): AVRSimulator | RP2040Simulator | RiscVSimulator {
|
||||
let sim: AVRSimulator | RP2040Simulator | RiscVSimulator;
|
||||
): AVRSimulator | RP2040Simulator | RiscVSimulator | Esp32C3Simulator {
|
||||
let sim: AVRSimulator | RP2040Simulator | RiscVSimulator | Esp32C3Simulator;
|
||||
if (boardKind === 'arduino-mega') {
|
||||
sim = new AVRSimulator(pm, 'mega');
|
||||
} else if (boardKind === 'attiny85') {
|
||||
|
|
@ -169,6 +179,9 @@ function createSimulator(
|
|||
sim = new RiscVSimulator(pm);
|
||||
} else if (boardKind === 'raspberry-pi-pico' || boardKind === 'pi-pico-w') {
|
||||
sim = new RP2040Simulator(pm);
|
||||
} else if (isRiscVEsp32Kind(boardKind)) {
|
||||
// ESP32-C3 / XIAO-C3 / C3 SuperMini — browser-side RV32IMC emulator
|
||||
sim = new Esp32C3Simulator(pm);
|
||||
} else {
|
||||
// arduino-uno, arduino-nano
|
||||
sim = new AVRSimulator(pm, 'uno');
|
||||
|
|
@ -418,9 +431,20 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
if (!board) return;
|
||||
|
||||
if (isEsp32Kind(board.boardKind)) {
|
||||
// For ESP32: program is base64-encoded .bin — send to QEMU via bridge
|
||||
// Xtensa ESP32 boards: program is base64-encoded .bin — send to QEMU via bridge
|
||||
const esp32Bridge = getEsp32Bridge(boardId);
|
||||
if (esp32Bridge) esp32Bridge.loadFirmware(program);
|
||||
} else if (isRiscVEsp32Kind(board.boardKind)) {
|
||||
// RISC-V ESP32-C3 boards: parse merged flash image and load into browser emulator
|
||||
const sim = getBoardSimulator(boardId);
|
||||
if (sim instanceof Esp32C3Simulator) {
|
||||
try {
|
||||
sim.loadFlashImage(program);
|
||||
} catch (err) {
|
||||
console.error(`[Esp32C3Simulator] loadFlashImage failed for ${boardId}:`, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const sim = getBoardSimulator(boardId);
|
||||
if (sim && board.boardKind !== 'raspberry-pi-3') {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ export type BoardKind =
|
|||
| 'esp32-s3' // Xtensa LX7, QEMU backend
|
||||
| 'xiao-esp32-s3' // Seeed XIAO ESP32-S3, QEMU (esp32-s3)
|
||||
| 'arduino-nano-esp32' // Arduino Nano ESP32 (S3), QEMU (esp32-s3)
|
||||
| 'esp32-c3' // RISC-V, QEMU backend
|
||||
| 'xiao-esp32-c3' // Seeed XIAO ESP32-C3, QEMU (esp32-c3)
|
||||
| 'aitewinrobot-esp32c3-supermini' // ESP32-C3 SuperMini, QEMU (esp32-c3)
|
||||
| 'esp32-c3' // RISC-V RV32IMC, browser emulation (Esp32C3Simulator)
|
||||
| 'xiao-esp32-c3' // Seeed XIAO ESP32-C3, browser emulation (Esp32C3Simulator)
|
||||
| 'aitewinrobot-esp32c3-supermini' // ESP32-C3 SuperMini, browser emulation (Esp32C3Simulator)
|
||||
| 'attiny85' // AVR ATtiny85, browser emulation (avr8js)
|
||||
| 'riscv-generic'; // RV32I generic MCU (CH32V003 target), browser emulation
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* ESP32 image format parser.
|
||||
*
|
||||
* The backend produces a merged 4 MB flash image containing:
|
||||
* offset 0x0000 : empty (0xFF)
|
||||
* offset 0x1000 : bootloader (ESP32 image format)
|
||||
* offset 0x8000 : partition table
|
||||
* offset 0x10000: application (ESP32 image format) ← we parse this
|
||||
*
|
||||
* ESP32 image header (24 bytes, ESP-IDF esp_image_format.h):
|
||||
* +0 magic 0xE9
|
||||
* +1 segment_count
|
||||
* +2 spi_mode
|
||||
* +3 spi_speed_size
|
||||
* +4 entry_addr uint32 LE
|
||||
* +8 … extended fields …
|
||||
* total: 24 bytes
|
||||
*
|
||||
* Each segment (8-byte header + data):
|
||||
* +0 load_addr uint32 LE — virtual address to load data at
|
||||
* +4 data_len uint32 LE
|
||||
* +8 data[data_len]
|
||||
*/
|
||||
|
||||
export const ESP32_APP_FLASH_OFFSET = 0x10000;
|
||||
const ESP32_MAGIC = 0xE9;
|
||||
const HEADER_SIZE = 24;
|
||||
const SEG_HDR_SIZE = 8;
|
||||
|
||||
export interface Esp32Segment {
|
||||
loadAddr: number;
|
||||
data: Uint8Array;
|
||||
}
|
||||
|
||||
export interface Esp32ParsedImage {
|
||||
entryPoint: number;
|
||||
segments: Esp32Segment[];
|
||||
}
|
||||
|
||||
function parseAppAt(img: Uint8Array, base: number): Esp32ParsedImage {
|
||||
const view = new DataView(img.buffer, img.byteOffset, img.byteLength);
|
||||
|
||||
if (view.getUint8(base) !== ESP32_MAGIC) {
|
||||
throw new Error(
|
||||
`Bad ESP32 magic at 0x${base.toString(16)}: ` +
|
||||
`expected 0xE9, got 0x${view.getUint8(base).toString(16)}`
|
||||
);
|
||||
}
|
||||
|
||||
const segCount = view.getUint8(base + 1);
|
||||
const entryPoint = view.getUint32(base + 4, /*littleEndian=*/true);
|
||||
|
||||
const segments: Esp32Segment[] = [];
|
||||
let pos = base + HEADER_SIZE;
|
||||
|
||||
for (let i = 0; i < segCount; i++) {
|
||||
if (pos + SEG_HDR_SIZE > img.length) break;
|
||||
|
||||
const loadAddr = view.getUint32(pos, true);
|
||||
const dataLen = view.getUint32(pos + 4, true);
|
||||
pos += SEG_HDR_SIZE;
|
||||
|
||||
if (pos + dataLen > img.length) {
|
||||
console.warn(`[esp32ImageParser] Segment ${i} data truncated`);
|
||||
break;
|
||||
}
|
||||
|
||||
segments.push({ loadAddr, data: img.slice(pos, pos + dataLen) });
|
||||
pos += dataLen;
|
||||
}
|
||||
|
||||
return { entryPoint, segments };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a merged ESP32 flash image or a raw app binary.
|
||||
*
|
||||
* Accepts:
|
||||
* - 4 MB merged image (bootloader + partitions + app) — reads app at 0x10000
|
||||
* - Raw app binary (magic 0xE9 at offset 0)
|
||||
*
|
||||
* Throws if no valid image is found.
|
||||
*/
|
||||
export function parseMergedFlashImage(data: Uint8Array): Esp32ParsedImage {
|
||||
// Standard merged image: app at offset 0x10000
|
||||
if (data.length >= ESP32_APP_FLASH_OFFSET + HEADER_SIZE && data[ESP32_APP_FLASH_OFFSET] === ESP32_MAGIC) {
|
||||
return parseAppAt(data, ESP32_APP_FLASH_OFFSET);
|
||||
}
|
||||
|
||||
// Fallback: raw app binary (no bootloader prefix)
|
||||
if (data.length >= HEADER_SIZE && data[0] === ESP32_MAGIC) {
|
||||
return parseAppAt(data, 0);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No valid ESP32 image magic found ` +
|
||||
`(tried offsets 0x10000 and 0x0, image size ${data.length} bytes)`
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue