Any processor - no matter how simple or complex - is of little use if it cannot execute some program. I could not find any listing or ROM code for any EMZ1001A application, the closest was this note about using EMZ1001A as a DMTF frequency signal decoder. In addition, executing a program is the only way to test the instructions, so I decided to write 2 "apps" to validate the processor and demo it.
Next problem was lack of development toolchain. AMI documentation describes a sophisticated development system that includes hardware and software - with a macro-assembler. Even if this could be found today, it would be not practical to integrate with PC-based FPGA toolchain, so only option was to write own.
One approach would be to leverage already existing tools, for example a universal cross-assembler. Then I realized, I already have written one myself - my micro-code compiler!
How does a 2-pass microcode compiler become a 2-pass assembler? Not easily, except for lucky coincidence that EMZ1001A uses only 8-bit op-codes, which is exactly the same as horizontal microcode, where each microinstruction has exactly the same width (usually 20 bits +), and the number of microinstruction formats is very limited (often times, only 1 format, meaning each field in microinstruction has same meaning in each microinstruction).
Microcode compiler allows defining multiple fields in the microinstruction (from emz.mcc include file):
// define any slices of the instruction word format
f76 .valfield 2 values * default 0;
f54 .valfield 2 values * default 0;
f32 .valfield 2 values * default 0;
f10 .valfield 2 values * default 0;
With these 2-bit fields, EMZ1001A instructions formats can be expressed as:
Format | Layout o - opcode bit U - operand bit that must appear inverted in code X - operant bit that must appear not-inverted in code | Sample instructions | .mcc definition |
---|---|---|---|
8+0 | oooooooo | NOP...DISN | opr8 .valfield f76 .. f10 values * default 0; |
6+2 | ooooooUU | SZM, STM, RSM, LB* | opr6 .valfield f76 .. f32 values * default 0; val2 .valfield f10 .. f10 values * default 0; |
6+2 inverted | ooooooUU | XC*, LAM | opr6 .valfield f76 .. f32 values * default 0; val2 .valfield f10 .. f10 values * default 0; |
4+4 | ooooXXXX | LAI, ADIS | opr4 .valfield f76 .. f54 values * default 0; val4 .valfield f32 .. f10 values * default 0; |
4+4 inverted | ooooUUUU | PP | opr4 .valfield f76 .. f54 values * default 0;
val4 .valfield f32 .. f10 values * default 0; |
2+6 | ooXXXXXX | JMP, JMS | opr2 .valfield f76 .. f76 values * default 0;
val6 .valfield f54 .. f10 values * default 0; |
To differentiate between inverting and non-inverting formats, simply the "macro" includes the ! symbol which will appear before the operand and result in inverted bits in the binary:
// 2-bit operand, inverted (0x30 .. 0x3F)
XCI .alias opr6 = 0b001100, val2 = 3 & !;
XCD .alias opr6 = 0b001101, val2 = 3 & !;
XC .alias opr6 = 0b001110, val2 = 3 & !;
LAM .alias opr6 = 0b001111, val2 = 3 & !;
// 2-bit operand, not inverted (0x40 .. 0x4F)
LBZ .alias opr6 = 0b010000, val2 =;
LBF .alias opr6 = 0b010001, val2 =;
LBE .alias opr6 = 0b010010, val2 =;
LBEP .alias opr6 = 0b010011, val2 =;
With this, it is possible to easily define each EMZ1001A instruction and write assembly code similar to the original (main difference is that each line must end in ; and that the comment character is different //)
In summary, I had to make 3 changes to existing microcode-compiler:
- Support the include directive so I can include the definition of fields and instructions in each assembly program (implementation line 467+: #include simply recursively calls the Pass0() in which it has been encountered)
- Fix various bugs in the field definitions when "virtual" fields contain one or more "real fields" (in this example fields f76..f10 are real, but opr8 which spans all of them is virtual. Values can be assigned only to one within the instruction)
- Allow to use file extension names beside .mcc (so the "apps" have extension .emz for convenience)
- Support more expressions and their evaluation. Implementation can be seen in lines 140+ in the monster GetValueAndMask() function - it is a great example how external pressures can lead to bad code. I wanted to move along with the project, so instead of rewriting into a well-known single or double stack based evaluation algorithm, I modified existing function to call itself recursively until reaches a leaf node which is either <constant> or <constant><operator><constant>. Recursive algorithm is much worse because at each level it partially reparses the expression.
With a working assembler, the toolchain becomes simple:
- Write .emz assembly file in any editor, include emz1001.mcc
- Compile with commands similar to:
@echo off @echo -- Compile code for internal and external ROMs @echo See https://github.com/zpekic/MicroCodeCompiler ..\..\..\Sys9900\mcc\mcc\bin\Debug\mcc.exe /a:emz Fibonacci.emz ..\..\..\Sys9900\mcc\mcc\bin\Debug\mcc.exe /a:emz HelloWorld.emz @echo -- Convert image into 8*8 pixel character generator ROM @echo See https://github.com/zpekic/Sys_TIM-011/tree/master/Img2Tim ..\..\..\Sys_TIM-011\Sys_TIM-011\Img2Tim\Img2Tim\bin\Debug\img2tim.exe ..\doc\iskra-logo.bmp -x -c
- If successful, .hex file will be generated describing the binary
- Feed the .hex file path in the ROM definition as a generic parameter in the VHDL project:
-- 1k of internal ROM contains the "Hello world!" program
firmware: rom1k generic map(
filename => "..\prog\helloworld_code.hex",
default_value => X"00" -- NOP
)
port map(
D => introm,
A => pc,
nOE => '0'
);
Note: Xilinx (now AMD) as Intel competitor of course does not support natively loading .hex files during build time, but a simple file reader included in the project package file (see init_filememory() function) I wrote is sufficient.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.