Writing "Hello, world!" from scratch part I: making a new ISA

“If you wish to make an apple pie from scratch, you must first invent the universe.”

― Carl Sagan

What is an ISA?

An instruction set architecture[1], or ISA, describes the instruction set of a processor and its behavior. This is not enough to know how a processor works but this is the information needed to operate it.

1: https://en.wikipedia.org/wiki/Instruction_set_architecture

I made my own ISA named Reflet which you can check out on the Reflet GitHub repository[2].

2: https://github.com/Arkaeriit/reflet

Designing a new ISA

Wishlist

Why would one design a new ISA? With RISC-V and MIPS being open ISA with great support, there is not much point in doing so. Personally, I mostly did this for fun and artistic aspiration. I had a few ideas trotting in my head about what a fun ISA could be. I did want wanted to make a simple ISA, but rather a family of ISA for 8-bit, 16-bit, 32-bit, 64-bit, or even more bit-having processors that all share the same instruction set. At first, I even wanted to be able to make a processor with exotic word sizes but I scrapped that idea. I also wanted a Von Newman architecture and having instructions being a single indexable amount of memory. Those three needs combined meant that I needed to have instruction on only 8 bits. This is quite low, even some 8-bit computers use 16 bits instructions.

On the other hand, I really liked the idea of instructions being 8 bits wide. This meant that I would probably end up with every possible byte being a valid instruction (and this ended up being the case). I find it quite beautiful that any binary file would be a valid program in my instruction set.

Another thing I wanted is to have a lot of general-purpose registers. I really like the idea of running a processor only from its ROM and registers, without using any RAM, like the HP nanoprocessor[3]. This need does not mix well with having 8-bits wide instruction as indexing this register takes space.

3: https://www.righto.com/2020/09/inside-hp-nanoprocessor-high-speed.html

Solutions

To solve those issues, I needed to have an accumulator-like register. Indeed, had I used only general-purpose registers, when using two registers in each instruction, I would have needed to use less than 3 bits to access each register. This would have meant having less than 8 general-purpose registers. I could have had different categories of registers that are addressed differently but I really did not like this idea.

The solution is to have an accumulator-like register that is implicitly used by (almost) all instruction. Some instructions have a 4-bit opcode and a 4-bit operand that let me index 16 registers. Those instructions use both the working register (name of my accumulator-like register) and another register. Some other instructions only have an 8-bit opcode and don't interact with other registers than the working register. In the end, I ended up with the following instruction set:

One could notice that if I needed to squeeze a bit more instruction, I could. For example, I could replace the `slp` instruction with `read WR` or `cpy WR` which are two instructions that do not change the state of the processor. Furthermore, I could fuse the `debug`, `quit`, and maybe some other instructions with a single instruction that uses the state of the working register to control its behavior. I might do that in the future if I feel like the instruction set needs more instructions.

As far as registers go, I have the working registers, a status register, a stack pointer, and a program counter as special registers. This left 12 general-purpose registers which is quite handy. As far as the status register go, as I want the behavior to be very similar with all word size, only the 8 LSB are used for status.

The complete (but still very small) documentation is available on the Reflet GitHub repository[4].

4: https://github.com/Arkaeriit/reflet

Simulator

Before writing the hardware description for my processor, I made a simulator[5]. The point of the simulator is to test easily if programs I compiled to Reflet machine code work.

5: https://github.com/Arkaeriit/reflet/tree/master/simulator

The simulator can only do very simple I/O, reading chars from stdin one at a time and writing chars to stdout one at a time. Fortunately, this is just enough to write a *Hello, world!* program. Outputting character works by writing the desired character to at the address 0x1 and then, writing 0 to the address 0x0. Reading characters works in quite a similar way.

The simulator also has some monitoring capacities to help in the debugging process and can simulate hardware interrupts.

Assembler

Writing machine code by hand is doable, especially with an ISA as simple as Reflet. Indeed, the opcodes being either 4 or 8 bits, they are quite easy to read or write in hexadecimal. But for our sanity's sake and to use some labels and macro, I made an assembler/linker[6] that converts the assembly language into Reflet machine code.

6: https://github.com/Arkaeriit/reflet/tree/master/assembler

This assembler is not very well written, it needs some optimizations and it does not work with proper tokenizer-parser, instead, it works with a *Let's read each lines*-*I will wing it* technology stack.

Nevertheless, as writing macros for it is quite easy and it comes with very convenient default macros, it is perfectly usable as an assembler.

Hello world

Now that we have both an assembler and a simulator, we can write and execute a program. Let's write an *Hello, world!*:

We can observe that the program does not make any assumption regarding the word size of the processor. Thus, we can compile it and run it for a Reflet processor of any word size.

Let's compile it and run it on an 8-bit simulated processor:

Yay! It works. Let's have a look at the generated machine code:

At first, it doesn't look like much but by taking a closer look, we can read the machine code. For example, we can see that the function `putc` starts at address 98 and we can read the instruction we rote in assembly. 0x11 is `set 1`, 0x34 is `cpy R4`, 0x03 is `tbm`...

Conclusion

Now that we have designed the ISA and that we have made tools to write machine code for it, the next step is to implement it in a hardware description language such as Verilog.

Back to homepage