Sometimes, when you're deep in the weeds of assembly code at 2 AM, you write something and suddenly realize: "Wait, I've seen this before." Not in another codebase, not in a textbook — but in a grainy 1946 film about the birth of electronic computing.
Let me tell you about the unexpected connection between the GMS/359 project and ENIAC, and how a simple hex conversion routine turned into a journey through 80 years of computing history.
The Problem: Displaying Scan Codes in Hex
The GMS/359 system recently gained PS/2 keyboard support. The physical layer is working — bytes flow from an ancient Mitsumi keyboard through the GMS 2870 Multiplexor Channel into memory. But raw scan codes are just numbers. When debugging, you want to see 1C on your terminal, not an unprintable byte.
The challenge: convert a byte (0x00–0xFF) to two ASCII hex digits. On a modern CPU, this is trivial — shift, mask, add, conditionally adjust. But GMS/359 is intentionally minimal. We had Load Register, Store, Branch, and I/O instructions. No shifts. No bitwise AND. No comparison operations.
What we did have, as of this week, was the new AR (Add Register) instruction:
AR R1, R2 ; R1 ← R1 + R2
And that's all we needed.
The Solution: 256-Byte Lookup Tables
Without bit manipulation, we use brute force with elegance — lookup tables. Two of them, each 256 bytes:
hex_hi_table: Given byte value N, returns the ASCII character for the high nibble (N >> 4) hex_lo_table: Given byte value N, returns the ASCII character for the low nibble (N & 0x0F)
The low nibble table looks like this:
hex_lo_table:
DB '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
DB '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
DB '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
; ... repeats 16 times total
The same pattern, repeated 16 times. Index 0x00 gives '0'. Index 0x0F gives 'F'. Index 0x10 gives '0' again. Index 0x1F gives 'F' again. The table encodes the low nibble extraction implicitly through its structure.
The conversion code is beautifully simple:
LFI R10, hex_hi_table ; Load table base address
AR R10, R1 ; Add scan code as offset
LB R2, [R10] ; Fetch the ASCII digit!
Three instructions. No shifts, no masks, no branches. Just load, add, load.
And Then I Saw It
When I looked at the repeating pattern of that table — '0' through 'F', sixteen times over — something clicked. I'd seen this visual pattern before. Not in code. In vacuum tubes.
This is ENIAC's decimal accumulator display from February 1946. Each row represents one decimal digit of a register. Each row has ten positions — ten bulbs. The lit bulb indicates the value. Position 0 lit means zero. Position 5 lit means five.
It's the same principle.
In ENIAC, the electron flow through vacuum tubes determined which bulb would glow. The physical position of the light is the value.
In GMS/359, the address offset into the table determines which byte we fetch. The position in the table is the value.
ENIAC (1946):
○○○○●○○○○○ = digit 4 (bulb at position 4 glows)
GMS/359 (2026):
'0','1','2','3','4','5'...
↑
index 4 = '4'
The bulbs are one-hot encoded decimal. The table is direct-mapped hexadecimal. Same concept, 80 years apart.
Looking at this image of an ENIAC operator at the console, surrounded by those glowing decimal displays, I realized something profound: we're still doing the same thing. The technology changed — vacuum tubes to transistors to FPGAs — but the fundamental ideas persist. Position encodes value. Structure encodes logic.
The New Instructions That Made This Possible
This hex display routine showcases several recent additions to the GMS 2050 CPU:
LFI (Load Fullword Immediate) — Load a 32-bit constant into a register. Essential for setting up pointers to data structures and tables.
LFI R10, hex_lo_table ; R10 = address of table (32-bit immediate)
AR (Add Register) — Add two registers. Simple, essential, and it sets condition codes. This is what enables computed addressing — table base plus offset.
AR R10, R1 ; R10 = R10 + R1 (table + index)
LB (Load Byte) — Load a single byte from memory into a register, zero-extended. Now with full base register support!
LB R2, [R10] ; R2 = memory[R10]
SB (Store Byte) — Store the low byte of a register to memory. Used to build the output string.
SB [R9+1], R2 ; Store low hex digit to buffer
The base register addressing deserves special mention. We moved from the original S/360 notation (LB R2, 0(R10)) to a cleaner NASM-style syntax: LB R2, [R10] or LB R2, [R10+5]. Square brackets for memory references, just like x86 assembly. It's a small thing, but it makes the code so much more readable.
The Full Picture
Here's the complete hex conversion in context:
kbd_loop:
; Read scan code from keyboard
LFI R1, kbd_ccw
ST [CAW], R1
SIO 011h ; Start keyboard read
poll_kbd:
TIO 011h
BC 4, poll_kbd ; Wait for keypress...
; Load the scan code
LFI R8, kbd_buffer
LB R1, [R8] ; R1 = scan code (0x00-0xFF)
; Convert high nibble
LFI R10, hex_hi_table
AR R10, R1
LB R2, [R10] ; R2 = high hex digit ASCII
LFI R9, uart_buf
SB [R9], R2
; Convert low nibble
LFI R10, hex_lo_table
AR R10, R1
LB R2, [R10] ; R2 = low hex digit ASCII
SB [R9+1], R2
; Send to UART...
When you press a key, you see its scan code in hex on the terminal. Press 'A', see 1C. Release it, see F0 1C. Arrow keys show their E0 prefix bytes. It's immensely satisfying.
80 Years
ENIAC (1946) → IBM S/360 (1964) → GMS/359 (2026)
tubes transistors FPGA (GateMate)
decimal EBCDIC/hex ASCII/hex
wired program microcode VHDL state machine
18,000 tubes hybrid ICs ~5,000 LUTs
30 tons varies 41mm × 41mm
The ENIAC engineers would recognize what we're doing. The principle is the same. Position encodes information. Structure replaces logic. Tables trade space for time and complexity.
When I look at that repeating pattern — '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' — I don't just see bytes anymore. I see glowing bulbs in a Philadelphia basement in 1946. I see the fundamental ideas of computing, persistent and immortal, expressed in whatever medium we have at hand.
The GMS/359 Computing System is an FPGA-based recreation of IBM System/360 architecture using the Cologne Chip GateMate platform. Current status: CPU with 12 instructions, working Channel I/O, video output, UART, and PS/2 keyboard support. The journey continues.




No comments:
Post a Comment