概要

TinyTapeoutで作る音源IC」に引き続き、個人でも参加できるASIC開発プロジェクト “TinyTapeout” の第6回の試作に参加して開発した自作8bit CPUです。 “TinyTapeout” そのものの紹介については「TinyTapeoutでオレオレICを作ろう」に書いていますので、そちらも併せてご覧ください。

きっかけ

TinyTapeoutで作る音源IC」に書いた通り、TinyTapeoutの第5回の試作で音源ICを作りました。音源ICは最後の最後であれこれ欲張った関係でTinyTapeoutの試作タイル(回路を納めるシリコン上の区画、1個当たり50ドル)2個分の設計になってしまいましたが、この結果から試作タイル1個でも結構いろいろ作れるのではないかという感触を得ました。

そんなわけで、第6回の試作には音源ICよりもむしろFPGAなどでの「自作」の定番であるところのCPUを自作して投稿してみようと考えたのでした。前回設計した音源ICでは波形メモリを内蔵したことが回路面積が増えた主な要因でした。しかし、CPUでは必ずしも波形メモリのような比較的規模の大きいメモリを内部に持つ必要はありません。そのような特徴から、試作タイル1個に収めるという制約条件でもそれなりに遊べるものを作ることができるのではないかと考えたのでした。

ちなみに自作CPUは高校の課題研究で実装したロボット制御用のCPU、2020年ごろに実装したPIC12F508互換のCPUに続いて3回目です。たまにやりたくなる何かがあるのかもしれません。今回はこれまでと違ってFPGA上での実装だけでなくASIC化したところと、最初からTinyTapeoutの試作タイル1個に収めるべく、リソースの削減を意識して設計した点が個人的に新しい点といえます。

設計と実装

今回はとにかくTinyTapeoutの試作タイル1個に収めることが目標でしたが、同時に余暇の時間で締め切りに間に合うように設計しなければならないという制約もありました。そんなわけで、回路規模と実装・評価の手間のそれぞれの観点で複雑になりすぎないような設計とすることを意識しました。

ちなみに、今回もTinyTapeoutに実際に投稿する前にFPGA上で動くことは確認済です。Lattice社のiCE40 Ultra FPGAで動かしたところ、大体fmaxは50MHz程度だったと記憶しています。

命令セットアーキテクチャ

一般に使われているCPUと同じ命令セットアーキテクチャを使うとその命令セットアーキテクチャ向けのツールチェインが使えるというメリットがあります。しかし、Cコンパイラレベルのものを流用できるように作ろうとすると命令をきちんとフルセットで実装して評価する必要が出てきますし、試作タイル1個に収めるという制約下で流用できそうな命令セットとなるとどのみちアセンブリ言語で書くことをある程度前提としたものになりそうだったので今回は命令セットアーキテクチャも自作としました。

今回設計した命令セットアーキテクチャの概要は以下の通りです。詳細はGitHubのリポジトリ内のInstruction Set Summaryを参照してください。

  • 命令/データともに16bit幅のアドレス空間を持つ
  • 8bit幅の汎用レジスタが8本
  • 16bit固定長の命令セット
  • RISC風のレジスタ-レジスタ間演算を基本とした演算命令
  • すべての命令は条件付き実行が可能

今回の命令セットで特徴的なのは最後の「すべての命令は条件付き実行が可能」というところです。条件付き命令はARMの命令セットのものが有名ですが、ARM命令と違って16bitの命令フィールドにすべての機能を押し込むために少しトリッキーな実装をしています。具体的には、それぞれの命令そのものには「条件付き実行か無条件実行か」を示すフラグだけを用意し、そのフラグが立っている時だけ条件付き実行をするようにしています。条件付き実行の判定条件は命令そのものには埋め込まれず、別に用意した命令が実行された際に、オペランドに指定されたビットマスクとステータスレジスタとのANDを取り、1が立っていたら条件成立、そうでなければ条件不成立となります。

分岐命令もこの仕組みを使って条件分岐を実装する前提になっているので、アセンブリ言語で書くとちょっと冗長になりがちです。そこで対策として後述の通りPC側で動かすアセンブラでマクロ命令を定義してこの辺のあれこれを1つのマクロ命令として記述できるようにしています。同様に、算術・論理演算の命令も実際の命令としては最低限のものだけ用意して、アセンブラ側でマクロ命令を定義してより複雑な命令を実現することを前提としています。例えば減算はデクリメントのみの実装となっています。この辺りの割り切り方はPICの命令セットを参考にしています。

また、命令を割り当てるビットフィールドも場合分けが複雑にならないように、様々な種別の命令でできるだけ同じ場所にビットフィールドの区切りが来るようにオペランドを配置しています。この辺りの最適化はRISC-Vがまじめにやっているところだと思います。今回はExcelにポチポチ書きながらやっただけなので最適ではないかもしれませんが、それなりに気を付けたポイントの一つです。

あとは地味なところですが、16bitの命令フィールドがすべて0またはすべて1のどちらでもNOPとして扱われる仕様にしてあります。命令メモリがFlash(ブランクの場合0xFFが読み出される)なので、こうしておけば変なアドレスから命令を読んでしまっても極端におかしな状態にはならないだろうということで、こういう仕様にしています。

