Pocket の他の互換コアで遊んでみたい? うん、それもあるけど…私はもっと openFPGA を知ろうと思う。 Lチカには正直、興味はないよ。だから見て確かめるんだ。 そうだね、これは自分のためだ。
2023年12月現在、この openFPGA を活用できるのは Pocket のみ。 予約祭りに乗じて運よく購入できた人は、 せっかく高価なものなのでその手の中にある Pocket でFPGA遊びにも興じてみるのもいいのかなって。
Chronological Pocket.
dist/ │ ├─Assets/ │ └─ex_platform/ │ └─common/ │ ├─Cores/ │ └─Developer.Core Template/ │ ├─audio.json │ ├─bitstream.rbf_r │ ├─core.json │ ├─data.json │ ├─info.txt │ ├─input.json │ ├─interact.json │ ├─variants.json │ └─video.json │ └─Platforms/ ├─_images/ │ └─ex_platform.bin │ └─ex_platform.json
Assets/ex_platform/common
の中には通常、
FPGAコアから参照するデータ等を置く(Lチカでは使用しない)。
Cores/Developer.Core Template
にはFPGAデータとそれに付随する情報を格納する。
*.json 類は core-template に含まれているものをとりあえずそのままコピー。
FPGAデータはcore-template/output/bitstream.rbf_r
をコピーしてくる。
Platforms
もcore-template/dist/platforms
からそのままコピー。
Pocket でmicroSDカードにアクセスする設定は、
AnalogueOS メニューから[Tools]→[Developer]→[USB SD Access]にチェックを入れる。
Pocket にmicroSDカードをセットし電源を入れ、
PCとUSB接続するとエクスプローラからアクセスできるようになる。
エクスプローラからアクセスしたmicroSDカードの中身は、
このような感じにフォルダが作成されている。
上で作成した Assets、Cores、Platforms をmicroSDカードのルートに上書きコピーし、
右のBボタンやら×ボタンやらの位置のボタンでSDアクセスモードからexitする。
exitしたらUSBケーブルを徐に抜く。
core-template/src/fpga/core/core_top.v
の597行目に、
以下の598~602行目を追記し保存する。
HDLアドカレの貴重なHDL要素だからね。
if(y_count >= VID_V_BPORCH && y_count < VID_V_ACTIVE+VID_V_BPORCH) begin
// data enable. this is the active region of the line
vidout_de <= 1;
vidout_rgb[23:16] <= 8'd60;
vidout_rgb[15:8] <= 8'd60;
vidout_rgb[7:0] <= 8'd60;
// ここから追記
if (x_count < 60 || y_count > 200) begin
vidout_rgb[23:16] <= 8'd255; // Red
vidout_rgb[15:8] <= 8'd00; // Green
vidout_rgb[7:0] <= 8'd00; // Blue
end
// ここまで追記
end
Quartus Prime 18.1 Lite Edition をダウンロードしインストール。 Lite Edition はライセンスファイルの取得や設定が不要で使用できる。
Quartus のインストールが完了したら早速 Quartus を起動し、
サンプルのプロジェクトファイルcore-template/src/fpga/ap_core.qpf
を開く。
そして、メニューの[Processing]→[Start Compilation]を実行。
論理合成・配置配線・FPGAデータ生成が完了したら、core-template/src/fpga/output_files/ap_core.rbf
が更新されているはずだけど、
実はそのままでは openFPGA で使用できず、ビットリバース処理が必要。
なんだかレガシーな理由で .rbf のビットが逆順になっているらしい。
ビットリバースは自分でプログラム作ってやってもいいんだけど、
openFPGA で作成された最初のコアであるspacemen3/PDP-1 の Source code(PDP-1-1.1.0.zip)内にあるものを流用できる。
PDP-1-1.1.0/src/output_files
内の reverse_bits.exe と run.bat を、
ap_core.rbf と同じ場所の
core-template/src/fpga/output_files
にコピー。
以下のように、run.bat の1行目以外は適宜 rem でコメントアウトするなりしてくれればいい。
reverse_bits.exe ap_core.rbf bitstream.rbf_r
rem copy /y bitstream.rbf_r "H:\Cores\Spacemen3.PDP1\bitstream.rbf_r"
rem copy /y bitstream.rbf_r "..\..\dist\Cores\Spacemen3.PDP1\bitstream.rbf_r"
run.bat を実行すると bitstream.rbf_r が生成される。
Pocket を起動しUSBケーブルでPCと接続後、
bitstream.rbf_r をCores/Developer.Core Template/bitstream.rbf_r
に上書きしSDアクセスモードからexitする。
サンプルを実行した時と同様に AnalogueOS メニューから[openFPGA]→[Example Platform]→[Run]から実行し、
赤のLが表示されれば成功。
目がチカチカするね。
もし赤のLが出ない場合は、microSDカードのフォルダSystem
内にあるキャッシュファイル(*.bin)を全て削除してリトライするといいかも。
短い旅だったけど、これで君もアナログポケットマスターだね。
Intermission.
通常の Pocket でもダウンロードケーブルが使用できるようなカスタム裏蓋が発売されることを期待してる。
ちょっとした問題が…。 たまたまTBSラジオ(FM90.5MHz)を付けていたら、FPGAデータダウンロード時にひどいノイズを受信してしまう。 文化放送(FM91.6MHz)には影響ないみたい。 ノイズ源どこ…。
Analogue Pocket(あっぽけ)でポリゴンデモ動いた
— かんな丸!! (@pgate1) October 11, 2022
VGA出力回路がそのまま使えるんだね pic.twitter.com/LnOuwTZejw
デバッグには観測性が必要なので、 回路内ロジックアナライザや仮想JTAG経由のデータ送受信などでFPGA内データを観測する。
ダウンロードケーブルを使っての開発のコツとしては、 FPGAコアに関連する環境である各種パラメータを記述した *.json と読み込ませるデータ等をあらかじめmicroSDカード内に配置しておき、 openFPGA から仮のFPGAコアを起動した状態で Quartus Programmer からFPGAデータを書き込むこと。 こうすることで、書き込んだFPGAデータは仮のFPGAコアが起動した環境で動作する形になる。 言い換えると、*.json の記述とFPGAデータのパラメータ等が異なるとうまく動作しない。 例えば、解像度が異なる設定の video.json を読み込んでいる状態でFPGAデータを書き込んでも画面が期待通りに表示されないなど。
実際の基板はこんな感じ。
参考: 今話題の「openFPGA」とは何か? 中華エミュ機とはまったく異なるその実力と魅力
ちなみに、Pocket のFPGAとほぼ同等のものが Terasic社のFPGAボードDE0-CV (Cyclone V E、5CEBA4F23C7)に搭載されているので、 ユーザコアをDE0-CVで開発し Pocket に移植、という手が使える。 ただし Pocket のFPGAの方がスピードグレードがワンランク下なので、 動作周波数をメットさせるのに苦労するかもしれない(した)。 …残念ながらDE0-CVは2023年11月をもって生産終了したんだけどね。
ちなみにちなみに、Pocket 搭載FPGAと同じ型番のFPGAが、 同社の『SuperNt』や『MegaSg』や『Duo』にも使用されていたことと、 AnalogueOS機能実行には ARMプロセッサ内蔵の Cyclone V SoC ではなく別にPICを置いたのは、 このFPGAをボリュームディスカウントで購入したのかな?と勝手に妄想したり。 でもさすがに携帯機で Cyclone V SoC 使うのは消費電力や熱対策なんかが大変なことになるか。
FPGA周辺回路として使用可能なものを紹介する。
回路図が公開されていないのが残念だけど、サンプルのピンアサインファイルを流用すれば使えるのでまぁいいや。
core_top.v
のポート宣言をベースに解説する。
// infrared
input wire port_ir_rx,
output wire port_ir_tx,
output wire port_ir_rx_disable,
// cellular psram 0 and 1, two chips (64mbit x2 dual die per chip)
output wire [21:16] cram0_a,
inout wire [15:0] cram0_dq,
input wire cram0_wait,
output wire cram0_clk,
output wire cram0_adv_n,
output wire cram0_cre,
output wire cram0_ce0_n,
output wire cram0_ce1_n,
output wire cram0_oe_n,
output wire cram0_we_n,
output wire cram0_ub_n,
output wire cram0_lb_n,
output wire [21:16] cram1_a,
inout wire [15:0] cram1_dq,
input wire cram1_wait,
output wire cram1_clk,
output wire cram1_adv_n,
output wire cram1_cre,
output wire cram1_ce0_n,
output wire cram1_ce1_n,
output wire cram1_oe_n,
output wire cram1_we_n,
output wire cram1_ub_n,
output wire cram1_lb_n,
// sdram, 512mbit 16bit
output wire [12:0] dram_a,
output wire [1:0] dram_ba,
inout wire [15:0] dram_dq,
output wire [1:0] dram_dqm,
output wire dram_clk,
output wire dram_cke,
output wire dram_ras_n,
output wire dram_cas_n,
output wire dram_we_n,
// sram, 1mbit 16bit
output wire [16:0] sram_a,
inout wire [15:0] sram_dq,
output wire sram_oe_n,
output wire sram_we_n,
output wire sram_ub_n,
output wire sram_lb_n,
// RFU internal i2c bus
inout wire aux_sda,
output wire aux_scl,
ビデオ出力はVGA信号同様のフォーマットで出力すればプラットフォームのスケーラーが合わせてくれる。
video.json
の記述とも合わせること。
また、複数の解像度を設定すれば、コアから選択できる。
LCDの最大解像度は、スペックとしてはGBの10倍にあたる 1600 x 1440 だけど、
openFPGA として設定できるのは今のところ 800 x 720 まで。
上位モジュールの APF で 24bitRGB を 12bitDDR に変換している。
オーディオ出力は 16bit ステレオの 48kHz が出せて、 I2Cで通信ということでAudioDACチップが載ってる。
// video, audio output to scaler
output wire [23:0] video_rgb,
output wire video_rgb_clock,
output wire video_rgb_clock_90,
output wire video_de,
output wire video_skip,
output wire video_vs,
output wire video_hs,
output wire audio_mclk,
input wire audio_adc,
output wire audio_dac,
output wire audio_lrck,
// bridge bus connection
// synchronous to clk_74a
output wire bridge_endian_little,
input wire [31:0] bridge_addr,
input wire bridge_rd,
output reg [31:0] bridge_rd_data,
input wire bridge_wr,
input wire [31:0] bridge_wr_data,
Pocket のボタンとしてはcont1_key[15:0]
に割り当てられている。
各キーのビット割り当てについてはコメントの通り。
Pocket 用の Dock と、例えば DualShock4 をペアリングすれば、
L2 R2 等も使えるのでは。使えるといいな。
APF 内 io_pad_controllerモジュールでキー入力値をSPIで取得し、 各コントローラレジスタにセットしている模様。
// controller data
//
// key bitmap:
// [0] dpad_up
// [1] dpad_down
// [2] dpad_left
// [3] dpad_right
// [4] face_a
// [5] face_b
// [6] face_x
// [7] face_y
// [8] trig_l1
// [9] trig_r1
// [10] trig_l2
// [11] trig_r2
// [12] trig_l3
// [13] trig_r3
// [14] face_select
// [15] face_start
// [31:28] type
// joy values - unsigned
// [ 7: 0] lstick_x
// [15: 8] lstick_y
// [23:16] rstick_x
// [31:24] rstick_y
// trigger values - unsigned
// [ 7: 0] ltrig
// [15: 8] rtrig
//
input wire [31:0] cont1_key,
input wire [31:0] cont2_key,
input wire [31:0] cont3_key,
input wire [31:0] cont4_key,
input wire [31:0] cont1_joy,
input wire [31:0] cont2_joy,
input wire [31:0] cont3_joy,
input wire [31:0] cont4_joy,
input wire [15:0] cont1_trig,
input wire [15:0] cont2_trig,
input wire [15:0] cont3_trig,
input wire [15:0] cont4_trig
microSDのデータ読み込みは、APF のブリッジ経由で行う。
ここではSDRAMもしくは内蔵RAMへのデータリードを説明する。
microSD内のデータファイルにはdata.json
でターゲットIDや、
ファイルアクセスオプション等を指定しておく。
こうすることでFPGAコアロード時に APF に対して、
ブリッジからどのデータファイルにアクセスされるかといった情報がセットされる。
{
"data": {
"magic": "APF_VER_1",
"data_slots": [
{
"name": "BIOS",
"id": "0x20",
"required": false,
"parameters": "0x008",
"deferload": true,
"filename": "scph5500.bin",
"size_exact": 524288,
"address": "0x00000000"
},
{
"name": "ROM",
"id": "0x40",
"required": true,
"parameters": "0x008",
"deferload": true,
"extensions": ["bin", "img", "exe"],
"size_maximum": "0x40000000",
"address": "0x00080000"
}
]
}
}
ユーザコアからブリッジへ、SDRAMへのリードリクエストを出す場合の記述は以下。
ターゲットファイルは APF のスロットにセットされているので、
スロット内アドレスを指定して、SDRAMの指定アドレスに指定バイトリードする。
stage_name apf2sdram { task do(target_id_reg, target_slotoffset_reg, target_bridgeaddr_reg, target_length_reg); }
stage apf2sdram {
first_state st1;
state st1 par{
// 信号の同期化待ち
reg_wr wait_count<4>;
wait_count++;
if(/&wait_count) goto st2;
}
state st2 if(sdram_ack){
// リードリクエスト
set_sdram_reg := 0b1;
goto st3;
}
state st3 if(target_dataslot_read){
set_sdram_reg := 0b0;
goto st4;
}
state st4 if(^ram_reloading){
goto st1;
finish;
}
}
// SDRAMへのリードリクエスト起動
// ターゲットファイルID、スロット内アドレス、SDRAMアドレス、リードバイトサイズを指定
generate apf2sdram.do(0x0020, 0, 0x01000000, 524288);
コア内の内蔵RAMへのリードリクエストの場合はこう。
同様にスロット内アドレスを指定して、ここでは4kByteの内蔵RAMにリードしている。
任意のリードバイトサイズの時は、リードエンドで ram_reloading が 0 となり、
残りの内蔵RAMデータはガベージになる。
ram_4k file_ram; // 内蔵RAMインスタンス(32bit x 1024word)
stage_name apf2bram { task do(target_id_reg, target_slotoffset_reg, target_length_reg); }
stage apf2bram {
first_state st1;
state st1 par{
// リードリクエスト
set_bram_reg := 0b1;
goto st2;
}
state st2 if(target_dataslot_read){
set_bram_reg := 0b0;
goto st3;
}
state st3 if(bram_word_wr | ^ram_reloading){
reg_wr adrs<10>;
file_ram.write(adrs, bram_wdata);
adrs++;
if(/&adrs) goto st4;
}
state st4 if(^ram_reloading){
file_ram.read(0);
goto st1;
finish;
}
}
// 内蔵RAMへのリードリクエスト起動
// ターゲットファイルID、スロット内アドレス、リードバイトサイズを指定
generate apf2bram.do(0x0040, file_adrs<31:12>||0x000, 4096);
いずれも、リードバイトサイズがファイルサイズ上限境界を超えている場合、
リードリクエストは APF で失効されるので注意。
ユーザコアからのリードリクエスト信号はシンクロナイザを通してブリッジへのリクエストに変える。 ブリッジから APF へのリードリクエストが通り、 microSDからリードされたデータがブリッジ経由で1.4us毎に32bit Writeされてくる。
always @(posedge clk_74a) begin
// ユーザコアからのReadリクエストを処理
case(reload_state)
0: begin
// コアからのReadリクエスト待ち
if(set_sdram_r | set_bram_r) begin
// SDRAMへのReadリクエストの場合
if(set_sdram_r) set_sdram_in <= 1;
// コア内蔵RAMへのReadリクエストの場合
if(set_bram_r) set_bram_in <= 1;
ram_reloading <= 1;
// ブリッジへ渡す各データ情報
target_dataslot_id <= target_id;
target_dataslot_slotoffset <= target_slotoffset;
target_dataslot_bridgeaddr <= target_bridgeaddr;
target_dataslot_length <= target_length;
target_dataslot_read <= 1;
reload_state <= 1;
end
end
1: begin
// wait for ack
if(target_dataslot_ack) begin
target_dataslot_read <= 0;
reload_state <= 2;
end
end
2: begin
if(target_dataslot_done) begin
set_sdram_in <= 0;
set_bram_in <= 0;
ram_reloading <= 0;
reload_state <= 0;
end
end
endcase
// ブリッジからのデータWriteを捌く
sdram_word_wr <= 0;
bram_word_wr <= 0;
if(bridge_wr) begin
casex(bridge_addr[31:24])
8'b000000xx: begin
// SDRAMへのWrite信号
if(set_sdram_in) sdram_word_wr <= 1;
// コア内蔵RAMへのWrite信号
if(set_bram_in) bram_word_wr <= 1;
sdram_word_wrmask <= 2'b00;
sdram_word_addr <= bridge_addr[25:2];
sdram_word_data <= bridge_wr_data;
end
endcase
end
end
SRAMはサイズが小さいのでPSメモリーカードデータ置き場として使えそう。 あとPSRAMが2個あるということは、これを描画フレームバッファとして使えということなのか。
ROMイメージデータの置き場所としては実質的にmicroSDしかなく、 またAnalogueOS管理内に大きなメモリ領域があるわけではなさそうなので、 数百MByteのデータを一度に全て読み込むことはできない。 そのためFPGA内に自前で適切なサイズのバッファを用意し、 その分だけブリッジに対してリードコマンドをリクエストする。 microSDからのデータは、ある程度のサイクルをかけて送られてくる。
バッファについては、一度に読み込むデータのサイズにより適切なバッファサイズを用意する。 一度のリードコマンドでできるだけたくさんのデータをリードする方がウエイトがかからず全体的なデータ読み込み時間を短くできる。 逆にバッファサイズが小さいとリードコマンドの回数が多くなり、 その分だけウェイト回数が多くなり全体的なリード時間が長くなってしまう。 計測したところバッファサイズとリード速度は次のようになった。
microSDカードから16MByteを複数回に分けてリードしたテスト結果:
バッファサイズ | リード回数 | リード時間 | リード速度 |
---|---|---|---|
2 kByte | 8192 | 26 sec | 630 kByte/sec |
4 kByte | 4096 | 20 sec | 820 kByte/sec |
8 kByte | 2048 | 15 sec | 1090 kByte/sec |
16 kByte | 1024 | 11 sec | 1490 kByte/sec |
32 kByte | 512 | 10 sec | 1630 kByte/sec |
FPGAの内蔵RAMをバッファとして使用してるけど、 PlayStationコアではリソースが厳しく4kByte程度しか確保できなかった。 これだと、CD-ROMコントローラからの1回のシーケンシャルリード要求が2,340Byteのため、 毎回microSDカードへアクセスしてしまうことになる。 そこで、APF 内で使われてる謎の内蔵ROM領域(更新日時等が記録されるらしい) をコメントアウトしても動いたので、バッファを4kByteから8kByteにできた。 これでmicroSDカードアクセスの頻度が1/3になって、 PSロゴからSQUAREロゴが表示されるまでほんの少し短くなった気がする。 …というのがファームウェアバージョンbeta-7までの話。 この時はバッファサイズ8kByteでもリード速度250kByte/secしか出てなかったので効果があったんだけどね。
ファームウェアバージョンが1.1に上がってmicroSDカードアクセスが平均5倍ほどに高速化したため (上の表はこの1.1のもの)、 バッファサイズが4kByteでも8kByteでも変わらなくなったので今は4kByteでやってる。 ちなみに、SDHCでもリード速度約1MByte/sec出ているので、 PlayStation実機のCD-ROMリード速度である300kByte/secは満足している。
村長「こちらが『GB互換機をPlayStationにする魔法』でございます」
某エルフ「いいじゃん」
村長「しかしPocketとPlayStationで検索しても、ポケステしか出てきません」
某エルフ「PocketStationって名付けなくてよかったよね」 (合成のフリーマン ~FPGAの魔法~)
PlayStation on FPGA feat. Pocket:
— かんな丸!! (@pgate1) January 19, 2023
あっぽけことAnalogue PocketでトバルNo.1をプレイできるようになった。まだCPUとGPUの並列実行ができてないのでスローだけど、ちらつきは少ないかな。まぁほぼ生ポリみたいな感じなので。 pic.twitter.com/JmS1UY2ujA
PlayStation on FPGA feat. Pocket:ちらつきの低減と、CD-ROMコントローラの機能追加、横512ピクセルへの対応で、デュープリズムやサガフロンティア2を動かせました! pic.twitter.com/skdXEh7d9D
— かんな丸!! (@pgate1) January 29, 2023
動画後半の『アインハンダー』はちらついてしまい目チカがひどいけど、
現状はちらつかないよう改善できた。
まぁ、遊べないこともない…?
プレイできる程度に移植はしたものの、やはり外部メモリの使い方がへたっピでFPSが出ない。 バースト転送・バッファ・複数リクエストの処理など、 メモリの使い方を制する者がFPGAを制すると言っても過言ではない。 あと複数クロック。それからタイミング制約。
しかし、Pocket 向けの中途半端なPlayStationコアをリリースしたあと、 界隈の焚き付けに失敗し、結局は自分でまともなものを出さざるを得ないような雰囲気になってる。 これが期待値のアンダーシュートだよ…。
ターゲット | 使用率 | 内蔵RAM | DSP | Fmax |
---|---|---|---|---|
DE0-CV | 92% | 79% | 98% | 49.8 MHz |
93% | 99% | 98% | 47.5 MHz |
あっぽけの寝床にちょうどよいポーチ見っけた pic.twitter.com/p36WMeNeoF
— かんな丸!! (@pgate1) January 23, 2023
NintendoDS向けに発売された『ニンテンドーDSブラウザー』には、 『DSメモリー拡張カートリッジ』が同梱されている。 特に初期ロットのものはGBAカートリッジ形状のため、Pocket にセット可能。 チップはEM64と書かれたものが搭載されていて、64MbitのSRAMらしい。 ただ、初期化処理をしないと拡張メモリエリアにアクセスできない仕様なうえ、 カスタム品なので仕様も見つけられず。 完全にネタ画像となってしまっている。Pocketのメモリが足りない!?
— かんな丸!! (@pgate1) January 31, 2023
そんなときはコレを…こうじゃ
これで64MbitのSRAM追加なのじゃ! pic.twitter.com/G74uiVP3Op
また『DS振動カートリッジ』についてはすでに openFPGA の GBC/GBAコアでサポートされていて、 振動対応ゲームプレイ時にちゃんと振動する。 これも初期のGBAカートリッジ形状のものが Pocket に無理なくセット可能。Pocketに振動機能を追加したい?
— かんな丸!! (@pgate1) 2024年9月21日
そんなこともあろうかと…こうじゃ! pic.twitter.com/eJVbeq82TE
令和のGBソフト『NEKO Can DREAM』が到着したので、Pocket開発の合間に遊ぶ!猫分儀スミレさんありがとう! pic.twitter.com/gohyaceAHD
— かんな丸!! (@pgate1) January 7, 2023
ゲームプレイもいいけど、openFPGA という立派な開発用フレームワークが公開されたので、 ポータブルゲーミングFPGA環境として動かせると楽しいわけで。 つまり Pocket を持ち歩けばどこでもHDLの実装ができるということ。 本記事も openFPGA の使い方が分かるに従い追記していく。
openFPGA については、今後 openFPGA をサポートした『SuperNt』や『MegaSg』の後継機等が出てくる可能性は…? すでに移植されたコアが動くように Pocket と同じFPGAを搭載するのか。 それとも openFPGA2 みたいに改良してくるのか。 それにしても、すでに発表されている『Duo』や『3D』が openFPGA をサポートしないとアナウンスされた時の界隈の落胆っぷりがすごかったよね。 (2025/05/05 追記)Duo の基板画像を見つけたので確認したところ、 メインFPGAに加えサブFPGAとPICで AnalogueOS を使用してはいるものの、 Pocket にあった openFPGA 用の SDRAM や PSRAM が搭載されてないため、 ファームウェアアップデートでの openFPGA 対応は今後もなさそう。