NandToTetris is a course which has you build a full computer from "scratch":
Logic gates -> Chips -> RAM -> CPU -> Computer -> Assembler -> Compiler -> OS -> Tetris
All this is done via software defined hardware emulation.
I'm building an emulator for this entire stack in C.
- No external dependencies (so far)
- Start with a single software defined NAND gate in C
- The first chips are built from this NAND gate
- Then these chips are used and combined to build even more chips
- Technically you could build EVERYTHING from just NAND gates, but that would take too long to code up
- When I complete this project, I may write a script that converts all my non NAND gate components to pure NAND gates
- Don't use certain programming utilities: boolean logic operators, bitwise logic operators etc
- Instead we leverage the gates/chips to implement such logic
- Build more and more base chips from the NAND gate
- Simple gates: OR, AND, NOT, XOR
- Simple chips: DMux, Mux
- 16 bit variants
For comparison, most emulator projects start right at the CPU level and don't sequentially build primitive structures
- They look at CPU truth table / Instruction set and implement that logic directly
- More straight forward, but you skip all the gates/chips fun.
A really simple way to start contributing would be to write some truth table tests for the logic gates, then move up to more complex components.
Please see docs/testing.md
to get started.
Heres an example test for a nand gate
#include <stdio.h>
#include "test-nand.h"
#include "nand.h"
#include "test-utils.h"
void test_nand(Test_Counter *counter)
{
// Truth table for NAND: expected outputs
const int expected[4] = {1, 1, 1, 0}; // outputs for (0,0), (0,1), (1,0), (1,1)
for (int i = 0; i < 4; i++)
{
Nand nand = {
.input.a = (i >> 1) & 1, // Gets first bit (0011)
.input.b = i & 1, // Gets second bit (0101)
};
nand_gate(&nand);
test_assert(nand.output.out == expected[i], counter,
"NAND(%d,%d) should be %d",
nand.input.a,
nand.input.b,
expected[i]);
}
}
Confused? Some code example may help.
Heres example code for my NAND gate:
/*
* | a | b | out |
* ---------------
* | 0 | 0 | 1 |
* | 0 | 1 | 1 |
* | 1 | 0 | 1 |
* | 1 | 1 | 0 |
*/
typedef struct Nand_Input
{
bool a;
bool b;
} Nand_Input;
typedef struct Nand_Output
{
bool out;
} Nand_Output;
typedef struct Nand
{
Nand_Input input;
Nand_Output output;
} Nand;
void nand_gate(Nand *nand_unit)
{
nand_unit->output.out = !(nand_unit->input.a & nand_unit->input.b);
}
From this gate I build a NOT gate (note, no boolean operators)
typedef struct Not_Input
{
bool in;
} Not_Input;
typedef struct Not_Output
{
bool out;
} Not_Output;
typedef struct Not
{
Not_Input input;
Not_Output output;
} Not;
void not_gate(Not *not_unit)
{
Nand nand_unit = {
.input.a = not_unit->input.in,
.input.b = not_unit->input.in,
};
nand_gate(&nand_unit);
not_unit->output.out = nand_unit.output.out;
}
Then OR / AND / XOR / MUX / DMUX ..... and their 16 bit versions.
Heres a more complex chip, a 8-way DMux
/*
* [a, b, c, d, e, f, g, h] = [in, 0, 0, 0, 0, 0, 0, 0] if sel = 000
* [0, in, 0, 0, 0, 0, 0, 0] if sel = 001
* [0, 0, in, 0, 0, 0, 0, 0] if sel = 010
* [0, 0, 0, in, 0, 0, 0, 0] if sel = 011
* [0, 0, 0, 0, in, 0, 0, 0] if sel = 100
* [0, 0, 0, 0, 0, in, 0, 0] if sel = 101
* [0, 0, 0, 0, 0, 0, in, 0] if sel = 110
* [0, 0, 0, 0, 0, 0, 0, in] if sel = 111
*/
typedef struct Dmux_8way_Input
{
bool in;
bool sel[3];
} Dmux_8way_Input;
typedef struct Dmux_8way_Output
{
bool a;
bool b;
bool c;
bool d;
bool e;
bool f;
bool g;
bool h;
} Dmux_8way_Output;
typedef struct Dmux_8way
{
Dmux_8way_Input input;
Dmux_8way_Output output;
} Dmux_8way;
void dmux_8way_chip(Dmux_8way *dmux_8way_unit)
{
Dmux dmux_unit;
Dmux_4way dmux_4way_unit_1, dmux_4way_unit_2;
// dmux
dmux_unit.input.in = dmux_8way_unit->input.in;
dmux_unit.input.sel = dmux_8way_unit->input.sel[2];
dmux_chip(&dmux_unit);
dmux_4way_unit_1.input.in = dmux_unit.output.a;
memcpy(dmux_4way_unit_1.input.sel, dmux_8way_unit->input.sel, sizeof(dmux_4way_unit_1.input.sel));
dmux_4way_chip(&dmux_4way_unit_1);
dmux_4way_unit_2.input.in = dmux_unit.output.b;
memcpy(dmux_4way_unit_2.input.sel, dmux_8way_unit->input.sel, sizeof(dmux_4way_unit_2.input.sel));
dmux_4way_chip(&dmux_4way_unit_2);
dmux_8way_unit->output.a = dmux_4way_unit_1.output.a;
dmux_8way_unit->output.b = dmux_4way_unit_1.output.b;
dmux_8way_unit->output.c = dmux_4way_unit_1.output.c;
dmux_8way_unit->output.d = dmux_4way_unit_1.output.d;
dmux_8way_unit->output.e = dmux_4way_unit_2.output.a;
dmux_8way_unit->output.f = dmux_4way_unit_2.output.b;
dmux_8way_unit->output.g = dmux_4way_unit_2.output.c;
dmux_8way_unit->output.h = dmux_4way_unit_2.output.d;
}
/*
* out = a if sel = 000
* b if sel = 001
* c if sel = 010
* d if sel = 011
* e if sel = 100
* f if sel = 101
* g if sel = 110
* h if sel = 111
*/
typedef struct Mux16_8way_Input
{
bool a[WORD_SIZE];
bool b[WORD_SIZE];
bool c[WORD_SIZE];
bool d[WORD_SIZE];
bool e[WORD_SIZE];
bool f[WORD_SIZE];
bool g[WORD_SIZE];
bool h[WORD_SIZE];
bool sel[3];
} Mux16_8way_Input;
typedef struct Mux16_8way_Output
{
bool out[WORD_SIZE];
} Mux16_8way_Output;
typedef struct Mux16_8way
{
Mux16_8way_Input input;
Mux16_8way_Output output;
} Mux16_8way;
void mux16_8way_chip(Mux16_8way *mux16_8way_unit)
{
Mux16_4way mux16_4way_unit_1, mux16_4way_unit_2;
Mux16 mux16_unit;
memcpy(mux16_4way_unit_1.input.sel, mux16_8way_unit->input.sel, sizeof(mux16_4way_unit_1.input.sel));
memcpy(mux16_4way_unit_1.input.a, mux16_8way_unit->input.a, sizeof(mux16_4way_unit_1.input.a));
memcpy(mux16_4way_unit_1.input.b, mux16_8way_unit->input.b, sizeof(mux16_4way_unit_1.input.b));
memcpy(mux16_4way_unit_1.input.c, mux16_8way_unit->input.c, sizeof(mux16_4way_unit_1.input.c));
memcpy(mux16_4way_unit_1.input.d, mux16_8way_unit->input.d, sizeof(mux16_4way_unit_1.input.d));
mux16_4way_chip(&mux16_4way_unit_1);
memcpy(mux16_4way_unit_2.input.sel, mux16_8way_unit->input.sel, sizeof(mux16_4way_unit_2.input.sel));
memcpy(mux16_4way_unit_2.input.a, mux16_8way_unit->input.e, sizeof(mux16_4way_unit_2.input.a));
memcpy(mux16_4way_unit_2.input.b, mux16_8way_unit->input.f, sizeof(mux16_4way_unit_2.input.b));
memcpy(mux16_4way_unit_2.input.c, mux16_8way_unit->input.g, sizeof(mux16_4way_unit_2.input.c));
memcpy(mux16_4way_unit_2.input.d, mux16_8way_unit->input.h, sizeof(mux16_4way_unit_2.input.d));
mux16_4way_chip(&mux16_4way_unit_2);
mux16_unit.input.sel = mux16_8way_unit->input.sel[2];
memcpy(mux16_unit.input.a, mux16_4way_unit_1.output.out, sizeof(mux16_unit.input.a));
memcpy(mux16_unit.input.b, mux16_4way_unit_2.output.out, sizeof(mux16_unit.input.b));
mux16_chip(&mux16_unit);
memcpy(mux16_8way_unit->output.out, mux16_unit.output.out, sizeof(mux16_8way_unit->output.out));
}
I have only started this project yesterday, so have completed 1 out of 7 hardware projects so far. I plan to build out the full PC this way, then begin work writing an Assembler in C.