|
|
This post was updated on .
I am proud to present my solo hobby project NAND. This year-long undertaking follows the completed Nand to Tetris course, but ported to the web with its own runtime, user interface, and IDE. You can try out some example programs I wrote in Jack on NAND such as 2048, a genetic algorithm, and a manual stack overflow to corrupt the screen.
Check out NAND at https://nand.arhan.sh and its repository at https://github.com/ArhanChaudhary/NANDAdditionally, I've authored an extensive writeup about the project. Read about it on the README.
|
|
Wow, great work! Really well done!
I see that you are also using compressed constants for the charset to reduce the size of Output - so I'm not the only one doing this :-) In addition to packing bits of multiple characters into a constant, I am using a simple RLE encoding. Depending on the type of encoding, I ended up with 285 or 315 constants for the whole charset (one version for "normal" translation and one version which works better with threaded code. If I see it correctly, you are using 96*4=384 constants instead of the 96*12=1152 constants of the original implementation, which gives you already a significant reduction in size.
One thing I don't understand: why are you poking a value into the Keyboard I/O address on Sys.halt? I guess that is for debugging purposes, but shouldn't that address be read-only by default (and consequently that RAM address shows still 0 even after the halt)?
Other than that: I also used green on black for my own emulator as default, though you have to be aware that it implicitly inverses the Hack specification. I only realized that with the Chess program, where the board and piece colors were flipped... Perhaps you could add a color select option.
And finally: if there was an option to load ASM or BIN code directly into the emulator, this would become my favorite nand2tetris IDE ;-)
|
|
I'm poking the value 32767 into the Keyboard address on Sys.halt as a poor-man's flag to tell the runtime when to stop. The runtime polls for this value at this memory address, and I didn't want to introduce another memory address to remain spec-compliant. After the halt, it's instantly set back to 0 when this flag is enabled to reset it.
I'm not sure if a color selector is really needed, but I could definitely look into an ASM or BIN loader which would be trivially easy to implement in terms of code.
|
|
Also, woohoo for Nand to Tetris! My project was #1 on Hacker news for half a day and the repository has gained over 300 stars. I'm so grateful for this course and its opportunities
|
|
That is a marvelous emulator! Love it <3 Excellent job!
|
|
Awesome news!
I had a bit of free time to spare. I implemented exactly that into https://nand.arhan.sh/ — a JACK/VM/ASM/BIN loader, along with an option to clear the RAM.
:-)
|
|
Noticed the bug, and took a look at my code. I played around with it for a while, but for the life of me I couldn't get the corrected CPU to work without making it unnecessarily complicated. I haven't touched the code in half a year, so I was a bit disoriented trying to figure out how everything worked again. xD
For now, I'll put that bug fix on hold until I'm spontaneously motivated again or someone else helps
If anyone happens to be curious, here's what I've been trying to fix https://github.com/ArhanChaudhary/NAND/blob/main/src/core/architecture.rs#L30
|
|
NVM, I managed to fix it :D
|
|
I didn't have the time to work on a patch myself. Tested it just now and it works perfectly.
Great job!
|
|
Just found another program that doesn't work... After having a look at the code, I believe the issue is related to ADDRESS_M which is updated inside cpu before the a register is updated. Therefore the next read (of the following command) is still done with the old address. I'm actually surprised that this is working at all. Anyway, it is not possible to move the ADDRESS_M update behind the a register update either, because then a potential memory write would already go to the new A instead.
The most pragmatic solution could be to move the ADDRESS_M update completely out of cpu and inside computer() instead:
fn computer(reset: bool) {
let (out_m, load_m) = cpu(
memory(0, false, unsafe { ADDRESS_M }),
memory::rom32k(unsafe { memory::PC }),
reset,
);
memory(out_m, load_m, unsafe { ADDRESS_M });
// update address line for next instruction
unsafe {
ADDRESS_M = slice16_0to14(alu_y1);
}
}
I don't have an environment to test this code myself however...
|
|
This post was updated on .
Could you provide example assembly that doesn't work so I could debug this myself?
BTW, if you want to run the environment for yourself, execute
cargo install wasm-pack@0.13.0
npm i
npm run rwbuild
npm run dev
|
|
The bug seems to be more subtle than I thought, and it took me a while to narrow it down. It appears to be a combination of AM= and Keyboard access. This sample code will fail:
(LOOP)
@KBD
D=A
@THAT // just for demonstration, could be any address
0 // NOP, this doesn't help, so it is no timing issue
AM=D // FAILS!!!
0 // NOP, this doesn't help, so it is no timing issue
//
// This would work instead of AM=D
// M=D
// A=D
D=M // read KBD state
@LOOP
D;JEQ // loop until key pressed
// The program will exit the loop immediately with
// D==24576 here ?!
@END // halt
(END)
0;JMP
I couldn't reproduce it with any other address than KBD, so it seems it is somehow related to the keyboard access...
|
|
Thankfully the AM=D bug you pointed out was just a simple typo
pub fn keyboard(in_: u16, load: bool) -> u16 {
- if load {
+ if load && unsafe { CLOCK } {
unsafe {
PRESSED_KEY = in_;
}
In other news, your program inspired me to implement a step-by-step debugger into my emulator. After pausing a program during execution, click "Step" to execute one instruction at a time. I think it is a really useful feature 8)
If you don't see the feature immediately, the old version might be cached on your device; reload and it should work.
|
|
Very nice work. I have tested it with a couple of programs and they all work fine. Another idea for additional features: For my own emulator (Java-based, local) I recently added a profiler.
On the VM level, profiling is straightforward (and has been implemented here for example), but for ASM code it is a bit tricky on the Hack platform. Specifically it is not trivial to identify if a jump was actually a call (or a return) without relying on label naming. The standard way of tackling this is to add debug information to the binary, but I wanted to be able to profile other existing Hack binaries as well. One of the challenges is for example that the final jump of the call is not necessarily done from within the caller itself, but from a global call handler instead. In the end my profiler takes various data points into account and detects the call structure quite reliably. A simple example output with the Pong.asm from project 06 is shown below (not pressing any key and just waiting until Game Over and Sys.halt has been reached).
Alternatively the output can be shown as a tree:
Please note, that the official Pong.asm doesn't follow the naming conventions and has all class names in lower case. That asm also contains loop labels which are valid function names and could potentially cause naming conflicts (another challenge for the profiler...).
|
|
A general purpose hack profiler is a very interesting idea. I doubt I could ever figure out how to profile raw binaries. But you since you already mentioned debug info, I wanted to point out that NAND’s assembler already encodes debug info in its output to map the PC to the VM instruction in the ROM memory view. It doesn’t look like an assembly profiler for my assembler specifically would be too hard, but again an all purpose assembly profiler is a huge technical challenge.
Mostly likely, I won’t work on this for some time to focus on other technical projects. I’m definitely putting this in the backlog though.
|
|