In this blog I share my observations, thoughts and experience about computers, linguistics, philosophy and many other things that interest me.

Saturday, January 17, 2026

From ENIAC's Glowing Bulbs to FPGA Lookup Tables: 80 Years of the Same Idea

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 project started with Figure 1 of the IBM System/360 Principles of Operation — a block diagram that stuck in my mind for 15 years. Now it's grown to encompass ENIAC's ghosts too. We're not just building a retro computer. We're participating in a conversation that's been going on since the first electronic bit was stored.


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: