TMS0800 FPGA implementation in VHDL

Inspired by and I set out to do similar in VHDL, for MicroNova Mercury board.

Public Chat
Similar projects worth following
Not one, but two vintage desktop calculators rolled into a single FPGA!

This VHDL implementation encapsulates both TI Datamath and Sinclair Scientific - a single input signal "virtual pin" switches the mode.

The "chip" is hosted within glue logic that provides interface to the Mercury baseboard hardware (switches, buttons, 4 digit LED, VGA). The keyboard is hex PMOD, the keys change meaning if in TI or Sinclair mode. All of this is (somewhat) documented here or in the source code.

The core of the implementation is 256 deep * 52 bit wide microcode routine. In a single clock cycle, a single 4-bit digit is processed, and to execute a single TMS0800 instruction, usually 12+ cycles are needed.

Both modes are operational, but with some bugs. Time permitting I may continue fixing them and improving. I hope somebody finds this project interesting, it was tons of fun and learning, and truly fascinating journey into retro-computing!

There are plenty of great implementations of different and historically important CPUs available on various FPGA-based platforms, but to my knowledge very few trying to implement calculator CPUs. Old calculators where one of the first examples of "microcontrollers" because on the same chip they contained CPU, RAM, ROM and I/O (interface with keyboard and displays). Given that they are a bit different, I thought it would be useful and interesting to describe the design path I took. 

The original patent ( ) explains the guts of the calculator CPU in a very detailed and accurate manner (albeit in a bit of a specific “patentesque” language intellectual property lawyers may be familiar with). However, there are few challenges of using this patent for direct implementation in VHDL:

-        1970s MOS technology is heavy on latches, which doesn’t align too well with FPGAs which are all about clocked registers

-        The design had to expanded to include both TI and Sinclair (the originals are distinct and separate chips, each with own mask and ROM contents)

-        The original is not microcode driven

Even so, the end result still somewhat resembles the main components of the original CPU. This project log describes the internals of calculator, as they come together is the main CPU entity implementation file -


The main source of info driving the display unit is the A register. It is however not just a bunch of simple BCD digits that can be directly multiplexed out to the 7 segment + decimal point display because:

-        TI and Sinclair have different numeric formats

-        Different digit values are used for negative sign

-        Error processing is different (Sinclair has essentially none, while TI uses a bit 5 in BFLAG register)

-        TI displays the decimal point on the place indicated by value in the LSD of register A, while Sinclair always displays the decimal point in fixed place

-        TI has blanking of leading zeroes, Sinclair doesn’t

All of the details above are hidden from the main entity. What comes out is the multiplexed segment (anode) / digits (cathode) output which can drive the display but also the columns of the keyboard. Note that the digits are driven from “digit10” (MSD) to “digit0” (LSD) because that is the only reasonable way to implement leading zero blanking. This is a problem because most of the calculations happen from LSD towards MSD (such as in add/subtract start with 1s then 10s, then 100s etc.).


There are two of these in the CPU, one containing the TI, another the Sinclair code. During the build time, the appropriate “.asm” file is parsed and loaded as binary content into these ROMs. Note that:

-        TMS800 instructions are originally 11-bit wide words. In this design the extra MSB bit is used to indicate a hardware breakpoint (not present in original chips)

-        320 words adds up to 1 256 word ROM + 1 64 bit ROM. In 1975 chip real estate was tight, but on modern FPGAs it is much simpler to “round up” to 512 words

-        Both ROMs are driven from the same 9-bit program counter register (“signal pc:…”) and both outputs go into a 2-to-1 12 bit multiplexer as selected by the “Sinclair” input signal

-        The “.asm” files are checked in, but they can be re-generated...

Read more »


Latest bit file to try out if you have Mercury FPGA + baseboard + PMOD KYPD + any VGA monitor with analog connector. Put all switches to "off", and press BTN3 to start the calculator in TI mode. SW7 = on puts it in Sinclair mode, and SW0 allows tracing on VGA.

