Stella

Release 6.7

Integrated Debugger

(a work in progress)


Contents


Features

The debugger in Stella may never be complete, as we're constantly adding new features requested by homebrew developers. However, even in its current form it's still quite powerful, and is able to boast at least one feature that no other 2600 debugger has; it's completely cross-platform.

Here's a (non-comprehensive) list of what the debugger can do so far:

Future planned features:


How to use the Debugger

Pressing ` (aka backtick, backquote, grave accent) toggles the debugger on & off. When you exit the debugger, the emulation resumes at the current program counter, and continues until either a breakpoint/trap is hit, or the ` key is pressed again.

The main debugger window will look similar to the following (note that the letters here are for reference in the document below; they aren't actually present in the debugger):

For space reasons, the Prompt, TIA, I/O and Audio displays are split into 4 tabs, only one of which is visible at a time. You can use the mouse or keyboard to select which tab you want to view. Control/Cmd + Tab cycles between tabs from left-to-right, Shift-Control/Cmd + Tab cycles right-to-left. Pressing Tab (or Shift + Tab) cycles between widgets in the current tab (except for in the Prompt Tab, where 'tab' is used for something else).

Note for the GUI display:

You can also enter the debugger at emulator startup by use the 'debug' command on the command line, or alternatively within the ROM launcher in 'Power-on options':

  ; will enter the debugger before any instructions run
  stella -debug mygame.bin

  ; alternatively, you can use 'break' to accomplish the same thing
  ; $fffc is the 6502/6507 init vector. This command will break and enter the
  ; debugger before the first 6507 instruction runs, so you can debug the
  ; startup code:
  stella -break "*($fffc)" mygame.bin

Using the ` key will always enter the debugger at the end of the frame (for NTSC games usually scanLine 262). This is because Stella only checks for keystrokes once per frame. Once in the debugger, you can control execution by stepping one instruction, scanLine, or frame at a time (or more than one at a time, using commands in the prompt). You can also set breakpoints or traps, which will cause the emulator to enter the debugger when they are triggered, even if it happens in mid-frame.


Startup

At startup, the debugger automatically tries load a number of files which will provide additional information for the debugger and can make debugging more convenient.

Note that all files are only accessed if you enter the debugger at least once during a program run. This means you can create these files, and not worry about slowing down emulation unless you're actively using the debugger.


Global Buttons

There are some buttons on the right top that always show up no matter which tab you are looking at. This is because these are the ones that are most frequently used.

The larger button at the left top (labeled '<') performs the rewind operation, which will undo the previous Step/Trace/Scan/Frame... advance, the smaller button at the left bottom (labeled '>') performs the unwind operation, which will undo the previous rewind operation. The rewind buffer is 100 levels deep by default, the size can be configured e.g. in the Developer Settings - Time Machine dialog.

The other operations are Step, Trace, Scan +1, Frame +1 and Run.

You can also use the buttons from anywhere in the GUI via hotkeys.

Function Key
Step ¹ Control + S
Trace ¹ Control + T
Scan +1 ¹ Control + L
Frame +1 ¹ Control + F
Rewind 1 Alt + Left arrow
Rewind 10 Shift-Alt + Left arrow
Rewind all Alt + Down arrow
Unwind 1 Alt + Right arrow
Unwind 10 Shift-Alt + Right arrow
Unwind all Alt + Up arrow
Run, exits debugger Backquote (`), Escape

For MacOS use 'Cmd' instead of  'Alt' key.

¹: External I/O input is ignored, the state of the I/O Tab remains unchanged.

To the left of the global buttons, you find the 'Options...' button.

This opens the Options Menu which is described in detail in the User's Guide.


Change Tracking

The debugger tracks changes to the CPU, TIA and RIOT registers and RAM by displaying changed locations or registers with a red background after each step, trace, scanLine, or frame advance. This sounds simple, and it is, but it's also amazingly useful.

One clarification about the change tracking: it only tracks when values have changed. If the ROM writes a value into a RAM location that already contained the same value, that's not considered a change (old value was $whatever, new value is the same, so nothing got tracked). This may change in a future version of Stella.


(A) Prompt Tab

This is a command-line interface, similar to the DOS DEBUG command or Supermon for the C=64.

Editing keys work about like you'd expect them to in Windows, but many Bash-style commands are also supported:

FunctionKey
Move cursor to beginning of lineHome
Move cursor to end of lineEnd
Remove character to right of cursorDelete
Remove character to left of cursorBackspace
Same function as 'Home'Control + A
Same function as 'End'Control + E
Same function as 'Delete'Control + D
Remove all characters from cursor to end of lineControl + K
Remove all characters from cursor to beginning of lineControl + U
Remove entire word to left of cursorControl + W
Copy current lineControl + C
Cut current lineControl + X
Paste over current lineControl + V
Scroll up through previous commands one screen/pageShift + PgUp
Scroll down through previous commands one screen/pageShift + PgDown
Scroll up through previous commands one lineShift + Up
Scroll down through previous commands one lineShift + Down
Scroll to beginning of commandsShift + Home
Scroll to end of commandsShift + End

You can also scroll with the mouse. Copy and paste is currently only supported for a complete line.

To see the available commands, enter "help". For extended help, type "help cmd", where 'cmd' is the command you wish to know about. The available commands are listed in Prompt Commands at the end of this section. Bash-style tab completion is supported for commands, labels and functions (see below).

For now, there are some functions that only exist in the prompt. We intend to add GUI equivalents for all (or almost all?) of the prompt commands in future releases. People who like command prompts will be able to use the prompt, but people who hate them will have a fully functional debugger without typing (or without typing much, anyway).

