diff --git a/docs/fpga/T27OS_ON_FPGA_FEASIBILITY.md b/docs/fpga/T27OS_ON_FPGA_FEASIBILITY.md new file mode 100644 index 0000000000..fbc73399ae --- /dev/null +++ b/docs/fpga/T27OS_ON_FPGA_FEASIBILITY.md @@ -0,0 +1,235 @@ +# T27 OS on the FPGA: feasibility and plan + +Refs gHashTag/t27#10782 (T27 OS epic), PR #10783 (the T27 OS specs). Owner, 2026-10-19: "ideally T27 OS should +run on the FPGA". + +Status words used below: **measured** = run in this work on the Railway lab; **read** = judged from the spec's +text, not lowered or run; **not established** = nothing in the repository says. + +## 1. The answer in one paragraph + +The decisions of T27 OS can run on the bench AX7203 today; its window cannot, and should not. Four panes whose +herdr status is advanced by the real `pane_step` from `specs/t27os/pane_state.t27` lower to about 470 LCs and 270 +flip-flops (**measured**, yosys `synth_xilinx`), and their screen leaves the board as VT100 on the UART, where the +T27 OS terminal pane (or any terminal) shows it. That is `specs/fpga/t27os_uart_shell_ax7203.t27`. The window stays +on the Mac, as a client of the board, exactly as it is a client of the PTY server today. + +## 2. What T27 OS is made of, and where each part can go + +T27 OS specs on `feat/t27os-petal-menu` (PR #10783). Counts are from the spec text: `@extern` imports, slice +types, `while` loops, `f64`. + +| spec | what it decides | host-bound? | on the board | +|---|---|---|---| +| `agents.t27` | agent table, session states S_*, restore actions | no (scalars, two `str` consts) | **measured**: its S_* constants are used by the shell spec; the fns are scalar | +| `pane_state.t27` | pane status word and `pane_step`, `shown_state`, `pane_needs_you` | no | **measured**: `pane_step`, `shown_state`, `pane_needs_you`, `ps_state` lower inside the shell spec (`hook_event` takes a slice, so it is left out) | +| `vt.t27` | VT/ANSI grid: bytes in, cells out | no | **measured as a test-side reader only**: it uses slices and loops, which `t27b verilog` does not take in the clocked path. The board only writes bytes; it never parses them. A grid on silicon is stage 3 | +| `windows.t27` | the window model: spaces, sessions, the sidebar | no (no `@extern`, no slices; 17 loops, 19 `f64`) | **read**: pure logic, but loop-heavy; it needs a ROM/FSM rewrite of its loops and a fixed-point rewrite of its `f64` before it can lower | +| `queen.t27` | Queen panel, suggestions, GOD MODE | no (6 loops, 60 `f64`) | **read**: same as `windows.t27`; `f64` is the cost | +| `layout.t27`, `term_pane.t27` | placement of cells and panels in pixels; key bytes to the PTY | no, but pixel geometry only matters to a display | **read**: `layout` is `f64` geometry, useless without a display; `term_pane`'s key-to-bytes table is a ROM and could lower | +| `doctor.t27` | repositories on the machine, disk cost | yes in spirit (it reads the file system through the host) | stays on the host | +| `pty_proto.t27`, `pty_logic.t27`, `session_store.t27` | wire format, slot allocation, the manifest | `pty_proto` is pure but slice-heavy; `pty_logic` has 2 `@extern`; `session_store` is a file format | `pty_proto`'s frame codec is the natural wire format for stage 2 (Ethernet or UART framing); the rest stays on the host | +| `pty_server.t27`, `pty_client.t27`, `pty_sys.t27` | the PTY server and its client over libSystem | **yes**: `forkpty`, `poll`, Unix sockets (32 `@extern` in `pty_sys`) | replaced on the board by the UART/Ethernet link (see section 3) | +| `glass_window.t27`, `hot_reload.t27` | the AppKit window, hot reload | **yes**: AppKit, CoreGraphics, CoreText, `ld` | stays on the Mac; it becomes the display client of the board | + +Two routes lower pure logic, and both exist in the repository: + +1. **t27 to Verilog to bitstream.** `t27b verilog` (the pure-t27 board flow, as `specs/fpga/rc_filter_ax7203.t27`). + Limits that decide what lowers: no signed types, no slices and no array indexing in the clocked path, tables + become ROMs, no loops in the tick body. `pane_state` and `agents` meet these; `vt`, `windows`, `queen` do not + yet. +2. **t27 to WASM on the existing wasm board cell** (`specs/fpga/wasm_board_ax7203.t27`). Its wire format and + `on_clock(rxd)` step loop would run a compiled `windows`/`queen` unchanged in source; its top is outside this + repository, so this route was **not** exercised here. It is the way to run the loop-heavy specs without + rewriting them, at the price of a WASM core on the die. + +## 3. What replaces the host-bound parts + +| host-bound part | on the board | +|---|---| +| AppKit window, CoreText drawing | the T27 OS window as a client: it already hosts a terminal pane, so it shows the board's VT100 stream. HDMI is **not established**: no AX7203 HDMI pin table or schematic is in the repository (`fpga/HARDWARE_SSOT.md` lists UART, LEDs, the 200 MHz clock, the two RGMII PHYs and DDR3 only) | +| `forkpty` and the agent processes | nothing: an agent CLI cannot run on an FPGA. The board holds the agents' **status**, not the agents. A hook from the host (Claude Code's `UserPromptSubmit`, `Notification`, `Stop`) becomes one event byte on the link | +| Unix socket to the PTY server | UART (stage 1), then Ethernet (stage 2) | +| Keychain | not replaced; secrets stay on the host. The board never holds a credential | +| the manifest on disk | the board has no disk; the 16 MiB N25Q128 flash is on the AX7203 (`fpga/HARDWARE_SSOT.md`), but a manifest in flash is **not established** as a plan | + +## 4. I/O on the AX7203 + +From `fpga/HARDWARE_SSOT.md` (the bench board is an ALINX AX7203, `xc7a200tfbg484-2`): + +- **UART**: TX N15, RX P20, LVCMOS33. 115200 8N1 from a 100 MHz PLL clock (divider 868) is what + `rc_filter_ax7203.t27` already measured on this board. +- **Ethernet**: two KSZ9031 RGMII PHYs (the cabled PHY 1 runs at 100 Mb/s). The repository has `eth_beacon`, + `eth_rx`, `eth_reply`, `eth_crc`, and a UDP node on this board was measured on 2026-10-10 (HARDWARE_SSOT.md). + That is the stage 2 transport. +- **LEDs**: B13 C13 D14 D15, active low. The shell uses them as blocked / working / done / heartbeat. +- **Button**: T6 (active low), LVCMOS15; resets the shell. +- **HDMI output**: an SiI9134 transmitter fed by a parallel RGB bus, 29 pins checked against the official AX7203 manual pp. 31-32 (section 5b); I2C balls still second-hand. +- **DDR3**: 1 GB present; two DDR3 cells exist (`ddr3_head_bandwidth`, `ddr3_ternary_matvec`). Not needed before a + scrollback ring per pane (stage 3). + +## 5. Staged plan + +| stage | what runs on the board | what it needs | state | +|---|---|---|---| +| 0 | `pane_step`/`shown_state` on silicon, screen as VT100 over UART; keys and a script drive the events | this PR | spec, tests and RTL lowering **measured**; bitstream and bench run **blocked** (section 6) | +| 1 | the host feeds real events: a small host-side client sends one byte per Claude Code hook to the board's RX, so the board's screen shows the user's real panes | a host client in `.t27` (the window's `pty_client.t27` already speaks to a server; a UART transport is a new `@extern` set in `pty_sys.t27`) | next | +| 2 | the same over Ethernet with `pty_proto.t27`'s frame codec | lower `pty_proto` encode/decode (slice-free subset), reuse the UDP node | later | +| 3 | `windows.t27` (spaces, sidebar) and a grid (`vt`) on the board | rewrite loops as FSMs or run them on the wasm cell; `f64` to fixed point; DDR3 for scrollback | later | +| 4 | display | parallel-RGB scanout (`vga_text_core.t27`, done) plus an SiI9134 I2C initialiser and a pin table checked against the manual | core **measured**; board top blocked (section 5b) | + +## 5b. Stage 4: a text screen on the owner's VGA monitor (2026-10-19) + +The owner's monitor is VGA, attached to the board's HDMI output through an active HDMI-to-VGA adapter. + +`specs/fpga/vga_text_core.t27` is the part that does not depend on how the pixels leave the chip: 640x480@60 +timing (800x525 total, syncs low), an 8x8 glyph ROM and a text scanout, one `on_clock` per pixel. **Measured** on +the lab: 7 tests pass (96 low hsync ticks a line, 800 ticks a line, 2 low vsync lines, 525 lines, 307200 visible +pixels, the first glyph row of "T" on the stream, the stream equal to `pixel_at` for 8 lines, blanking black); +`t27b verilog` lowers it (636 lines); yosys `synth_xilinx` passes (392 cells). + +### The AX7203 HDMI output is not a TMDS output from the FPGA + +Two community projects that read the ALINX manual (pages 31-32 for the video part) agree, pin for pin, that the +board's HDMI output is an **SiI9134 HDMI transmitter fed by a parallel bus**: 24 data bits, a pixel clock, DE, +HSYNC and VSYNC, plus a reset and an I2C configuration bus. The FPGA does not drive TMDS pairs, so a TMDS +encoder and 10:1 serialiser in `.t27` would have no pin to drive on this board. The route is a **parallel RGB +scanout** (what the core already is) plus an **I2C initialiser** for the SiI9134. The adapter then sees an +ordinary HDMI/DVI signal from the transmitter chip. + +Sources: the pin table below is **checked against the official manual** (see the record under the table); the +first two were the second-hand sources it was found in: + +- `LilithSemi/harbor`, `packages/harbor/lib/src/soc/board.dart` (AX7203 pin table, "p.31-32"). +- `adilsondias-engineer/20-fpga-order-book`, `constraints/ax7203_rgmii.xdc` (lines 324-345 and after). +- The ALINX manual itself (see the record under the table). + +Both lists agree, and so does the manual (Table 3.4.1, pp. 31-32), on every HDMI-output ball below (all LVCMOS33). Only the two I2C rows are not in the manual text: + +| signal | ball | signal | ball | +|---|---|---|---| +| hdmi_clk (to IDCK) | M13 | hdmi_d[12] | L14 | +| hdmi_de | V13 | hdmi_d[13] | L15 | +| hdmi_hsync | T15 | hdmi_d[14] | M15 | +| hdmi_vsync | T14 | hdmi_d[15] | M16 | +| hdmi_reset_n | J19 | hdmi_d[16] | L18 | +| hdmi_d[0] | V14 | hdmi_d[17] | M18 | +| hdmi_d[1] | H14 | hdmi_d[18] | N18 | +| hdmi_d[2] | J14 | hdmi_d[19] | N19 | +| hdmi_d[3] | K13 | hdmi_d[20] | M20 | +| hdmi_d[4] | K14 | hdmi_d[21] | N20 | +| hdmi_d[5] | L13 | hdmi_d[22] | L21 | +| hdmi_d[6] | L19 | hdmi_d[23] | M21 | +| hdmi_d[7] | L20 | i2c scl | E16 | +| hdmi_d[8] | K17 | i2c sda | F16 | +| hdmi_d[9] | J17 | | | +| hdmi_d[10] | L16 | | | +| hdmi_d[11] | K16 | | | + +**Record of the check (2026-10-19).** File `AX7203 V1.0 User Manual.pdf`, 2,814,987 bytes, sha256 +`361a512dc683be0f7f7d415362259ea8ed34aedff55f6469e20542fe6f4ddce8`, 43 pages, "Copyright Alinx 2026", body text +names the AX7203 (the PDF's Subject metadata says AX7101; it is a copy-over, the content is the AX7203). Source: +ALINX's own Download page (`https://www.en.alinx.com/` > Download), which links the public ALINX Dropbox folder +`_Standard Products / Artix-7 / AX7203 / AX7203_2023.1`. The PDF is not committed (licence); this table is. +Section 3.4 "HDMI Output Interface" names the chip as the SIL9134 ("supports up to 1080P@60Hz output and 3D +output"), says its IIC configuration interface is wired to FPGA I/O and that it is "initialized and controlled +through FPGA programming", and lists the 29 balls in Table 3.4.1 on pp. 31-32. Every one equals the table above. +Section 3.5 is the HDMI **input** (SIL9013), a different connector. + +The data bits are not named by colour in the manual: the chip's input setup (over I2C) decides which group is +red, green or blue. The I2C balls come from LiteX, not from the manual ("the manual draws the nets but gives no +balls"), and the manual gives no I2C address. The initialiser in `harbor` (`sii9134_initializer.dart`) writes +two registers, `0x72 <- reg 0x08 = 0x35` and `0x7A <- reg 0x2F = 0x00`, after a reset low of at least 50 us; that +sequence is **third-party and not run on this bench**. + +### What is still missing before a board top + +1. ~~The pin table checked against the official manual~~ **Done 2026-10-19** for the 29 HDMI-output pins, and the + two I2C balls are now confirmed too: the official ALINX schematic package `04_...zip` (6,327,128 bytes, sha256 + `fdb16c3c0da5eb9db2be3a07d18f6dbab55b771d8e10f34419722564ac869b61`, same public Dropbox folder, not committed) + holds `AX7203_PIN_Define.xls`, which lists HDMI_SCL = E16 (bank 16 L2_P), HDMI_SDA = F16 (bank 16 L5_N) and + 9134_NRESET = J19. The schematic PDF `AX7203-2018-2-2.pdf` in it is titled "AX7203 extension board" and shows + SiI9134 pins CSCL/CSDA on those nets, 33R series on DSCL/DSDA, R62/R63 4.7K pull-ups to 3V3 on the HDMI_SCL/SDA + nets, and CI2CA tied through R57 10K (the other end is a ground symbol the text extraction cannot read, so the + address bit is not established here). +2. The SiI9134 input setup for 24-bit RGB with separate syncs. **Read 2026-10-19 as a fact source only** (text + read, nothing copied or run) from ALINX's own HDMI-output demo `11_1_hdmi_out.zip` + (`01_demo_document/Demo/11_1_hdmi_out`, same public Dropbox folder; 9,814,322 bytes, sha256 + `65a7d521b6b8a502f14a975e0caae2a621b9c0a36a206b95e6b10eef43ae02fa`; not committed). Its `lut_hdmi.v` writes six + 8-bit-register entries as (8-bit I2C write address, register, value): `72:08=35`, `7A:2F=00`, `60:05=10`, + `60:08=05`, `60:09=01`, `60:05=04`; the list ends at the first entry whose address byte is `FF`. That fixes the + SiI9134 write address as **0x72** (7-bit 0x39, so the CI2CA strap reads low) and shows two more devices + written at 0x7A and 0x60. Facts about the demo's board use: `hdmi_nreset` is the PLL `locked` flag (reset is + released when the pixel PLL locks), I2C starts only after that, and the picture generator is held in reset + until the last I2C entry is done. Pin lines in its `hdmi_out.xdc` match the table above (J19, M13, E16, F16; + clock R4 `DIFF_SSTL15`). The demo's timing is 1920x1080 by default (`video_define.v`), so its pixel clock is + not ours; whether the same six writes are right for 640x480 at 25 MHz is **not established** (the register + meanings are in the chip's reference, which this session does not have). Still unproven on silicon. +3. A way to drive an open-drain I2C pad from `.t27`: no tristate primitive was found in the repository, and + `t27b verilog` was not shown to emit one. This needs a look in `specs/fpga/` for an `IOBUF`-style cell. +4. A 25 MHz pixel clock: from the 200 MHz input, the PLL used by the other cells gives 25 MHz with a VCO at + 800 MHz and an output divide of 32 (to be checked in the PLL spec, not assumed). + +The monitor needs plugging in only when the board top for this route is merged and its bitstream is +programmed. The cable to connect is **the board's HDMI output (the connector marked HDMI output, not HDMI input) +to the HDMI-to-VGA adapter, and the adapter to the monitor's VGA cable**. + +## 6. Why there is no bench transcript in this PR + +What was done: the spec's 8 tests pass at run time (`t27b test` under qemu-aarch64 on the lab, since the t27b JIT +runs on arm64 only), the RTL lowers (`t27b verilog`, 1074 lines), yosys synthesises it for xc7 (1274 cells, about +468 LCs, 0 problems from its check pass), and the pin stream is checked byte for byte against the screen function. + +What is missing, exactly: + +1. **Place and route.** The lab has yosys and iverilog but no `nextpnr-xilinx`, no `xc7a200tfbg484-2.bin` chipdb and + no prjxray database (`t27c preflight` there: "T27_OPENXC7 is not set"). The openXC7 toolchain and that chipdb + exist on the owner's Mac (`~/openxc7-src`), where the standing rule forbids heavy runs. The fix is to install + the toolchain on the lab, or to approve one place-and-route on the Mac. Until then no bitstream exists. +2. **The board.** `openFPGALoader --scan-usb` on the Mac found no USB device when it was run, so the cable was not + visible. +3. **RTL-level simulation of the pins.** The spec's own test already decodes the pin the machine drives and + compares it to the screen function; a simulation of the lowered Verilog through the PLL primitive was not done + (the lab has no unisim model of `PLLE2_ADV`, and a hand-written Verilog shim is barred by the only-t27 rule). + +No UART transcript is shown because none was captured. The expected screen is in the spec header and is asserted, +cell by cell, by `the_screen_is_the_t27os_vt_grid_as_the_layout_says`. + +## 7. Dependency + +The spec uses `specs/t27os/{agents,pane_state,vt}.t27` by `use`; those files are on `feat/t27os-petal-menu` +(PR #10783) and not yet on `master`. Until that PR lands, `t27b` cannot resolve the imports on `master` alone. + +## 6. What the trinity-fpga experience says about a video bitstream (read 2026-10-19) + +Source: `gHashTag/trinity-fpga` (checkout `~/trinity-fpga`, `fpga/*.md`, `fpga/openxc7-synth/`, +`docs/HW_CAMPAIGN_AX7203_2026_07_30.md`). Only the parts that bite a VGA/HDMI bitstream are kept here. + +- **No HDMI/TMDS design or pin exists there**; the HDMI map comes from the ALINX documents (section 5b). +- **Clock.** The 200 MHz oscillator is R4(+)/T4(-); declare it `DIFF_SSTL15`, **not** `LVDS`: with `LVDS` the + board's DONE stayed dark and nothing ran (`HARDWARE_REFERENCE.md`, Pin Mapping, Clock Input). It goes + IBUFDS then BUFG. The 25.175/25 MHz pixel clock must be derived from it, never from a fabric divider + (`COMMON_PITFALLS.md`, Clock Domain Issues: use clock enables, not divided clocks; a 25 MHz pixel clock needs an + MMCM/PLL or a one-in-eight enable on 200 MHz). +- **IO standards.** The HDMI-output pins are bank 13/15/16 and the manual lists them LVCMOS33; LEDs are LVCMOS18 + (B13, C13, D14, D15), reset T6 is LVCMOS15, UART N15/P20 is LVCMOS33. Banks must not mix VCCO: the VGA/HDMI bus + must stay on 3.3 V banks, the LED bank is 1.8 V. The openxc7 chipdb lists `TMDS_33`, but the AX7203 has no TMDS + pins to the connector (SiI9134 does the serialising), so it is not needed. +- **SRAM-only loading.** Bitstreams are loaded to SRAM with `openFPGALoader -c digilent_hs2` and the flash is left + alone (`fpga/HARDWARE_SSOT.md`); the Platform Cable `fxload` ritual in trinity-fpga applies to a 0x03FD cable + that is not on this bench. A bitstream that gets no DONE after load is a pin/IO-standard problem first + (the LVDS case above), not a cable problem. +- **Toolchain.** openXC7 (yosys + nextpnr-xilinx + prjxray) is the only toolchain that produced working + bitstreams (1 of 1; FORGE 0 of 23). nextpnr-xilinx mapped `LOC R23` to the T23 site on the 100T chipdb + (`COMMON_PITFALLS.md`): after P&R, **read the placed pin back from the FASM before trusting any pin** of the + video bus. The fbg484 chipdb is not on the lab yet (section 5, step 4). +- **Verification discipline.** A blinking or scanning output needs video, not a photo (`COMMON_PITFALLS.md`, + Single Photo for Blinking LED): `hdmi_grab` compares frames for this reason. Their Tier-E rule: nothing counts + as silicon without CI + bitstream sha256 + readback log + IDCODE, so every VGA/HDMI result in this note stays + **[simulated]** until a captured frame from the real output exists. +- **IDCODE is not in conflict.** trinity-fpga's campaign file says `0x13636093`, `fpga/HARDWARE_SSOT.md` measured + `0x03636093` on this bench. Both are the XC7A200T part number `0x3636`; the leading nibble is the silicon + revision (their own `fpga/IDCODE.md` lists "ver 1 / ver 0", and `ax7203_al321.cfg` accepts both). The SSOT + value is what this bench reads; use `./scripts/tri boards` rather than a literal. +- **Do not copy** from there: the QMTech XC7A100T pins (U22, T23, R23), the `0x13631093` IDCODE and the + Platform-Cable flow. They belong to another board and cable. diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index cdefaedfce..034d29f3c4 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -380,6 +380,7 @@ {"path": "specs/fpga/eth_rx.t27", "t27b": "pass"}, {"path": "specs/fpga/fifo.t27", "t27b": "pass"}, {"path": "specs/fpga/gf16_accel.t27", "t27b": "pass"}, + {"path": "specs/fpga/hdmi_grab.t27", "t27b": "blocked", "blocker": "type [N]T", "reason": "unimplemented"}, {"path": "specs/fpga/hw_types.t27", "t27b": "pass"}, {"path": "specs/fpga/jtag_status.t27", "t27b": "pass"}, {"path": "specs/fpga/linker.t27", "t27b": "pass"}, @@ -403,6 +404,7 @@ {"path": "specs/fpga/simulator.t27", "t27b": "pass"}, {"path": "specs/fpga/stdlib.t27", "t27b": "pass"}, {"path": "specs/fpga/systolic.t27", "t27b": "pass"}, + {"path": "specs/fpga/t27os_uart_shell_ax7203.t27", "t27b": "pass"}, {"path": "specs/fpga/tern9m_l0_k_weights.t27", "t27b": "pass"}, {"path": "specs/fpga/ternary_isa.t27", "t27b": "pass"}, {"path": "specs/fpga/ternary_link.t27", "t27b": "pass"}, @@ -421,6 +423,7 @@ {"path": "specs/fpga/trinet_node.t27", "t27b": "pass"}, {"path": "specs/fpga/vcd_conformance_compare.t27", "t27b": "blocked", "blocker": "ExprArrayLiteral", "reason": "unimplemented"}, {"path": "specs/fpga/verification/build_verify.t27", "t27b": "pass"}, + {"path": "specs/fpga/vga_text_core.t27", "t27b": "pass"}, {"path": "specs/fpga/wasm_board_ax7203.t27", "t27b": "pass"}, {"path": "specs/fpga/wasm_interp.t27", "t27b": "pass"}, {"path": "specs/functions/analytics-sales-advise.t27", "t27b": "pass"}, diff --git a/specs/fpga/hdmi_grab.t27 b/specs/fpga/hdmi_grab.t27 new file mode 100644 index 0000000000..fab95d27d6 --- /dev/null +++ b/specs/fpga/hdmi_grab.t27 @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/fpga/hdmi_grab.t27 -- grab the picture the AX7203 sends to the Mac through a USB HDMI dongle and +; judge it against the frame the board is meant to show (Refs gHashTag/t27#10782, T27 OS on the FPGA). +; Owner, 2026-10-19: the board's HDMI output is wired to this Mac through a USB HDMI dongle. macOS lists no +; capture device yet (FaceTime HD Camera, OBS Virtual Camera, Capture screen 0), so the dongle may not be +; a UVC capture device, or may not be enumerated. This program says which, instead of guessing. +; -- +; WHAT RUNS WHERE. t27 has no AVCaptureSession and no callbacks, so the grab is the plain `ffmpeg -f +; avfoundation` command line, started from main by libc `system`; everything that decides anything is +; ordinary t27 with tests: which listed device is the capture one, and whether a grabbed frame is the +; expected frame. No other language is involved. +; -- +; THE EXPECTED FRAME is specs/fpga/vga_text_core.t27's: black, with the title "T27 OS" in white 8x8 +; glyphs at the top left of a 640x480 picture. The checker asks that core's own `pixel_at`, so the +; expectation is not copied. A grabbed frame of any size is sampled at the centre of each of the 640x480 +; logical pixels, which assumes the dongle shows the whole picture: no border, no overscan. +; -- +; THE RULES, each with a test below: +; - device pick: in the "AVFoundation video devices" section of ffmpeg's list, the first device whose +; name has none of FaceTime, OBS Virtual, Capture screen, iPhone; -1 when there is none. The audio +; section is never read; +; - a frame is a binary PPM (P6, 255). Not that: verdict 1. Fewer bytes than its header promises, or +; smaller than 64x48: verdict 2; +; - the 384 title pixels (48x8) are lit (luma >= 128) exactly where pixel_at says, give or take +; TITLE_TOL; more mismatches: verdict 3. A black, a white, an inverted and an eight-pixel-shifted +; frame all fail here; +; - on a grid over the rest of the picture (every 16th logical pixel, past the title rows) at most BG_TOL +; samples are lit; more: verdict 4. A frame with a lit square in the middle fails here; +; - otherwise verdict 0, PASS. +; -- +; HOW (main): HG_FRAMES (default 3) frames, HG_TRIES (default 12) looks for the device, 5 s apart, then +; stops with a message. It writes /tmp/hdmi_grab_N.ppm and .png. On the lab: `t27c gen` this file, then +; `zig build-exe -lc`; the Mac runs only that binary: ./hdmi_grab +; -- +; The honest limits: +; - it does not know the dongle. A device that ffmpeg lists is taken to be the capture one; if two pass +; the filter, the first wins, and the message lists all of them; +; - the checker is a threshold on luma, not a similarity score; blur, a scaler's ringing or a changed +; gamma inside TITLE_TOL pass, anything beyond fails; +; - the pattern is the VGA core's one-bit frame. Colour bars are not in the core (its output is one +; bit); a 24-bit pattern needs the core to drive the SiI9134's RGB bus first; +; - the avfoundation input's first frames are often dark, so the grab keeps the fifth; +; - nothing here proves the board is the source. A frame that passes is the expected frame seen on the +; dongle; the board in the loop is the reading of the owner's eyes at the other end of the cable. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module fpga_hdmi_grab; + +use fpga::vga_text_core::pixel_at; + +pub const TITLE_W : usize = 48; +pub const TITLE_H : usize = 8; +pub const TITLE_TOL : usize = 16; +pub const BG_TOL : usize = 8; +pub const LOGICAL_W : usize = 640; +pub const LOGICAL_H : usize = 480; +pub const LUMA_ON : usize = 128; +pub const MIN_W : usize = 64; +pub const MIN_H : usize = 48; + +pub const V_PASS : usize = 0; +pub const V_NOT_PPM : usize = 1; +pub const V_SHORT : usize = 2; +pub const V_TITLE : usize = 3; +pub const V_BACKGROUND : usize = 4; + +// ---- text helpers ---- + +pub fn hg_is_digit(c: u8) -> bool { + return c >= '0' and c <= '9'; +} + +/// Does s[a..b] contain the fixed text nd. +pub fn hg_has(s: []const u8, a: usize, b: usize, nd: []const u8) -> bool { + const k : usize = nd.len; + if (k == 0 or b < a + k) { return false; } + var i : usize = a; + while (i + k <= b) { + var j : usize = 0; + var same : bool = true; + while (j < k) { + if (s[i + j] != nd[j]) { same = false; } + j += 1; + } + if (same) { return true; } + i += 1; + } + return false; +} + +// ---- which listed device is the capture one ---- + +/// The ']' that closes the last "[digits]" in the line s[a..b]; b when the line has none. +pub fn hg_br_end(s: []const u8, a: usize, b: usize) -> usize { + var last : usize = b; + var i : usize = a; + while (i < b) { + if (s[i] == '[') { + var j : usize = i + 1; + while (j < b and hg_is_digit(s[j])) { j += 1; } + if (j > i + 1 and j < b and s[j] == ']') { last = j; } + } + i += 1; + } + return last; +} + +/// The number inside the "[digits]" that ends at e. +pub fn hg_br_val(s: []const u8, a: usize, e: usize) -> i32 { + var v : i32 = 0; + var m : i32 = 1; + var j : usize = e; + while (j > a and hg_is_digit(s[j - 1])) { + v = v + m * @as(i32, @intCast(s[j - 1] - '0')); + m = m * 10; + j -= 1; + } + return v; +} + +/// A name this program never takes for the board's capture device. +pub fn hg_excluded(s: []const u8, a: usize, b: usize) -> bool { + if (hg_has(s, a, b, "FaceTime")) { return true; } + if (hg_has(s, a, b, "OBS Virtual")) { return true; } + if (hg_has(s, a, b, "Capture screen")) { return true; } + if (hg_has(s, a, b, "iPhone")) { return true; } + return false; +} + +/// The index of the n-th (from 0) video device that is not excluded in ffmpeg's avfoundation list +/// s[0..len]; -1 when there are not that many. +pub fn hg_pick_nth(s: []const u8, len: usize, want: usize) -> i32 { + var in_video : bool = false; + var seen : usize = 0; + var ls : usize = 0; + var i : usize = 0; + while (i <= len) { + if (i == len or s[i] == '\n') { + if (hg_has(s, ls, i, "AVFoundation video devices")) { + in_video = true; + } else if (hg_has(s, ls, i, "AVFoundation audio devices")) { + in_video = false; + } else if (in_video) { + const e : usize = hg_br_end(s, ls, i); + if (e < i and hg_excluded(s, e + 1, i) == false) { + if (seen == want) { return hg_br_val(s, ls, e); } + seen += 1; + } + } + ls = i + 1; + } + i += 1; + } + return -1; +} + +pub fn hg_pick(s: []const u8, len: usize) -> i32 { + return hg_pick_nth(s, len, 0); +} + +/// How many video devices the list has, excluded or not. +pub fn hg_video_count(s: []const u8, len: usize) -> usize { + var in_video : bool = false; + var n : usize = 0; + var ls : usize = 0; + var i : usize = 0; + while (i <= len) { + if (i == len or s[i] == '\n') { + if (hg_has(s, ls, i, "AVFoundation video devices")) { + in_video = true; + } else if (hg_has(s, ls, i, "AVFoundation audio devices")) { + in_video = false; + } else if (in_video and hg_br_end(s, ls, i) < i) { + n += 1; + } + ls = i + 1; + } + i += 1; + } + return n; +} + +// ---- the frame ---- + +pub fn hg_skip_ws(f: []const u8, n: usize, p: usize) -> usize { + var i : usize = p; + while (i < n and (f[i] == ' ' or f[i] == '\n' or f[i] == '\t' or f[i] == '\r')) { i += 1; } + return i; +} + +pub fn hg_num_end(f: []const u8, n: usize, p: usize) -> usize { + var i : usize = p; + while (i < n and hg_is_digit(f[i])) { i += 1; } + return i; +} + +pub fn hg_num_at(f: []const u8, p: usize, e: usize) -> usize { + var v : usize = 0; + var i : usize = p; + while (i < e) { + v = v * 10 + @as(usize, @intCast(f[i] - '0')); + i += 1; + } + return v; +} + +/// The byte just past the header of a P6 frame f[0..n] with maxval 255; 0 when it is not one. +pub fn hg_data_start(f: []const u8, n: usize) -> usize { + if (n < 11 or f[0] != 'P' or f[1] != '6') { return 0; } + const a : usize = hg_skip_ws(f, n, 2); + const ae : usize = hg_num_end(f, n, a); + const b : usize = hg_skip_ws(f, n, ae); + const be : usize = hg_num_end(f, n, b); + const c : usize = hg_skip_ws(f, n, be); + const ce : usize = hg_num_end(f, n, c); + if (ae == a or be == b or ce == c or ce >= n) { return 0; } + if (hg_num_at(f, c, ce) != 255) { return 0; } + return ce + 1; +} + +pub fn hg_width(f: []const u8, n: usize) -> usize { + const a : usize = hg_skip_ws(f, n, 2); + return hg_num_at(f, a, hg_num_end(f, n, a)); +} + +pub fn hg_height(f: []const u8, n: usize) -> usize { + const a : usize = hg_skip_ws(f, n, 2); + const b : usize = hg_skip_ws(f, n, hg_num_end(f, n, a)); + return hg_num_at(f, b, hg_num_end(f, n, b)); +} + +/// Is the logical pixel (x, y) of the 640x480 picture lit in the w x h frame whose pixels start at d. +pub fn hg_lit(f: []const u8, d: usize, w: usize, h: usize, x: usize, y: usize) -> bool { + const sx : usize = (x * 2 + 1) * w / (2 * LOGICAL_W); + const sy : usize = (y * 2 + 1) * h / (2 * LOGICAL_H); + const o : usize = d + (sy * w + sx) * 3; + const l : usize = (@as(usize, @intCast(f[o])) * 30 + @as(usize, @intCast(f[o + 1])) * 59 + @as(usize, @intCast(f[o + 2])) * 11) / 100; + return l >= LUMA_ON; +} + +/// The verdict on the frame f[0..n]: V_PASS or the first rule it breaks. +pub fn hg_check(f: []const u8, n: usize) -> usize { + const d : usize = hg_data_start(f, n); + if (d == 0) { return V_NOT_PPM; } + const w : usize = hg_width(f, n); + const h : usize = hg_height(f, n); + if (w < MIN_W or h < MIN_H or n < d + w * h * 3) { return V_SHORT; } + var bad : usize = 0; + var y : usize = 0; + while (y < TITLE_H) { + var x : usize = 0; + while (x < TITLE_W) { + const want : bool = pixel_at(@as(u32, @intCast(x)), @as(u32, @intCast(y))); + if (hg_lit(f, d, w, h, x, y) != want) { bad += 1; } + x += 1; + } + y += 1; + } + if (bad > TITLE_TOL) { return V_TITLE; } + var lit : usize = 0; + y = 16; + while (y < LOGICAL_H) { + var x : usize = 8; + while (x < LOGICAL_W) { + if (hg_lit(f, d, w, h, x, y)) { lit += 1; } + x += 16; + } + y += 16; + } + if (lit > BG_TOL) { return V_BACKGROUND; } + return V_PASS; +} + +// ---- synthetic frames, for the tests ---- + +pub fn hg_put_num(out: []u8, p: usize, v: usize) -> usize { + var digits : usize = 1; + var t : usize = v; + while (t >= 10) { + digits += 1; + t = t / 10; + } + var k : usize = digits; + t = v; + while (k > 0) { + out[p + k - 1] = @as(u8, @intCast(t % 10)) + '0'; + t = t / 10; + k -= 1; + } + return p + digits; +} + +pub fn hg_put_header(out: []u8, w: usize, h: usize) -> usize { + out[0] = 'P'; + out[1] = '6'; + out[2] = '\n'; + var p : usize = hg_put_num(out, 3, w); + out[p] = ' '; + p = hg_put_num(out, p + 1, h); + out[p] = '\n'; + p = hg_put_num(out, p + 1, 255); + out[p] = '\n'; + return p + 1; +} + +/// A w x h frame in out: mode 0 the expected frame, 1 black, 2 white, 3 inverted, 4 the title eight +/// logical pixels to the right, 5 the expected frame plus a lit 80x80 square in the middle. +pub fn hg_synth(out: []u8, w: usize, h: usize, mode: usize) -> usize { + var p : usize = hg_put_header(out, w, h); + var py : usize = 0; + while (py < h) { + var px : usize = 0; + while (px < w) { + const x : usize = px * LOGICAL_W / w; + const y : usize = py * LOGICAL_H / h; + var on : bool = false; + if (mode == 0 or mode == 5) { on = pixel_at(@as(u32, @intCast(x)), @as(u32, @intCast(y))); } + if (mode == 2) { on = true; } + if (mode == 3) { on = pixel_at(@as(u32, @intCast(x)), @as(u32, @intCast(y))) == false; } + if (mode == 4 and x >= 8) { on = pixel_at(@as(u32, @intCast(x - 8)), @as(u32, @intCast(y))); } + if (mode == 5 and x >= 280 and x < 360 and y >= 200 and y < 280) { on = true; } + var v : u8 = 0; + if (on) { v = 255; } + out[p] = v; + out[p + 1] = v; + out[p + 2] = v; + p += 3; + px += 1; + } + py += 1; + } + return p; +} + +// ---- tests ---- + +pub const LIST_NO_DONGLE : str = "[AVFoundation indev @ 0x1] AVFoundation video devices:\n[AVFoundation indev @ 0x1] [0] FaceTime HD Camera\n[AVFoundation indev @ 0x1] [1] OBS Virtual Camera\n[AVFoundation indev @ 0x1] [2] Capture screen 0\n[AVFoundation indev @ 0x1] AVFoundation audio devices:\n[AVFoundation indev @ 0x1] [0] MacBook Pro Microphone\n"; +pub const LIST_DONGLE : str = "[AVFoundation indev @ 0x1] AVFoundation video devices:\n[AVFoundation indev @ 0x1] [0] FaceTime HD Camera\n[AVFoundation indev @ 0x1] [1] USB3.0 Capture\n[AVFoundation indev @ 0x1] [2] OBS Virtual Camera\n[AVFoundation indev @ 0x1] [3] Capture screen 0\n[AVFoundation indev @ 0x1] AVFoundation audio devices:\n[AVFoundation indev @ 0x1] [0] USB3.0 Capture Audio\n"; +pub const LIST_AUDIO_ONLY : str = "[AVFoundation indev @ 0x1] AVFoundation video devices:\n[AVFoundation indev @ 0x1] [0] FaceTime HD Camera\n[AVFoundation indev @ 0x1] AVFoundation audio devices:\n[AVFoundation indev @ 0x1] [0] HDMI Audio Capture\n"; +pub const LIST_TWO_DIGIT : str = "[AVFoundation indev @ 0x1] AVFoundation video devices:\n[AVFoundation indev @ 0x1] [0] FaceTime HD Camera\n[AVFoundation indev @ 0x1] [12] Elgato HD60\n"; + +test hg_the_three_devices_macos_lists_today_are_no_capture_device { + assert hg_pick(LIST_NO_DONGLE, LIST_NO_DONGLE.len) == -1; + assert hg_video_count(LIST_NO_DONGLE, LIST_NO_DONGLE.len) == 3; +} + +test hg_a_dongle_in_the_video_section_is_picked_by_its_index { + assert hg_pick(LIST_DONGLE, LIST_DONGLE.len) == 1; + assert hg_video_count(LIST_DONGLE, LIST_DONGLE.len) == 4; + assert hg_pick_nth(LIST_DONGLE, LIST_DONGLE.len, 1) == -1; +} + +test hg_a_capture_name_in_the_audio_section_is_not_a_video_device { + assert hg_pick(LIST_AUDIO_ONLY, LIST_AUDIO_ONLY.len) == -1; +} + +test hg_a_two_digit_index_is_read_whole { + assert hg_pick(LIST_TWO_DIGIT, LIST_TWO_DIGIT.len) == 12; +} + +test hg_an_empty_list_has_no_device { + assert hg_pick("", 0) == -1; + assert hg_video_count("", 0) == 0; +} + +test hg_the_expected_frame_passes_at_640x480_and_at_1280x720 { + var buf : [3000000]u8 = undefined; + var n : usize = hg_synth(&buf, 640, 480, 0); + assert n == 15 + 640 * 480 * 3; + assert hg_check(&buf, n) == V_PASS; + n = hg_synth(&buf, 1280, 720, 0); + assert hg_check(&buf, n) == V_PASS; +} + +test hg_a_black_frame_fails_on_the_title { + var buf : [1000000]u8 = undefined; + const n : usize = hg_synth(&buf, 640, 480, 1); + assert hg_check(&buf, n) == V_TITLE; +} + +test hg_a_white_frame_fails_on_the_title { + var buf : [1000000]u8 = undefined; + const n : usize = hg_synth(&buf, 640, 480, 2); + assert hg_check(&buf, n) == V_TITLE; +} + +test hg_an_inverted_frame_fails_on_the_title { + var buf : [1000000]u8 = undefined; + const n : usize = hg_synth(&buf, 640, 480, 3); + assert hg_check(&buf, n) == V_TITLE; +} + +test hg_a_title_shifted_by_eight_pixels_fails_on_the_title { + var buf : [1000000]u8 = undefined; + const n : usize = hg_synth(&buf, 640, 480, 4); + assert hg_check(&buf, n) == V_TITLE; +} + +test hg_a_lit_square_in_the_middle_fails_on_the_background { + var buf : [1000000]u8 = undefined; + const n : usize = hg_synth(&buf, 640, 480, 5); + assert hg_check(&buf, n) == V_BACKGROUND; +} + +test hg_a_truncated_frame_is_short { + var buf : [1000000]u8 = undefined; + const n : usize = hg_synth(&buf, 640, 480, 0); + assert hg_check(&buf, n - 3) == V_SHORT; + assert hg_check(&buf, 40) == V_SHORT; +} + +test hg_a_frame_below_the_minimum_size_is_short { + var buf : [1000000]u8 = undefined; + const n : usize = hg_synth(&buf, 32, 24, 0); + assert hg_check(&buf, n) == V_SHORT; +} + +test hg_text_that_is_not_a_ppm_is_refused { + assert hg_check("hello, this is not an image at all", 34) == V_NOT_PPM; + assert hg_check("P5\n160 120\n255\n", 15) == V_NOT_PPM; + assert hg_check("P6\n160 120\n65535\n", 17) == V_NOT_PPM; + assert hg_check("", 0) == V_NOT_PPM; +} + +// ---- main: the libc part. The tests above never link libc; only main reaches it. ---- + +pub var HG_BUF : [8388608]u8 = undefined; +pub var HG_LIST : [65536]u8 = undefined; +pub var HG_CMD : [512]u8 = undefined; +pub var HG_PATH : [64]u8 = undefined; + +/// Copy the fixed text s into out at w; the new write position. +pub fn hg_cat(out: []u8, w: usize, s: []const u8) -> usize { + var i : usize = 0; + while (i < s.len) { + out[w + i] = s[i]; + i += 1; + } + return w + s.len; +} + +/// The number the environment variable name holds; dflt when it is unset or not a number. +fn hg_env(name: [*:0]const u8, dflt: usize) -> usize { + const p = std.c.getenv(name); + if (p == null) { return dflt; } + const s = p.?; + var v : usize = 0; + var i : usize = 0; + while (s[i] != 0) { + if (hg_is_digit(s[i]) == false) { return dflt; } + v = v * 10 + @as(usize, @intCast(s[i] - '0')); + i += 1; + } + if (i == 0) { return dflt; } + return v; +} + +/// Run the shell command in HG_CMD[0..w]; true when it exited with 0. +fn hg_sh(w: usize) -> bool { + HG_CMD[w] = 0; + var av : [4]usize = undefined; + av[0] = @intFromPtr("sh"); + av[1] = @intFromPtr("-c"); + av[2] = @intFromPtr(&HG_CMD); + av[3] = 0; + const pid = std.c.fork(); + if (pid == 0) { + _ = std.c.execve("/bin/sh", @ptrCast(&av), std.c.environ); + std.c._exit(127); + } + if (pid < 0) { return false; } + var st : c_int = 0; + _ = std.c.waitpid(pid, &st, 0); + return st == 0; +} + +/// Read the file named by the NUL-terminated HG_PATH into out; the byte count, 0 when it cannot be read. +fn hg_slurp(out: []u8) -> usize { + const fp = std.c.fopen(@ptrCast(&HG_PATH), "rb"); + if (fp == null) { return 0; } + var n : usize = 0; + var r : usize = 1; + while (r > 0 and n < out.len) { + r = std.c.fread(out[n..out.len].ptr, 1, out.len - n, fp.?); + n += r; + } + _ = std.c.fclose(fp.?); + return n; +} + +fn hg_path(name: []const u8, k: usize, ext: []const u8) -> void { + var w : usize = hg_cat(&HG_PATH, 0, name); + w = hg_put_num(&HG_PATH, w, k); + w = hg_cat(&HG_PATH, w, ext); + HG_PATH[w] = 0; +} + +pub fn main() void { + const frames : usize = hg_env("HG_FRAMES", 3); + const tries : usize = hg_env("HG_TRIES", 12); + var dev : i32 = -1; + var t : usize = 0; + while (t < tries and dev < 0) { + var w : usize = hg_cat(&HG_CMD, 0, "ffmpeg -hide_banner -f avfoundation -list_devices true -i \"\" 2> /tmp/hdmi_grab_list.txt"); + _ = hg_sh(w); + w = hg_cat(&HG_PATH, 0, "/tmp/hdmi_grab_list.txt"); + HG_PATH[w] = 0; + const n : usize = hg_slurp(&HG_LIST); + dev = hg_pick(&HG_LIST, n); + if (dev < 0) { + const v : usize = hg_video_count(&HG_LIST, n); + std.debug.print("hdmi_grab: NO CAPTURE DEVICE. macOS lists {d} video device(s), none a capture device (FaceTime, OBS Virtual, Capture screen and iPhone are skipped). The dongle is not UVC or not enumerated: replug it, check the port, and look in System Information under USB. Try {d}/{d}, again in 5 s.\n", .{ v, t + 1, tries }); + w = hg_cat(&HG_CMD, 0, "sleep 5"); + _ = hg_sh(w); + } + t += 1; + } + if (dev < 0) { + std.debug.print("hdmi_grab: gave up after {d} looks. Nothing was grabbed and nothing is claimed about the board.\n", .{tries}); + std.c.exit(2); + } + std.debug.print("hdmi_grab: capture device index {d}\n", .{dev}); + var fails : usize = 0; + var k : usize = 0; + while (k < frames) { + var w : usize = hg_cat(&HG_CMD, 0, "ffmpeg -hide_banner -loglevel error -y -f avfoundation -i \""); + w = hg_put_num(&HG_CMD, w, @as(usize, @intCast(dev))); + w = hg_cat(&HG_CMD, w, ":none\" -frames:v 5 -update 1 /tmp/hdmi_grab_"); + w = hg_put_num(&HG_CMD, w, k); + w = hg_cat(&HG_CMD, w, ".ppm"); + if (hg_sh(w) == false) { + std.debug.print("frame {d}: FAIL ffmpeg could not grab from device {d}\n", .{ k, dev }); + fails += 1; + } else { + hg_path("/tmp/hdmi_grab_", k, ".ppm"); + const n : usize = hg_slurp(&HG_BUF); + const v : usize = hg_check(&HG_BUF, n); + if (v == V_PASS) { + std.debug.print("frame {d}: PASS {d}x{d}, the T27 OS title on black\n", .{ k, hg_width(&HG_BUF, n), hg_height(&HG_BUF, n) }); + } else { + std.debug.print("frame {d}: FAIL verdict {d} (1 not a PPM, 2 short, 3 title wrong, 4 background lit) in /tmp/hdmi_grab_{d}.ppm\n", .{ k, v, k }); + fails += 1; + } + w = hg_cat(&HG_CMD, 0, "ffmpeg -hide_banner -loglevel error -y -i /tmp/hdmi_grab_"); + w = hg_put_num(&HG_CMD, w, k); + w = hg_cat(&HG_CMD, w, ".ppm /tmp/hdmi_grab_"); + w = hg_put_num(&HG_CMD, w, k); + w = hg_cat(&HG_CMD, w, ".png"); + _ = hg_sh(w); + } + k += 1; + } + std.debug.print("hdmi_grab: {d} of {d} frames pass\n", .{ frames - fails, frames }); + if (fails > 0) { std.c.exit(1); } +} diff --git a/specs/fpga/t27os_uart_shell_ax7203.t27 b/specs/fpga/t27os_uart_shell_ax7203.t27 new file mode 100644 index 0000000000..f353848a2f --- /dev/null +++ b/specs/fpga/t27os_uart_shell_ax7203.t27 @@ -0,0 +1,790 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/fpga/t27os_uart_shell_ax7203.t27 -- T27 OS over UART: the pane logic of T27 OS runs on the ALINX AX7203 +; Owner, 2026-10-19: "ideally T27 OS should run on the FPGA". T27 OS (epic #10782) is a native window with a +; sidebar and terminal panes, backed by a PTY server. Its window, its forkpty and its keychain are host-bound; +; its decisions are not. This board cell is the decisions: four agent panes whose herdr status (WORKING, +; BLOCKED, IDLE, DONE) is advanced by t27os::pane_step -- the very function the PTY server calls -- and a text +; screen that the board draws as VT100 on its UART, so any terminal, the T27 OS window's terminal pane included, +; shows the screen driven by silicon. docs/fpga/T27OS_ON_FPGA_FEASIBILITY.md is the analysis and the plan. +; - +; REUSE, NOT COPIES. pane_step, shown_state and pane_needs_you come from specs/t27os/pane_state.t27 by `use`; +; the state numbers S_* from specs/t27os/agents.t27; hex_digit from specs/fpga/board_receipt.t27. The tests feed +; the board's byte stream into specs/t27os/vt.t27, the T27 OS terminal grid, and read the grid back: the screen +; the board draws is the screen the T27 OS terminal pane would show. vt.t27 uses slices and loops, so it is a +; test-side reader only; it is not lowered onto the board (the board only writes bytes, it never parses them). +; - +; THE SCREEN. 30 columns, 8 lines, redrawn forever, home-cursor first (ESC [ H), no clear, so it does not flicker: +; T27 OS @ AX7203 t=0005 +; SPACES 1:core 2:fpga 3:hdl +; AGENTS (herdr states) +; > 1 Claude WORKING +; 2 Codex IDLE +; 3 Gemini BLOCKED ! +; 4 OpenCode DONE * +; keys 1-4 s k n r t o q x +; "t=" is the number of events the panes have taken (4 hex digits). ">" marks the selected pane, "!" a pane that +; needs you (blocked), "*" a pane whose finished turn nobody has seen (shown DONE). Every line is 32 bytes (30 +; characters, CR, LF); the first segment is the 3-byte home sequence. 259 bytes a frame, about 44 frames a second. +; - +; THE EVENTS. Two sources feed pane_step, one event a clock at most: +; 1. a built-in script, one step every tick_div clocks (0.5 s at 100 MHz), 16 steps in a loop: it spawns the four +; panes, walks a Claude-like pane through submit / stop / seen, an un-hooked pane through output / quiet, a +; pane through a permission prompt and the answer, and a pane through exit and respawn; +; 2. the UART receiver, 115200 8N1 on P20: keys 1-4 select a pane, and s k n r t o q x z send the selected pane +; the event SEEN, KEY, HOOK_NOTIFY, HOOK_SUBMIT, HOOK_STOP, OUTPUT, QUIET, EXIT_OK, SPAWNED. The key wins a +; clash with the script (one clock in 5e7; the script's event for that step is lost). +; - +; THE CLOCK. 100 MHz from a PLL on the 200 MHz oscillator, UART divider 868 (115207 baud), as rc_filter_ax7203. The +; button T6 (active low) resets every pane to STOPPED and restarts the script. LEDs (active low): B13 a pane is +; blocked, C13 a pane is working, D14 a pane shows DONE, D15 a heartbeat. +; - +; WHAT A PASS SAYS. The RTL `t27b verilog` lowers from this file, driven in simulation, puts on the UART exactly the +; bytes the spec's screen function gives, which the T27 OS vt grid reads as the screen above; and pane_step is the +; one in pane_state.t27. WHAT IT DOES NOT SAY. Nothing about the AppKit window, the PTY server, forkpty or the +; keychain: those stay on the host. Nothing about a real agent: the events are a script and the keyboard. A +; bitstream from this file has not been shown to run on the bench board unless the PR that carries it says so. +; ASCII only, English only. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module FpgaT27osUartShell; + +use fpga::board_receipt; +use t27os::agents::S_STOPPED; +use t27os::agents::S_WORKING; +use t27os::agents::S_BLOCKED; +use t27os::agents::S_IDLE; +use t27os::agents::S_DONE; +use t27os::pane_state::P_HOOK_START; +use t27os::pane_state::P_HOOK_SUBMIT; +use t27os::pane_state::P_HOOK_NOTIFY; +use t27os::pane_state::P_HOOK_STOP; +use t27os::pane_state::P_OUTPUT; +use t27os::pane_state::P_QUIET; +use t27os::pane_state::P_KEY; +use t27os::pane_state::P_SEEN; +use t27os::pane_state::P_EXIT_OK; +use t27os::pane_state::P_SPAWNED; +use t27os::pane_state::pane_step; +use t27os::pane_state::shown_state; +use t27os::pane_state::pane_needs_you; +use t27os::pane_state::ps_state; +use t27os::pane_state::ps_unseen; +use t27os::vt::vt_init; +use t27os::vt::vt_feed; +use t27os::vt::vt_cell; + +pub const KIND : str = "board-cell"; +pub const ID : str = "fpga/t27os_uart_shell_ax7203"; +pub const REFERENCE : str = "specs/t27os/pane_state.t27, specs/t27os/agents.t27, specs/t27os/vt.t27"; +pub const BOARD : str = "ALINX AX7203"; +pub const PART : str = "xc7a200tfbg484-2"; + +pub const PANES : u8 = 4; +pub const NOEV : u32 = 15; +pub const SEGS : u8 = 9; +pub const COLS : u32 = 32; +pub const FRAME_BYTES : u32 = 259; +pub const SCRIPT_LEN : u8 = 16; +pub const BAUD_DIV : u16 = 868; +pub const TICK_DIV : u32 = 50000000; + +; --- the text: ASCII packed 8 to a u64, first character in the high byte ------------------------- +pub const NAME_CLAUDE : u64 = 0x436c617564652020; +pub const NAME_CODEX : u64 = 0x436f646578202020; +pub const NAME_GEMINI : u64 = 0x47656d696e692020; +pub const NAME_OPENCODE : u64 = 0x4f70656e436f6465; +pub const STATE_STOPPED : u64 = 0x53544f5050454420; +pub const STATE_WORKING : u64 = 0x574f524b494e4720; +pub const STATE_BLOCKED : u64 = 0x424c4f434b454420; +pub const STATE_IDLE : u64 = 0x49444c4520202020; +pub const STATE_DONE : u64 = 0x444f4e4520202020; + +/// Byte i (0..7) of a packed word, first character first. +pub fn pk(w: u64, i: u8) -> u8 { + return ((w >> ((7 - (i & 7)) * 8)) & 255) as u8; +} + +/// The packed name of agent pane i. +pub fn name_of(i: u8) -> u64 { + if (i == 0) { return NAME_CLAUDE; } + if (i == 1) { return NAME_CODEX; } + if (i == 2) { return NAME_GEMINI; } + return NAME_OPENCODE; +} + +/// The packed name of a herdr state (agents.t27 S_*). +pub fn state_name(s: u32) -> u64 { + if (s == S_WORKING) { return STATE_WORKING; } + if (s == S_BLOCKED) { return STATE_BLOCKED; } + if (s == S_IDLE) { return STATE_IDLE; } + if (s == S_DONE) { return STATE_DONE; } + return STATE_STOPPED; +} + +/// The four 8-byte chunks of the static lines, 30 characters then CR LF. Segment 1 is the title (its hex digits +/// are written over t= by screen_char), 2 the spaces, 3 the agents header, 8 the key legend. +pub fn static_chunk(seg: u8, c: u8) -> u64 { + if (seg == 1) { + if (c == 0) { return 0x543237204f532040; } + if (c == 1) { return 0x2041583732303320; } + if (c == 2) { return 0x743d202020202020; } + return 0x2020202020200d0a; + } + if (seg == 2) { + if (c == 0) { return 0x5350414345532031; } + if (c == 1) { return 0x3a636f726520323a; } + if (c == 2) { return 0x6670676120333a68; } + return 0x646c202020200d0a; + } + if (seg == 3) { + if (c == 0) { return 0x4147454e54532028; } + if (c == 1) { return 0x6865726472207374; } + if (c == 2) { return 0x6174657329202020; } + return 0x2020202020200d0a; + } + if (c == 0) { return 0x6b65797320312d34; } + if (c == 1) { return 0x2073206b206e2072; } + if (c == 2) { return 0x2074206f20712078; } + return 0x2020202020200d0a; +} + +/// Character pos of the line of agent pane i whose status word is w; sel is the selected pane. +pub fn row_char(i: u8, pos: u8, sel: u8, w: u32) -> u8 { + if (pos == 0) { + if (sel == i) { return 62; } + return 32; + } + if (pos == 2) { return 49 + i; } + if (pos >= 4 and pos <= 11) { return pk(name_of(i), pos - 4); } + if (pos >= 13 and pos <= 19) { return pk(state_name(shown_state(w)), pos - 13); } + if (pos == 21) { + if (pane_needs_you(w)) { return 33; } + if (shown_state(w) == S_DONE) { return 42; } + return 32; + } + if (pos == 30) { return 13; } + if (pos == 31) { return 10; } + return 32; +} + +/// The last position of segment s: the home sequence is 3 bytes, every line 32. +pub fn seg_last(s: u8) -> u8 { + if (s == 0) { return 2; } + return 31; +} + +/// The status word of pane i out of the four. +pub fn word_of(i: u8, w0: u32, w1: u32, w2: u32, w3: u32) -> u32 { + if (i == 0) { return w0; } + if (i == 1) { return w1; } + if (i == 2) { return w2; } + return w3; +} + +/// Byte pos of segment seg of the screen; evn is the event count shown as t=. +pub fn screen_char(seg: u8, pos: u8, sel: u8, w0: u32, w1: u32, w2: u32, w3: u32, evn: u16) -> u8 { + if (seg == 0) { + if (pos == 0) { return 27; } + if (pos == 1) { return 91; } + return 72; + } + if (seg >= 4 and seg <= 7) { return row_char(seg - 4, pos, sel, word_of(seg - 4, w0, w1, w2, w3)); } + if (seg == 1 and pos >= 18 and pos <= 21) { return hex_digit((evn >> ((21 - pos) * 4)) as u8); } + if (seg == 8) { return pk(static_chunk(8, pos >> 3), pos & 7); } + return pk(static_chunk(seg, pos >> 3), pos & 7); +} + +/// The pane the script's step n sends its event to. +pub fn script_pane(n: u8) -> u8 { + if (n == 0) { return 0; } + if (n == 1) { return 1; } + if (n == 2) { return 2; } + if (n == 3) { return 3; } + if (n == 4) { return 0; } + if (n == 5) { return 1; } + if (n == 6) { return 2; } + if (n == 7) { return 2; } + if (n == 8) { return 0; } + if (n == 9) { return 1; } + if (n == 10) { return 3; } + if (n == 11) { return 3; } + if (n == 12) { return 2; } + if (n == 13) { return 0; } + if (n == 14) { return 2; } + return 3; +} + +/// The event of the script's step n. +pub fn script_ev(n: u8) -> u32 { + if (n < 4) { return P_SPAWNED; } + if (n == 4) { return P_HOOK_SUBMIT; } + if (n == 5) { return P_OUTPUT; } + if (n == 6) { return P_HOOK_SUBMIT; } + if (n == 7) { return P_HOOK_NOTIFY; } + if (n == 8) { return P_HOOK_STOP; } + if (n == 9) { return P_QUIET; } + if (n == 10) { return P_OUTPUT; } + if (n == 11) { return P_EXIT_OK; } + if (n == 12) { return P_KEY; } + if (n == 13) { return P_SEEN; } + if (n == 14) { return P_HOOK_STOP; } + return P_SPAWNED; +} + +/// The event a keyboard byte stands for; NOEV when the byte is not an event key. +pub fn key_event(b: u8) -> u32 { + if (b == 115) { return P_SEEN; } + if (b == 107) { return P_KEY; } + if (b == 110) { return P_HOOK_NOTIFY; } + if (b == 114) { return P_HOOK_SUBMIT; } + if (b == 116) { return P_HOOK_STOP; } + if (b == 111) { return P_OUTPUT; } + if (b == 113) { return P_QUIET; } + if (b == 120) { return P_EXIT_OK; } + if (b == 122) { return P_SPAWNED; } + return NOEV; +} + +/// A UART frame, sent low bit first: a start bit 0, the 8 data bits, a stop bit 1. +pub fn frame(ch: u8) -> u16 { + return ((ch as u16) << 1) | 512; +} + +; --- the board ---------------------------------------------------------------------------------- + +/// A differential input buffer (the 200 MHz oscillator on R4/T4). +pub struct IbufdsOut { O: bool } +pub fn IBUFDS(I: bool, IB: bool) -> IbufdsOut {} + +/// A global clock buffer. +pub struct BufgOut { O: bool } +pub fn BUFG(I: bool) -> BufgOut {} + +/// A PLL: 200 MHz in, VCO = 200 * 8 = 1600 MHz, CLKOUT0 = 1600 / 16 = 100 MHz; CLKFBOUT feeds CLKFBIN. +pub struct PllOut { CLKOUT0: bool, CLKFBOUT: bool, LOCKED: bool } +pub fn PLLE2_ADV(CLKIN1: bool, CLKFBIN: bool, RST: bool, PWRDWN: bool, CLKIN1_PERIOD: f64, REF_JITTER1: f64, CLKFBOUT_MULT: comptime_int, DIVCLK_DIVIDE: comptime_int, CLKOUT0_DIVIDE: comptime_int, CLKOUT0_PHASE: comptime_int, COMPENSATION: str) -> PllOut {} + +/// An input buffer, so the button and the UART line reach on_clock as instance outputs. +pub struct IbufOut { O: bool } +pub fn IBUF(I: bool) -> IbufOut {} + +; --- the pins in -------------------------------------------------------------------------------- +const clk200_p : bool = undefined; +const clk200_p_LOC : str = "R4"; +const clk200_p_IOSTANDARD : str = "DIFF_SSTL15"; +const clk200_n : bool = undefined; +const clk200_n_LOC : str = "T4"; +const clk200_n_IOSTANDARD : str = "DIFF_SSTL15"; +const cpu_reset_n : bool = undefined; +const cpu_reset_n_LOC : str = "T6"; +const cpu_reset_n_IOSTANDARD : str = "LVCMOS15"; +const uart_rx : bool = undefined; +const uart_rx_LOC : str = "P20"; +const uart_rx_IOSTANDARD : str = "LVCMOS33"; + +; --- the clock and the reset -------------------------------------------------------------------- +const clkbuf : IbufdsOut = IBUFDS(clk200_p, clk200_n); +const clkbuf_O_PERIOD_PS : u32 = 5000; +const pll : PllOut = PLLE2_ADV(clkbuf.O, pll.CLKFBOUT, false, false, 5.0, 0.01, 8, 1, 16, 0, "FALSE"); +const gclk : BufgOut = BUFG(pll.CLKOUT0); +const gclk_O_PERIOD_PS : u32 = 10000; + +const btn : IbufOut = IBUF(cpu_reset_n); +const rxb : IbufOut = IBUF(uart_rx); + +// No asynchronous reset, as rc_filter_ax7203: the button and LOCKED pass two registers, and while they say no, +// on_clock puts every register back to its start on the edge. +const clk : bool = gclk.O; +const rst_n : bool = true; +const en : bool = true; + +; --- the registers ------------------------------------------------------------------------------ +var w0 : u32 = 0; +var w1 : u32 = 0; +var w2 : u32 = 0; +var w3 : u32 = 0; +var sel : u8 = 0; +var evn : u16 = 0; +var pend_p : u8 = 0; +var pend_e : u32 = 15; +var scr : u8 = 0; +var tmr : u32 = 0; +var tick_div : u32 = 50000000; +var baud_div : u16 = 868; +var tx_cnt : u16 = 0; +var tx_sh : u16 = 1023; +var tx_left : u8 = 0; +var seg : u8 = 0; +var pos : u8 = 0; +var frames : u32 = 0; +var rx_busy : bool = false; +var rx_cnt : u16 = 0; +var rx_nb : u8 = 0; +var rx_sh : u8 = 0; +var rx_key : u8 = 0; +var rx_new : bool = false; +var rx_s0 : bool = true; +var rx_s1 : bool = true; +var hb : u32 = 0; +var go0 : bool = false; +var go1 : bool = false; + +; --- the pins out: registers, set from the state one clock later (a pub var is an output reg) ---- +pub var uart_tx : bool = true; +const uart_tx_LOC : str = "N15"; +const uart_tx_IOSTANDARD : str = "LVCMOS33"; +pub var led_0 : bool = true; +const led_0_LOC : str = "B13"; +const led_0_IOSTANDARD : str = "LVCMOS33"; +pub var led_1 : bool = true; +const led_1_LOC : str = "C13"; +const led_1_IOSTANDARD : str = "LVCMOS33"; +pub var led_2 : bool = true; +const led_2_LOC : str = "D14"; +const led_2_IOSTANDARD : str = "LVCMOS33"; +pub var led_3 : bool = false; +const led_3_LOC : str = "D15"; +const led_3_IOSTANDARD : str = "LVCMOS33"; + +// Every var is read before it is written, so the test's order and the clock edge agree: the transmitter reads the +// panes first, then the pending event is applied, then the script and the keyboard queue the next one. +fn on_clock(pll_LOCKED: bool, btn_O: bool, rxb_O: bool) { + uart_tx = (tx_sh & 1) == 1; + led_0 = !(pane_needs_you(w0) or pane_needs_you(w1) or pane_needs_you(w2) or pane_needs_you(w3)); + led_1 = !(ps_state(w0) == S_WORKING or ps_state(w1) == S_WORKING or ps_state(w2) == S_WORKING or ps_state(w3) == S_WORKING); + led_2 = !(shown_state(w0) == S_DONE or shown_state(w1) == S_DONE or shown_state(w2) == S_DONE or shown_state(w3) == S_DONE); + led_3 = ((hb >> 27) & 1) == 1; + hb = hb + 1; + if (!go1) { + w0 = 0; + w1 = 0; + w2 = 0; + w3 = 0; + sel = 0; + evn = 0; + pend_p = 0; + pend_e = NOEV; + scr = 0; + tmr = 0; + tx_cnt = 0; + tx_sh = 1023; + tx_left = 0; + seg = 0; + pos = 0; + frames = 0; + rx_busy = false; + rx_cnt = 0; + rx_nb = 0; + rx_sh = 0; + rx_key = 0; + rx_new = false; + } else { + // the transmitter: one byte every ten bit times, the screen over and over + if (tx_cnt != 0) { + tx_cnt = tx_cnt - 1; + } else { + tx_cnt = baud_div - 1; + if (tx_left != 0) { + tx_sh = (tx_sh >> 1) | 512; + tx_left = tx_left - 1; + } else { + tx_sh = frame(screen_char(seg, pos, sel, w0, w1, w2, w3, evn)); + tx_left = 9; + if (pos == seg_last(seg)) { + pos = 0; + if (seg == SEGS - 1) { + seg = 0; + frames = frames + 1; + } else { + seg = seg + 1; + } + } else { + pos = pos + 1; + } + } + } + // the queued event reaches its pane + if (pend_e != NOEV) { + if (pend_p == 0) { + w0 = pane_step(w0, pend_e); + } else if (pend_p == 1) { + w1 = pane_step(w1, pend_e); + } else if (pend_p == 2) { + w2 = pane_step(w2, pend_e); + } else { + w3 = pane_step(w3, pend_e); + } + evn = evn + 1; + pend_e = NOEV; + } + // the script queues its next event + if (tmr + 1 >= tick_div) { + tmr = 0; + pend_p = script_pane(scr); + pend_e = script_ev(scr); + if (scr == SCRIPT_LEN - 1) { + scr = 0; + } else { + scr = scr + 1; + } + } else { + tmr = tmr + 1; + } + // a received key selects a pane or queues an event for the selected one + if (rx_new) { + rx_new = false; + if (rx_key >= 49 and rx_key <= 52) { + sel = rx_key - 49; + } else if (key_event(rx_key) != NOEV) { + pend_p = sel; + pend_e = key_event(rx_key); + } + } + // the receiver: 8N1, sampled in the middle of each bit + if (rx_busy) { + if (rx_cnt != 0) { + rx_cnt = rx_cnt - 1; + } else { + rx_cnt = baud_div - 1; + if (rx_nb == 8) { + rx_busy = false; + rx_key = rx_sh; + rx_new = true; + } else { + if (rx_s1) { + rx_sh = (rx_sh >> 1) | 128; + } else { + rx_sh = rx_sh >> 1; + } + rx_nb = rx_nb + 1; + } + } + } else if (!rx_s1) { + rx_busy = true; + rx_cnt = baud_div + (baud_div >> 1) - 1; + rx_nb = 0; + rx_sh = 0; + } + } + rx_s1 = rx_s0; + rx_s0 = rxb_O; + go1 = go0; + go0 = pll_LOCKED and btn_O; +} + +; --- the tests ---------------------------------------------------------------------------------- + +test the_packed_words_spell_the_names_and_states { + assert_eq(pk(NAME_CLAUDE, 0), 67); + assert_eq(pk(NAME_CLAUDE, 5), 101); + assert_eq(pk(NAME_CODEX, 4), 120); + assert_eq(pk(NAME_OPENCODE, 7), 101); + assert_eq(pk(STATE_WORKING, 0), 87); + assert_eq(pk(STATE_BLOCKED, 6), 68); + assert_eq(pk(STATE_IDLE, 3), 69); + assert_eq(pk(STATE_IDLE, 4), 32); + assert_eq(pk(STATE_DONE, 0), 68); + assert_eq(pk(STATE_STOPPED, 0), 83); + assert_eq(state_name(S_STOPPED), STATE_STOPPED); + assert_eq(state_name(S_WORKING), STATE_WORKING); + assert_eq(state_name(S_BLOCKED), STATE_BLOCKED); + assert_eq(state_name(S_IDLE), STATE_IDLE); + assert_eq(state_name(S_DONE), STATE_DONE); +} + +test the_script_walks_the_panes_through_the_herdr_states { + var a : u32 = 0; + var b : u32 = 0; + var c : u32 = 0; + var d : u32 = 0; + var n : u8 = 0; + var mid : bool = false; + while (n < SCRIPT_LEN) { + const p : u8 = script_pane(n); + const e : u32 = script_ev(n); + if (p == 0) { a = pane_step(a, e); } + if (p == 1) { b = pane_step(b, e); } + if (p == 2) { c = pane_step(c, e); } + if (p == 3) { d = pane_step(d, e); } + if (n == 7) { + // after the permission prompt: pane 2 needs you, pane 0 works, pane 1 works un-hooked + assert pane_needs_you(c); + assert_eq(shown_state(a), S_WORKING); + assert_eq(shown_state(b), S_WORKING); + mid = true; + } + if (n == 8) { assert_eq(shown_state(a), S_DONE); } + n = n + 1; + } + assert mid; + // the end of the loop: pane 0 saw its finished turn, pane 1 went quiet, pane 2 finished unseen, pane 3 respawned + assert_eq(shown_state(a), S_IDLE); + assert_eq(shown_state(b), S_IDLE); + assert_eq(shown_state(c), S_DONE); + assert ps_unseen(c); + assert_eq(shown_state(d), S_IDLE); +} + +test the_keys_map_to_events { + assert_eq(key_event(115), P_SEEN); + assert_eq(key_event(107), P_KEY); + assert_eq(key_event(110), P_HOOK_NOTIFY); + assert_eq(key_event(114), P_HOOK_SUBMIT); + assert_eq(key_event(116), P_HOOK_STOP); + assert_eq(key_event(111), P_OUTPUT); + assert_eq(key_event(113), P_QUIET); + assert_eq(key_event(120), P_EXIT_OK); + assert_eq(key_event(122), P_SPAWNED); + assert_eq(key_event(65), NOEV); + assert_eq(key_event(49), NOEV); +} + +test the_screen_is_the_t27os_vt_grid_as_the_layout_says { + var st : [10]u32 = undefined; + var cells : [288]u32 = undefined; + assert vt_init(st[0..10], cells[0..288], 32, 9); + const a : u32 = pane_step(pane_step(pane_step(0, P_SPAWNED), P_HOOK_SUBMIT), P_HOOK_NOTIFY); + const b : u32 = pane_step(0, P_SPAWNED); + const c : u32 = pane_step(pane_step(pane_step(0, P_SPAWNED), P_HOOK_SUBMIT), P_HOOK_STOP); + const d : u32 = pane_step(pane_step(0, P_SPAWNED), P_OUTPUT); + var s : u8 = 0; + var p : u8 = 0; + var n : u32 = 0; + while (s < SEGS) { + p = 0; + while (p <= seg_last(s)) { + assert vt_feed(st[0..10], cells[0..288], screen_char(s, p, 2, a, b, c, d, 0x00A5)); + n = n + 1; + p = p + 1; + } + s = s + 1; + } + assert_eq(n, FRAME_BYTES); + // line 0, the title + assert_eq(vt_cell(st[0..10], cells[0..288], 0, 0), 84); + assert_eq(vt_cell(st[0..10], cells[0..288], 7, 0), 64); + assert_eq(vt_cell(st[0..10], cells[0..288], 16, 0), 116); + assert_eq(vt_cell(st[0..10], cells[0..288], 17, 0), 61); + assert_eq(vt_cell(st[0..10], cells[0..288], 18, 0), 48); + assert_eq(vt_cell(st[0..10], cells[0..288], 19, 0), 48); + assert_eq(vt_cell(st[0..10], cells[0..288], 20, 0), 65); + assert_eq(vt_cell(st[0..10], cells[0..288], 21, 0), 53); + // line 1 and 2, the spaces and the header + assert_eq(vt_cell(st[0..10], cells[0..288], 0, 1), 83); + assert_eq(vt_cell(st[0..10], cells[0..288], 7, 1), 49); + assert_eq(vt_cell(st[0..10], cells[0..288], 0, 2), 65); + // line 3, pane 0 BLOCKED, not selected: " 1 Claude BLOCKED !" + assert_eq(vt_cell(st[0..10], cells[0..288], 0, 3), 32); + assert_eq(vt_cell(st[0..10], cells[0..288], 2, 3), 49); + assert_eq(vt_cell(st[0..10], cells[0..288], 4, 3), 67); + assert_eq(vt_cell(st[0..10], cells[0..288], 13, 3), 66); + assert_eq(vt_cell(st[0..10], cells[0..288], 19, 3), 68); + assert_eq(vt_cell(st[0..10], cells[0..288], 21, 3), 33); + // line 4, pane 1 IDLE + assert_eq(vt_cell(st[0..10], cells[0..288], 2, 4), 50); + assert_eq(vt_cell(st[0..10], cells[0..288], 13, 4), 73); + assert_eq(vt_cell(st[0..10], cells[0..288], 21, 4), 32); + // line 5, pane 2 is the selected one and finished unseen: "> 3 Gemini DONE *" + assert_eq(vt_cell(st[0..10], cells[0..288], 0, 5), 62); + assert_eq(vt_cell(st[0..10], cells[0..288], 4, 5), 71); + assert_eq(vt_cell(st[0..10], cells[0..288], 13, 5), 68); + assert_eq(vt_cell(st[0..10], cells[0..288], 21, 5), 42); + // line 6, pane 3 WORKING + assert_eq(vt_cell(st[0..10], cells[0..288], 4, 6), 79); + assert_eq(vt_cell(st[0..10], cells[0..288], 13, 6), 87); + // line 7, the key legend, and the grid below it stays blank + assert_eq(vt_cell(st[0..10], cells[0..288], 0, 7), 107); + assert_eq(vt_cell(st[0..10], cells[0..288], 0, 8), 32); + // the cursor ends at the start of line 8: nothing scrolled + assert_eq(st[0], 0); + assert_eq(st[1], 8); +} + +test the_pins_carry_exactly_the_screen_bytes { + // the board machine, in simulation: baud divider 4, decode the uart_tx pin like a terminal does, and compare + // every byte to the screen function while the panes take their events from the script + w0 = 0; + w1 = 0; + w2 = 0; + w3 = 0; + sel = 0; + evn = 0; + pend_e = NOEV; + scr = 0; + tmr = 0; + tick_div = 1000000; + baud_div = 4; + tx_cnt = 0; + tx_sh = 1023; + tx_left = 0; + seg = 0; + pos = 0; + frames = 0; + rx_busy = false; + rx_new = false; + rx_s0 = true; + rx_s1 = true; + go0 = true; + go1 = true; + var got : u32 = 0; + var s : u8 = 0; + var p : u8 = 0; + var clocks : u32 = 0; + var cnt : u32 = 0; + var bits : u8 = 0; + var acc : u8 = 0; + var prev : bool = true; + // advance the machine until the transmitter is idle-high once, then watch for start bits + while (got < FRAME_BYTES) { + on_clock(true, true, true); + clocks = clocks + 1; + if (cnt == 0 and prev and !uart_tx) { + // a start bit: sample 1.5 bit times later, then every bit time + cnt = 6; + bits = 0; + acc = 0; + } else if (cnt != 0) { + cnt = cnt - 1; + if (cnt == 0) { + if (bits < 8) { + if (uart_tx) { acc = (acc >> 1) | 128; } else { acc = acc >> 1; } + bits = bits + 1; + cnt = 4; + } else { + assert uart_tx; + assert_eq(acc, screen_char(s, p, 0, 0, 0, 0, 0, 0)); + got = got + 1; + if (p == seg_last(s)) { + p = 0; + s = s + 1; + } else { + p = p + 1; + } + } + } + } + prev = uart_tx; + assert clocks < 100000; + } + assert_eq(s, SEGS); + // nothing moved in that time: no script step, no key + assert_eq(w0, 0); + assert_eq(evn, 0); +} + +test the_script_clock_applies_one_event_per_tick { + w0 = 0; + w1 = 0; + w2 = 0; + w3 = 0; + evn = 0; + pend_e = NOEV; + scr = 0; + tmr = 0; + tick_div = 5; + baud_div = 868; + tx_cnt = 0; + tx_sh = 1023; + tx_left = 0; + seg = 0; + pos = 0; + rx_busy = false; + rx_new = false; + rx_s0 = true; + rx_s1 = true; + go0 = true; + go1 = true; + var i : u32 = 0; + while (i < 5 * 8 + 2) { + on_clock(true, true, true); + i = i + 1; + } + // 8 script steps (all four spawns, then submit, output, submit, notify) have been queued and applied + assert_eq(evn, 8); + assert_eq(shown_state(w0), S_WORKING); + assert_eq(shown_state(w1), S_WORKING); + assert_eq(shown_state(w2), S_BLOCKED); + assert_eq(shown_state(w3), S_IDLE); + assert led_0 == false; + assert led_1 == false; + assert led_2 == true; +} + +test a_key_over_the_uart_selects_and_sends { + w0 = 0; + w1 = 0; + w2 = 0; + w3 = 0; + sel = 0; + evn = 0; + pend_e = NOEV; + scr = 0; + tmr = 0; + tick_div = 100000000; + baud_div = 16; + tx_cnt = 0; + tx_sh = 1023; + tx_left = 0; + rx_busy = false; + rx_new = false; + rx_s0 = true; + rx_s1 = true; + go0 = true; + go1 = true; + var k : u8 = 0; + var bit : u8 = 0; + var t : u8 = 0; + var line : bool = true; + // type "3" then "z" (select pane 2, spawn it) then "r" (hook submit): 8N1, 16 clocks a bit + while (k < 3) { + const ch : u8 = kbd_byte(k); + bit = 0; + while (bit < 10) { + if (bit == 0) { line = false; } + else if (bit == 9) { line = true; } + else { line = ((ch >> (bit - 1)) & 1) == 1; } + t = 0; + while (t < 16) { + on_clock(true, true, line); + t = t + 1; + } + bit = bit + 1; + } + t = 0; + while (t < 100) { + on_clock(true, true, true); + t = t + 1; + } + k = k + 1; + } + assert_eq(sel, 2); + assert_eq(shown_state(w2), S_WORKING); + assert_eq(shown_state(w0), S_STOPPED); + assert_eq(evn, 2); +} + +/// The test keyboard: "3", "z", "r". +pub fn kbd_byte(k: u8) -> u8 { + if (k == 0) { return 51; } + if (k == 1) { return 122; } + return 114; +} + +test the_button_resets_every_pane_and_the_script { + w0 = 777; + w1 = 5; + sel = 3; + evn = 9; + scr = 6; + go0 = false; + go1 = false; + on_clock(true, true, true); + assert_eq(w0, 0); + assert_eq(w1, 0); + assert_eq(sel, 0); + assert_eq(evn, 0); + assert_eq(scr, 0); + assert_eq(pend_e, NOEV); + assert_eq(seg, 0); + assert_eq(tx_sh, 1023); +} diff --git a/specs/fpga/vga_text_core.t27 b/specs/fpga/vga_text_core.t27 new file mode 100644 index 0000000000..f8e8e061cc --- /dev/null +++ b/specs/fpga/vga_text_core.t27 @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/fpga/vga_text_core.t27 -- 640x480@60 VGA timing and an 8x8 text-mode scanout, with no board pins +; Owner, 2026-10-19: a VGA monitor is available for the AX7203, to show the T27 OS screen. HOW VGA REACHES THE +; BOARD IS NOT ESTABLISHED in this repository (fpga/HARDWARE_SSOT.md and every .xdc here carry no VGA, HDMI or +; DVI pin), so this cell has no pins and no top: it is the part that does not depend on the connector. A board +; top will add the pin attributes once the owner confirms the connector and its schematic pin table. +; - +; THE TIMING (pixel clock 25.175 MHz nominal, 25 MHz is close enough; one on_clock = one pixel): +; horizontal: 640 visible, 16 front porch, 96 sync (low), 48 back porch = 800 +; vertical: 480 visible, 10 front porch, 2 sync (low), 33 back porch = 525 +; THE TEXT: 8x8 glyphs, 80 columns by 60 rows. The text comes from cell_char(col, row); the glyphs from +; glyph_bits(ch), eight row bytes packed into a u64, first row first, bit 7 the leftmost pixel. Only the glyphs +; of the demo line are drawn; the font grows glyph by glyph and everything else is blank. +; - +; WHAT A PASS SAYS: the tests step on_clock for whole frames and count: 96 low hsync ticks a line, 800 ticks a +; line, 2 low vsync lines, 525 lines, 307200 visible pixels, and the first glyph row of "T" on the stream. +; WHAT IT DOES NOT SAY: nothing here has met a monitor. Sync polarity, pixel clock accuracy and the color wiring +; are only proved by the screen. +module FpgaVgaTextCore; + +pub const H_VISIBLE : u32 = 640; +pub const H_SYNC_START : u32 = 656; +pub const H_SYNC_END : u32 = 752; +pub const H_TOTAL : u32 = 800; +pub const V_VISIBLE : u32 = 480; +pub const V_SYNC_START : u32 = 490; +pub const V_SYNC_END : u32 = 492; +pub const V_TOTAL : u32 = 525; + +// "T27 OS " packed 8 to a word, first character first. +pub const TITLE : u64 = 0x543237204f532020; + +pub const G_T : u64 = 0x7e18181818181800; +pub const G_2 : u64 = 0x3c66060c18307e00; +pub const G_7 : u64 = 0x7e060c1818181800; +pub const G_O : u64 = 0x3c66666666663c00; +pub const G_S : u64 = 0x3c6630180c663c00; + +/// Byte i (0..7) of a packed word, first byte first. +pub fn pk(w: u64, i: u8) -> u8 { + return ((w >> ((7 - (i & 7)) * 8)) & 255) as u8; +} + +/// The glyph of ch: eight row bytes, first row first. Blank for every character not drawn yet. +pub fn glyph_bits(ch: u8) -> u64 { + if (ch == 84) { return G_T; } + if (ch == 50) { return G_2; } + if (ch == 55) { return G_7; } + if (ch == 79) { return G_O; } + if (ch == 83) { return G_S; } + return 0; +} + +/// The character in text cell (col, row): the demo title on row 0, blanks elsewhere. +pub fn cell_char(col: u32, row: u32) -> u8 { + if (row == 0) { + if (col < 8) { return pk(TITLE, col as u8); } + } + return 32; +} + +/// The pixel at (x, y) of the visible area: true is white. +pub fn pixel_at(x: u32, y: u32) -> bool { + var ch : u8 = cell_char(x >> 3, y >> 3); + var bits : u64 = glyph_bits(ch); + var rowb : u8 = pk(bits, (y & 7) as u8); + var sh : u32 = 7 - (x & 7); + return ((rowb >> sh) & 1) == 1; +} + +pub var hc : u32 = 0; +pub var vc : u32 = 0; +pub var hsync : bool = true; +pub var vsync : bool = true; +pub var de : bool = false; +pub var pix : bool = false; + +// One pixel clock. The outputs are registered: after the tick for (hc, vc) they describe that pixel, and the +// counters already point at the next one. +fn on_clock() { + var h : u32 = hc; + var v : u32 = vc; + var vis : bool = (h < H_VISIBLE) and (v < V_VISIBLE); + de = vis; + hsync = !((h >= H_SYNC_START) and (h < H_SYNC_END)); + vsync = !((v >= V_SYNC_START) and (v < V_SYNC_END)); + pix = vis and pixel_at(h, v); + if (h == H_TOTAL - 1) { + hc = 0; + if (v == V_TOTAL - 1) { + vc = 0; + } else { + vc = v + 1; + } + } else { + hc = h + 1; + } +} + +test the_font_draws_a_T_with_a_bar_and_a_stem { + // row 0 of T is 0x7e: pixels 1..6 on, 0 and 7 off; row 3 is the stem 0x18: pixels 3 and 4 + assert pixel_at(0, 0) == false; + assert pixel_at(1, 0) == true; + assert pixel_at(6, 0) == true; + assert pixel_at(7, 0) == false; + assert pixel_at(2, 3) == false; + assert pixel_at(3, 3) == true; + assert pixel_at(4, 3) == true; + assert pixel_at(5, 3) == false; + // the last glyph row (7) is blank, and so is every cell after the title + assert pixel_at(3, 7) == false; + assert pixel_at(70, 0) == false; + assert pixel_at(3, 8) == false; +} + +test the_title_is_t_2_7_blank_o_s { + assert cell_char(0, 0) == 84; + assert cell_char(1, 0) == 50; + assert cell_char(2, 0) == 55; + assert cell_char(3, 0) == 32; + assert cell_char(4, 0) == 79; + assert cell_char(5, 0) == 83; + assert cell_char(0, 1) == 32; +} + +test the_line_is_800_pixels_with_a_96_pixel_low_hsync_and_640_visible { + hc = 0; + vc = 0; + var lo : u32 = 0; + var vis : u32 = 0; + var i : u32 = 0; + while (i < H_TOTAL) { + on_clock(); + if (hsync == false) { lo = lo + 1; } + if (de) { vis = vis + 1; } + i = i + 1; + } + assert_eq(lo, 96); + assert_eq(vis, 640); + // the counter wrapped to the start of line 1 + assert_eq(hc, 0); + assert_eq(vc, 1); +} + +test the_frame_is_525_lines_with_2_low_vsync_lines_and_307200_visible_pixels { + hc = 0; + vc = 0; + var lo : u32 = 0; + var vis : u32 = 0; + var i : u32 = 0; + while (i < H_TOTAL * V_TOTAL) { + on_clock(); + if (vsync == false) { lo = lo + 1; } + if (de) { vis = vis + 1; } + i = i + 1; + } + assert_eq(lo, 2 * H_TOTAL); + assert_eq(vis, 307200); + assert_eq(hc, 0); + assert_eq(vc, 0); +} + +test the_stream_of_the_first_glyph_row_is_the_T_bar { + hc = 0; + vc = 0; + // pixels 0..7 of line 0: 0 1 1 1 1 1 1 0 + on_clock(); + assert pix == false; + on_clock(); + assert pix == true; + on_clock(); + assert pix == true; + on_clock(); + assert pix == true; + on_clock(); + assert pix == true; + on_clock(); + assert pix == true; + on_clock(); + assert pix == true; + on_clock(); + assert pix == false; +} + +test the_stream_equals_pixel_at_on_the_first_eight_lines { + hc = 0; + vc = 0; + var bad : u32 = 0; + var n : u32 = 0; + while (n < 8 * H_TOTAL) { + var x : u32 = hc; + var y : u32 = vc; + on_clock(); + var want : bool = (x < H_VISIBLE) and pixel_at(x, y); + if (pix != want) { bad = bad + 1; } + n = n + 1; + } + assert_eq(bad, 0); +} + +test the_blanking_is_black { + hc = H_VISIBLE; + vc = 0; + on_clock(); + assert de == false; + assert pix == false; + hc = 0; + vc = V_VISIBLE; + on_clock(); + assert de == false; + assert pix == false; +}