Stepping Into ROM: Source-Level Debugging for Monitor Code
By John Hardy
When I step through a TEC-1 program the interesting work often happens at the boundary where my code calls into the monitor ROM then the ROM calls back into my routines. The trouble was that Debug80 only knew about the source files I assembled for the current session. This meant the ROM was a black box since I could see it executing in the disassembly view but I could not see the original source lines. I could not set breakpoints by label either. That gap made it hard to understand what the ROM was doing when my program misbehaved so the fix required two changes to the debugger architecture. The debugger needed a way to load additional listing files that live outside the project. It also needed to build source maps from those listings so that stepping plus breakpoints work the same way they do in user code. The result is a new extraListings configuration option that accepts a list of paths to .lst files. When a debug session starts the adapter loads each listing then parses it then merges the resulting segments into the main source map.
The Extra Listings Configuration
The configuration lives in debug80.json alongside the other platform settings. A typical TEC-1 setup now includes the extra listings array pointing to the ROM listing file. The path resolves relative to the debug80.json base directory though absolute paths also work for users who want to reference listings from a central location. If a listing file is missing the adapter logs a warning to the Debug Console then continues without it. This keeps the session usable even when the ROM source is unavailable. This graceful degradation means that users can share project configurations without requiring everyone to have the same ROM sources installed. The adapter loads each listing at session start then parses the contents line by line building a source map that the debugger uses to correlate addresses with source lines throughout the session.
Building Source Maps from Listings
A listing file contains the assembled output alongside the original source lines. The format varies by assembler but the essential structure is an address column followed by a hex dump of the generated bytes followed by the source text. The adapter parses each line then extracts address ranges for every instruction. Those ranges become segments in the source map where each line with a valid address becomes a segment recording the start address plus the byte count plus the line number. When the CPU hits an address in that range the debugger can now jump to the correct line in the listing file to show the original assembly code alongside the current register state. The parsing logic handles the common listing formats including asm80 plus tasm plus zmac output. The adapter detects the format automatically by examining the first few lines of the file which means users do not need to specify the assembler they used.
Compiling Source Files on the Fly
Listing files work well when they exist but sometimes I only have the original assembly source. I wanted the debugger to handle that case too. If a listing file sits next to a .source.asm file with the same base name the adapter compiles the source using asm80 then builds the mapping from the compiler output. If it sits next to a plain .asm file that also works. The compilation happens at session start when the adapter calls asm80 directly via its JavaScript API using a file resolver hook that lets asm80 resolve include files relative to the source directory. The compile result contains a list of lines with addresses plus byte counts plus a symbol table. The adapter walks both structures then builds segments plus anchors for the source map. This means I can drop a ROM source file next to the listing then the debugger will pick it up automatically. I can set breakpoints by clicking in the source margin while the stack trace shows the original labels instead of raw addresses.
The ROM Source Picker
With multiple ROM sources loaded I needed a way to open them during a session. The debugger now registers a command called debug80.openRomSource that queries the adapter for the list of loaded ROM sources then presents them in a quick pick menu. Selecting an entry opens the file in the editor. The command distinguishes between listing files plus source files. If both exist for the same ROM the picker shows both options so I can choose the listing when I want to see the hex dump. I can choose the source when I want to read the assembly without the noise. The picker also shows the file path plus the address range the source covers. This helps me find the right file quickly when working with multiple ROM modules that each have their own source files.
Shadow Memory Plus Address Aliasing
The TEC-1G adds another layer of complexity because its memory controller can shadow the ROM region at 0x0000–0x07FF with RAM at 0xC000–0xC7FF. With shadow mode active the CPU sees the high RAM contents at the low addresses. This is how the TEC-1G boots since the ROM lives at 0xC000 but the CPU starts execution at 0x0000 because the shadow activates at power-on. The debugger needed to understand this aliasing because when I set a breakpoint at a ROM label the label resolves to an address like 0xC100. With shadow mode active the CPU executes that code at 0x0100 so the breakpoint would never fire because the addresses did not match. I fixed this by teaching the breakpoint checker to consider shadow aliases. When the CPU stops the adapter checks both the raw PC plus its shadow alias against the breakpoint set. The same logic applies to source lookup so if the PC is in the shadowed region the adapter tries the shadow alias when resolving the source file.
This change also required updating the TEC-1G runtime to enable shadow mode at power-on because the previous behaviour started with shadow disabled. This meant the ROM had to explicitly enable it before execution could begin. That was incorrect since the real hardware boots with shadow active so the CPU can fetch from ROM at address zero.
What This Enables
I can now step through the TEC-1 monitor ROM the same way I step through my own code. When my program calls GETKEY I can follow execution into the ROM then watch it scan the keypad. When a subroutine misbehaves I can set a breakpoint inside the ROM then inspect the state when it fires. The source map merging means that the debugger treats ROM code plus user code as a unified address space. The stack trace shows labels from both. The disassembly view annotates ROM addresses with their source lines. The memory panel can jump to ROM symbols. This is the debugging experience I wanted from the start. The ROM is no longer a black box but just another module in the program visible plus inspectable like everything else.