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.
INVOKING THE COMPILER
Starting with -h command line argument lists the usage:
>mcc.exe -h -------------------------------------------------------- -- mcc V0.9.0627 - Custom microcode compiler (c)2020-... -- https://github.com/zpekic/MicroCodeCompiler -------------------------------------------------------- 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 https://hackaday.io/project/172073-microcoding-for-fpgas
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").
GENERAL SYNTAX RULES FOR SOURCE.MCC
The mcc source file is a text file with extension .mcc with few general rules:
- each statement must end with ;
- statements can go into multiple lines (encouraged for clarity)
- if the statement is a microinstruction, a comma delimits a microcode field, and semicolon the instructions, meaning "1 semicolon = 1 microcode cycle"
- labels end with colon (by convention, but not enforced), and follow the usual rules (can start with _ or alpha characters, but may contain no special characters except _)
- labels starting with _ cannot be jumped to (this is useful to explicitly forbid some jump destinations)
- everything is case-insensitive (but lowecase is encouraged)
- comments - everything after // until end of line is ignored. Currently, no multi-line comments are supported
- constants can be given as decimal, hex (0x...), octal (0o...), or binary (0b...). In some cases (for example .org), the binary/octal/hex can contain ? wildcard, which indicated 1, 3, or 4 "don't care" bits. In addition 'char' constant will be represented by its basic ASCII code.
- from the compiler perspective, source.mcc contains only:
- statements (keywords starting with dot)
Following statements are currently recognized by mcc.exe:
Design definition statements:
Reserves memory for microcode:
- 2^depth will be the number of words
- each word will be <width> words wide - this must be equal or bigger than the sum of all the field widths
- successful compile will produce all the files in the <filelist>, they will all contain same data but described according to file format
- <bytewidth> will be used for .hex file format to have that record size (must be equal or greater than <width>
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;
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;
- Currently, only .vhd output file format is supported
- stackdepth must be between 2 and 8 (inclusive)
Example: generate standard microcode controller, with 4 level stack depth, rising edge clock:
.controller tty_control_unit.vhd, 4;
Microcode field definition statements:
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 char_is_zero, cursorx_ge_maxcol, cursory_ge_maxrow, cursorx_is_zero, cursory_is_zero, memory_ready, false // hard-code to 0 default true;
- width must match the microcode depth
- targetlist can contain:
- predefined commands (next, repeat, return, fork) - if the order of these is changed, the generated controller.vhd file must be updated
- @ meaning any valid label for jump
- range (from .. to) - useful to allow reuse of field to bring in constants to microinstruction, for example if condition is "false" then obviously "then" field will never be used so its width can be reused for a constant value to be consumed by the microinstruction.
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;
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
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:
- alphanumeric symbols (symbol will be assigned to a value based on order encountered)
- range (value assigned will be checked to be in the range)
- dash (-) meaning that value in the order is forbidden to be assigned
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.
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
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:
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:
- location can be any between 0 and 2^depth - 1
- locations can be defined in increasing order only, gaps are allowed, going back isn't
- location value cannot contain wildcards
- at least one .org must be present (usually .org 0 to set the location for microinstruction executed at reset)
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;
This statement establishes the link between an instruction and its start address. Rules are:
- pattern can be any value between 0 to 2^mapper depth - 1
- pattern can contain wildcards if specified as hex, binary or octal
- order of map statement is important! Most generic map statements (with most wildcard bits) must be put first, and more restrictive after. This way mappings can be targeted to specific instruction register bit combinations.
- multiple map statements can be put one after the other - this means multiple instruction patterns will map to same microcode entry point.
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];
- Field and value are given when describing the microcode fields (.regfield or .valfield)
- The number of field/values can be from 1 to total number of fields in the design, but each can only be used once
- If field/value is not specified, the default is used (that is why selecting right defaults is crucial for a good microcode design!)
- For readability, it is useful to put each field/value in own line, end it with comma, and if/then/else on last line with semicolon
- any number of field/value and also if/then/else can be replaced with symbol defined in .alias
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;
- 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)
- 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)
- 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