やまどり
RP2354でROMエミュレータを自作

RP2354でROMエミュレータを自作

秋月電子で売られていたAKI80を動かすにはROMが必要になるのですが、令和の時代にROMライターの入手が非常に難しいため、RP2354でROMのエミュレータを自作しました。オープンソースで公開しています。

はじめに

秋月電子で売られていたAKI80を開発をしようとすると、M27C256のようなEPROMが必要になります。しかし現実には、UV照射消去型のEPROMは入手性が悪く、書き込み器も必要で、デバッグのたびに消去→書き込みを繰り返すのは手間がかかります。

そこで「ROMソケットに差し込むだけで、USBからデータを流し込めるデバイス」を自作しました。MCUはRP2354Aを使い、自作PCBを起こしてM27C256互換のROMエミュレータとして動作させることができます。本記事ではハードウェア設計からファームウェア、ホストツールまで一通りまとめています。

オープンソースで公開しているので、興味があればぜひ。

https://github.com/Yarnadori/RP-ROM

内容

M27C256 EPROMの仕様

秋月電子で売られている、AKI80のモニターROMがM27C256なわけですが、仕様を整理しておきます。

  • 容量:32KB
  • アドレス線:A0〜A14(15本)
  • データ線:D0〜D7(8本)
  • 制御線:CE#(チップイネーブル)、OE#(アウトプットイネーブル)
  • アクセスタイム:100ns

動作としては「CE# と OE# が両方 Low になったとき、アドレスに対応するデータをデータ線に出力する」だけです。シンプルに見えますが、アクセスタイムがなかなかシビアで、そこが今回の設計の肝になります。

ハードウェア設計

KiCad で自作 PCB を設計しました。

M27C256は600mil幅の28pinDIPパッケージです。エミュレータもこれと同じフォームファクタに収めたかったので、外付けフラッシュ不要のフラッシュ内蔵版RP2354Aを選びました。外付けフラッシュを省くことで基板を小型化できます。

RP2354Aの使い方についてはこちらに適当ですがまとめています。

https://www.yamadori.app/blog/rp2354-gsg/

電源周りは公式推奨配置に従い、その他はパッケージのサイズに合わせてレイアウトしました。

部品に関してはLCSCで調達を行い、基板製造はJLCPCBにお願いしました。

はんだ付けはリフローでおこない、こんな感じのができました。

ピン配置について

本来はファームウェアを作る際に便利なよう、GPIOのピン番号とアドレス線を連番にしたかったのですが、スペースの都合でバラバラになってしまいました。これが後述のLUT設計につながります。

ファームウェア設計

ファームウェアはPicoSDKを使用しました。

RP2354はデュアルコアなので、役割を分けました。

  • Core 1:ROM エミュレーション専用。アドレスを読んでデータを出力するだけのタイトループ
  • Core 0:USB シリアルのコマンド処理(書き込み・読み出しなど)

Core 1 はフラッシュに一切アクセスさせていません。フラッシュアクセスにはレイテンシのばらつきがあり、タイミングがズレるためです。__not_in_flash_func() マクロでホットパスのコードとデータをすべて SRAM に置いています。

クロックは標準ではアクセスタイムが要件を満たせないため、360MHzにオーバークロックして動かしています。コア電圧を 1.30V に上げて安定動作させています。

LUT でアドレス変換を高速化

先ほどの「GPIO 番号がバラバラ問題」の解決策として、LUT(ルックアップテーブル)を使いました。起動時に GPIO ビットパターン→アドレスの変換テーブルを事前計算しておき、ホットループではテーブルを引くだけにしています。

さらに「アドレス→データピンの GPIO 出力マスク」も丸ごと 1 本のテーブルにまとめました(addr_out_lut)。32KB × 4B = 128KB を SRAM に置く計算ですが、RP2350 は SRAM が 512KB あるので余裕です。

おかげでホットループはこんなにシンプルになっています。

while ((g = sio_hw->gpio_in) & OE_MASK)
    tight_loop_contents();

uint32_t out = addr_out_lut[gpio_to_addr(g)];
sio_hw->gpio_set = out;
sio_hw->gpio_clr = DATA_PIN_MASK ^ out;
sio_hw->gpio_oe_set = DATA_PIN_MASK;

OE# アサートのタイミングで GPIO を読み取り、テーブルを 1 回引いてデータを出力する、という流れです。

電源を切ってもデータを保持

内蔵フラッシュの末尾 36KB を使って ROM データを永続化しています。マジックナンバー('RPOM')で有効性を確認し、電源投入時に自動でロードします。

フラッシュへの書き込み中は XIP が止まるため、その間 Core 1 を安全に停止させる仕組みも入れています。rom_updating フラグで Core 1 を一時停止・再開する、という設計です。

USB コマンドプロトコル

USB CDC Serial で 1 バイトのコマンドを受け付けます。

コマンド

動作

W

続く 32768 バイトを ROM データとして書き込み

R

ROM データ 32768 バイトを返す

E

全バイト 0xFF で消去

I

デバイス情報を表示

シンプルですが、実用上これで十分でした。

書き込みツール

本体にシリアルで直接コマンドを送れますが、32KBのデータをコンソール操作で流すのは面倒です。そこでPython製のCLIツールを用意しました。

bash
uv run rom_tool.py write COM3 firmware.hex   # 書き込み
uv run rom_tool.py read  COM3 dump.bin       # 読み出し
uv run rom_tool.py verify COM3 firmware.hex  # 書き込み後の確認
uv run rom_tool.py erase COM3               # 消去
uv run rom_tool.py info  COM3               # デバイス情報

.binとIntel HEXの両形式に対応しています。拡張子がない場合は先頭バイトを見て自動判別します。32KB に満たないファイルは 0xFF でパディングします。

動作確認

AKI80に差し込んでLチカを動作させることができました。

電源投入直後はROMの準備が間に合わないため、一度リセットをかける必要があります。ここは今後改善したいポイントです。

おわりに

RP2354Aのデュアルコアを活用してコアの役割を分離し、SRAMへのコード配置と360MHz動作の組み合わせでM27C256のアクセスタイム要件をクリアできました。LUTで事前計算することでホットループを極限までシンプルに保てたのが設計の要点です。

ROMエミュレータがあると、EPROMの消去・書き込みなしでUSBからデータを即座に差し替えられるので、古いハードウェアの開発が格段に楽になります。KiCadのプロジェクト・ファームウェア・ツール一式はリポジトリに公開しているので、同じようなことをやろうとしている方はぜひ参考にしてみてください。