マイクロアーキテクチャ

マイクロアーキテクチャと言うほど大それたものは正直ないのですが、なんといっても今回はリソースの制約が厳しいのでとにかく省リソースを意識した設計としています。パイプラインレジスタにリソースを持っていかれても面白くないですし、そもそも後述の通り主記憶が外付けのSPI Flashである時点でパイプライン化したところでストールしまくって大して速くならないので、各ステージの処理を順繰りに行うマルチサイクル設計としました。ALUもレジスタ幅と同じ8bit幅のALUを使っていて、1bitずつ順繰りに演算するといったことはしていないので、普通のマルチサイクル設計というところだと思います。

外部バス

TinyTapeoutの試作チップで使えるデジタルIOピン1は以下の24本のみとなります。

  • 入力専用ピン8本
  • 出力専用ピン8本
  • 入出力兼用ピン8本

というわけで、昔の40ピンDIPな8bit CPUのようにCPUのバスをパラレルバスとして外に引き出すのは難しいといえます。一方でチップの内部に命令やデータを格納するメモリを十分確保するのも難しいので、今回は外部にSPI FlashとSPI PSRAM(疑似SRAM)を接続できるようにしています。SPI Flashは命令用、SPI PSRAMはデータ用です。SPI Flashから都度都度命令を読み出す関係上、1命令の実行にかなりのクロック数がかかりますが、まあ実用を意識した実装というわけでもないので、これでよしとしました。

これらのSPIメモリを制御するためのメモリコントローラーは以前に「ESP32+FPGAで作るPCMシンセ」を作った際に書いたメモリコントローラーを流用しています。PCMシンセではデータの読み出し帯域を稼ぐためにQuad SPIモードに対応させましたが、今回はリソース優先ということでSingle SPIモードのみ対応とし、SPI FlashとSPI PSRAMで外部バスとメモリコントローラーを共有するような実装としています。

ペリフェラル

外部バスをSPIにしたことによって残ったIOピンがあったので、デバッグ用に以下のペリフェラルを用意しました。

  • GPIO8本(ただしうち4本は出力専用)
  • SPI送信回路(SPIメモリ用とは別)
  • 命令フェッチパルス出力

GPIO8本は本当は全ピン入出力兼用としたかったのですが、そのようにするとぎりぎりタイル1個に入りきらず、泣く泣く4本は出力専用としました(そのくらいぎりぎりを攻めています!)。SPI送信回路はTinyTapeoutの第5回の試作で作った音源ICに制御用のI/FとしてSPIスレーブを実装していたため、つけておけばこのCPUから音源ICを制御できるだろうという狙いで実装しました。命令フェッチパルスはおまけで付けたのですが、動作確認をする上では結構役に立ちました。

マクロアセンブラ

命令セットアーキテクチャのところで書いたように、今回のCPUは回路側をシンプルにする代わりに面倒事はアセンブラでなんとかするというアプローチの設計になっています。そんなわけで、ある程度まじめにマクロアセンブラを実装しました。ざっくりとした仕様を以下に示します。

  • 変数(データメモリに対するラベル)、ジャンプ先ラベル、マクロ命令の定義が可能
  • アセンブル結果の各種フォーマットでの出力
    • SPI Flashへの書き込み用の.binファイル
    • Verilog HDL用の$readmembタスク向けの.txtファイル
  • 命令セットおよびマクロ命令はYAMLファイルとして定義可能
  • マクロ命令は再帰的に定義可能(マクロ命令の中で他のマクロ命令を呼び出せる)

生のアセンブリ言語では結構書きにくい(というか冗長になる)命令セットなので、とにかく今回はマクロ命令を簡単に定義できるようにしました。また、命令セットもYAMLで定義することで、CPUの実装中に命令の種類を増やしたり減らしたりしたときにもすぐに対応できるようになっています。次に自作CPUをやるのがいつかは分かりませんが、結構柔軟に使えるようにしてあるのでまた自作CPUをやりたくなった時にも流用できるかなと思っています。

評価

試作チップの載った評価ボードは2024年の年末に届きました。第5回の時はおまけ(?)はシールだけだったと思うのですが、今回は今回の試作チップの中身のパターンをカラーシルクで描いたカード状の基板もおまけで付いてきました。評価ボード上段のキャリアボードの真ん中に載っているのが実際に試作したチップです。この第6回からアナログI/Oが追加されたので、それらを外部に引き出すためのコネクタもキャリアボードに取り付けられています。

TinyTapeoutの評価ボードの下段、ベースボード側にはPMODコネクタと呼ばれるコネクタが実装されています。試作チップの信号はベースボードに実装されているRP2040に接続されるとともに、PMODコネクタにも引き出されています。今回は命令とデータ用のメモリとしてSPI Flash、SPI PSRAMを試作チップに接続しなければならないので、このPMODコネクタに刺さるような簡単なデバッグ用基板を組み立てました。2024年の9月に仕事の都合でカナダ・バンクーバーに引っ越してきたため、秋葉原にちょっと行って部品を買ってくることができず、日本からあの部品を持ってきておけばよかった…などと後悔しつつもなんとか部品を集めて基板を作ることになったのでした。この基板にはメモリの他に、実際にプログラムがきちんと動いていることを確認するためにLEDとスイッチによる簡単な入出力コンソールも実装しました。

