lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

自作RISC-Vでスイッチサイエンスの測距ToFセンサー VL53L1X を動かす

2000円もせずに400cm測れる超優秀なセンサーモジュールがあります。

www.switch-science.com

4mまで測れるので単眼カメラの停止時の車間詰め補助に使おうと思っています。
ADAS搭載車両で、前方車両が発進したことを知らせる機能がある車両がありますが、大抵4m以上車両間隔が開くと通知されます。


チッ どいつもこいつもI2C使いやがって


というわけで自作RISC-VにI2Cモジュールを接続します。
といっても、私がI2Cモジュールを一生懸命1から作成するわけではなく、すでにintel Quartus Prime/Platform Designerに用意されているので、これを接続するだけです。
Avalon-MM Masterに対応するとこのようにモジュールの低レイヤを意識せず接続できるのでとても便利です。
CPU側から見ると、スレーブモジュールがSDRAMだろうとSPIメモリだろうと特定のアドレスに向けてLoad/Store命令を実行してデータのRWをしているだけです。

I2C HALの整備

NiosII用のHALを使用することはライセンス違反となる可能性があります。なので、取り急ぎHALを作りました。
Embedded Peripherals IP User Guideのp.170から、Figure 47~51,54を参考にすればさほど難しいモジュールではありません。

全体は以下を参照ください。
KyogenRV/qsys_i2c.c at master · panda5mt/KyogenRV · GitHub

VL53L1X API

STMicroからC API、PololuからArduino用のC++ APIが提供されています。Pololu提供のAPIが使いやすく思えましたので、C用に書き直しました。
全体は以下を参照ください。ライセンスはPololuをベースにしているため、3条項修正BSDライセンスに準拠しています。
KyogenRV/VL53L1X.c at master · panda5mt/KyogenRV · GitHub

動かしてみた

以下にmain.cを示します。

#include <stdio.h>
#include <stdint.h>
#include "krv_utils.h"
#include "xprintf.h"

#define USE_VL53L1X (1)
#define USE_SDRAM   (1)

#ifdef USE_VL53L1X
    #include "qsys_i2c.h"
    #include "VL53L1X.h"
#endif //USE_VL53L1X

#ifdef USE_SDRAM
int32_t sdram_test(void) {
    uint32_t
    data,length;

    length = SDRAM_0_END - SDRAM_0_BASE;

    xprintf("SDRAM write start\r\n");
    for (int k = 0 ; k < length ; k = k + 4) {
        put32(SDRAM_0_BASE + k, k);
    }

    xprintf("SDRAM read start\r\n");
    for (int k = 0 ; k < length ; k = k + 4) {
        data = get32(SDRAM_0_BASE + k);
        if(data != k) {
            xprintf("error fount at 0x%x: expecting %d but got %d\r\n",k,k,data);
            return -1;
        }
    }
    return 0;
}
#endif // USE_SDRAM

// main function
int32_t main(int argc, char *argv[]) {
    uint64_t i;
    xdev_out(&uart_putc);       // override xprintf

#ifdef USE_VL53L1X
    xprintf("I2C init\r\n");
    i2c_init(I2C_0_BASE);
    i2c_disable_isr(I2C_0_BASE);

    if(true == VL53L1X_init()) {
        xprintf("VL5351X init OK.\r\n");
        VL53L1X_setDistanceMode(VL53L1X_Long);
        VL53L1X_setMeasurementTimingBudget(50000);

        VL53L1X_startContinuous(50);
    } else {
        xprintf("VL5351X init failed.\r\n");
    }
#endif //USE_VL53L1X

uint32_t data = 0;

#ifdef USE_SDRAM
    if(0 == sdram_test()) {
        xprintf("SDRAM r/w test OK!\r\n");
    } else {
        xprintf("SDRAM r/w test fail......\r\n");
    }
#endif //USE_SDRAM

    xprintf("KyogenRV (RV32I) Start...\r\n");
    while(1){
        wait_ms(500);
        put32(PIO_0_BASE, 0x55);
        wait_ms(500);
        put32(PIO_0_BASE, 0xAA);

#ifdef USE_VL53L1X
        data = VL53L1X_read(true);
        xprintf("value = %d\r\n",data);
#endif //USE_VL53L1X

        i = get_time_ms() / 1000;
        xprintf("machine time = %llu second\r\n",i);
    }
    return 0;
}

define文の

#define USE_VL53L1X (1)
#define USE_SDRAM   (1)

はそれぞれ、VL53L1Xを使う場合、及びSDR SDRAMを接続する場合の設定です。
使わない場合はそれぞれ0にしてください。

動作風景

youtu.be

走行中の車を追跡する単眼カメラの実装(2.深度推定と夜間走行対応)

前回、パーティクルフィルタを用いた車両の追跡を簡易的に実装しました。
こちらをさらに展開していきます。

対象車両

この装置の対象は、4輪の軽自動車、超小型車を想定しています。また車両への後付けにも対応したいと思っています。
つまり、先進運転支援システム(ADAS)にあまりお金がかけられない車両を想定しています。
高級グレードですと、LIDAR、デュアルカメラ、ミリ波レーダから何種類かを組み合わせセンサーフュージョンを構築するはずです。
単眼カメラのみを利用している車種もあります。HONDAのFITなどです。

単眼カメラ単体の場合はカメラ自体の要求仕様がかなり厳しいと思われます。
ダイナミックレンジが広くなくてはいけません。例えば、晴天時も夜間時も等しく深度推定を行う必要があります。白飛びすると測定は困難です。

また、夜間時に前方の車がブレーキをかけると制動灯が強めに光ります。この時、ダイナミックレンジがせまいと、距離が正確に測れない場合があります。

いずれにしても、車高、ボディ、フロントガラス形状などで形が決まります。
つまり車両設計時に全てが決まってしまうため、あまり柔軟性がないと言えます。とくにデュアルカメラブレーキサポートシステムの場合、車体の剛性も決まってしまいます。

想定環境

今回は単眼カメラと、停車時の車間詰め用の補助として4,5メートルくらいを測定できる安価なToFセンサを想定しています。単眼カメラはダイナミックレンジはそれほど良くなくても動くよう設計しました。


イメージセンサはビデオ会議用の2〜3千円で変えるwebカメラを想定しています。
制御側も、高性能なGPUを搭載したものではなく、ローエンドFPGAを想定しています。

実装

対象物の特徴点の変化により、深度を推定するようにしました。
雑な説明をすると、物体が近づく程この特徴点は増えます。遠ざかるほど減ります。
もちろん、対象物の大きさはまちまちなので、MATLABにより大きさを推定するロジックを作りました。
大きさによる正規化をし、深度推定を行います。

youtu.be

ダイナミックレンジが狭い安価なカメラで夜間の深度推定に対応する

問題は夜間です。特にバイパスなどを走行していると、遠方が明るく見えます。遠近法で街灯が集中しているように見えるからです。以下の画像のような感じになります。カメラからみると実際には遠方にあるのに近く見える事故が起きます。


f:id:Lynx-EyED:20210212192819p:plain
ここでも、特徴量を物体の大きさで正規化することにより、この問題を回避しています。
深度推定がすこし雑になる部分がありますが、安全側に倒した深度推定になっていると思います。

youtu.be

次回はintel FPGAへ実装した自作RISCVへカメラデータをDMA転送するロジックの実装を行います。

ADASご相談【法人限定】

コメントにて、ADAS試作設計など窓口を開設して欲しいとの意見を複数頂戴しています。
取り急ぎとなり恐縮ですが、フォームを作成いたしました。法人のみですが、設計のご相談・イベント展示用から量産前設計までのコンサルティングを賜ります。お問合わせフォームからご連絡ください。

(お問合せフォームは大まかな位置情報・ブラウザ情報を取得しています。ご了承ください)
https://kyogens.wixsite.com/-site-1

自作RISC-V向けにCライブラリの整備やPlatformDesigner連携をする

前回の記事でCが使えるようになりました。ですが、標準入出力もない乗除算もできない不便なものでした。
組み込みマイコンレベルで使えるように整備していきます。

libgccをリンクする

RV32Iは算術演算命令セットとしては加減算とシフトくらいしかありません。
乗除算は上記機能を組み合わせることによって実装しますので、libgccをリンクする必要が生じます。
uint同士の乗算など一部例外はありますが、大抵の場合libgccを用意するか、必要とされるルーチンを自作しないとできないはずです。
電子計算機の授業じゃあるまい自作してたら時間が足りません。

参考:
stackoverflow.com

これを参考に以下のようにMakefileを修正しました。

COPS = -Wall -march=rv32i -mabi=ilp32 -O2 -nostartfiles -ffreestanding
COPS2 = -Wall -march=rv32i -mabi=ilp32 -O2 -nostartfiles -ffreestanding -Xlinker -T -Xlinker $(MEMMAP)


# -------------中略-------------

$(ASM_DIR)/main.o : $(ASM_DIR)/main.c
	$(RISCVGNU)-gcc $(COPS) -c $(ASM_DIR)/main.c -o $(ASM_DIR)/main.o

$(ASM_DIR)/blinker.elf : $(ASM_DIR)/linker.ld $(ASM_DIR)/utils.o $(ASM_DIR)/main.o
	$(RISCVGNU)-gcc $(COPS2) $(ASM_DIR)/utils.o  $(ASM_DIR)/main.o -lgcc -o $(ASM_DIR)/blinker.elf

修正したMakefile全体はこちらをご覧ください
github.com

まともなprintfを準備する

まとも= int型/float型の表示かつ32文字以上は一度にprintfできるもの、くらいに捉えてもらえれば。
ChaNさんのxprintfを使わせていただきました。移植まで5分かかりませんでした。
ありがとうございます。
移植のためにしたことは、

  • 上記ファイルのダウンロード、プロジェクトへのコピー
  • int64_t型をprintfすることが多いのでxprintf.hのXF_USE_LLIを1に変更
  • Makefileにxprintf.cの追加

使う際は、すでに似非printfを実装するのに使っていた1文字出力関数uart_putc()関数をオーバーライドするだけで済みます。

xdev_out(&uart_putc);

楽なうえに超軽量です。riscv32-unknown-elfでlong longを有効にしてもxprintfは1kB弱です。
なお、標準関数のsprintfを使うと10CL025の場合、On-Chip RAMの大半を食い尽くす現象が発生したので試していません。お前のためにOn-Chip RAM用意したわけじゃないんだよsprintf君。

サンプルコードは以下。
github.com

例外を実装する

アセンブラレベルではmtvecに例外番地を指定すると、何らかの例外発生時に指定番地に飛ぶように記述できます。なお、指定していない場合、今回作成したRISC-Vは0xDEAD番地にジャンプします。(SignalTapで観察したときにわかりやすいと言う理由でこのような実装にした)
スタートアップコードを見ていただければわかりますが、__expr()関数にジャンプするようにしています。ですのでCソース側で__expr()を作成すると、例外発生時に任意のコードを実行します。現在の実装では

// exception
void __expr(void) {
    xprintf("program exception....\r\n");
    xprintf("cpu stop.\r\n");
    while(1);
}

