始めに
ある日、Tiny Tapeoutという、学術や個人開発者向けの半導体チップ作成のサービスを知りました。自作の簡単な5ステージ版のRISC-Vを細々と開発している身としては、自分のチップを作成できるのでは?と少し心躍らせて、作成フローに乗せると、許されている最大サイズに対しても5倍から10倍の面積が必要となり、意気消沈。かくなる上は、ということで5ステージ版の論理を再構成してステートマシンタイプのRISC-V RV32I互換CPUを作ってみました。顛末はRISC-Vデイ東京2025秋に登壇させていただき、約10分と短い時間ですがお話させていただきました。(リンク先に資料とビデオあります。)
作成したCPUはFPGAでも50MHzで動作し、FPGAを使用した検証でCのベアメタル動作確認などを行っております。RV32IとRSIC-Vの最小構成ながら、割込みやタイマーを搭載し、FreeRTOSのデモの動作を確認しております。ただ残念な点はステートマシン+レジスタ以外一切内部メモリを持たずに、すべて外部のQSPIメモリへの読み書きになるため、1命令の動作が50~70サイクルかかり、事実上700KHzくらいの戦闘能力しかないことです。
このページでは作成した論理の概要と仕様を紹介いたします。
1. リンク等
2. 特徴
- RSICV命令セットを使用しているが、ステートマシンタイプの最小構成
- 命令セット RISCV 32i 32bit最小命令セット
- Mモードのみ対応(S,Uモード非対応)
- Fence等特殊命令一部非サポート
- 外部割込み信号1本、タイマー割り込みをサポート
- ecallサポート, ebreak未検証
- exception illegal opsのみサポート
- メモリとしてQSPIをサポート PSRAMおよびFLASH(未検証)
- QSPI分周比1:2までサポート
- uartはrx側モニター機能あり⇒ runコマンド実行時にrxとして使用
- 64bit Free Run Counterを装備
- GPIO out 3bit、inout 4bit。モード信号は入力専用ピンとして使用可能
Tiny Tapeout向けに作成したが、同一構成でFPGA(Arty A7)での動作を確認しております。
3. ピン割り当て
-
システム
Clk : in : clock
Rst_n : in :リセット(負論理) -
QSPI
sck : out : QSPI向けクロック(分周)
ce_n[2:0] : out : チップイネーブル それぞれのメモリアドレスに紐づけられる
sio[3:0] inout : データ信号 コマンド[0]のみ使用 アドレス、データ4ビット使用 -
割り込み
Interrupt0 : in : 外部割込み信号 -
汎用IO
GPO[2:0] : out : 3色LED出力向け
GPIO[3:0] : inout : 汎用の入出力ピン -
モードピン 全てリセット直後に値をサンプリングしてチップの動作初期値を選択
init_latency[1:0] : in : QSPIメモリのレイテンシカウンタ初期値を選択
0: 7 cycle 1: 8 cycle 2: 9 cycle 3: 6cycle → 最終仕様では調整
init_qspicmd : in : QSPIのce_n[0]のメモリのコマンドの初期値を選択
リセット時の0x0000_0000からのメモリの種類を選択可能 0:psram 1:flash
init_cpu_start : in : CPUをリセット直後スタートさせるかどうか 0: startさせない 1:startさせる
init_uart[1:0] : in : UARTの初期接続分周比選択 → 値は最終仕様による -
SPI I/Fモード時のピン動作
GIO[0] : out : spi_sck シリアルクロック
GIO[1] : out : spi_csn チップセレクト
GIO[2] : out : spi_mosi MOSIアウトプット
init_qspicmd : in : spi_miso MISO インプット
4. メモリマップ
- 0x0000_0000 – 0x00FF_FFFF : メモリ0領域 ce_n[0]が出力される
- 0x0100_0000 – 0x01FF_FFFF : メモリ1領域 ce_n[1]が出力される
- 0x0200_0000 – 0x02FF_FFFF : メモリ3領域 ce_n[2]が出力される
- 0x0300_0000 – 0xBFFF_FFFF : 不使用領域 (0x0000_0000 – 0x02FF_FFFFがラップされる)
- 0xC000_0000 – 0xC000_FFFF : I/Oバス領域 内部I/Oレジスタ向けI/Oバスに接続
- 0xC001_0000 – 0xFFFF_FFFF : 不使用領域
5. I/Oレジスタマップ
- 0xC000_XXXX下位16ビットの表記
5.1 QSPI関連レジスタ
- 0xF400
[3:0] RW QSPI read latency 0 メモリ領域0向けレイテンシ値 - 0xF404
[3:0] RW QSPI read latency 1 メモリ領域1向けレイテンシ値 - 0xF408
[3:0] RW QSPI read latency 2 メモリ領域2向けレイテンシ値 - 0xF40C
[9:0] RW QSPI sck 分周比 デフォルト6分周(値0x3)
[16] RW QSPI debug用 CE# writeコマンドエンド時半サイクル短縮
[17] RW QSPI debug用 CE# readコマンドエンド時半サイクル短縮
[18] RW QSPI debug用 CE# writeコマンドスタート時半サイクル短縮
[19] RW QSPI debug用 CE# readコマンドスタート時半サイクル短縮
[20] RW QSPI debug用 CE# high期間追加wait加算
※ 以下のデバッグレジスタは暫定位置
[21] Debug用信号出力イネーブル PCブレイクポイント用
[22] Debug用信号出力イネーブル データリードポイント用
[23] Debug用信号出力イネーブル データライトポイント用 - 0xF410
[7:0] RW QSPI 第1リードコマンド(psram向け) リセット時 0xEB(psram) - 0xF414
[7:0] RW QSPI 第2リードコマンド(flash向け) リセット時 0x6B(flash) - 0xF418
[7:0] RW QSPI 第1ライトコマンド(psram向け) リセット時 0x38(psram) - 0xF41C
[7:0] RW QSPI 第2ライトコマンド(flash向け) リセット時 0x38(使わないためpsramと同一) - 0xF420
[5:0] RW read/wirte コマンド選択レジスタ
[0]:ce_n[0] 向けread [1]:ce_n[0]向けwrite [2]:ce_n[1] 向けread [3]:ce_n[1]向けwrite
[4]:ce_n[2] 向けread [5]:ce_n[2]向けwrite
リセット時にqspi_init[2]の値で[1:0]の初期値を変更 sqpi_init[2]:0 2’b00, qspi_init[2]:1 2’b11 - 0xF424
[2:0] RW QSPI read latch edge timing
[6:4] RW QSPI sck delay timing
5.2 Free Run Counter 関連レジスタ
- 0xF800
[31:0] RW Free Run Counter Lower 32bit read:現在値 write:即時反映 - 0xF804
[31:0] RW Free Run Counter Upper 32bit read:現在値 write:即時反映 - 0xF808
[31:0] RW Free Run Counter 比較値 Lower 32bit read:現在値 write:即時反映 - 0xF80C
[31:0] RW Free Run Counter 比較値 Upper 32bit read:現在値 write:即時反映
Free Run Counter 比較値はFree Run Counterと比較され、
現在値が比較値以上となった時点でタイマー割込み信号が発信される - 0xF810
[2:0] RW Free Run Counterコントロールレジスタ
[0] : enable bit 1:enable 0:disable
[1] : WO 1書き込みでカウンタリセット
[2] : 割込み状態ビット read: 1:タイマー割込み発生 0:タイマー割込みなし write : 0:割込み状態クリア
5.3 External interrupt 関連レジスタ
- 0xFA00
[1:0] RW External Interrupt Enable 1:enable 0:disable
[0] : RX受付インタラプト
[1] : Interrupt_0 ピンインタラプト - 0xFA04
[1:0] RW External Interrupt Status/Clear ビット配置はenableと同一
read : 1:インタラプト受信 0:インタラプト未受信
write : 1:クリアマスク 0: クリア 例:双方のビットをクリアしたい場合は2’b00を書き込む
5.4 UART関連
- 0xFC00
[7:0] RW uart tx 出力キャラクタ write : txに即時出力 read : 最後に書いた値が読める - 0xFC04
[0] RO uart tx FIFO full tx fifoの状態を確認できる 1:full - 0xFC08
[15:0] RW uart 通信周波数比 write: 周波数比の変更 read: 現在の値が読める
通信周波数比 = CPUクロック周波数 ÷ UART通信ボーレート - 0xFC0C
[9:0] RO uart rx 入力キャラクタ
[7:0]最後に入力されたキャラクタ
[8] 以前に読まれていない場合1
[9] 読まれる前に上書きされたとき1 - 0xFC10
[0] uart rx CPU実行時のrxの値をtxにエコーバックするのをdisableする
0: エコーバックあり 1:エコーバックなし
5.5 GPIO関連
- 0xFE00
[2:0] RW GPOut wirte: out value read: current out value - 0xFE04
[5:0] RO GPIn read: currnt in value
ただし [5:4] init_uart, [3] init_cpu_start, [2:1] init_latency, [0] init_qspicmd - 0xFE10
[3:0] RW GPIO output value
write: 出力値書き込み read: 現在CPUとして出力している値 - 0xFE14
[3:0] RO GPIO input value
実際のピンの値が読める(open drain使用などではoutput valueと食い違う) - 0xFE18
[3:0] RW GPIO enable value
write: 1:出力イネーブルオン 0: 出力イネーブルオフ
read: 現在のイネーブル値を読める
I2Cなどでは、outvalueは all1にしておいて、enable valueを変更することでOpen drainを実現可能
5.6 SPI関連
※キュー制御にバグあり(2026/4/12現在)
- 0xF200 RW SPI mode register
[0] : SPI enable: 0 : disable(関連ピンは普通の outputとinputピンとして使用可能)
1 : enable(関連ピンはSPIとして使用)
[2:1] : SPI mode 0-3 0:[2=CPOL, 1=CPHA]に相当
[3] : SPI bit endian : 1:big endian(default) 0: little endian
[18:16] : SPI sck phase between MOSI
[22:20] : SPI sck phase between MISO
[26:24] : SPI MISO data latch timing - 0xF204 RW SPI sck divide register
[9:0] : SPI sck divider parameter - 0xF208 RW SPI MOSI FIFO register
[7:0] : WO : MOSI data 書き込みレジスタ
[8] : RO : MOSI FIFO Full
[9] : RO : MOSI FIFO Empty - 0xF20C RW SPI MISO FIFO register
[7:0] : RO : MISO data 読み出しレジスタ
[8] : RO : MISO FIFO Full
[9] : RO : MISO FIFO Empty
[10] : WO : MISO FIFO reset
6. UARTモニタ機能
- Riscvコアの制御およびメモリやI/Oの読み書きをするためにUART rxにモニタ機能を搭載している。
- 予約キャラクタがあり、入力するとそれぞれの命令動作となる。以下に命令のフォーマットを示す。
- 全てのキャラクタ入力はエコーバックされる。されない場合通信不良が起きている。
- gコマンド実行中はrx/txともにrsicvコアのuart I/Oの入出力として動作する。
gコマンド実行時以外はuart I/Oの入出力とはならない。
6.1 コマンドフォーマット
- g : goto PC address ( Ctrl-cを押すまで実行 ) : format: g <address>
- Ctrl-c : 全てのコマンドの中断をする。困ったらCtrl-cを押す : format: Ctrl-c
- w : メモリへのデータライトをする: format: w <address> <data>... Ctrl-c
- r : メモリからのデータリードをする : format: r <start_address> <end_address>
- s : break point アドレスの設定: format s <break point address>
設定時 : breakしたいアドレス値を指定
解除時 : アドレスに奇数を指定(bit[0]に1を立てる)
I/Oレジスタの0xF40C[21] Debug用信号出力イネーブル PCブレイクポイント用をセットすると
ブレイク時にled[2]ピンがハイになる
※省回路化のため、回路の共用により、break point 関連の命令(s,l,m)は、最後に設定したもののみ有効 - j : 現在のPCの値を表示: format j
- i : I/Oレジスタ等のデータライトをする。アドレス上位2ビットにより対象が変わる
: format: I <address> <data> .... Ctrl-c
上位2bit == 2’b11 ; I/Oレジスタ
上位2bit == 2’b10 : CSRレジスタ
上位2bit == 2’b00 : Register File
※ただしCSRとRFの場合、レジスタ番号の4倍のアドレスを指定する
例:
CSR mstatus (0x300)のアドレス 80000c00
RF x3レジスタ 0000 000c - p : I/Oレジスタ等のデータリードをする。アドレス上位16ビットは無視される
: format: p <start_address> <end_address>
上位2bit == 2’b11 ; I/Oレジスタ
上位2bit == 2’b10 : CSRレジスタ
上位2bit == 2’b00 : Register File
※ただしCSRとRFの場合、レジスタ番号の4倍のアドレスを指定する
例
CSR mstatus (0x300)のアドレス 80000c00
RF x3レジスタ 0000 000c - t : メモリの0埋め
: format t <start_address> <end_address>
7. 論理概要
7.1 CPUステートマシン動作
チップ面積の制約上、ステートマシン処理を採用
以下の処理を順次行っている
-
①プログラムカウンタ(PC:命令列中の現在位置)を更新 (PC)
通常は次の命令に行く(+4byteする) jump命令ではとび先アドレスに変更する -
②命令読み込み (IMR)
QSPIメモリからPCの指すアドレスの命令を読み出す -
③命令デコード (DEC)
読み出した命令がどの命令(オペレータ)でどういうパラメータ(オペランド)で動作するかを抽出
デコーダでそれぞれの情報に分解、抽出 -
④レジスタファイル読み出し (RF)
③で得たオペランドを基に、使用するレジスタのデータを読み出す -
⑤命令実行 (EXE)
③で得たオペレータの処理を④で得たデータに対して行う -
⑥メモリ操作 (DMA)
③の命令デコード結果がメモリ操作命令(ロード、ストア)の場合、
⑤でできたアドレスにデータをストア、またはアドレスからロードしてレジスタに書き戻す
メモリ操作命令以外は、⑤の結果をレジスタに書き戻す
⇒ ①(PC)に戻る
7.2 論理ブロック図
- 以下にトップの概要を示します。黄色は5 stage版から大きく流用
- バス構成の思想としては、
- コントローラ側になれるのがriscvコアとUARTモニタのみである
- 双方は絶対同時に動作しない前提のため、バス信号は簡易実装している
⇒メモリバスはbus_logic.v、I/Oバスはfpga_top.vでリクエストをORしているだけである
7.3 詳細論理
※長文になるため、省略
gitのRTLを読んでください。
# 最後に
作成したステートマシンタイプのRISC-V RV32I互換CPUの仕様と概略を記載してみました。動作させるためには、FPGAにQSPI PSRAMを接続する必要があり、少々電子工作が必要となります。動作も遅いですが、一応FreeRTOSも動くくらいには検証されているので、動作することくらいは確認できると思います。あまり参考にならないかもしれませんが、Tiny Tapeoutというニッチな目的のために作るとこんなものになるのかとみてやってください。


