SFD-700 mkII デベロッパーズガイド

SFD-700 mkII デベロッパーズガイド

このガイドでは、SFD-700 mkII のプログラマブルロジックのソースコードを詳しく解説します。特にハードウェア記述言語に不慣れな読者向けに VHDL の概念とイディオムを説明することに重点を置いています。CPLD 設計のすべてのプロセスと並行シグナル割り当てを、それを実装する VHDL とともにわかりやすい英語で説明します。
ハードウェアアーキテクチャとレジスタマップについてはテクニカルガイドを参照してください。インストールと使用方法についてはユーザーマニュアルを参照してください。

非 VHDL プログラマー向け VHDL 入門

VHDL(VHSIC ハードウェア記述言語、VHSIC は超高速集積回路の略)は伝統的な意味でのプログラミング言語ではありません — プロセッサが一度に 1 ステップずつ実行する一連のステップを記述するのではありません。代わりに VHDL はハードウェア回路を記述します:論理ゲート、フリップフロップ、マルチプレクサ、およびそれらの接続。VHDL 設計が CPLD または FPGA 向けにコンパイル(合成)されると、ツールはあなたの記述をデバイスに恒久的に組み込まれる実際のゲートとレジスタのネットリストに変換します。

すべてが同時に実行される
VHDL について理解すべき最も重要なことは、アーキテクチャ内のすべてのプロセスとシグナル割り当てが並行して実行されるということです。行 10 が行 9 の後に実行されるプログラムとは異なり、VHDL ではロジックのすべてのブロックが常にアクティブで、その入力に常に応答しています — それが記述する実際のハードウェアと同様に。
SFD-700 CPLD では、クロック分周器、ドライブレジスタ、I/O デコーダ、その他すべてのブロックが、CPLD の電源が入った瞬間から 24 時間 365 日同時にアクティブです。

エンティティ、アーキテクチャ、ポート
VHDL 設計には 2 つの部分があります:
  • エンティティは外部インターフェースです — 入出力ピンをリストします。コンポーネントのピン図と考えてください。SFD-700 では、エンティティはすべての物理的な CPLD ピンをリストします:Z80_ADDR、Z80_DATA、FDCn、DRQ、MODE、CLK_16M など。
  • アーキテクチャは内部実装です — ピンを接続するロジックを記述します。ここにプロセスとシグナル割り当てが存在します。
1 つのエンティティに対して複数のアーキテクチャが存在できますが(同じインターフェースの異なる実装)、SFD-700 は RTL(レジスタ転送レベル — 設計がレジスタ間を流れるデータとして記述されることを意味する慣習的な名前)という単一のアーキテクチャを使用します。

シグナル
シグナルは設計の内部ワイヤーです。architecture キーワードと begin キーワードの間に宣言されます。シグナルは 1 ビット(std_logic)、ビットのバス(std_logic_vector)、または整数を運ぶことができます。SFD-700 では、FDC_SELni(FDC I/O サイクルが発生していることを示す内部ワイヤー)のようなシグナルはシグナルです — 外部ピンではなく、ロジックブロック間の内部接続です。
シグナル名の n サフィックス(例:FDC_SELni)は「アクティブロウ」を意味する慣例です — シグナルはロジックゼロ('0')のときにアサート(意味がある、アクティブ)され、ロジックワン('1')のときにデアサートされます。

プロセス
プロセスは順次(クロック付き)または組み合わせ論理を記述するコードブロックです。プロセスは process(...) begin ... end process で囲まれ、馴染みのある構造を含みます:ifcase、変数割り当て。
クロック付きプロセスはフリップフロップレジスタを記述します — 特定の瞬間(クロックエッジ)に値を捕捉して次のクロックエッジまで保持するロジック。これが CPLD 内の登録状態の基礎です:ページレジスタ、ドライブ選択レジスタ、割り込み有効、メモリ管理フラグ。クロック付きプロセスは常に if rising_edge(CLK) then を含みます。
組み合わせプロセスまたは並行シグナル割り当ては純粋な論理ゲートを記述します — 出力は入力の即座の関数であり、ストレージはありません。SFD-700 の I/O デコードロジックは組み合わせです:Z80 アドレスバスと制御シグナルが変わるたびに、選択出力は即座に更新されます(CPLD の伝播遅延以内で)。

使用されるデータ型
  • std_logic — '0'、'1'、'Z'(ハイインピーダンス/トライステート)、またはシミュレーションで使用される様々な他のメタ値を持てる単一のバイナリシグナル。'0' と '1' が 2 つの実際のハードウェア状態。'Z' はシグナルが駆動されていないことを意味します。
  • std_logic_vector(N downto 0) — N+1 ビットのバス。downto はビット N が最上位ビットであることを意味します。Z80_ADDR は std_logic_vector(15 downto 0) — ビット 15 が MSB の 16 ビットアドレスバス。
  • integer range 0 to 7case と比較文が自然に動作するように IFMODE(マシンモード)に使用される有界整数。
  • boolean — 真または偽。ヘルパー関数 to_std_logic(L: boolean) がブール式を std_logic '0' または '1' に変換します。

ソースツリー

SFD-700 mkII の VHDL ソースファイルはリポジトリの CPLD/v1.2/ ディレクトリにあります:
ファイル 役割
sfd700_pkg.vhd パッケージ — 定数、マシンモード値、ユーティリティ関数
sfd700_Toplevel.vhd トップレベルエンティティ — CPLD の物理ピンを実装にマッピング
sfd700.vhd 実装 — すべてのロジックプロセスとシグナル割り当て
build/sfd700.qpf Quartus II プロジェクトファイル
build/sfd700.qsf Quartus II 設定ファイル(ピン割り当て、デバイス設定)
build/output_files/sfd700.pof コンパイルされた CPLD プログラミングファイル
CUPL/ v1.0/v1.1 ボード用 GAL CUPL ソース

パッケージファイル:sfd700_pkg.vhd

パッケージファイルは他の VHDL ファイルが使用できる定数と関数の共有ライブラリです。C ヘッダーファイルと考えてください — 設計全体にマジックナンバーを繰り返さないよう共通のシンボルを定義します。
package sfd700_pkg として宣言され、他のファイルで use work.sfd700_pkg.all を通じて使用されます。

論理状態定数
設計全体に '1' や '0' を書く代わりに、パッケージは名前付きエイリアスを定義します:
constant YES  : std_logic := '1';    -- アサーション:はい、真、アクティブ
constant NO   : std_logic := '0';    -- 否定:いいえ、偽、非アクティブ
constant HI   : std_logic := '1';    -- シグナルがロジックハイ(電圧)
constant LO   : std_logic := '0';    -- シグナルがロジックロウ(グランド)
constant ONE  : std_logic := '1';    -- バイナリ 1
constant ZERO : std_logic := '0';    -- バイナリ 0
constant HIZ  : std_logic := 'Z';    -- ハイインピーダンス:バスを駆動しない
これらはスタイリスティックです — YES'1' はハードウェアで同一です。意図は自己文書化するコードを作ることです。ROM_CSn <= NO(チップセレクトが非アクティブハイに駆動される)と見れば意味が即座に明確です。