- 146.12 kB - 09/14/2019 at 06:49


  • 1 × FPGA board
  • 1 × Baseboard with PMOD, display, switches etc.
  • 1 × Free (no longer supported!) FPGA development system from Xilinx
  • 1 × 16 button PMOD keyboard for input

  • Bugs disclosure

    zpekic09/22/2019 at 18:02 0 comments

    Few annoying bugs I was unable to track down and fix so far. Note that none of them affect the calculations due to workarounds or fixes applies.

    1. Ghost digits on 7-seg leds

    These appear on any CPU frequency higher that 50kHz or so, but only in the calculator display path, not when displaying the internal debug content of macro and microprogram counters. The display strobing goes from higher digits to lower (this is necessary because only this way the leading zero surpression can work) and it appears the segments "race ahead" of the digit strobes. 

    This bug renders 7 seg display unusable for normal calculator operations. The workaround is to use VGA only to display entry and results, which is no great loss as 4 digit 7-seg can only display half of the digits at a time, one has to press BTN0 to switch to upper half, making it very impractical to begin with.

    2. Not all digits of internal registers displayed on VGA debug screen

    TMS0800 registers are either 11 BCD digits long (A, B, C), or 11 bits long (AF, BF). There is a timing problem in VGA debug component or its driver which "prints" only:

    9 last digits at 12.5MHz

    10 last digits at 6.25MHz

    At lower frequencies (57.6kHz and 1kHz) all digits are printed on the screen. None of this impacts operation of the calculator core, except makes first 1-2 digits (containing sign etc.) invisible in debug mode.

    3. Stray code execution

    There is no unconditional jump in TMS0800 - those are modeled by "knowing" the state of CF flag and executing "jump on condition set/reset" instructions accordingly. However due to probably another unknown bug, in Sinclair mode at the end of ROM (part of AntiLog evaluation routine) CF is sometimes not the expected "1". This has been "fixed" with a hack explained in the code, but the real fix should be in some underlying issue (perhaps microcode implementation):

    prog_sinclair: rom512x12

    generic map
    -- HACKHACK: Why is this "random instruction" being passed in to intitialize the ROM???
    -- Last instruction in Sinclair ROM as 0x13F is "BINE ALOGDIV" - however in some cases
    -- CF is 0 which means it will not be executed and execution will continue at 0x140, which 
    -- will bring it back to the right place, instead of executing bad opcodes, or NOPs which 
    -- would wrap up to reset location 0. This is indication of another bug but for now this
    -- "works". 
    fill_value => BIE_ALOGDIV,
    sinclair_mode => true, -- hint to show correct disassembled listing (Sinclair mode)
    asm_filename => "./sourceCode_sinclair.asm",
    lst_filename => "./tms0800/output/sourceCode_sinclair.lst"
    port map
    address => pc,
    data => instruction_sinclair

    4. Numeric digit keyboard off by one

    Each TMS0800 instruction "pulls" the next right keyboard scan line down. That is the reason why the sequence of instructions is arranged exactly like the keys on the keyboard:

    0 0111 0001 0071 10 00000 0000          BKO    CLEAR  ; Clear key pressed?
    0 0111 0010 0072 10 00101 0101          BKO    EQLKEY ; Equal key pressed?
    0 0111 0011 0073 10 00100 1101          BKO    PLSKEY ; Plus key pressed?
    0 0111 0100 0074 10 00011 1101          BKO    MINKEY ; Minus key pressed?
    0 0111 0101 0075 10 00100 1100          BKO    MLTKEY ; Mult key pressed?
    0 0111 0110 0076 10 00100 1011          BKO    DIVKEY ; Divide key pressed?
    0 0111 0111 0077 10 00110 0011          BKO    CEKEY  ; CE key pressed?
    0 0111 1000 0078 10 00101 1101          BKO    DPTKEY ; Decimal point key pressed?
    0 0111 1001 0079 10 00111 1110          BKO    ZERKEY ; Zero key pressed?
    0 0111 1010 007A 11 10011 1111          EXAB   ALL    ; Process digit key...
    0 0111 1011 007B 11 11010 0010 ...

    Read more »

  • Patterns for creating microcode-driven core in VHDL

    zpekic09/05/2019 at 03:19 0 comments

    There are various well known and documented patterns and best practices to create FSM (finite state machine) designs that work well on FPGAs. However from what I have seen, not much in terms of how to have a simple, good microcode pattern. One could of course adapt the methodology (and even tooling) popular in the bit-slice era when microcoding was the most popular way to create custom processors and logic (which I have done too for the Am9080 project using Am2901 slices), but that approach is not very streamlined either. 

    My first attempt was to write a separate microcode compiler in C# which would "spit out" file in a format that could directly be used to prime read-only memory (microcode or mapping ROM) in the VDHL. Starting work on that I realized that may be too heavy-weight for my needs, and also has the disadvantage of adding another proprietary tool to the toolchain and extra step in the journey towards the .bit file. 

    Better alternative seemed to be to do this right in VHDL, "synthesizing" the contents of the microcode ROM as VHDL code is being compiled. Eventually, I settled on the approach described below, which I used in this project both for the calculator core and for the VGA tracer components.

    That simpler solution turned out to be just a combination of few tricks in VHDL. Here is the pattern:

    • define microinstruction which as a "NOP" to be all zeroes (all hardware driven by microinstruction should interpret zero control bits as doing nothing!!)
    • each microinstruction is comprised of multiple "fields" of varying length - for each of these write a helper function taking as parameters everything needed to describe the functionality of the target component (in many cases these are driving selects to muxes, or maybe enabling some signals or similar) and returning the desired value of the targeted field, inserted into a "NOP" microinstruction
    • concatenate any number of these functions with simple "or" (as long as same function is not used more than once!)

    Most of this can be seen in a single file:

    For example look at how alu function is defined:

    -- 3 BITS 13..11
    -- alias alu_fun: std_logic_vector(2 downto 0) is u_code(13 downto 11);
    impure function uc_alu(alu_fun: in std_logic_vector(2 downto 0)) return std_logic_vector is
         return X"00000" & "000000000000000000" & alu_fun & "00000000000";
    end uc_alu;

    as you can see it just returns 3 bits in the right place in the microinstruction word defining the ALU function, and the rest of what it returns is all zeros:

    -- ALU functions
    constant fun_zero : std_logic_vector(2 downto 0) := "000";
    constant fun_s : std_logic_vector(2 downto 0) := "001";
    constant fun_r : std_logic_vector(2 downto 0) := "010";
    constant fun_xor : std_logic_vector(2 downto 0) := "011";
    constant fun_adchex :std_logic_vector(2 downto 0) := "100";
    constant fun_adcbcd :std_logic_vector(2 downto 0) := "101";
    constant fun_sbchex :std_logic_vector(2 downto 0) := "110";
    constant fun_sbcbcd :std_logic_vector(2 downto 0) := "111";

    Obviously, the actual ALU can now consume the same definition and implement the functionality accordingly:

    with fun select
    y <=   s when fun_s,
              r when fun_r,
             (s xor r) when fun_xor,
             sum0(3 downto 0) when fun_adchex,
             sum2(3 downto 0) when fun_adcbcd,
             dif0(3 downto 0) when fun_sbchex,
             dif2(3 downto 0) when fun_sbcbcd,
             "0000" when others;

    given that the return is "zeros" for anything outside this field - meaning NOP for all other components driven by the microinstruction, it won't impact them. So one can simply "or" it together with any other similar functions ("helpers") to create a microinstruction to do drive other components as needed:

    110 =>

    Read more »

  • Initial Sinclair upload

    zpekic09/03/2019 at 06:59 0 comments

    I was able to verify few operations on basic arguments and compare them to real Sinclair Scientific for match. Following problems remain:

    - LED blurs at higher frequency. Calculation can be done at 12.5MHz but LED is blurry above 40kHz or so

    - breakpoint and single step logic needs to be laid out more logically on the switches, and dual clock unit avoided

    - VGA tracer still drops some character from display.

    Note that none of the above impacts calculation. I also briefly tested TI mode, seems no regression was introduced there.

