EMZ1001A microcontroller implementation is contained in 2 VHDL source files:
- emz1001_package.vhd - contains definitions of types (e.g. memory "formats") and functions used in more than one place in the design. The main helper here is a rudimentary Intel .hex file reader that allows memory initialization during build time (so that the memory image with proper content is included in the .bit stream uploaded to FPGA)
- EMZ1001A.vhd - contains all of the processor logic
As of 2022-12-20, everything described in Iskra / AMI documentation is implemented, except:
- BRK instruction (op code 0x01) - I could not find info what did it do in real device, so right now it is doing a NOP
- KREF signal - this is of little use on this purely digital system with no analog components. However, it could be implemented by emulating KREF and Kx signals as PWM and then comparing duty cycle ratio between them and if Kx >= KREF, Kx would read as 1
- Test mode - when ROMS is fed back with negated SYNC, CPU enters test mode which outputs on D lines content of internal ROM. Of great use on "black box" real device, it is of little use here when all the guts of the microcontroller are wide open, including the firmware ROM content placed there
The "detailed block diagram" from the documentation is not extremely helpful with recreating the device, but still gives some useful hints.
- A lines are complex - they are either data outputs from a cascade of 2 13-bit latches, or output 13-bit instruction address if accessing external memory
- D lines are even more complex - there are 2 output modes (OUT / DISPx) and 2 input modes (INP and reading instruction from external memory), all dependent of CPU mode of operation, SYNC state and instruction
- Stack depth is 3 (or 4), but width is only 10 bits - 3 bits to select 1k bank are not on the stack. This means that it is possible to JMP from bank to bank, but not JMS (you can go, but can't return :-))
- PP staging registers (page, bank, and which one is next to be updated) are indicated with "PREP" on the diagram
- Some internal paths are 8-bit which is interesting for a 4-bit CPU as it allows higher internal bandwidth for simultaneous execution of multiple operations
Timing
EMZ1001A has a very rigid timing: all instructions are executed in 1 machine cycle, machine cycle has always 4 clock cycles (T1, T3, T5, T7) and 2 phases (SYNC low - instruction fetch, and SYNC high - instruction decode). Implementation presented here is cycle accurate and follows the real device based on what I could infer from documentation. 2 outside clock sources are consumed, which drive 3 "processes" (VHDL term for defining how registers are updated):
Clock | CLK (CPU operating frequency) | I3 (assumed to be A/C mains 50Hz or 60Hz) |
low to high transition | capture state of RUN, K, I inputs during cycles T3, T5, T7 respectively on_clk_up: process(CLK, nPOR) | count up until limit set by EUR is reached at which point set a flag that can be consumed by SOS to skip on_i_clk: process(nPOR, i_clk, sos_clr) |
high to low transition | Advance through T1, T3, T5, T7 cycles (using a 1-hot ring counter) and tie almost all of internal register updates based on cycle, run mode, current instruction on_clk_down: process(CLK, nPOR) | N/A |
async (reset condition) | Initialize internal registers (except 64 nibble RAM) | clear counter on reset, clear flag on reset and SOS execution |
Main action is in the on_clk_down: process(CLK, nPOR). RUN and SKIP indicate the states of ir_run and ir_skp flags.
Always | RUN | RUN | NO RUN | NO RUN | |
At the end of: | SKIP | NO SKIP | SKIP | NO SKIP | |
T1 | Capture ROMS state in the middle of SYNC low | - | - | - | - |
T3 | - | Load instruction register with NOP (0x00) | Load instruction register from ROM(PC) | - | - |
T5 | Capture ROMS state in the middle of SYNC high | Update skip flag Increment PC Execute NOP | Update skip flag Increment PC Execute all instructions except JMP, RT, RTS JMS: increment stack pointer | - | - |
T7 | - | - | JMP and JMS: update PC based on 6-bits in the instruction and state of PP prepared registers RT, RTS: decrement stack pointer | - | - |
Some notes:
- PC never actually "skips" - lower 10 bit always increment by 1, SKIP is just "force feeding" instruction register (ir_current) with NOP, so skipping means execute a NOP instead of next instruction
- When JMS increments stack pointer (ir_sp) at the end of T5, the PC already advanced by 1, meaning that the stack level below will contain the right return address which is 1 + current PC
- JMP and JMS executed at end of T7 have almost same logic, but JMP will "overwrite" PC in current stack level, while JMS will do so for the "new" level, because ir_sp has already been incremented. The difference in logic is that if page register is not set, JMS will go to page 15, and JMP will stay on same page
- There is no difference between RT (return) and RTS (return and skip) from process perspective, both just decrement the stack pointer.
- Skip state left from previous instruction is consumed at T3 and new skip state is set at end of T5. This ensures that correct value of SKIP flag is presented to STATUS output during T7 (same works for other 3 flag states STATUS brings out from the device)
-- STATUS is a 4 to 1 mux
STATUS <= (t1 and (not mr_d_driven)) or (t3 and bl_is_13) or (t5 and mr_cy) or (t7 and ir_skp);
- Capturing the state of ROMS pin during SYNC low and SYNC high into 2-bit ir_roms register is critical because it determines the microcontroller mode of operation:
-- decide if internal or external ROM is used
with ir_roms select ir_introm <=
not(ir_bank(2) or ir_bank(1) or ir_bank(0)) when "00", -- LOW, internal for bank0, external for others
'0' when "01", -- SYNC, always external
'1' when "10", -- /SYNC, not implemented test mode, assume "internal"
'1' when others; -- HI, always internal
A/C frequency timer
An interesting feature is a simple divide by 60 or divide by 50 timer which allows 1 second intervals to be measured in simplest way. It was probably added for apps where simple but not to precise clock is needed, like microwave, dishwasher, maybe a toaster oven.
- First, A/C input (through some galvanic decoupling and voltage limiting) must be connected to input I(3)
- Execute EUR with right values. These values are stored into a 2-bit register ir_eur, value of which is address to a lookup table that determined XOR mask and counter limit
... when opr_eur => -- EUR ir_eur <= mr_a(3) & mr_a(0); -- middle 2 bits are ignored ... -- EUR lookup constant eur_lookup: mem4x14 := ( O"73" & X"FF", -- 00 60Hz, inverting O"73" & X"00", -- 01 60Hz, not inverting (power-up default) O"61" & X"FF", -- 10 50Hz, inverting O"61" & X"00" -- 11 50Hz, not inverting );
3. When the counter reaches the limit, ir_sec flag is set, which is consumed as skip condition for SOS instruction. If set, SOS asynchronously resets this flag, allowing another 1 second to elapse and be detected.
-- 1 second timer logic
sos_clr <= (ir_run and ir_sec and t7) when (opr = opr_sos) else '0';
on_i_clk: process(nPOR, i_clk, sos_clr)
begin
if ((nPOR = '0') or (sos_clr = '1')) then
ir_sec <= '0';
if (nPOR = '0') then
ir_cnt <= (others => '0');
end if;
else
if (rising_edge(i_clk)) then
if (ir_cnt = eur_limit) then -- if 50 or 60 reached
ir_cnt <= (others => '0'); -- reset counter
ir_sec <= '1'; -- set 1 second flag
else
ir_cnt <= std_logic_vector(unsigned(ir_cnt) + 1);
end if;
end if;
end if;
end process;
This is as close as EMZ1001A can get to timer interrupt - main loop of the program (e.g. updating LEDs, scanning keyboard) needs to run all the time, with SOS places somewhere in that loop to detect events 1 second apart.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.