Saturday, July 30, 2022

7400 Logic Calculator: One ROM Design

After finishing the new design for my 7400 Logic Calculator, I continued to think about different ways to build a minimal design that would fit in a calculator body. Watching James Sharman's videos about his breadboard computer got me thinking about eliminating the microcode sequencer ROM, which would save a lot of room. It eventually dawned on me that the ALU ROM could be eliminated too if it was combined with the program ROM. ALU operations then become ROM lookups and can share the same circuitry used for looking up constants which also eliminates chips. Based on this, I came up with a modified design using only 18 chips compared to the other design's 21. Best of all, the whole thing fits into just one 32K EEPROM rather than three.

Hardware
Like the previous design, this is a 4-bit architecture which has a lot of advantages for a calculator. Here are the various registers and other chips:

Instruction register - 8-bit latch that holds the op code read out of ROM at the beginning of every instruction.

A and B registers - Two 8-bit latches that hold the two values to be fed to the ALU. Only the bottom four bits of each latch are used, so a single chip with two 4-bit latches like the 74LS874 would work if it was available in HC. While each register inputs from bits 0-3 of the data bus, they only output to the bus together with each one driving four of the eight data bus bits. This means A can be written directly into B, but B can only be written to A with the help of the ALU.

Carry register - One flip-flop that holds the carry output from ALU operations. Since the ROM is only 32K, the top bit of each 16-bit memory address can be used to signify whether a ROM lookup is an ALU operation. If it is, some circuitry substitutes one of the address bus lines for the carry register output feeding the carry to the ALU lookup. Other circuitry latches one bit of the ROM lookup output into the carry register.

Address registers - Two 8-bit latches supply the address for constant and ALU lookups from ROM. A bit in the instruction controls whether the register for the high byte of the address is Z-stated. Pull-down resistors on the high byte allow the register for the low byte of the address to work like zero-page addressing on the 6502 where only one address byte is needed to access the first 256 bytes of RAM. For 16-bit jump addresses calculated and held in RAM, this type of zero-page mechanism is necessary since the register for the high address byte buffers the byte to be loaded into the low byte of the program counter and can't be used for the high byte of the RAM address.

Program counter - Two 4-bit counters control the low eight bits of the program counter, and one 8-bit latch controls the high byte. This saves one chip over using four 4-bit counters but requires that the latch for the high byte be reloaded at the end of every 256-byte page of program memory. As mentioned above, the low byte of the program counter is loaded from the high byte of the address register. This is a bit unconventional but allows jumps to addresses stored in RAM without having to add an extra chip.

EEPROM - One 32K EEPROM holds both the program and the ALU. Since each ALU operation takes two 4-bit arguments and a carry input, each operation uses 512 bytes of the EEPROM. Around 32 ALU operations would allow a comfortable instruction set but would only leave 16K for the program. Since the instruction encoding (described below) is so inefficient, it makes sense to use a small instruction set to maximize the amount of program space. Using around 20 ALU operations would leave 22K for the program.

RAM - One 8K SRAM for data can be accessed using the same address registers mentioned above that allow reading constants and ALU operations from the EEPROM.

Keypad input - One 8-bit buffer receives input from the calculator's keypad.

Input and output selectors - Two 74HC138 demultiplexers select which chips read and write the bus. The selection bits for the demultiplexers are driven directly by bits in the instruction. The bus inputs and outputs are similar to the ones from my last design:

   Outputs to bus
   Register A and B combined
   EEPROM
   RAM
   Keyboard input

   Inputs from bus
   Register A
   Register B
   Address register low
   Address register high
   Program counter low
   Program counter high
   RAM
   LCD

Microcode sequencing - This part of the design is a bit more complicated than the last design since 7400 logic chips need to replace the functionality of the microcode ROM. To keep the chip count as low as possible, the microcoding is extremely simplified. One clock cycle loads the instruction and the next latches the input and output pair connected to the bus specified by the instruction. This is so simple that it doesn't even need a microcode counter since a flip-flop is enough to keep track of the two states. For a lot of other small jobs like incrementing the program counter or the special handling of the carry bit, a few logic gates are enough. Apart from the reset logic, the system only needs five inverters, three AND gates and seven OR gates which fit into just four 7400 logic chips. This brings the chip count to only 18.

