tranZPUter FusionX デベロッパーズガイド

概要

tranZPUter FusionXのソフトウェアスタックは、完全なZ80エミュレーションおよび仮想ハードウェアシステムを形成する3つの明確な層に編成されています。各層には明確に定義された責任があり、新しいホスト機のサポートを追加する場合、または既存のものを拡張する場合は、3つすべての層にわたる協調した変更が必要です。
  1. Linuxカーネルモジュールz80drv.kottymzdrv.ko)— Cで記述され、SigmaStar Linux 4.9-rtカーネルツリーに対してビルドされます。z80drvは分離されたCPUコアでZ80エミュレーションディスパッチループを実行し、SPIとGPIOでCPLDと通信します。ttymzdrvはSharp MZキーボードと文字ディスプレイハードウェアへのTTYインターフェースを提供します。
  2. ユーザー空間ユーティリティz80ctrlk64fcpusharpbiter)— Cで記述され、arm-linux-gnueabihf-gccでクロスコンパイルされます。これらのユーティリティは実行時にカーネルモジュールを起動、停止、設定します。k64fcpuはK64F仮想CPUインターフェースをエミュレートするデーモンとして動作し、sharpbiterはホスト機ソフトウェアとLinuxコンソールの間でキーボードとディスプレイへのアクセスを調整します。
  3. CPLD RTL(VHDL) — Altera Quartus II 13.0.1 SP1 Web Editionでビルドされます。CPLDはホスト機のZ80バス上に直接置かれ、物理的なZ80バス信号とSigmaStar SOM(System-on-Module)の間のハードウェアインターフェースとして機能します。すべてのバスサイクルをキャプチャし、SOMに通知し、読み取りサイクルのためにSOMに代わってZ80データバスを駆動します。
新しいマシンのサポートを追加するには、3つすべての層に変更が必要です:カーネルドライバの新しいメモリマップ定義と仮想ハードウェアモジュール、ターゲットマシンのバスピンアウトとタイミング用にコンパイルされた新しいCPLD VHDLバリアント、そしてそれらを結びつける起動スクリプトです。

ソースツリー

すべてのソースコードはFusionX/リポジトリルート下に編成されています。以下のディレクトリレイアウトは開発者が作業する必要がある主要なファイルを示しています。ビルドバリアントディレクトリ(src.mz80a/src.mz700/など)にはMakefilesrc/内の共通ソースファイルへのシンボリックリンクが含まれており、マシン固有のファイルのみがバリアント間で異なります。
FusionX/
├── CPLD/
│   └── v1.0/
│       ├── MZ80A/build/          Sharp MZ-80A用Quartus IIプロジェクト
│       ├── MZ700/build/          Sharp MZ-700用Quartus IIプロジェクト
│       ├── MZ2000/build/         Sharp MZ-2000用Quartus IIプロジェクト
│       ├── PCW8256/build/        Amstrad PCW-8256用Quartus IIプロジェクト
│       ├── tzpuFusionX.vhd           メインRTL(FSM、SPI、バスインターフェース、ビデオ/オーディオ)
│       ├── tzpuFusionX_Toplevel.vhd  トップレベルエンティティとI/Oピンアサイン
│       └── tzpuFusionX_pkg.vhd       共有パッケージ(型、定数、ジェネリクス)
└── software/
    ├── linux/
    │   └── Build_FusionX.sh      マスタービルドスクリプト(U-boot + カーネル + rootfs + apps)
    └── FusionX/
        └── src/
            ├── z80drv/
            │   ├── src.mz80a/    MZ-80Aビルドバリアント用MakefileとSymlinks
            │   ├── src.mz700/    MZ-700ビルドバリアント
            │   ├── src.mz2000/   MZ-2000ビルドバリアント
            │   ├── src.pcw/      PCW-8256ビルドバリアント
            │   └── src/          共通ソースファイル:
            │       ├── z80driver.c      メインカーネルモジュール:ディスパッチループ、メモリ/IOルーティング
            │       ├── z80driver.h      データ構造、マシンごとのメモリマップ定数
            │       ├── z80io.c          HAL:SPI書き込みパス、GPIO読み取りパス、CPLD通信
            │       ├── emumz.c          Zeta Z80命令実行ラッパー
            │       ├── z80vhw_mz80a.c   MZ-80A仮想ハードウェア
            │       ├── z80vhw_mz700.c   MZ-700仮想ハードウェア
            │       ├── z80vhw_mz2000.c  MZ-2000仮想ハードウェア
            │       ├── z80vhw_pcw.c     PCW-8256仮想ハードウェア
            │       ├── z80vhw_rfs.c     ROMファイリングシステム仮想デバイス
            │       └── z80vhw_tzpu.c    tranZPUter SW仮想ハードウェア(K64Fスタブ)
            ├── ttymz/
            │   ├── Makefile
            │   └── ttymzdrv.c           MZキーボード/ディスプレイTTYドライバ
            └── utils/
                ├── z80ctrl.c            z80drv制御ユーティリティ
                ├── k64fcpu.c            K64F仮想CPUデーモン
                └── sharpbiter.c         キーボード/ディスプレイアービタデーモン

開発環境のセットアップ

FusionXビルド環境には3つの独立したツールチェーンが必要です:ユーザー空間ユーティリティとカーネルモジュール用のARMクロスコンパイラ、カーネルヘッダアクセス用のSigmaStar SDK、CPLD合成用のAltera Quartus IIです。これらはすべて標準的なx86-64 Linuxホスト(Ubuntu 20.04 LTS以降を推奨)にインストールできます。

ツールチェーン

  • クロスコンパイラ: arm-linux-gnueabihf-gccsudo apt install gcc-arm-linux-gnueabihfでインストールします。これはCortex-A7 SOM用の標準ハードフロートARMクロスコンパイラです。
  • カーネルヘッダ: SigmaStar SDKソースツリーから取得します。SDKはカーネルモジュールをビルドするために必要なフルカーネルソース(SigmaStarパッチ付きLinux 4.9-rt)を提供します。ディストロのカーネルヘッダは使用しないでください。SigmaStarパッチ済みカーネルは異なるABIを持っています。
  • CPLDツールチェーン: Altera Quartus II 13.0.1 SP1 Web Edition(無料)。この特定のバージョンが必要です。後のバージョンはFusionXボードで使用されているMAX7000AEファミリのサポートを削除しました。intel.comのIntel/Alteaレガシーソフトウェアセクションからダウンロードしてください。
  • ビルドツール: makebclibssl-devgitsudo apt install make bc libssl-dev gitでインストールします。

SDKのセットアップ

SigmaStar SDKはSSD202 SOM用の完全なビルド環境を提供します。U-boot、PREEMPT_RTパッチ済みカーネル、Buildrootベースのルートファイルシステムを含みます。マスタービルドスクリプトBuild_FusionX.shはフルビルドパイプラインを管理します。
# SigmaStar SDKをクローン/取得(アクセスについてはメンテナに連絡するかプロジェクトリポジトリを確認)
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm
export KERNEL_DIR=/path/to/sigmastar-sdk/kernel


# SSD202 SOM用フルNANDフラッシュイメージをビルド(初回ビルド)
cd software/linux/
./Build_FusionX.sh -f nand -p ssd202 -o 2D06 -m 256
-f nandフラグはNANDフラッシュ出力を選択し、-p ssd202はSigmaStar SSD202プラットフォームを選択し、-o 2D06はチップステッピングを指定し、-m 256はフラッシュサイズをメガバイト単位で設定します。出力イメージはproject/image/output/images/に配置され、以下のOTAセクションで説明するSDカードアップグレードメカニズムを介してNANDに書き込むことができます。

カーネルモジュールのビルド

各ビルドバリアントディレクトリにはシンボリックリンクで共通ソースファイルを参照するMakefileが含まれています。特定のマシンターゲット用にz80drv.koをビルドするには、対応するバリアントディレクトリに入り、カーネルディレクトリとクロスコンパイラを指定してmakeを実行してください。
# MZ-80AターゲットのためにZ80drvをビルド
cd software/FusionX/src/z80drv/src.mz80a
make KDIR=/path/to/sigmastar-sdk/kernel CROSS_COMPILE=arm-linux-gnueabihf-


# ビルドされたモジュールをSSH経由で実行中のFusionXボードにコピー
scp z80drv.ko root@192.168.1.100:/apps/FusionX/modules/


# FusionXボード上:古いモジュールをアンロードして新しいものをロード
rmmod z80drv
insmod /apps/FusionX/modules/z80drv.ko
新しいモジュールをロードした後、初期化エラーについてdmesgを確認してください。モジュールパラメータ(特定のビルドでサポートされている場合)はinsmodコマンドラインで渡すことができます。

継続的インテグレーション(Jenkins)

CI/CDとは? 継続的インテグレーション / 継続的デリバリー(CI/CD)とは、リポジトリにプッシュされたすべてのコード変更が、専用サーバー上でビルドとテストのシーケンスを自動的にトリガーするプラクティスです。開発マシンでビルドスクリプトを手動で実行してリリースファイルを手動でアップロードする代わりに、CIサーバーがリポジトリのクローン、すべてのビルドステップの実行、結果のパッケージング、ダウンロード可能なリリースの公開を行います。何か問題が発生した場合、サーバーは即座にメール通知を送信します。これにより、問題を早期に検出でき(たとえば、ローカルマシンに存在していたがコミットされていなかったファイルの欠落など)、すべてのリリースがクリーンで再現可能な出発点からビルドされることが保証されます。
FusionXプロジェクトはJenkins(人気のあるオープンソース自動化サーバー)をVPS(Virtual Private Server)上で使用しています。Jenkins自体はDockerコンテナ内で動作し、セットアップと移植性を容易にしています。Quartus II CPLDコンパイル用に2番目のDockerコンテナを起動します。このセクションでは、新しいサーバーからの完全なセットアップ手順を説明します。

サーバー要件
以下の仕様のLinuxサーバー(Debian、Ubuntu、または類似のもの)が必要です:
  • 最低2 GB RAM(4 GB推奨 — Quartusのコンパイルはメモリを多く消費します)
  • 20 GBの空きディスク容量(Jenkinsデータ、Dockerイメージ、ビルドアーティファクト用)
  • DockerとDocker Composeがインストール済みであること
  • Gitea(またはGitHub)リポジトリへのネットワークアクセス
  • ドメイン名または静的IPアドレス(Webhookコールバック用)
# Install Docker on Debian/Ubuntu
sudo apt update && sudo apt install -y docker.io docker-compose
sudo systemctl enable docker
sudo systemctl start docker

# Allow your user to run Docker commands without sudo
sudo usermod -aG docker $USER
# Log out and back in for the group change to take effect

サーバーへのARMクロスコンパイラのインストール
FusionXのカーネルモジュールとユーザー空間ツールにはLinaro GCC 5.5クロスコンパイラが必要です。これはJenkinsコンテナ内ではなく、ホストマシンにインストールする必要があります。実行時にコンテナにバインドマウントされるためです。
# Download and install Linaro GCC 5.5-2017.10
cd /tmp
wget https://releases.linaro.org/components/toolchain/binaries/5.5-2017.10/arm-linux-gnueabihf/gcc-linaro-5.5.0-2017.10-x86_64_arm-linux-gnueabihf.tar.xz
sudo mkdir -p /opt/arm-linux-gnueabihf
sudo tar xf gcc-linaro-5.5.0-2017.10-x86_64_arm-linux-gnueabihf.tar.xz \
    --strip-components=1 -C /opt/arm-linux-gnueabihf

# Verify
/opt/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc --version
# Expected: arm-linux-gnueabihf-gcc (Linaro GCC 5.5-2017.10) 5.5.0
なぜLinaro 5.5なのか? カーネルモジュールはSigmaStar Linux 4.9-rtカーネルに対してビルドされており、このカーネルはまさにこのツールチェーンでコンパイルされています。異なるGCCバージョンを使用すると、互換性のないABI(Application Binary Interface)を持つモジュールが生成され、コンパイルエラーなしでコンパイルされたとしても、FusionXボードでのロード時にクラッシュする可能性があります。

Quartus II Dockerイメージのビルド
CPLDビットストリームはDockerコンテナ内で動作するAltera Quartus II 13.0.1 SP1 Web Editionによってコンパイルされます。これにより、Quartusをネイティブにインストールする必要がなくなります(Ubuntu 16.04と32ビットライブラリが必要です)。サーバー上に以下のDockerfileを作成してください:
# Dockerfile.13.0.1 — Quartus II 13.0.1 SP1 for headless CPLD compilation
FROM ubuntu:xenial

ENV DEBIAN_FRONTEND=noninteractive

RUN dpkg --add-architecture i386 && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
        wget ca-certificates lib32z1 lib32stdc++6 lib32gcc1 \
        libx11-6:i386 libxext6:i386 libxrender1:i386 \
        libxtst6:i386 libxi6:i386 libfreetype6:i386 \
        libfontconfig1:i386 libsm6:i386 libxft2:i386 \
        unzip xvfb libncurses5 libncurses5:i386 && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /tmp/quartus_install

# Download Quartus II 13.0.1 SP1 Web Edition installer + device support
RUN wget -q http://download.altera.com/akdlm/software/acdsinst/13.0sp1/232/ib_installers/QuartusSetupWeb-13.0.1.232.run && \
    wget -q http://download.altera.com/akdlm/software/acdsinst/13.0sp1/232/ib_installers/QuartusSetupWeb-13.0.1.232-linux.run && \
    wget -q http://download.altera.com/akdlm/software/acdsinst/13.0sp1/232/ib_installers/max_013001.qdz && \
    wget -q http://download.altera.com/akdlm/software/acdsinst/13.0sp1/232/ib_installers/cyclone_013001.qdz && \
    wget -q http://download.altera.com/akdlm/software/acdsinst/13.0sp1/232/ib_installers/arria_013001.qdz && \
    chmod +x *.run

# Install Quartus unattended
RUN ./QuartusSetupWeb-13.0.1.232.run --mode unattended \
        --installdir /opt/altera --accept_eula 1 && \
    rm -rf /tmp/quartus_install

ENV PATH="/opt/altera/quartus/bin:${PATH}"

WORKDIR /build
CMD ["/bin/bash"]
Dockerイメージをビルドします(Alteraのサーバーから約2 GBをダウンロードするため、10〜15分かかります):
# Build the Quartus Docker image
docker build -f Dockerfile.13.0.1 -t quartus-ii-13.0.1 .

# Verify the image works
docker run --rm quartus-ii-13.0.1 quartus_sh --version
# Expected output: Version 13.0.1 Build 232
Jenkinsを使用せずに、コマンドラインからCPLDビットストリームを対話的にコンパイルすることもできます:
# Compile a single CPLD target from the host command line
cd /path/to/tzpuFusionX
docker run --rm --net=host \
    -v "$(pwd):$(pwd)" \
    -w "$(pwd)/CPLD/v1.0/MZ80A/build" \
    quartus-ii-13.0.1 \
    quartus_sh --flow compile tzpuFusionX_MZ80A

# Compile all four targets
for target in MZ80A MZ700 MZ2000 PCW8256; do
    docker run --rm --net=host \
        -v "$(pwd):$(pwd)" \
        -w "$(pwd)/CPLD/v1.0/${target}/build" \
        quartus-ii-13.0.1 \
        quartus_sh --flow compile "tzpuFusionX_${target}"
done
Quartus GUIで対話的なCPLD編集が必要な場合(たとえば、ピンアサインの変更やRTLビューアの表示)、X11フォワーディングで実行できます:
# Run Quartus GUI with X11 forwarding (requires X server on host)
docker run --rm --net=host \
    -e DISPLAY=$DISPLAY \
    -v /tmp/.X11-unix:/tmp/.X11-unix \
    -v "$(pwd):$(pwd)" \
    -w "$(pwd)" \
    quartus-ii-13.0.1 \
    quartus

Jenkinsのインストール
Jenkinsは独自のDockerコンテナ内で動作します。サーバー上にプロジェクトディレクトリを作成し、以下の2つのファイルを追加してください:
# Create the Jenkins directory structure
sudo mkdir -p /srv/jenkins/data
cd /srv/jenkins
Dockerfile — 公式Jenkins LTSイメージを拡張し、PipelineがQuartusコンテナを起動できるようにDocker CLIツールを追加します:
# /srv/jenkins/Dockerfile
FROM jenkins/jenkins:lts

USER root

# Install Docker CLI (not the full daemon — Jenkins uses the host's Docker via the socket)
RUN curl -fsSL https://get.docker.com | sh && rm -rf /var/lib/apt/lists/*

# Match the Docker group GID on the host so Jenkins can access /var/run/docker.sock.
# Run "getent group docker" on the host to find the correct GID (commonly 994 or 999).
ARG DOCKER_GID=994
RUN groupadd -g ${DOCKER_GID} hostdocker || true && \
    usermod -aG ${DOCKER_GID} jenkins

USER jenkins
docker-compose.yml — 必要なすべてのボリュームマウントを含むJenkinsサービスを定義します:
# /srv/jenkins/docker-compose.yml
version: '3.8'
services:
  jenkins:
    build: .
    ports:
      - "8080:8080"       # Jenkins web UI
      - "50000:50000"     # Jenkins agent communication port
    volumes:
      - /srv/jenkins/data:/var/jenkins_home                      # Persistent Jenkins data
      - /opt/arm-linux-gnueabihf:/opt/arm-linux-gnueabihf:ro   # ARM cross-compiler (read-only)
      - /var/run/docker.sock:/var/run/docker.sock                # Host Docker socket for sibling containers
    environment:
      - JAVA_OPTS=-Djenkins.install.runSetupWizard=false
    restart: unless-stopped
3つのボリュームマウントが重要です:
  • /srv/jenkins/data/var/jenkins_home — すべてのJenkins設定、ジョブ、ビルド履歴がここに保存されます。コンテナの再起動後も永続化されます。
  • /opt/arm-linux-gnueabihf — 前のステップでインストールしたLinaro GCC 5.5クロスコンパイラ。PipelineがARMバイナリをコンパイルできるように読み取り専用でマウントされます。
  • /var/run/docker.sock — ホストのDockerソケット。JenkinsがDocker-inside-Dockerではなく、シブリングDockerコンテナ(Quartus)を起動できるようにします。
# Start Jenkins
cd /srv/jenkins
docker-compose up -d

# Check the logs for the initial admin password (first run only)
docker-compose logs jenkins | grep "initial admin password" -A 2

# Jenkins is now running at http://your-server:8080

Jenkinsの初期設定
ブラウザでhttp://your-server:8080を開きます。初回起動時にJenkinsは初期管理者パスワードを求めます。上記のコンテナログで確認するか、/srv/jenkins/data/secrets/initialAdminPasswordから読み取ってください。
プラグインのインストール: プロンプトが表示されたら「推奨プラグインをインストール」を選択します。完了後、Jenkinsの管理 → プラグイン → 利用可能から以下の追加プラグインをインストールしてください:
  • Generic Webhook Trigger — Gitea(またはGitHub)がコードのプッシュ時にシンプルなHTTP POSTでビルドをトリガーできるようにします。
  • Pipeline — ビルドスクリプトをGroovyコードとして記述できるようにします(通常、推奨プラグインにプリインストールされています)。
  • Email Extension — ビルドの成功/失敗のメール通知用です(オプションですが推奨)。
管理者ユーザーの作成: Jenkinsは最初の管理者アカウントの作成を求めます。強力なパスワードを選択してください。これはすべてのビルドジョブを管理するために使用するアカウントです。

Pipelineジョブの作成
Jenkins「Pipeline」ジョブは、チェックアウトからリリースのアップロードまで、ビルドプロセス全体を定義するGroovyスクリプトを読み取ります。FusionXビルドPipelineを作成するには:
  1. Jenkinsダッシュボードから新規ジョブ作成をクリックします。
  2. 名前を入力し(例:FusionX-Build)、Pipelineを選択して、OKをクリックします。
  3. Pipelineセクションまでスクロールします。定義を「Pipeline script」に設定し、以下に示すGroovy Pipelineスクリプトを貼り付けます。
  4. 保存をクリックします。

Pipelineスクリプト
FusionX用の完全なJenkins Pipelineスクリプトを以下に示します。すべてのコンポーネント(Z80 ROM、TZFS、CP/M、カーネルモジュール、SPIツール、CPLDビットストリーム)をビルドし、tarballにパッケージングし、ダウンロード可能なアセットとしてtarballを添付したGiteaリリースを作成します。先頭の環境変数をGiteaサーバーのURL、リポジトリの詳細、APIトークンに合わせて更新する必要があります。
pipeline {
    agent any

    environment {
        GITEA_URL   = "https://git.eaw.app"          // Your Gitea server URL
        REPO_URL    = "https://git.eaw.app/eaw/tzpuFusionX.git"
        GITEA_TOKEN = credentials('gitea-api-token')  // Jenkins credential ID for Gitea API token
        GITEA_OWNER = "eaw"                           // Gitea username or organisation
        GITEA_REPO  = "tzpuFusionX"                   // Repository name
        PATH        = "/opt/arm-linux-gnueabihf/bin:${env.PATH}"
    }

    triggers {
        // Triggered by Gitea webhook — see "Gitea Webhook Setup" below
        GenericTrigger(
            genericVariables: [
                [key: 'ref', value: '$.ref']
            ],
            causeString: 'Triggered by Gitea push to $ref',
            token: 'fusionx-build-trigger',
            printContributedVariables: true,
            printPostContent: false,
            regexpFilterText: '$ref',
            regexpFilterExpression: '^refs/heads/(main|master)$'
        )
    }

    stages {
        stage('Checkout') {
            steps {
                cleanWs()
                git url: "${REPO_URL}", branch: 'master'
                sh 'git submodule update --init --recursive'
            }
        }

        stage('Determine Version') {
            steps {
                script {
                    // Read VERSION file, or auto-increment from last Gitea release tag
                    if (!fileExists('VERSION')) {
                        writeFile file: 'VERSION', text: '1.00'
                    }
                    def repoVersion = readFile('VERSION').trim()
                    def gitDiff = sh(script: 'git diff HEAD~1 -- VERSION 2>/dev/null || echo NO_PREV',
                                     returnStdout: true).trim()
                    if (gitDiff.contains('NO_PREV') || gitDiff == '') {
                        def lastTag = sh(script: """
                            curl -sf -H "Authorization: token \$GITEA_TOKEN" \
                                "\$GITEA_URL/api/v1/repos/\$GITEA_OWNER/\$GITEA_REPO/releases?limit=1" \
                            | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4 || echo ''
                        """, returnStdout: true).trim()
                        if (lastTag && lastTag.startsWith('v')) {
                            def lastVer = lastTag.substring(1)
                            def parts = lastVer.tokenize('.')
                            def major = parts[0] as int
                            def minor = (parts.size() > 1 ? parts[1] : '0') as int
                            minor += 1
                            if (minor >= 100) { major += 1; minor = 0 }
                            env.VERSION = "${major}.${String.format('%02d', minor)}"
                        } else {
                            env.VERSION = repoVersion
                        }
                    } else {
                        env.VERSION = repoVersion
                    }
                    echo "Building version: ${env.VERSION}"
                }
            }
        }

        stage('Build Assembly ROMs') {
            steps {
                sh 'chmod +x build.sh && mkdir -p software/tmp software/roms'
                sh './build.sh --asm'
                sh 'mkdir -p release/roms && cp software/roms/*.rom software/roms/*.mzf release/roms/ 2>/dev/null || true'
            }
        }

        stage('Build TZFS ROMs') {
            steps {
                sh './build.sh --tzfs'
                sh 'mkdir -p release/tzfs && cp software/roms/tzfs_*.rom software/roms/testtz_*.mzf release/tzfs/ 2>/dev/null || true'
            }
        }

        stage('Build CP/M') {
            steps {
                sh './build.sh --cpm'
                sh 'mkdir -p release/cpm && cp software/roms/cpm223_*.bin release/cpm/ 2>/dev/null || true'
            }
        }

        stage('Build Drivers') {
            steps {
                sh './build.sh --drivers || true'
                sh 'mkdir -p release/drivers && cp software/FusionX/modules/*.ko software/FusionX/bin/* release/drivers/ 2>/dev/null || true'
            }
        }

        stage('Build SPI Tools') {
            steps {
                sh './build.sh --spi || true'
                sh 'mkdir -p release/drivers && cp software/FusionX/src/spitools/mspi_main release/drivers/ 2>/dev/null || true'
            }
        }

        stage('Build CPLD Bitstreams') {
            steps {
                script {
                    // Jenkins runs inside a container where /var/jenkins_home maps to /srv/jenkins/data
                    // on the host. Quartus runs as a SIBLING container, so volume mounts must use HOST paths.
                    def workspace = pwd()
                    def hostWorkspace = workspace.replace('/var/jenkins_home', '/srv/jenkins/data')
                    sh 'mkdir -p release/cpld'
                    sh """
                        for target in MZ80A MZ700 MZ2000 PCW8256; do
                            PROJECT="tzpuFusionX_\${target}"
                            BUILDDIR="CPLD/v1.0/\${target}/build"
                            if [ -f "${workspace}/\${BUILDDIR}/\${PROJECT}.qpf" ]; then
                                echo "==> Compiling CPLD: \${target}"
                                docker run --rm --net=host \\
                                    -v "${hostWorkspace}:${workspace}" \\
                                    -w "${workspace}/\${BUILDDIR}" \\
                                    quartus-ii-13.0.1 \\
                                    /opt/altera/quartus/bin/quartus_sh --flow compile "\${PROJECT}" \\
                                && cp "${workspace}/\${BUILDDIR}/output_files/\${PROJECT}.pof" release/cpld/ 2>/dev/null \\
                                || echo "  [FAIL] CPLD: \${target}"
                            fi
                        done
                    """
                }
            }
        }

        stage('Package Releases') {
            steps {
                script {
                    def ver = env.VERSION
                    sh """
                        cd release/roms && tar czf ../../FusionX-ROMs-v${ver}.tar.gz *    && cd ../..
                        cd release/tzfs && tar czf ../../FusionX-TZFS-v${ver}.tar.gz *    && cd ../..
                        cd release/cpm  && tar czf ../../FusionX-CPM-v${ver}.tar.gz *     && cd ../..
                        [ -n "\$(ls release/cpld/ 2>/dev/null)" ] && \
                            cd release/cpld && tar czf ../../FusionX-CPLD-v${ver}.tar.gz *    && cd ../..
                        [ -n "\$(ls release/drivers/ 2>/dev/null)" ] && \
                            cd release/drivers && tar czf ../../FusionX-Drivers-v${ver}.tar.gz * && cd ../..
                    """
                    archiveArtifacts artifacts: "FusionX-*-v${ver}.tar.gz"
                }
            }
        }

        stage('Create Gitea Release') {
            steps {
                script {
                    def ver = env.VERSION
                    def tag = "v${ver}"
                    def commitSha = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
                    def shortSha = commitSha.take(8)

                    // Delete any existing release with the same tag
                    sh """
                        OLD_ID=\$(curl -s -H "Authorization: token \$GITEA_TOKEN" \
                            "\$GITEA_URL/api/v1/repos/\$GITEA_OWNER/\$GITEA_REPO/releases/tags/${tag}" \
                            | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
                        [ -n "\$OLD_ID" ] && curl -s -X DELETE -H "Authorization: token \$GITEA_TOKEN" \
                            "\$GITEA_URL/api/v1/repos/\$GITEA_OWNER/\$GITEA_REPO/releases/\$OLD_ID"
                    """

                    // Create release via Gitea API
                    def body = "Automated build from commit ${shortSha}\\n\\nIncludes:\\n" +
                        "- FusionX-ROMs-v${ver}.tar.gz\\n- FusionX-TZFS-v${ver}.tar.gz\\n" +
                        "- FusionX-CPM-v${ver}.tar.gz\\n- FusionX-CPLD-v${ver}.tar.gz\\n" +
                        "- FusionX-Drivers-v${ver}.tar.gz"
                    def releaseJson = """{"tag_name":"${tag}","name":"FusionX ${tag}","body":"${body}","target_commitish":"${commitSha}"}"""
                    writeFile file: 'release_body.json', text: releaseJson
                    sh 'curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" -d @release_body.json "$GITEA_URL/api/v1/repos/$GITEA_OWNER/$GITEA_REPO/releases" > release_response.json'
                    def releaseId = sh(script: "grep -o '\"id\":[0-9]*' release_response.json | head -1 | cut -d: -f2", returnStdout: true).trim()

                    // Upload release tarballs
                    sh """
                        for asset in FusionX-ROMs-v${ver}.tar.gz FusionX-TZFS-v${ver}.tar.gz FusionX-CPM-v${ver}.tar.gz FusionX-CPLD-v${ver}.tar.gz FusionX-Drivers-v${ver}.tar.gz; do
                            [ -f "\${asset}" ] && curl -sf -H "Authorization: token \$GITEA_TOKEN" \
                                -F "attachment=@\${asset}" \
                                "\$GITEA_URL/api/v1/repos/\$GITEA_OWNER/\$GITEA_REPO/releases/${releaseId}/assets"
                        done
                    """
                }
            }
        }
    }

    post {
        success {
            mail to: 'your-email@example.com',
                 subject: "FusionX Build v${env.VERSION} - SUCCESS",
                 body: "FusionX build v${env.VERSION} completed.\nJob: ${env.JOB_NAME} #${env.BUILD_NUMBER}\nURL: ${env.BUILD_URL}"
        }
        failure {
            mail to: 'your-email@example.com',
                 subject: "FusionX Build - FAILED",
                 body: "FusionX build failed.\nJob: ${env.JOB_NAME} #${env.BUILD_NUMBER}\nURL: ${env.BUILD_URL}"
        }
        always { cleanWs() }
    }
}

Gitea Webhookの設定
Webhookは、コードがプッシュされるたびにGiteaサーバーからJenkinsに通知を送信するよう設定します。これがないと、コミットのたびにJenkinsで手動で「ビルド実行」をクリックする必要があります。
  1. Giteaで、リポジトリ → 設定WebhooksWebhookを追加Giteaに移動します。
  2. ターゲットURLを以下に設定します:
    http://your-server:8080/generic-webhook-trigger/invoke?token=fusionx-build-trigger
    tokenの値はPipelineのGenericTriggerブロック内のtoken:フィールドと一致する必要があります。
  3. HTTPメソッドをPOSTに、Content Typeapplication/jsonに設定します。
  4. トリガーで「Push Events」を選択します。
  5. Webhookを追加をクリックします。Giteaは即座にテストpingを送信します。Jenkinsで受信されたことを確認してください。
GitHubリポジトリの場合も手順は同じです:Settings → Webhooks → Add webhookに移動し、同じURLを使用します。Generic Webhook Triggerプラグインは、refフィールドを含むJSON本文でHTTP POSTを送信できる任意のシステムで動作します。

シブリングコンテナの仕組み
このセットアップで最も微妙な部分はCPLDビルドステップです。JenkinsはDockerコンテナ内で動作しており、CPLDビットストリームをコンパイルするために2番目のDockerコンテナ(Quartus)を起動する必要があります。これはホストのDockerソケット(/var/run/docker.sock)をJenkinsコンテナにマウントすることで実現されます。つまり、QuartusコンテナはJenkins内のネストされたコンテナとしてではなく、ホスト上のシブリングとして実行されます。
これには重要な影響があります:ボリュームマウントパスはJenkinsコンテナのパスではなく、ホストのパスを使用する必要があります。Jenkinsコンテナ内ではワークスペースは/var/jenkins_home/workspace/FusionX-Buildにありますが、ホストファイルシステム上では同じディレクトリが/srv/jenkins/data/workspace/FusionX-Buildにあります。Pipelineはこれを自動的に変換します:
// Inside the Jenkins pipeline:
def workspace = pwd()
// workspace = "/var/jenkins_home/workspace/FusionX-Build"

def hostWorkspace = workspace.replace('/var/jenkins_home', '/srv/jenkins/data')
// hostWorkspace = "/srv/jenkins/data/workspace/FusionX-Build"

