diff --git a/apps/website/public/term/index.html b/apps/website/public/term/index.html index 4f5cdc40bd..af89dbfefb 100644 --- a/apps/website/public/term/index.html +++ b/apps/website/public/term/index.html @@ -12,9 +12,9 @@ - + - + + + +
+
Trinity S³AIAll recordings →
+

WASM on the AX7203: flash, self-test, 134/134 exact

+

The t27-generated WASM interpreter on an ALINX AX7203 (xc7a200t), bitstream from the openXC7 flow, spec at trinity-fpga 4655702ed: openFPGALoader flashes over the onboard FT232H, the model-board self-test shows every conformance check able to fail, and one run answers all 134 requests exactly over UART.

+
+
recorded 2026-10-12 04:14 UTCreal 32.5 sshown 32.5 sexit codes 0 0 0 0 0
+
$ git log -1 --format='%h %s' | cut -c1-100 +$ openFPGALoader --scan-usb +$ openFPGALoader -c digilent_hs2 --busdev-num 000:005 /tmp/wasm-build/wasm_board_ax7203.bit +$ python3 conformance/wasm_board_ax7203.py --self-test +$ python3 conformance/wasm_board_ax7203.py --port /dev/cu.usbserial-130
+

Staged: the prompt and the typing. Real: every byte the commands printed, at the time they printed it. Any silence longer than 2 s is shown for 2 s, and the title bar says so while it happens. Edited: nothing. No GIF: this recording has none.