View all 3 project logs

  • 1
    Using the calculators without compiling the code

    1. Connect the hardware as depicted on second image

    2. Download the sys0800.bit file

    3. Set all switches on the baseboard to "off" (== closer to the edge of the board)

    4. Use to upload the .bit file to Mercury board

    --- at this point you should see "logo" at the bottom of VGA screen, and red 0 at random location in the black band above --

    5. Press BTN3 to start calculator in TI mode. Keys are mapped as:

    + = A

    -  = B

    * = C

    / = D

    = = E

    . = F

    BTN3 = single step next

    BTN2 = C[lear]

    BTN1 = C[lear]E[ntry]

    BTN0 = show upper 4 digits on baseboard 7-seg LEDs

    6. Set SW7 to on to switch to Sinclair Scientific mode

    7. Press RESET (button on mercury board) to reset, otherwise registers will contain garbage from TI mode

    8. Press BTN3 to start in Sinclair mode. Keys are mapped as:

    log/antilog = A

    tan/arctan = B

    sin/arcsin = C

    cos/arccos = D

    E == E

    Down = F

    BTN3 = single step next

    BTN2 = C[lear]

    BTN1 = Up

    BTN0 = show upper 4 digits on baseboard 7-set LEDs

    Happy calculations, and please report all the bugs! No promises about fixing them though... :-)

    PS. If you want to see some of the internal workings, play with the switches and buttons. The declaration of top-level entity has some details:

    entity sys0800 is
        Port ( 
    -- 50MHz on the Mercury board
    CLK: in std_logic;
    -- Master reset button on Mercury board
    USR_BTN: in std_logic; 
    -- Switches on baseboard
    -- SW(0) -- show trace on VGA
    -- SW(1) -- show debug data (program and microcode program counter) on 7seg instead of calculator data
    -- SW(2) -- enable microcode single stepping (use with BTN(3))
    -- SW(3) -- enable calculator program breakpoints
    -- SW(4) -- (not used)
    -- SW(6 downto 5) -- system clock speed                 --   0   0    1024 Hz                 --   0   1    57600 Hz (close to original calculator frequency)                 --   1   0  6.25 MHz                --   1   1  12.5 MHz
    -- SW(7)
    --   0   TI Datamath
    --   1   Sinclair Scientific
    SW: in std_logic_vector(7 downto 0); 
    -- Push buttons on baseboard
    -- BTN0 - show upper 4 digits on 7seg LEDs
    -- BTN1 - CE[NTRY] key for TI and UP key for Sinclair
    -- BTN2 - C[LEAR] key for both TI and Sinclair (this is also "reset" for Sinclair)
    -- BTN3 - single step clock cycle forward if in SS mode (NOTE: single press on this button is needed after reset to unlock SS circuit)
    BTN: in std_logic_vector(3 downto 0); 
    -- Stereo audio output on baseboard
    --AUDIO_OUT_L, AUDIO_OUT_R: out std_logic;
    -- 7seg LED on baseboard 
    A_TO_G: out std_logic_vector(6 downto 0); 
    AN: out std_logic_vector(3 downto 0); 
    DOT: out std_logic; 
    -- 4 LEDs on Mercury board (3 and 2 are used by VGA VSYNC and HSYNC)
    LED: out std_logic_vector(1 downto 0);
    -- ADC interface
    -- channel input
    -- 0 Audio Left
    -- 1 Audio Right
    -- 2 Temperature
    -- 3 Light -- 4 Pot
    -- 5 Channel 5 (free)
    -- 6 Channel 6 (free)
    -- 7 Channel 7 (free)
    --ADC_MISO: in std_logic;
    --ADC_MOSI: out std_logic;
    --ADC_SCK: out std_logic;
    --ADC_CSN: out std_logic;
    --PS2_DATA: in std_logic;
    --PS2_CLOCK: in std_logic;
    --VGA interface
    --register state is traced to VGA after each instruction if SW0 = on
    --640*480 50Hz mode is used, which give 80*60 character display
    --but to save memory, only 80*50 are used which fits into 4k video RAM
    HSYNC: out std_logic;
    VSYNC: out std_logic;
    RED: out std_logic_vector(2 downto 0);
    GRN: out std_logic_vector(2 downto 0);
    BLU: out std_logic_vector(1 downto 0);
    --PMOD interface
    --connection to
    PMOD: inout std_logic_vector(7 downto 0)
    end sys0800;

View all instructions

Enjoy this project?



Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates