In case you want to skip much theory below and dig-in in a practical way, follow this guide: https://hackaday.io/project/182959-custom-circuit-testing-using-intel-hex-files/log/201614-micro-coded-controller-deep-dive
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:
- code memory description (.code statement)
- mapper memory description (.mapper statement)
- microinstruction word description containing:
- sequencer description (.if .then .else)
- values available in current microinstruction clock ( .valfield)
- values available in next microinstruction clock (captured at next clock: .regfield)
- simple macros (.alias)
- microcode statements. These are:
- put into specific microcode locations using .org statement
- mapped to specific instruction patterns using .map statement
- containing any number of value and/or register assignment, but only up to 1 if-then-else statement
- commas can be read as "simultaneous in 1 clock cycle", and 1 semicolon maps to 1 cycle
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:
- compile (mcc.exe source.mcc) to generate code/mapper memory files
- 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:
- addresswidth - integer (default = 0 (auto-size)) - number of locations in the memory block
- wordwidth - integer (default = 8) - number of bits in one word
- recordwidth - integer (default = 16) - number of bytes per line in .hex file output
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:
- tty_screen_code.mif - useful for Altera/Intel tools
- tty_screen_code.cgf - for Xilinx
- tty_screen_code.coe - for Xilinx (not specified in the statements above so not generated, link is to equivalent file from CDP180X.mcc)
- tty_screen_code.vhd - a "VHDL Package" good for all FPGA compilers, can be included in the list of project files (the "tty" prefix allows any number of microcoded design to be included into same FPGA project, differentiated by this prefix)
- tty_screen_code.hex - useful for any tools, including possibly loading into the FPGA ("dynamic microcode!") during runtime. The paramer "4" indicates 4 bytes per line (usually is 16 for .hex 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-... -- https://github.com/zpekic/MicroCodeCompiler -------------------------------------------------------- -- 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; begin -- 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) begin if (reset = '1') then uPC0 <= (others => '0'); -- reset clears top microcode program counter else 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; else 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 => null; end case; end if; end if; end if; end process; end;
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.)
-- AND (OPTIONALLY BUT USUALLY)
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
- Support "include" files in source.mcc
- Support .hex, .mif, .coe, .cgf for conversion source file type
- Simple expression evaluation for values
- Conditional compile (#ifdef / #ifndef / #else / #endif)