Microcode compiler in FPGA toolchain

A project log for Microcoding for FPGAs

A microcode compiler developed to fit into FPGA toolchain and validated to develop CDP1805-like CPU and text-based video controller

zpekiczpekic 06/14/2020 at 00:560 Comments


In case you want to skip much theory below and dig-in in a practical way, follow this guide:

The following diagram above illustrates the high-level code / project flow that uses mcc microcode compiler. Details are elaborated below.


Microcode source code file is a simple text file that typically contains following sections:

A single statement can go into any number of lines for clarity, but last one must be terminated by a ;

Labels can stand in front of aliases to be used ("expanded") in code later, or in front of microcode statements, to be used as target to goto/gosub (except _ starting labels to prevent that on purpose, for example first 4 cycles after reset)


This is a 2-pass, in-memory compiler written in pretty straightforward C# / .Net that should make it portable to other platforms (although this has not been evaluated)

There are 2 modes to use it:

  1. compile (mcc.exe source.mcc) to generate code/mapper memory files
  2. convert (mcc.exe source.bin [addresswidth] [wordwidth] [recordwidth])

Both will produce extensive warning and error list on the console, as well as source.log file with detailed execution log.

Currently, only conversion from bin (for example, EPROM image) is supported, but I plan to add other file formats too. Conversion parameters are:

Generated files

In order to facilitate ease of use in standard vendor or open-source FGPA toolchain downstream, multiple data format files are generated. All contain same information though!

The .code, .mapper, .controller statements describe the files generated:

.code 6, 32, tty_screen_code.mif, tty_screen_code.cgf, tty:tty_screen_code.vhd, tty_screen_code.hex, 4;
.mapper 7, 6, tty_screen_map.mif, tty_screen_map.cgf, tty:tty_screen_map.vhd, tty_screen_map.hex, 1;
.controller cpu_control_unit.vhd, 8;

This will generate:

A code memory block of 64 words 32 bits wide, and store it to following files:

A mapper memory block 128 words, 6 bits wide, files similar to above.

The .controller statement will generate a .vhd file with the integer parameter giving the depth of the "hardware stack" - 8 is probably the most reasonably used, simpler designs can get away with 4 or even 2. 

An example of generated controller vhd file for stack depth of 4:

-- mcc V0.9.0627 - Custom microcode compiler (c)2020-... 
-- Auto-generated file, do not modify. To customize, create 'controller_template.vhd' file in mcc.exe folder
-- Supported placeholders:  [NAME], [STACK_DEF], [STACK_PUSH], [STACK_POP].
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use IEEE.numeric_std.all;

entity tty_control_unit is
     Generic (
            CODE_DEPTH : positive;
            IF_WIDTH : positive
     Port ( 
          -- standard inputs
          reset : in  STD_LOGIC;
          clk : in  STD_LOGIC;
          -- design specific inputs
          seq_cond : in  STD_LOGIC_VECTOR (IF_WIDTH - 1 downto 0);
          seq_then : in  STD_LOGIC_VECTOR (CODE_DEPTH - 1 downto 0);
          seq_else : in  STD_LOGIC_VECTOR (CODE_DEPTH - 1 downto 0);
          seq_fork : in  STD_LOGIC_VECTOR (CODE_DEPTH - 1 downto 0);
          cond : in  STD_LOGIC_VECTOR (2 ** IF_WIDTH - 1 downto 0);
          -- outputs
          ui_nextinstr : buffer  STD_LOGIC_VECTOR (CODE_DEPTH - 1 downto 0);
          ui_address : out  STD_LOGIC_VECTOR (CODE_DEPTH - 1 downto 0));
end tty_control_unit;

architecture Behavioral of tty_control_unit is

constant zero: std_logic_vector(31 downto 0) := X"00000000";

signal uPC0, uPC1, uPC2, uPC3 : std_logic_vector(CODE_DEPTH - 1 downto 0);
signal condition, push_then_jump: std_logic;


-- uPC holds the address of current microinstruction
ui_address <= uPC0;
-- evaluate if true/false
condition <= cond(to_integer(unsigned(seq_cond)));
-- select next instruction based on condition
ui_nextinstr <= seq_then when (condition = '1') else seq_else;
-- check if jump or one of 4 "special" instructions
push_then_jump <= '0' when (ui_nextinstr(CODE_DEPTH - 1 downto 2) = zero(CODE_DEPTH - 3 downto 0)) else '1';

sequence: process(reset, clk, push_then_jump, ui_nextinstr)
if (reset = '1') then
       uPC0 <= (others => '0');    -- reset clears top microcode program counter
       if (rising_edge(clk)) then
             if (push_then_jump = '1') then
        uPC0 <= ui_nextinstr;
        uPC1 <= std_logic_vector(unsigned(uPC0) + 1);
        uPC2 <= uPC1;
        uPC3 <= uPC2;
                 case (ui_nextinstr(1 downto 0)) is
                     when "00" =>    -- next
                         uPC0 <= std_logic_vector(unsigned(uPC0) + 1);
                     when "01" =>    -- repeat
                         uPC0 <= uPC0;
                     when "10" =>    -- return
                uPC0 <= uPC1;
                uPC1 <= uPC2;
                uPC2 <= uPC3;
                uPC3 <= (others => '0');
                     when "11" =>    -- fork
                         uPC0 <= seq_fork;
                     when others =>
                 end case;
             end if;
         end if;
end if;
end process; 


FPGA project files

At this point, any tooling / editor can be used to develop the FPGA design using any methodology. But if the microcoded design is to be included then following must be included in the project:

1 standard control unit VHD per microcoded controller / CPU (see above for generated example)

-- AND --

At least 1 of the files describing code memory block (e.g. .coe or vhd etc.)


At least 1 of the files describing mapper memory

Including the files above in the project generates a dependency between microcode source and final FPGA .bit file - after changing the .mcc file, compiler must be run, which triggers update of files in main project, which requires a rebuild of project to create a new file.

Note that it is possible to have microcoded design without the mapper, but not without microcode. However is it possible to "misuse" the compiler to produce complex lookup memory maps by using the .mapper and .org directives. 

Future improvement plans