8-bit Computer: ALU

With an "A" and "B" register in place for my 8-bit computer, I need to implement an Arithmetic Logic (ALU).  The ALU is the part of the computer where the interesting stuff happens - data is manipulated in some way, i.e. where the computer actually COMPUTES!  Ben Eater uses 2 x 74LS283 4-bit addition ICs in his ALU to perform 8-bit addition of an A and B register, and uses a control signal and some glue logic to implement subtraction by way of 2s complement addition.  Flags are then set based on the outcome of the ALU, and the program can branch (or not).  I considered a similar implementation with 74LS283 chips, but also adding some logic gate, i.e. an 8-bit NAND or NOR function to construct different logic functions.  The outputs from the addition/subtraction section and the NAND/NOR logic section would then need to be multiplexed to select the output of interest and latched into the flag register which I decided not to have the appetite for.

ALU design

On reflection I decided to keep my ALU simple and cheat (kinda) by using 2 x 74LS181, 4-bit ALU/function generators.  The chips generate 16 logic functions (wikipedia), including addition and subtraction and so are an efficient use of space for a breadboard ALU.  Four inputs are used to select one of 16 functions.  In addition a further input selects between logic and arithmetic mode, and a carry-in bit can be specified.  In total 6 control signals are required therefore for input into the ALU.  A further two control signals will be required to specify the ALU-output to the databus and latching the ALU-flags into the flag register.  Ultimately the control signals will be outputs from a panel of EEPROMs and the more control signals I have, the more EEPROMs I will need, and the more of a pain in the a** it will be to program the microcode across them all.  Therefore I am mindful to try and be as efficient as possible with control signals, for example I may use a decoding system for the databus outputs, as only one chip can output onto the databus at once.  In this case, I am using the lower 4-bits of the instruction register, and the carry-bit from the flag register, as a lookup into an EEPROM on the ALU to set the 4 control signals, the mode signal and the carry-in bit.  Therefore I have eliminated the need for 6 direct control signals in the ALU!  The 74LS181-based ALU is shown below, fed by registers A and B directly, and the 6 control signals set by the EEPROM (AT28C16).



I'm addressing the Atmel AT28C16 EEPROM chip using the lower 4-bits of the instruction and the carry bit, to form a 5-bit address.  The address looks up the output byte that then correctly sets S0-3, M and Carry-in on the 74LS181 chips, using the following table:


I wrote a python script (ALU_rom.py on github) which generates ALU.py.  I then hooked up my micropython pyboard (v1.0) to my EEPROM chip and used it as a EEPROM programmer by running main.py (imports ALU.py previously generated).  After writing the EEPROM into the ALU, and entering data into A and B, and setting the instruction 4-bits manually, all seems to be working as planned!

FLAG register

The purpose of the flag register, is primary to set signals high or low based on the output of the ALU, such that program branching can occur conditionally.  After some thought, I have decided to implement a 4-bit flag register to monitor:
  • Zero flag (Z). Set true when the ALU output is equal to 0.  This can be used for implementing JZ (jump if zero) and JNZ (jump if not zero) instructions.
  • Carry flag (C). Note that the carry bit is actually inverted because of the way the 75LS181 operates, so a Carry = High means no carry bit set, and a Carry = Low means a carry bit is set.  I will probably write the microcode to obscure this from the programmer though as it is a little confusing.
  • Negative (N) = Useful for negative number arithmetic, and indicates whether the MSB of the ALU output is high or low.  At the moment I haven't thought too much about implementing negative numbers, but this flag will probably be useful when I do.
  • There is no fourth flag! I reserve the option to add something later...  
The 4-bits of the FLAG register can either be set from the flag values derived from the ALU or imported from the data bus, by use of a multiplex 74LS157 (quad 2-line to 1-line data selector/multiplexer).  After the multiplex, the values are latched with a 74LS173 D-type flip-flop.  The register can also be put onto the data bus with the octal bus transceiver 74LS245.   This ability to store and restore the FLAG register (from the stack for example) will be useful if I implement interrupts later.  The remaining 4 bits higher of the FLAG register can be used for holding other useful status bits, such as whether there is a UART character in the RX buffer.  Note that 8-bits of the flag register go onto the data bus, so that the higher bits in particular can be examined and acted on, i.e. imagine we CALL checkRX:

checkRX:
MOV A, FLAGS    ; Move FLAGS register into A
AND (1<<RX_WAITING)    ; Test the RX_WAITING bit for UART RX status 
JNZ getRXchar          ; If there is a bit, jump to getRXchar
; No character carry on doing stuff
.
.
ret