としています。適宜書き換えてください。全体のソースコードは以下から。
KyogenRV/krv_utils.c at 0.2.6 · panda5mt/KyogenRV · GitHub

  • 例外を発生させてみる

main.c 内でLoad Address Misaligned*1例外を発生させてみます。lw命令(4バイトロード命令)は32bitのアラインがあります。わざとアラインメントを跨ぐ命令を記述します。

int main(int argc, char *argv[]) {
    xdev_out(&uart_putc);       // override xprintf

   // (中略)

    xprintf("KyogenRV (RV32I) Start...\r\n");

    get32(0x01); // <- ミスアライン発生

    // .......

}

UARTから出力されるメッセージを見ます。


f:id:Lynx-EyED:20210129085730p:plain

PlatformDesignerで作成したペリフェラル番地をCヘッダに書き出す方法

NiosII/e /f用に作成されたAPIを他の自作Avalon-MMマスタのために利用することはHALのライセンス違反となりできません。
しかしPlatformDesignerで作成したペリフェラル番地をヘッダファイルとして取得し利用することは可能です。
下記のように実行すると、ヘッダファイルqsys_mem_map.hが得られます。

sopcinfo2swinfo.exe --input=${DESIGN_NAME}.sopcinfo
swinfo2header.exe --swinfo ${DESIGN_NAME}.swinfo --module ${Avalon-MMモジュール名} --master ${モジュールに複数のマスタがある場合、使用するマスタ名を指定} --single ${SWDIRW}/qsys_mem_map.h

このヘッダファイルをCのプロジェクトで使うことができます。

SDRAMコントローラの実装

Avalon-MMスレーブは、マスタ側がリード命令を発行した次サイクルでデータを用意します*2。ですがスレーブ側が有効なデータを準備できないときがあります。このときスレーブはマスタヘwaitrequestを発行しリード動作を待ってもらうのですが、タイミング次第で、発行されたwaitrequestが有効か否かがわからない状態でした。SDRAMではこの状況がCASレイテンシとの兼ね合いで多発します。この部分を修正し、無事SDRAMのリードライトが正しく動作するようになりました。

SDRAMのRWコードはこんなふうに書けます。上記のqsys_mem_map.hをインクルードしていることが前提となります。

// SDRAM test
int sdram_test(void) {
    uint32_t   data,length;

    length = SDRAM_0_END - SDRAM_0_BASE;

    xprintf("SDRAM write start\r\n");
    for (int k = 0 ; k < length ; k = k + 4){
        put32(SDRAM_0_BASE + k, k);
    }

    xprintf("SDRAM read start\r\n");
    for (int k = 0 ; k < length ; k = k + 4){
        data = get32(SDRAM_0_BASE + k);
        if(data != k) {
            xprintf("error fount at 0x%x: expecting %d but got %d\r\n",k,k,data);
            return -1;
        }
    }
    return 0;
}
    

プロジェクト全体のダウンロード

以下からCloneできます。

git clone http://github.com/panda5mt/KyogenRV -b 0.2.6 --depth 1 

github.com

*1:参照: https://riscv.org/wp-content/uploads/2017/05/riscv-privileged-v1.10.pdf p.35

*2:PlatformDesignerで指定したレイテンシに依存

Raspberry Pi Zero/3B/4BでOpenOCDを使う(GPIOによるBitbanging)

FPGAのSVF PlayerとしてOpenOCDを使う

ベンダに依存しないJTAGテストパターン形式としてSVFがあります。
www.xjtag.com
OpenOCDもこれに対応しているため、出先でconfigをしなければならない場合など、FPGAのメイン開発じゃない時はかなり重宝しています。
ターミナルとSSHが動く端末 + ラズパイがあればFPGAのコンフィグができます。

以下はintel FPGA Cyclone10LPの場合に実施した内容です。SVFファイルはすでに準備しているものとして進めます。

Xilinx/AMDは知りません。調べてとか言われてもしないからな。


Latticeについては一部のMACHシリーズにおいてTRSTピンがあります。
下記の

  • TRST=High, ENABLE=Lowにしてから以下の内容を実施する
  • TRSTをラズパイの任意のピンに接続し、ENABLE=Lowにしてから後述する設定ファイルに明記する

いずれかを実施する必要があります。


ラズパイのGPIO番号を確かめておく

GPIOを使うので、ピン番号とGPIOに振られている番号の対応をしなければなりません。

sudo apt install python3-gpiozero 
pinout

世代によって表示は変わりますが以下のような感じになります。

