From d4a3ccc42c4d6c32523f26c0176c805b22d9136d Mon Sep 17 00:00:00 2001 From: Arti Zirk Date: Fri, 16 Oct 2026 17:25:04 +0300 Subject: [PATCH] harness: model the battery-backed NVR at bit 0x6000000 The terminal's settings NVR was not modelled: 0x6000000 fell inside the video-memory mapping (0x04000000..0x1fffffff), so NVR reads returned stale framebuffer words and writes were lost. That is the owner's 'User settings invalid' plus 'settings do not persist' (and the font path is stored here). Map a persisted NVRAM over the 0x800-word settings block; MAME now saves and reloads it (nvram/vt1200/nvram), verified write->restart->read. No MAME-tree change. --- harness/mame/NVRAM_REPORT.md | 139 +++++++++++++++++++++++++++++++++++ harness/mame/README.md | 5 ++ harness/mame/vt1200.cpp | 26 +++++++ 3 files changed, 170 insertions(+) create mode 100644 harness/mame/NVRAM_REPORT.md diff --git a/harness/mame/NVRAM_REPORT.md b/harness/mame/NVRAM_REPORT.md new file mode 100644 index 0000000..5772ef3 --- /dev/null +++ b/harness/mame/NVRAM_REPORT.md @@ -0,0 +1,139 @@ +# The NVR (non-volatile settings RAM) — model gap and fix + +Date: 2026-10-16. Firmware `vt1200_v22.bin` md5 +`b46a3e442ccb86134d2356bd70d22fd2` (**unmodified**). **No MAME-tree change.** + +## Verdict (short) + +* The terminal's NVRAM/settings live at **bit `0x6000000` (physical byte + `0xC00000`)**: a 16-bit-wide region whose user-settings block is **0x800 bytes, + one byte per 16-bit word** (i.e. 0x800 words). +* Our model **did not model it at all.** That address lies *inside* the driver's + video-memory mapping (`0x04000000`..`0x1fffffff`), so every NVR read returned + whatever unrelated VRAM happened to hold and every NVR write went into the + volatile VRAM array and was lost on reset/exit. This is the same "write it + somewhere that throws the value away" pattern that produced the phantom DUART + interrupt and the phantom level-0 pass. +* **Fix:** a persisted `NVRAM` device mapped over the video-memory entry + (`map(0x6000000, 0x6007fff).ram().share("nvram")` + + `NVRAM(config, "nvram", nvram_device::DEFAULT_ALL_1)`). It is saved to / + loaded from MAME's nvram directory (`~/.mame/nvram/vt1200/nvram`, 4096 bytes), + so it survives a full **restart**, not just a reset — mechanically verified + below. +* The **font path is a setting in this NVR** (service guide, quoted below), so + with the region modelled it has somewhere to live. The font *data* is **not** + here. + +## What the service guide says + +`docs/EK-V1000-SV-002_service-guide.txt`: + +> Nonvolatile RAM (NVR) on the system logic board stores the user's customized +> settings, including group codes, host name or IP address, font paths, and the +> terminal IP address. + +> The terminal stores feature settings in nonvolatile RAM (NVR) memory. NVR +> retains the settings when the terminal is turned off, so the terminal can +> restore the settings when power is turned on again. + +(There is also a level-0/level-1 "NVR Checksum Test", error 14.) + +## The firmware's side (address, size, format) + +Two routines, both already named in `reference/memory-space-map.md` row 7: + +* **`nvr_user_settings_validate`** (bit `0x3F4ABE50`, byte `0x07E957CA`): + * `MOVI 0x800 / MOVI 0x1a0020 / MOVI 0x6000000 / CALLA io_settings_copy_expand` + — copies 0x800 bytes from **`0x6000000`** into RAM `0x1a0020`; + * sums the bytes, compares the sum against a stored value, then checks a + `0x55`/`0xAA` magic; on a mismatch it prints `User settings invalid.` + (bit `0x3F4B5FC0`) or `Old user settings layout.` (bit `0x3F4B5EF0`). +* **the save routine** (bit `0x3F4AC880`, byte `0x07E95910`): runs the validator, + computes a checksum over RAM `0x1a0020`, then + `MOVI 0x800 / MOVI 0x1a0020 / MOVI 0x6000000 / CALLA io_settings_copy_linear` + — copies 0x800 bytes RAM → **`0x6000000`**, one settings **byte per 16-bit + word** (`io_settings_copy_linear`: `MOVB *A7,A12` then `SETF 0x10` and + `MOVE A12,*A0+`, so the word stride is 16 bits). + +So `0x800` words is the right size for the settings block; larger reads/writes +were never observed (the only absolute operands pointing here are the two above). + +## The bug in the harness + +`program_map` maps video memory over the whole `0x04000000`..`0x1fffffff` range, +and `0x06000000` is inside it. There was no entry for it, and the board windows +(`0x05800000`, `0x05900000`, `0x06200000`, `0x06400000`) and the MAC ROM +(`0x06800000`) do not cover it. So all NVR traffic fell through to the `vram` +share: reads returned stale framebuffer words (which fail the checksum), writes +were accepted but evaporated. Nothing wrote a `.nv` file and nothing was +reloaded, which is exactly the owner's "User settings invalid" + "settings do +not persist". + +## The fix + +```c++ +// program_map(), registered AFTER the video-memory mapping so it wins: +map(0x6000000, 0x6007fff).ram().share("nvram"); + +// machine_config: +NVRAM(config, "nvram", nvram_device::DEFAULT_ALL_1); +``` + +`nvram_device` holds the same-named share and writes it to +`/vt1200/nvram` on exit. + +## Does it persist? — mechanically, yes (verified) + +A write through the *CPU* to NVR word `0x700`, then a full MAME restart: + +``` +# process 1 (write): +prog:write_u16(0x6007000, 0xBEEF) -> file ~/.mame/nvram/vt1200/nvram @0xE00 = ef be +# process 2 (fresh run): +prog:read_u16(0x6007000) -> 0xBEEF +``` + +So the region is saved at exit and reloaded at start — settings would survive a +**restart**, not merely a soft reset. (Baselines unaffected; see below.) + +## What I could *not* demonstrate, stated plainly + +The OS itself writes the NVR during cold-start: in a `VT1200_SKIP_LEVEL1=1` boot +the whole block goes `FF…FF` (the power-on default) → `00…00` at frame 144. On a +blank NVR that is a *zeroed* (still-invalid) block, which is why the Terminal +Manager still prints `User settings invalid.` / `System defaults loaded.` — that +is the correct first-boot behaviour, not a bug. I could **not** drive the +Customize → *Save Current Settings* flow from a headless script, so the +firmware's own save → restart → load round trip is **not** demonstrated here. +The owner can settle it in one step: set the font path, **Save Current +Settings**, quit, restart, see whether it comes back. If it does not, the next +suspect is the save path's own initialisation of the block (the frame-144 write), +not the region's existence. + +## The font cache is not in the NVR + +* The font **path** is a setting here (see the service guide quote). The font + **data** is not: 2 KiB cannot hold it, and the firmware never touches + `0x6000000` beyond the 0x800-word settings block. +* There is no disk/FS device anywhere in the address map + (`reference/memory-space-map.md`): the terminal's storage is main RAM + VRAM + + this NVR. A downloaded font would live in main RAM. +* The string `Disk full or allocation exceeded` is used in **exactly one place**: + the message for **TFTP error code 3** in the terminal's `tftp_read` ERROR-packet + handler (bit `~0x3F5EDB30`; the error-message table is at file `0x1A7028`). + So on that path the terminal **prints** TFTP error 3 rather than generating it + from a local storage API. Whether the error 3 arrives from the host's TFTP + server or from the terminal's own server needs a packet capture; the code does + not settle it. (Note `harness/serve.py` is being modified by another worker — + that may be the relevant server.) + +## Baselines (unchanged by this fix) + +* `progress.lua` 200 frames, no env: `samples=198 (3 distinct PCs)`. +* `VT1200_IO_PATH_STUB=1`: `samples=53 (75 distinct PCs)`. +* `VT1200_SKIP_LEVEL1=1`, frame-3000 dump reproduces + `duart2_default_tm_frame3000.png` (AE = 1826 — the cursor only). + +The `.nv` file is written under the default `nvram` directory +(`~/.mame/nvram/vt1200/nvram`); nothing of the owner's existed there before this +change, and no existing `.nv` was deleted. diff --git a/harness/mame/README.md b/harness/mame/README.md index 3363aee..4eee4fe 100644 --- a/harness/mame/README.md +++ b/harness/mame/README.md @@ -69,6 +69,7 @@ Reports in this directory: | `TABLET_REPORT.md` | the VSXXX-AB tablet: the self-test device-code ID, the 5-byte absolute report, and absolute host-pointer input | | `LANCE_REPORT.md` | the LANCE Ethernet model: the register file, the 8-byte address ROM and its checksum, the byte↔bit DMA, and the remaining loopback/CRC blocker | | `LANCE_LOOPBACK_REPORT.md` | `C — LANCE TEST`: the TX buffer **does** carry a correct FCS in the passing `DTCR=1` case and deliberately carries **none** in the failing one; the residual mismatch is MAME never setting `RMD1_FRAM` | +| `NVRAM_REPORT.md` | the battery-backed NVR (bit `0x6000000`): previously unmodelled (its address was swallowed by the video-memory mapping), now a persisted `nvram_device`; the font path lives there, the font data does not | | `RENDER_REPORT.md` | the active-pixel scanline renderer, and the geometry it derived | | `DUART_REPORT.md` | the DUARTs: wiring, the ISR-driven transmit, the datasheet-verified model edits | | `BRINGUP_LOG.md` | the whole bring-up narrative, **including withdrawn claims** — read it sceptically | @@ -131,6 +132,10 @@ to be a six-variable incantation; it is now the default, and a bare modelled as the eight bytes the firmware actually reads: `08 00 2b 16 4b 5a c1 86` (MAC + a checksum it validates). See `LANCE_REPORT.md`. +* The **NVR** (battery-backed settings RAM) at **bit `0x6000000`** — a persisted + `nvram_device` over the 0x800-word user-settings block, saved to MAME's nvram + directory. Before this it was unmodelled and its address was swallowed by the + video-memory mapping, so settings never persisted. See `NVRAM_REPORT.md`. A bare run gets through POST and then **fails level 0's I/O data path test**, so it lands in the interactive diagnostics menu (`VERSION 2.08 - SELECTED TEST TYPE:`) diff --git a/harness/mame/vt1200.cpp b/harness/mame/vt1200.cpp index 41b25d1..617a98c 100644 --- a/harness/mame/vt1200.cpp +++ b/harness/mame/vt1200.cpp @@ -195,6 +195,7 @@ #include "emupal.h" #include "machine/am79c90.h" #include "machine/mc68681.h" +#include "machine/nvram.h" #include "screen.h" #include @@ -647,6 +648,7 @@ public: , m_mouse(*this, "mouse") , m_tablet(*this, "tablet") , m_lance(*this, "lance") + , m_nvram(*this, "nvram") , m_vram(*this, "vram") { } @@ -740,6 +742,12 @@ private: // main-RAM word B>>1. See the note there. required_device m_lance; + // Battery-backed NVRAM ("NVR" in the service guide): the OS keeps its + // user settings / font path / IP address here. A plain 16-bit RAM at bit + // 0x6000000 (byte 0xC00000), persisted by MAME's nvram_device through the + // shared region named "nvram". See NVRAM_REPORT.md. + required_device m_nvram; + // Ethernet address ROM: bit 0x6800000 (physical byte 0xD00000). The // firmware reads EIGHT bytes, one per 16-bit step: // movi 0x6800000,a11 / ... / movb *a7(0x0),*a11(0x1b0) / addk 16,a7 @@ -1123,6 +1131,18 @@ void vt1200_state::program_map(address_map &map) // (addk 16,a7), read with movb (low byte of the word -- see lance_rom_r): // six MAC bytes then a two-byte checksum, so eight 16-bit words. map(0x6800000, 0x6800000 + 0x007f).r(FUNC(vt1200_state::lance_rom_r)); + + // Battery-backed NVRAM at bit 0x6000000 (byte 0xC00000). The service guide + // calls it NVR ("stores the user's customized settings, including group + // codes, host name or IP address, font paths, and the terminal IP + // address"). The firmware's validate/save routines (bit 0x3F4ABE50 and + // 0x3F4AC880) copy a 0x800-byte settings block between RAM 0x1a0020 and + // this device, one settings byte per 16-bit word, so 0x800 words covers + // it. Registered AFTER the video-memory mapping -- which spans + // 0x04000000..0x1fffffff and would otherwise swallow this address -- so + // the NVRAM wins. MAME's nvram_device persists it via the "nvram" share + // (see NVRAM_REPORT.md). + map(0x6000000, 0x6007fff).ram().share("nvram"); } // MAME may hand us either an entry-relative offset (0..0xffff) or the absolute // address; BOARD_BASE is 0x10000-aligned, so ORing the low 16 bits is correct @@ -1697,6 +1717,12 @@ void vt1200_state::vt1200(machine_config &config) // whatever MAME's default palette held (reported: red). Two entries suffice. PALETTE(config, m_palette, palette_device::MONOCHROME).set_entries(2); + // Battery-backed NVRAM. DEFAULT_ALL_1 models an erased/blank NVR; the OS + // then reports "User settings invalid" once and writes its defaults, and + // from then on the region is saved to (and reloaded from) MAME's nvram + // directory -- i.e. settings survive a restart, not just a reset. + NVRAM(config, "nvram", nvram_device::DEFAULT_ALL_1); + // The two Signetics SCN2681 DUARTs (same family as the Motorola MC68681: // 2681 = RD/WR strobes, 68681 = R/W + DTACK + interrupt vector). The // VT1200 firmware drives them with the generic 68681 register map, and the