Close

Initializing ROM during build-time from files

A project log for From bit-slice to Basic (and symbolic tracing)

Step by step from micro-coded Intel 8080 compatible CPU based on Am2901 slices to small system running Tiny Basic from the Disco Era.

zpekiczpekic 03/29/2023 at 06:420 Comments

(for related subject, run-time initialization of ROMs/RAMs, see here)

FPGA-based designs often have various read-only stores. Their content can be defined in different ways:

Last option was used in this project. There are 3 ROM stores that needed initializing and they have different formats:

Tiny Basic ROM (2k*8)

Assembling Tiny Basic source using zmac assembler produces multiple output files, one of which is in Intel hex format. Given that Xilinx was (and after AMD takeover) remains Intel / Altera competitor, it's freeware ISE 14.7 does not offer direct support  for .hex files. However, creating a parser for it is relatively straightforward, esp. if some fancier features (not used in the file that needs to be ingested) are left out. The "magic" is visible in the ROM source code file:

architecture Behavioral of rom1k is

-- function defined in the package pulls in the content of the 
-- hex file in generic parameter
constant rom: filemem(0 to (2 ** address_size) - 1) := init_filememory(filename, 2 ** address_size, default_value);
--attribute rom_style : string;
--attribute rom_style of rom : constant is "block";

begin

D <= rom(to_integer(unsigned(A))) when (nOE = '0') else "ZZZZZZZZ";

end Behavioral;

The usual inline ROM initialization similar to (note type is an array fixed in both depth and width dimensions):

type mem16x16 is array(0 to 15) of std_logic_vector(15 downto 0);
constant decode4to16: mem16x16 := (
    "1111111111111110",
    "1111111111111101",
    "1111111111111011",
    "1111111111110111",
    "1111111111101111",
    "1111111111011111",
    "1111111110111111",
    "1111111101111111",
    "1111111011111111",
    "1111110111111111",
    "1111101111111111",
    "1111011111111111",
    "1110111111111111",
    "1101111111111111",
    "1011111111111111",
    "0111111111111111"
);

is replaced by a function call (note array type has variable depth but fixed 1 byte width):


type filemem is array(natural range <>) of std_logic_vector(7 downto 0);

impure function init_filememory(file_name : in string; depth: in integer; default_value: std_logic_vector(7 downto 0)) return filemem;

This function will be invoked during build time, and it will use file_name, 2^address_size and default byte value as parameters to run. The init_filememory() function can be found in the package source file which is included in each project source as needed.

The gist of the function is a line-by-line read of the file referenced in the parameter. The beginning of the record (<colon><bytecount><address><recordtype>) is always the same, and then record type is inspected to be either 00 (data) or 01 (end of file), other types are not supported. Bytes are then parsed from rest of the line (the number of expected hex digits is known) and written to the temporary variable. When whole file is parsed, input file is closed and the temporary variable returned as result of the function call, which creates data structure compatible with the constant ROM definition file. 

This build-time initialization works for RAMs too. In that case "constant rom" should be replaced by "signal ram" and code to write content when write and select are asserted added. 

The other 2 ROMs that needed initialization are in the CPU itself. 

Mapping PROM (256*12)

The contents of this memory is taken directly from the article, and the format is:

<AAAA> <DDD>[;comments]

AAAA, DDD are hex characters to define address (00H-FFH, upper 2 are not used) and data (000H-FFFH). This simple format is easy to parse, and the code is under load_mem() function in the VHDL component file.

load_mem() and dump_mem() are wrapped into common init_wordmemory() function:

constant data_from_file: t_mem256x12 := init_wordmemory("../am9080/prom/mapper.mif", "../am9080/prom/mapper.hex", 256, uPrgAddress_nop);

This way, the side effect of loading the memory during the build time is also a generation of a "check contents" file that allows to see if all data has been properly ingested - a big sanity saver!

