第5回AIエッジコンテスト(実装コンテスト③)に参加しています。
このコンテストではUltra96v2上にRISC-Vコアを実装し、その上でAIアプリケーションを実行することが求められています。
ARMコアやXilinx DPUの使用は認められていますが、AIアプリケーションにおける何らかの処理をRISC-Vコア上で動作させることが応募条件となっています。
コンテスト主催のSIGNATEからRISC-Vコアの実装例が公開されていますが、実装例はベアメタルでの実行だったため、Petalinuxから実行させてみました。
Environment
- Host: Ubuntu18.04
- Vivado/Petalinux 2020.2
Vivadoデザイン
公開されているVivado.zipをダウンロードしてVivadoプロジェクトを開きます。
source /tools/Xilinx/Vivado/2020.2/settings64.sh
vivado ultra96_riscv.xpr
ブロックデザインは以下のようになっています。
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を使用しています。
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 を起動するときにやったこと
Ultra96-V2 をUSB-LAN 変換アダプタでネットワークに接続
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プロジェクト設定を変更します。
petalinux-config
- UARTの設定変更 (psu_uart_0 → psu_uart_1)
- 
MACHINE_NAMEの変更 
Ultra96用の設定?コンポーネント?が反映されるらしい。なぜかrev1指定らしい。実際に使用しているのはrev2ですが・・

次に、USB-LANを使うためにカーネルコンフィグを修正します。今回はwifiは使用できるようにしていません。使用しているUSB-LANアダプタはUSB-LAN1000Rです。
petalinux-config -c kernel
Realtek RTL8152 Based USB Ethernet Adaptersを有効化

あと、必須ではありませんが、CPUの省電力化設定を無効化しておきます。


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を実行してpetalinux packageの追加
最後に、デバイスツリーを追記してGPIO, DMEM BRAM, IMEM BRAMをUIOで制御できるようにします。UIOでの制御については以下の記事で詳しく説明されています。
参考:ZYBO (Zynq) 初心者ガイド (16) Linuxから自作IPをUIOで制御する
./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に上書きします。

./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でフォーマットしておきます。
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デバイスとして認識されているかを確認します。
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で制御する
以下のソースコードをtest.cとして作成します。UIO0/UIO1/UIO2のmmapするサイズは、Vivadoで設定したAddress Editorのアドレス空間のサイズに合わせたサイズを設定しています。
※(2021.12.12追記)ソースコードを修正しました。
# 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;
}
テストプログラムをコンパイルし、実行します。
うまく行けば、以下のように表示されるはずです!
gcc test.c
./a.out
Hello World
12 34 46
Pass
Successfully ran Hello World application
dev/memを用いた制御方法
参考記事でも紹介されているとおり、/dev/uio/の代わりにdev/memを用いた制御は意図せずメモリを破壊する可能性があるので危険ですが、簡単に行えるのでそれを試したソースコードも追記しておきます。
# 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;
}
— lp6m (@lp6m1) December 10, 2021
というわけで、UIOを使用して制御することでRISC-Vのリファレンス実装をPetalinuxから動作させることができました。