デバッグ用基板と評価ボードを合体させるとこんな感じになります。右側はSPI Flashを書き込むために新たにAmazonで買ったSPI Flashライターです。USB-MIDIホストを作るときに使ったCH554マイコンのメーカーであるWCHのICが載ったライターでした。今回初めて使ったのですが、よく分からないところから書き込み用のソフトをダウンロードしてこないといけなかったりして、なかなかワイルドなライターでした。

デバッグ用基板を作ったところで適当なテストプログラムを書き、ベースボード側に取り付けられている試作チップのテスト用のRP2040にも音源ICのデモの際に作ったファームウェアを改造したものを書き込んで動作確認をしました。しかしうんともすんとも言いません。RP2040側に実装したデバッグ機能のバグを取ってもまだうまく動きません。何よりおまけで付けたはずの命令フェッチパルスすら出てこないのでした。これはおかしいということでいろいろ調べてみると、実はベースボードの設計が第5回のものから変わっているのを発見しました。GitHub上には第5回の試作用のベースボードの回路図のPDFファイルしかなかったのですが、KiCADのファイルの方を見てみると結線が大幅に変わっていました。これに伴い、試作チップの起動に必要な信号群のRP2040のGPIOへの割り当ても変更になっていたのでした。ファームウェア上での信号の割り当てを修正すると、とりあえず命令フェッチパルスは出るようになりました。

しかし思った通りにLEDが点滅せず、変なパターンでの点滅をしていて、まだきちんとCPUが動いていないようでした。

CPUが外部のSPI Flashから正しく命令を読めていないのでこういうことが起こっているのか、命令は正しく読めているのにCPU内部の処理結果がおかしいのでこうなっているのかを切り分けるため、SPI Flash用のバスにロジックアナライザーを取り付けて、SPI Flashに対して送られている信号をチェックしました。するとリセット解除直後の最初の命令読み出しは正しく0x0000番地に対して行えているのですが、次の命令読み出しでは0x8508番地と、想定と異なるアドレスへの読み出しを行っていました。最初の命令をNOPにしてもこれが起きるので、プログラムカウンタの更新が間に合っていないのではないかと考えました。そこで試作チップに与えるクロック周波数を41.67MHzから31.25MHzに落としたところ、予想が正しかったようで無事に正しく動作するようになりました。今回はリソース利用率が80.08%と高かったので、最高動作周波数があまり高くならなかったのかもしれません。

その後、各種命令群やSPI PSRAMへのリード・ライトなども一通り行い、きちんと動いていることを確認できました。デバッグ用のSPI送信回路を自作の音源ICに接続して制御する実験はまだやっていないので、どこかでトライしたいと思っています。

デモ動画

IOを簡単に制御しているだけなのでシンプルですが、ボタンを押すとLEDが順次点滅するデモ動画を置いておきます。

まとめ

個人でも参加できるASIC開発プロジェクトの”TinyTapeout”第6回の試作で、命令セットから自作した8bit CPUをASICとして実装しました。また、デバッグ用の基板やマクロアセンブラも作り、簡単なプログラムを試作チップ上で動作させました。

正直なところ1命令の実行に約100クロックかかるので結構遅い(とはいえ1秒間に31万回以上の命令を実行できます)のですが、メモリ空間はそれなりにあるので、きちんとプログラムを書けばいろいろ遊べるのではないかと思います。

また、動作速度の最大のボトルネックとなっているのはSPI Flashからの命令読み出しなので、ここを改善してやれば1命令当たりの所要サイクル数(CPI)がかなり改善されるはずです。例えば、メモリコントローラーの実装が少し複雑になりますが、SPI Flashの連続読み出し機能を使って読み出しコマンドの発行をできるだけ省略してやるとCPIが向上するはずです。このような実装にすると、SPI Flashへのコマンド発行が発生するのは

  • SPI FlashのCSをアサートした直後の読み出し時
  • SPI Flashのページ境界をまたぐ時
  • 分岐が発生して直前と全く異なるアドレスへのアクセスが必要になった時

の3パターンになります。このうち最後の分岐発生時のコマンド発行は、今回の命令セットアーキテクチャの特徴である条件付き実行を活用し、分岐そのものを減らすことで防ぐことができます。この改善を入れてまたTinyTapeoutで試作してもいいのですが、せっかくタイル1個+評価ボードで150ドルを出すなら、また全く違う何かを設計したいかな…とも思うのでした。

関連リポジトリ

本文中でもいくつか言及していますが、今回の自作CPUに関係するリポジトリをまとめて紹介しておきます。

  1. 第6回の試作からはアナログI/Oピンが用意されているので、追加でお金を払えばアナログI/Oピンは以下とは別に確保できます。
公開日:2025/02/23 最終更新日:2025/02/24