mem2hex is the simpler of two components. It is a linear memory reader, that adds some additional characters to the hex stream of memory bytes to produce a valid hex record. These records are 16 or 32 data bytes long, with the exception of last record which always has the same format (: 00 0000 01 FF)
Component structure (.vhd)
The main part are the registers that keep the state as the hex record is being assembled:
- mem_page, 3 bits long. This is the upper part of the memory address (A15..A13). It is handled separately to allow easy match with the 8 PAGE inputs to the circuit. Each of these independently enables generation of 8k block of memory as hex output. As the mem_page is incremented, a simple 3-to-8 decoder compares with current mem_page with the PAGE - if there is a match, the records are generated otherwise skipped ( page_match <= PAGE(to_integer(unsigned(mem_page))); ) . update_mem_page() process defines the operations on this register, and is simply clear, increment or stay the same
- mem_addr, 13 bits long. This is the lower part of the memory address A12..A0, that points within a byte in 8k block. update_mem_addr() process clears, increments or keeps this register same value
- checksum. 16-bit register that accumulates the checksum of each hex line. The value of this register updates at each clock cycle (lines 259 - 264), as a sum of checksum_r and checksum_s MUXs (line 280) which are both under microcode control. By default, checksum_r passes the register to one input of 16-bit adder, and checksum_s passes 0, resulting in no update. Other combinations cover address, data, complement etc. as needed to generate final checksum (only lower byte is eventually output)
- d, 8 bits long. This accepts the byte value read from external memory. update_d() process also allows this register to be loaded with constants 0 and 1 which are useful to generate the hex record, in addition to memory read and no change.
- count, 8 bits long. The update_count process updates this register by initializing it to 16 or 32 based on input pin COUNTSEL, decrementing it, or zeroing.
- CHAR, 8 bit long. This register holds the ASCII code of the character written to the output stream. The update_char() process allows it to be initialized with some constants (space, CR, LF, zero, colon) as needed for the record to be assembled, but importantly, it picks up hex to ascii conversion (4 bit to 8 - bit) through a lookup table (lines 79 - 95). The 4-bit hex value is selected by the MUX in line 332, which allows 4-bit chunks of registers to be selected for the stream, for example to generate XXXX address, 4 microinstructions are needed to drive the MUX with selections from A15...A12 to A3..A0.
Conditions: to drive the microcode logic, state of some registers must be detected, for example if count has reached zero, address / page is zero etc. These are in line 160...163.
Memory bus interface: this is a "Z80" - like interface. mem2hex behaves as a DMA-output device:
- nBUSREQ output is asserted low to demand bus
- nBUSACK input is read, and if low, means mem2hex can access bus, otherwise there is indefinite wait until this signal goes low
- nRD is asserted low to initiate read
- ADDR is driven by page and address registers
- nWAIT is read during bus read cycle, if found low, cycle is repeated, and when high, next clock transfers DBUS to d register
- when nBUSACK is low, nRD and ADDR are enabled, otherwise they are tri-state. This allows connecting these signals to common system bus
Code structure (.mcc)
Microcode starts with the definition of storage and controller unit:
.code 6, 34, mem2hex_code.mif, mem2hex_code.cgf, mem2hex_code.coe, m2h:mem2hex_code.vhd, mem2hex_code.hex, mem2hex_code.bin, 8; .mapper 8, 6, mem2hex_map.mif, mem2hex_map.cgf, mem2hex_map.coe, m2h:mem2hex_map.vhd, mem2hex_map.hex, mem2hex_map.bin, 1; .controller mem2hex_control_unit.vhd, 4;
- controller with 4 level deep stack (current + 3 subroutines deep max)
- code memory (microcode) 34 bits in width and 64 (2^6) words long (for padding, 8 bytes will be generated per word. m2h prefix will be used to differentiate from other possible files with same names in the project)
- mapper memory of 256 locations of 6 bits (usually mappers translate from instruction register to entry point in the microcode implementing that instructions, but in this case the "instruction" is the PAGE input which can be any of 256 combinations of enabling 8k block memory to be output or not. The special value 00000000 means only the hex file last record will be output). 1 byte will be used to round up the padding (as the values are just 6 bits)
After these, the fields in the microcode must be defined:
- regfields - these are assigned with <= in the microcode and indicate state will be captured at NEXT ACTIVE microcode clock edge
- valfields - assigned with = in the microcode, and indicate that the value will be held DURING CURRENT microcycle.
- if field - can occur only once and describes valid conditions to use for branching. Usually true and false (always and never) are included for convenience.
- then field - valid branch destinations. Control unit is hard-wired to recognize first 4 values as next, repeat, return, fork
- else field - same as above
Alias definitions are convenience to minimize coding repetion. They are simple forms of "macros".
// useful aliases, these are evaluated as simple text replacement of label with everything between .alias and ; goto: .alias if false then next else; noop: .alias if true then next else next; back: .alias if true then return else return;
with the above, writing "goto foo;" is a simple jump, and writing "back;" is unconditional return from subroutine etc.
The controller unit is wired to check the then and else part of the sequence and if they are same and not one of the first 4 reserved values (next, repeat, return, fork), branch to that location but pushing the current address + 1 on the stack. This is done in 1 cycle. There are two ways to achieve this in the microcode:
- use .sub pragma with label defined by it
- use "if condition then label else label" (which can be shortened as .alias)
#1 is preferred because is allows listing any number of regfield name definitions. These are not true "parameters" but handy way to indicate which register values will be set as the subroutine starts executing.
(to be continued)