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:
  • 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.
There can be multiple architectures for one entity (different implementations of the same interface), though the SFD-700 uses a single architecture named rtl (Register Transfer Level — a conventional name meaning the design is described in terms of data flowing between registers).

Signals
Signals are the internal wires of the design. They are declared between the 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').

Processes
A process is a block of code that describes sequential (clocked) or combinatorial logic. Processes are enclosed in 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).

Data Types Used
  • 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. The downto means bit N is the most significant bit. Z80_ADDR is std_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 that case and comparison statements work naturally.
  • boolean — true or false. The helper function to_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 package sfd700_pkg and used by the other files via use work.sfd700_pkg.all.

Logic State Constants
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 — 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.

Machine Mode Constants
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 cpld128 and its rtl architecture — the entire functional logic of the CPLD. Every process and concurrent assignment is documented below.

Entity and Port Declarations
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:
  • 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.

Internal Signal Declarations
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 others branch — these values have no defined meaning in the protocol)
Design decision — always deselect first: On every write to 0xDC, all four drive-select registers are first cleared to '0' before any one is set. This means a single write always transitions cleanly from "whatever was selected before" to "the newly requested drive". This prevents two drives being simultaneously selected, which would cause a hardware conflict on the floppy ribbon cable.
Motor control: Bit 7 of the write directly controls REG_MOTOR — no encoding or inversion. Software must set this bit to 1 to spin up the drive before issuing any WD1773 commands, and should clear it when done to extend drive life.
Output connections: The four REG_DRIVE signals drive the DRVSAn–DRVSDn outputs through inverters in the concurrent assignments: 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.
A single register bit and a single process handle both operations by using Z80_RDn as the data input. This is a compact design that saves macrocells.
Output: 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.
IFMODE in the sensitivity list: IFMODE appears in the sensitivity list because the reset block reads it to determine the initial page value. This is not a clock-based dependency — it only matters during the asynchronous reset phase.
Runtime paging: The firmware can switch to a different FXXX page at runtime by writing to port 0x61, allowing access to additional ROM modules stored beyond page 1.

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).
Four separate edge detectors: The process monitors four separate select signals and uses four variable-based edge detectors, one for each transition:
  • 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)
Effect on chip selects: The ROM_SELni and RAM_SELni concurrent assignments include 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:
  • 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 and RAM Chip Selects
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:
  • 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.
RAM_SELni follows the same structure but tests REG_RAMEN = '1'. The two selects are mutually exclusive (only one can be active at any time since REG_RAMEN is a single bit).

I/O Port Decoder
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_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.
All fourteen I/O port decode signals (FDC_SELni, DRIVE_WR_SELni, DDEN_WR_SELni, SIDE_WR_SELni, INTEN_SELni, EXXX_RD/WR_SELni, FXXX_RD/WR_SELni, RAMEN_RD/WR_SELni, MODE_RD_SELni, ROMINHSET/CLR_WR_SELni, ROMDISSET/CLR_WR_SELni) follow this same pattern. The maximum fan-out node — the address decode tree that feeds all fourteen — drives 67 macrocell inputs, which is why the CPLD is noted in the architecture documentation as having a "high fan-out" node.

Z80 Data Bus Multiplexer
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:
  • 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 Bus — Inverted Data / ROM-RAM Upper Address
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: (others => 'Z') — the ID bus is tristated so neither the CPLD nor any other logic is driving it, preventing bus contention.

ROM_A10 — Page Address Bit / MZ-80A DRQ Trick
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:
  1. Add a new signal NEW_PORT_SELni : std_logic; in the signal declaration block.
  2. 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';
  3. Add a process similar to SETDRIVE that captures data from Z80_DATA on the falling edge of NEW_PORT_SELni.
  4. Use the captured register value in a concurrent output assignment.
Adding a new machine mode:
  1. Add a new constant to sfd700_pkg.vhd: constant MODE_NEWMACHINE : integer := 7;
  2. Update the SETFXXXPAGE process reset block if the new machine requires a different default ROM page.
  3. Update ROM_SELni and RAM_SELni if the new machine needs onboard ROM/RAM mapped.
  4. Add MZ-700-style memory management to SETHIMEM if the new machine uses incompatible memory paging.
Changing the clock divider ratio: Replace the toggle flip-flop in FDCCLK with a counter to divide by any even number. For a divide-by-4 (16 MHz → 4 MHz output): use a 2-bit counter and toggle the output every 2 input cycles.

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 .REG suffix 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 with rising_edge(CLK).
For v1.0/v1.1 boards, the two CUPL files are 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)