RISC-V+Zephyr(OS)でファミコンのゲームを動かす

RISC-V+Zephyr(OS)でファミコンのゲームを動かす

はじめに

ソフトコアとして作成したRISC-VおよびZephyr(OS)を使って、昔ながらのファミコンゲームを動かしてみます。
以下のような感じです。

※上記ゲームは下記の「TkShoot」を使用させてもらいました。
http://hp.vector.co.jp/authors/VA042397/nes/sample.html

ファミコンのゲームを動かすために以下のソースコードを使用します。
https://github.com/espressif/esp32-nesemu

ゲームソフトは著作権フリーとして公開されているものを使用します。
http://hp.vector.co.jp/authors/VA042397/nes/sample.html
https://www.zophar.net/pdroms/nes.html

全体の流れ

  • 使用機材
  • 開発環境の構築
  • FPGAの開発
  • ソフトウェアの開発
  • ハードウェアの取り付け作業
  • 動作確認

使用機材

  • ubuntu 20.04(6コア、メモリ8G、ストレージ256G以上)
  • Arty A7-100T
  • LCD(320×240 ILI9341)
  • PS2コントローラー
  • 10KΩ抵抗(PS2コントローラーをArty-A7に接続する際に、プルアップ抵抗として使用する)
  • SDカードおよびPModインタフェースのカードスロット
  • 接続用配線材

開発環境の構築

パッケージをインストール

$ sudo apt-get update
$ sudo apt-get install openocd dtc fakeroot perl-bignum json-c-devel verilator ¥
                 python3-devel python3-setuptools libevent-devel \
                 libmpc-devel mpfr-devel meson expat-devel 

Xilinx開発ツール(Vivado)をインストール
https://www.acri.c.titech.ac.jp/wordpress/archives/12916https://docs.xilinx.com/r/ja-JP/ug973-vivado-release-notes-install-license/%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%8A%E3%82%88%E3%81%B3%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB

RISC-V GNU Compiler Toolchainをインストール

$ git clone --recursive https://github.com/riscv/riscv-gnu-toolchain
$ pushd riscv-gnu-toolchain
$ ./configure --prefix=/opt/rv64 --enable-multilib
$ make newlib linux
$ popd 

※かなり時間がかかります。

パスを通す

export PATH=$PATH:/opt/riscv/bin 

※.bashrcにも書いておくと便利です。

inux-on-litex-vexriscvとLitexの開発環境をインストール

$ mkdir litex-vexriscv
$ cd litex-vexriscv
$ git clone https://github.com/litex-hub/linux-on-litex-vexriscv
$ cd linux-on-litex-vexriscv
$ wget https://raw.githubusercontent.com/enjoy-digital/litex/master/litex_setup.py
$ chmod +x litex_setup.py
$ ./litex_setup.py --init --install –user
$ ./litex_setup.py --update 

※ユーザーのディレクトリ(.local/bin)にPythonがセットアップされるため、複数の環境は構築できないので注意。

Zephyr OSソースツリーをインストール

$ pip3 install --user -U west
$ west init --mr v3.3.0 ~/zephyrproject
$ cd ˜/zephyrproject
$ west update 

CMakeパッケージをエクスポート

$ west zephyr-export 

Python依存ライブラリをインストール

$ pip3 install --user -r ˜/zephyrproject/zephyr/scripts/requirements.txt 

ツールチェーン(Zephyr OS SDK)をインストール

$wget    https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.0/zephyr-sdk-0.16.0_linux-x86_64.tar.xz
$ tar xvf zephyr-sdk-0.16.0_linux-x86_64.tar.xz
$ cd zephyr-sdk-0.16.0 

※詳しくは以下を参照。
https://docs.zephyrproject.org/3.1.0/boards/riscv/litex_vexriscv/doc/index.html
https://www.mikan-tech.net/entry/zephyr-os-install

FPGAの開発

litexのフレームワークを使ってFPGAの設定と論理合成を行う。


˜/litex-vexriscv/litex-boards/litex_boards/targets/digilent_arty.py

# GPIOのimportを以下に修正
from litex.soc.cores.gpio import GPIOIn, GPIOOut, GPIOInOut, GPIOTristate

# LCD用のGPIOを追加
self.gpio0 = GPIOOut(
    pads = platform.request_all("gpio0_out"),
)

# PS2コントローラー用のGPIOを追加
self.gpio1 = GPIOInOut(
    in_pads = platform.request_all("gpio1_in"),
    out_pads = platform.request_all("gpio1_out"),
)

# LCD用のSPIを追加
cti_data_width = 8
cti_clk_freq = 40e6
spi_pads = self.platform.request("spi")

self.spi = SPIMaster(spi_pads, cti_data_width, sys_clk_freq, cti_clk_freq) 

˜/litex-vexriscv/litex-boards/litex_boards/platforms/digilent_arty.py

# LCD用のGPIOを追加
("gpio0_out", 0,Pins("M16"), IOStandard("LVCMOS33")),
("gpio0_out", 1,Pins("N14"), IOStandard("LVCMOS33")),

# PS2コントローラー用のGPIOを追加
("gpio1_in", 0,Pins("G13"), IOStandard("LVCMOS33")),
("gpio1_out", 0,Pins("B11"), IOStandard("LVCMOS33")),
("gpio1_out", 1,Pins("A11"), IOStandard("LVCMOS33")),
("gpio1_out", 2,Pins("D12"), IOStandard("LVCMOS33")),

# LCD用のSPIを追加
("spi", 0,
    Subsignal("clk", Pins("P17")),
    Subsignal("cs_n", Pins("V17")),
    Subsignal("mosi", Pins("U18")),
    Subsignal("miso", Pins("R17")),
    IOStandard("LVCMOS33"),
), 

※各ピンの位置はお好みでOK

下記コマンドで論理合成する

./targets/digilent_arty.py --timer-uptime --variant=a7-100 --with-sdcard --csr-json csr_arty_a7.json –build 

ビットファイル(.bit)をコンフィグレーションデータ(.mcs)に変換する

$ cd build/digilent_arty/gateware
$ cat > arty_a7_mcs.tcl <<EOF
write_cfgmem -force -format mcs -interface spix4 -size 16 -loadbit "up 0x0 digilent_arty.bit" -file arty_a7.mcs
EOF
$ vivado -mode batch -source arty_a7_mcs.tcl 

生成されるarty_a7.mcsをコンフィグレーションメモリに書き込む。

Arty-A7をVivadoをインストールしたPCにmicroUSBで接続する。

Vivadoを起動 > Hardware Managerを起動 > Open Target (Auto Connect) > xc7a100t_0を右クリック > Add Configuration Memory Device

メモリは以下を選択する(使用するFPGAによって読み替える)。
s25fl127s-spi-x1_x2_x4

arty_a7.mcsを選択して書き込む。
電源をOFF/ONすることで、arty_a7.mcsに従ってFPGAがコンフィグレーションされる。

以上でFPGAの開発は完了となる。

ソフトウェアの開発

Zephyrが用意しているサンプルコードをベースにし、下記ソースコードを今回のハードウェアプラットフォーム向けに移植する。
https://github.com/espressif/esp32-nesemu

CMakeLists.txtにビルド対象のソースコードを追加

以下のようにビルド対象のソースコードとインクルードパスを追加していく

target_sources(app PRIVATE src/main.c)
target_sources(app PRIVATE src/components/nofrendo/bitmap.c)
target_sources(app PRIVATE src/components/nofrendo/config.c)
…
target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/components/nofrendo/libsnss/)
target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/components/nofrendo/cpu/)
target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/components/nofrendo-esp32/) 

FreeRTOSの機能をZephyrの機能に置き換える

include文については以下のようにコンパイルスイッチで切り替わるようにする。

components/nofrendo-esp32/video_audio.c

#ifndef __ZEPHYR_NESEMU__
#include <freertos/FreeRTOS.h>
#include <freertos/timers.h>
#include <freertos/task.h>
#include <freertos/queue.h>
#else /* __ZEPHYR_NESEMU__ */
#include <zephyr/kernel.h>
#endif /* __ZEPHYR_NESEMU__ */

タイマーやメッセージキューなど、FreeRTOSが提供する機能をZephyrの同等機能に置き換えていく(ビルドエラーがなくなるまで…)

components/nofrendo-esp32/video_audio.c

int osd_installtimer(int frequency, void *func, int funcsize, void *counter, int countersize)
{
#ifndef __ZEPHYR_NESEMU__
    printf("Timer install, freq=%d\n", frequency);
    timer=xTimerCreate("nes",configTICK_RATE_HZ/frequency, pdTRUE, NULL, func);
    xTimerStart(timer, 0);
#else /* __ZEPHYR_NESEMU__ */
    printk("Timer install, freq=%d\n", frequency);
    k_timer_init(&timer, func, NULL);
    k_timer_start(&timer, K_MSEC(1000/frequency), K_MSEC(1000/frequency));
#endif

    return 0;
} 

SPIを制御するためのソースコードを実装

components/nofrendo-esp32/spi_lcd.cをベースに、アドレスおよび書き込み処理の部分を今回のハードウェアに合わせこむ。
例えば以下のように実装する。

components/nofrendo-esp32/spi_lcd.c (もともとのソースコード)

#define LCD_SEL_CMD() GPIO.out_w1tc = (1 << PIN_NUM_DC) // Low to send command 

components/nofrendo-esp32/spi_lcd_zephy.c (今回のソースコード)

#define CSR_GPIO0_OUT_ADDR (CSR_GPIO0_BASE + 0x000L)
#define CSR_GPIO0_OUT_SIZE 1
static inline uint32_t gpio0_out_read(void) {
    return csr_read_simple((CSR_GPIO0_BASE + 0x000L));
}
static inline void gpio0_out_write(uint32_t v) {
    csr_write_simple(v, (CSR_GPIO0_BASE + 0x000L));
}
…
#define LCD_SEL_CMD()   gpio0_out_write(gpio0_out_read() & ~(1 << PIN_NUM_DC)) // Low to send command 

上記のCSR_GPIO0_BASEに設定するアドレスは、FPGAの開発時に作成される「csr_arty_a7.json」の内容から設定する

PS2コントローラーを制御するためのソースコードを実装

components/nofrendo-esp32/psxcontroller.cをベースに、アドレスおよび書き込み処理の部分を今回のハードウェアに合わせこむ。
例えば以下のように実装する。

components/nofrendo-esp32/psxcontroller.c (もともとのソースコード)

for (x=0; x<8; x++) {
    if (send&1) {
        GPIO.out_w1ts=(1<<PSX_CMD);
    } else {
        GPIO.out_w1tc=(1<<PSX_CMD);
    } 

components/nofrendo-esp32/psxcontroller_zephyr.c (今回のソースコード)

#define CSR_GPIO1_OUT_ADDR (CSR_GPIO1_OUT_BASE + 0x000L)
#define CSR_GPIO1_OUT_SIZE 1
static inline uint32_t gpio1_out_read(void) {
    return csr_read_simple((CSR_GPIO1_OUT_BASE + 0x000L));
}
static inline void gpio1_out_write(uint32_t v) {
    csr_write_simple(v, (CSR_GPIO1_OUT_BASE + 0x000L));
}

#define PSX_CMD (1) // Out

#define PSX_CMD_SET() gpio1_out_write(gpio1_out_read() | (1 << PSX_CMD))
#define PSX_CMD_CLR() gpio1_out_write(gpio1_out_read() & ~(1 << PSX_CMD))

for (x=0; x<8; x++) {
    if (send&1) {
        PSX_CMD_SET();
    } else {
        PSX_CMD_CLR();
    } 

上記のCSR_GPIO1_OUT_BASEに設定するアドレスは、FPGAの開発時に作成される「csr_arty_a7.json」の内容から設定する

sin/cos関数を使用できるようにするためライブラリを追加

prj.conf

CONFIG_NEWLIB_LIBC=y 

ESP32は奇数アドレスへの参照を許容するが、RISC-Vは許容しないため処理を修正

components/nofrendo/cpu/nes6502.c

INLINE uint32 bank_readword(register uint32 address)
…
    if(address&0x01) {
        uint8 *p;
        uint32 x;
        p = (uint8 *)(cpu.mem_page[address >> NES6502_BANKSHIFT] + (address & NES6502_BANKMASK));
        x = *(p+1);
        x <<= 8;
        x |= *(p+0);
        return x;
    } else {
        return (uint32) (*(uint16 *)(cpu.mem_page[address >> NES6502_BANKSHIFT] + (address & NES6502_BANKMASK)));
    } 

ベースとなるコードのままだと確保したメモリ外へのアクセスが発生するため処理を修正

components/nofrendo/nes/nes_ppu.c

void ppu_scanline(bitmap_t *bmp, int scanline, bool draw_flag)
…
#if 0
    if (scanline < 240)
#else
    if ( scanline < bmp->height )
#endif 

ゲームのROMデータをビルド時にバイナリに抱き込む

NESのエミュレータにゲームのROMデータをインプットする方法は、使用するプラットフォームによって取れる手段が異なる。

方法1) SDカード上にファイルシステムを構築し、*.nesというファイルとして置いておく

方法2) 不揮発性メモリの特定番地に書き込んでおき、NESのエミュ―レータ実行時に特定番地から読み込む

