Tuesday, June 26, 2012

Debugging Tips for 6502 Programing

After working on some 6502 assembler code recently I was thinking about some useful tips for debugging that I could pass on. Here are a few thoughts based on my experience. Many of these are not specific to the 6502 but valid for assembly language or any software debugging in general.

First, here are some suggestions on things to to to minimize your time spent debugging:

1. Use a cross-assembler

While some programmers can write machine code in their head and change code in memory on the fly, with assembler code you can't afford to make simple mistakes like entering the wrong hex code for an instruction. For any significant amount of code I recommend you use a cross-assembler (or at last a resident assembler, like Krusader).

2. Comment your code

It really helps to comment code, even if you are the only person who may ever look at it. A few weeks or months from now you may not understand what your code is doing if it is uncommented. As compared to high-level languages like C or C++ (or BASIC), I think it is good practice to comment almost every line of code, with additional comments describing what each routine does and tricky features and algorithms, etc. Here is an example:

; Get character from keyboard
; Returns character in A
; Clears high bit to be valid ASCII
; Registers changed: A
        LDA KBDCR               ; Read keyboard control register
        BPL GetKey              ; Loop until key pressed (bit 7 goes high)
        LDA KBD                 ; Get keyboard data
        AND #%01111111          ; Clear ms bit to convert to standard ASCII

3. Use a source code management system

Even if you are the only person who may ever use your code, using a source code management system will help you keep different versions of your code during development. This is really useful for the times when you break something that was previously working and you need to figure out what changed. If you use a remote server it also protects you against losing all your work due to human error or hardware failure. There are many services now like github.com, many of which are free for free software projects, that will host your code so you don't have to set up a server.

4. Build up and test small routines

Don't write several hundred lines of code and then attempt to debug it. Write code in small routines and test them individually. It is much easier to debug and your code will likely end up being more modular as well.

5. Do a desk check

A desk check is a manual process of running through your code, as if you were the computer, verifying that it works correctly. It is often helpful to actually step through the code and keep track of values of variables and registers on paper. You will often find errors in your code this way and can more efficiently find and correct the problems.

I have to admit that I sometimes only go back and do a desk check when my code doesn't work the first time (which is usually the case).

6. Print statement debugging

Often you need to figure out where your code is going, and what the values of certain variables are. A tried and true method of debugging is to put instructions in to print information at key points in the code.

You may want to write functions that can print strings and 8 and 16-bit values that you can call for this purpose. In simple code it may be enough to just print single characters or values. If your hardware is such that you don't have a device to send characters to (screen, serial port, etc.) then even a LED or output port can be used to output debug information.

7. Use a simulator.

If you don't have a good debugger on your system you should consider using one of the available 6502 simulators to run and test your code. They may provide a better environment for debugging and testing and eliminate the possibility of hardware problems.

8. Use a debugger

On the Replica 1 the Krusader assembler provide a "mini-monitor" which lets you change registers and single step through code.

My JMON monitor  also has a breakpoint feature that works in conjunction with the mini monitor.

An assembler listing generated by your cross-assembler is also very helpful when single stepping and using breakpoints so you can relate the code to addresses.

Common Errors

Here is a list of what in my experience are some of the more common errors made, even by experienced programmers, when writing in 6502 assembler.

Off by one errors

This is the classic error where a condition test is off by one, such as a loop which runs one time too few or too often.

Branching on the wrong condition

It is easy to use a "BEQ" when you meant "BNE" and hard to notice the error when looking at the code you wrote. Good comments can help here.

Forgetting to clear/set carry before addition/subtraction

This is specific to the 6502 and most programmers soon learn not to make this mistake.

Use # when not needed or missing it when needed

It is easy to forget to use immediate addressing mode mode when that is what you intended or inadvertently use immediate addressing when you shouldn't. For example, the two code examples below are probably wrong but you can easily overlook the mistake, particularly in your own code:

  LDA '$'               ; probably should have been #'$'
  JSR PrintChar

  LDA #VAL              ; if VAL is a memory location, this is wrong
  JSR PrintByte

Registers being changed by called routines

A common error is to call a subroutine and forget that it changes some registers containing values that you subsequently use. I try to always document what registers are changed and try to preserve all registers in code unless I need to optimize them for efficiency.

Conflicts in memory locations (usually page zero)

This is a common error, especially when calling other code that might use memory locations used by your code.

Branching to the wrong part of loop

It is easy to branch to the wrong location in a loop. How many times does the code below run through the loop?

loop: LDX #10
      BNE loop

I recently had an error like this (but a little more subtle):

        JSR GetKey              ; Get character from keyboard
        CMP #CR                 ; key pressed?
        BEQ EnterPressed        ; If so, handle it
        CMP #ESC                ; key pressed?
        BEQ EscapePressed       ; If so, handle it
        JSR PrintChar           ; Echo the key pressed
        STA IN+1,X              ; Store character in buffer (skip first length byte)
        INX                     ; Advance index into buffer
        CPX #$7E                ; Buffer full?
        BEQ EnterPressed        ; If so, return as if was pressed
        BNE loop                ; Always taken

You can ignore most of what the code is doing. The problem is the last instruction. I meant to branch to label "loop1" but I branched to "loop", which was in a completely different routine (so it assembled okay). In part it happened because I copied and pasted the code from a similar routine and then changed it but left the wrong label. The program ran but the behaviour left me scratching my head and I had to single step through the code before I realized the problem.

Here are some additional errors which in my experience are less common but you should watch out for:

Pushing data on the stack and not correctly restoring it

A particularly bad error is pushing data on the stack and then doing an RTS, which will cause your code to return to the wrong address.

Leaving the CPU in decimal mode

If you use decimal mode, be sure it set it back to binary mode when done.

Jump indirect across page boundary

The 6502 has a bug when using jump indirect across a page boundary. The 65C02 CPU fixes this.

There are many good books on 6502 programming, some of which are freely available on the Internet on sites like http://www.6502.org.

A good reference on 6502 hardware and software advice that was recently posted on the 6502.org forum is here.

No comments: