RISC-VをUltra96上のpetalinuxから実行

[第5回AIエッジコンテスト（実装コンテスト③）](https://signate.jp/competitions/537)に参加しています。
このコンテストではUltra96v2上にRISC-Vコアを実装し、その上でAIアプリケーションを実行することが求められています。
ARMコアやXilinx DPUの使用は認められていますが、AIアプリケーションにおける何らかの処理をRISC-Vコア上で動作させることが応募条件となっています。
コンテスト主催のSIGNATEから[RISC-Vコアの実装例が公開されています](https://signate.jp/competitions/537/discussions/risc-v)が、実装例はベアメタルでの実行だったため、Petalinuxから実行させてみました。

# Environment
- Host: Ubuntu18.04
- Vivado/Petalinux 2020.2

# Vivadoデザイン
公開されているVivado.zipをダウンロードしてVivadoプロジェクトを開きます。

```sh
source /tools/Xilinx/Vivado/2020.2/settings64.sh
vivado ultra96_riscv.xpr
```

ブロックデザインは以下のようになっています。

![Screenshot from 2021-12-11 01-41-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/e52e844d-2e25-5480-584a-394514e34d88.png)

RISC-Vコアの命令メモリ（IMEM）とデータメモリ（DMEM）が`Block RAM Generator`で実装されており、RISC-VコアとBRAM、ARM PSコアとBRAMは共にAXIプロトコルで通信するようになっています。2つのコアからのIMEM、DMEMへの読み書き要求は、AXI Smart Connectが読み書きの調停をしてくれているのだと思います。

ARMコアの搭載されていないFPGAでのCPU実装の経験はありましたが、MPSoCにおけるCPU実装の経験はなかったので勉強になりました。

このリファレンス実装ではRISC-Vコアの実装は[SpinalHDL/VexRiscv](https://github.com/SpinalHDL/VexRiscv)を使用しています。
2018年の RISC-V SoftCPU コンテストで優勝している実装で、RocketchipよりもFPGAボード上で高周波数で動作させられるようです。

`Generate Bitstream`を実行して論理合成、配置配線を実行し、ビットストリームを生成します。
ビットストリームの生成後、XSAファイルをエクスポートします。tclコンソールで以下を実行しました。

```
cd <Vivado Project Dir>
write_hw_platform -include_bit -force ultra96_riscv.xsa
validate_hw_platform ./ultra96_riscv.xsa
```

# Petalinuxプロジェクトの作成
以下の記事を参考にさせていただきました。ありがとうございます。
[Ultra96V2 で Petalinux を起動するときにやったこと](https://qiita.com/daiki0321/items/fee01d227271cbecdea3)
[Ultra96-V2 をUSB-LAN 変換アダプタでネットワークに接続](https://marsee101.blog.fc2.com/blog-entry-4737.html)

```sh
source ~/petalinux_2020.2/settings.sh
# プロジェクトの作成
petalinux-create -t project --template zynqMP --name ultra96_riscv
cd ultra96_riscv
# HW情報の読み込み
petalinux-config --get-hw-description <Vivado Project Directory>/ultra96_riscv.xsa
```

Ultra96を動作させるためのPetalinuxプロジェクト設定を変更します。

```sh
petalinux-config
```
- UARTの設定変更 (psu_uart_0 → psu_uart_1)

![Screenshot from 2021-12-11 01-45-19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/2ba42250-84ac-c496-b035-45e415f84d4b.png)
- Image Packing Configurationの変更
Root filesystem typeをEXT4に変更
![Screenshot from 2021-12-11 01-44-38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/18469550-0da7-6e82-0ba0-65c656cd158f.png)
- rootfsファイルシステムの変更
Root filesystem formatの末尾に`ext4`を追加
![Screenshot from 2021-12-11 01-45-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/d55f67ec-dff0-fe71-2e35-dd35ecf01314.png)

- MACHINE_NAMEの変更

Ultra96用の設定？コンポーネント？が反映されるらしい。なぜかrev1指定らしい。実際に使用しているのはrev2ですが・・
![Screenshot from 2021-12-11 01-46-46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/36cc682a-739d-0f34-6cae-34932745c168.png)

次に、USB-LANを使うためにカーネルコンフィグを修正します。今回はwifiは使用できるようにしていません。使用しているUSB-LANアダプタは[USB-LAN1000R](https://www.planex.co.jp/products/usb-lan1000r/)です。

```sh
petalinux-config -c kernel
```
`Realtek RTL8152 Based USB Ethernet Adapters`を有効化
![Screenshot_from_2021-12-09_01-21-35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/4383e5c6-fa44-9ffe-01a1-79301d66cdd7.png)
あと、必須ではありませんが、CPUの省電力化設定を無効化しておきます。
![145076680-eedab3cf-d90f-40c0-a41a-09c1d8b7ee75.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/cfe700ae-7cf5-b3d8-8328-fd7bd1aa9d1f.png)
![145076683-790130f2-9ec4-442a-8fa6-2b1bd954385b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/70536b93-11f9-ea5d-eca2-dc3c24e3421d.png)

rootfsに色々追加していきます。gccさえ使えれば今回は問題ないのですべては必要ないと思います。
`/project-spec/meta-user/conf/user-rootfsconfig`に以下を追記

```
CONFIG_xrt
CONFIG_xrt-dev
CONFIG_zocl
CONFIG_opencl-clhpp-dev
CONFIG_opencl-headers-dev
CONFIG_packagegroup-petalinux-opencv
```

[add_petalinux_packages.sh](https://github.com/Avnet/vitis/blob/2020p2_u96v2_sbc_base_20210426_105325/add_petalinux_packages.sh)を実行してpetalinux packageの追加

最後に、デバイスツリーを追記してGPIO, DMEM BRAM, IMEM BRAMをUIOで制御できるようにします。UIOでの制御については以下の記事で詳しく説明されています。
参考：[ZYBO (Zynq) 初心者ガイド (16) Linuxから自作IPをUIOで制御する](https://qiita.com/iwatake2222/items/da91ce4dc2a8a8df3c0a)
`./components/./plnx_workspace/device-tree/device-tree/pl.dtsi`を閲覧して自動生成されたdtsiを見る。（もしかすると一旦petalinux-buildを実行しないと自動生成されないかもしれません）AXI GPIOとAXI BRAM Controllerにそれぞれ`axi_gpio_0, IMEM_CONTROL, DMEM_CONTROL`のラベルが割り当てられており、これらの`compatible`プロパティを`system-user.dtsi`で`generic-uio`に上書きします。
![Screenshot from 2021-12-11 02-00-51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/251914b8-d2e5-1d55-fe33-a2115ee34aff.png)


`./project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi`の編集
以下のように記述しました。

```
/include/ "system-conf.dtsi"
/ {
    chosen {
		bootargs = "earlycon console=ttyPS0,115200 clk_ignore_unused root=/dev/mmcblk0p2 rw rootwait cma=512M uio_pdrv_genirq.of_id=generic-uio";
	};
    xlnk {
        compatible = "xlnx,xlnk-1.0";
    };

};

&sdhci0 {
    disable-wp;
};


&axi_gpio_0 {
    compatible = "generic-uio";
};

&IMEM_CONTROL {
    compatible = "generic-uio";
};

&DMEM_CONTROL {
    compatible = "generic-uio";
};
```
最後にpetalinuxプロジェクトをビルドします。SDカードの第1パーティションをFAT, 第2パーティションをext4でフォーマットしておきます。

```sh
petalinux-build
petalinux-package --boot --fsbl ./images/linux/zynqmp_fsbl.elf --fpga ./images/linux/system.bit --uboot --force
cd images/linux
cp BOOT.BIN /media/lp6m/BOOT/
cp image.ub /media/lp6m/BOOT/
cp boot.scr /media/lp6m/BOOT/
sudo tar xvf rootfs.tar.gz -C /media/lp6m/rootfs/
```
以上でSDカードの作成は完了です。Ultra96にSDカードを接続し、ssh接続して以降の作業を行います。

# アプリケーションの作成
ssh接続し、デバイスツリーで設定した各IPがどのUIOデバイスとして認識されているかを確認します。

```sh
root@ultra96_riscv:~# cat /sys/class/uio/uio0/maps/map0/name
axi_bram_ctrl@a0002000
root@ultra96_riscv:~# cat /sys/class/uio/uio1/maps/map0/name
axi_bram_ctrl@a0000000
root@ultra96_riscv:~# cat /sys/class/uio/uio2/maps/map0/name
gpio@a0010000
```
IMEMがUIO1, DMEMが UIO0, AXI_GPIOがUIO2として認識されていることがわかりました。

リファレンス実装で公開されているテストアプリケーションをPetalinux向けに修正します。基本的には参考記事同様に、UIOデバイスを開いて物理アドレスから仮想アドレスを取得し、仮想アドレスを使ってFPGA回路の制御を行います。
[ZYBO (Zynq) 初心者ガイド (16) Linuxから自作IPをUIOで制御する](https://qiita.com/iwatake2222/items/da91ce4dc2a8a8df3c0a)

以下のソースコードを`test.c`として作成します。`UIO0/UIO1/UIO2`のmmapするサイズは、Vivadoで設定したAddress Editorのアドレス空間のサイズに合わせたサイズを設定しています。

![Screenshot from 2021-12-12 01-04-53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243354/2469f7ca-5965-32fe-0a06-87e803073dcc.png)

※(2021.12.12追記)ソースコードを修正しました。

```c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <fcntl.h>

#define REG(address) *(volatile unsigned int*)(address)

// GPIO[0]=RISC_V_IMEM_RESET、RISC_V_DMEM_RESET
// GPIO[1]=LED0
// GPIO[2]=LED1
// This program is DMEM[0]+DMEM[1]=DMEM[2]
int main()
{
    unsigned int c;
    int uio0_fd = open("/dev/uio0", O_RDWR | O_SYNC);
    unsigned int* DMEM_BASE = (unsigned int*) mmap(NULL, 0x2000, PROT_READ|PROT_WRITE, MAP_SHARED, uio0_fd, 0);
    int uio1_fd = open("/dev/uio1", O_RDWR | O_SYNC);
    unsigned int* IMEM_BASE = (unsigned int*) mmap(NULL, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, uio1_fd, 0);
    int uio2_fd = open("/dev/uio2", O_RDWR | O_SYNC);
    unsigned int* GPIO_DATA = (unsigned int*) mmap(NULL, 0x8000, PROT_READ|PROT_WRITE, MAP_SHARED, uio2_fd, 0);
    unsigned int* GPIO_TRI = GPIO_DATA + 1;
    printf("Hello World\n\r");
    REG(GPIO_TRI) = 0x00;
    REG(GPIO_DATA) = 0x02; // LED0
    
    /* Memory access test */
    c = 0;
    IMEM_BASE[0] = 0xA0002437; //  0: lui s0,0x41000
    IMEM_BASE[1] = 0x00040413; //  4: mv  s0,s0
    IMEM_BASE[2] = 0x00042603; //  8: lw  a2,0(s0) # 0x41000000
    IMEM_BASE[3] = 0x00442683; //  C: lw  a3,4(s0)
    IMEM_BASE[4] = 0x00d60733; // 10: add a4,a2,a3
    IMEM_BASE[5] = 0x00e42423; // 14: sw  a4,0(s0) # 0x41000000
    IMEM_BASE[6] = 0x0000006f; // 18: j   0x18
    DMEM_BASE[0] = 0x00000012; //  0: 0x12
    DMEM_BASE[1] = 0x00000034; //  4: 0x34
    sleep(1);
    REG(GPIO_DATA) = 0x04; // LED1
    sleep(1);
    REG(GPIO_DATA) = 0x01; // Reset off
    sleep(1);
    REG(GPIO_DATA) = 0x00; // Reset on
    sleep(1);
    unsigned int c1 = DMEM_BASE[0];
    unsigned int c2 = DMEM_BASE[1];
    unsigned int c3 = DMEM_BASE[2];
    printf("%x %x %x\n\r",c1, c2, c3);
    c =  DMEM_BASE[2];
    if(c==0x00000046){ // 0x12+0x34=0x46
        printf("Pass\n\r");
        REG(GPIO_DATA) = 0x04; // LED1
    }else{
        printf("Fail\n\r");
        REG(GPIO_DATA) = 0x06; // LED1,0
    }
    sleep(1);

    printf("Successfully ran Hello World application\n\r");
    return 0;
}

```

テストプログラムをコンパイルし、実行します。
うまく行けば、以下のように表示されるはずです！

```sh
gcc test.c
./a.out
Hello World
12 34 46
Pass
Successfully ran Hello World application
```

### dev/memを用いた制御方法
参考記事でも紹介されているとおり、`/dev/uio/`の代わりに`dev/mem`を用いた制御は意図せずメモリを破壊する可能性があるので危険ですが、簡単に行えるのでそれを試したソースコードも追記しておきます。

```c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <fcntl.h>

#define REG(address) *(volatile unsigned int*)(address)

#define HW_GPIO_BASE (0xA0010000) /* XPAR_AXI_GPIO_0_BASEADDR */
#define HW_GPIO_DATA (HW_GPIO_BASE + 0x0000)
#define HW_GPIO_TRI  (HW_GPIO_BASE + 0x0004)

#define HW_IMEM_BASE  (0xA0000000)
#define HW_DMEM_BASE  (0xA0002000)

// GPIO[0]=RISC_V_IMEM_RESET、RISC_V_DMEM_RESET
// GPIO[1]=LED0
// GPIO[2]=LED1
unsigned int *assignToPhysicalUInt(unsigned long address,unsigned int size){
	int devmem = open("/dev/mem", O_RDWR | O_SYNC);
	off_t PageOffset = (off_t) address % getpagesize();
	off_t PageAddress = (off_t) (address - PageOffset);
	return (unsigned int *) mmap(0, size*sizeof(unsigned int), PROT_READ|PROT_WRITE, MAP_SHARED, devmem, PageAddress);
}
// This program is DMEM[0]+DMEM[1]=DMEM[2]
int main()
{
    unsigned int c;
    volatile unsigned int* GPIO_DATA = assignToPhysicalUInt(HW_GPIO_DATA, 0x4);
    volatile unsigned int* GPIO_TRI = assignToPhysicalUInt(HW_GPIO_TRI, 0x1);
    volatile unsigned int* IMEM_BASE = assignToPhysicalUInt(HW_IMEM_BASE, 0x1000);
    volatile unsigned int* DMEM_BASE = assignToPhysicalUInt(HW_DMEM_BASE, 0x2000);

    printf("Hello World\n\r");
    REG(GPIO_TRI) = 0x00;
    REG(GPIO_DATA) = 0x02; // LED0
    
    /* Memory access test */
    c = 0;
    IMEM_BASE[0] = 0xA0002437; //  0: lui s0,0x41000
    IMEM_BASE[1] = 0x00040413; //  4: mv  s0,s0
    IMEM_BASE[2] = 0x00042603; //  8: lw  a2,0(s0) # 0x41000000
    IMEM_BASE[3] = 0x00442683; //  C: lw  a3,4(s0)
    IMEM_BASE[4] = 0x00d60733; // 10: add a4,a2,a3
    IMEM_BASE[5] = 0x00e42423; // 14: sw  a4,0(s0) # 0x41000000
    IMEM_BASE[6] = 0x0000006f; // 18: j   0x18
    DMEM_BASE[0] = 0x00000012; //  0: 0x12
    DMEM_BASE[1] = 0x00000034; //  4: 0x34
    sleep(1);
    REG(GPIO_DATA) = 0x04; // LED1
    sleep(1);
    REG(GPIO_DATA) = 0x01; // Reset off
    sleep(1);
    REG(GPIO_DATA) = 0x00; // Reset on
    sleep(1);
    unsigned int c1 = DMEM_BASE[0];
    unsigned int c2 = DMEM_BASE[1];
    unsigned int c3 = DMEM_BASE[2];
    printf("%x %x %x\n\r", c1, c2, c3);
    c =  DMEM_BASE[2];
    if(c==0x00000046){ // 0x12+0x34=0x46
        printf("Pass\n\r");
        REG(GPIO_DATA) = 0x04; // LED1
    }else{
        printf("Fail\n\r");
        REG(GPIO_DATA) = 0x06; // LED1,0
    }
    sleep(1);

    printf("Successfully ran Hello World application\n\r");
    return 0;
}
```

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">わー <a href="https://t.co/d8XzKvNobD">pic.twitter.com/d8XzKvNobD</a></p>&mdash; lp6m (@lp6m1) <a href="https://twitter.com/lp6m1/status/1469342268629078017?ref_src=twsrc%5Etfw">December 10, 2021</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

というわけで、UIOを使用して制御することでRISC-Vのリファレンス実装をPetalinuxから動作させることができました。