Sunday 31 July 2016

What and why...

This page is somewhere to document all the stages of the development of a discrete-logic processor.

I'll discuss the basics of a microprocessor; simulate it as I go along in Logisim (Logisim); and develop the hardware using the free version of Eagle CAD (Eagle CAD). Eventually there will also have to be some sort of assembler for it - probably in Python, since I need the practice. As things get developed they'll be available for download; I encourage you to experiment, modify, extend... it's the best way to learn. And since everything is done using free tools, until you actually want to build one, it costs nothing.

I'm a Linux user by choice, but all the tools mentioned are available for Windows, should you prefer it.

The Basics

This is probably obvious, but it's perhaps worth a brief overview of what a microprocessor is. It's remarkably simple, in concept: a processor is a device which takes a sequence of instructions and data and manipulates the data according to the instructions. At its simplest, it takes a value from some sort of memory and puts it somewhere else in memory. A slightly more complex operation does some logical or arithmetical process on the value before it saves it back to the memory. Finally, there is some mechanism to allow the processor to perform jumps - to let it make loops.

To operate the processor, a number of things have to happen in the right order.
  • the processor has to get the next instruction from memory.
  • it has to decode this instruction to decide what to do next.
  • it has to execute the decoded instruction - this may require further access of the memory, either reading, writing, or both
  • it then gets the next instruction and repeats.
Strictly speaking, any process can be executed with the simplest of processor. However, there are certain things which can make it simpler to write a program, if it's in any way complex. With this in mind, let's have a look at what this processor ought to be able to do.

Memory size

For simplicity, we'll keep this as an eight-bit processor. That is, the access width for internal registers and for external memory is eight bits wide. We'll make the memory itself a maximum size of 64k bytes - which needs sixteen bits of address to specify any particular memory location. This is the same size as most of the popular processors in the 1970s and 1980s; our processor should have similar abilities to those.

Programmer's model

I propose a simple model which uses an accumulator A as the source or destination for the majority of instructions. Two internal registers - X and Y may be used either for short term storage (e.g. loop counters) or together as a sixteen bit pointer to memory. A sixteen bit stack pointer allows local storage and the delivery and return of parameters to subroutines.
A number of memory access modes are available:
  • immediate - where the second byte of the instruction is an eight-bit data byte.
  • absolute - the second and third bytes of the instruction are the address of the data byte being referred to.
  • indexed - a little more complex; the second and third bytes of the instruction point to an address in memory. At that address and the following one is a pointer to the actual memory location with the data required.
  • XY - similar to the above, but the XY register pair points to the memory location.
  • stack - the stack pointer register pair is again similar, but with an added twist: after a write instruction to the stack, the pointer is automatically decremented. Before a read instruction, the pointer is automatically incremented.

Logic and arithmetic

To do anything useful, the processor needs to be able to do a minimum of arithmetic and logical instructions. While it is possible to make all the possible options using nothing more than NAND instructions it's less than simple - better to have all that likely to be needed to hand, so here's a short list of the functions I'd like to include:
  • adc - add with carry. It can be argued that add without carry is more useful, since you don't need to clear the carry before an addition, but it's a lot more simple for multi-byte operations if the carry is include. So adc it is...
  • sbc - for the same reasons.
  • inc - just add one...
  • dec - or take one away...
  • and - logical and.
  • or - logical or.
  • xor - logical exclusive or.
  • shr - shift the bits in the target right by one, loading the carry into the top bit and shifting the lowest bit into the carry. This is useful for arithmetic division as well as for serial/parallel conversion.
  • shl - as above but shifting left; the carry goes into bit zero and is loaded from bit seven. Useful for multiplication.
The observant will have noticed that this is nine instructions... in the world of computing, we do rather like powers of two; eight is rather nice. However, I have a sneaky approach I'll discuss later to manage the shl instruction.


Jumps and calls and things

The other thing a processor needs to do is jump around. If you can't change the flow through a program depending on the results of previous processing, you can't really do much at all.
Here's what I'd like to include:
  • jmp - jump to an absolute address.
  • jc - jump if the carry flag is set.
  • jnc - jump if the carry flag is clear.
  • jz - jump if the zero flag is set.
  • jnz - jump if the zero flag is clear.
  • jsr - jump to a subroutine, pushing the current program counter on the stack before the jump occurs.
  • ret - jump back from a subroutine, using the previously stored address.
I have no doubt that I will be considering other instructions later. I'm looking for a minimal instruction set, but I'm not wedded to it. If I can add useful instructions without too much hassle - and it's possible some will drop out in the implementation of the decoding logic - then I will.