// The Quartus container mounts the HOST path, but uses the Jenkins path as the mount point
// so that all paths in the Quartus project files resolve correctly.
docker run --rm --net=host \
    -v "${hostWorkspace}:${workspace}" \
    -w "${workspace}/CPLD/v1.0/MZ80A/build" \
    quartus-ii-13.0.1 \
    quartus_sh --flow compile tzpuFusionX_MZ80A
--net=hostフラグは、QuartusがAlteraライセンスサーバーに起動時にコンタクトするために必要です。これがないと、コンテナはライセンスチェックを解決できず、Quartusは実行を拒否します。

最初のビルドの検証
PipelineとWebhookの設定が完了したら:
  1. masterブランチにコミットをプッシュします。GiteaがJenkinsにWebhookを送信します。
  2. JenkinsでFusionX-Buildジョブをクリックします。青い進行バーが表示された新しいビルドが確認できるはずです。
  3. ビルド番号をクリックし、次にコンソール出力をクリックして、ビルドをリアルタイムで監視します。
  4. ビルドが成功すると、すべてのステージが緑色で表示され、「SUCCESS」ステータスが表示されます。ビルドは通常3〜5分かかります。
  5. Giteaリポジトリのリリースページを確認してください。ダウンロード可能なtarballを含む新しいリリースが表示されるはずです。
ビルドが失敗した場合、コンソール出力にどのステージが失敗したか、およびその理由が正確に表示されます。よくある初回実行時の問題:
  • "arm-linux-gnueabihf-gcc: not found" — クロスコンパイラがコンテナにマウントされていません。docker-compose.yml/opt/arm-linux-gnueabihfのボリュームマウントを確認してください。
  • "docker: command not found" — Docker CLIがJenkinsイメージにインストールされていません。上記のDockerfileを使用してJenkins Dockerイメージをリビルドしてください。
  • "permission denied" on docker.sock — Jenkins DockerfileのDOCKER_GIDがホストのDockerグループGIDと一致していません。ホストでgetent group dockerを実行し、Dockerfileを更新してください。
  • Quartusライセンスエラー — Quartusコンテナが--net=hostで起動されていて、ライセンスサーバーに到達できることを確認してください。
  • "fixdep: error opening config file" — 依存関係がファイル名にスペースを使用しています。Zetaライブラリはこの問題を回避するためにリポジトリにベンダリングされています。git submodule update --init --recursiveでサブモジュールが初期化されていることを確認してください。

主要データ構造

z80driver.hで定義されたデータ構造はエミュレーションのバックボーンを形成しています。ドライバを変更したり新しいマシンターゲットを追加したりする前に、これらを理解することが不可欠です。

メモリマップ

サポートされている各マシンには、そのメモリ領域を定義するプリプロセッサ定数のセットがあり、続いてディスパッチループが実行時に使用するt_memRegion構造体のテーブルがあります。定数は適切なTARGET_*定義によってガードされており、これはバリアントMakefileによって設定されます。
// z80driver.h — MZ-80Aメモリマップの例
#define MZ80A_MONITOR_ROM_ADDR    0x0000
#define MZ80A_MONITOR_ROM_SIZE    0x1000  // 4KBシステムモニタ
#define MZ80A_VRAM_ADDR           0xD000  // ビデオRAM開始
#define MZ80A_VRAM_SIZE           0x0800  // 2KB VRAM
#define MZ80A_IO_KEYBOARD         0xE000  // キーボードI/Oポート

typedef struct {
    uint32_t  baseAddr;       // Z80アドレス空間の開始
    uint32_t  size;           // 領域サイズ(バイト)
    uint8_t  *data;           // エミュレーテッドメモリバッファへのポインタ
    uint8_t   type;           // MEM_ROM, MEM_RAM, MEM_VHARDWARE, MEM_PHYSICAL
    void     (*read_fn)(uint16_t addr, uint8_t *data);   // 仮想ハードウェア読み取りハンドラ
    void     (*write_fn)(uint16_t addr, uint8_t data);   // 仮想ハードウェア書き込みハンドラ
} t_memRegion;
typeフィールドはディスパッチの動作を制御します:MEM_ROM領域はバッファからデータを返し、書き込みを無視します;MEM_RAM領域はバッファへの読み取りと書き込みの両方を許可します;MEM_VHARDWARE領域はバッファに触れずread_fn/write_fnハンドラを呼び出します;MEM_PHYSICAL領域はサイクルをホスト機バス上の実際のハードウェアに渡します。

バスサイクルリクエスト

CPLDがGPIOを介して保留中のバスサイクルを通知すると、HAL層はアドレスと制御ラインを読み取り、t_busCycle構造体に格納します。この構造体はその後ルーティングのためにディスパッチループに渡されます。
// CPLD GPIOピンからキャプチャされたバスサイクル情報
typedef struct {
    uint16_t  address;        // Z80アドレスバスA0-A15
    uint8_t   data;           // Z80データバスD0-D7
    uint8_t   busType;        // BUS_MREQ_RD, BUS_MREQ_WR, BUS_IORQ_RD, BUS_IORQ_WR, BUS_M1
    bool      isRead;         // true = 読み取りサイクル、false = 書き込みサイクル
} t_busCycle;

仮想ハードウェアモジュールインターフェース

すべてのz80vhw_*.cファイルは固定された関数セットを実装します。ディスパッチループはバスサイクルがMEM_VHARDWAREとして登録された領域をターゲットとする場合にこれらの関数を呼び出します。インターフェース関数はすべての仮想ハードウェアモジュールに存在しなければなりません。特定のマシンでは一部がノーオペレーションであっても同様です。
// 各z80vhw_*.cはこれらの関数を実装します:

// モジュールロード時に一度呼び出される — メモリ/IO領域とハンドラを登録
int vhw_init(void);

// メモリ読み取りハンドラ — メモリマップ領域のためにディスパッチループから呼び出される
uint8_t vhw_mem_read(uint16_t addr);

// メモリ書き込みハンドラ
void vhw_mem_write(uint16_t addr, uint8_t data);

// I/O読み取りハンドラ(6502ベースのマシンには使用されないが、Z80では常に存在する)
uint8_t vhw_io_read(uint16_t port);

// I/O書き込みハンドラ
void vhw_io_write(uint16_t port, uint8_t data);

// Z80リセット時に呼び出される — ハードウェア状態を再初期化
void vhw_reset(void);

// ディスパッチループから定期的に呼び出される — タイマー、サウンドエミュレーションなどのため
void vhw_tick(uint64_t cycles);
vhw_init()関数はこのモジュールが処理するアドレス範囲をディスパッチループに伝えるためにregister_mem_region()register_io_handler()を呼び出す責任があります。vhw_init()で行われた登録はカーネルモジュールのライフタイムにわたって持続します。

z80driver.c — ディスパッチループ