Instruction set
Since the design doesn't have a microcode ROM, bits in the instruction control the behavior of the system directly. Since ALU operations are ROM lookups, the 8-bit instructions don't encode ALU operations. Here's the encoding:

Bits 0-2 - Bus input selection
Bits 3-4 - But output selection
Bit 5 - Whether to disable address high register for zero page addressing
Bit 6 - Whether to enable program counter output for loading constants in instruction from ROM or to enable the address registers for loading constants at another address in ROM
Bit 7 - Whether to increment the program counter after loading the instruction. Needed for loading constants in instructions.

The simplified hardware design comes at a price as each instruction accomplishes very little. Here's an example of adding two 4-bit numbers stored at fixed addresses in RAM then writing the sum back:

001. MOV ALO, #Foo_lo
002. MOV AHI, #Foo_hi
003. MOV A, RAM
004. MOV ALO, #Bar_lo
005. MOV AHI, #Bar_hi
006. MOV B, RAM
007. MOV AHI, #ADD
008. MOV ALO, AB
009. MOV A, ROM
010. MOV ALO, #Foo_lo
011. MOV AHI, #Foo_hi
012. MOV RAM, A

 ;Load low byte of address Foo
 ;Load high byte of address Foo
 ;Load nibble at Foo into A reg
 ;Load low byte of address Bar
 ;Load high byte of address Bar
 ;Load nibble at Bar into B reg
 ;Load address of ADD ALU lookup
 ;Load A and B reg into ALU lookup
 ;Load calculated sum into A reg
 ;Load low byte of address Foo
 ;Load high byte of address Foo
 ;Write nibble in A reg to Foo
 Total:
 
 2 bytes
 2 bytes
 1 byte
 2 bytes
 2 bytes
 1 byte
 2 bytes
 1 byte
 1 byte
 2 bytes
 2 bytes
 1 byte
 19 bytes 
As you can see, this is extremely inefficient. The 22K or so of ROM allocated for program space might not be enough when a single 4-bit add needs 19 bytes. Lines 2, 5, and 11 can be eliminated if the value is stored in zero page, but the sequence would still be 13 bytes long. The only way programming like this would be feasible is extensive use of macros which might look like this:
   
001. MOV A, RAM Foo
002. ADD A, RAM Bar
003. MOV RAM Foo, A
 ;Load nibble at Foo into A reg
 ;Add nibble at Bar to A reg
 ;Write calculated sum to Foo
Total:
 5 bytes
 9 bytes
 5 bytes
 19 bytes

A good macro system could keep track of the address high register and avoid setting it when it already holds the correct value to save 1-2 bytes in some cases. The RAM prefix for addresses is one idea for keep RAM and ROM address ranges separate. Two different instruction names such as MOVC and MOVX like the 8051 has are another way that might work. 

The only way to squeeze all of the planned functionality into ROM may be to implement a VM with small tokens like I'm trying to do for my 6507 Graphing Calculator project. This may be too slow since address calculations take so many cycles, so another option could be a threading model like most Forths where the program consists of a list of jump addresses or subroutine calls. Since there is no hardware support for subroutines, the most compact model would be a list of 4-byte jump instructions which could be very compact.

As mentioned above, sticking to 20 or so ALU operations would leave 22K of ROM space for the program. Here's a tentative list that would probably be enough:

1. Set carry
2. Clear carry
3. Add with carry
4. Subtract with carry
5. Decimal add with carry
6. Decimal subtract with carry
7. Decimal multiply
8. Decimal divide
9. Rotate left
10. Rotate right
 11. Move A to B
 12. AND
 13. OR
 14. XOR
 15. Increment
 16. Decrement
 17. Add to jump address if carry set
 18. Set carry if equal
 

Since the B register, like the A register, can only read from the low four bits of the data bus, it's not possible to write to both registers at once in the cases like multiply and divide where part of the ALU result needs to go to the B register. One idea is to set the carry bit first then do the lookup to get part of the result in A. This instruction will also clear the carry bit. The next instruction reads the ALU output again but because the carry is clear, the ALU will supply the value that goes into B.

Conclusion
After writing out everything about this design, it seems much less feasible than the design from my last 7400 logic calculator. When the time comes to start working on one of them, I'll have to decide whether it's worth it to make the calculator into a kit to sell which would favor the simplified design. For now, I'll keep working on my 2022 project goals.

No comments:

Post a Comment