Note: unlike the rest of the UI, whatever is shown in the prompt will not be updated during debugging and thus eventually become "stale". You can update it just by re-running the relevant commands in the prompt.


Tab Key Auto-Complete

While entering a command, label or function, you can type a partial name and press the (Shift +) Tab key to attempt to auto-complete it. If you've ever used "bash", this will be immediately familiar. If not, try it: load up a ROM, go to the debugger, type "g" (but don't press Enter), then hit Tab. The "g" will change to "gfx" (since this is the only built-in command starting with a "g"). If there are multiple possible completions (try with "tr" instead of "g"), you can tab through them. After the first character, the autocompletion considers all characters in the right order as a match (e.g. "twf" will be completed to "trapWriteIf"). Alternatively you can make use of the camel case names and type e.g. "tWI" ("trapWriteIf") or "lAS" ("LoadAllStates").

Tab completion works on all labels: built-in, loaded from a symbol file, or set during debugging with the "define" command. It also works with built-in functions and those defined with the "function" command, but it doesn't (yet) work on filenames.


Expressions

Almost every command takes a value: the "a" command takes a byte to stuff into the accumulator, the "break" command takes an address to set/clear a breakpoint at. These values can be as a hex constant ($ff, $1234), or as complex as "the low byte of the 16-bit value located at the address pointed to by the binary number 1010010110100101" (which would be "@<\1010010110100101"). You can also use registers and labels in expressions.

You can use arithmetic and boolean operators in expressions. The syntax is very C-like. The operators supported are:

      + - * /  (add, subtract, multiply, divide: 2+2 is 4)
      %        (modulus/remainder: 3%2 is 1)
      & | ^ ~  (bitwise AND, OR, XOR, NOT: 2&3 is 2)
      && || !  (logical AND, OR, NOT: 2&&3 is 1, 2||0 is 0)
      ( )      (parentheses for grouping: (2+2)*3 is 12)
      * @      (byte and word pointer dereference: *$80 is the byte stored
                at location $80)
      [ ]      (array-style byte pointer dereference: $80[1] is the byte
                stored at location ($80+1) or $81)
      < >      (prefix versions: low and high byte. <$abcd is $cd)
      == < > <= >= !=
               (comparison: equality, less-than, greater-than, less-or-equals,
                greater-or-equals, not-equals)
      << >>    (bit shifts, left and right: 1<<1 is 2, 2>>1 is 1)

Division by zero is not an error: it results in zero instead.

None of the operators change the values of their operands. There are no variable-assignment or increment/decrement operators. This may change in the future, which is why we used "==" for equality instead of just "=".

The bitwise and logical boolean operators are different in that the bitwise operators operate on all the bits of the operand (just like AND, ORA, EOR in 6502 asm), while the logical operators treat their operands as 0 for false, non-zero for true, and return either 0 or 1. So $1234&$5678 results in $1230, whereas $1234&&$5678 results in 1. This is just like C or C++...

Prefixes

Like some programming languages, the debugger uses prefixed characters to change the meaning of an expression. The prefixes are:

Remember, you can use arbitrarily complex expressions with any command that takes arguments.


Breakpoints, watches and traps, oh my!

Breakpoints

A breakpoint is a "hotspot" in your program that causes the emulator to stop emulating and jump into the debugger ¹. You can set as many breakpoints as you like. The command is "break xx yy" where xx is any expression and yy a bank number. Both arguments are optional. If you have created a symbol file, you can use labels for the expression.

Example: You have got a label called "kernel". To break there, the command is "break kernel". After you've set the breakpoint, exit the debugger (enter "run" or click the 'Run' button). The emulator will run until it gets to the breakpoint, then it will enter the debugger with the Program Counter pointing to the instruction at the breakpoint.

Breakpoints happen *before* an instruction is executed: the instruction at the breakpoint location will be the "next" instruction.

To remove a breakpoint, you just run the same command you used to set it. In the example, "break kernel" will remove the breakpoint. The "break" command can be thought of as a *toggle*: it turns the breakpoint on & off, like a light switch.

You could also use "clearBreaks" to remove all the breakpoints. Also, there is a "listBreaks" command that will list them all.

¹ By enabling "logBreaks" you can log the current state into the System Log and continue emulation instead.

Conditional Breaks

A conditional breakpoint causes the emulator to enter the debugger when some arbitrary condition becomes true. "True" means "not zero" here: "2+2" is considered true because it's not zero. "2-2" is false, because it evaluates to zero. This is exactly how things work in C and lots of other languages, but it might take some getting used to if you've never used such a language.

Suppose you want to enter the debugger when the Game Reset switch is pressed. Looking at the Stella Programmers' Guide, we see that this switch is read at bit 0 of SWCHB. This bit will be 0 if the switch is pressed, or 1 otherwise.

To have an expression read the contents of an address, we use the dereference operator "*". Since we're looking at SWCHB, we need "*SWCHB".

We're only wanting to look at bit 0, so let's mask off all the other bits: "*SWCHB&1". The expression now evaluates to bit 0 of SWCHB. We're almost there: this will be 1 (true) if the switch is NOT pressed. We want to break if it IS pressed...

So we invert the sense of the test with a logical NOT operator (which is the "!" operator): !(*SWCHB&1). The parentheses are necessary as we want to apply the ! to the result of the &, not just the first term (the "*SWCHB").

"breakIf !(*SWCHB&1)" will do the job for us. However, it's an ugly mess of letters, numbers, and punctuation. We can do a little better:

"breakIf { !(*SWCHB & 1 ) }" is a lot more readable, isn't it? If you're going to use readable expressions with spaces in them, enclose the entire expression in curly braces.

Remember that Stella only checks for input once per frame, so a break condition that depends on input (like SWCHB) will always happen at the end of a frame. This is different from how a real 2600 works, but most ROMs only check for input once per frame anyway.

Conditional breaks appear in "listBreaks", numbered starting from zero. You can remove a cond-break with "delBreakIf number", where the number comes from "listBreaks" or by entering the same conditional break again.

Any time the debugger is entered due to a trap, breakpoint, or conditional break, the reason will be displayed in the Breakpoint/Trap Status area.

Functions

There is one annoyance about complex expressions: once we remove the conditional break with "delBreakIf" or "clearBreaks", we'd have to retype it (or search backwards with the up-arrow key) if we wanted to use it again.

We can avoid this by defining the expression as a function, then using "breakIf function_name":

  function gameReset { !(*SWCHB & 1 ) }
  breakIf gameReset

Now we have a meaningful name for the condition, so we can use it again. Not only that: we can use the function as part of a bigger expression. Suppose we've also defined a gameSelect function that evaluates to true if the Game Select switch is pressed. We want to break when the user presses both Select and Reset:

  breakIf { gameReset && gameSelect }

User-defined functions appear in "listFunctions", which shows the label and expression for each function. Functions can be removed with "delFunction label", where the labels come from "listFunctions".

Built-in Functions

Stella has some pre-defined functions for use with the "breakIf" command. These allow you to break and enter the debugger on various conditions without having to define the conditions yourself.

Built-in functions and pseudo-registers always start with an _ (underscore) character. It is suggested that you don't start labels in your game's source with underscores, if you plan to use them with the Stella debugger.

FunctionDefinitionDescription
_joy0Left !(*SWCHA & $40) Left joystick moved left
_joy0Right !(*SWCHA & $80) Left joystick moved right
_joy0Up !(*SWCHA & $10) Left joystick moved up
_joy0Down !(*SWCHA & $20) Left joystick moved down
_joy0Fire !(*INPT4 & $80) Left joystick fire button pressed
_joy1Left !(*SWCHA & $04) Right joystick moved left
_joy1Right !(*SWCHA & $08) Right joystick moved right
_joy1Up !(*SWCHA & $01) Right joystick moved up
_joy1Down !(*SWCHA & $02) Right joystick moved down
_joy1Fire !(*INPT5 & $80) Right joystick fire button pressed
_select !(*SWCHB & $02) Game Select pressed
_reset !(*SWCHB & $01) Game Reset pressed
_color *SWCHB & $08 Color/BW set to Color
_bw !(*SWCHB & $08) Color/BW set to BW
_diff0B !(*SWCHB & $40) Left difficulty set to B (easy)
_diff0A *SWCHB & $40 Left difficulty set to A (hard)
_diff1B !(*SWCHB & $80) Right difficulty set to B (easy)
_diff1A *SWCHB & $80 Right difficulty set to A (hard)

Don't worry about memorizing them all: the Prompt "help" command will show you a list of them.

Pseudo-Registers

These "registers" are provided for you to use in your conditional breaks. They're not registers in the conventional sense, since they don't exist in a real system. For example, while the debugger keeps track of the number of scanlines in a frame, a real system would not (there is no register that holds 'number of scanlines' on an actual console).

FunctionDescription
_bank Currently selected bank
_cClocks Color clocks on a scanLine
_cyclesHi Higher 32 bits of number of cycles since emulation started
_cyclesLo Lower 32 bits of number of cycles since emulation started
_fCount Number of frames since emulation started
_fCycles Number of cycles since frame started
_fTimReadCyclesNumber of cycles used by timer reads since frame started
_fWsyncCyclesNumber of cycles skipped by WSYNC since frame started
_iCycles Number of cycles of last instruction
_inTim Current INTIM value
_scan Current scanLine count
_scanEnd Scanline count at end of last frame
_sCycles Number of cycles in current scanLine
_timInt Current TIMINT value
_timWrapRead Timer read wrapped on this cycle
_timWrapWrite Timer write wrapped on this cycle
_vBlank Whether vertical blank is enabled (1 or 0)
_vSync Whether vertical sync is enabled (1 or 0)

_scan always contains the current scanLine count. You can use this to break your program in the middle of your kernel. Example:

    breakIf _scan==#64

This will cause Stella to enter the debugger when the TIA reaches the beginning of the 64th scanLine.

_bank always contains the currently selected bank. For 2K or 4K (non-bankswitched) ROMs, it will always contain 0. One useful use is:

    breakIf { pc==myLabel && _bank==1 }

This is similar to setting a regular breakpoint, but it will only trigger when bank 1 is selected.

Watches

A watch is an expression that gets evaluated and printed before every prompt. This is useful for e.g. tracking the contents of a memory location while stepping through code that modifies it.

You can set up to 10 watches (in future the number will be unlimited). Since the expression isn't evaluated until it's used, you can include registers: "watch *y" will show you the contents of the location pointed to by the Y register, even if the Y register changes.

The watches are numbered. The numbers are printed along with the watches, so you can tell which is which. To delete a watch use the "delWatch" command with the watch number (1 to whatever). You can also delete them all with the "clearWatches" command.

Note that there's no real point in watching a label or CPU register without dereferencing it: Labels are constants, and CPU registers are already visible in the CPU Registers widget

Traps

A trap is similar to a breakpoint, except that it catches accesses to a memory address, rather than specific location in the program. They're useful for finding code that modifies TIA registers or memory.