ディスパッチループはFusionXシステム全体のホットパスです。CPU1に固定されたカーネルスレッドとして実行され(カーネルブート引数のisolcpus=1で分離)、Z80バスサイクル中にLinuxスケジューラがプリエンプトすることを防ぎます。CPLDがインターセプトするすべてのZ80マシンサイクルは、Z80のホールド時間内(4MHzでは約250ns)にこのループによってサービスされなければなりません。
各イテレーションでループは以下のステップを実行します:保留中のバスサイクル(アドレスとサイクルタイプ)を取得するためにCPLD GPIOラインを読み取り;登録されたメモリ領域テーブルでアドレスを検索し;適切なハンドラにディスパッチします(ROMデータを返す、カーネルRAMバッファにアクセスする、仮想ハードウェアハンドラを呼び出す、またはオペコードフェッチサイクルのためにZeta Z80ライブラリを呼び出す)。読み取りサイクルの場合、応答バイトはSPIでCPLDに書き戻されます。定期的なハウスキーピング(vhw_tick()の呼び出し)は2048イテレーションごとに実行されます。
// 簡略化されたディスパッチループ (z80driver.c)
static int z80_emulation_thread(void *data)
{
    while (!kthread_should_stop()) {
        // 1. CPLDが保留中のバスサイクルを通知するのを待つ(GPIO IRQまたはポール)
        t_busCycle cycle = cpld_read_bus_cycle();    // GPIOピンを読み取る

        // 2. サイクルタイプとアドレスに基づいてディスパッチ
        if (cycle.busType == BUS_M1) {
            // オペコードフェッチ — Zetaライブラリで1つのZ80命令を実行
            zeta_run_one_instruction(&z80_state);

        } else if (cycle.busType == BUS_MREQ_RD) {
            // メモリ読み取り
            uint8_t data = dispatch_mem_read(cycle.address);
            cpld_write_data(data);                   // CPLDへのSPI書き込み

        } else if (cycle.busType == BUS_MREQ_WR) {
            // メモリ書き込み
            dispatch_mem_write(cycle.address, cycle.data);

        } else if (cycle.busType == BUS_IORQ_RD) {
            // I/O読み取り
            uint8_t data = dispatch_io_read(cycle.address & 0xFF);
            cpld_write_data(data);

        } else if (cycle.busType == BUS_IORQ_WR) {
            // I/O書き込み
            dispatch_io_write(cycle.address & 0xFF, cycle.data);
        }

        // 3. 定期的なハウスキーピング
        if ((cycle_count++ & 0x7FF) == 0)
            vhw_tick(cycle_count);
    }
    return 0;
}
dispatch_mem_read()dispatch_mem_write()関数は登録されたt_memRegionテーブルを線形検索して要求されたアドレスをカバーする領域を見つけます。パフォーマンスのために、テーブルは最も頻繁にアクセスされる領域を最初に(通常はRAM、次にROM、次に仮想ハードウェア)順序づけるべきです。将来の最適化として、線形検索をアドレスの上位8ビットでインデックスされた256エントリのルックアップテーブルに置き換えることができます。

新しいマシンの追加

新しいホスト機のサポートを追加することは最も一般的な開発者タスクです。このプロセスにはソフトウェアスタックの3つの層すべてにわたる5つの独立したステップが含まれます。以下の例では仮想的なSinclair ZX Spectrum 48Kポートを使用して各ステップを具体的に説明します。

ステップ1 — z80driver.hでメモリマップを定義する

仮想ハードウェアモジュールが必要とするメモリ領域、I/Oポート、マシン固有の定数を定義する新しい#ifdef TARGET_*ブロックをz80driver.hに追加します。既存のマシン定義で確立された命名規則に従ってください。
#ifdef TARGET_SPECTRUM48K
  #define TARGET_NAME           "Spectrum48K"
  #define SPECTRUM_ROM_ADDR     0x0000
  #define SPECTRUM_ROM_SIZE     0x4000   // 16KB Spectrum ROM
  #define SPECTRUM_RAM_ADDR     0x4000
  #define SPECTRUM_RAM_SIZE     0xC000   // 48KB RAM
  #define SPECTRUM_ULA_PORT     0xFE    // ULA I/Oポート(キーボード、ボーダー、スピーカー、テープ)
#endif

ステップ2 — 仮想ハードウェアモジュールを作成する

software/FusionX/src/z80drv/src/に新しいファイルz80vhw_spectrum48k.cを作成します。このファイルは仮想ハードウェアインターフェースのすべての関数を実装しなければなりません。以下のスケルトンはキーボードマトリクスのスキャン、ボーダーカラー、スピーカー、MIC出力をカバーする完全なSpectrum 48K ULA実装を示しています。
// software/FusionX/src/z80drv/src/z80vhw_spectrum48k.c
#include "z80driver.h"

// ROMデータ — 初期化時にSDカードからロードされるかコンパイルイン
static uint8_t spectrum_rom[0x4000];
static uint8_t spectrum_ram[0xC000];

// ULA状態
static uint8_t ula_border = 7;    // 初期化時に白いボーダー
static uint8_t ula_ear = 0;
static uint8_t ula_mic = 0;

