Today is the first day I got unit tests working for the CPU in Digital, so I think I'm finally confident enough to share a status update. Although it's been really fun to play around with the Digital toolkit it's always nervewracking to get a circuit working, and then be too afraid to modify it. You might start building other signals around the signals you don't understand. Soon enough you're scribbling arcane boolean nonsense into your diary just to keep track of any signal at all:
June 17: "some comments on pin inputs" D_RCK is pulse D_CCK is OR(AND(OR(p4, RAM), clk, pulse) and(~clk, pulse)) R_U/~D is ~D_U/~D
Surprisingly, none of this was correct in the end! Unit tests can solve this problem of letting you refactor ugly circuits while confirming the result of the circuit is correct. Digital's internal "test" component isn't the right model for a CPU-scale simulation, but instead, they provide an the emulated CPU that in its source tree that shows how to properly build a remote test harness. The Digital example processor has an accompanying Assembler that has an example of a TCP client that can control a running instance of Digital, and step through each clock cycle or whenever a BREAK occurs to walk through a circuit.
The only thing missing from this protocol in order to run unit tests is a way to tap into Digital's "measurement" system, which allows you to display signals in real-time as the circuit runs (cool!) and track them in a dedicated "Measurement" pane. I pitched a "measure" command for the TCP connection and added a command like this for a fork on my own Github.
I am experimenting with writing Python code to generate .hex files (used to load the program ROM) and also to test the circuit. Here is a test file "t01.py" that writes out a temporary binary, then loads it via the TCP interface "debug()", then asserts values of specific signals at a given clock cycle:
from tools import * start() push_literal(0xcafe) push_literal(0x0010) store() push_literal(0x0010) load() drop() push_literal(0x0000) jump_if_0(0x0000) push_literal(0x0001) jump_if_0(0x0000) debug() step_until(PC=0x3) validate(tos_bus=0xCAFE) step_until(PC=0xc) validate(TOS=0xCAFE) print('success')
By running t01.py and several other tests, it's quick to validate whether a circuit change makes a consequential impact on the circuit. One example is that this makes it easy to refactor excess gate logic in a 74xx-based simulation and confirm that with fewer spare gates, your circuit operation isn't impacted. Reducing the total number of spare NOT and AND gates in signal logic has been easy to do with this setup in place.