+ +
+ + + diff --git a/apps/website/public/term/wasm-board-ax7203/meta.json b/apps/website/public/term/wasm-board-ax7203/meta.json new file mode 100644 index 0000000000..b51c9651fe --- /dev/null +++ b/apps/website/public/term/wasm-board-ax7203/meta.json @@ -0,0 +1,27 @@ +{ + "id": "wasm-board-ax7203", + "title": "WASM on the AX7203: flash, self-test, 134/134 exact", + "desc": "The t27-generated WASM interpreter on an ALINX AX7203 (xc7a200t), bitstream from the openXC7 flow, spec at trinity-fpga 4655702ed: openFPGALoader flashes over the onboard FT232H, the model-board self-test shows every conformance check able to fail, and one run answers all 134 requests exactly over UART.", + "url": "https://t27.ai/term/wasm-board-ax7203/", + "gif": null, + "post": "https://t27.ai/#/blog/a-wasm-interpreter-ran-on-the-board", + "recorded": "2026-10-12 04:14 UTC", + "real_s": 32.5, + "shown_s": 32.5, + "commands": [ + "git log -1 --format='%h %s' | cut -c1-100", + "openFPGALoader --scan-usb", + "openFPGALoader -c digilent_hs2 --busdev-num 000:005 /tmp/wasm-build/wasm_board_ax7203.bit", + "python3 conformance/wasm_board_ax7203.py --self-test", + "python3 conformance/wasm_board_ax7203.py --port /dev/cu.usbserial-130" + ], + "exit_codes": [ + "0", + "0", + "0", + "0", + "0" + ], + "redacted": [], + "published": "2026-10-12" +} diff --git a/apps/website/public/term/wasm-board-ax7203/session.cast b/apps/website/public/term/wasm-board-ax7203/session.cast new file mode 100644 index 0000000000..47b053c59b --- /dev/null +++ b/apps/website/public/term/wasm-board-ax7203/session.cast @@ -0,0 +1,316 @@ +{"version": 2, "width": 104, "height": 30, "timestamp": 1791778476, "title": "WASM on the AX7203: flash, self-test, 134/134 exact", "commands": ["git log -1 --format='%h %s' | cut -c1-100", "openFPGALoader --scan-usb", "openFPGALoader -c digilent_hs2 --busdev-num 000:005 /tmp/wasm-build/wasm_board_ax7203.bit", "python3 conformance/wasm_board_ax7203.py --self-test", "python3 conformance/wasm_board_ax7203.py --port /dev/cu.usbserial-130"], "redacted": []} +[0.0, "o", "\u001b[38;2;255;215;0mt27 $ \u001b[0m"] +[0.4, "o", "g"] +[0.4351, "o", "i"] +[0.4702, "o", "t"] +[0.5053, "o", " "] +[0.5404, "o", "l"] +[0.5755, "o", "o"] +[0.6106, "o", "g"] +[0.6457, "o", " "] +[0.6808, "o", "-"] +[0.7159, "o", "1"] +[0.751, "o", " "] +[0.7861, "o", "-"] +[0.8212, "o", "-"] +[0.8563, "o", "f"] +[0.8914, "o", "o"] +[0.9265, "o", "r"] +[0.9616, "o", "m"] +[0.9967, "o", "a"] +[1.0318, "o", "t"] +[1.0669, "o", "="] +[1.102, "o", "'"] +[1.1371, "o", "%"] +[1.1722, "o", "h"] +[1.2073, "o", " "] +[1.2424, "o", "%"] +[1.2775, "o", "s"] +[1.3126, "o", "'"] +[1.3477, "o", " "] +[1.3828, "o", "|"] +[1.4179, "o", " "] +[1.453, "o", "c"] +[1.4881, "o", "u"] +[1.5232, "o", "t"] +[1.5583, "o", " "] +[1.5934, "o", "-"] +[1.6285, "o", "c"] +[1.6636, "o", "1"] +[1.6987, "o", "-"] +[1.7338, "o", "1"] +[1.7689, "o", "0"] +[1.804, "o", "0"] +[2.1391, "o", "\r\n"] +[2.1657, "o", "4655702ed docs(fpga): experience log for wasm_board_ax7203 silicon run\r\n"] +[2.1669, "x", "0"] +[3.1669, "o", "\u001b[38;2;255;215;0mt27 $ \u001b[0m"] +[3.5669, "o", "o"] +[3.602, "o", "p"] +[3.6371, "o", "e"] +[3.6722, "o", "n"] +[3.7073, "o", "F"] +[3.7424, "o", "P"] +[3.7775, "o", "G"] +[3.8126, "o", "A"] +[3.8477, "o", "L"] +[3.8828, "o", "o"] +[3.9179, "o", "a"] +[3.953, "o", "d"] +[3.9881, "o", "e"] +[4.0232, "o", "r"] +[4.0583, "o", " "] +[4.0934, "o", "-"] +[4.1285, "o", "-"] +[4.1636, "o", "s"] +[4.1987, "o", "c"] +[4.2338, "o", "a"] +[4.2689, "o", "n"] +[4.304, "o", "-"] +[4.3391, "o", "u"] +[4.3742, "o", "s"] +[4.4093, "o", "b"] +[4.7444, "o", "\r\n"] +[4.7801, "o", "empty\r\nBus device vid:pid probe_type manufacturer serial product\r\n000 005 0x0403:0x6014 ft232H Digilent 210512180081 Digilent USB Device\r\n000 002 0x0403:0x6010 FTDI2232 Digilent 210203859289 Digilent Adept USB Device"] +[4.7809, "o", "\r\n"] +[4.7816, "x", "0"] +[5.7816, "o", "\u001b[38;2;255;215;0mt27 $ \u001b[0m"] +[6.1816, "o", "o"] +[6.2167, "o", "p"] +[6.2518, "o", "e"] +[6.2869, "o", "n"] +[6.322, "o", "F"] +[6.3571, "o", "P"] +[6.3922, "o", "G"] +[6.4273, "o", "A"] +[6.4624, "o", "L"] +[6.4975, "o", "o"] +[6.5326, "o", "a"] +[6.5677, "o", "d"] +[6.6028, "o", "e"] +[6.6379, "o", "r"] +[6.673, "o", " "] +[6.7081, "o", "-"] +[6.7432, "o", "c"] +[6.7783, "o", " "] +[6.8134, "o", "d"] +[6.8485, "o", "i"] +[6.8836, "o", "g"] +[6.9187, "o", "i"] +[6.9538, "o", "l"] +[6.9889, "o", "e"] +[7.024, "o", "n"] +[7.0591, "o", "t"] +[7.0942, "o", "_"] +[7.1293, "o", "h"] +[7.1644, "o", "s"] +[7.1995, "o", "2"] +[7.2346, "o", " "] +[7.2697, "o", "-"] +[7.3048, "o", "-"] +[7.3399, "o", "b"] +[7.375, "o", "u"] +[7.4101, "o", "s"] +[7.4452, "o", "d"] +[7.4803, "o", "e"] +[7.5154, "o", "v"] +[7.5505, "o", "-"] +[7.5856, "o", "n"] +[7.6207, "o", "u"] +[7.6558, "o", "m"] +[7.6909, "o", " "] +[7.726, "o", "0"] +[7.7611, "o", "0"] +[7.7962, "o", "0"] +[7.8313, "o", ":"] +[7.8664, "o", "0"] +[7.9015, "o", "0"] +[7.9366, "o", "5"] +[7.9717, "o", " "] +[8.0068, "o", "/"] +[8.0419, "o", "t"] +[8.077, "o", "m"] +[8.1121, "o", "p"] +[8.1472, "o", "/"] +[8.1823, "o", "w"] +[8.2174, "o", "a"] +[8.2525, "o", "s"] +[8.2876, "o", "m"] +[8.3227, "o", "-"] +[8.3578, "o", "b"] +[8.3929, "o", "u"] +[8.428, "o", "i"] +[8.4631, "o", "l"] +[8.4982, "o", "d"] +[8.5333, "o", "/"] +[8.5684, "o", "w"] +[8.6035, "o", "a"] +[8.6386, "o", "s"] +[8.6737, "o", "m"] +[8.7088, "o", "_"] +[8.7439, "o", "b"] +[8.779, "o", "o"] +[8.8141, "o", "a"] +[8.8492, "o", "r"] +[8.8843, "o", "d"] +[8.9194, "o", "_"] +[8.9545, "o", "a"] +[8.9896, "o", "x"] +[9.0247, "o", "7"] +[9.0598, "o", "2"] +[9.0949, "o", "0"] +[9.13, "o", "3"] +[9.1651, "o", "."] +[9.2002, "o", "b"] +[9.2353, "o", "i"] +[9.2704, "o", "t"] +[9.6055, "o", "\r\n"] +[9.6302, "o", "empty\r\nJtag frequency : requested 6.00MHz -> real 6.00MHz "] +[9.6355, "o", "\r\nOpen file "] +[9.64, "o", "DONE\r\nParse file "] +[9.6589, "o", "DONE\r\nload program\r\n"] +[10.7533, "o", "\rLoad SRAM: [==== ] 7.00%"] +[11.8437, "o", "\r\n\rLoad SRAM: [======== ] 15.00%"] +[12.9432, "o", "\r\n\rLoad SRAM: [============ ] 23.00%"] +[14.0158, "o", "\r\n\rLoad SRAM: [================ ] 31.00%"] +[15.1244, "o", "\r\n\rLoad SRAM: [==================== ] 39.00%"] +[16.2358, "o", "\r\n\rLoad SRAM: [======================== ] 47.00%"] +[17.3394, "o", "\r\n\rLoad SRAM: [============================ ] 55.00%"] +[18.4551, "o", "\r\n\rLoad SRAM: [================================ ] 63.00%"] +[19.5531, "o", "\r\n\rLoad SRAM: [==================================== ] 71.00%"] +[20.6458, "o", "\r\n\rLoad SRAM: [======================================== ] 79.00%"] +[21.7531, "o", "\r\n\rLoad SRAM: [============================================ ] 87.00%"] +[22.8339, "o", "\r\n\rLoad SRAM: [================================================ ] 95.00%"] +[23.3924, "o", "\r\n\rLoad SRAM: [==================================================] 100.00%\r\n\r\nDone"] +[23.3957, "o", "\r\nShift IR 35\r\nir: 1 isc_done 1 isc_ena 0 init 1 done 1\r\n"] +[23.3975, "x", "0"] +[24.3975, "o", "\u001b[38;2;255;215;0mt27 $ \u001b[0m"] +[24.7975, "o", "p"] +[24.8326, "o", "y"] +[24.8677, "o", "t"] +[24.9028, "o", "h"] +[24.9379, "o", "o"] +[24.973, "o", "n"] +[25.0081, "o", "3"] +[25.0432, "o", " "] +[25.0783, "o", "c"] +[25.1134, "o", "o"] +[25.1485, "o", "n"] +[25.1836, "o", "f"] +[25.2187, "o", "o"] +[25.2538, "o", "r"] +[25.2889, "o", "m"] +[25.324, "o", "a"] +[25.3591, "o", "n"] +[25.3942, "o", "c"] +[25.4293, "o", "e"] +[25.4644, "o", "/"] +[25.4995, "o", "w"] +[25.5346, "o", "a"] +[25.5697, "o", "s"] +[25.6048, "o", "m"] +[25.6399, "o", "_"] +[25.675, "o", "b"] +[25.7101, "o", "o"] +[25.7452, "o", "a"] +[25.7803, "o", "r"] +[25.8154, "o", "d"] +[25.8505, "o", "_"] +[25.8856, "o", "a"] +[25.9207, "o", "x"] +[25.9558, "o", "7"] +[25.9909, "o", "2"] +[26.026, "o", "0"] +[26.0611, "o", "3"] +[26.0962, "o", "."] +[26.1313, "o", "p"] +[26.1664, "o", "y"] +[26.2015, "o", " "] +[26.2366, "o", "-"] +[26.2717, "o", "-"] +[26.3068, "o", "s"] +[26.3419, "o", "e"] +[26.377, "o", "l"] +[26.4121, "o", "f"] +[26.4472, "o", "-"] +[26.4823, "o", "t"] +[26.5174, "o", "e"] +[26.5525, "o", "s"] +[26.5876, "o", "t"] +[26.9227, "o", "\r\n"] +[26.9923, "o", "self-test on 134 requests, model board, window 1\r\n [ok ] clean model board passes every request\r\n [ok ] fault lie caught: WASM BOARD RESULT: 133/134 responses exact (fails=1, lost=0)\r\n [ok ] fault badop caught: WASM BOARD RESULT: 133/134 responses exact (fails=1, lost=0)\r\n [ok ] fault seq caught: WASM BOARD RESULT: 7/134 responses exact (fails=0, lost=127)\r\n [ok ] fault drop caught: WASM BOARD RESULT: 7/134 responses exact (fails=0, lost=127)\r\n [ok ] fault slip caught: WASM BOARD RESULT: 7/134 responses exact (fails=0, lost=127)\r\n [ok ] wrong program caught: WASM BOARD RESULT: 19/134 responses exact (fails=115, lost=0)\r\n [ok ] program 13 answered BADOP SEQ 0 0 0\r\n [ok ] program 99 answered BADOP SEQ 0 0 0\r\n [ok ] frame layout SYNC0 SYNC1 OP SEQ SA SB x[4] LE, pad to Y_OFF\r\nSELF-TEST PASS\r\n"] +[26.9969, "x", "0"] +[27.9969, "o", "\u001b[38;2;255;215;0mt27 $ \u001b[0m"] +[28.3969, "o", "p"] +[28.432, "o", "y"] +[28.4671, "o", "t"] +[28.5022, "o", "h"] +[28.5373, "o", "o"] +[28.5724, "o", "n"] +[28.6075, "o", "3"] +[28.6426, "o", " "] +[28.6777, "o", "c"] +[28.7128, "o", "o"] +[28.7479, "o", "n"] +[28.783, "o", "f"] +[28.8181, "o", "o"] +[28.8532, "o", "r"] +[28.8883, "o", "m"] +[28.9234, "o", "a"] +[28.9585, "o", "n"] +[28.9936, "o", "c"] +[29.0287, "o", "e"] +[29.0638, "o", "/"] +[29.0989, "o", "w"] +[29.134, "o", "a"] +[29.1691, "o", "s"] +[29.2042, "o", "m"] +[29.2393, "o", "_"] +[29.2744, "o", "b"] +[29.3095, "o", "o"] +[29.3446, "o", "a"] +[29.3797, "o", "r"] +[29.4148, "o", "d"] +[29.4499, "o", "_"] +[29.485, "o", "a"] +[29.5201, "o", "x"] +[29.5552, "o", "7"] +[29.5903, "o", "2"] +[29.6254, "o", "0"] +[29.6605, "o", "3"] +[29.6956, "o", "."] +[29.7307, "o", "p"] +[29.7658, "o", "y"] +[29.8009, "o", " "] +[29.836, "o", "-"] +[29.8711, "o", "-"] +[29.9062, "o", "p"] +[29.9413, "o", "o"] +[29.9764, "o", "r"] +[30.0115, "o", "t"] +[30.0466, "o", " "] +[30.0817, "o", "/"] +[30.1168, "o", "d"] +[30.1519, "o", "e"] +[30.187, "o", "v"] +[30.2221, "o", "/"] +[30.2572, "o", "c"] +[30.2923, "o", "u"] +[30.3274, "o", "."] +[30.3625, "o", "u"] +[30.3976, "o", "s"] +[30.4327, "o", "b"] +[30.4678, "o", "s"] +[30.5029, "o", "e"] +[30.538, "o", "r"] +[30.5731, "o", "i"] +[30.6082, "o", "a"] +[30.6433, "o", "l"] +[30.6784, "o", "-"] +[30.7135, "o", "1"] +[30.7486, "o", "3"] +[30.7837, "o", "0"] +[31.1188, "o", "\r\n"] +[31.4876, "o", "# spec wasm_board_ax7203 sha256 d3b18e197cda742e0e22608e2021d23db320be5d800206c75bb2e94107954400\r\n# port /dev/cu.usbserial-130 baud 1144744 (wire 1142857) window 1\r\n# vectors sha256 89527478cf813f99b9e76fce77f1b846b5f3ddbf22cb212f08b5ac0eec108f8f (134 requests)\r\n [ok ] error bad 1/1\r\n [ok ] error op99 1/1\r\n [ok ] pinned add_double 2/2\r\n [ok ] pinned answer 2/2\r\n [ok ] pinned xmul_not 2/2\r\n [ok ] pinned sub_and_or 2/2\r\n [ok ] pinned shl_sar 2/2\r\n [ok ] pinned eq 2/2\r\n [ok ] pinned ne 2/2\r\n [ok ] pinned le_s 2/2\r\n [ok ] pinned ge_s 2/2\r\n [ok ] pinned eqz 2/2\r\n [ok ] pinned gt_s 2/2\r\n [ok ] pinned set_get 2/2\r\n [ok ] pinned tee_dup 2/2\r\n [ok ] random add_double 8/8\r\n [ok ] random answer 8/8\r\n [ok ] random xmul_not 8/8\r\n [ok ] random sub_and_or 8/8\r\n [ok ] random shl_sar 8/8\r\n [ok ] random eq 8/8\r\n [ok ] random ne 8/8\r\n [ok ] random le_s 8/8\r\n [ok ] random ge_s 8/8\r\n [ok ] random eqz 8/8\r\n [ok ] random gt_s 8/8\r\n [ok ] random set_get 8/8\r\n [ok ] random tee_dup 8/8\r\n [ok ] wrap add_double 1/1\r\n [ok ] wrap sub_and_or 1/1\r\nsent 134, answered 134, window 1\r\nelapsed 0.3 s (492 answers/s)\r\nWASM BOARD RESULT: 134/134 responses exact (fails=0, lost=0)\r\n"] +[31.5014, "x", "0"] +[32.5014, "o", "\u001b[38;2;255;215;0mt27 $ \u001b[0m"] diff --git a/apps/website/scripts/wasm-board-cast.mjs b/apps/website/scripts/wasm-board-cast.mjs new file mode 100644 index 0000000000..068076697c --- /dev/null +++ b/apps/website/scripts/wasm-board-cast.mjs @@ -0,0 +1,162 @@ +#!/usr/bin/env node +// wasm-board-cast.mjs -- record the WASM interpreter board cell's silicon conformance run. +// +// One recording, five commands, against the board on the desk: the checkout the +// conformance trio belongs to, the USB scan that shows the one FT232H, the openFPGALoader +// flash of the openXC7 bitstream, the model-board self-test that proves every check can +// fail, and the 134-request run over UART. It reuses the recording machinery of +// t27c-mutation-casts.mjs: the prompt and the typing are staged, every output byte and +// its timing are the real ones, and a silence longer than 2 s is shown for 2 s. +// +// It refuses to write anything unless every command exits 0, the scan shows exactly one +// FT232H at the given busdev, the flash prints done, the self-test prints SELF-TEST PASS, +// and the run prints the spec's PASS_LINE with the vectors sha256 the published run used. +// A recording that does not show what the post says is not published. +// +// Run: node scripts/wasm-board-cast.mjs --repo ~/trinity-fpga --bit /tmp/wasm-build/wasm_board_ax7203.bit \ +// [--busdev 000:005] [--port /dev/cu.usbserial-130] [--chrome PATH] +import { spawn } from 'node:child_process' +import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import { bareEnv } from './tri-help-casts.mjs' +import { eventsOf, castText, shownOf, Screen, pageHtml, cardHtml, indexWith } from './t27c-mutation-casts.mjs' + +const WIDTH = 104 +const HEIGHT = 30 +const TERM = join(fileURLToPath(new URL('../public/term/', import.meta.url))) +const ORIGIN = 'https://t27.ai' +const ID = 'wasm-board-ax7203' +const POST = 'https://t27.ai/#/blog/a-wasm-interpreter-ran-on-the-board' +const VECTORS_SHA_PREFIX = '89527478cf813f99' // conformance/wasm_board_vectors.py --write, 2026-10-12 run +const PASS_LINE = 'WASM BOARD RESULT: 134/134 responses exact (fails=0, lost=0)' + +// ------------------------------------------------------------------ running (as t27c-mutation-casts runs) + +function run(command, cwd, env) { + return new Promise((resolve) => { + const t0 = process.hrtime.bigint() + const at = () => Number(process.hrtime.bigint() - t0) / 1e9 + const chunks = [] + const child = spawn('bash', ['-c', command], { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] }) + const take = (buf) => chunks.push({ at: at(), text: buf.toString('utf8') }) + child.stdout.on('data', take) + child.stderr.on('data', take) + child.on('error', (e) => resolve({ chunks: [{ at: at(), text: `${e.message}\n` }], exit: 'spawn-error', real: at() })) + child.on('close', (code, signal) => resolve({ chunks, exit: String(code ?? signal), real: at() })) + }) +} + +const screenAfter = (events, upTo = Infinity) => { + const s = new Screen() + for (const [t, k, text] of events) if (k === 'o' && t <= upTo) s.feed(text) + return s +} + +function findChrome() { + return [process.env.CHROME, '/usr/bin/chromium', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/Applications/BrowserOS neo.app/Contents/MacOS/BrowserOS neo'].find((p) => p && existsSync(p)) +} + +async function render(chrome, html, png) { + const { spawn } = await import('node:child_process') + const { mkdtempSync, rmSync } = await import('node:fs') + const { tmpdir } = await import('node:os') + const { pathToFileURL } = await import('node:url') + const work = mkdtempSync(join(tmpdir(), 'term-card-')) + const src = join(work, 'card.html') + writeFileSync(src, html) + const before = existsSync(png) ? statSync(png).mtimeMs : 0 + const b = spawn(chrome, ['--headless=new', `--user-data-dir=${join(work, 'profile')}`, '--allow-file-access-from-files', '--hide-scrollbars', + '--force-device-scale-factor=1', '--window-size=1200,630', '--virtual-time-budget=3000', `--screenshot=${png}`, pathToFileURL(src).href], { stdio: 'ignore', detached: true }) + const deadline = Date.now() + 60_000 + while (Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 300)) + if (existsSync(png) && statSync(png).mtimeMs > before && statSync(png).size > 0) break + } + await new Promise((r) => setTimeout(r, 300)) + const exited = new Promise((r) => b.once('exit', r)) + try { process.kill(-b.pid, 'SIGKILL') } catch { b.kill('SIGKILL') } + await Promise.race([exited, new Promise((r) => setTimeout(r, 5000))]) + try { rmSync(work, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }) } catch { /* a temp dir the OS will clear */ } + if (!existsSync(png) || statSync(png).mtimeMs <= before) throw new Error(`wasm-board-cast: no card written to ${png}`) +} + +// ------------------------------------------------------------------ main + +async function main() { + const arg = (k, d) => { const i = process.argv.indexOf(k); return i > 0 ? process.argv[i + 1] : d } + const repo = arg('--repo') + const bit = arg('--bit') + const busdev = arg('--busdev', '000:005') + const port = arg('--port', '/dev/cu.usbserial-130') + if (!repo || !bit) { console.error('usage: wasm-board-cast.mjs --repo DIR --bit FILE [--busdev 000:005] [--port /dev/cu.usbserial-130] [--chrome PATH]'); process.exit(2) } + const chrome = arg('--chrome') || findChrome() + if (!chrome) throw new Error('wasm-board-cast: no Chromium for the card; set --chrome') + if (!existsSync(bit)) throw new Error(`wasm-board-cast: no bitstream at ${bit}`) + if (!existsSync(port)) throw new Error(`wasm-board-cast: no UART port at ${port}`) + const env = { ...bareEnv(), COLUMNS: String(WIDTH), LINES: String(HEIGHT), PATH: process.env.PATH } + + const commands = [ + "git log -1 --format='%h %s' | cut -c1-100", + 'openFPGALoader --scan-usb', + `openFPGALoader -c digilent_hs2 --busdev-num ${busdev} ${bit}`, + 'python3 conformance/wasm_board_ax7203.py --self-test', + `python3 conformance/wasm_board_ax7203.py --port ${port}`, + ] + const steps = [] + for (const c of commands) steps.push(await run(c, repo, env)) + const out = (i) => steps[i].chunks.map((c) => c.text).join('') + + const problems = [] + steps.forEach((s, i) => { if (s.exit !== '0') problems.push(`command ${i} (${commands[i]}) exited ${s.exit}`) }) + const scan = out(1) + const ft232 = scan.split('\n').filter((l) => /ft232H/i.test(l)) + // --scan-usb prints "Bus device" as two space-separated columns ("000 005"), the flag takes a colon. + const onAir = busdev.replace(':', ' ') + if (ft232.length !== 1 || !ft232[0].includes(onAir)) problems.push(`scan shows ${ft232.length} FT232H lines, wanted exactly one at ${onAir}`) + if (!/done/.test(out(2))) problems.push('the flash did not print done') + if (!out(3).includes('SELF-TEST PASS')) problems.push('the self-test did not print SELF-TEST PASS') + const runOut = out(4) + if (!runOut.split('\n').some((l) => l.trim() === PASS_LINE)) problems.push(`the run did not print the spec's PASS_LINE`) + if (!runOut.includes(`vectors sha256 ${VECTORS_SHA_PREFIX}`)) problems.push(`the run's vectors sha256 does not start ${VECTORS_SHA_PREFIX}`) + if (problems.length) { console.error(problems.join('\n')); process.exit(1) } + + const events = eventsOf(commands, steps) + const recordedAt = new Date() + const recorded = recordedAt.toISOString().replace('T', ' ').slice(0, 16) + ' UTC' + const real = events[events.length - 1][0] + const commit = out(0).trim().split(' ')[0] + const meta = { + id: ID, + title: 'WASM on the AX7203: flash, self-test, 134/134 exact', + desc: `The t27-generated WASM interpreter on an ALINX AX7203 (xc7a200t), bitstream from the openXC7 flow, spec at trinity-fpga ${commit}: ` + + 'openFPGALoader flashes over the onboard FT232H, the model-board self-test shows every conformance check able to fail, ' + + 'and one run answers all 134 requests exactly over UART.', + url: `${ORIGIN}/term/${ID}/`, + gif: null, + post: POST, + recorded, + real_s: Number(real.toFixed(1)), + shown_s: Number(shownOf(events).toFixed(1)), + commands, + exit_codes: steps.map((s) => s.exit), + redacted: [], + published: recordedAt.toISOString().slice(0, 10), + } + const header = { version: 2, width: WIDTH, height: HEIGHT, timestamp: Math.floor(recordedAt.getTime() / 1000), title: meta.title, commands, redacted: [] } + const dir = join(TERM, ID) + mkdirSync(dir, { recursive: true }) + writeFileSync(join(dir, 'session.cast'), castText(header, events)) + writeFileSync(join(dir, 'meta.json'), JSON.stringify(meta, null, 2) + '\n') + writeFileSync(join(dir, 'index.html'), pageHtml(meta, screenAfter(events))) + // The card shows the moment the run has printed its report: the per-program rows and the PASS_LINE. + const atPass = events.filter((e) => e[1] === 'x').at(-1)[0] + await render(chrome, cardHtml(meta, screenAfter(events, atPass)), join(dir, 'card.png')) + writeFileSync(join(TERM, 'index.html'), indexWith(readFileSync(join(TERM, 'index.html'), 'utf8'), [meta])) + console.log(`wasm-board-cast: wrote ${ID} (${meta.real_s} s real, card at the PASS_LINE), listed in public/term/index.html`) +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) await main() diff --git a/apps/website/src/data/blog/bodies/a-wasm-interpreter-ran-on-the-board.ts b/apps/website/src/data/blog/bodies/a-wasm-interpreter-ran-on-the-board.ts new file mode 100644 index 0000000000..13c57575a1 --- /dev/null +++ b/apps/website/src/data/blog/bodies/a-wasm-interpreter-ran-on-the-board.ts @@ -0,0 +1,171 @@ +import type { Block } from '../types' + +export const body: Block[] = [ + { + kind: 'p', + text: 'Overnight on 12 October one t27 spec became a Verilog core, openXC7 placed it on an ALINX AX7203 (Artix-7 XC7A200T-2), and the chip answered every request exactly: 134 of 134, six runs in a row, 804 of 804 answers bit for bit. This post is the receipt trail -- what runs, what it took, and what it is not.', + }, + { + kind: 'terminal', + src: 'term/wasm-board-ax7203/session.cast', + title: 'The bench session: flash, self-test, run', + caption: 'Recorded on the bench, 12 October 2026, 04:14 UTC: five commands, all exit 0, 32.5 seconds. The page with the player is at t27.ai/term/wasm-board-ax7203/.', + share: 'https://t27.ai/term/wasm-board-ax7203/', + }, + { kind: 'h', text: 'What the silicon runs' }, + { + kind: 'p', + text: 'The core on the chip is fpga_wasm_board, generated by t27c gen-verilog from specs/fpga/wasm_board_ax7203.t27 in gHashTag/t27 (branch wasm-board, commit 351e3cdf4). It speaks UART at 1,144,744 baud, parses AA 55 frames, and interprets 21 opcodes of the WebAssembly MVP set. Fourteen programs -- 101 bytes of them -- sit in a ROM built from the same spec; the wire carries requests and answers, not binaries.', + }, + { + kind: 'ul', + items: [ + 'UART receive and transmit at 1,144,744 baud, with an AA 55 frame parser in front.', + 'A 21-opcode interpreter for the WebAssembly MVP set.', + 'A ROM of 14 programs, 101 bytes, assembled from the spec.', + 'i32.mul as a 32-clock shift-add microsequence -- the one instruction allowed to take its time.', + ], + }, + { + kind: 'p', + text: 'The bring-up top -- STARTUPE2 for the configuration clock, a three-flop UART synchroniser, the LEDs -- is hand-written Verilog (fpga/vivado/wasm_board_ax7203.v), under the owner-approved-foreign exception in that repository. Everything that computes is generated from the spec; everything that only brings the chip up is not.', + }, + { kind: 'h', text: 'Timing: a nextpnr estimate, 18.57 to 70.41 MHz' }, + { + kind: 'p', + text: 'The toolchain is openXC7: yosys with -flatten -abc9 -nocarry -nodsp -nosrl, then nextpnr (heap placer, router1, seed 1). The first cut was a nextpnr estimate of 18.57 MHz. Rewriting i32.mul into a 32-clock shift-add microsequence took the estimate to 70.41 MHz against a 68.7 MHz bar. The core costs 3,910 of 269,200 LUTs on the XC7A200T -- one percent -- and 1,127 flip-flops. The local build takes about 65 seconds without Docker, and the bitstream is 9,730,755 bytes.', + }, + { kind: 'h', text: 'The self-test caught the first bug, and it was ours' }, + { + kind: 'p', + text: 'Before any hardware run, the conformance harness in trinity-fpga checks a model of the board against a golden interpreter -- an independent WASM MVP interpreter with its own opcode table and a general signed-LEB128 decoder, sharing no code with the spec. Fifteen pin cross-checks tie the spec port list to the wires.', + }, + { + kind: 'p', + text: 'The harness self-test injects six faults -- a lie, a bad opcode, a sequence break, a dropped frame, a bit slip, the wrong program -- and proves every check fails on its own fault. That is what caught the real bug before the board did: X_OFF and Y_OFF in the spec are offsets into the frame body, but on the wire x sits at byte six, two sync bytes in. Read the offsets the wrong way and 76 of 134 checks fail -- against a board that is working perfectly. The self-test turned that into a fix made before the first flash.', + }, + { kind: 'h', text: 'On the board' }, + { + kind: 'p', + text: 'openFPGALoader flashes the bitstream over the FT232H cable in about 15 seconds, with no sudo, and reports done. Then the board answered 134 of 134 requests exactly -- six consecutive runs, 804 of 804 answers, zero mismatches, zero lost frames, 416 answers per second at the pace of the link. The vector set is pinned by hash:', + }, + { + kind: 'code', + text: '89527478cf813f99b9e76fce77f1b846b5f3ddbf22cb212f08b5ac0eec108f8f', + }, + { + kind: 'table', + head: ['What', 'Result'], + rows: [ + ['Timing (nextpnr estimate)', 'estimate 70.41 MHz against a 68.7 bar; 18.57 before the i32.mul microsequence'], + ['Area on XC7A200T', '3,910 of 269,200 LUTs (1%); 1,127 FFs'], + ['Board runs', '6 x 134/134 exact = 804/804; 416 answers/s'], + ['Build', 'openXC7, local, ~65 s, no Docker; bitstream 9,730,755 bytes'], + ], + }, + { kind: 'h', text: 'The neighbourhood' }, + { + kind: 'p', + text: 'Software interpreters -- wasm3, WAMR, wasmi -- run WASM on CPUs and run it well; WASM in hardware is a much thinner file. A small academic set of hand-written WASM-on-FPGA designs appeared through 2024-2026, and eBPF offload on smart NICs plus the old Java-in-silicon line (picoJava, JOP) are the precedent that a bytecode ISA can be a hardware target at all. What is different here is not speed -- none is claimed against any of them -- but provenance and order of work: the computing core is generated from a spec, and the conformance harness, with its self-tested fault injection, was written and passing before the first bitstream touched the board.', + }, + { kind: 'h', text: 'What this is not' }, + { + kind: 'ul', + items: [ + 'Twenty-one opcodes is a subset of the WebAssembly MVP set, not the ISA.', + 'Programs live in a ROM built from the spec; nothing on the wire loads a .wasm binary yet.', + '416 answers per second is the pace of the UART link, not a ceiling of the interpreter.', + 'One board, one night, six runs; no voltage or temperature corners were swept.', + 'The bring-up top is hand-written Verilog under the owner-approved-foreign exception.', + ], + }, + { + kind: 'p', + text: 'Epic trinity-fpga#917 -- AI-generated FPGA hardware that executes WASM -- has its MVP closed on silicon. Next: widen the opcode set, load programs over the wire, and shrink the hand-written top toward zero.', + }, +] + +export const ruBody: Block[] = [ + { + kind: 'p', + text: 'За ночь на 12 октября одна спека t27 стала Verilog-ядром, openXC7 уложила его на ALINX AX7203 (Artix-7 XC7A200T-2), и кристалл ответил на каждый запрос точно: 134 из 134, шесть прогонов подряд, 804 из 804 ответов бит в бит. Этот пост -- цепочка квитанций: что работает, чего это стоило и чем это не является.', + }, + { + kind: 'terminal', + src: 'term/wasm-board-ax7203/session.cast', + title: 'Сессия на стенде: прошивка, самотест, прогон', + caption: 'Записано на стенде 12 октября 2026, 04:14 UTC: пять команд, все с кодом 0, 32,5 секунды. Страница с плеером -- t27.ai/term/wasm-board-ax7203/.', + share: 'https://t27.ai/term/wasm-board-ax7203/', + }, + { kind: 'h', text: 'Что исполняет кремний' }, + { + kind: 'p', + text: 'Ядро на кристалле -- fpga_wasm_board, сгенерированное t27c gen-verilog из specs/fpga/wasm_board_ax7203.t27 в gHashTag/t27 (ветка wasm-board, коммит 351e3cdf4). Оно говорит по UART на 1 144 744 бода, разбирает кадры AA 55 и исполняет 21 опкод из набора WebAssembly MVP. Четырнадцать программ -- 101 байт -- лежат в ROM, собранном из той же спеки; по проводу ходят запросы и ответы, а не бинарники.', + }, + { + kind: 'ul', + items: [ + 'Приём и передача UART на 1 144 744 бода, впереди парсер кадров AA 55.', + 'Интерпретатор на 21 опкод из набора WebAssembly MVP.', + 'ROM на 14 программ, 101 байт, собранный из спеки.', + 'i32.mul как 32-тактовая микропоследовательность сдвигов и сложений -- единственная команда, которой позволено не спешить.', + ], + }, + { + kind: 'p', + text: 'Верхний уровень подъёма платы -- STARTUPE2 для конфигурационного тактового сигнала, трёхтриггерный синхронизатор UART, светодиоды -- написан вручную (fpga/vivado/wasm_board_ax7203.v), под исключением owner-approved-foreign в том репозитории. Всё, что считает, сгенерировано из спеки; всё, что только поднимает кристалл, -- нет.', + }, + { kind: 'h', text: 'Тайминг: оценка nextpnr, от 18,57 до 70,41 МГц' }, + { + kind: 'p', + text: 'Инструментарий -- openXC7: yosys с -flatten -abc9 -nocarry -nodsp -nosrl, затем nextpnr (размещатель heap, router1, seed 1). Оценка nextpnr для первого варианта -- 18,57 МГц. Переписывание i32.mul в 32-тактовую микропоследовательность сдвигов и сложений подняло оценку до 70,41 МГц при планке 68,7. Ядро стоит 3 910 из 269 200 LUT на XC7A200T -- один процент -- и 1 127 триггеров. Локальная сборка занимает около 65 секунд без Docker, битстрим -- 9 730 755 байт.', + }, + { kind: 'h', text: 'Самотест поймал первый баг, и это был наш баг' }, + { + kind: 'p', + text: 'До любого прогона на железе конформансный стенд в trinity-fpga сверяет модель платы с золотым интерпретатором -- независимым интерпретатором WASM MVP со своей таблицей опкодов и общим декодером signed-LEB128, не делящим со спекой ни строчки. Пятнадцать перекрёстных проверок привязывают список портов спеки к проводам.', + }, + { + kind: 'p', + text: 'Самотест стенда вводит шесть отказов -- ложь, плохой опкод, разрыв последовательности, потерянный кадр, соскальзывание бита, чужая программа -- и доказывает, что каждая проверка падает на своём отказе. Именно это и поймало настоящий баг до платы: X_OFF и Y_OFF в спеке -- смещения в теле кадра, но на проводе x лежит в байте шесть, после двух байтов синхронизации. Прочитай смещения не так -- и 76 из 134 проверок падают, при полностью исправной плате. Самотест превратил это в правку до первой прошивки.', + }, + { kind: 'h', text: 'На плате' }, + { + kind: 'p', + text: 'openFPGALoader прошивает битстрим по кабелю FT232H секунд за пятнадцать, без sudo, и отвечает done. Дальше плата ответила на 134 из 134 запросов точно -- шесть прогонов подряд, 804 из 804 ответов, ноль расхождений, ноль потерянных кадров, 416 ответов в секунду в темпе линии. Набор векторов закреплён хэшем:', + }, + { + kind: 'code', + text: '89527478cf813f99b9e76fce77f1b846b5f3ddbf22cb212f08b5ac0eec108f8f', + }, + { + kind: 'table', + head: ['Что', 'Результат'], + rows: [ + ['Тайминг (оценка nextpnr)', 'оценка: 70,41 МГц при планке 68,7; 18,57 до микропоследовательности i32.mul'], + ['Площадь на XC7A200T', '3 910 из 269 200 LUT (1%); 1 127 триггеров'], + ['Прогоны на плате', '6 x 134/134 точно = 804/804; 416 ответов/с'], + ['Сборка', 'openXC7, локально, ~65 с, без Docker; битстрим 9 730 755 байт'], + ], + }, + { kind: 'h', text: 'Округа' }, + { + kind: 'p', + text: 'Программные интерпретаторы -- wasm3, WAMR, wasmi -- исполняют WASM на CPU и делают это хорошо; WASM в железе -- куда менее населённая область. За 2024-2026 появилась небольшая академическая серия рукописных WASM-on-FPGA проектов, а выгрузка eBPF на умные сетевые карты и старая линия Java-в-кремнии (picoJava, JOP) -- прецедент того, что байткод вообще может быть целью для железа. Отличие здесь не в скорости -- против них не заявлено никакой, -- а в происхождении и порядке работы: считающее ядро сгенерировано из спеки, а конформансный стенд с самотестом отказов был написан и проходил до того, как первый битстрим коснулся платы.', + }, + { kind: 'h', text: 'Чем это не является' }, + { + kind: 'ul', + items: [ + 'Двадцать один опкод -- подмножество набора WebAssembly MVP, а не весь ISA.', + 'Программы лежат в ROM, собранном из спеки; .wasm-бинарник по проводу пока не грузится.', + '416 ответов в секунду -- это темп UART-линии, а не потолок интерпретатора.', + 'Одна плата, одна ночь, шесть прогонов; углы по напряжению и температуре не проверялись.', + 'Верхний уровень подъёма платы -- рукописный Verilog под исключением owner-approved-foreign.', + ], + }, + { + kind: 'p', + text: 'Эпик trinity-fpga#917 -- ИИ-сгенерированное железо, исполняющее WASM -- закрыл свой MVP на кремнии. Дальше: расширять набор опкодов, грузить программы по проводу и ужимать рукописный верхний уровень до нуля.', + }, +] diff --git a/apps/website/src/data/blog/index.ts b/apps/website/src/data/blog/index.ts index 16fcb8b3e1..77f37996f0 100644 --- a/apps/website/src/data/blog/index.ts +++ b/apps/website/src/data/blog/index.ts @@ -2,6 +2,40 @@ import type { PostMeta } from './types' /** Индекс блога: список и метаданные без тяжёлых тел публикаций. */ export const postsIndex: PostMeta[] = [ + { + slug: "a-wasm-interpreter-ran-on-the-board", + title: "A WASM interpreter ran on the board: 134 of 134, from a spec", + summary: "[the programs live in a ROM built from the spec; the wire carries requests, not .wasm binaries; one board, one night, six runs] A t27 spec in gHashTag/t27 (branch wasm-board, 351e3cdf4) became a Verilog core through t27c gen-verilog, passed through openXC7 onto an ALINX AX7203, and answered 134 of 134 requests exactly -- six runs in a row, 804 of 804. 21 opcodes of the WASM MVP set, a 14-program 101-byte ROM, i32.mul as a 32-clock shift-add microsequence; timing went from 18.57 to 70.41 MHz against a 68.7 bar at one percent of the LUTs -- every frequency here is a nextpnr estimate, not sign-off timing. The self-tested conformance harness caught a wire-offset bug that would have failed 76 checks against a perfectly working board.", + date: "2026-10-12", + readingMinutes: 4, + tags: ["t27", "FPGA", "WASM"], + receipts: [ + { label: "trinity-fpga#918: the merged PR, +676/-0", href: "https://github.com/gHashTag/trinity-fpga/pull/918" }, + { label: "Epic trinity-fpga#917: AI-generated FPGA hardware that executes WASM", href: "https://github.com/gHashTag/trinity-fpga/issues/917" }, + { label: "The spec: gHashTag/t27 specs/fpga/wasm_board_ax7203.t27 (wasm-board, 351e3cdf4)", href: "https://github.com/gHashTag/t27/blob/351e3cdf4/specs/fpga/wasm_board_ax7203.t27" }, + { label: "The recorded bench session", href: "https://t27.ai/term/wasm-board-ax7203/" }, + { label: "Experience log: the night on the bench", href: "https://github.com/gHashTag/trinity-fpga/blob/main/fpga/experience/2026-10-12-wasm-board-ax7203.trinity.md" }, + ], + openQuestions: [ + "Twenty-one opcodes is a subset of the WebAssembly MVP set; the full ISA is unclaimed.", + "The programs live in a ROM built from the spec; nothing on the wire loads a .wasm binary yet.", + "416 answers per second is the pace of the 1,144,744-baud UART link, not a ceiling of the interpreter.", + "One board, one night, six runs: no voltage or temperature corners were swept.", + "The bring-up top is hand-written Verilog under the owner-approved-foreign exception; shrinking it is future work.", + ], + published: true, + ru: { + title: "WASM-интерпретатор заработал на плате: 134 из 134, из спеки", + summary: "[программы лежат в ROM, собранном из спеки; по проводу ходят запросы, а не .wasm-бинарники; одна плата, одна ночь, шесть прогонов] Спека t27 в gHashTag/t27 (ветка wasm-board, 351e3cdf4) стала Verilog-ядром через t27c gen-verilog, прошла через openXC7 на ALINX AX7203 и ответила на 134 из 134 запросов точно -- шесть прогонов подряд, 804 из 804. 21 опкод из набора WASM MVP, ROM на 14 программ и 101 байт, i32.mul как 32-тактовая микропоследовательность; тайминг вырос с 18,57 до 70,41 МГц при планке 68,7 на одном проценте LUT -- каждая частота здесь оценка nextpnr, а не подписанный тайминг. Самотест конформансного стенда поймал баг со смещениями в проводе, который завалил бы 76 проверок на полностью исправной плате.", + openQuestions: [ + "Двадцать один опкод -- подмножество набора WebAssembly MVP; полный ISA не заявлен.", + "Программы лежат в ROM, собранном из спеки; .wasm-бинарник по проводу пока не грузится.", + "416 ответов в секунду -- темп UART-линии на 1 144 744 бода, а не потолок интерпретатора.", + "Одна плата, одна ночь, шесть прогонов: углы по напряжению и температуре не проверялись.", + "Верхний уровень подъёма платы -- рукописный Verilog под исключением owner-approved-foreign; ужимать его -- будущая работа.", + ], + }, + }, { slug: "arithmetic-and-dsp-a-widget-per-lesson", title: "Arithmetic and DSP on the FPGA: 27 lessons, and a widget in each one that runs a t27 spec", diff --git a/apps/website/src/data/blog/posts.ts b/apps/website/src/data/blog/posts.ts index cc2adf5a0a..b88647a9c6 100644 --- a/apps/website/src/data/blog/posts.ts +++ b/apps/website/src/data/blog/posts.ts @@ -1,3 +1,4 @@ +import { body as body_a_wasm_interpreter_ran_on_the_board, ruBody as ruBody_a_wasm_interpreter_ran_on_the_board } from './bodies/a-wasm-interpreter-ran-on-the-board' import { body as body_arithmetic_and_dsp_a_widget_per_lesson, ruBody as ruBody_arithmetic_and_dsp_a_widget_per_lesson } from './bodies/arithmetic-and-dsp-a-widget-per-lesson' import { body as body_the_ternary_machine_course, ruBody as ruBody_the_ternary_machine_course } from './bodies/the-ternary-machine-course' import { body as body_three_widgets_under_the_bus, ruBody as ruBody_three_widgets_under_the_bus } from './bodies/three-widgets-under-the-bus' @@ -113,6 +114,7 @@ import { body as body_features_that_change_no_bits, ruBody as ruBody_features_th import { body as body_one_commit_nine_workflow_outcomes, ruBody as ruBody_one_commit_nine_workflow_outcomes } from './bodies/one-commit-nine-workflow-outcomes' const bodies: Record = { + 'a-wasm-interpreter-ran-on-the-board': { body: body_a_wasm_interpreter_ran_on_the_board, ruBody: ruBody_a_wasm_interpreter_ran_on_the_board }, 'a-3-mb-compiler-spec-to-aarch64-in-5-ms': { body: body_a_3_mb_compiler_spec_to_aarch64_in_5_ms, ruBody: ruBody_a_3_mb_compiler_spec_to_aarch64_in_5_ms }, 'actors-beat-the-loop-on-recovery-not-everywhere': { body: body_actors_beat_the_loop_on_recovery_not_everywhere, ruBody: ruBody_actors_beat_the_loop_on_recovery_not_everywhere }, 'from-a-minute-to-a-second': { body: body_from_a_minute_to_a_second, ruBody: ruBody_from_a_minute_to_a_second },