Traps can also combined with a condition ("trapIf"). If an access to a memory address is caught, the condition is evaluated additionally. Only if the condition is true too, the emulations stops. For details about conditions see Conditional Breaks described above.

An example: you are debugging a game, and you want to stop the emulation and enter the debugger whenever RESP0 is strobed. You'd use the command "trap RESP0" to set the trap, then exit the debugger. The emulator will run until the next time RESP0 is accessed (either read or write). Once the trap is hit, you can examine the TIA state to see what the actual player 0 position is, in color clocks (or you can in the future when we implement that feature in the TIA dump!)

Unlike breakpoints, traps stop the emulation *after* the instruction that triggered the trap. The reason for this is simple: until the instruction is executed, the emulator can't know it's going to hit a trap. After the trap is hit, the instruction is done executing, and whatever effects it may have had on e.g. the TIA state have already happened... but we don't have a way to run the emulated VCS in reverse, so the best we can do is stop before the next instruction runs.

Traps come in two varieties: read access traps and write access traps. It is possible to set both types of trap on the same address (that's what the plain "trap" command does). To set a read or write only trap, use "trapRead(if)" or "trapWrite(if)".

All traps appear in "listTraps", numbered starting from zero. You can remove a trap with "delTrap number", where the number comes from "listTraps" or by entering the identical trap again. You can get rid of all traps at once with the "clearTraps" command.


Save your work!

Stella offers several commands to save your work inside the debugger for later re-use.


Prompt Commands

Type "help" to see this list in the debugger.
Type "help 'cmd'" to see extended information about the given command.

                a - Set Accumulator to <value>
              aud - Mark 'AUD' range in disassembly
         autoSave - Automatically execute "save" when exiting the debugger
             base - Set default number base to <base> (bin, dec, hex)
             bCol - Mark 'BCOL' range in disassembly
            break - Set/clear breakpoint at <address> and <bank>
          breakIf - Set/clear breakpoint on <condition>
       breakLabel - Set/clear breakpoint on <address> (no mirrors, all banks)
                c - Carry Flag: set (0 or 1), or toggle (no arg)
            cheat - Use a cheat code (see manual for cheat types)
      clearBreaks - Clear all breakpoints
      clearConfig - Clear DiStella config directives [bank xx]
     clearHistory - Clear the prompt history
clearSaveStateIfs - Clear all saveState points
       clearTraps - Clear all traps
     clearWatches - Clear all watches
              cls - Clear prompt area of text
             code - Mark 'CODE' range in disassembly
              col - Mark 'COL' range in disassembly
        colorTest - Show value xx as TIA color
                d - Decimal Mode Flag: set (0 or 1), or toggle (no arg)
             data - Mark 'DATA' range in disassembly
      debugColors - Show Fixed Debug Colors information
           define - Define label xx for address yy
       delBreakIf - Delete conditional breakIf <xx>
      delFunction - Delete function with label xx
   delSaveStateIf - Delete conditional saveState point <xx>
          delTrap - Delete trap <xx>
         delWatch - Delete watch <xx>
           disAsm - Disassemble address xx [yy lines] (default=PC)
             dump - Dump data at address <xx> [to yy] [1: memory; 2: CPU state; 4: input regs] [?]
             exec - Execute script file <xx> [prefix]
          exitRom - Exit emulator, return to ROM launcher
            frame - Advance emulation by <xx> frames (default=1)
         function - Define function name xx for expression yy
              gfx - Mark 'GFX' range in disassembly
             help - help <command>
           joy0Up - Set joystick 0 up direction to value <x> (0 or 1), or toggle (no arg)
         joy0Down - Set joystick 0 down direction to value <x> (0 or 1), or toggle (no arg)
         joy0Left - Set joystick 0 left direction to value <x> (0 or 1), or toggle (no arg)
        joy0Right - Set joystick 0 right direction to value  (0 or 1), or toggle (no arg)
         joy0Fire - Set joystick 0 fire button to value <x> (0 or 1), or toggle (no arg)
           joy1Up - Set joystick 1 up direction to value <x> (0 or 1), or toggle (no arg)
         joy1Down - Set joystick 1 down direction to value <x> (0 or 1), or toggle (no arg)
         joy1Left - Set joystick 1 left direction to value <x> (0 or 1), or toggle (no arg)
        joy1Right - Set joystick 1 right direction to value <x> (0 or 1), or toggle (no arg)
         joy1Fire - Set joystick 1 fire button to value <x> (0 or 1), or toggle (no arg)
             jump - Scroll disassembly to address xx
       listBreaks - List breakpoints
       listConfig - List DiStella config directives [bank xx]
    listFunctions - List user-defined functions
 listSaveStateIfs - List saveState points
        listTraps - List traps
       loadConfig - Load DiStella config file
    loadAllStates - Load all emulator states
        loadState - Load emulator state xx (0-9)
        logBreaks - Logs breaks and traps and continues emulation
                n - Negative Flag: set (0 or 1), or toggle (no arg)
          palette - Show current TIA palette
               pc - Set Program Counter to address xx
             pCol - Mark 'PCOL' range in disassembly
             pGfx - Mark 'PGFX' range in disassembly
            print - Evaluate/print expression xx in hex/dec/binary
              ram - Show ZP RAM, or set address xx to yy1 [yy2 ...]
            reset - Reset system to power-on state
           rewind - Rewind state by one or [xx] steps/traces/scanlines/frames...
             riot - Show RIOT timer/input status
              rom - Set ROM address xx to yy1 [yy2 ...]
              row - Mark 'ROW' range in disassembly
              run - Exit debugger, return to emulator
            runTo - Run until string xx in disassembly
          runToPc - Run until PC is set to value xx
                s - Set Stack Pointer to value xx
             save - Save breaks, watches, traps and functions to file [xx or ?]
       saveAccess - Save access counters to CSV file [?]
       saveConfig - Save DiStella config file (with default name)
          saveDis - Save DiStella disassembly to file [?]
          saveRom - Save (possibly patched) ROM to file [?]
          saveSes - Save console session to file [?]
         saveSnap - Save current TIA image to PNG file
    saveAllStates - Save all emulator states
        saveState - Save emulator state xx (valid args 0-9)
      saveStateIf - Create saveState on <condition>
         scanLine - Advance emulation by <xx> scanlines (default=1)
             step - Single step CPU [with count xx]
        stepWhile - Single step CPU while <condition> is true
              tia - Show TIA state
            trace - Single step CPU over subroutines [with count xx]
             trap - Trap read/write access to address(es) xx [yy]
           trapIf - On <condition> trap R/W access to address(es) xx [yy]
         trapRead - Trap read access to address(es) xx [yy]
       trapReadIf - On <condition> trap read access to address(es) xx [yy]
        trapWrite - Trap write access to address(es) xx [yy]
      trapWriteIf - On <condition> trap write access to address(es) xx [yy]
             type - Show disassembly type for address xx [yy]
             uHex - Toggle upper/lowercase HEX display
            undef - Undefine label xx (if defined)
           unwind - Unwind state state by one or [xx] steps/traces/scanlines/frames...
                v - Overflow Flag: set (0 or 1), or toggle (no arg)
            watch - Print contents of address xx before every prompt
                x - Set X Register to value xx
                y - Set Y Register to value xx
                z - Zero Flag: set (0 or 1), or toggle (no arg)

(B) TIA Tab

When selected, this tab shows detailed status of all the TIA registers (except for audio; use the Audio tab for those).

Most of the values on the TIA tab will be self-explanatory to a 2600 developer.

Many of the variables inside the TIA can only be written to by the 6502. The debugger lets you get inside the TIA and see the contents of these variables. These include the color registers, player/missile graphics and positions, and the playfield.

You can control almost every aspect of the TIA from here, too: most of the displays are editable. You can even toggle individual bits in the GRP0/1 and playfield registers (remember to double-click).

The buttons allow you to write to any of the strobe registers at any time.

The collision registers are displayed in decoded format, in a table. You can see exactly which objects have hit what. These are read-only displays; you can't toggle the bits in the current release of Stella. Of course, you can clear all the collisions with the CXCLR Strobe button.

To the right of each color register, you'll see a small rectangle drawn in the current color. Changing a color register will change the color of this rectangle.

Both player graphics registers (GRP0 and GRP1) come in two versions: a "new" and an "old" register. Writing GRP0 updates the value in the "new" version of GRP0 and, at the same time, copies the value in the "new" GRP1 register into its "old" counterpart. Writing to GRP1 behaves the same way, with the roles of GRP0 and GRP1 switched. The debugger shows both registers, the "old" register being located below the "new" one. If VDEL is off, the TIA displays the content of the "new" register, and the debugger tab reflects this by crossing out the old register. If VDEL is enabled, the TIA displays the "old" register, and the lines are removed in the tab.

The "enable ball" register (ENABL) also comes in a "new" and an "old" version. The content of "new" is copied into "old" on writes to GRP1, and VDELBL selects the register that is used to control the ball. This is visualized in the debugger in the same way as the two copies of GRP0 and GRP1

For many registers, writes don't take effect immediatelly as the TIA takes some color clocks to change state. In Stella's TIA core, this is implemented by queueing the writes, and the contents of this queue are visualized in the debugger in the "Queued Writes" area of the TIA tab.


(C) I/O Tab

When selected, this tab shows detailed status of the Input, Output, and Timer portion of the RIOT/M6532 chip (the RAM portion is accessed in another part of the debugger).

As with the TIA tab, most of the values here will be self-explanatory to a 2600 developer, and many can be modified. However, the SWCHx registers need further explanation:

SWCHx(W/R) can be modified; here (W) stands for write, (R) for read. Similarly, SWxCNT can be directly modified. However, the results of reading back from the SWCHx register are influenced by SWxCNT, and SWCHx(R) is reflecting this result.


(D) Audio Tab

This tab lets you view the contents of the TIA audio registers and the effective volume resulting from the two channel volumes.

This tab will grow some features in a future release.


(E) TIA Display

In the upper left of the debugger, you'll see the current frame of video as generated by the TIA. If a complete frame hasn't been drawn, the partial contents of the current frame will be displayed up to the current scanLine, with the contents of the old frame (in black & white) filling the rest of the display. Note that if 'phosphor mode' or TV effects are enabled, you won't see the effects here; this shows the raw TIA image only.

To e.g. watch the TIA draw the frame one scanLine at a time, you can use the 'Scan+1' button, the prompt "scan" command or the Control-L key.

You can also right-click anywhere in this window to show a context menu, as illustrated:

The options are as follows:


(F) TIA Information

To the right of the TIA Display area, TIA information is displayed (all values are decimal):

The indicators are as follows (note that all these are read-only):


(G) TIA Zoom

Below the TIA Information is the TIA Zoom area. This allows you to enlarge part of the TIA display, so you can see fine details. Like the TIA Display area, this one does generate frames as the real system would.

You can also right-click anywhere in this window to show a context menu, as illustrated:

These options allow you to:

If you click on the output window, you can zoom with the mouse wheel too. And you can either drag and drop the zoom position with the mouse or you can scroll around using the cursor, PageUp/Dn and Home/End keys. You can also select the zoom position from a context menu in the TIA Display.


(H) Breakpoint/Trap Status

Below the TIA Zoom there is a status line that shows the reason and the address the debugger was entered (if a breakpoint or trap was hit), as shown:

The output here will generally be self-explanatory. Due to space concerns, the reason will be shown as follows:

See the Breakpoints, watches and traps... section for details.


(I) CPU Registers

This displays the current CPU state, as shown:

All the registers and flags are displayed, and can be changed by double-clicking on them (to the left). Flags are toggled on double-click. Selected registers here can also be changed by using the 'Data Operations' buttons, further described in (J). All items are shown in hex. Any label defined for the current PC value is shown to the right. Decimal and binary equivalents are shown for SP/A/X/Y to the right (first decimal, then binary).

The column to the far right shows the 'source' of contents of the respective registers. For example, consider the command 'LDA ($80),Y'. The operand of the command resolves to some address, which isn't always easy to determine at first glance. The 'Src Addr' area shows the actual resulting operand/address being used with the given opcode.

The destination address of the last write is shown besides 'Dest'.

There's not much else to say about the CPU Registers widget: if you know 6502 assembly, it's pretty self-explanatory. If you don't, well, you should learn :)


(J) Data Operations Buttons

These buttons can be used to change values in either CPU Registers, the M6532/RIOT RAM or Detailed Cartridge Extended RAM Information, depending on which of these widgets is currently in focus.

Each of these buttons also have a keyboard shortcut (indicated in square brackets). In fact, many of the inputboxes in various parts of the debugger respond to these same keyboard shortcuts. If in doubt, give them a try.

ButtonShortutDescription
0
'z'
Set the current location/register to zero.
Inv
'i' or '!'
Invert the current location/register (toggle all its bits)
Neg
'n'
Negate the current location/register (twos' complement negative)
++
'+' or '='
Increment the current location/register.
--
'-'
Decrement the current location/register.
<<
'<' or ','
Shift the current location/register left.
>>
'>' or '.'
Shift the current location/register right.

Any bits shifted out of the location/register with << or >> are lost (they will NOT end up in the Carry flag).


(K) M6532/RIOT RAM

This is a spreadsheet-like GUI for inspecting and changing the contents of the 2600's zero-page RAM.

You can navigate with either the mouse or the keyboard (see below). To change a RAM location, either double-click on it or press 'Enter' while it's highlighted. Enter the new value (hex, other formats using the bottom textboxes), then press 'Enter' to make the change. The currently selected RAM cell can also be changed by using the Data Operations Buttons or the associated shortcut keys.

You can navigate all RAM grids (and other data grids too) using the following keyboard shortcuts.

ShortutDescription
Left arrowMove to left cell.
Right arrowMove to right cell.
HomeMove to leftmost cell.
EndMove to rightmost cell.
Up arrowMove to cell above.
Down arrowMove to cell below.
Page UpMove to top visible row.
Page DownMove to bottom visible row.
Shift + Page UpScroll one page up.
Shift + Page DownScroll one page down.

The 'Undo' button in the upper right should be self-explanatory; it will undo the most previous operation to one cell only. The 'Revert' button is more comprehensive. It will undo all operations on all cells since you first made a change.

The UI objects at the bottom refer to the currently selected RAM cell. The 'Label' textbox shows the label attached to this RAM location (if any), and the other three textboxes show the hex, decimal and binary equivalent value. The values can be edited here too.

The remaining buttons to the right are further explained in the next section.

(L) M6532/RIOT RAM (search/compare mode)

The RAM widget also lets you search memory for values such as lives or remaining energy, but it's also very useful when debugging to determine which memory location holds which quantity.

To search the RAM, click 'Search...' and enter a byte value into the search editbox (0-255). All matching values will be highlighted in the RAM widget. If no value was entered, all RAM locations will be highlighted.

The 'Compare...' button is used to compare the given value using all addresses currently highlighted. This may be an absolute number (such as 2), or a comparative number (such as -1). Using a '+' or '-' operator means 'search addresses for values that have changed by that amount'.

The 'Reset' button resets the entire operation; it clears the highlighted addresses and allows another search.

The following is an example of inspecting all addresses that have decreased by 1:


(M) ROM Disassembly

This area contains a disassembly of the current bank of ROM. If a symbol file is loaded, the disassembly will have labels. Even without a symbol file, the standard TIA/RIOT labels will still be present.

The disassembly is often quite extensive, and whenever possible tries to automatically differentiate between code, graphics, data and unused bytes. There are actually two levels of disassembly in Stella. First, the emulation core tracks accesses as a game is running, making for very accurate results. This is known as a dynamic analysis. Second, the built-in DiStella code does a static analysis, which tentatively fills in sections that the dynamic disassembler missed (usually because the addresses haven't been accessed at runtime yet).

As such, code can be marked in two ways (absolute, when done by the emulation core), and tentative (when done by DiStella, and the emulation core hasn't accessed it yet). Such 'tentative' code is marked with the '*' symbol, indicating that it has the potential to be accessed as code sometime during the program run. This gives very useful information, since it can indicate areas toggled by an option in the game (ie, when a player dies, when difficulty level changes, etc). It can also indicate whether blocks of code after a relative jump are in fact code, or simply data.

The "Bank state" is self-explanatory, and shows a summary of the current bank information. For normal bankswitched ROMs, this will be the current bank number, however more advanced schemes will show other types of information here. More detailed information is available in Detailed Bankswitch Information.

Each line of disassembly has four fields:

At this point, we should explain the various 'types' that the disassembler can use. These are known as 'directives', and partly correspond to configuration options from the standalone DiStella program. They are listed in order of decreasing hierarchy:

CODE Addresses which have appeared in the program counter, or which tentatively can appear in the program counter. These can be edited in hex.
GFX Addresses which contain data stored in the player graphics registers (GRP0/GRP1). These addresses are shown with a bitmap of the graphics, which can be edited in either hex or binary. The bitmap is shown as large blocks.
PGFX Addresses which contain data stored in the playfield graphics registers (PF0/PF1/PF2). These addresses are shown with a bitmap of the graphics, which can be edited in either hex or binary. The bitmap is shown as small dashes.
COL Addresses which contain data stored in the player color registers (COLUP0/COLUP1). These addresses are shown as color constants, which can be edited in hex. The color constant names are depending on the ROM's TV type.
PCOL Addresses which contain data stored in the playfield color register (COLUPF). These addresses are shown as color constants, which can be edited in hex. The color constant names are depending on the ROM's TV type.
BCOL Addresses which contain data stored in the background color register (COLUBK). These addresses are shown as color constants, which can be edited in hex. The color constant names are depending on the ROM's TV type.
AUD Addresses which contain data stored in the audio registers (AUDC0/AUDC1/AUDF0/AUDF1/AUDV0/AUDV1). These can be edited in hex.
DATA Addresses used as an operand for some opcode. These can be edited in hex.
ROW Addresses not used as any of the above. These are shown up to 8 per line and cannot be edited.

For code sections, the 6502 mnemonic will be UPPERCASE for all standard instructions, or lowercase for "illegal" 6502 instructions (like "dcp"). If automatic resolving of code sections has been disabled for any reason, you'll likely see a lot of illegal opcodes if you scroll to a data table in ROM. This can also occur if the disassembler hasn't yet encountered addresses in the PC. If you step/trace/scanLine/frame advance into such an area, the disassembler will make note of it, and disassemble it correctly from that point on.

You can scroll through the disassembly with the mouse or keyboard. To center the display on the current PC, press the Space bar.

Any time the Program Counter changes (due to a Step, Trace, Frame or Scanline advance, or manually setting the PC), the disassembly will scroll to the current PC location.

Even though ROM is supposed to be Read Only Memory, this is an emulator: you can change ROM all you want within the debugger. The hex bytes in the ROM Widget are editable. Double-click on them to edit them. When you're done, press Enter to accept the changes (in which case the cart will be re-disasembled) or Escape to cancel them. Note that only instructions that have been fully disassembled can be edited. In particular, blank lines or 'ROW' directives cannot be edited. Also note that certain ROMs can have sections of address space swapped in and out dynamically. As such, changing the contents of a certain address will change the area pointed to at that time. In particular, modifying an address that points to internal RAM will change the RAM, not the underlying ROM. A future release may graphically differentiate between RAM and ROM areas.

ROM Disassembly Settings

The ROM Disassembly also contains a Settings dialog, accessible by right-clicking anywhere in the listing:

The following options are available:

Limitations

These limitations will be addressed in a future release of Stella.


(N) Detailed Bankswitch Information

This area shows a detailed breakdown of the bankswitching scheme. Since the bankswitch schemes can greatly vary in operation, this tab will be different for each scheme, but its specific functionality should be self-explanatory. An example of both 4K (non-bankswitched) and DPC (Pitfall II) is as follows:

In many cases, quite a bit of the scheme functionality can be modified. Go ahead and try to change something!


(O) Detailed Cartridge Extended RAM Information

If applicable, this area shows a detailed breakdown of any extra RAM supported by the bankswitching scheme. Since the bankswitch schemes can greatly vary in operation, this tab will be different for each scheme, but its specific functionality should be self-explanatory. An example of both F6SC (16K Atari + ram) and DPC (Pitfall II) is as follows:

The RAM is shown in a grid similar to how zero-page RAM is shown in M6532/RIOT RAM (K) and (L). See those sections for a description of usage.

In the cases where RAM is always mapped into the same place in the cartridge address space (such as Sara-chip), the RAM addresses are labeled as such. In other cases, such as when the RAM is either quiescent (and mapped in at different places), or not viewable by the 6507 at all, the RAM addresses are labeled as the cart sees them. In the examples above, F8SC RAM is labeled starting at its read port, or $F080. However, the RAM in the DPC scheme is not viewable by the 6507, so its addresses start from $0.

DiStella Configuration Files

As mentioned in ROM Disassembly, Stella supports the following directives: CODE, GFX, PGFX, COL, PCOL, BCOL, AUD, DATA, ROW. While the debugger will try to automatically mark address space with the appropriate directive, there are times when it will fail. There are several options in this case:

  1. Manually set the directives: Directives can be set in the debugger prompt with the aud/code/col/bcol/gfx/pCol/pGfx/data/row commands. These accept an address range for the given directive type. Setting a range with the same type a second time will remove that directive from the range.
  2. Use configuration files: Configuration files can be used to automatically load a list of directives when a ROM is loaded. These files can be generated with the 'saveConfig' command, and loaded with the 'loadConfig' command. There are also 'listConfig' and 'clearConfig' commands to show and erase (respectively) the current directive listing. Upon opening the debugger for the first time, Stella attempts to load a configuration file from the folder containing the ROM. Assuming a ROM named "rr.a26" exists, the config file must be named rr.cfg.

Tutorial: How to hack a ROM

Here is a step-by-step guide that shows you how to use the debugger to actually do something useful. No experience with debuggers is necessary, but it helps to know at least a little about 6502 programming.

  1. Get the Atari Battlezone ROM image. Make sure you've got the regular NTSC version. Load it up in Stella and press TAB to get to the main menu. From there, click on "Game Information". For "Name", it should say "Battlezone (1983) (Atari)" and for MD5Sum it should say "41f252a66c6301f1e8ab3612c19bc5d4". The rest of this tutorial assumes you're using this version of the ROM; it may or may not work with the PAL version, or with any of the various "hacked" versions floating around on the 'net.
  2. Start the game. You begin the game with 5 lives (count the tank symbols at the bottom of the screen).
  3. Enter the debugger by pressing the ` (backquote) key. Don't get killed before you do this, though. You should still have all 5 lives.
  4. In the RAM display, click the 'Search' button and enter "5" for input. This searches RAM for your value and highlights all addresses that match the input. You should see two addresses highlighted: "00a5" and "00ba". These are the only two addresses that currently have the value 5, so they're the most likely candidates for "number of lives" counter. (However, some games might actually store one less than the real number of lives, or one more, so you might have to experiment a bit. Since this is a "rigged demo", I already know Battlezone stores the actual number of lives. Most games do, actually).
  5. Exit the debugger by pressing ` (backquote) again. The game will pick up where you left off.
  6. Get killed! Ram an enemy tank, or let him shoot you. Wait for the explosion to finish. You will now have 4 lives.
  7. Enter the debugger again. Click the 'Compare...' button in RAM widget and enter a value of 4. Now the RAM widget should only show one highlighted address: "00ba". What we did was search within our previous results (the ones that were 5 before) for the new value 4. Address $00ba used to have the value 5, but now it has 4. This means that Battlezone (almost certainly) stores the current number of lives at address $00ba.
  8. Test your theory. Go to the RAM display and change address $ba to some high number like $ff (you could use the Prompt instead: enter "ram $ba $ff"). Exit the debugger again (or advance the frame). You should now see lots of lives at the bottom of the screen (of course, there isn't room to display $ff (255) of them!)... play the game, get killed a few times, notice that you have lots of lives.
  9. Now it's time to decide what sort of "ROM hack" we want to accomplish. We've found the "lives" counter for the game, so we can either have the game start with lots of lives, or change the game code so we can't get killed (AKA immortality), or change the code so we always have the same number of lives (so we never run out, AKA infinite lives). Let's go for infinite lives: it's a little harder than just starting with lots of lives, but not as difficult as immortality (for that, we have to disable the collision checking code, which means we have to find and understand it first!)
  10. Set a Write Trap on the lives counter address: "trapWrite $ba" in the Prompt. Exit the debugger and play until you get killed. When you die, the trap will cause the emulator to enter the debugger with the Program Counter pointing to the instruction *after* the one that wrote to location $ba.
  11. Once in the debugger, look at the ROM display. The PC should be at address $f238, instruction "LDA $e1". You want to examine a few instructions before the PC, so scroll up using the mouse or arrow keys. Do you see the one that affects the lives counter? That's right, it's the "DEC $ba" at location $f236.
  12. Let's stop the DEC $ba from happening. We can't just delete the instruction (it would mess up the addressing of everything afterwards, if it were even possible), but we can replace it with some other instruction(s).

    Since we just want to get rid of the instruction, we can replace it with NOP (no operation). From looking at the disassembly, you can see that "DEC $ba" is a 2-byte long instruction, so we will need two one-byte NOP instructions to replace it. From reading the prompt help (the "help" command), you can see that the "rom" command is what we use to patch ROM.

    Unfortunately, Stella doesn't contain an assembler, so we can't just type NOP to put a NOP instruction in the code. We'll have to use the hex opcode instead.

    Now crack open your 6502 reference manual and look up the NOP instruction's opcode... OK, OK, I'll just tell you what it is: it's $EA (234 decimal). We need two of them, so the bytes to insert will look like:

        $ea $ea

    Select the line at address $f236 and enter 'ROM patch' mode. This is done by either double-clicking the line, or pressing enter. Then delete the bytes with backspace key and enter "ea ea". Another way to do this would have been to enter "rom $f236 $ea $ea" in the Prompt widget.

  13. Test your patch. First, set location $ba to some number of lives that can be displayed on the screen ("poke $ba 3" or enter directly into the RAM display). Now exit the debugger and play the game. You should see 3 lives on the screen.
  14. The crucial test: get killed again! After the explosion, you will *still* see 3 lives: Success! We've hacked Battlezone to give us infinite lives.
  15. Save your work. In the prompt: "saveRom". You now have your very own infinite-lives version of Battlezone. The file will be saved in your HOME directory (NOT your ROM directory), so you might want to move it to your ROM directory if it isn't the current directory.
  16. Test the new ROM: exit Stella, and re-run it. Open your ROM (or give its name on the command line) and play the game. You can play forever! It worked.

Now, try the same techniques on some other ROM image (try Pac-Man). Some games store (lives+1) or (lives-1) instead of the actual number, so try searching for those if you can't seem to make it work.

If you successfully patch a ROM in the debugger, but the saved version won't work, or looks funny, you might need to add an entry to the stella.pro file, to tell Stella what bankswitch and/or TV type to use. That's outside the scope of this tutorial :)

Of course, the debugger is useful for a lot more than cheating and hacking ROMs. Remember, with great power comes great responsibility, so you have no excuse to avoid writing that game you've been thinking about for so long now :)