LoginSignup
14
7

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-11

第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

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

Screenshot from 2021-12-11 01-41-01.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を使用しています。
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)

Screenshot from 2021-12-11 01-45-19.png
- Image Packing Configurationの変更
Root filesystem typeをEXT4に変更
Screenshot from 2021-12-11 01-44-38.png
- rootfsファイルシステムの変更
Root filesystem formatの末尾にext4を追加
Screenshot from 2021-12-11 01-45-48.png

  • MACHINE_NAMEの変更

Ultra96用の設定?コンポーネント?が反映されるらしい。なぜかrev1指定らしい。実際に使用しているのはrev2ですが・・
Screenshot from 2021-12-11 01-46-46.png

次に、USB-LANを使うためにカーネルコンフィグを修正します。今回はwifiは使用できるようにしていません。使用しているUSB-LANアダプタはUSB-LAN1000Rです。

petalinux-config -c kernel

Realtek RTL8152 Based USB Ethernet Adaptersを有効化
Screenshot_from_2021-12-09_01-21-35.png
あと、必須ではありませんが、CPUの省電力化設定を無効化しておきます。
145076680-eedab3cf-d90f-40c0-a41a-09c1d8b7ee75.png
145076683-790130f2-9ec4-442a-8fa6-2b1bd954385b.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を実行して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.dtsigeneric-uioに上書きします。
Screenshot from 2021-12-11 02-00-51.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でフォーマットしておきます。

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のアドレス空間のサイズに合わせたサイズを設定しています。

Screenshot from 2021-12-12 01-04-53.png

※(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;
}

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

14
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
7