SFD-700 mkII Developer's Guide
SFD-700 mkII Developer's Guide
This guide provides a thorough walkthrough of the SFD-700 mkII programmable logic source code, with particular attention to explaining VHDL concepts and idioms for readers who are not familiar with hardware description languages. Every process and concurrent signal assignment in the CPLD design is explained in plain English, alongside the VHDL that implements it.
For hardware architecture and register maps see the Technical Guide. For installation and usage see the User Manual.
Introduction to VHDL for Non-VHDL Programmers
VHDL (VHSIC Hardware Description Language, where VHSIC stands for Very High Speed Integrated Circuit) is not a programming language in the traditional sense — it does not describe a sequence of steps that a processor executes one at a time. Instead, VHDL describes hardware circuits: logic gates, flip-flops, multiplexers, and their connections. When a VHDL design is compiled (synthesised) for a CPLD or FPGA, the tools translate your description into a netlist of actual gates and registers that are permanently wired into the device.
Everything Runs Simultaneously
The most important thing to understand about VHDL is that all processes and signal assignments in an architecture run concurrently. Unlike a program where line 10 executes after line 9, in VHDL every block of logic is active and responding to its inputs at all times — just like the real hardware it describes.
In the SFD-700 CPLD, the clock divider, the drive register, the I/O decoder, and every other block are all active simultaneously, 24 hours a day, from the moment the CPLD is powered on.
Entities, Architectures, and Ports
A VHDL design has two parts:
Signals
- The entity is the external interface — it lists the input and output pins. Think of it as the component's pin diagram. In the SFD-700, the entity lists every physical CPLD pin: Z80_ADDR, Z80_DATA, FDCn, DRQ, MODE, CLK_16M, and so on.
- The architecture is the internal implementation — it describes the logic that connects the pins. This is where the processes and signal assignments live.
rtl (Register Transfer Level — a conventional name meaning the design is described in terms of data flowing between registers).
Signals are the internal wires of the design. They are declared between the
Processes
architecture keyword and the begin keyword. A signal can carry a single bit (std_logic), a bus of bits (std_logic_vector), or an integer. In the SFD-700, signals like FDC_SELni (the internal wire that indicates an FDC I/O cycle is happening) are signals — they are not external pins, just internal connections between logic blocks.
The n suffix in signal names (e.g. FDC_SELni) is a convention meaning "active low" — the signal is asserted (meaningful, active) when it is logic zero ('0'), and deasserted when it is logic one ('1').
A process is a block of code that describes sequential (clocked) or combinatorial logic. Processes are enclosed in
Data Types Used
process(...) begin ... end process and contain the familiar constructs: if, case, variable assignments.
A clocked process describes flip-flop registers — logic that captures a value at a specific instant (the clock edge) and holds it until the next clock edge. This is the basis of all registered state in the CPLD: page registers, drive selection registers, interrupt enable, and memory management flags. A clocked process always contains if rising_edge(CLK) then.
A combinatorial process or concurrent signal assignment describes pure logic gates — output is an immediate function of inputs, with no storage. The I/O decode logic in the SFD-700 is combinatorial: whenever the Z80 address bus and control signals change, the select outputs update immediately (within the CPLD's propagation delay).
std_logic— a single binary signal that can be '0', '1', 'Z' (high-impedance / tri-state), or various other meta-values used in simulation. '0' and '1' are the two real hardware states. 'Z' means the signal is not being driven — in bi-directional bus applications this means "hands off the bus".std_logic_vector(N downto 0)— a bus of N+1 bits. Thedowntomeans bit N is the most significant bit. Z80_ADDR isstd_logic_vector(15 downto 0)— a 16-bit address bus where bit 15 is the MSB.integer range 0 to 7— a bounded integer used here for IFMODE (the machine mode) so thatcaseand comparison statements work naturally.boolean— true or false. The helper functionto_std_logic(L: boolean)converts a boolean expression to a std_logic '0' or '1'.
Source Tree
The SFD-700 mkII VHDL source files are in the
CPLD/v1.2/ directory of the repository:
| File | Role |
|---|---|
sfd700_pkg.vhd |
Package — constants, machine mode values, utility functions |
sfd700_Toplevel.vhd |
Top-level entity — maps CPLD physical pins to the implementation |
sfd700.vhd |
Implementation — all logic processes and signal assignments |
build/sfd700.qpf |
Quartus II project file |
build/sfd700.qsf |
Quartus II settings file (pin assignments, device settings) |
build/output_files/sfd700.pof |
Compiled CPLD programming file |
CUPL/ |
GAL CUPL sources for v1.0/v1.1 boards |
Package File: sfd700_pkg.vhd
The package file is a shared library of constants and functions that the other VHDL files can use. Think of it as a C header file — it defines common symbols so you do not have to repeat magic numbers throughout the design.
It is declared as
Logic State Constants
package sfd700_pkg and used by the other files via use work.sfd700_pkg.all.
Rather than writing '1' or '0' throughout the design, the package defines named aliases:
constant YES : std_logic := '1'; -- Assertion: yes, true, active constant NO : std_logic := '0'; -- Negation: no, false, inactive constant HI : std_logic := '1'; -- Signal is at logic high (voltage) constant LO : std_logic := '0'; -- Signal is at logic low (ground) constant ONE : std_logic := '1'; -- Binary one constant ZERO : std_logic := '0'; -- Binary zero constant HIZ : std_logic := 'Z'; -- High impedance: not driving the bus
These are stylistic —
Machine Mode Constants
YES and '1' are identical in hardware. The intent is to make the code self-documenting. When you see ROM_CSn <= NO (chip select driven inactive high) the meaning is instantly clear.
The three MODE jumper bits can represent values 0–7. Rather than comparing the IFMODE signal to bare numbers, the package gives them names:
constant MODE_MZ1200 : integer := 0; -- Sharp MZ-1200 (alias of MZ-80A) constant MODE_MZ80A : integer := 0; -- Sharp MZ-80A constant MODE_MZ700 : integer := 1; -- Sharp MZ-700 constant MODE_MZ80B : integer := 2; -- Sharp MZ-80B constant MODE_MZ800 : integer := 3; -- Sharp MZ-800 constant MODE_MZ1500 : integer := 4; -- Sharp MZ-1500 constant MODE_MZ2000 : integer := 5; -- Sharp MZ-2000 constant MODE_MZ2200 : integer := 6; -- Sharp MZ-2200
Notice that MODE_MZ1200 and MODE_MZ80A both equal 0 — these two machines are sufficiently similar in their bus timing and ROM requirements that the same logic serves both.
Utility Functions
Several utility functions are defined and used elsewhere:
IntMax(a, b) — returns the larger of two integers. Used in parameter calculations.
log2ceil(arg) — returns the number of bits needed to represent a positive integer. For example,
log2ceil(128) = 7. Used to calculate bus widths automatically from capacity parameters rather than hardcoding.
clockTicks(period, clock) — given a time period in nanoseconds and a clock frequency in Hz, returns the number of whole clock cycles that fit in that period. Useful for calculating timer pre-load values or wait-state counts at synthesis time.
reverse_vector(slv) — reverses the bit order of a standard logic vector. Used when bus bits must be reordered (for example, when connecting to a peripheral that uses the opposite bit-endianness convention).
to_std_logic(i) — converts integer 0 to std_logic '0', and any other integer to std_logic '1'. Allows integer conditions to be used directly where a std_logic value is needed.
bit_to_integer(s) — converts a std_logic bit to a natural integer (0 or 1). Useful for using a single bit as an array index.
Top Level File: sfd700_Toplevel.vhd
The top-level file defines the entity
sfd700 — the interface that Quartus maps to the physical CPLD pins. Its role is purely structural: it declares the same port list as the implementation entity (cpld128 in sfd700.vhd) and then instantiates that implementation, wiring every port through with a direct 1:1 mapping.
Why have a separate top-level file? In VHDL design practice, separating the top-level "chip boundary" entity from the implementation entity is good hygiene — it makes it easy to swap in a simulation testbench, instantiate the design inside a larger design, or add wrapper logic (such as JTAG boundary-scan cells) without touching the core logic. For the SFD-700, the split is clean: sfd700 is what the physical device looks like from the outside, and cpld128 is what it does on the inside.
architecture rtl of sfd700 is
begin
cpldl128Toplevel : entity work.cpld128
port map
(
Z80_ADDR => Z80_ADDR,
Z80_DATA => Z80_DATA,
Z80_M1n => Z80_M1n,
Z80_RDn => Z80_RDn,
...
CLK_16M => CLK_16M,
CLK_FDC => CLK_FDC,
CLK_BUS0 => CLK_BUS0
);
end architecture;
The
port map statement (with => between port name and signal name) connects the external pin names of sfd700 to the corresponding pins of the cpld128 component. Since both entities have identical port names, every connection is portname => portname.
The Altera-specific library altera; use altera.altera_syn_attributes.all; imports Altera synthesis directives that allow per-pin attributes (such as drive strength and pin locking) to be specified. These are used by Quartus during place-and-route.
Main Logic File: sfd700.vhd
This file contains the entity
Entity and Port Declarations
cpld128 and its rtl architecture — the entire functional logic of the CPLD. Every process and concurrent assignment is documented below.
The entity port list groups signals into seven logical categories:
entity cpld128 is
port (
-- Z80 Address Bus
Z80_ADDR : in std_logic_vector(15 downto 0); -- 16-bit address from host Z80
-- Z80 Data Bus (bidirectional)
Z80_DATA : inout std_logic_vector(7 downto 0); -- 8-bit data — can be driven or read
-- Z80 Control Signals
Z80_M1n : in std_logic; -- Machine Cycle 1 (active low) — Z80 is fetching an opcode
Z80_RDn : in std_logic; -- Read strobe (active low)
Z80_WRn : in std_logic; -- Write strobe (active low)
Z80_IORQn : in std_logic; -- I/O Request (active low)
Z80_MREQn : in std_logic; -- Memory Request (active low)
Z80_INT : out std_logic; -- Interrupt to Z80 (active high)
Z80_EXWAITn : out std_logic; -- External wait (active low, stretches bus cycle)
Z80_RESETn : in std_logic; -- System reset (active low)
-- Inverted Data / ROM-RAM Upper Address Bus
ID : inout std_logic_vector(7 downto 0); -- Dual-purpose: inverted FDC data OR upper ROM/RAM address
-- ROM/RAM Control
ROM_A10 : out std_logic; -- ROM page bit / MZ-80A DRQ mux
RAM_A10 : out std_logic; -- RAM page bit
ROM_CSn : out std_logic; -- Flash ROM chip select (active low)
RAM_CSn : out std_logic; -- SRAM chip select (active low)
RSV : out std_logic; -- Reserved output pin
-- Host Machine Mode
MODE : in std_logic_vector(2 downto 0); -- 3-bit mode jumper
-- Floppy Disk Interface
FDCn : out std_logic; -- WD1773 chip select (active low)
INTRQ : in std_logic; -- WD1773 command-complete interrupt
DRQ : in std_logic; -- WD1773 data request
DDENn : out std_logic; -- Double density enable (active low)
SIDE1 : out std_logic; -- Disk head side select
MOTOR : out std_logic; -- Spindle motor enable
DRVSAn : out std_logic; -- Drive A chip select (active low)
DRVSBn : out std_logic; -- Drive B chip select (active low)
DRVSCn : out std_logic; -- Drive C chip select (active low)
DRVSDn : out std_logic; -- Drive D chip select (active low)
-- Clocks
CLK_16M : in std_logic; -- 16 MHz crystal oscillator
CLK_FDC : out std_logic; -- 8 MHz to WD1773
CLK_BUS0 : in std_logic -- Host Z80 bus clock (reserved)
);
end entity;
Port directions:
Internal Signal Declarations
in— input to the CPLD (driven by external hardware).out— output from the CPLD (driven by the CPLD logic).inout— bidirectional: the CPLD can both read and drive the signal. Used for data buses (Z80_DATA, ID) where the direction changes depending on bus cycle type.
Between the architecture's
is and begin keywords, all internal wires (signals) are declared. These are not physical pins — they are the "wires" connecting logic blocks inside the CPLD. A selection of the most important ones:
-- Address decode select signals (internal wires, not physical pins) signal FDC_SELni : std_logic; -- '0' when a WD1773 register is being accessed signal DRIVE_WR_SELni : std_logic; -- '0' when port 0xDC is being written signal DDEN_WR_SELni : std_logic; -- '0' when port 0xDE is being written signal SIDE_WR_SELni : std_logic; -- '0' when port 0xDD is being written signal INTEN_SELni : std_logic; -- '0' when port 0xDF is being accessed signal EXXX_WR_SELni : std_logic; -- '0' when port 0x60 is being written signal FXXX_WR_SELni : std_logic; -- '0' when port 0x61 is being written signal MEM_EXXX_SELni : std_logic; -- '0' when memory address is E300h-EFFFh signal MEM_FXXX_SELni : std_logic; -- '0' when memory address is F000h-FFFFh -- Registered state (these become flip-flops inside the CPLD) signal REG_DRIVEA : std_logic; -- Drive A selected signal REG_DRIVEB : std_logic; -- Drive B selected signal REG_DRIVEC : std_logic; -- Drive C selected signal REG_DRIVED : std_logic; -- Drive D selected signal REG_MOTOR : std_logic; -- Motor running signal REG_SIDE : std_logic; -- Disk head side signal REG_DDEN : std_logic; -- Double density mode active signal REG_INT : std_logic; -- Interrupt enable signal REG_EXXX_PAGE : std_logic_vector(6 downto 0); -- 7-bit EXXX page number signal REG_FXXX_PAGE : std_logic_vector(6 downto 0); -- 7-bit FXXX page number signal REG_ROMDIS : std_logic; -- MZ-700: ROM disabled (DRAM mapped) signal REG_ROMINH : std_logic; -- MZ-700: ROM inhibited entirely signal REG_RAMEN : std_logic; -- SRAM (not ROM) is active -- Machine mode (captured from jumper on reset) signal IFMODE : integer range 0 to 7 := 0; -- Clock signal CLK_8Mi : std_logic := '0'; -- 8 MHz divided clock (internal)
Process: FDCCLK — Clock Divider
FDCCLK: process( CLK_16M )
begin
if(rising_edge(CLK_16M)) then
CLK_8Mi <= not CLK_8Mi;
end if;
end process;
What it does: This process contains a single toggle flip-flop that divides the 16 MHz primary clock by two, producing the 8 MHz clock required by the WD1773.
How it works: The sensitivity list
( CLK_16M ) means "re-evaluate this process whenever CLK_16M changes". The if rising_edge(CLK_16M) test means "only act on the rising edge (0→1 transition) of the clock". On every rising edge, CLK_8Mi is inverted — toggling between '0' and '1'. This is a perfect divide-by-two: one full cycle of CLK_8Mi spans two cycles of CLK_16M, giving exactly 8 MHz.
Hardware result: One D flip-flop with Q connected back to its own complement input (D = not Q). The output Q is CLK_8Mi, which is then driven straight out of the CPLD on the CLK_FDC pin.
Why 8 MHz? The WD1773 datasheet specifies an 8 MHz input clock for standard MFM floppy operations. The chip uses this to time the bit-cell windows and PLL lock for data separation.
Process: SETMODE — Mode Register Latch
SETMODE: process( Z80_RESETn, MODE )
begin
if(Z80_RESETn = '0') then
IFMODE <= to_integer(unsigned(MODE));
end if;
end process;
What it does: Samples the three MODE jumper input pins and stores their value as an integer in IFMODE whenever the system is held in reset.
How it works: The sensitivity list includes both Z80_RESETn and MODE. Whenever either changes, the process runs. If Z80_RESETn is low (system in reset), IFMODE is loaded from the MODE pins. The conversion chain
to_integer(unsigned(MODE)) converts the three-bit std_logic_vector MODE into an integer 0–7.
Important detail: This process has no clock edge — it is level-sensitive to Z80_RESETn being low. This means IFMODE is updated throughout the entire duration of a reset, not just on the rising or falling edge. In synthesis, this creates a simple asynchronous register load rather than a clocked flip-flop. The IFMODE value is stable as long as Z80_RESETn is held asserted, then held by the flip-flop output when reset is released.
Why latch on reset? The MODE jumper is a PCB jumper set before power-on. Latching it on reset ensures the design reads a stable value (jumpers are debounced by the reset duration) and that any glitches during power-up don't corrupt the mode selection. Once reset is released, IFMODE is fixed for the session.
IFMODE usage: IFMODE is tested in the concurrent signal assignments for ROM_SELni, RAM_SELni, and ROM_A10 to conditionally apply machine-specific behaviour (e.g. the MZ-80A DRQ trick, MZ-700 memory management gating).
Process: SETSIDE — Head Side Register
SETSIDE: process( Z80_RESETn, CLK_16M, SIDE_WR_SELni )
variable SIDE_SEL_LASTni : std_logic;
begin
if(Z80_RESETn = '0') then
REG_SIDE <= '0';
SIDE_SEL_LASTni := '0';
elsif(rising_edge(CLK_16M)) then
if(SIDE_WR_SELni = '0' and SIDE_SEL_LASTni = '1') then
REG_SIDE <= not Z80_DATA(0);
end if;
SIDE_SEL_LASTni := SIDE_WR_SELni;
end if;
end process;
What it does: Captures the head side selection (front or back of the floppy disk) when the Z80 writes to I/O port 0xDD.
How it works: This process uses an edge-detection technique to identify the falling edge of SIDE_WR_SELni (the active-low write select signal for port 0xDD). SIDE_SEL_LASTni is a
variable that holds the previous clock-cycle's value of SIDE_WR_SELni. On each rising clock edge, if SIDE_WR_SELni is now '0' (asserted) but was '1' last cycle, a falling edge has just occurred — this is when the Z80 write strobe is fresh and the data bus contains valid data. At that exact clock edge, REG_SIDE is loaded from Z80_DATA(0).
Variable vs. signal: SIDE_SEL_LASTni is declared as a variable rather than a signal. In VHDL, variables inside processes are updated immediately when assigned (like a normal programming language variable), while signals are only updated after the process suspends. Using a variable for "last value" storage means it reliably holds last cycle's value without the signal scheduling complexity.
Inversion: REG_SIDE <= not Z80_DATA(0) — the bit is inverted because the WD1773 side select convention is opposite to the Z80 write convention: Z80 writes D0=0 to select side 1, D0=1 to select side 0. The inversion aligns the physical SIDE1 output to the WD1773's expectation.
Reset behaviour: REG_SIDE is set to '0' on reset, which selects side 0 (top head) — the default for single-sided operations.
Process: SETDDEN — Double Density Enable
SETDDEN: process( Z80_RESETn, CLK_16M, DDEN_WR_SELni )
variable DDEN_SEL_LASTni : std_logic;
begin
if(Z80_RESETn = '0') then
REG_DDEN <= '1';
DDEN_SEL_LASTni := '0';
elsif(rising_edge(CLK_16M)) then
if(DDEN_WR_SELni = '0' and DDEN_SEL_LASTni = '1') then
REG_DDEN <= not Z80_DATA(0);
end if;
DDEN_SEL_LASTni := DDEN_WR_SELni;
end if;
end process;
What it does: Controls whether the WD1773 operates in double-density (MFM) or single-density (FM) mode. Written via I/O port 0xDE.
Structure: Identical edge-detection pattern to SETSIDE. On the falling edge of DDEN_WR_SELni (port 0xDE write), REG_DDEN is loaded as the inverse of Z80_DATA(0). The WD1773 DDENn input is active-low: driving it low enables double density. The Z80 software convention is D0=0 for double density — the inversion maps this correctly.
Reset value: REG_DDEN = '1' on reset means DDENn is driven high = single density selected by default. In practice, the firmware immediately sets double density before any FDC command.
Process: SETDRIVE — Drive and Motor Control
SETDRIVE: process( Z80_RESETn, CLK_16M, DRIVE_WR_SELni )
variable DRIVE_SEL_LASTni: std_logic;
begin
if(Z80_RESETn = '0') then
REG_DRIVEA <= '0'; REG_DRIVEB <= '0';
REG_DRIVEC <= '0'; REG_DRIVED <= '0';
REG_MOTOR <= '0';
DRIVE_SEL_LASTni := '0';
elsif(rising_edge(CLK_16M)) then
if(DRIVE_WR_SELni = '0' and DRIVE_SEL_LASTni = '1') then
REG_DRIVEA <= '0'; REG_DRIVEB <= '0';
REG_DRIVEC <= '0'; REG_DRIVED <= '0';
REG_MOTOR <= Z80_DATA(7);
case(to_integer(unsigned(Z80_DATA(2 downto 0)))) is
when 0 => -- No drive
when 4 => REG_DRIVEA <= '1'; -- Select Drive A
when 5 => REG_DRIVEB <= '1'; -- Select Drive B
when 6 => REG_DRIVEC <= '1'; -- Select Drive C
when 7 => REG_DRIVED <= '1'; -- Select Drive D
when others =>
end case;
end if;
DRIVE_SEL_LASTni := DRIVE_WR_SELni;
end if;
end process;
What it does: Handles writes to I/O port 0xDC to select one of four floppy drives and control the spindle motor.
Drive select encoding: The Z80 writes an 8-bit value where bits [2:0] encode the drive number using a non-sequential encoding:
- 0 → no drive selected (all deselected)
- 4 → Drive A
- 5 → Drive B
- 6 → Drive C
- 7 → Drive D
- 1, 2, 3 → no action (the
when othersbranch — these values have no defined meaning in the protocol)
DRVSAn <= not REG_DRIVEA. The active-low floppy drive standard requires the select line to be pulled low to assert — inverting the active-high register is therefore correct.
Process: SETINT — Interrupt Enable Register
SETINT: process( Z80_RESETn, CLK_16M, INTEN_SELni )
variable INTEN_SEL_LASTni: std_logic;
begin
if(Z80_RESETn = '0') then
REG_INT <= '0';
INTEN_SEL_LASTni := '0';
elsif(rising_edge(CLK_16M)) then
if(INTEN_SELni = '0' and INTEN_SEL_LASTni = '1') then
REG_INT <= Z80_RDn;
end if;
INTEN_SEL_LASTni := INTEN_SELni;
end if;
end process;
What it does: Controls whether the WD1773's INTRQ signal is routed to the Z80's INT line.
Dual-function port: Port 0xDF is both the "enable interrupt" port (on write) and the "disable interrupt" port (on read). This is implemented elegantly: INTEN_SELni is asserted on both reads and writes to 0xDF. When the falling edge of INTEN_SELni is detected, REG_INT is loaded with the current value of Z80_RDn:
- Write cycle: Z80_WRn is low, Z80_RDn is high → REG_INT = '1' → interrupt enabled.
- Read cycle: Z80_RDn is low, Z80_WRn is high → REG_INT = '0' → interrupt disabled.
Z80_INT <= INTRQ when REG_INT = '1' else '0' — when enabled, the WD1773's INTRQ is passed directly to the Z80 INT pin; when disabled, the INT pin is held low (inactive).
Process: SETEXXXPAGE — EXXX Window Page Register
SETEXXXPAGE: process( Z80_RESETn, CLK_16M, IFMODE, EXXX_WR_SELni )
variable EXXX_SEL_LASTni : std_logic;
begin
if(Z80_RESETn = '0') then
REG_EXXX_PAGE <= "0000010"; -- Reset to page 2 (RFS start bank)
EXXX_SEL_LASTni := '0';
elsif(rising_edge(CLK_16M)) then
if(EXXX_WR_SELni = '0' and EXXX_SEL_LASTni = '1') then
REG_EXXX_PAGE <= Z80_DATA(6 downto 0);
end if;
EXXX_SEL_LASTni := EXXX_WR_SELni;
end if;
end process;
What it does: Stores the 7-bit page number that selects which 4 KB page of the Flash ROM or SRAM is visible in the EXXX memory window (E300h–EFFFh).
Reset value: "0000010" in binary = page 2 in decimal. On reset, the EXXX window is pre-mapped to page 2 which contains the RFS (ROM Filing System) start bank. This means RFS code is immediately available at the correct address after reset without any software initialisation step.
Writable at runtime: Software can change the page at any time by writing a new 7-bit value to I/O port 0x60. The remapping takes effect at the next memory cycle — there is no cache or pipeline to flush. This allows the firmware to bank-switch through all 128 pages of the Flash ROM to access different RFS modules.
Readback: The current page value can be read back via the concurrent Z80_DATA assignment (
Z80_DATA <= '0' & REG_EXXX_PAGE when EXXX_RD_SELni = '0'). The leading '0' pads the 7-bit page to 8 bits with a zero MSB.
Process: SETFXXXPAGE — FXXX Window Page Register
SETFXXXPAGE: process( Z80_RESETn, CLK_16M, IFMODE, FXXX_WR_SELni )
variable FXXX_SEL_LASTni : std_logic;
begin
if(Z80_RESETn = '0') then
REG_FXXX_PAGE <= (others => '0'); -- Default: page 0 (MZ-80A AFI ROM)
if(IFMODE = MODE_MZ700) then
REG_FXXX_PAGE(1 downto 0) <= "01"; -- MZ-700: page 1 (MZ-700 AFI ROM)
end if;
elsif(rising_edge(CLK_16M)) then
if(FXXX_WR_SELni = '0' and FXXX_SEL_LASTni = '1') then
REG_FXXX_PAGE <= Z80_DATA(6 downto 0);
end if;
FXXX_SEL_LASTni := FXXX_WR_SELni;
end if;
end process;
What it does: Stores the 7-bit page number for the FXXX memory window (F000h–FFFFh) and initialises it to the correct AFI (Autostart Floppy Interface) ROM page for the host machine.
Machine-dependent reset value: The FXXX window holds the machine's boot ROM. Different machines require different boot ROM images:
- MZ-80A / MZ-1200 (MODE 0) and all others:
(others => '0')= page 0 = the MZ-80A AFI boot ROM. This is the default. - MZ-700 (MODE 1): The reset section also checks
if(IFMODE = MODE_MZ700)and overrides to page 1 ("01") which holds the MZ-700 AFI boot ROM. Page 1 contains firmware with MZ-700-specific BASIC entry points and Monitor hook addresses.
Process: SETRAMEN — ROM / RAM Selection
SETRAMEN: process( Z80_RESETn, CLK_16M, RAMEN_WR_SELni )
variable RAMEN_SEL_LASTni: std_logic;
begin
if(Z80_RESETn = '0') then
REG_RAMEN <= '0';
elsif(rising_edge(CLK_16M)) then
if(RAMEN_WR_SELni = '0' and RAMEN_SEL_LASTni = '1') then
REG_RAMEN <= Z80_DATA(0);
end if;
RAMEN_SEL_LASTni := RAMEN_WR_SELni;
end if;
end process;
What it does: A single-bit register that chooses whether the Flash ROM or the SRAM is accessed through the EXXX and FXXX memory windows.
Operation: Writing D0=0 to port 0x62 selects Flash ROM (REG_RAMEN = '0'). Writing D0=1 selects SRAM (REG_RAMEN = '1'). The ROM_SELni and RAM_SELni concurrent assignments check REG_RAMEN to decide which chip select to assert.
Default: REG_RAMEN = '0' on reset means the Flash ROM is active by default — correct for booting.
Use case: The RFS firmware switches to SRAM after boot to use it as a workspace for filing system directory and program data, then switches back to Flash ROM to execute the next RFS module.
Process: SETHIMEM — MZ-700 / MZ-1500 Memory Management
SETHIMEM: process( Z80_RESETn, CLK_16M,
ROMINHSET_WR_SELni, ROMINHCLR_WR_SELni,
ROMDISSET_WR_SELni, ROMDISCLR_WR_SELni )
variable ROMINHSET_LAST : std_logic;
variable ROMINHCLR_LAST : std_logic;
variable ROMDISSET_LAST : std_logic;
variable ROMDISCLR_LAST : std_logic;
begin
if(Z80_RESETn = '0') then
REG_ROMINH <= '0';
REG_ROMDIS <= '0';
...
elsif(rising_edge(CLK_16M)) then
if(ROMINHSET_WR_SELni = '0' and ROMINHSET_LAST = '1') then REG_ROMINH <= '1'; end if;
if(ROMINHCLR_WR_SELni = '0' and ROMINHCLR_LAST = '1') then REG_ROMINH <= '0'; end if;
if(ROMDISSET_WR_SELni = '0' and ROMDISSET_LAST = '1') then REG_ROMDIS <= '1'; end if;
if(ROMDISCLR_WR_SELni = '0' and ROMDISCLR_LAST = '1') then REG_ROMDIS <= '0'; end if;
...
end if;
end process;
What it does: Tracks the MZ-700 and MZ-1500 host memory management state and maintains two flag registers (REG_ROMINH and REG_ROMDIS) that tell the chip-select logic whether the SFD-700's ROM and RAM windows should be visible to the host.
Why this is needed: The MZ-700 and MZ-1500 have a hardware memory management unit that can bank the CPU's internal 64KB DRAM and other resources into the upper address space (D000h–FFFFh), which is the same range used by the SFD-700's Flash ROM and SRAM windows. If the host banks its DRAM here but the SFD-700's ROM is still asserting its chip select for that range, there will be a bus conflict — two devices simultaneously trying to drive the data bus with different values, potentially damaging both.
The two flags:
- REG_ROMDIS — "ROM disabled". Set when the host writes 0xE1 (map DRAM to D000h–FFFFh). This means the host has put its own DRAM over the SFD-700's windows. The SFD-700 must go silent. Cleared when the host writes 0xE3 or 0xE4 (returning the upper address space to memory-mapped I/O and monitor ROM, where the SFD-700 is again safe to appear).
- REG_ROMINH — "ROM inhibited". Set when the host writes 0xE5 (inhibit all D000h–FFFFh access). This is a stronger suppression — the host is making the entire upper address space inert. The SFD-700 must also go silent. Cleared when the host writes 0xE6 (return to default mapping).
- ROMINHSET_WR_SELni → set REG_ROMINH (triggered by port 0xE5 write)
- ROMINHCLR_WR_SELni → clear REG_ROMINH (triggered by port 0xE6 write)
- ROMDISSET_WR_SELni → set REG_ROMDIS (triggered by port 0xE1 write)
- ROMDISCLR_WR_SELni → clear REG_ROMDIS (triggered by ports 0xE3 or 0xE4 write)
REG_ROMINH = '0' and REG_ROMDIS = '0' as mandatory conditions for asserting the chip selects. If either flag is set, the chip selects remain inactive (high), keeping the SFD-700 completely off the bus.
This process only runs in MODEs 1 and 4: The I/O decode signals ROMINHSET_WR_SELni, ROMINHCLR_WR_SELni, ROMDISSET_WR_SELni, and ROMDISCLR_WR_SELni are generated unconditionally by the concurrent assignments, but the ROM_SELni and RAM_SELni assignments only check REG_ROMINH and REG_ROMDIS for MZ-700 and MZ-1500 modes. For other modes, these flags have no effect and the corresponding I/O ports are not meaningful.
Concurrent Signal Assignments
The concurrent signal assignments outside of any process form the combinatorial logic of the CPLD. They are evaluated continuously — whenever any input signal changes, the output is updated within one CPLD propagation delay. This section documents the most significant ones.
Memory Address Decoder
MEM_EXXX_SELni <= '0' when Z80_MREQn = '0'
and unsigned(Z80_ADDR(15 downto 8)) >= X"E3"
and unsigned(Z80_ADDR(15 downto 8)) < X"F0"
else '1';
MEM_FXXX_SELni <= '0' when Z80_MREQn = '0'
and unsigned(Z80_ADDR(15 downto 8)) >= X"F0"
and unsigned(Z80_ADDR(15 downto 8)) <= X"FF"
else '1';
These decode the Z80 address bus for memory requests:
ROM and RAM Chip Selects
- MEM_EXXX_SELni goes low ('0') when the Z80 is accessing memory (MREQn = '0') and the high byte of the address is in the range 0xE3–0xEF. The lower boundary 0xE3 rather than 0xE0 excludes E000h–E2FFh which is the MZ-700/MZ-1500 memory-mapped I/O area.
- MEM_FXXX_SELni goes low when the high byte is 0xF0–0xFF, covering the full F000h–FFFFh boot ROM window.
unsigned(...) casts the std_logic_vector to an unsigned integer so the >= and <= comparisons work correctly. Without this cast, VHDL would perform string comparison rather than numeric comparison.
ROM_SELni <= '0' when (IFMODE = MODE_MZ700 or IFMODE = MODE_MZ1500)
and REG_ROMINH = '0' and REG_ROMDIS = '0'
and REG_RAMEN = '0'
and (MEM_EXXX_SELni = '0' or MEM_FXXX_SELni = '0')
else
'0' when (IFMODE = MODE_MZ1200 or IFMODE = MODE_MZ80A)
and REG_RAMEN = '0'
and (MEM_EXXX_SELni = '0' or MEM_FXXX_SELni = '0')
else '1';
This single assignment captures the complete ROM chip-select logic. It is asserted ('0') only when all conditions are simultaneously true:
I/O Port Decoder
- The machine is MZ-700/MZ-1500 or MZ-80A/MZ-1200 (other machines don't use the onboard ROM).
- For MZ-700/MZ-1500: REG_ROMINH and REG_ROMDIS are both clear (the host has not blocked upper memory access).
- REG_RAMEN is '0' (Flash ROM, not SRAM, is selected).
- The Z80 address falls in the EXXX or FXXX window.
FDC_SELni <= '0' when Z80_IORQn = '0'
and (Z80_WRn = '0' or Z80_RDn = '0')
and unsigned(Z80_ADDR(7 downto 0)) >= X"D8"
and unsigned(Z80_ADDR(7 downto 0)) < X"DC"
else '1';
DRIVE_WR_SELni <= '0' when Z80_IORQn = '0' and Z80_WRn = '0'
and unsigned(Z80_ADDR(7 downto 0)) = X"DC"
else '1';
Every I/O port select signal is a simple combinatorial expression:
Z80 Data Bus Multiplexer
- Z80_IORQn = '0' confirms this is an I/O cycle (not a memory cycle).
- Z80_WRn = '0' or Z80_RDn = '0' confirms either a write or read is occurring (not just an address placed on the bus).
- The address comparison identifies the specific port.
Z80_DATA <= not ID when FDC_SELni = '0' and Z80_WRn = '1'
else '0' & REG_EXXX_PAGE when EXXX_RD_SELni = '0'
else '0' & REG_FXXX_PAGE when FXXX_RD_SELni = '0'
else "0000000" & REG_RAMEN when RAMEN_RD_SELni = '0'
else "00000" & MODE when MODE_RD_SELni = '0'
else (others => 'Z');
This is the read-data multiplexer for the Z80 data bus. It controls what the CPLD drives onto Z80_DATA during any Z80 read cycle:
ID Bus — Inverted Data / ROM-RAM Upper Address
- WD1773 register read: FDC_SELni is asserted and this is a read (Z80_WRn = '1'). ID[7:0] contains the WD1773's response (on its inverted bus). The CPLD re-inverts it (
not ID) before placing it on Z80_DATA so the Z80 receives correct polarity data. - EXXX page register read (port 0x60): Drives the 7-bit REG_EXXX_PAGE value, zero-extended to 8 bits (
'0' & REG_EXXX_PAGE). - FXXX page register read (port 0x61): Drives REG_FXXX_PAGE similarly.
- RAM enable register read (port 0x62): Drives REG_RAMEN as the LSB, with seven zeros above it.
- Mode register read (port 0x63): Drives the three-bit MODE jumper value, zero-extended to 8 bits.
- Default:
(others => 'Z')— the CPLD tristates the data bus (high impedance). This allows other devices (the Flash ROM, SRAM, or the host computer's internal devices) to drive the bus without conflict.
ID <= not Z80_DATA when Z80_WRn = '0' and FDC_SELni = '0'
else REG_EXXX_PAGE & Z80_ADDR(11) when MEM_EXXX_SELni = '0'
else REG_FXXX_PAGE & Z80_ADDR(11) when MEM_FXXX_SELni = '0'
else (others => 'Z');
The ID bus is dual-purpose — it serves two completely different functions depending on what is happening on the bus:
FDC write cycles (Z80_WRn = '0' and FDC_SELni = '0'): The CPLD takes Z80_DATA, inverts every bit, and drives the result onto ID. The WD1773 data pins are connected to ID — it receives the inverted value, which to it looks like a correctly-formatted command byte.
Memory read/write cycles in EXXX window: The CPLD drives ID with {REG_EXXX_PAGE[6:0], Z80_ADDR[11]}. This concatenation creates an 8-bit value where the upper 7 bits are the page register and the LSB is address bit 11 from the Z80. ID[7:0] connects to the Flash ROM and SRAM address pins A11–A18 (the upper address bits beyond the natural 10-bit word address). This is how page switching works physically — the CPLD drives the high address lines on behalf of the Z80.
Memory cycles in FXXX window: Same as EXXX but using REG_FXXX_PAGE.
All other cycles:
ROM_A10 — Page Address Bit / MZ-80A DRQ Trick
(others => 'Z') — the ID bus is tristated so neither the CPLD nor any other logic is driving it, preventing bus contention.
ROM_A10 <= '1' when (IFMODE = MODE_MZ1200 or IFMODE = MODE_MZ80A) and DRQ = '1'
else Z80_ADDR(10);
This single line implements the MZ-80A speed compensation trick. For all machines other than MZ-80A/MZ-1200, ROM_A10 simply follows Z80_ADDR(10) — the natural address bit used for intra-page addressing within a 4 KB page.
In MODE 0 (MZ-80A/MZ-1200), DRQ is substituted for ADDR(10) whenever DRQ is asserted ('1'). The boot ROM contains mirrored copies of the read/wait routines in alternating 1 KB segments. With DRQ routing A10, the Z80 naturally executes the "byte ready" path when DRQ is high and the "wait" path when DRQ is low — no software polling, no cycles wasted.
RAM_A10 always follows Z80_ADDR(10) — the DRQ trick applies only to the Flash ROM, not the SRAM.
Modifying the CPLD Logic
The VHDL source is structured to make common modifications straightforward:
Adding a new I/O port:
- Add a new
signal NEW_PORT_SELni : std_logic;in the signal declaration block. - Add a concurrent assignment:
NEW_PORT_SELni <= '0' when Z80_IORQn = '0' and Z80_WRn = '0' and unsigned(Z80_ADDR(7 downto 0)) = X"XX" else '1'; - Add a process similar to SETDRIVE that captures data from Z80_DATA on the falling edge of NEW_PORT_SELni.
- Use the captured register value in a concurrent output assignment.
- Add a new constant to
sfd700_pkg.vhd:constant MODE_NEWMACHINE : integer := 7; - Update the SETFXXXPAGE process reset block if the new machine requires a different default ROM page.
- Update ROM_SELni and RAM_SELni if the new machine needs onboard ROM/RAM mapped.
- Add MZ-700-style memory management to SETHIMEM if the new machine uses incompatible memory paging.
GAL CUPL Logic (v1.0 / v1.1)
The
CUPL/ directory contains the programmable logic source for the v1.0 and v1.1 boards. CUPL (Compiler for Universal Programmable Logic) is a logic equation language for GAL/PAL devices — it is conceptually simpler than VHDL because GALs implement only single-level combinatorial and simple registered logic.
A CUPL source file defines:
- PIN declarations — mapping input/output names to physical device pins.
- Equations — sum-of-products boolean expressions defining each output as a function of the inputs. For example:
FDCn = !(IORQn # (A7 & !A6 & !A5 & !A4 & !A3 & !A2));reads as "FDCn is low when IORQn is low AND the address bits decode to 0xD8–0xDB". - Registered outputs — the
.REGsuffix on an output name tells CUPL this output is a D flip-flop register clocked by the device's CLK pin, equivalent to a process withrising_edge(CLK).
SFD700_1.PLD (GAL26CV12, I/O decoder) and SFD700_2.PLD (GAL16V8, ROM decoder). Pre-compiled JEDEC files are also provided for direct programming without recompilation.
Reference Sites
| Resource | Link |
|---|---|
| SFD-700 mkII project page | /sfd700/ |
| SFD-700 mkII User Manual | /sfd700-usermanual/ |
| SFD-700 mkII Technical Guide | /sfd700-technicalguide/ |
| SFD-800 (companion MZ-800 card) | /sfd800/ |
| VHDL Language Reference Manual | IEEE 1076-2008 standard |
| Altera MAX7000S Datasheet | Intel FPGA product pages — MAX 7000S family |
| Quartus II 13.0.1 SP1 Web Edition | Intel FPGA Software Archive |
| WD1773 Datasheet | Western Digital / historical archive |
| CUPL Reference Manual | Logical Devices Inc. (archived) |