$ pinout
,--------------------------------.
| oooooooooooooooooooo J8     +====
| 1ooooooooooooooooooo        | USB
|                             +====
|      Pi Model ???V1.4          |
|      +----+                 +====
| |D|  |SoC |                 | USB
| |S|  |    |                 +====
| |I|  +----+                    |
|                   |C|     +======
|                   |S|     |   Net
| pwr        |HDMI| |I||A|  +======
`-| |--------|    |----|V|-------'

Revision           : d03114
SoC                : Unknown
RAM                : NoneMb
Storage            : MicroSD
USB ports          : 4 (excluding power)
Ethernet ports     : 1
Wi-fi              : False
Bluetooth          : False
Camera ports (CSI) : 1
Display ports (DSI): 1

J8:
   3V3  (1) (2)  5V    
 GPIO2  (3) (4)  5V    
 GPIO3  (5) (6)  GND   
 GPIO4  (7) (8)  GPIO14
   GND  (9) (10) GPIO15
GPIO17 (11) (12) GPIO18
GPIO27 (13) (14) GND   
GPIO22 (15) (16) GPIO23
   3V3 (17) (18) GPIO24
GPIO10 (19) (20) GND   
 GPIO9 (21) (22) GPIO25
GPIO11 (23) (24) GPIO8 
   GND (25) (26) GPIO7 
 GPIO0 (27) (28) GPIO1 
 GPIO5 (29) (30) GND   
 GPIO6 (31) (32) GPIO12
GPIO13 (33) (34) GND   
GPIO19 (35) (36) GPIO16
GPIO26 (37) (38) GPIO20
   GND (39) (40) GPIO21

For further information, please refer to https://pinout.xyz/

上記を参考にしながらどのGPIOにOpenOCDでコンフィグする際のどの信号を割り当てるかを決定します。

OpenOCDの導入と設定ファイルの準備

2021年1月現在、特に独自にビルドしなくてもBitbangingがつかえるようになっていました。

sudo apt install openocd

参考:
https://github.com/arduino/OpenOCD/blob/master/tcl/interface/raspberrypi-native.cfg

設定ファイルを用意します。
openocd.cfg

# Cyclone 10LP FPGA Module (10CL025YU256)
# https://lynxeyed.hatenablog.com/
# openocd -f openocd.cfg -c init -c "svf foobar.svf"
adapter driver bcm2835gpio

# raspi Zero:0x20000000
#bcm2835gpio_peripheral_base 0x20000000

# raspi 3B/3B+ :0x3F000000 
#bcm2835gpio_peripheral_base 0x3F000000

# raspi 4B:0x7E000000
bcm2835gpio_peripheral_base 0x7E000000

bcm2835gpio_speed_coeffs 113714 28

# Each of the JTAG lines need a gpio number set: 
# tck tms tdi tdo Header pin numbers: tck tms tdi tdo

bcm2835gpio_jtag_nums 26 19 21 20
#bcm2835gpio_trst_num 8 # <- Latticeの場合、これを設定する必要がある。

adapter speed 1000
transport select jtag
# IDCODE Information for Cyclone 10 LP Devices  
# 0000 0010 0000 1111 0011 000 0110 1110 1
jtag newtap 10cl025u tap -expected-id 0x020F30dd -irlen 10 

ラズパイの世代によってGPIOのベース番地が違う

bcm2835gpio_peripheral_baseがベース番地を設定する部分です。
使用する世代によってコメントアウトを適宜するなどして対応してください

  • raspi Zero:0x20000000
  • raspi 3B/3B+ :0x3F000000
  • raspi 4B:0x7E000000

他のラズバイを使用したことがないので、動作検証したのはこの3世代だけです。すみません。

TCK, TMS, TDI, TDOの対応

bcm2835gpio_jtag_numsに対応するGPIO番号(ラズパイの40pinアサインではなくGPIOの番号)を順番に書いていきます。
私の環境の場合、作成したFPGA基板が以下のように対応しています。
TCK = GPIO26
TMS = GPIO19
TDI = GPIO21
TDO = GPIO20
となっているので、

bcm2835gpio_jtag_nums 26 19 21 20

と書いています。
Latticeの一部のFPGAはTRSTピンが必要ですが、該当デバイスは、対応しているGPIO番号を設定し

bcm2835gpio_trst_num 8

のように書けます。

IDCODEの設定

これは10CL025Uの場合ですが、IDCODE = 0x020f30ddです。manufacturer idは0x06eとなってintelではなくAlteraと表示されるはずです。これはしゃーないね。

jtag newtap 10cl025u tap -expected-id 0x020F30dd -irlen 10 

FPGAをコンフィグする

先程の設定ファイルをopenocd.cfgと言う名前で任意の場所に保存します。ファイル名も場所も自由です。
foobar.svfをコンフィグするなら

sudo openocd -f openocd.cfg -c init -c "svf foobar.svf"

おしまい。

C言語で自作RISC-Vを動作させる

前回の記事でアセンブラでLチカ、Hello worldはできました。
今回はC言語で試してみました。
実用性を求めるとC記述でも動作することが重要ですが、なによりもハーバードアーキテクチャの場合、Load/Store動作のデバッグになると思います。関数にジャンプする時、main関数にreturnする時、容赦無くLoad/Storeの連発をするからです。

2,3個Load/store命令が連続しているようなコードでは動作(実際はしていないのだけれどもタイミングの功罪で動いているように見える)していても、数十回連続すると動作しない事態が起こり得ます。
Cでは容赦無くLoad/Store行うので、良い試験になります。

GCCの準備

いままでアセンブラのみ使っていたためまともな環境を構築していませんでした。
※ビルドに必要なパッケージ(automake autotools-dev curlなどなど)はすでに導入済みとします。

git clone https://github.com/riscv/riscv-gnu-toolchain
cd riscv-gnu-toolchain
./configure --prefix=/opt/riscv --with-arch=rv32i 
sudo make

ここで、/opt/の権限によりますがsudo makeしないと不可解なエラーになります。permission deniedって出ればわかりやすいのですが。
.bashrcなどでPATHを通したら完成です。

$ riscv32-unknown-elf-gcc -v
Using built-in specs.
COLLECT_GCC=riscv32-unknown-elf-gcc
COLLECT_LTO_WRAPPER=/opt/riscv/libexec/gcc/riscv32-unknown-elf/10.2.0/lto-wrapper
Target: riscv32-unknown-elf
Configured with: /mnt/c/riscv-gnu-toolchain/riscv-gcc/configure --target=riscv32-unknown-elf --prefix=/opt/riscv --disable-shared --disable-threads --enable-languages=c,c++ --with-system-zlib --enable-tls --with-newlib --with-sysroot=/opt/riscv/riscv32-unknown-elf --with-native-system-header-dir=/include --disable-libmudflap --disable-libssp --disable-libquadmath --disable-libgomp --disable-nls --disable-tm-clone-registry --src=.././riscv-gcc --disable-multilib --with-abi=ilp32 --with-arch=rv32i --with-tune=rocket 'CFLAGS_FOR_TARGET=-Os   -mcmodel=medlow' 'CXXFLAGS_FOR_TARGET=-Os   -mcmodel=medlow'
Thread model: single
Supported LTO compression algorithms: zlib
gcc version 10.2.0 (GCC) 

リンカ・スタートアップの準備

参考サイト。みつきん(id:mickey_happygolucky)さんのブログです。
mickey-happygolucky.hatenablog.com

ほぼそのままですが、.text領域と.bss領域がことなりますので、それを書き換え、stackの最終アドレスが0x7fff(ただし4-byte-alignment)になるようにスタートアップコードをなおしました。良記事でした。ありがとうございます。

linker.ld

OUTPUT_ARCH( "riscv" )
ENTRY(_start)

SECTIONS
{
  . = 0x00000000;
  .text.init : { *(.text.init) }
  .tohost : { *(.tohost) }
  .text : { *(.text) }
  .data : { *(.data) }
  .bss : { *(.bss) }
  _end = .;
}



---------ここから:2021年1月18日加筆修正---------
スタートアップコードは少し修正しています。今回作成したRV32IはPrivillaged ISAの規格通り、Machine-mode timer(mtime)を64bitで実装しています。
下位32bitをget_timel()、上位32bitをget_timeh()で取得できるようにしました。
utils.S

    .section .text.init;
    .global _start
_start:
    lui    sp, 0x08
    call main
    j .

    .global dummy
dummy:
    ret

    .global put32
put32:
    sw x11,(x10)
    ret

    .global get32
get32:
    lw x10,(x10)
    ret

    .global get_timel
get_timel:
    csrr    x10, time
    ret

    .global get_timeh
get_timeh:
    csrr    x10, timeh
    ret

Cを記述する

簡単なLチカコードです。こちらもみつきんさんのコードとあまり変わりませんが、タイマーmtimeを使用しています。
コードを記述する前にKyogenRVの現時点で最新のタグが付されているバージョンをcloneします。

git clone http://github.com/panda5mt/KyogenRV -b 0.2.6 --depth 1 
make sdk

make sdkVerilog生成、テストコードのhexファイル生成、今回のテスト用Cコードコンパイルを一括で行います。
Scala/chisel環境のない方は実行しない方がいいと思います。すでにコンパイル済みのhexファイルが消えるので。
後述しますが、今回のCプロジェクトをコンパイルするだけの場合はmake c_allを実行します。

KyogenRVプロジェクトルート/src/sw/main.cを記述します。
github.com

void put32(unsigned int, unsigned int);
unsigned int get32(unsigned int);
unsigned int get_timel(void);
unsigned int get_timeh(void);

void dummy (void);

#define GPIO_BASE       0x8000
#define XTAL_FREQ_KHZ   60000

// wait msec counter
void wait_ms(unsigned int msec){
    volatile unsigned int oldtime;
    oldtime = get_timel();
    while((get_timel()-oldtime) < XTAL_FREQ_KHZ * msec); //1msec
}


// main function
int main(int argc, char *argv[]) {
    while (1) {
        wait_ms(500);
        put32(GPIO_BASE, 0x55);

        wait_ms(500);
        put32(GPIO_BASE, 0xAA);
    }
    return 0;
}

標準IOもなにもインクルードしていない状態です。これで動作します。
プロジェクトのルートに戻り、下記を実行します

make c_all

KyogenRVプロジェクトルート/src/sw/blinker.hexが生成されたhexです。
Quartus Primeで使用するインテルhexを生成する場合は
プロジェクトのルートに戻り、下記を実行します

./mk_intel_hex.py

KyogenRVプロジェクトルート/fpga/chisel_generated/blinker_intel.hexが生成されたインテルhexです。
前回のようにPlatformDesignerでon-chip ramの初期値に設定し、コンパイルすることでFPGA上で動作します。

動作風景
youtu.be

アセンブル

CPUデバッグ時にはアセンブラでみた方が良いと思います。

make c_reverse

で逆アセンブルできます。
どのようなコードになっているか見ることができます。

セクション .text.init の逆アセンブル:

00000000 <_start>:
   0:	00008137          	lui	sp,0x8
   4:	0a8000ef          	jal	ra,ac <main>
   8:	0000006f          	j	8 <_start+0x8>

0000000c <dummy>:
   c:	00008067          	ret

00000010 <put32>:
  10:	00b52023          	sw	a1,0(a0)
  14:	00008067          	ret

00000018 <get32>:
  18:	00052503          	lw	a0,0(a0)
  1c:	00008067          	ret

00000020 <get_timel>:
  20:	c0102573          	rdtime	a0
  24:	00008067          	ret

00000028 <get_timeh>:
  28:	c8102573          	rdtimeh	a0
  2c:	00008067          	ret

セクション .text の逆アセンブル:

00000030 <wait_ms>:
  30:	fd010113          	addi	sp,sp,-48 # 7fd0 <_end+0x7ee0>
  34:	02112623          	sw	ra,44(sp)
  38:	02812423          	sw	s0,40(sp)
  3c:	03010413          	addi	s0,sp,48
  40:	fca42e23          	sw	a0,-36(s0)
  44:	fddff0ef          	jal	ra,20 <get_timel>
  48:	00050793          	mv	a5,a0
  4c:	fef42623          	sw	a5,-20(s0)
  50:	00000013          	nop
  54:	fcdff0ef          	jal	ra,20 <get_timel>
  58:	00050713          	mv	a4,a0
  5c:	fec42783          	lw	a5,-20(s0)
  60:	40f706b3          	sub	a3,a4,a5
  64:	fdc42703          	lw	a4,-36(s0)
  68:	00070793          	mv	a5,a4
  6c:	00579793          	slli	a5,a5,0x5
  70:	40e787b3          	sub	a5,a5,a4
  74:	00279793          	slli	a5,a5,0x2
  78:	00e787b3          	add	a5,a5,a4
  7c:	00479713          	slli	a4,a5,0x4
  80:	40f70733          	sub	a4,a4,a5
  84:	00571793          	slli	a5,a4,0x5
  88:	00078713          	mv	a4,a5
  8c:	00070793          	mv	a5,a4
  90:	fcf6e2e3          	bltu	a3,a5,54 <wait_ms+0x24>
  94:	00000013          	nop
  98:	00000013          	nop
  9c:	02c12083          	lw	ra,44(sp)
  a0:	02812403          	lw	s0,40(sp)
  a4:	03010113          	addi	sp,sp,48
  a8:	00008067          	ret

000000ac <main>:
  ac:	fe010113          	addi	sp,sp,-32
  b0:	00112e23          	sw	ra,28(sp)
  b4:	00812c23          	sw	s0,24(sp)
  b8:	02010413          	addi	s0,sp,32
  bc:	fea42623          	sw	a0,-20(s0)
  c0:	feb42423          	sw	a1,-24(s0)
  c4:	1f400513          	li	a0,500
  c8:	f69ff0ef          	jal	ra,30 <wait_ms>
  cc:	05500593          	li	a1,85
  d0:	00008537          	lui	a0,0x8
  d4:	f3dff0ef          	jal	ra,10 <put32>
  d8:	1f400513          	li	a0,500
  dc:	f55ff0ef          	jal	ra,30 <wait_ms>
  e0:	0aa00593          	li	a1,170
  e4:	00008537          	lui	a0,0x8
  e8:	f29ff0ef          	jal	ra,10 <put32>
  ec:	fd9ff06f          	j	c4 <main+0x18>

---------ここまで:2021年1月18日加筆修正---------

この分量であれば、アセンブラでも追いかけられるレベルです。
SignalTap片手に波形を追いかけながらCPUデバッグをしました。

問題点:riscv-testsがChiselシミュレーション上で失敗する

今回の修正でchisel-iotester上ではriscv-testsが失敗するようになってしまいました。
もちろんCyclone10LP FPGA上では正しく動作します。

理由(だけ)は簡単です。メモリやメモリマップ上のAvalon-MMスレーブの挙動をChiselで完全にはシミュレートできていないからです。
スレーブデバイスのwaitrequest含めたAvalon-MMの挙動をChiselで完全シミュレートする価値があるかというと、目的と手段の入れ替わりが生じているように思えます。今のところその意義を感じていません。そんなことするのであれば一旦Verilogを生成してQuartusでBFMテストに流すべきと思います。

対処としては、下記のいずれか選択をすると思われます

  • riscv-testsのChisel上でのシミュレーションは諦める(エラーのまま放置)。
  • シミュレーションはパスできるようにする:AvalonMMに対応したバスブリッジを別ファイルで用意し、シミュレーション時は当該ファイルを除外、HDL生成時は含めて生成する

後者の方がスマートですね。あとはやる気の問題かな。

Intel FPGA Cyclone 10LPで自作RISC-Vを動作させた話

この記事を執筆した段階ではアセンブラのみ動作が確認できていました。その後C言語での動作確認も終えています。詳しくはこちらをご覧ください。
自作RISC-V向けにCライブラリの整備やPlatformDesigner連携をする - lynxeyedの電音鍵盤

この記事はQuartus Primeサイドから見たブログ記事にしたいと思います。
CPUを設計する、というお題は私自身16bit/32bitともにあるので*1、ちょうど良いレベルの難易度だったかなと思います。
奇しくも今年はRISC-Vは10周年だそうで。
riscv.org

ターゲットはintel FPGA Cyclone 10LPです。
github.com

git clone http://github.com/panda5mt/KyogenRV -b 0.2.6 --depth 1 

プロジェクトルート/fpga/がQuartus Primeプロジェクトになります。Quartus Prime Lite 20.1.1で動作確認。


前回のブログで紹介したあと、いろいろ反響がありました。(ブログアクセス数も1日で450人とか)
使ってくれた方もいました。




Avalon-MMつかいの魔術師がいじるとこうなる
CPUは魔改造されてからが本番。

CPU諸元

  • ISA
    • User-Level ISA Version 2.2
    • Privileged ISA Version 1.11 (マシンモード)
  • 割り込み
    • 外部割り込みのみ
  • 実行形式
    • キャッシュなし、5段パイプライン(フェッチ/デコード/実行/メモリアクセス/レジスタライトバック)、インオーダ実行方式
  • 最高周波数
    • 70MHz ~ 100MHz(intelFPGAのグレードによる)
  • 開発に使った言語
    • Chisel v.3.4
  • 占有ロジックエレメント数
    • 6000LE ~ 12000LEほど(命令メモリ容量や接続モジュールにより変動します)


RV32IのISAに準拠、マシンモードのみ(=スーパーバイザ非対応)ですが、仕様通りの例外(ecallなど)にも対応しています。
割り込みは外部割り込みのみ暫定対応。(タイマー割り込みはメモリマップドモジュールタイマーを設け、トリガーを外部割り込みへ接続し使うようにしてください)

Platform Designerで接続

昨今ではよほど小さなプロジェクトでない限りintel FPGAで動作する=Avalonバス接続可能を意味するレベルでメジャーになってきたと思います。
以下のいずれかの方法でプログラムメモリを接続します。

  • 速度パフォーマンスが少し良い方法

命令バス、データバスは独立なので、命令読み込みウェイトがかかる時間が圧倒的に少ない。
今回作ったRV32Iはキャッシュを持たない設計にしたのでこの接続だと処理速度は向上する。デュアルポートRAMなので使用ロジックが若干増える。


f:id:Lynx-EyED:20201218170635p:plain

命令バスが、データバスとシェアする方法。命令読み込みウェイトがたびたびかかる。
データバスがメモリ空間に頻繁にアクセスするプログラムだと、上記の接続方法より処理速度が1/2以下になることもある。でも使用ロジックは上記より少ない。
今回の設計では優先度をデータメモリ > 命令メモリとする。メモリアクセス頻度はその優先度の逆なのでBack Pressureで処理してくれる。


f:id:Lynx-EyED:20201218170656p:plain


ま、でもそんなに変わんないと思う。組み込みCPUの用途でかんがえると永延とデータメモリにアクセスするのみ or 永延とCPU内部で演算しかしないといった極端な動作はあまり考えられないので。RTOS載せるのであればその段階でパフォーマンスの考慮してもいいかもしれない。

  • Avalon-MMになんでこだわるのか

一言で言うならば、ペリフェラルまで作りたくないから。Platform Designerで思いつく限りのマイコンとしてのペリフェラルが用意されている。
そのためこちら側はペリフェラルの詳細ロジックを調べることなく、バス上にペリフェラルを繋げていけば良い。楽。
新たにペリフェラルを作る必要が生じても、規格に則って作れば良い。

CPU(マスタ)とペリフェラル(スレーブ)両方の検証とテストとか、恐ろしくてできない。

確かにRISC-V自作界隈、メモリやUARTやGPIOまで自作しているのが散見されます。
あるいはAXIの独自サブセットをつくってやはりペリフェラルまで自分で用意している。それも非難できない側面もあります。
AXIがあまりFPGAフレンドリな仕様をしていないうえ、Xのツールであるビなんとかが自由度高すぎるために設定が大変。(大変だった。キラキラGUIに騙される。もうやりたくない)

Avalon-MMはFPGAのバスとして(同期回路を接続する前提のバスとして)開発されていて、マスタが複数ある場合のアンチグリッドも自動でしてくれるので楽。AXIで疲弊するなら乗り換えも検討するのもいいと思います。Intelに。Cyclone10LP安いし。インテル入ってるって言えるし (古い

サンプルプログラム

  • Lチカ

github.com
Platform Designerで0x8000番地に8bit幅のPIOを設置した場合の前提になります。
約0.5秒間隔(CPUが70MHz動作時)で0x55と0xAAを書き込むものです。LEDが接続されているとBlinkが確認できるはず。
ビルドには以下のコマンドをプロジェクトルートフォルダで実行します。(riscv-toolchainとpython3.7以上が必要です)

./build_asm.py
./mk_intel_hex.py

ビルドするとプロジェクトルートディレクトリ/fpga/chisel_generatedインテルHex形式でバイナリが生成されるので、Platform DesignerでOn-Chip Memoryの初期値に設定しビルドすると動作します。

動作状況:
youtu.be

github.com
UARTでHello worldするサンプルです。9600baud,8N1で送信します。UARTはPlatform Designerで0x8020番地に設置します。
ビルドには以下のコマンドをプロジェクトルートフォルダで実行します。(riscv-toolchainとpython3.7以上が必要です)

./build_asm.py
./mk_intel_hex.py

ビルドするとプロジェクトルートディレクトリ/fpga/chisel_generatedインテルHex形式でバイナリが生成されるので、Platform DesignerでOn-Chip Memoryの初期値に設定しビルドすると動作します。
動作風景:Raspberry Pi ZeroをOpenOCDドングルかつUSB-UARTの代用にしています。OpenOCD経由でQuartus Primeで生成させたsvfを書き込み、UARTにcuでコネクトしています。
youtu.be


プロジェクトルートディレクトリ/src/sw/に作成した*.sファイルを置きます。命令メモリ開始アドレスは0x0000としてください。

ビルドには以下のコマンドをプロジェクトルートフォルダで実行します。(riscv-toolchainとpython3.7以上が必要です)

./build_asm.py
./mk_intel_hex.py

ビルドするとプロジェクトルートディレクトリ/fpga/chisel_generatedインテルHex形式でバイナリが生成されるので、Platform DesignerでOn-Chip Memoryの初期値に設定しビルドすると動作するはずです。

これから

以下をやっていく予定です。
個人的にも結構急ぎで必要なのでスピード上げてやっていくつもりです。

  • C言語開発のためのリンカスクリプト等、環境用意
  • OpenOCDなどでフックできるようなデバッグ回路のコア接続
  • 割り込み要因を増やす
  • 7段パイプラインにしたRV64Iを作る(CSRなし、独自例外、独自割り込み)

おわり。

*1:某大手印刷会社でオフセット印刷のコントローラとして後継機が動いています

Chiselを使ったRISC-Vの勉強(最終話:FPGAへの実装2)

今月中にQuartus Primeユーザー向けにもまとめを書きます。ご期待いただければ。

この記事はChiselサイドから見たブログ記事にしたいと思います。

Alternative HDLとしてそこそこ使えるじゃん!という手応えを感じたのとriscv-testsにFPGA実機上でもパスできるようになったので、ここでお勉強は一旦おしまいにしたいと思います。ここから先は、CPUコアへのデバッグロジックの接続、Cコンパイラ向けリンカスクリプトの記述、とChiselではない話になっていきます。

もちろんこれからChiselはかなりの頻度でネタとして取り上げると思います。(現時点で車載LKAS画像認識エンジンの記述に使っています。)


今回ターゲットはintel FPGA Cyclone 10LPです。
RV32IのISAに準拠、マシンモードのみ(=スーパーバイザ非対応)ですが、仕様通りの例外にも対応しています。
割り込みは外部割り込みのみ対応。(タイマー割り込みはメモリマップドタイマーモジュールを設け、トリガーを外部割り込みへ接続し使うようにしてください)

ふつーの32bit組み込みマイコンです。
github.com

git clone http://github.com/panda5mt/KyogenRV -b 0.2.6 --depth 1 

プログラム例

徳ではなくLチカ基板を積むのが組み込みエンジニア道らしいのでBlink LEDしてみます。PIOにLEDを接続するものとして、PIOのアドレスを0x8000番地、8bit幅と想定し記述します。GPIOに約0.5secごとに0x55,0xAAを交互に記述します。動作周波数は70MHzとします。
このCPUはmtimeを64bitで実装しています。今回は下位32bitだけを参照します。mtimeはクロックに同期してカウントアップします。(kyogenrv-root-directory)/src/sw/test.sに以下を記述します。

        nop
        lui     x1, 0x08        # x1 = 0x8000
_loop0:
        csrr    x2, time        # x2 = time (oldtime)
        li      x3, 0xAA        # x3 = 0xAA
        sw      x3, 0(x1)       # dmem[x1] = x3 = 0xAA
_loop1:
        csrr    x4, time        # x4 = time (nowtime)
        sub     x5, x4, x2      # x5 = x4 - x2
        lui     x6, 0x2160      # x6 = 0x2160_000
        bltu    x5, x6, _loop1  # if(x5 < x6) then loop
_loop2:
        csrr    x2, time        # x2 = time (oldtime)
        li      x3, 0x55        # x3 = 0x55
        sw      x3, 0(x1)       # dmem[x1] = x3 = 0x55
_loop3:
        csrr    x4, time        # x4 = time (nowtime)
        sub     x5, x4, x2      # x5 = x4 - x2
        lui     x6, 0x2160      # x6 = 0x2160_000
        bltu    x5, x6, _loop3  # if(x5 < x6) then loop
        jal     x0, _loop0


記述したら

make test #ここでアセンブルとシミュレーションが行われる
./mk_intel_hex.py

します。python3.7以上が必要です。(kyogenrv-root-directory)/fpga/chisel_generated/test_intel.hexが生成hexです。
(kyogenrv-root-directory)/fpga/がQuartus Primeのフォルダです。Quartus Prime Lite 20.1.1で動作確認しています。
Platform Designer上で、On-chip MemoryのMemory Initializationの項目で先程生成したhexファイルを指定し、Generate HDL-> Finish ->全体のコンパイルをします。


f:id:Lynx-EyED:20201213141614p:plain

書き込むと、こんな感じ。
youtu.be


以下はこれを設計する際に行ったことと感想です。

Chiselの学習をしてみて

学習コストがすごく高い。座学と簡単な例題解いていても永遠に終わらない気がしたのでRV32Iを設計しながら学習を進めました。
このブログのChisel3のタグで見ていただければ何をやっていたかは把握できると思います。
コストが高いものの、Chisel-iotester*1で実機を使わずとも、バイナリを読み込ませてCPUとして実行した時の挙動が詳細に追えました。

また、Bundleを使うことによって信号線を整理し、再利用がしやすい環境なのも良い点です。メモリ、バス、CPU本体、デバッグ用信号を分けて開発できました。加えて、高位言語ではないため合成された結果がまるで理解不能、ということもありませんでした。(そこまで大した物を作っていない、ということもありますが)

  • みんなFPGA使うならメモリはSyncReadMemつかおうね

Chiselそのものが悪いわけではありませんが。同期読み込みではレイテンシが1クロック以上あるはずです。これを前提にしていないRISCV/Chisel教本,ブログがありすぎました。ほんとダメ。ASIC用途ならそれでいいのか知りませんが(多分非同期多すぎてもダメだと思うんだけどどうなのかな)、ASIC起こすユーザそんなにいるのかな。。。非同期メモリはたとえFPGAベンダでサポートしているとしてもロジックの使用率の上昇、Fmaxの低下、期待しないビヘイビアの原因になるので、極力避けたい。

命令メモリ、データメモリ、汎用レジスタを全てMemで記述してる例が多く、ドツボにハマりました。
FPGA実装段階で5段パイプラインを1から作り直しました。
今更なのですが、WBステージで格納した汎用レジスタをEXステージで利用する場合、これが原因で取りこぼしてしまうため、該当する場合のみストールするようにしました。今度一から作り直す機会があったら6段パイプラインにしよう。

Avalon-MMスレーブはかなり作っていたのですが、マスタ(しかも命令+データのダブルマスタ)はほぼ初めてでした。かなり難儀しました。
ごくごく普通のパイプラインかつレイテンシが1(Avalon MM自体はレイテンシ変更可能)のメモリとして考えると、ああなんだ、そんなに難しくないじゃん。となります。あと、waitrequestが発生したら問答無用でCPU側(Master側)のステートマシンは全停止させる。ピクリとも動かさない。というのを徹底するとすんなり動きます。
いつもは読めるが、稀に記録もした覚えがない変なデータが読める(1 or 2ビットだけ異なるというのがよくありました)、違うアドレスに書き込んだ値が読める、しかも再現不能という場合はwaitrequestが来ているにもかかわらず、マスタを動かしている可能性があるかもしれません。

さいごに

Cyclone 10LPはイイゾ。

*1:これがなかったらChiselの価値ない気がします。なかったら勉強しませんでした