: 10 0000 00 086 022 0DF 06D 0AB 0AA 01B 05A 000 071 0DC 06F 0AB 0AA 01B 05D B9
: 10 0010 00 000 0E5 0DF 0F1 0AB 0AA 01B 05F 000 073 156 0F6 0AB 0AA 01B 060 ED
: 10 0020 00 000 0E9 0D3 0F3 0AB 0AA 01B 13F 000 074 0C9 0F8 0AB 0AA 01B 0C8 35
: 10 0030 00 000 0ED 02D 0F5 0A6 0A2 01E 09C 000 075 026 0FA 0AB 0AA 01B 09D 4D
: 10 0040 00 014 014 014 014 014 014 018 014 014 014 014 014 014 014 018 014 B8
: 10 0050 00 014 014 014 014 014 014 018 014 014 014 014 014 014 014 018 014 B8
: 10 0060 00 014 014 014 014 014 014 018 014 014 014 014 014 014 014 018 014 B8
: 10 0070 00 015 015 015 015 015 015 082 015 014 014 014 014 014 014 018 014 47
: 10 0080 00 034 034 034 034 034 034 035 034 03A 03A 03A 03A 03A 03A 08C 03A 3D
: 10 0090 00 0AC 0AC 0AC 0AC 0AC 0AC 0AD 0AC 0B2 0B2 0B2 0B2 0B2 0B2 0B4 0B2 0D
: 10 00A0 00 09F 09F 09F 09F 09F 09F 0B9 09F 0A0 0A0 0A0 0A0 0A0 0A0 0BC 0A0 D2
: 10 00B0 00 0A1 0A1 0A1 0A1 0A1 0A1 0BF 0A1 076 076 076 076 086 076 079 076 17
: 10 00C0 00 10D 07C 107 042 10A 064 038 055 114 050 10E 000 111 047 03F 055 D5
: 10 00D0 00 11B 0FB 115 08B 118 064 0B0 055 122 000 11C 087 11F 000 0B7 055 D9
: 10 00E0 00 129 101 123 092 126 064 0C2 055 130 09B 12A 159 12D 000 0C4 055 EC
: 10 00F0 00 137 150 131 090 134 068 0C6 055 13E 091 138 08F 13B 000 077 055 64
: 00 0000 01 FF

Looking at the contents of mapper.hex file above, it becomes obvious which 8080 instructions are implemented at which locations of microcode. Many instructions map to same microcode entry points, because they only differ by some parameter (e.g. register number) in instruction itself, for example:

014H - MOV r, r

015H - MOV M, r

018H - MOV r, M

082H - HLT (this would theoretically be MOV M, M looking at the register encoding, but such an instruction does not make sense (except as a long delay) so has been repurposed into HLT) 

Microprogram memory (512*56)

This memory is the brains of any micro-coded design. 56-bit width immediately indicates that it is a "horizontal" microcode store/design. For example, a much more complex 8086-processor has 512*21 bit microcode, which gives it away as "vertical". The contents of this data file have been created by AMDASM assembler, which I don't have. But the original article has both the listing and resulting output. So I commented the source lines and ingested only the data part in following format:

<AAAA> <b16> <b16> <b16> <b8>[; comments]

AAAA - address in hex (000H-01FFH valid)