// キーボードマトリクス(8行×5列)
static uint8_t key_matrix[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

uint8_t vhw_mem_read(uint16_t addr) {
    if (addr < SPECTRUM_ROM_SIZE)
        return spectrum_rom[addr];
    if (addr >= SPECTRUM_RAM_ADDR)
        return spectrum_ram[addr - SPECTRUM_RAM_ADDR];
    return 0xFF;
}

void vhw_mem_write(uint16_t addr, uint8_t data) {
    if (addr >= SPECTRUM_RAM_ADDR)
        spectrum_ram[addr - SPECTRUM_RAM_ADDR] = data;
    // ROM書き込みは無視される
}

// ... (詳細な実装は省略)

ステップ3 — ビルドバリアントディレクトリを作成する

各マシンターゲットにはMakefileと必要な共通ソースファイルへのシンボリックリンクを含む独自のビルドバリアントディレクトリがあります。これにより、異なる-DTARGET_*フラグでコードを複製せずに同じソースファイルをコンパイルできます。
mkdir software/FusionX/src/z80drv/src.spectrum48k
cd software/FusionX/src/z80drv/src.spectrum48k


# このバリアントに必要な共通ソースファイルへのシンボリックリンクを追加
ln -s ../src/z80driver.c .
ln -s ../src/z80io.c .
ln -s ../src/emumz.c .
ln -s ../src/z80vhw_spectrum48k.c .
新しいディレクトリにターゲット定義をリストし、オブジェクトファイルを列挙し、カーネルビルドシステムを参照するMakefileを作成します。既存のバリアントMakefileに対するキーの追加は以下の通りです:
ccflags-y += -DTARGET_SPECTRUM48K
obj-m := z80drv.o
z80drv-objs := z80driver.o z80io.o emumz.o z80vhw_spectrum48k.o

ステップ4 — CPLD VHDLバリアントを作成する

物理的なソケットピンアウト、バスタイミング、アドレスデコードがマシン間で異なるため、CPLD RTLは各ホスト機に合わせてカスタマイズされなければなりません。推奨されるアプローチは、新しいターゲットに建築的に近い既存のQuartus IIプロジェクトをコピーして3つのVHDLファイルを変更することです。
  • 開始点としてCPLD/v1.0/MZ80A/CPLD/v1.0/Spectrum48K/にコピーします。
  • tzpuFusionX_Toplevel.vhdを編集:新しいマシンの物理的なソケットピンアウトに合わせてLOCATION制約のI/Oピンアサインを調整します。各マシンは異なるコネクタを使用してZ80バス信号を異なるCPLDピンにマッピングします。
  • tzpuFusionX.vhdを編集:マシン固有のFSM状態を追加します。Spectrum 48Kの場合、ULAのコンテンドメモリウェイトステート挿入(ULAは各水平スキャンラインの上半分の間4MHzでZ80からサイクルを奪います)を追加し、SpectumのZ80クロック3.5MHzに合わせてバスホールドタイミングを調整します。
  • tzpuFusionX_pkg.vhdを編集:Spectrum 48K用の新しいMACHINE_TYPE定数と新しいタイミングパラメータを追加します。
  • Quartus II 13.0.1 SP1で新しいプロジェクトを開き、解析と合成、フィッタ、アセンブラを実行して.pofビットストリームファイルを生成します。
  • USB Blaster(または互換品)プログラマとQuartus IIプログラマツールを使用してJTAG経由で新しいビットストリームをCPLDにプログラムします。

ステップ5 — 起動スクリプトを作成する

起動スクリプトはカーネルモジュールを正しい順序でロードし、アービタデーモンを起動し、z80ctrlを使用して新しいマシン用のZ80エミュレーションを設定します。スクリプトは実行可能にして、FusionXルートファイルシステムの適切な場所に配置しなければなりません。
#!/bin/sh

# startZ80_Spectrum48K.sh
insmod /apps/FusionX/modules/ttymzdrv.ko
/apps/FusionX/bin/sharpbiter &
taskset -c 1 insmod /apps/FusionX/modules/z80drv.ko
/apps/FusionX/bin/z80ctrl --adddev --device spectrum48k
/apps/FusionX/bin/z80ctrl --loadrom /sd/ROM/spectrum48k.rom --addr 0x0000 --size 0x4000
/apps/FusionX/bin/z80ctrl --start
insmod呼び出しの前のtaskset -c 1プレフィックスはモジュール初期化スレッドがCPU1で開始されることを保証します。モジュールがロードされると、ディスパッチループ自体はカーネルスレッドアフィニティAPIを介してCPU1に固定されます。--loadromコマンドはZ80が起動する前にSDカードからカーネルモジュールのROMバッファにROMイメージを転送します。

仮想ハードウェアデバイスの追加

既存のマシンモジュールへの新しい仮想周辺機器の追加は一般的な増分タスクです。例えば、仮想RTC、仮想シリアルポート、または元々は持っていなかった仮想サウンドチップをマシンに追加します。このプロセスには、未使用のI/Oポートアドレス範囲の選択、レジスタレイアウトの定義、読み取り/書き込みハンドラの実装、vhw_init()でのハンドラの登録が含まれます。
以下の例では、I/Oポート0xB00xB7でMZ-80A仮想ハードウェアモジュールに仮想リアルタイムクロック(RTC)を追加します。RTCは秒、分、時間、日、月、年の8つのバイト幅レジスタと2つの予備レジスタを公開します。Linuxカーネルのktime_get_real_ts64()関数(または同等品)は仮想RTCレジスタをホストシステムクロックと同期させるためにvhw_tick()から呼び出すことができます。
// z80vhw_mz80a.cで — I/Oポート0xB0-0xB7に仮想RTCを追加

typedef struct {
    uint8_t seconds, minutes, hours, day, month, year;
} t_virtualRTC;

static t_virtualRTC rtc = {0};

static uint8_t rtc_read(uint16_t port) {
    switch (port & 0x07) {
        case 0: return rtc.seconds;
        case 1: return rtc.minutes;
        case 2: return rtc.hours;
        case 3: return rtc.day;
        case 4: return rtc.month;
        case 5: return rtc.year;
        default: return 0xFF;
    }
}

// vhw_init()で:
register_io_handler(0xB0, 0x08, rtc_read, rtc_write);
register_io_handler()の2番目の引数はポート範囲サイズ(8ポート、0xB00xB7)です。ディスパッチループはこの範囲内のポートアドレスをターゲットとするI/Oサイクルに対してrtc_read()またはrtc_write()を呼び出します。

CPLDの変更

tzpuFusionX.vhdのCPLD RTLは開発者が拡張または変更する必要があるかもしれないいくつかの有限状態機械と通信プロトコルを実装しています。CPLDを変更する最も一般的な理由は、SOMとCPLD間で通信される新機能のための新しいSPIコマンドコードの追加、新しいホスト機のためのバスタイミングの調整、コンテンドメモリを持つマシン(Spectrum ULAなど)のためのウェイトステート挿入の追加です。

tzpuFusionX.vhdの主要箇所

  • Z80バスFSM: マルチフェーズZ80バスサイクル(M1、MREQ、IORQ、RD、WR、RFSH、HALT、BUSRQ、BUSAK)を処理します。DMAバスリクエスト/グラントなどの新しいサイクルタイプを追加するには、このFSMに新しい状態を追加して適切なVHDL信号をアサートします。Z80 CPU技術マニュアルで指定されているすべてのZ80バスタイミング要件を満たすよう注意してください。セットアップとホールドタイムの違反はデバッグが非常に困難なデータ破損を引き起こします。
  • SOMインターフェース: SPIスレーブとGPIO信号アサイン。新しいSPIコマンドはSPIデコーダプロセスを拡張することで追加できます。SOMへのGPIO信号は物理的なピン数によって制限されます。ピンを再アサインする場合はSOM Linux GPIOドライバの対応する変更が必要です。
  • ビデオタイミング: コンポジットビデオ出力のためのSync pulse生成。ビデオタイミングの変更はマシン固有です。MZ-80A、MZ-700、MZ-2000はキャラクタセルサイズとSync極性が異なります。タイミングの変更には水平および垂直周波数を維持するための慎重なカウンタ演算が必要です。
  • マシン固有のFSM状態: Spectrum ULAは各水平スキャンラインの上半分の間にアドレス0x40000x7FFFのメモリサイクルをコンテンドします。これを実装するには、CPLDが正しいピクセルクロックフェーズでZ80バス上のWAITをアサートする必要があります。バンク切り替え制御信号(ページドROMまたはRAMを持つマシン用)もここで管理されます。

新しいSPIコマンドの追加

SPIコマンドコードはtzpuFusionX_pkg.vhdの定数として定義され、tzpuFusionX.vhdのSPI受信プロセスでデコードされます。新しいコマンドを追加するには、まずパッケージファイルに定数を定義し、次にSPIデコーダcaseステートメントにwhen節を追加します。
-- tzpuFusionX_pkg.vhdで — 新しいコマンド定数を追加
constant CMD_SET_BORDER : std_logic_vector(7 downto 0) := x"42";

-- tzpuFusionX.vhd SPIデコーダプロセスで
when CMD_SET_BORDER =>
    border_colour <= spi_data_in(2 downto 0);
z80io.c内の対応するSOM側コードは、SPIバスを介して新しいコマンドバイトとそれに続くペイロードバイトを送信しなければなりません。SOMは常にSPIマスターであり、CPLDは常にSPIスレーブです。新しいコマンドを追加した後、コマンドバイトがパッケージファイル内の既存のコマンド定数と衝突しないことを確認してください。

Quartus SignalTap IIによるデバッグ

SignalTap IIはAlteraのインシステムロジックアナライザで、CPLDファブリックに直接埋め込まれています。これは不正なFSM状態遷移、SPIフレーミングエラー、バスタイミング問題などのCPLD RTL問題をデバッグするための主要なツールです。使用方法:
  1. ターゲットマシン用のQuartus IIプロジェクトを開き、ツールメニューからSignalTap IIロジックアナライザを起動します。
  2. 観察したいVHDL内部信号(FSM状態レジスタ、SPIバイトカウンタ、バス制御ライン)をプローブ信号として追加します。
  3. トリガー条件を設定します(例:SPIコマンドバイトが0x42に等しい場合にトリガー)、サンプル深度とクロックソースを設定します。
  4. プロジェクトを再コンパイル(解析と合成 + フィッタ + アセンブラ)してSignalTap IIロジックをビットストリームに埋め込みます。SignalTap IIはCPLDロジックリソースを消費します。設計がすでに100%の使用率に近い場合、スペースを確保するために一時的に他のロジックを削除する必要があるかもしれません。
  5. JTAG経由で新しいビットストリームをCPLDにプログラムし、SignalTap II内からインシステムキャプチャを開始します。FusionXソフトウェアを実行して条件をトリガーし、キャプチャされた波形を検査します。

ユーザーユーティリティのクロスコンパイル

ユーザー空間ユーティリティ(z80ctrlk64fcpusharpbiter)は単純なCプログラムであり、簡単なテストのために単一のgcc呼び出しでクロスコンパイルできます。プロダクションビルドでは、フルルートファイルシステムイメージが生成されるときに自動的に再ビルドされ含まれるように、Buildrootパッケージとしてユーティリティを追加することが推奨されるアプローチです。
# z80ctrlをクイックテスト用にクロスコンパイル
arm-linux-gnueabihf-gcc -o z80ctrl z80ctrl.c -lpthread


# SSH経由で実行中のFusionXボードにコピー
scp z80ctrl root@192.168.1.100:/apps/FusionX/bin/


# あるいは、フルBuildrootパイプライン経由ですべてのユーティリティをビルド(リリースには推奨)
cd software/linux/
./Build_FusionX.sh -f nand -p ssd202 -o 2D06 -m 256
手動でクロスコンパイルする場合、ターゲットアーキテクチャがSOMと正確に一致していることを確認してください:arm-linux-gnueabihf-(ハードフロート、ARMv7-A、Thumb-2)。ソフトフロートバリアント(arm-linux-gnueabi-)を使用すると実行できるバイナリが生成されますが、浮動小数点性能が著しく低くなります。ユーティリティが任意のライブラリ(例:libpthreadlibrt)にリンクされている場合、対応するARMライブラリがターゲットrootfsに存在しなければなりません。

OTAファームウェアアップデート

FusionXはSDカードベースのOTA(Over-the-Air)ファームウェアアップデートメカニズムをサポートしています。アップデートプロセスはU-bootブートローダー(または早期Linuxinitスクリプト)によって処理され、SDカード上の有効なアップグレードイメージを検出し、メインオペレーティングシステムにハンドオフする前にNANDにフラッシュします。これによりルーチンのファームウェアアップデートにJTAG接続やシリアルコンソールが不要になります。

アップデートイメージのビルド

完全なNANDフラッシュイメージを生成するためにマスタービルドスクリプトを実行します。すべての出力イメージはproject/image/output/images/に配置されます。SDカードアップデートに関連するファイルはNANDパーティションイメージとアップデートスクリプトです。
cd software/linux/
./Build_FusionX.sh -f nand -p ssd202 -o 2D06 -m 256


# 出力イメージの場所: project/image/output/images/

# 主要ファイル:

#   uboot.img          U-bootブートローダーイメージ

#   kernel.img         Linuxカーネル + デバイスツリー

#   rootfs.ubifs       ルートファイルシステム(UBIFS)

#   FusionX_apps.tar   アプリケーションファイル(カーネルモジュール、ユーティリティ、ROMイメージ)

SDカードの準備

SDカードはFAT32としてフォーマットされ、期待されるディレクトリ構造にアップグレードイメージファイルを含んでいなければなりません。U-bootアップグレードスクリプトはこのブートでアップグレードを実行するかどうかを判断するためにセンチネルファイルの存在を確認します。
# SDカードを準備(SDカードが/media/sdcardにマウントされていると仮定)
mkdir -p /media/sdcard/FusionX/upgrade/


# パーティションイメージをSDカードにコピー
cp project/image/output/images/uboot.img       /media/sdcard/FusionX/upgrade/
cp project/image/output/images/kernel.img      /media/sdcard/FusionX/upgrade/
cp project/image/output/images/rootfs.ubifs    /media/sdcard/FusionX/upgrade/
cp project/image/output/images/FusionX_apps.tar /media/sdcard/FusionX/upgrade/


# アップグレードをトリガーするセンチネルファイルを作成
touch /media/sdcard/FusionX/upgrade/DO_UPGRADE


# 電源投入前に安全に取り出してFusionXに挿入
sync
umount /media/sdcard

アップグレードプロセス

  1. 電源投入前に準備済みSDカードをFusionX SDスロットに挿入します。
  2. FusionXの電源を入れます。U-boot(またはLinuxinitスクリプト)がSDカードを検出してDO_UPGRADEセンチネルファイルを見つけます。
  3. アップグレードスクリプトは関連するNANDパーティションを消去して新しいイメージを書き込みます。これはrootfsのサイズに応じて約2〜5分かかります。このステップ中は電源を切らないでください。
  4. アップグレードスクリプトはDO_UPGRADEセンチネルファイルを削除(または名前変更)してシステムを再起動します。
  5. 再起動後のLEDパターンがアップグレード成功を示した後にSDカードを取り出します。ボードは新しくプログラムされたNANDからブートするようになります。

よくある落とし穴

  • z80drvでCPU0でブロック: CPU1のz80drvディスパッチループで実行されているコードからの任意のLinuxシステムコール(ファイルI/O、GFP_KERNELでのkmalloc、スリープ)はスケジューラのプリエンプションまたはデッドロックのリスクがあります。ホットパスではカーネルセーフでノンブロッキングのAPIのみを使用してください。ブロックする可能性がある操作はCPU0の別のカーネルスレッドに延期し、ロックフリーリングバッファまたはアトミックフラグを介して通信しなければなりません。
  • CPLDビットストリームバリアントの不一致: MZ-80AピンアサインでCPLDをコンパイルしてMZ-700に接続されたハードウェアにプログラムすると、不正なバスタイミングとアドレスデコードが発生します。マシンは起動しているように見えますが、Z80バスから誤ったデータを読み取ります。プログラムする前に必ずQuartus IIプロジェクトの設定(デバイス、ピンアサイン、パッケージファイル内のマシン固有のジェネリクス)が物理的なハードウェアと一致していることを確認してください。
  • CPU分離が有効でない: カーネルブート引数にisolcpus=1が存在しない場合、LinuxスケジューラはZ80バスサイクル中にz80drvディスパッチスレッドをプリエンプトし、ホスト機が停止またはクラッシュを引き起こします。cat /proc/cmdlineで分離が有効であることを確認し、CPU1がリストされているかどうかを確認してください。
  • SPIクロックスキュー: SOM SPIバスは50MHzで動作しており、SPIライン下の適切なグランドプレーンとともに一致したトレース長を持つ慎重なPCBレイアウトが必要です。
  • ROMイメージの欠落: z80ctrl --loadromはSDカードにROMファイルが見つからなくても成功を報告する場合があります。Z80を起動する前に必ずdmesg | grep z80drvでカーネルモジュールが成功したROMロードを報告していることを確認してください。
  • vhw_tick周波数: vhw_tick()は約2048マイクロ秒ごとではなく、約2048 Z80サイクルごとに呼び出されます。4MHzではこれは約0.5msのティックに相当します。正確なタイマーエミュレーションのために、ティック呼び出し回数ではなくcycles引数を介して渡された実際の経過サイクルデルタをカウントしてください。
  • GPIO読み取りレイテンシ: Z80アドレスバスとサイクルタイプをキャプチャするために使用されるGPIO読み取りパスはSigmaStar SOMで読み取りごとに約200nsのオーバーヘッドがあります。これは4MHz Z80動作には十分高速ですが、より高いクロック速度で動作するマシンにはギリギリかもしれません。

参考サイト

リソース リンク
tranZPUter FusionX プロジェクトページ /tranzputer-fusionx/
tranZPUter FusionX ユーザーマニュアル /tranzputer-fusionx-usermanual/
tranZPUter FusionX テクニカルガイド /tranzputer-fusionx-technicalguide/
SigmaStar SSD202 製品ページ sigmastar.com.tw
Altera MAX7000AE デバイスファミリデータシート intel.com
Zeta Z80 エミュレータライブラリ github.com/redcode/Zeta
Quartus II 13.0.1 SP1 Web Edition intel.com レガシーソフトウェア
Linux PREEMPT_RT パッチ wiki.linuxfoundation.org/realtime
Buildroot プロジェクト buildroot.org
FusionX デベロッパーズガイド /tranzputer-fusionx-developersguide/
arm-linux-gnueabihf ツールチェーン packages.ubuntu.com/gcc-arm-linux-gnueabihf

無線規制に関する注意事項

本デバイスは SigmaStar SSD202 SOM に内蔵された SSW101B 2.4 GHz IEEE 802.11 b/g/n 無線トランシーバを搭載しており、世界各国の無線周波数規制(米国の FCC Part 15 Subpart C、欧州連合の無線機器指令 2014/53/EU を含む)において意図的放射器に該当します。
SOM モジュール自体は既存の規制認証を取得していますが、そのモジュールレベルの認証は、モジュールを組み込んだ完成品に自動的に適用されるものではありません。事前認証モジュールの免除規定は、個人の趣味愛好家個人使用、実験、または教育目的で少数のデバイスを製作する場合に、個別の機器認可を取得せずに行うことを許可するものです。
重要な制限事項
  • 組み立てられたデバイスは、完成品が独自にテストされ、該当する管轄区域で機器認可(例:FCC ID、認定機関による CE マーキング評価)を取得しない限り、第三者への販売、販売の申し出、贈与、またはその他の方法での配布を行ってはなりません
  • 個人使用のために少数を製作することは、趣味愛好家および実験使用の規定(例:FCC § 15.23)に基づき、デバイスが有害な干渉を引き起こさない限り、一般的に許可されています。
  • 規制要件は国によって異なります。米国外の製作者は、適用される規則について自国の無線周波数当局に確認してください。
製作者の責任
本設計に基づいて製作されたデバイスが、管轄区域内の適用されるすべての無線周波数規制に準拠することは、製作者の単独の責任です。著者は本設計を個人使用、教育、および趣味愛好家向けに提供しており、本設計から製作されたデバイスが商業的配布の規制要件を満たすことについて、いかなる表明も行いません。