マシンモード定数
3 つの MODE ジャンパービットは値 0–7 を表すことができます。IFMODE シグナルを生の数字と比較する代わりに、パッケージは名前を与えます:
constant MODE_MZ1200 : integer := 0;   -- Sharp MZ-1200(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
MODE_MZ1200 と MODE_MZ80A の両方が 0 に等しいことに注意してください — これら 2 つのマシンはバスタイミングと ROM 要件において十分に類似しているため、同じロジックが両方に対応します。

ユーティリティ関数
いくつかのユーティリティ関数が定義されて他の場所で使用されます:
IntMax(a, b) — 2 つの整数の大きい方を返します。パラメータ計算に使用されます。
log2ceil(arg) — 正の整数を表すために必要なビット数を返します。例えば log2ceil(128) = 7。容量パラメータからバス幅を自動的に計算するために使用されます。
clockTicks(period, clock) — ナノ秒単位の時間周期とHz 単位のクロック周波数が与えられると、その期間に収まるクロックサイクルの整数を返します。合成時のタイマープリロード値またはウェイトステート数を計算するのに便利です。
reverse_vector(slv) — 標準論理ベクターのビット順を逆にします。バスビットを並べ替える必要がある場合に使用されます。
to_std_logic(i) — 整数 0 を std_logic '0' に、その他の整数を std_logic '1' に変換します。整数条件を std_logic 値が必要な場所で直接使用できます。
bit_to_integer(s) — std_logic ビットを自然整数(0 または 1)に変換します。単一ビットを配列インデックスとして使用するのに便利です。

トップレベルファイル:sfd700_Toplevel.vhd

トップレベルファイルはエンティティ sfd700 を定義します — Quartus が物理 CPLD ピンにマッピングするインターフェース。その役割は純粋に構造的です:実装エンティティ(sfd700.vhdcpld128)と同じポートリストを宣言し、その実装をインスタンス化して、すべてのポートを直接 1:1 マッピングでワイヤリングします。
なぜ別のトップレベルファイルがあるのか?VHDL の設計実践において、トップレベルの「チップ境界」エンティティを実装エンティティから分離することは良い衛生的習慣です — シミュレーションテストベンチを組み込んだり、より大きな設計内に設計をインスタンス化したり、コアロジックに触れずにラッパーロジック(JTAG バウンダリスキャンセルなど)を追加したりするのが容易になります。SFD-700 では分割がきれいです:sfd700 は物理デバイスが外部からどのように見えるかで、cpld128 は内部で何をするかです。
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;
port map 文(ポート名とシグナル名の間に =>)は sfd700 の外部ピン名を cpld128 コンポーネントの対応するピンに接続します。両エンティティが同一のポート名を持つため、すべての接続は ポート名 => ポート名 になります。

メインロジックファイル:sfd700.vhd

このファイルはエンティティ cpld128 とその rtl アーキテクチャを含みます — CPLD の完全な機能ロジック。以下にすべてのプロセスと並行割り当てを文書化します。

エンティティとポート宣言
エンティティポートリストはシグナルを 7 つの論理カテゴリにグループ化します:
entity cpld128 is
    port (
        -- Z80 アドレスバス
        Z80_ADDR    : in    std_logic_vector(15 downto 0);  -- ホスト Z80 からの 16 ビットアドレス
        -- Z80 データバス(双方向)
        Z80_DATA    : inout std_logic_vector(7 downto 0);   -- 8 ビットデータ — 駆動または読み取り可能
        -- Z80 制御シグナル
        Z80_M1n     : in    std_logic;   -- マシンサイクル 1(アクティブロウ)— Z80 がオペコードをフェッチ中
        Z80_RDn     : in    std_logic;   -- 読み取りストローブ(アクティブロウ)
        Z80_WRn     : in    std_logic;   -- 書き込みストローブ(アクティブロウ)
        Z80_IORQn   : in    std_logic;   -- I/O リクエスト(アクティブロウ)
        Z80_MREQn   : in    std_logic;   -- メモリリクエスト(アクティブロウ)
        Z80_INT     : out   std_logic;   -- Z80 への割り込み(アクティブハイ)
        Z80_EXWAITn : out   std_logic;   -- 外部ウェイト(アクティブロウ、バスサイクルを延長)
        Z80_RESETn  : in    std_logic;   -- システムリセット(アクティブロウ)
        -- 反転データ / ROM-RAM 上位アドレスバス
        ID          : inout std_logic_vector(7 downto 0);   -- 二重目的:反転 FDC データまたは ROM/RAM 上位アドレス
        -- ROM/RAM 制御
        ROM_A10     : out   std_logic;   -- ROM ページビット / MZ-80A DRQ マルチプレクサ
        RAM_A10     : out   std_logic;   -- RAM ページビット
        ROM_CSn     : out   std_logic;   -- フラッシュ ROM チップセレクト(アクティブロウ)
        RAM_CSn     : out   std_logic;   -- SRAM チップセレクト(アクティブロウ)
        RSV         : out   std_logic;   -- 予約済み出力ピン
        -- ホストマシンモード
        MODE        : in    std_logic_vector(2 downto 0);   -- 3 ビットモードジャンパー
        -- フロッピーディスクインターフェース
        FDCn        : out   std_logic;   -- WD1773 チップセレクト(アクティブロウ)
        INTRQ       : in    std_logic;   -- WD1773 コマンド完了割り込み
        DRQ         : in    std_logic;   -- WD1773 データリクエスト
        DDENn       : out   std_logic;   -- ダブル密度有効化(アクティブロウ)
        SIDE1       : out   std_logic;   -- ディスクヘッドサイド選択
        MOTOR       : out   std_logic;   -- スピンドルモーター有効化
        DRVSAn      : out   std_logic;   -- ドライブ A チップセレクト(アクティブロウ)
        DRVSBn      : out   std_logic;   -- ドライブ B チップセレクト(アクティブロウ)
        DRVSCn      : out   std_logic;   -- ドライブ C チップセレクト(アクティブロウ)
        DRVSDn      : out   std_logic;   -- ドライブ D チップセレクト(アクティブロウ)
        -- クロック
        CLK_16M     : in    std_logic;   -- 16 MHz 水晶発振器
        CLK_FDC     : out   std_logic;   -- WD1773 への 8 MHz
        CLK_BUS0    : in    std_logic    -- ホスト Z80 バスクロック(予約済み)
    );
end entity;
ポート方向:
  • in — CPLD への入力(外部ハードウェアが駆動)。
  • out — CPLD からの出力(CPLD ロジックが駆動)。
  • inout — 双方向:CPLD はシグナルを読み取りも駆動もできます。バスサイクルタイプによって方向が変わるデータバス(Z80_DATA、ID)に使用されます。

内部シグナル宣言
アーキテクチャの isbegin キーワードの間に、すべての内部ワイヤー(シグナル)が宣言されます。これらは物理ピンではなく、CPLD 内のロジックブロックを接続する「ワイヤー」です:
-- アドレスデコード選択シグナル(内部ワイヤー、物理ピンではない)
signal FDC_SELni        : std_logic;   -- WD1773 レジスタにアクセス中に '0'
signal DRIVE_WR_SELni   : std_logic;   -- ポート 0xDC に書き込み中に '0'
signal DDEN_WR_SELni    : std_logic;   -- ポート 0xDE に書き込み中に '0'
signal SIDE_WR_SELni    : std_logic;   -- ポート 0xDD に書き込み中に '0'
signal INTEN_SELni      : std_logic;   -- ポート 0xDF にアクセス中に '0'
signal EXXX_WR_SELni    : std_logic;   -- ポート 0x60 に書き込み中に '0'
signal FXXX_WR_SELni    : std_logic;   -- ポート 0x61 に書き込み中に '0'
signal MEM_EXXX_SELni   : std_logic;   -- メモリアドレスが E300h-EFFFh の時に '0'
signal MEM_FXXX_SELni   : std_logic;   -- メモリアドレスが F000h-FFFFh の時に '0'

-- 登録状態(これらは CPLD 内のフリップフロップになる)
signal REG_DRIVEA       : std_logic;   -- ドライブ A 選択
signal REG_DRIVEB       : std_logic;   -- ドライブ B 選択
signal REG_DRIVEC       : std_logic;   -- ドライブ C 選択
signal REG_DRIVED       : std_logic;   -- ドライブ D 選択
signal REG_MOTOR        : std_logic;   -- モーター動作中
signal REG_SIDE         : std_logic;   -- ディスクヘッドサイド
signal REG_DDEN         : std_logic;   -- ダブル密度モードアクティブ
signal REG_INT          : std_logic;   -- 割り込み有効
signal REG_EXXX_PAGE    : std_logic_vector(7 downto 0);   -- 8 ビット EXXX ページレジスター(D7=ROM/RAM 選択、[6:0]=ページアドレス)
signal REG_FXXX_PAGE    : std_logic_vector(7 downto 0);   -- 8 ビット FXXX ページレジスター(D7=ROM/RAM 選択、[6:0]=ページアドレス)
signal REG_ROMDIS       : std_logic;   -- MZ-700:ROM 無効(DRAM マッピング)
signal REG_ROMINH       : std_logic;   -- MZ-700:ROM 完全禁止

-- マシンモード(リセット時にジャンパーからキャプチャ)
signal IFMODE           : integer range 0 to 7 := 0;

-- クロック
signal CLK_8Mi          : std_logic := '0';   -- 8 MHz 分周クロック(内部)

プロセス:FDCCLK — クロック分周器

FDCCLK: process( CLK_16M )
begin
    if(rising_edge(CLK_16M)) then
        CLK_8Mi <= not CLK_8Mi;
    end if;
end process;
何をするか:このプロセスには単一のトグルフリップフロップが含まれており、16 MHz のプライマリクロックを 2 で割り、WD1773 が必要とする 8 MHz クロックを生成します。
どのように動作するか:感度リスト ( CLK_16M ) は「CLK_16M が変化するたびにこのプロセスを再評価する」を意味します。if rising_edge(CLK_16M) テストは「クロックの立ち上がりエッジ(0→1 遷移)のみでアクションを取る」を意味します。すべての立ち上がりエッジで CLK_8Mi が反転され、'0' と '1' の間でトグルします。これは完璧な 2 分周です:CLK_8Mi の 1 つのフルサイクルは CLK_16M の 2 サイクルにわたり、正確に 8 MHz を与えます。
ハードウェアの結果:Q が自身の補数入力(D = not Q)に接続されている 1 つの D フリップフロップ。出力 Q が CLK_8Mi で、CPLD の CLK_FDC ピンから直接駆動されます。
なぜ 8 MHz なのか?WD1773 のデータシートは標準的な MFM フロッピー操作のために 8 MHz の入力クロックを指定しています。チップはこれを使用してビットセルウィンドウとデータ分離のための PLL ロックをタイミングします。

プロセス:SETMODE — モードレジスタラッチ

SETMODE: process( Z80_RESETn, MODE )
begin
    if(Z80_RESETn = '0') then
        IFMODE <= to_integer(unsigned(MODE));
    end if;
end process;
何をするか:システムがリセット状態に保たれているとき、3 つの MODE ジャンパー入力ピンをサンプリングして、その値を整数として IFMODE に保存します。
どのように動作するか:感度リストには Z80_RESETn と MODE の両方が含まれます。いずれかが変化するたびにプロセスが実行されます。Z80_RESETn がロウ(システムリセット中)であれば、IFMODE が MODE ピンから読み込まれます。変換チェーン to_integer(unsigned(MODE)) は 3 ビットの std_logic_vector MODE を整数 0–7 に変換します。
重要な詳細:このプロセスにはクロックエッジがありません — Z80_RESETn がロウであることにレベルセンシティブです。これは IFMODE がリセットの持続時間全体を通じて更新されることを意味します。合成では、これはクロック付きフリップフロップではなく単純な非同期レジスタロードを作成します。IFMODE 値はリセットがアサートされている間は安定しており、リセットが解放されるとフリップフロップ出力によって保持されます。
なぜリセット時にラッチするのか?MODE ジャンパーは電源投入前に設定される PCB ジャンパーです。リセット時にラッチすることで、設計が安定した値を読み取り(ジャンパーはリセット期間でデバウンスされる)、電源投入中のグリッチがモード選択を破壊しないことが保証されます。リセットが解放されると、IFMODE はそのセッションで固定されます。

プロセス:SETSIDE — ヘッドサイドレジスタ

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;
何をするか:Z80 が I/O ポート 0xDD に書き込んだ時、ヘッドサイド選択(フロッピーディスクの前面または背面)をキャプチャします。
どのように動作するか:このプロセスはエッジ検出技術を使用して SIDE_WR_SELni(ポート 0xDD の書き込み選択シグナルのアクティブロウ)の立ち下がりエッジを識別します。SIDE_SEL_LASTni は前のクロックサイクルの SIDE_WR_SELni の値を保持する variable です。各立ち上がりクロックエッジで、SIDE_WR_SELni が今 '0'(アサート)だが前のサイクルは '1' だった場合、立ち下がりエッジが発生したばかりです — これが Z80 書き込みストローブが新鮮でデータバスに有効なデータが含まれているときです。その正確なクロックエッジで REG_SIDE が Z80_DATA(0) から読み込まれます。
変数対シグナル:SIDE_SEL_LASTni は signal ではなく variable として宣言されています。VHDL では、プロセス内の変数は割り当てられると即座に更新されます(通常のプログラミング言語変数のように)が、シグナルはプロセスが中断された後にのみ更新されます。
反転:REG_SIDE <= not Z80_DATA(0) — WD1773 のサイド選択規約が Z80 の書き込み規約の反対であるためビットが反転されます:Z80 ソフトウェアはサイド 1 を選択するために D0=0 を書き込み、サイド 0 を選択するために D0=1 を書き込みます。反転により物理的な SIDE1 出力が WD1773 の期待に合わせられます。

プロセス:SETDDEN — ダブル密度有効化

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;
何をするか:WD1773 がダブル密度(MFM)またはシングル密度(FM)モードで動作するかどうかを制御します。I/O ポート 0xDE 経由で書き込まれます。
構造:SETSIDE と同一のエッジ検出パターン。DDEN_WR_SELni(ポート 0xDE への書き込み)の立ち下がりエッジで、REG_DDEN が Z80_DATA(0) の反転として読み込まれます。WD1773 の DDENn 入力はアクティブロウ:それをロウに駆動するとダブル密度が有効になります。Z80 ソフトウェアの規約はダブル密度に D0=0 です — 反転によりこれが正しくマッピングされます。
リセット値:リセット時の REG_DDEN = '1' は DDENn がハイに駆動されること = デフォルトでシングル密度が選択されることを意味します。実際には、ファームウェアは FDC コマンドの前に直ちにダブル密度を設定します。

プロセス:SETDRIVE — ドライブとモーター制御

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 =>                          -- ドライブなし
                when 4 => REG_DRIVEA <= '1';       -- ドライブ A を選択
                when 5 => REG_DRIVEB <= '1';       -- ドライブ B を選択
                when 6 => REG_DRIVEC <= '1';       -- ドライブ C を選択
                when 7 => REG_DRIVED <= '1';       -- ドライブ D を選択
                when others =>
            end case;
        end if;
        DRIVE_SEL_LASTni := DRIVE_WR_SELni;
    end if;
end process;
何をするか:I/O ポート 0xDC への書き込みを処理して 4 つのフロッピードライブのうちの 1 つを選択し、スピンドルモーターを制御します。
ドライブ選択エンコーディング:Z80 はビット [2:0] が非順次エンコーディングでドライブ番号をエンコードする 8 ビット値を書き込みます:
  • 0 → ドライブが選択されていない(すべてデセレクト)
  • 4 → ドライブ A
  • 5 → ドライブ B
  • 6 → ドライブ C
  • 7 → ドライブ D
  • 1、2、3 → アクションなし(when others ブランチ — これらの値にはプロトコルで定義された意味がない)
設計上の決定 — 常に最初にデセレクト:0xDC への書き込みのたびに、4 つすべてのドライブ選択レジスタが最初に '0' にクリアされてから 1 つが設定されます。これにより 1 回の書き込みが常に「以前に選択されていたもの」から「新しく要求されたドライブ」にきれいに遷移します。これにより 2 つのドライブが同時に選択されることを防ぎます。
モーター制御:書き込みのビット 7 は REG_MOTOR を直接制御します。ソフトウェアは WD1773 コマンドを発行する前にこのビットを 1 に設定してドライブをスピンアップさせ、完了したら寿命を延ばすためにクリアすべきです。

プロセス:SETINT — 割り込み有効レジスタ

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;
何をするか:WD1773 の INTRQ シグナルを Z80 の INT ラインにルーティングするかどうかを制御します。
デュアルファンクションポート:ポート 0xDF は「割り込み有効化」ポート(書き込み時)と「割り込み無効化」ポート(読み取り時)の両方です。これはエレガントに実装されています:INTEN_SELni は 0xDF への読み書き両方でアサートされます。INTEN_SELni の立ち下がりエッジが検出されると、REG_INT は Z80_RDn の現在の値で読み込まれます:
  • 書き込みサイクル:Z80_WRn がロウ、Z80_RDn がハイ → REG_INT = '1' → 割り込み有効。
  • 読み取りサイクル:Z80_RDn がロウ、Z80_WRn がハイ → REG_INT = '0' → 割り込み無効。
単一のレジスタビットと単一のプロセスが Z80_RDn をデータ入力として使用することで両方の操作を処理します。これはマクロセルを節約するコンパクトな設計です。

プロセス:SETEXXXPAGE — EXXX ウィンドウページレジスタ

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    <= "00000010";   -- Reset to page 2 (D7=0 → Flash ROM, [6:0]=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(7 downto 0);
        end if;
        EXXX_SEL_LASTni  := EXXX_WR_SELni;
    end if;
end process;
何をするか:EXXX メモリウィンドウ(E300h–EFFFh)でフラッシュ ROM または SRAM のどの 4 KB ページが見えるかを選択する 8 ビット EXXX ページレジスターを保存します。ビット 7(D7)はストレージタイプを選択します — D7=0 で Flash ROM、D7=1 で SRAM。ビット[6:0] はページアドレスを保持します。
リセット値:"00000010" バイナリ = D7=0(Flash ROM)、10 進数でページ 2。リセット時、EXXX ウィンドウは RFS(ROM ファイリングシステム)スタートバンクを含む Flash ROM ページ 2 に事前マッピングされます。これにより、ソフトウェアの初期化ステップなしにリセット後すぐに RFS コードが正しいアドレスで利用可能になります。
実行時に書き込み可能:ソフトウェアは I/O ポート 0x60 に 8 ビット値を書き込むことでいつでもページアドレスとストレージモードの両方を変更できます。リマッピングは次のメモリサイクルで有効になります — フラッシュされるキャッシュやパイプラインはありません。これにより、ファームウェアがフラッシュ ROM または SRAM の 128 ページすべてをバンク切り替えし、このウィンドウの ROM と RAM を独立して切り替えることができます。
読み戻し:現在のレジスター値は並行 Z80_DATA 割り当て(Z80_DATA <= REG_EXXX_PAGE when EXXX_RD_SELni = '0')を通じて読み戻すことができます。完全な 8 ビット値が返されます — D7 は現在の ROM/RAM 選択を反映し、ビット[6:0] はページアドレスを反映します。

プロセス:SETFXXXPAGE — FXXX ウィンドウページレジスタ

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: D7=0 (Flash ROM), 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(7 downto 0);
        end if;
        FXXX_SEL_LASTni  := FXXX_WR_SELni;
    end if;
end process;
何をするか:FXXX メモリウィンドウ(F000h–FFFFh)の 8 ビットページレジスターを保存し、ホストマシンの正しい AFI(オートスタートフロッピーインターフェース)ROM ページに初期化します。EXXX レジスターと同様に、ビット 7(D7)は Flash ROM(0)または SRAM(1)を選択し、ビット[6:0] はページアドレスを保持します。
マシン依存のリセット値:FXXX ウィンドウはマシンのブート ROM を保持します。異なるマシンには異なるブート ROM イメージが必要です:
  • MZ-80A / MZ-1200(MODE 0)およびその他すべて:(others => '0') = ページ 0 = MZ-80A AFI ブート ROM。これがデフォルトです。
  • MZ-700(MODE 1):リセットセクションも if(IFMODE = MODE_MZ700) を確認し、MZ-700 AFI ブート ROM を保持するページ 1("01")に上書きします。ページ 1 には MZ-700 固有の BASIC エントリーポイントとモニターフックアドレスを含むファームウェアが格納されています。
感度リストの IFMODE:IFMODE が感度リストに含まれるのは、リセットブロックが初期ページ値を決定するために読み取るためです。これはクロックベースの依存関係ではなく — 非同期リセットフェーズでのみ重要です。
ランタイムページング:ファームウェアはポート 0x61 に書き込むことで実行時に異なる FXXX ページに切り替えたり ROM/RAM 選択を変更したりすることができ、ページ 1 以降に格納された追加 ROM モジュールへのアクセスやワークスペース用の SRAM への切り替えが可能です。

RAM / ROM 選択(ページレジスタービット 7 経由)

v1.2 からの変更:専用の REG_RAMEN レジスターとその I/O ポート 0x62 は削除されました。RAM/ROM 選択は各ページレジスターのビット 7(D7)で制御されるようになりました — EXXX ウィンドウには REG_EXXX_PAGE(7)、FXXX ウィンドウには REG_FXXX_PAGE(7) を使用します。D7=0 で Flash ROM を選択、D7=1 で SRAM を選択します。
利点:各メモリウィンドウが Flash ROM または SRAM を独立して選択できるようになりました。以前は、単一の REG_RAMEN ビットが両方のウィンドウを同時に制御していました。ページアドレスとストレージモードは 1 回の I/O 書き込みで設定でき、ポート 0x62 への別途の書き込みが不要になりました。
デフォルト:両方のページレジスターは D7=0 でリセットされるため、デフォルトでは Flash ROM がアクティブです — ブートに正しいです。
用途:RFS ファームウェアは一方のウィンドウを Flash ROM にマップし(コード実行用)、もう一方のウィンドウで SRAM にアクセスできます(ワークスペース用)。すべてウィンドウごとに 1 回のレジスター書き込みで行えます。

プロセス:SETHIMEM — MZ-700 / MZ-1500 メモリ管理

何をするか:MZ-700 および MZ-1500 ホストのメモリ管理状態を追跡し、SFD-700 の ROM と RAM ウィンドウがホストに見えるかどうかを I/O チップセレクトロジックに伝える 2 つのフラグレジスタ(REG_ROMINH と REG_ROMDIS)を維持します。
なぜこれが必要なのか:MZ-700 および MZ-1500 には、CPU の内部 64KB DRAM とその他のリソースを上位アドレス空間(D000h–FFFFh)にバンクインできるハードウェアメモリ管理ユニットがあります。これは SFD-700 のフラッシュ ROM と SRAM ウィンドウが使用しているのと同じ範囲です。ホストが DRAM をここにバンクしたが SFD-700 の ROM がその範囲に対してチップセレクトをアサートし続けると、バス競合が発生します — 2 つのデバイスが異なる値でデータバスを同時に駆動しようとして、両方を損傷させる可能性があります。
2 つのフラグ:
  • REG_ROMDIS — 「ROM 無効」。ホストが 0xE1 を書き込んだとき(D000h–FFFFh に DRAM をマッピング)に設定されます。これはホストが SFD-700 のウィンドウ上に独自の DRAM を配置したことを意味します。SFD-700 は沈黙しなければなりません。ホストが 0xE3 または 0xE4 を書き込んだとき(上位アドレス空間をメモリマッピング I/O とモニター ROM に戻す)にクリアされます。
  • REG_ROMINH — 「ROM 禁止」。ホストが 0xE5 を書き込んだとき(D000h–FFFFh へのすべてのアクセスを禁止)に設定されます。SFD-700 も沈黙しなければなりません。ホストが 0xE6 を書き込んだとき(デフォルトマッピングに戻す)にクリアされます。
チップセレクトへの効果:ROM_SELni と RAM_SELni の並行割り当てには、チップセレクトをアサートするための必須条件として REG_ROMINH = '0' および REG_ROMDIS = '0' が含まれています。いずれかのフラグが設定されると、チップセレクトは非アクティブ(ハイ)のままになり、SFD-700 をバスから完全にオフにします。

並行シグナル割り当て

プロセスの外の並行シグナル割り当てが CPLD の組み合わせロジックを形成します。入力シグナルが変化するたびに継続的に評価されます — 入力シグナルが変化すると、出力は 1 つの CPLD 伝播遅延以内に更新されます。

メモリアドレスデコーダ
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';
これらはメモリリクエストの Z80 アドレスバスをデコードします:
  • MEM_EXXX_SELni は Z80 がメモリにアクセス中(MREQn = '0')でアドレスの上位バイトが 0xE3–0xEF の範囲にある場合にロウ('0')になります。下限 0xE3 ではなく 0xE0 は E000h–E2FFh を除外します(MZ-700/MZ-1500 のメモリマッピング I/O 領域)。
  • MEM_FXXX_SELni は上位バイトが 0xF0–0xFF のとき、フルの F000h–FFFFh ブートロムウィンドウをカバーするときにロウになります。
unsigned(...) は std_logic_vector をアンサイン整数にキャストして >=<= の比較が正しく動作するようにします。このキャストなしでは VHDL は数値比較ではなく文字列比較を実行します。

ROM と RAM チップセレクト
ROM_SELni <= '0' when (IFMODE = MODE_MZ700 or IFMODE = MODE_MZ1500)
                       and REG_ROMINH = '0' and REG_ROMDIS = '0'
                       and ((MEM_EXXX_SELni = '0' and REG_EXXX_PAGE(7) = '0')
                            or (MEM_FXXX_SELni = '0' and REG_FXXX_PAGE(7) = '0'))
             else
             '0' when (IFMODE = MODE_MZ1200 or IFMODE = MODE_MZ80A)
                       and ((MEM_EXXX_SELni = '0' and REG_EXXX_PAGE(7) = '0')
                            or (MEM_FXXX_SELni = '0' and REG_FXXX_PAGE(7) = '0'))
             else '1';
この単一の割り当てが完全な ROM チップセレクトロジックをキャプチャします。すべての条件が同時に真のときのみアサートされます('0'):
  • マシンが MZ-700/MZ-1500 または MZ-80A/MZ-1200 である(他のマシンはオンボード ROM を使用しない)。
  • MZ-700/MZ-1500 の場合:REG_ROMINH と REG_ROMDIS の両方がクリア(ホストが上位メモリアクセスをブロックしていない)。
  • アクティブウィンドウのページレジスターのビット 7 が '0'(そのウィンドウで Flash ROM が選択されている)。各ウィンドウは独立してチェックされます — EXXX ウィンドウには REG_EXXX_PAGE(7)、FXXX ウィンドウには REG_FXXX_PAGE(7)。
  • Z80 アドレスが EXXX または FXXX ウィンドウに収まる。
RAM_SELni は同じ構造に従いますがビット 7 = '1' をテストします。ROM と RAM のセレクトはウィンドウごとに互いに排他的です — 各ウィンドウは自身のページレジスターのビット 7 に基づいて Flash ROM または SRAM を独立して選択します。

I/O ポートデコーダ
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';
すべての I/O ポート選択シグナルはシンプルな組み合わせ式です:
  • Z80_IORQn = '0' はこれが I/O サイクル(メモリサイクルではない)であることを確認します。
  • Z80_WRn = '0' または Z80_RDn = '0' は書き込みまたは読み取りが発生していることを確認します。
  • アドレス比較が特定のポートを識別します。
12 の I/O ポートデコードシグナルすべて(FDC_SELni、DRIVE_WR_SELni、DDEN_WR_SELni、SIDE_WR_SELni、INTEN_SELni、EXXX_RD/WR_SELni、FXXX_RD/WR_SELni、MODE_RD_SELni、ROMINHSET/CLR_WR_SELni、ROMDISSET/CLR_WR_SELni)がこの同じパターンに従います。最大ファンアウトノード — 12 すべてに供給するアドレスデコードツリー — は 67 のマクロセル入力を駆動し、これがアーキテクチャドキュメントで「高ファンアウト」ノードとして注記されている理由です。

Z80 データバスマルチプレクサ
Z80_DATA <= not ID                         when FDC_SELni = '0' and Z80_WRn = '1'
            else REG_EXXX_PAGE             when EXXX_RD_SELni = '0'
            else REG_FXXX_PAGE             when FXXX_RD_SELni = '0'
            else "00000" & MODE            when MODE_RD_SELni = '0'
            else (others => 'Z');
これは Z80 データバスの読み取りデータマルチプレクサです。Z80 読み取りサイクル中に CPLD が Z80_DATA に駆動するものを制御します:
  • WD1773 レジスタ読み取り:FDC_SELni がアサートされてこれが読み取り(Z80_WRn = '1')。ID[7:0] には WD1773 のレスポンスが含まれます(反転バス上で)。CPLD はそれを再反転(not ID)して Z80_DATA に配置します。
  • EXXX ページレジスタ読み取り(ポート 0x60):完全な 8 ビットの REG_EXXX_PAGE 値を駆動します — D7 は ROM/RAM 選択ビット、ビット[6:0] はページアドレスです。
  • FXXX ページレジスタ読み取り(ポート 0x61):同様に REG_FXXX_PAGE を駆動します — D7 は ROM/RAM 選択、ビット[6:0] はページアドレスです。
  • モードレジスタ読み取り(ポート 0x63):3 ビットの MODE ジャンパー値をゼロ拡張して 8 ビットに駆動します。
  • デフォルト:(others => 'Z') — CPLD がデータバスをトライステートします(ハイインピーダンス)。これにより他のデバイス(フラッシュ ROM、SRAM、またはホストコンピューターの内部デバイス)が競合なしにバスを駆動できます。

ID バス — 反転データ / ROM-RAM 上位アドレス
ID <= not Z80_DATA                                when Z80_WRn = '0' and FDC_SELni = '0'
     else REG_EXXX_PAGE(6 downto 0) & Z80_ADDR(11)  when MEM_EXXX_SELni = '0'
     else REG_FXXX_PAGE(6 downto 0) & Z80_ADDR(11)  when MEM_FXXX_SELni = '0'
     else (others => 'Z');
ID バスは二重目的です — バス上で何が起きているかによって 2 つの完全に異なる機能を果たします:
FDC 書き込みサイクル(Z80_WRn = '0' および FDC_SELni = '0'):CPLD は Z80_DATA を取り、すべてのビットを反転して ID に駆動します。WD1773 のデータピンが ID に接続されています — 反転された値を受け取り、それを正しくフォーマットされたコマンドバイトとして認識します。
EXXX ウィンドウのメモリ読み書きサイクル:CPLD は {REG_EXXX_PAGE[6:0]、Z80_ADDR[11]} で ID を駆動します。この連結は上位 7 ビットがページレジスタで LSB が Z80 からのアドレスビット 11 である 8 ビット値を作成します。ID[7:0] はフラッシュ ROM と SRAM のアドレスピン A11–A18(自然な 10 ビットワードアドレスを超えた上位アドレスビット)に接続されています。これがページ切り替えが物理的にどのように機能するかです — CPLD が Z80 に代わって上位アドレスラインを駆動します。
FXXX ウィンドウのメモリサイクル:EXXX と同様ですが REG_FXXX_PAGE を使用します。
その他すべてのサイクル:(others => 'Z') — ID バスがトライステートされます。

ROM_A10 — ページアドレスビット / MZ-80A DRQ トリック
ROM_A10 <= '1' when (IFMODE = MODE_MZ1200 or IFMODE = MODE_MZ80A) and DRQ = '1'
           else Z80_ADDR(10);
この 1 行が MZ-80A スピード補正トリックを実装します。MZ-80A/MZ-1200 以外のすべてのマシンに対して、ROM_A10 は単純に Z80_ADDR(10) に従います — 4 KB ページ内のページ内アドレッシングに使用される自然なアドレスビット。
MODE 0(MZ-80A/MZ-1200)では、DRQ がアサートされている('1')ときに DRQ が ADDR(10) の代わりに使用されます。ブート ROM には読み取り/待機ルーティンのミラーコピーが 1 KB の交互セグメントに含まれています。DRQ が A10 をルーティングすることで、Z80 は DRQ がハイのときに自然に「バイト準備完了」パスを実行し、DRQ がロウのときに「待機」パスを実行します — ソフトウェアポーリングなし、無駄なサイクルなし。

CPLD ロジックの変更

VHDL ソースは一般的な変更を簡単にする構造になっています:
新しい I/O ポートの追加:
  1. シグナル宣言ブロックに新しい signal NEW_PORT_SELni : std_logic; を追加する。
  2. 並行割り当てを追加:NEW_PORT_SELni <= '0' when Z80_IORQn = '0' and Z80_WRn = '0' and unsigned(Z80_ADDR(7 downto 0)) = X"XX" else '1';
  3. NEW_PORT_SELni の立ち下がりエッジで Z80_DATA からデータをキャプチャする SETDRIVE に似たプロセスを追加する。
  4. 並行出力割り当てでキャプチャされたレジスタ値を使用する。
新しいマシンモードの追加:
  1. sfd700_pkg.vhd に新しい定数を追加:constant MODE_NEWMACHINE : integer := 7;
  2. 新しいマシンが異なるデフォルト ROM ページを必要とする場合は SETFXXXPAGE プロセスのリセットブロックを更新する。
  3. 新しいマシンがオンボード ROM/RAM をマッピングする必要がある場合は ROM_SELni と RAM_SELni を更新する。
  4. 新しいマシンが互換性のないメモリページングを使用する場合は MZ-700 スタイルのメモリ管理を SETHIMEM に追加する。
クロック分周比の変更:FDCCLK のトグルフリップフロップを任意の偶数で割るカウンターに置き換えます。4 分周(16 MHz → 4 MHz 出力)の場合:2 ビットカウンターを使用して 2 入力サイクルごとに出力をトグルします。

GAL CUPL ロジック(v1.0 / v1.1)

CUPL/ ディレクトリには v1.0 および v1.1 ボードのプログラマブルロジックソースが含まれています。CUPL(ユニバーサルプログラマブルロジックのためのコンパイラ)は GAL/PAL デバイスのロジック方程式言語です — GAL は単一レベルの組み合わせロジックと単純な登録ロジックのみを実装するため、VHDL よりも概念的にシンプルです。
CUPL ソースファイルは以下を定義します:
  • PIN 宣言 — 入出力名を物理デバイスピンにマッピング。
  • 方程式 — 各出力を入力の関数として定義する積和ブール式。例えば:FDCn = !(IORQn # (A7 & !A6 & !A5 & !A4 & !A3 & !A2)); は「FDCn は IORQn がロウ AND アドレスビットが 0xD8–0xDB にデコードされるときにロウ」と読めます。
  • 登録出力 — 出力名の .REG サフィックスは、この出力がデバイスの CLK ピンでクロックされる D フリップフロップレジスタであることを CUPL に伝え、rising_edge(CLK) を持つプロセスと同等です。
v1.0/v1.1 ボードの 2 つの CUPL ファイルは SFD700_1.PLD(GAL26CV12、I/O デコーダー)と SFD700_2.PLD(GAL16V8、ROM デコーダー)です。コンパイルなしに直接プログラミングするためのあらかじめコンパイルされた JEDEC ファイルも提供されています。

CI/CD パイプライン

SFD-700 mkII は、CPLD コンパイル、RFS ROM アセンブリ、およびリリース管理を自動化する Jenkins CI/CD パイプラインを使用しています。パイプラインは Gitea Webhook によってトリガーされ、バージョン管理された CPLD ビットストリームと ROM イメージを生成します。

Jenkins パイプライン概要

SFD700 CI/CD パイプラインは、EaW VPS 上の Docker コンテナ内で動作する Jenkins インスタンスで実行されます。main または master ブランチにコミットがプッシュされると、Gitea Webhook により自動的にトリガーされます。パイプラインのステージは以下の通りです:
  1. Checkout — ワークスペースをクリーンし、Gitea から SFD700 リポジトリをクローン。
  2. Determine Version — リポジトリルートの VERSION ファイルを読み取る。前回のコミットからバージョンが変更されていない場合、最新の Gitea リリースタグからバージョン番号を自動的にインクリメント。
  3. Build CPLD — Docker コンテナ内の Altera Quartus II 13.0.1 を使用して VHDL ソースをコンパイルし、EPM7128SLC84-15 CPLD 用の sfd700.pof ビットストリームを生成。
  4. Clone RFS — Gitea から RFS(ROM ファイリングシステム)リポジトリをクローン。RFS は複数のハードウェアプラットフォーム(RomDisk、SFD700、picoZ80)で共有されるため、別プロジェクトとして管理されている。
  5. Build RFS for SFD700 — RFS アセンブリ定義の BUILD_SFD700 フラグを設定し、GLASS アセンブラを使用して Z80 ROM コードをコンパイル。生成された ROM イメージは SFD700 ROM レイアウトに従ってパッケージ化。
  6. Package Releases — CPLD ビットストリーム、ROM イメージ、および結合完全パッケージの 3 つのリリースアーカイブを作成。
  7. Create Gitea Release — リリースアーティファクトをバージョン番号でタグ付けされた新しい Gitea リリースにアップロード。

Webhook 設定

パイプラインは、Jenkins Generic Webhook Trigger プラグインに POST リクエストを送信する Gitea Webhook によってトリガーされます。設定は以下の通りです:
  • Webhook URLhttp://<jenkins-container-ip>:8080/generic-webhook-trigger/invoke?token=sfd700-build-trigger
  • トークンsfd700-build-trigger
  • コンテンツタイプapplication/json
  • ブランチフィルターrefs/heads/main または refs/heads/master へのプッシュのみトリガー。フィーチャーブランチ、タグ、その他の ref へのプッシュは無視される。
Webhook URL は Jenkins コンテナの Docker 内部 IP を使用します。Jenkins と Gitea の両方が同じホスト上の Docker コンテナとして動作するためです。Generic Webhook Trigger プラグインは Gitea プッシュペイロードから $.ref フィールドを解析し、正規表現フィルターを適用して main/master ブランチへのプッシュのみビルドをトリガーするようにします。

バージョン管理

ビルドバージョンはリポジトリルートの VERSION ファイルに MAJOR.MINOR 形式(例:"1.05")で格納されます。バージョン管理は以下のルールに従います:
  • VERSION がトリガーコミットで変更されている場合、ファイルに指定されたバージョンがそのままリリースに使用される。
  • VERSION が未変更の場合、パイプラインは Gitea API で最新リリースタグを照会し、マイナーバージョン番号を自動インクリメント。
  • マイナーバージョンは 100 でロールオーバー:バージョン 1.99 は 2.00 にインクリメント(メジャーバージョンが 1 増加、マイナーが 00 にリセット)。
  • VERSION ファイルが存在せず、以前のリリースも見つからない場合、バージョンはデフォルトで 1.00 になる。

CPLD ビルドステージ

CPLD コンパイルは Docker コンテナ(quartus-ii-13.0.1)内で動作する Altera Quartus II 13.0.1 を使用します。このバージョンの Quartus は MAX7000S デバイスファミリーをサポートする最後のバージョンであるため必要です。ビルドは EPM7128SLC84-15 CPLD の JTAG プログラミングに適した .pof(Programmer Object File)を生成します。
Jenkins 自体が Docker コンテナ内で動作するため、Quartus ビルドはサイドカーコンテナパターンを使用します — 両方のコンテナがホスト上の Docker デーモンを共有します。これはワークスペースパスを Jenkins コンテナパスから Docker ホストパスにマッピングする必要があることを意味します:
// サイドカーコンテナボリュームの Groovy パスマッピング
def workspace = pwd()
def hostWorkspace = workspace.replace('/var/jenkins_home', '/srv/jenkins/data')

// ホストパスマッピングを使用してサイドカーコンテナで Quartus を実行
docker run --rm --net=host \
    -v "${hostWorkspace}:${workspace}" \
    -w "${workspace}/CPLD/v1.2/build" \
    quartus-ii-13.0.1 \
    /opt/altera/quartus/bin/quartus_sh --flow compile sfd700
--net=host フラグは Quartus がネットワークアクセスを必要とするライセンスチェックを実行するために必要です。--flow compile コマンドは完全な Quartus コンパイルフロー(Analysis & Synthesis、Fitter、Assembler、Timing Analyzer)を実行します。出力 .pof ファイルは CPLD/v1.2/build/output_files/sfd700.pof に書き込まれます。

RFS ビルドステージ

RFS ビルドステージは別の RFS リポジトリをクローンし、SFD700 ハードウェア専用の ROM ファイリングシステムをコンパイルします。ビルドプロセスは GLASS Z80 アセンブラ(glass-0.5.1.jar)を使用して Z80 ソースコードをアセンブルし、make_roms.sh スクリプトで生成されたバイナリを SFD700 ROM レイアウトにパッケージ化します。
ビルドターゲットは asm/include/rfs_definitions.asm 内のアセンブリ時等価定数で制御されます。パイプラインはコンパイル前に sed を使用して適切なビルドフラグを設定します:
; RFS ビルドターゲットフラグ(一度に 1 つのみ 1 に設定する必要がある)
BUILD_ROMDISK                    EQU     0       ; MZ-80A RomDisk ハードウェア
BUILD_SFD700                    EQU     1       ; SFD-700 mkII フロッピーアダプター
BUILD_PICOZ80                    EQU     0       ; picoZ80 エミュレーターボード
パイプラインは BUILD_SFD700 EQU 1 に設定し、他のすべてのターゲットを 0 に設定してから build.sh を実行します。このスクリプトは以下のツールを順番に呼び出します:
  1. assemble_rfs.sh — GLASS を使用して rfs.asm および rfs_mrom.asm をアセンブルし、roms/ ディレクトリに rfs.romrfs_mrom.rom を生成。
  2. assemble_cpm.sh — CP/M 関連 ROM をアセンブル(ビルドターゲットに該当する場合)。
  3. assemble_roms.sh — 追加 ROM イメージをアセンブル。
  4. make_roms.sh — アセンブルされた ROM を最終的な SFD700 ROM イメージにパッケージ化。SFD700 ROM レイアウトは 256KB の複合イメージです:
    SFD700 ROM レイアウト(256KB):
    0x00000-0x00FFF  MZ-80A フロッピー ROM(mz80afi_sfd700.rom、ミラーリング)
    0x01000-0x01FFF  MZ-1E05 MZ-700 フロッピー ROM(mz-1e05.rom)
    0x02000-0x0BFFF  RFS rfs.rom(40KB ROM ファイリングシステム)
    0x0C000-0x7FFFF  USER / ROM ファイルシステムストレージ
    

リリースアーティファクト

各パイプライン実行で 3 つの gzip 圧縮リリースアーカイブが生成されます:
アーティファクト 内容 用途
SFD700-CPLD-vX.XX.tar.gz CPLD ビットストリーム(sfd700.pof EPM7128SLC84-15 CPLD の JTAG プログラミング
SFD700-ROMs-vX.XX.tar.gz RFS ROM イメージ(SFD700_256.binrfs.romrfs_mrom.rom SFD700 ROM へのプログラマーによる書き込み
SFD700-Complete-vX.XX.tar.gz CPLD + ROM 結合パッケージ CPLD と ROM の両方の完全アップデート
CPLD ビットストリームは Quartus II Programmer(またはコマンドラインの quartus_pgm)を使用して USB-Blaster JTAG アダプター経由でプログラムされます。ROM イメージは標準の EPROM/Flash プログラマーを使用して SFD700 Flash ROM チップに書き込まれます。

パイプライン設定

Jenkins パイプラインは Jenkins コンテナ内の /var/jenkins_home/pipeline-scripts/sfd700-build.groovy に格納された Groovy スクリプトとして定義されています。初期化スクリプト(07-sfd700-pipeline.groovy)が Jenkins 起動時にこのパイプライン定義を読み込みます。パイプライン全体で使用されるキー環境変数は以下の通りです:
変数 目的
GITEA_URL Gitea インスタンスのベース URL(https://git.eaw.app)
REPO_URL SFD700 リポジトリの完全クローン URL
RFS_URL RFS リポジトリの完全クローン URL(ビルド中にクローン)
GITEA_TOKEN リリース作成とアセットアップロード用の API トークン
GITEA_OWNER リポジトリオーナー(”eaw”)
GITEA_REPO リポジトリ名(”SFD700”)
VPS 上のパイプラインインフラストラクチャは以下で構成されます:
  • Jenkins コンテナjenkins_jenkins_1 として実行され、/var/jenkins_home がホストの /srv/jenkins/data にマップ。
  • Quartus Docker イメージquartus-ii-13.0.1、MAX7000S デバイスサポートを含む Altera Quartus II 13.0.1 を含むビルド済みイメージ。FusionX パイプラインと共有。
  • Gitea インスタンスgit.eaw.app で SFD700 および RFS リポジトリをホスト。
  • 初期化スクリプト/var/jenkins_home/init.groovy.d/ に配置された Groovy スクリプトで、Jenkins 起動時にパイプラインジョブを作成および設定。
  • パイプラインスクリプト/var/jenkins_home/pipeline-scripts/ に配置され、初期化スクリプトによって読み込まれる完全な Groovy パイプライン定義を含む。

手動ビルドトリガー

リポジトリにプッシュせずにビルドを手動でトリガーするには、Webhook エンドポイントに POST リクエストを送信します:
# ビルドを手動でトリガー
curl -X POST \
    -H "Content-Type: application/json" \
    -d '{"ref": "refs/heads/main"}' \
    "http://<jenkins-ip>:8080/generic-webhook-trigger/invoke?token=sfd700-build-trigger"
Jenkins IP は Jenkins コンテナの Docker 内部 IP です(docker inspect jenkins_jenkins_1 で確認可能)。Webhook URL は Gitea ウェブインターフェースのリポジトリ設定 → Webhooks セクションからもアクセスでき、テスト配信ボタンで以前のプッシュイベントを再送信できます。

メール通知

パイプラインはビルド完了時にメール通知を送信します:
  • 成功 — 件名:「SFD700 Build vX.XX - SUCCESS」、ビルドバージョン、ジョブ名、ビルド番号、URL を含む。
  • 失敗 — 件名:「SFD700 Build - FAILED」、ジョブ URL と詳細はコンソール出力を確認する旨のメモを含む。
メールは Jenkins 初期化スクリプト 05-email-config.groovy で設定され、support@net2net-ips.com に送信されます。

参考サイト

リソース リンク
SFD-700 mkII プロジェクトページ /sfd700/
SFD-700 mkII ユーザーマニュアル /sfd700-usermanual/
SFD-700 mkII テクニカルガイド /sfd700-technicalguide/
SFD-800(コンパニオン MZ-800 カード) /sfd800/
VHDL 言語リファレンスマニュアル IEEE 1076-2008 標準
Altera MAX7000S データシート Intel FPGA 製品ページ — MAX 7000S ファミリー
Quartus II 13.0.1 SP1 Web Edition Intel FPGA ソフトウェアアーカイブ
WD1773 データシート Western Digital / 歴史的アーカイブ
CUPL リファレンスマニュアル Logical Devices Inc.(アーカイブ)