Unit testing NES 6502 assembly using Common Lisp

,

As part of the NES homebrew development I’ve been working on, I wanted to have some way to unit test my code. Doing so provides peace of mind that code works, and also allows for future changes to made safely. This is particularly important for low-level assembly, where correctness and speed are both important, and attempts at optimization can easily break code. Not finding any pre-existing tools, I figured I could easily pull something together.

The first step was finding a 6502 / NES emulator that allowed programmatic inspection of state (memory / registers) from the command-line with minimal overhead. The project cl-6502 by redline6561 fit the bill, allowing tests to use Common Lisp code to setup state, run assembled code, and then make assertions on the final state.

Next, I needed to make a tiny framework to bridge between cl-6502’s features and how my NES code was organized. My project uses ca65 as its assembler, which is nice because it can assemble files separately, and also it matches the syntax of cl-6502’s assembler. On the other hand cl-6502, being quite minimalistic, doesn’t support directives, so we need to preprocess our source to filter these out. It also has a very granular interface, and we’d prefer something a little higher-level, at the level of subroutines.

Since it’s safe to assume most homebrew developers aren’t familiar with Common Lisp, I wanted to provide a very simple and obvious API for the unit tests. The result, nes_unit_testing.lisp, favors convention over configuration, making a number of assumptions about how subroutines are written. For example, unit test filenames match the assembly source except they end in “_test.lisp”. Also, the test framework starts executing at the very beginning of the source file, meaning only one subroutine can exist per file.

Provided is a simple calculator ROM, that enables the input of two numbers, and can add them together. As part of this, there’s a file convert_decimal.asm which contains a function to convert an integer into 3 decimal digits. This is the documentation of the subroutine:

convert_decimal.asm

;ConvertDecimal
; Given a number in A, convert it to three decimal digits, in Y, X, A.
; .reg:a @in  The number to convert.
; .reg:a @out The ones digit, 0-9.
; .reg:x @out The tens digit, 0-9.
; .reg:y @out The hundreds digit, 0-2.
.proc ConvertDecimal
...
rts
.endproc

Complete code is here.

And the complete test code, which verifies every possible 8-bit value, looks like this:

convert_decimal_test.lisp

(in-package :nes-unit-testing)

(deftest convert-decimal-test
  (initialize-test-case :env `((remainder-mod8 :byte)))
  (loop for num from 0 upto 255
     do (run-test-case :a num)
        (expect-result :y (floor (/ num 100))
                       :x (mod (floor (/ num 10)) 10)
                       :a (mod num 10))))

Most of what’s happening here should be self-explanatory, but just in case, here’s a line by line breakdown.

Lines 1-3 set our package and start defining a test.

Line 4 initializes the test by parsing the source code, clearing ram, and defining variables our subroutine uses.

Line 5 performs a loop, iterating from 0 to 255, inclusive.

Line 6 assigns the number to the A register, then executes the subroutine.

Lines 7-9 verify that the Y register is set to the hundreds digit, the X register is set to the tens digit, and the A register is set to the ones digit.

Running the unit test is simple, once all the dependencies are installed. The included script ./run_tests.sh loads cl-6502, nes_unit_testing.lisp, and then runs all-tests-in-current-directory which loads every *_test.lisp script in the current directory and runs all the defined tests. Here’s what running the test looks like, with timing:

> time ./run_tests.sh
Success!

real 0m1.828s
user 0m1.468s
sys 0m0.249s

Even failures are pleasant. Removing line 57 from convert_decmal.asm (adc #$0a) breaks the code as shown:

> ./run_tests.sh
Failed on test: ((A 88))
A - Actual 254, Expect 8

Failed on test: ((A 89))
A - Actual 255, Expect 9

Failed on test: ((A 96))
A - Actual 252, Expect 6
...

Code for the calculator rom is on github here, and code for the unit testing framework is here.


7 responses to “Unit testing NES 6502 assembly using Common Lisp”

  1. Lars Brinkhoff Avatar
    Lars Brinkhoff

    You don’t need PROGN after the DO.

    1. dustmop Avatar
      dustmop

      Whoops! You’re completely right, I’ve edited the code snippet.

  2. Mateus Avatar
    Mateus

    Hi there, i always wanted to develop a NES game, i have already programmed some games with c++/OpenGl.But, i have a Mac, and i never found any useful tools for NES development on OS X , i was wondering if you could give me some advice.
    Thank you very much and sorry for the english.

    1. dustmop Avatar

      I also use OSX, though there are no dev tools aside from standard text editors. Check out http://nintendoage.com/pub/faq/NA/index.html?load=nerdy_nights_out.html for a good tutorial, but be aware that you’ll need to use assembly, C++ won’t cut it!

      1. Mateus Avatar
        Mateus

        Thank you very very much!
        So you write a assembly source file on a text editor, but to compile everything in a .nes executable don’t you need some tools ?
        I’m sorry for bothering you, but now that i know that you use a Mac, you are basically the only person i found that can help me, it really is a old dream of mine to write a nes game.
        PS:Thank you for pointing me in the right direction, i’ll start reading the tutorial right away.

        1. dustmop Avatar

          Correct! Check out https://github.com/dustmop/calculator, it contains a very simple NES rom, with full source and build instructions. You’ll need to install ca65 – http://cc65.github.io/cc65/getting-started.html.

          1. Mateus Avatar
            Mateus

            WOW it worked! Thank you so much, now i believe i have everything i need to get started , again thank you, you really really helped me.

Leave a Reply

Your email address will not be published. Required fields are marked *