上記方法1および2は、どちらも採用できるプラットフォームもあれば、ハードウェアの制約上採用できないプラットフォームもある。つまり、上記方法では移植性の面ではあまりよろしくない。

よって、今回はビルド時にプログラムのバイナリデータ内にゲームのROMデータを取り込むようにする。

#define IMPORT_BIN(sect, file, sym) __asm__ (¥
    ".section " #sect "¥n"                  /* Change section */¥
    ".balign 4¥n"                           /* Word alignment */¥
    ".global " #sym "¥n"                    /* Export the object address to other modules */¥
    #sym ":¥n"                              /* Define the object label */¥
    ".incbin ¥"" file "¥"¥n"                /* Import the file */¥
    ".global _sizeof_" #sym "¥n"            /* Export the object size to oher modules */¥
    ".set _sizeof_" #sym ", . - " #sym "¥n" /* Define the object size */¥
    ".balign 4¥n"                           /* Word alignment */¥
    ".section ¥".text¥"¥n")                 /* Restore section */
#ifndef NES6502_ROM_FILE
#define NES6502_ROM_FILE "../bin/rom.nes"
#endif /* NES6502_ROM_FILE */
IMPORT_BIN(".rodata", "../bin/rom.nes", romNes);
extern uint8_t romNes[]; 

ビルドする

下記コマンドでビルド実行

$ west build -b litex_vexriscv -- -DDTC_OVERLAY_FILE=overlay.dts 

ビルドに成功すると build/zephyr/zephyr.bin が生成される。
このファイルがZephyrおよびNESのエミュレーションを含んだバイナリファイルである。

ハードウェアの取り付け作業

以下のようにハードウェアの取り付けを行う。

PS2コントローラーの接続は以下のサイトを参考に、対応するピンに接続する。
https://github.com/espressif/esp32-nesemu
https://store.curiousinventor.com/guides/PS2/

PS2コントローラーのDATAピンは10KΩでプルアップさせる必要がある。
今回は、3.3Vのピンの間に抵抗を挟む形で以下のようにプルアップさせている。

動作確認

SDカードの準備

SDカードをFATファイルシステムでフォーマットする。
以下のファイルをSDカードのルート直下に配置する。

boot.json

{
    "zephyr.bin": "0x40000000"
} 

配置できたら、SDカードをカードスロットに挿入する。

電源ON

Arty-A7にmicroUSBケーブルを接続し、電源を供給する。
以下のようにゲームの画面が表示されれば完成。

おわり

本記事は、EdgeTech+ West2023の弊社展示物の作成方法として投稿しています。
EdgeTech+ West2023までにやり遂げたかったが、出来なかった事を以下に記載します。
もし余力があれば、チャレンジしてみてください!

  • ゲーム動作中のカクつきの改善(現状、ハード、またはソフトのパフォーマンスに問題がある)
  • ゲームの音を出す
  • ファミコン以外のゲーム機も動かせるようにしたい
  • ゲーム機をエミュレーションするソフトではなく、FPGAでゲーム機そのものをハードウェアレベルで再現させたい