b16 - 16-bit binary (X don't care are interpreted as 0)

b8 - 8 bit binary (X don't care are interpreted as 0)

The source code compiled during build-time again contains a helpful "dump data" function, and in this case it tries to reverse look-up the values from the microcode fields to print them in a human readable form. This provides a simple but very helpful microcode debugging and documentation technique. 

constant data_from_file: t_uinstruction512 := init_wordmemory("../am9080/prom/microcode.mif", "../am9080/prom/microcode.lst", 0, 512, uCode_default);
attribute rom_style : string;
attribute rom_style of data_from_file : constant is "block";

begin
    data <= data_from_file(to_integer(unsigned(a8)));

end Behavioral;

Below is the snippet from the generated microcode.lst file for the implementation of MOV r,r MOV r, M and MOV r, M instructions mentioned in the mapping description above. With the listing file format they start to make sense. 

-----------------------------------------------------------------------------------------
     I D DIRECT-VALUE NXT    P COND B SYSCTL OE OS   A UK S C W  AADR BADR DST   FCT  SRC
-----------------------------------------------------------------------------------------
0000 - m 038 C/R    - TRUE 0 NOC    -- INTE 0 11 0 1 16 R_PC R_PC RAMF  AND   ZA 
0001 - - 000 C/R    - TRUE 0 NOC    -- ---- 0 11 0 1 8  RAS1 R38Z RAMF  OR    DZ 
0002 - - 000 C/R    - TRUE 0 NOC    -- ---- 0 11 0 1 8  RAS1 RZ38 RAMF  AND   ZA 
0003 - - 000 R/PUSH ! TRUE 0 NOC    -- ADDR 0 11 0 1 8  RAS1 RAS1 RAMF  AND   ZA 
0004 i - =*= C/R    - RDY  0 MEMR   -- ADDR 0 11 0 1 16 R_PC R_PC RAMF  OR    ZA 
0005 - - 00C D/R    ! HOLD 0 NOC    -- ADDR 0 11 0 1 16 R_PC R_PC RAMF  ADD   ZA 
0006 (uninitialized)
0007 (uninitialized)
0008 (uninitialized)
0009 (uninitialized)
000A - - =*= R/RTN  - HOLD 0 HLDA   -- ---- 0 11 0 1 8  RAS1 RAS1 NOP   EXNOR DZ 
000B - - =*= R/F    - HOLD 0 HLDA   -- ---- 0 11 0 1 8  RAS1 RAS1 NOP   EXNOR DZ 
000C - - =*= D/R    ! HOLD 0 HLDA   -- ---- 0 11 0 1 8  RAS1 RAS1 NOP   EXNOR DZ 
000D - - =*= R/RTN  ! RDY  0 MEMR   -- ADDR 0 11 0 1 16 R_PC R_PC RAMF  OR    ZA 
000E - - =*= R/RTN  ! RDY  0 MEMW   YH ADDR 0 11 0 1 16 R_PC R_PC RAMF  OR    ZA 
000F - - =*= R/F    ! RDY  0 MEMW   YH ADDR 0 11 0 1 16 R_PC R_PC RAMF  OR    ZA 
0010 - - =*= R/F    ! RDY  0 MEMR   -- ADDR 0 11 0 1 16 R_PC R_PC RAMF  OR    ZA 
0011 - - =*= R/RTN  ! RDY  0 MEMR   -- ADDR 0 11 0 1 16 R_SP R_SP RAMF  OR    ZA 
0012 - - =*= R/RTN  ! RDY  0 MEMW   YH ADDR 0 11 0 1 16 R_SP R_SP RAMF  OR    ZA 
0013 - - =*= R/RTN  ! RDY  0 MEMW   YL ADDR 0 11 0 1 16 R_SP R_SP RAMF  OR    ZA 
0014 - - 00B R/F    - HOLD 1 NOC    -- ---- 1 11 0 1 8  RAS1 RAS1 RAMF  OR    ZA 
0015 - - 00A C/SBR  ! HOLD 0 NOC    -- ADDR 0 11 0 1 16 R_HL RAS1 NOP   OR    ZA 
0016 - - 00A C/SBR  ! HOLD 0 NOC    -- DATA 1 11 0 1 8  RAS1 RAS1 NOP   OR    ZA 
0017 - - =*= R/F    ! RDY  0 MEMW   YH ADDR 0 11 0 1 16 R_PC R_PC RAMF  OR    ZA 
0018 - - 00A C/SBR  ! HOLD 0 NOC    -- ADDR 0 11 0 1 16 R_HL RAS1 NOP   OR    ZA 
0019 - - 00D C/SBR  - RDY  0 MEMR   -- ADDR 0 11 0 1 16 R_PC R_PC RAMF  OR    ZA 
001A - - 00B R/F    - HOLD 1 NOC    -- ---- 0 11 0 1 8  RAS1 RAS1 RAMF  OR    DZ 
001B - - 00D C/SBR  - RDY  0 MEMR   -- ADDR 0 11 0 1 16 R_PC R_PC RAMF  ADD   ZA 
001C - - 00B R/F    - HOLD 1 NOC    -- ---- 0 11 0 1 8  RAS1 RAS1 RAMF  OR    DZ 
001D - - FFF R/F    ! TRUE 0 HLDA   FL INTE 0 11 0 1 8  RAS1 RAS1 NOP   EXNOR DZ 
001E - - 00D C/SBR  - RDY  0 MEMR   -- ---- 0 11 0 1 8  RAS1 RAS1 NOP   EXNOR DZ 
001F - - 00A C/SBR  ! HOLD 0 NOC    -- DATA 0 11 0 1 8  RAS1 RAS1 RAMF  OR    DZ 

Discussions