feat(web): auto-merge CrossPoint release app image into a 16 MB flash
Accept the ~5 MB release firmware.bin directly: place it at 0x10000 in a 0xFF-erased 16 MB image alongside the bundled ESP32-C3 bootloader (0x0) and CrossPoint partition table (0x8000). m25p80 requires a full chip-size backing, so the 16 MB image is still built, but the user no longer pre-merges. Full 16 MB images are still accepted as-is.
This commit is contained in:
@@ -47,7 +47,9 @@ make web
|
||||
# Open http://127.0.0.1:8000/web/
|
||||
```
|
||||
|
||||
Choose a merged 16 MB firmware `.bin`, select X3 or X4, and boot. The controls
|
||||
Choose a CrossPoint firmware `.bin` — either the ~5 MB release app image (it is
|
||||
merged in-browser with the bundled bootloader and partition table into a 16 MB
|
||||
flash) or a full 16 MB image — select X3 or X4, and boot. The controls
|
||||
map to the device's ADC button ladders and active-low Power GPIO. `make web`
|
||||
serves the required COOP/COEP headers for Emscripten pthreads; a generic static
|
||||
server without those headers will not work.
|
||||
|
||||
+13
-3
@@ -1,8 +1,18 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { BUTTONS, setButton, validateFirmwareSize } from '../web/app.js';
|
||||
import { BUTTONS, setButton, mergeFirmware } from '../web/app.js';
|
||||
|
||||
validateFirmwareSize(16 * 1024 * 1024);
|
||||
assert.throws(() => validateFirmwareSize(4 * 1024 * 1024), /exactly 16 MB/);
|
||||
// Merge - app image is placed at 0x10000 in a 0xFF-erased 16 MB flash with bootloader and partitions.
|
||||
const bootloader = new Uint8Array([1, 2, 3]);
|
||||
const partitions = new Uint8Array([4, 5, 6]);
|
||||
const app = new Uint8Array(1024);
|
||||
app[0] = 0xe9;
|
||||
const flash = mergeFirmware(app, bootloader, partitions);
|
||||
assert.equal(flash.length, 16 * 1024 * 1024);
|
||||
assert.deepEqual(flash.slice(0, 3), bootloader);
|
||||
assert.deepEqual(flash.slice(0x8000, 0x8003), partitions);
|
||||
assert.equal(flash[0x10000], 0xe9);
|
||||
assert.equal(flash[0x20000], 0xff);
|
||||
assert.throws(() => mergeFirmware(new Uint8Array([0x00]), bootloader, partitions), /0xE9/);
|
||||
|
||||
const calls = [];
|
||||
const module = {
|
||||
|
||||
+38
-6
@@ -1,4 +1,8 @@
|
||||
const FIRMWARE_SIZE = 16 * 1024 * 1024;
|
||||
const FLASH_SIZE = 16 * 1024 * 1024;
|
||||
const BOOTLOADER_OFFSET = 0x0;
|
||||
const PARTITIONS_OFFSET = 0x8000;
|
||||
const APP_OFFSET = 0x10000;
|
||||
const ESP_IMAGE_MAGIC = 0xe9;
|
||||
const RELEASED_ADC = 4095;
|
||||
|
||||
export const BUTTONS = {
|
||||
@@ -11,10 +15,39 @@ export const BUTTONS = {
|
||||
power: { type: 'gpio', pin: 3 },
|
||||
};
|
||||
|
||||
export function validateFirmwareSize(size) {
|
||||
if (size !== FIRMWARE_SIZE) {
|
||||
throw new Error(`Firmware must be exactly 16 MB; this file is ${(size / 1024 / 1024).toFixed(2)} MB.`);
|
||||
// Assemble Flash Image - m25p80 requires a backing file exactly the chip size (16 MB), so a bare app image is placed into a 0xFF-erased 16 MB flash alongside the bootloader and partition table.
|
||||
export function mergeFirmware(app, bootloader, partitions) {
|
||||
if (app[0] !== ESP_IMAGE_MAGIC) {
|
||||
throw new Error('Not an ESP32 app image (missing 0xE9 magic byte).');
|
||||
}
|
||||
if (APP_OFFSET + app.length > FLASH_SIZE) {
|
||||
throw new Error(`App image is too large for the ${FLASH_SIZE / 1024 / 1024} MB flash.`);
|
||||
}
|
||||
const flash = new Uint8Array(FLASH_SIZE).fill(0xff);
|
||||
flash.set(bootloader, BOOTLOADER_OFFSET);
|
||||
flash.set(partitions, PARTITIONS_OFFSET);
|
||||
flash.set(app, APP_OFFSET);
|
||||
return flash;
|
||||
}
|
||||
|
||||
async function fetchBin(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Could not load ${url.pathname} (${response.status}).`);
|
||||
}
|
||||
return new Uint8Array(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
// A full 16 MB image is used as-is; anything else is treated as a CrossPoint app image and merged with the bundled bootloader and partition table.
|
||||
async function prepareFirmware(bytes, assetRoot) {
|
||||
if (bytes.length === FLASH_SIZE) {
|
||||
return bytes;
|
||||
}
|
||||
const [bootloader, partitions] = await Promise.all([
|
||||
fetchBin(new URL('assets/esp32c3-bootloader.bin', assetRoot)),
|
||||
fetchBin(new URL('assets/crosspoint-partitions.bin', assetRoot)),
|
||||
]);
|
||||
return mergeFirmware(bytes, bootloader, partitions);
|
||||
}
|
||||
|
||||
export function setButton(module, name, pressed) {
|
||||
@@ -121,13 +154,12 @@ function enableControls(module) {
|
||||
}
|
||||
|
||||
async function boot(file, variant, setStatus) {
|
||||
validateFirmwareSize(file.size);
|
||||
if (!crossOriginIsolated) {
|
||||
throw new Error('This emulator requires COOP/COEP headers. Start it with `make web`.');
|
||||
}
|
||||
setStatus('Loading firmware…');
|
||||
|
||||
const firmware = new Uint8Array(await file.arrayBuffer());
|
||||
const firmware = await prepareFirmware(new Uint8Array(await file.arrayBuffer()), import.meta.url);
|
||||
const artifactRoot = new URL('../dist/wasm/', import.meta.url);
|
||||
const qemuUrl = new URL('qemu-system-riscv32.js', artifactRoot);
|
||||
const rom = new Uint8Array(await fetch(new URL('esp32c3-rom.bin', artifactRoot)).then(response => {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+1
-1
@@ -11,7 +11,7 @@
|
||||
<header>
|
||||
<p class="eyebrow">ESP32-C3 emulator</p>
|
||||
<h1>xteink X3 / X4</h1>
|
||||
<p>Choose a merged 16 MB firmware image and boot it locally in your browser.</p>
|
||||
<p>Choose a CrossPoint firmware <code>.bin</code> (the ~5 MB release app image, or a full 16 MB flash) and boot it locally in your browser.</p>
|
||||
</header>
|
||||
|
||||
<form id="boot-form" class="boot-panel">
|
||||
|
||||
Reference in New Issue
Block a user