getRXchar:
; Got a character, handle that here
ret

Combining Z and C flag for conditional jumps
I plan to implement a "Compare" (CMP) instruction to evaluate whether two values are equal, less than, or greater than, etc.  Internally, this will be implemented by evaluating a A-B on the ALU, setting the flags but not moving the result out of the ALU (as we would do with SUB instruction).

A-B (CMP instruction):
A<B A=B A>B
Z 0 1 0
C 1 0 0

We can see how different Z,C combinations can allow us to implement a JE (jump if equal), JNE (jump if not equal), JL (jump if less than) and JG (jump if greater than) instructions.  It is also trivial to implement a JLE (jump if less than or equal) and a JGE (jump if greater than or equal) by jumping on the equality condition as well in each case.  The final combination table of the Carry flag and Z flag to implement these instructions therefore looks like this:

C=0 C=1
Z=0 JG, JGE, JNE JL, JLE, JNE
Z=1 JE,JGE, JLE -

ALU instruction set

As previously mentioned, the lower four bits of the instruction register form part of the EEPROM lookup for the ALU control signals, in combination with the FLAG carry bit.  This means that all opcodes will mindlessly change the ALU function and output.  This isn't a problem as the microcode has to signal to the FLAG register to update the flags, or output the ALU product so no harm can be caused inadvertently.  In fact it has the advantage of saving ALU control signals and allowing easy implementation of different addressing modes and operations.  

For example let's define an instruction and give it the opcode 0x02:

NAND 0x42 ; means A = NAND(A, 0x42)
; machine-code would be 0x02, 0x42

The NAND product is produced by setting S3,S2,S1,S0 and M to L,H,L,L and H respectively on the 74LS181.  Given the way I am controlling the ALU, any instruction opcode with '2' as the lower 4 bits ( i.e. 2, 2+16, 2+16+16, etc) will direct the ALU to perform the NAND instruction,.  Now let's say we want to implement a different addressing mode where we want to NAND with the B register, we can define this new instruction at 0x02+16 = 0x12 i.e.:

NAND B ; means A = NAND(A,B), machine code is 0x02 + 16 = 0x12 

It will be up to the microcode to distinguish the two instructions (NAND immediate and NAND B) by implementing the different data addressing modes.  We could then implement a NAND [0x1234] or NAND [0x12] (zero-page addressing) in a similar way to use a value from a memory address.

In addition to the most useful logic and arithmetic operations, I have included operands for adding and subtracting with carry, for simulating 16-bit numbers (and beyond...).   For example, let's look at ADDC , add with carry:

; Add 0x1234 and 0x01F0 and store the result at memory location 0x0100.  Start with the lower bytes and then the higher bytes.

MOV A, 0x34    ; A = 0x34
ADD A, 0xF0    ; A = 0x34 + 0xF0 = 292 & 0xFF = 36, CARRY set
MOV A, [0x100] ; Store A in memory 0x0100
MOV A, 0x12    ; A = 0x12
ADDC 0x01      ; A = 0x12 + 0x01 + CARRY = 0x14
MOV A, [0x101] ; Store A in memory 0x0101

Subtract with carry (SUBC, opcode = 0x06) has also been implemented.  It turns out the same control signals for SUBC are required for "Compare with Carry", which is similar to CMP but uses the carry flag to remember the result of a previous CMP in the correct way and can be used to compare 16-bit numbers.  As the control signals are the same, I can set the opcode of CMPC in a spare slot, so long as the lower 4-bits = 0x06.  I used the next free slot, 0x24 = 38 = 6 + 2*16.  An example of CMP and CMPC in action:

; Let's compare 0xAA01 and 0xAA02.
MOV A, 0x01     ; A = 01
CMP A, 0x02     ; Carry flag is set
MOV A, 0xAA     ; A = 0xAA
CMPC A, 0xAA    ; carry flag (C=1) and Zero flag = 0
; Therefore A<B as required.  Without CMPC 0xAA = 0xAA we are misled to think A=B.

Well, that's enough about the ALU and the FLAG register.  Suddenly I feel like this pile of 7400 series logic chips has just become more interesting!  Of course it can't do anything until the control logic is implemented, but the ALU and FLAG register were crucial steps.  I think next I'll tackle the 16-bit Memory Address Register (MAR) and the logic to control the VRAM, SRAM and ROM!


Comments

Popular posts from this blog

Getting started with the Pro Micro Arduino Board

Arduino and Raspberry Pi serial communciation