Nibbler Software Tools
During development of the Nibbler CPU, I’ve created several software tools to help make my life easier. Some of these have been nearly as interesting as the CPU itself! While professionally I work with C++ or Java, for my personal projects I find the .NET languages like C# to be most convenient. They include a robust set of libraries for common data structures and I/O, and also make it a snap to build a GUI. To create my tools I use the free Visual Studio Express IDE, running on Windows. There are plenty of other development options like Eclipse and the GNU tools, but I’m already familiar with the Visual Studio IDE, and the $0.00 price is hard to beat!
Microcode Assembler
The simplest tool is the Microcode Assembler, which doesn’t actually assemble anything, but instead generates two binary files for the contents of the two microcode ROMs. The program is written in the version of C++ that uses managed code and the .NET foundation. I’ve never been entirely clear what the proper name for it is: Managed C++, or Visual C++, or C++/CLI?
In the beginning I intended to describe the microcode operations in a text file, using register transfer language, and then the Microcode Assembler would assemble it into binary. But when it became clear the microcode would be fairly simple, I dropped the idea of using RTL, and just wrote some C++ code to directly generate the required binary data. Using the microcode table from my previous post, this was a quick task. The program contains lots of bit-shifting fun like:
// 9 JNE for (int c = 0; c < 2; c++) { microcode[(9 << 3) | (c << 2) | (0 << 1) | 1] = 0x383F; // NE microcode[(9 << 3) | (c << 2) | (1 << 1) | 1] = 0xF83F; // E }
Whee! Those hexadecimal numbers are just rows from the microcode table, with the 16 control signals expressed as a 4-nibble hex value. Nothing very exciting here, but it gets the job done.
Assembler
I needed some way to write programs for the CPU, using symbolic instruction names and branch labels, instead of stringing together opcode values by hand. Woz supposedly hand-assembled most of the Apple II ROM routines, which is damn impressive, but not really something I wanted to duplicate.
I considered taking an existing open source assembler and modifying it for my needs, but as it turned out, I already had a suitable assembler that I’d previously written for my Tiny CPU project. With a few hours of work, I was able to adapt that assembler for Nibbler’s purposes.
Unlike the other tools, the assembler is written in vanilla C++, and runs as a command line program. It takes a single .asm file as input, and assembles the code into a .bin binary output file. It also generates a .sym symbol file, containing the values of all the labels and constants in the code, as well as the assembled address of each line of code. The symbol file is used later by the simulator, in order to perform source-level debugging.
The assembler doesn’t have any fancy features like macros or conditional compilation, but it does support:
- Decimal constants
- Hex constants, preceded by $
- Character constants, contained in ‘ ‘ single quotes
- < and > operators to extract the high or low nibble of a byte constant
- Named constants using #define (both data and address constants)
- Named labels, and jumping to a named label
- Unnamed + and – labels, for jumping forward and backward
- Comments, starting with ;
Here’s a snippet from an example .asm file, showing the kind of code the assembler can handle. This snippet watches the input buttons, and writes a character to the LCD whenever a button transition occurs.
; example.asm ; memory locations #define PREV_BUTTON_STATE $00D #define NEW_BUTTON_STATE $00C #define RETURN $00F #define LCD_OUT_H $000 #define LCD_OUT_L $001 ; buttons #define BUTTON_LEFT $1 #define BUTTON_NOT_LEFT $E begin: ; reset the button state lit #$F st PREV_BUTTON_STATE print_button_changes: ; wait for a button state change - in #0 cmpm PREV_BUTTON_STATE je - ; save the new state, and determine what changed st NEW_BUTTON_STATE check_left_pressed: ; is left button currently pressed? lit #BUTTON_NOT_LEFT norm NEW_BUTTON_STATE cmpi #BUTTON_LEFT jne check_left_released ; was it previously unpressed? lit #BUTTON_NOT_LEFT norm PREV_BUTTON_STATE cmpi #BUTTON_LEFT je check_right_pressed ; left button changed from unpressed to pressed: print an upper-case letter L lit #<'L' st LCD_OUT_H lit #>'L' st LCD_OUT_L jmp print_button_change check_left_released: ; was left button previously pressed? lit #BUTTON_NOT_LEFT norm PREV_BUTTON_STATE cmpi #BUTTON_LEFT jne check_right_pressed ; left button changed from pressed to unpressed: print a lower-case letter L lit #<'l' st LCD_OUT_H lit #>'l' st LCD_OUT_L jmp print_button_change check_right_pressed: ; ...handle the other buttons print_button_change: lit #1 st RETURN ; store 1 at RETURN, lcd_write uses this to know it should jump to next1 when it's finished jmp lcd_write ; writes the character at LCD_OUT_H, LCD_OUT_L to the LCD next1: ; update the previous button state to the new state ld NEW_BUTTON_STATE st PREV_BUTTON_STATE jmp print_button_changes
Simulator
The most complex tool by far is the machine simulator. Originally developed for BMOW, this GUI-based tool is written in Managed C++, and simulates the data and control paths of the CPU. It supports source level symbolic debugging, disassembly, microcode debugging, code breakpoints, data breakpoints, memory inspection, and I/O simulation of the LCD and input buttons. It’s not perfect, but it’s a sweet tool that makes diagnosing problems dramatically easier than it would be otherwise. And who doesn’t love simulation?
The simulator uses the assembled program binary, the program source and symbol files (if available), and the microcode ROM binaries. Because it uses the microcode to control the data paths, it needs no special knowledge of Nibbler’s instruction set in order to do the simulation: it’s just a dumb series of paths governed by control signals, same as the real hardware. But for the sake of code disassembly and debugging, there is some knowledge of the Nibbler instruction set built in.
In the image above, the simulator is doing source level debugging of a program similar to the previous button watcher example. Execution is currently stopped at the CMPM PREV_BUTTON_STATE
instruction, as shown by the yellow arrow. There’s a breakpoint two instructions further down, at the line with the red circle. If the simulator is started and a sim-button is pressed, the program will break out of the loop and stop at the breakpoint. Yahoo, interactive debugging!
From right to left, the simulator control buttons are:
- Run – Keep simulating until explicitly paused, or a breakpoint is hit
- Pause – Momentarily stop the simulation
- Microstep – Simulate one clock cycle, then pause
- Step – Simulate to the start of the next instruction, then pause
- Reset – Set the program counter to 0
The upper-left shows the current CPU state, and the contents of RAM. Below those is a 16×2 character LCD display, using a partial simulation of the LCD’s HD44780 controller module driven by the OUT0 and OUT1 ports.
At the bottom left is a microcode “disassembly” of the current instruction. In this case for the instruction CMPM, phase 0 uses the PC as an address into ROM, retrieving a byte which is stored in the Fetch register. Phase 0 also increments the PC. Phase 1 uses the ALU to calculate A minus RAM(addr), where the RAM address is formed from the immediate opcode value and the current program ROM byte, as described in previous posts. It also loads the C and E flag registers with the result of the ALU calculation, and increments the PC.
For all its capabilities, the simulator won’t catch every problem that might occur on the real hardware. It’s purely a dataflow simulator, and has no concept of timing at a finer resolution than one clock cycle. If there’s a race condition somewhere, or a timing requirement that isn’t met, the simulator won’t detect the problem. It’s also too forgiving of microcode errors that would break the real hardware, like enabling multiple sources to drive the data bus as the same time. But despite these shortcomings, I’ve found the simulator to be an incredibly helpful tool. And it’s a huge confidence boost to see everything running in the simulator, before building the actual hardware. 🙂
Read 3 comments and join the conversation
3 Comments so far
Leave a reply. For customer support issues, please use the Customer Support link instead of writing comments.
Any chance you will release the programs and code for this project?
For sure. The sources for all my previous projects are on their project summary pages, linked at the upper left of this page. Once the Nibbler design and tools have solidified, I’ll do that same for its sources.
Could you share simulator?