Microcode Compiler quick manual

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 07/05/2020 at 04:580 Comments

Disclaimer: The work on the MCC is still ongoing / evolving, so the current state on github may deviate from the description below. 

There is no install package for mcc.exe - it is a simple command line utility which will work on most versions of Windows, and can probably be also recompiled for other platforms that have .net and C# ported.


Starting with -h command line argument lists the usage:

>mcc.exe -h
-- mcc V0.9.0627 - Custom microcode compiler (c)2020-...

Compile mode (generate microcode, mapper and control unit files):
mcc.exe [relpath|fullpath\]sourcefile.mcc

Convert mode (generate sourcefile.coe, .cgf, .mif, .hex, .vhd files):
mcc.exe [relpath|fullpath\]sourcefile.bin [addresswidth [[wordwidth [recordwidth]]]
addresswidth   ... 2^addresswidth is memory depth (integer, range: 0 to 16, default: 0 which will infer from file size)
wordwidth      ... memory width (integer, values: 8, 16, 32 bits, default: 8 (1 byte))
recordwidth    ... used for .hex files (integer, values: 1, 2, 4, 8, 16, 32 bytes, default: 16)

For more info see

The convert mode allows usage as a handy utility to convert memory file formats that often come up in FPGA or other embedded system development. The focus here will be on the usage to generate elements of the microcoded design ("compile mode").


The mcc source file is a text file with extension .mcc with few general rules:


Following statements are currently recognized by mcc.exe:

Design definition statements:

.code depth, width, filelist, bytewidth;

Reserves memory for microcode:

Example: generate 5 files describing the 64 * 32 memory containing the generated microcode:

.code 6, 32, tty_screen_code.mif, tty_screen_code.cgf, tty:tty_screen_code.vhd, tty_screen_code.hex, tty_screen_code.bin, 4;

.mapper depth, width, filelist, bytewidth;

Reserves memory for mapper - this is the lookup memory that accepts bit patter from instruction register as address, and outputs the starting address of microcode implementing that instruction. The arguments are same like for .code statement

Example: generate 5 files describing the 128 * 6 memory containing the generated mapper:

.mapper 7, 6, tty_screen_map.mif, tty_screen_map.cgf, tty:tty_screen_map.vhd, tty_screen_map.hex, tty_screen_map.bin, 1;

.controller code.vhd, stackdepth[, rising|falling];

Example: generate standard microcode controller, with 4 level stack depth, rising edge clock:

.controller tty_control_unit.vhd, 4;

Microcode field definition statements:

label: .if width values conditionlist default defaulcondition;

For width of N, 2^N conditions need to be defined in the conditionlist, which a list of comma delimited symbols

By convention, condition 0 is "true" and condition 2^N-1 is "false"

"true" condition is usually designated as default which allows handy default "next" without writing anything.

Example: microcode controller unit consuming 8 conditions:

seq_cond:    .if 3 values 
            true,             // hard-code to 1
            false            // hard-code to 0
            default true;

label: .then width values targetlist default defaulttarget;

Example: 6 bits are reserved for target if condition is met, which can be either one of 4 predefined controller actions or branching to any valid label. Obviously, this design must have microcode depth of 64 words to be able to reach any location with 6 bits target address.

seq_then:    .then 6 values next, repeat, return, fork, @ default next;

label: .else width values targetlist default defaulttarget;

Exactly the same as "then" but consumed when the condition is false. Given that usually the default condition is true, and default target is "next" this field is more often used to contain a constant.

Example: just about anything is allowed in else:

seq_else:    .else 8 values next, repeat, return, fork, 0x00..0xFF, @ default next;    // any value as it can be a trace char

label .regfield width values valuelist default defaultvalue;

regfields assume that the design drives a "register" (of any length, from 1 to x bits) that will be updated at the end of the current microinstruction cycle. This is indicated with <= assignment. Compiler will enforce using the regfield only with <= assignment. 

Valuelist can contain any combination of:

Example: register can be updated by 3 possible values, or stay the same. 4 additional possibilites are undefined, but not forbidden:

reg_d:        .regfield 3 values same, alu_y, shift_dn_df, shift_dn_0 default same;    // 8 bit accumulator

It is important to note, that if microinstruction does not contain reg_d <= value, that will mean reg_d <= same ("register recirculated"). 

One measure of microinstruction efficiency is how many such "default nops" do they contain - ideally, as many as possible elements of the design should be engaged in a single microinstruction to boost parallelism. 

label .valfield width values valuelist default defaultvalue;

valfields assume driving signals in the design available immediately at the output of the microcode memory (just memory propagation delay), during the current microinstruction cycle. This is indicated by the = assignment. Compiler will enforce using the valfield only with = assignment.

Syntax of valuelist is same as for .regfield

Example: 2 valfields controlling MUXs bringing values to ALU. If not specified, ALU will operate on value of t register and data bus:

alu_r:        .valfield 2 values t, d, b, reg_hi default t;
alu_s:        .valfield 2 values bus,     d, const, reg_lo default bus;    // const comes from "else" value

.alias label definition;

Microinstructions typically contain many fields, which need to be specified together for an useful action to emerge. For clarity and to avoid bugs by omission, it is very useful to provide a shortcut for those. During the compilation, the label will be replaced verbatim therefore the end result of all the replacement must meet the syntax and logic rules. 

It is allowed to define an alias based on previously defined aliases.

Example: allow writing "trace CR" or "trace LF" which will cause calling "traceChar" routine while carrying ASCII constant which will be loaded into the reg_trace register at the end of current cycle: 

CR:        .alias 0x0D;
LF:        .alias 0x0A;
trace:     .alias reg_trace <= ss_disable_char, if true then traceChar else;

Microcode placement statements:

.org location

These are very similar to usual .org statements in assemblers - they define the location in the microcode memory where the next microinstruction will be placed. Following rules apply:

Example: simple startup sequence lasting 4 microinstruction cycles. However, first cycle could jump to any given place, but no instruction can jump to first 4 locations, as their addresses share the values with next, repeat, return, fork (that's why their labels start with _)

            .org 0;
//    First 4 microcode locations can't be used branch destinations
//    ---------------------------------------------------------------------------
_reset:        cursorx <= zero, cursory <= zero;  

_reset1:    cursorx <= zero, cursory <= zero;

_reset2:    cursorx <= zero, cursory <= zero;

_reset3:    cursorx <= zero, cursory <= zero; 

 .map pattern

This statement establishes the link between an instruction and its start address. Rules are:

Example: 1802 supports LDN R1...RF but not LDN R0, that op-code is reserve for IDL instruction. Therefore, first the generic LDN map is defined with wildcards for register number, and then overwritten with specific code for IDL:

        .map 0b0_0000_????;    // D <= M(R(N))
LDN:        exec_memread, sel_reg = n, y_bus, reg_d <= alu_y,
        if continue then fetch else dma_or_int;
        .map 0b0_0000_0000;    // override for LDN 0
IDL:        noop;
        // dead loop until DMA or INT detected
        if continue then IDL else dma_or_int;


Every non-empty, non-comment line which has no .keyword is assumed to be a microinstruction. The format is:

[label:] field [<]= value [, field [<]= value [...]][, if condition then target [else target];

Here is a 3 microinstruction routine that illustrates the above:

RNX:    reg_extend <= zero, sel_reg = n, reg_t <= alu_y, y_lo;    // T <= R(N).0 

        sel_reg = n, reg_b <= alu_y, y_hi;                    // B <= R(N).1 

        sel_reg = x, reg_r <= b_t,                            // R(X) <= B:T 
        if continue then fetch else dma_or_int;
  1.  label and alias (y_lo) is used, but there is no if/then/else, meaning that by default rules if true then next else next; will be executed by the microcode controller (execution will go to next microinstruction)
  2. no label, alias, both reg and value assignment used, all in same line, note that field ordering can be arbitrary (sel_reg = ... appears in different place)
  3. if statement explicitly specified, in separate line (still 1 microinstruction though) - "fetch" is a special microcode controller statement, while dma_or_int is a label


(Note: last 4 digits of version are month and day, other is pretty arbitrary, will go to 1.0.XXXX when sufficiently tested on other designs)


1. Support for overlapping microcode fields:

fieldname .regfield startfield..endfield

fieldname .valfield startfield..endfield

Obviously startfield and endfield need to defined before. The compiler will throw an error if that's the case, or .if / .then fields are being redefined.

This is an EXTREMELY useful feature as it allows even horizonal microcode a level of compactness. For example, it is common to have constant microcode fields, but usually only some microinstructions use it. With clever design (and extra select bit somewhere), such field can be reused for other purposes, with the right name to facilitate code readability / maintainability.

2. Support for floating point constants

32-bit value of single precisionIEEE754 representation will be instered in the specified microinstruction field. This is done in a very lazy implementation: if parsing as binary / octal / hex / decimal integer fails, there will be a ParseFloat() attempt. This means:

reg <= 1E3, ... // reg will be loaded with 0x000001E3 


reg <= 1.E3, ... // reg will be loaded with 0x447a0000