I decided to stop at this point and declare the initial version of the microcode complete (although the machine has not been simulated yet and the microcode is still untested). I have not yet completed faults and interrupts microcode, because I don’t know how I am going to do user/supervisor mode context switching and which approach I will choose for my memory subsystem organization for process memory isolation. Two options I am considering here are – implement some sort of paging (more sophisticated but allowing for real “virtual memory” with small chunks), or add to the design a physical memory offset set by the supervisor by a dedicated instruction and stored in some dedicated register (physical memory address would then always be physical offset plus logical address – simple approach but obviously memory wasting).
Before I make my decision and go any further, I want to simulate the whole thing with the following assumptions:
- run only in supervisor mode
- run with no faults/interrupts
- run with no memory subsystem (logical address becomes a physical address, with some necessary bit extension)
My goal now is to write a hardware level simulator, perform basic computations and run a test suite for the current instruction set (of course, I need to write or port some sort of assembler for this). When (if?) I succeed, I will return to the memory subsystem design and context switching, and add it to the simulator (along with necessary microcode).
Okay, here is what we have now:
Instruction Set
I currently have 217 finished instructions, including loads, stores, arithmetic and logic operations, compares, and branches. From the addressing modes I initially planned to implement, I decided to skip direct and indirect addressing, with absolute memory address as one of the operands. Not because they required some extra effort (they are computable in current design) but because I think they will not be too useful. The same can be achieved by register direct, register indirect and register indirect with offset operations. The opcode space is limited and I only have 39 opcodes left. Most of instructions are 16-bit operations although I also added their 8-bit counterparts for some addressing modes. I added a downloads section to this site, where I will always store the most up-to-date version, which should be considered a single source of truth for the machine’s instruction set. The current version is here.
Microcode Assembler
The microcode assembler is a C++ application. Its job is to parse microcode listing in a text form and output an array of bits to an Intel HEX file. I tried to make the assembler bulletproof, and some of its features saved me from making some silly microcode mistakes which would be later difficult to hunt. The source code of the microcode assembler may be found in the downloads page, too. It is an Apple’s XCode project but the code is pure C++, so it should compile everywhere. Technically, the application is a simple lexical analyzer, and a parser with code generator. I am not using lex or yacc here. The source syntax is best explained by providing an example, so here it is for the instruction ADC A, X (add with carry register X to register A, store result in A):
// ADC A, Y op($BC) 0, *, MDR <- Y 1, C, A <- A + MDR + 1; SETFLAGS_WORD; fetch 1, !C, A <- A + MDR; SETFLAGS_WORD; fetch endop
Instructions are enclosed in op and endop keywords, with op holding an actual opcode ($00-$10f). Each line starts with a step (cycle) number followed by a comma, and the expression for flag values. Here, an asterisk means that the step applies to all flag combinations, C means that the step is valid when carry flag is set, !C when it is not set. Other flags (Z, N, V) may be used in the same manner, e.g. C!ZN is also a valid flags expression (although I don’t think I would ever need to use expressions of such complexity). Following the flags is a set of microinstructions, delimited by semicolons. Data transfers and ALU operations are expressed by <- operator. Apart from transfers, other microops are possible, like SETFLAGS or FETCH. There are also register increment and decrement operations, for registers capable of behaving so without the ALU. Here is another, lengthier example for a register indirect with 16-bit offset load operation that takes 6 clock cycles to compute:
// LD A, (SP:#i16) op($25) 0, *, HI(MDR) <- MEM(PC); CODE; PC++ 1, *, LO(MDR) <- MEM(PC); CODE; PC++ 2, *, MAR <- MDR + SP 3, *, HI(A) <- MEM(MAR); MAR++ 4, *, LO(A) <- MEM(MAR); 5, *, fetch endop
In the download section there is a full source code of microcode as it stands today.
Microcode Word
The microcode word resulting from compilation is currently 4 bytes long and I hope to keep it this way. Four bytes gives me 32 microcode word bits of which I am using 29. Three bits are left for things related to faults/interrupts, and the memory subsystem. That’s not too much so in order to keep the microcode word 4 bytes long, I will probably need to optimize the field layout sometime soon. Here’s the current state:
Field | Length | Values | Description |
LBUS | 2 | MDR, A, X, Y | Left bus drive enable register select |
RBUS | 2 | MDR, MSW, ABUS, not_connected | Right bus drive enable register select |
ADRBUS | 2 | MAR, SP, DP, PC | Address bus drive enable register select |
BUSIFCMODE | 2 | MEM2ALULO, MEM2ALUHI, ALULO2MEM, ALUHI2MEM | Bus interface direction and mode |
BUSIFCEN | 1 | enable, disable | Bus interface enable signal |
LOAD | 4 | MDRLO, MDRHI, MDR, ALO, AHI, A, X, Y, MEM, MARLO, MARHI, MAR, SP, DP, PC, MSW | Load enable register select |
LOAD_IR | 1 | enable, disable | Load IR register signal |
ALUOP | 4 | as in 74LS181 function input | ALU function |
ALUMODE | 1 | ARITHMETIC, LOGIC | ALU mode |
ALUCARRYIN | 1 | enable, disable | ALU carry input enable signal |
ALUSHR | 1 | enable, disable | ALU right shifter enable signal |
LOADFLAGS | 2 | WORD, HI_BYTE, LO_BYTE, disable | Flags latch mode |
INCPC | 1 | enable, disable | PC++ enable signal |
INCMAR | 1 | enable, disable | MAR++ enable signal |
INCSP | 1 | enable, disable | SP++ enable signal |
DECSP | 1 | enable, disable | SP–– enable signal |
MEMSEG | 1 | CODE, DATA | Memory segment selection |
SUPERVISOR | 1 | enable, disable | Privileged (supervisor) instruction signal |
not connected | 3 | not connected | not connected |
One thing that worries me already is that I have already used all load destinations in 4-bit load field. I am pretty sure now that in order to implement supervisor-user mode context switching I will need yet another scratch register to store the supervisor stack pointer somehow (user mode programs may have their stack pointers set wherever in 64-kbyte data region). That’s one additional bit. Also, I need a bit to enable and disable interrupts from the microcode. That’s two, leaving me with one bit for memory subsystem microcode. Not enough. Some crunching will definitely be necessary if I want to avoid adding another byte (and chip) to the microcode store.