Boilerplate VHDL code generation from microcode

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 11/15/2020 at 07:280 Comments

As I started working on a new design, I realized quite a bit of VHDL code needed could be generated by the microcode compiler itself. 

Typically, a micro-coded CPU/controller contains:

  1. control unit (generated using .controller statement)
  2. microcode store (generated using .code statement)
  3. instruction mapper store (optional, generated using .mapper statement)
  4. various registers that need to be updated from different sources (register + MUX)
  5. internal components (such as ALUs) fed from different sources (MUX)
  6. various lookup tables / ROMs
  7. 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:




Condition codes

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.