Typically, a micro-coded CPU/controller contains:
- control unit (generated using .controller statement)
- microcode store (generated using .code statement)
- instruction mapper store (optional, generated using .mapper statement)
- various registers that need to be updated from different sources (register + MUX)
- internal components (such as ALUs) fed from different sources (MUX)
- various lookup tables / ROMs
- other combinatorial logic
The guiding principle behind my microcode compiler and associated hardware is a pattern and template oriented approach as a trade off between increased productivity and quality vs. somewhat less flexibility and compactness. To keep with that design philosophy I improved the compiler so that in addition to 1. - 3. above it can also generate boilerplate for .4. and 5.
Basic idea is to generate a (commented) boilerplate VHDL code which developer can choose to copy and paste into the design and modify there as needed.
While there is an extra copy/paste step, the beauty of this approach that no tooling updates or touches the human developed code, they remain independent.
Below are some examples:
run "mcc CDP180X.mcc" to generate:
The control unit requires a single bit to determine if the then or else instruction will be executed. This is described in the microcode using .if instruction:
seq_cond: .if 4 values true, // hard-code to 1 mode_1805, // external signal enabling 1805/1806 instructions sync, // to sync with regular machine cycle when exiting tracing routine cond_3X, // driven by 8 input mux connected to ir(2 downto 0), and ir(3) is xor cond_4, // not used cond_5, // not used continue, // not (DMA_IN or DMA_OUT or INT) continue_sw, // same as above, but also signal to use switch mux in else clause cond_8, // not used externalInt, // for BXI (force false in 1802 mode) counterInt, // for BCI (force false in 1802 mode) alu16_zero, // 16-bit ALU output (used in DBNZ only) cond_CX, // driven by 8 input mux connected to ir(2 downto 0), and ir(3) is xor traceEnabled, // high to trace each instruction traceReady, // high if tracer has processed the trace character false // hard-code to 0 default true;
This results in VHDL code that can be copied into control unit instantiation and hooked up to the various test points in the design (note how "true" and "false" have been recognized and turned into '1' and '0'):
---- Start boilerplate code (use with utmost caution!) ---- include '.controller <filename.vhd>, <stackdepth>;' in .mcc file to generate pre-canned microcode control unit and feed 'conditions' with: -- cond(seq_cond_true) => '1', -- cond(seq_cond_mode_1805) => mode_1805, -- cond(seq_cond_sync) => sync, -- cond(seq_cond_cond_3X) => cond_3X, -- cond(seq_cond_cond_4) => cond_4, -- cond(seq_cond_cond_5) => cond_5, -- cond(seq_cond_continue) => continue, -- cond(seq_cond_continue_sw) => continue_sw, -- cond(seq_cond_cond_8) => cond_8, -- cond(seq_cond_externalInt) => externalInt, -- cond(seq_cond_counterInt) => counterInt, -- cond(seq_cond_alu16_zero) => alu16_zero, -- cond(seq_cond_cond_CX) => cond_CX, -- cond(seq_cond_traceEnabled) => traceEnabled, -- cond(seq_cond_traceReady) => traceReady, -- cond(seq_cond_false) => '0', ---- End boilerplate code
MUX, 2 to 1
alu_cin: .valfield 1 values f1_or_f0, df default f1_or_f0; // f1_or_f1 will generate 0 for add, and 1 for subtract
becomes (note the pattern to check when clause for non-default):
---- Start boilerplate code (use with utmost caution!) -- alu_cin <= df when (cpu_alu_cin = alu_cin_df) else f1_or_f0; ---- End boilerplate code
MUX, 2^n to 1
sel_reg: .valfield 3 values zero, one, two, x, n, p default zero; // select source of R0-R15 address
becomes (note the attempt to recognize "zero" as all zeros):
---- Start boilerplate code (use with utmost caution!) -- with cpu_sel_reg select sel_reg <= -- (others => '0') when cpu_zero, -- default value -- one when cpu_one, -- two when cpu_two, -- x when cpu_x, -- n when cpu_n, -- p when cpu_p; ---- End boilerplate code
Register, single update source
reg_in: .regfield 1 values same, alu_y default same; // 8 bit instruction register
Single update source means there is a 2-to-1 mux in front of the register, and one of these sources "recirculates" the value, meaning no change of value:
---- Start boilerplate code (use with utmost caution!) -- update_reg_in: process(clk, cpu_reg_in) -- begin -- if (rising_edge(clk)) then -- if (cpu_reg_in = reg_in_alu_y) then -- reg_in <= alu_y; -- end if; -- end; -- end process; ---- End boilerplate code
Obviously, the design may be triggered on falling edge of the clk, which must be changed after pasting the snippet. Note the double comments around start/end, pasting and uncommenting the selection will keep these as a warning if needed.
Register, 2^n - 1 update sources
If there is a 2^n MUX in front of a register, then 1 out of these must be used for "no change" selection. This is usually done by denoting this "same" or "nop" option as default:
reg_r .regfield 3 values same, zero, r_plus_one, r_minus_one, yhi_rlo, rhi_ylo, b_t, - default same;
This option is present in the boilerplate code, but double commented to leave a simple choice to either handle it as the "others => null" case, or put it back explicitly:
---- Start boilerplate code (use with utmost caution!) -- update_reg_r: process(clk, cpu_reg_r) -- begin -- if (rising_edge(clk)) then -- case cpu_reg_r is ---- when reg_r_same => ---- reg_r <= reg_r; -- when reg_r_zero => -- reg_r <= (others => '0'); -- when reg_r_r_plus_one => -- reg_r <= r_plus_one; -- when reg_r_r_minus_one => -- reg_r <= r_minus_one; -- when reg_r_yhi_rlo => -- reg_r <= yhi_rlo; -- when reg_r_rhi_ylo => -- reg_r <= rhi_ylo; -- when reg_r_b_t => -- reg_r <= b_t; -- when others => -- null; -- end case; -- end; -- end process; ---- End boilerplate code
In addition to "zero", the compiler also recognized "inc", "dec", "neg" and "com" to generate assumed increment, decrement, negate and 2's complement expressions.