-- hello.lua -- load the hello-world payload into RAM and enter it. -- -- MECHANISM: this is DEBUGGER ENTRY, not a delivery mechanism. The firmware -- boots normally to the level-0 diagnostics menu; at frame HELLO_AT this script -- writes the payload bytes into main RAM and arms a breakpoint on the menu's -- idle loop (3FDCE400). The breakpoint action sets PC to the payload's bit -- address and resumes. Nothing is patched on disk; the ROM is untouched. -- -- Evidence printed from inside the emulator: -- * payload bytes read back from RAM equal the linked binary -- * the two magic words and the loop counter written by the payload -- * the video-RAM block the payload wrote -- -- vt1200 -rompath harness/mame/roms -video none -sound none -nothrottle \ -- -debug -debugger none \ -- -autoboot_script harness/mame/hello.lua -autoboot_delay 0 -- -- Env: -- HELLO_AT frame at which to load and enter (default 120) -- HELLO_END frame at which to inspect and exit (default 150) -- HELLO_PAYLOAD linked payload (default harness/hello-world/payload.bin) -- HELLO_ORIGIN payload bit address (default 01800000) local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end local machine = manager.machine local cpu = machine.devices[':maincpu'] local dbg = machine.debugger local prog = cpu.spaces['program'] if not dbg then print("HELLO ERROR: debugger not enabled; pass -debug") machine:exit() return end local AT = tonumber(os.getenv("HELLO_AT") or "120") local STOP_AT = tonumber(os.getenv("HELLO_END") or "150") local ORIGIN = tonumber(os.getenv("HELLO_ORIGIN") or "0x01800000") local PATH = os.getenv("HELLO_PAYLOAD") or "harness/hello-world/payload.bin" local MAGIC1 = 0x01810000 local MAGIC2 = 0x01810010 local COUNT = 0x01810020 local VRAM_ROW = 0x04019040 -- 04000000h + 100*1024 + 64 local IDLE_PC = 0x3FDCE400 -- level-0 menu idle loop (dsjs a9, itself) local payload = nil do local f, err = io.open(PATH, "rb") if not f then print("HELLO ERROR: cannot open " .. PATH .. ": " .. tostring(err)) machine:exit() return end payload = f:read("*a") f:close() print(string.format("HELLO: payload %s bytes=%d", PATH, #payload)) end local frames = 0 local loaded = false local entered = false local hist = {} local function load_payload() local n = math.floor(#payload / 2) local words = {} for i = 0, n - 1 do local lo = payload:byte(i * 2 + 1) local hi = payload:byte(i * 2 + 2) words[i] = lo | (hi << 8) prog:write_u16(ORIGIN + i * 16, words[i]) end -- Read back and compare: this is the check that the load is placed at the -- right bit address with the right word order. local bad = 0 for i = 0, n - 1 do if prog:read_u16(ORIGIN + i * 16) ~= words[i] then bad = bad + 1 if bad <= 4 then print(string.format("HELLO MISMATCH word %d at bit %s: wrote %04X read %04X", i, hex(ORIGIN + i * 16), words[i], prog:read_u16(ORIGIN + i * 16))) end end end print(string.format("HELLO: loaded %d words at bit %s; readback %s (%d bad)", n, hex(ORIGIN), bad == 0 and "OK" or "FAILED", bad)) -- Entry is a breakpoint action: the firmware reaches IDLE_PC every frame. -- (The action only assigns PC and resumes; the payload proves itself.) dbg:command("bpset " .. hex(IDLE_PC) .. ",,{ pc = " .. hex(ORIGIN) .. " ; g }") end local function inspect() local m1 = prog:read_u16(MAGIC1) local m2 = prog:read_u16(MAGIC2) local ct = prog:read_u16(COUNT) print(string.format("HELLO: MAGIC1@%s = %04X (%s)", hex(MAGIC1), m1, m1 == 0xDEAD and "DEAD ok" or "WRONG")) print(string.format("HELLO: MAGIC2@%s = %04X (%s)", hex(MAGIC2), m2, m2 == 0xBEEF and "BEEF ok" or "WRONG")) print(string.format("HELLO: COUNT @%s = %04X (%s)", hex(COUNT), ct, ct == 0x0400 and "1024 stores ok" or "WRONG")) -- The 16 words of row 100, then the same columns of row 163 (last row). local function rowdump(bitaddr, tag) local s = {} for i = 0, 15 do s[#s + 1] = string.format("%04X", prog:read_u16(bitaddr + i * 16)) end print(string.format("HELLO: VRAM %s @%s: %s", tag, hex(bitaddr), table.concat(s, " "))) end rowdump(VRAM_ROW, "row100 ") rowdump(VRAM_ROW + 63 * 1024, "row163 ") -- Every word of the 64x16 region must be FFFF: no holes, no short rows. local filled, holes = 0, 0 for r = 0, 63 do for c = 0, 15 do local v = prog:read_u16(VRAM_ROW + r * 1024 + c * 16) if v == 0xFFFF then filled = filled + 1 else holes = holes + 1 end end end print(string.format("HELLO: VRAM block %d/1024 words are FFFF, %d holes", filled, holes)) -- One word either side of the block must be untouched by us. print(string.format("HELLO: VRAM row100 word before start = %04X, after end = %04X", prog:read_u16(VRAM_ROW - 16), prog:read_u16(VRAM_ROW + 16 * 16))) print(string.format("HELLO: PC at inspection = %s (payload spin expected)", hex(cpu.state['PC'].value))) end emu.register_frame_done(function() frames = frames + 1 local pcv = cpu.state['PC'].value hist[pcv] = (hist[pcv] or 0) + 1 if frames == AT and not loaded then loaded = true load_payload() end if loaded and not entered and prog:read_u16(MAGIC1) == 0xDEAD then entered = true print(string.format("HELLO: payload has run by frame %d (PC=%s)", frames, hex(cpu.state['PC'].value))) end if frames >= STOP_AT then print(string.format("--- frame %d, %d distinct PCs sampled ---", frames, (function() local n = 0 for _ in pairs(hist) do n = n + 1 end return n end)())) inspect() machine:exit() end end, "vt1200-hello") dbg.visible_cpu = cpu dbg.execution_state = 'run'