自作RISC-V向けにCライブラリの整備やPlatformDesigner連携をする
前回の記事でCが使えるようになりました。ですが、標準入出力もない乗除算もできない不便なものでした。
組み込みマイコンレベルで使えるように整備していきます。
libgccをリンクする
RV32Iは算術演算命令セットとしては加減算とシフトくらいしかありません。
乗除算は上記機能を組み合わせることによって実装しますので、libgccをリンクする必要が生じます。
uint同士の乗算など一部例外はありますが、大抵の場合libgccを用意するか、必要とされるルーチンを自作しないとできないはずです。
電子計算機の授業じゃあるまい自作してたら時間が足りません。
これを参考に以下のように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から出力されるメッセージを見ます。
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
*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 sdk
でVerilog生成、テストコードの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
逆アセンブル
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人とか)
使ってくれた方もいました。
とりあえずCYC1000でKyogenRVうごいた。ピン定義設定して書き込んでも動かないのでなんでだ?と思ったら、メモリ初期化パスが絶対パスで書かれるからプログラムが書き込まれてなかったらしい。 pic.twitter.com/peHXwpJbHA
— Kenta IDA (@ciniml) 2020年12月13日
Avalon-MMつかいの魔術師がいじるとこうなる
ちなみに全部作り直すの面倒だったので、手元にあったNiosIIのプロジェクトにRV32を追加してヘテロマルチコアにしてあります https://t.co/OM8Oba8xaS
— 長船 俊@NicoTECH自造社団 (@s_osafune) 2020年12月13日
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なので使用ロジックが若干増える。
- 消費論理回路が少し減る方法
命令バスが、データバスとシェアする方法。命令読み込みウェイトがたびたびかかる。
データバスがメモリ空間に頻繁にアクセスするプログラムだと、上記の接続方法より処理速度が1/2以下になることもある。でも使用ロジックは上記より少ない。
今回の設計では優先度をデータメモリ > 命令メモリとする。メモリアクセス頻度はその優先度の逆なのでBack Pressureで処理してくれる。
ま、でもそんなに変わんないと思う。組み込み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の初期値に設定しビルドすると動作するはずです。
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 ->全体のコンパイルをします。
書き込むと、こんな感じ。
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 Master
Avalon-MMスレーブはかなり作っていたのですが、マスタ(しかも命令+データのダブルマスタ)はほぼ初めてでした。かなり難儀しました。
ごくごく普通のパイプラインかつレイテンシが1(Avalon MM自体はレイテンシ変更可能)のメモリとして考えると、ああなんだ、そんなに難しくないじゃん。となります。あと、waitrequestが発生したら問答無用でCPU側(Master側)のステートマシンは全停止させる。ピクリとも動かさない。というのを徹底するとすんなり動きます。
いつもは読めるが、稀に記録もした覚えがない変なデータが読める(1 or 2ビットだけ異なるというのがよくありました)、違うアドレスに書き込んだ値が読める、しかも再現不能という場合はwaitrequestが来ているにもかかわらず、マスタを動かしている可能性があるかもしれません。
さいごに
Cyclone 10LPはイイゾ。
*1:これがなかったらChiselの価値ない気がします。なかったら勉強しませんでした
Chiselを使ったRISC-Vの勉強(14.Intel HEXの生成)
Intel Hexファイルの生成
Intel FPGAで初期値をRAMに与えたい場合、インテルHEXかMIFファイルにしておく必要があります。
https://www.intel.co.jp/content/dam/altera-www/global/ja_JP/pdfs/literature/ug/ug_ram_rom_j.pdf
今後、OpenOCDでデバッグできるIFを実装する予定もありMIFではなくインテルHEXを選択しました。
lynxeyed.hatenablog.com
今までChiselプロジェクトには独自形式のHEXファイルを使用してきました。チェックサムもアドレスもない、0x0000番地から始まることかつ32bit長が前提のものです。文字列処理が煩雑にならないので便利でしたが、ここでインテル形式も生成できるようにしました。
インテルHex形式の規格
Wikipediaにあります。これ以上分かりやすい解説がなかったので。
en.wikipedia.org
今まで生成していたHexファイルにスタートバイト、バイトカウント、アドレス、レコードタイプ、チェックサムを追加するのが楽そうです。
ここはshとかMakefileで書ければ良かったのですが、ちょっとめんどくさかったので、pythonで書きました。10~20分もあればできてしまうような内容です。
僕は半日かかりました。天才なので。
コードの主要な部分です。
アドレスをインクリメントして、16進数で表記、チェックサムを間違えなければ難しいところはありません。
ex_start_code = ':' hex_byte_count = '04' hex_start_addr = 0x0000 hex_record_type = '00' hex_end_of_file = ':00000001FF' line = fr.readline() while line: hexdata = line.strip() li_hexdata = conv_hexlist(split_n(hexdata,2)) # read hexdata -> list li_hexaddr = conv_hexlist(split_n(format(hex_start_addr,'04x'),2)) checksum = (0 - (int(hex_byte_count) + sum(li_hexaddr) + int(hex_record_type) + sum(li_hexdata))) & 0xff wr_hex = hex_start_code+hex_byte_count+format(hex_start_addr,'04x')+hex_record_type+hexdata+format(checksum,'02x')+'\n' fw.write(wr_hex) hex_start_addr = hex_start_addr + 1 #increment address line = fr.readline() fw.write(hex_end_of_file+'\n') fw.close fr.close
適当。
KyogenRVのプロジェクトトップで下記を実行します。
make test #ここで今まで通りのHEXファイル生成 ./mk_intel_hex.py #上記HEXファイルを読み込み、インテルHEXファイルを生成
test_intel.hex
というインテルHEX形式ファイルが、fpga/chisel_generated/
に生成されます。
コード全文
高度運転支援向け単眼カメラの実装(1.MATLABでパーティクルフィルタを実装し評価してみる)
パーティクルフィルタの詳しい内容は扱いません。OpenCVのチュートリアルなどに詳しく扱われています。
例題として、特定の色のカラーボールの追跡はよく扱われます。
以下の手順をとることが多いでしょう。
- 入力画像をHSV変換→ 一定の輝度を持っているピクセルから色相を取り出し → 特徴点
- パーティクルフィルタをばら撒く
- 各パーティクル上、または一定の距離以内存在する特徴点の数*1 → 尤度の決定
- 尤度の高い座標にカラーボールが存在すると判定
- 4.で得られた座標を考慮しつつ、カラーボールが次の状態でどこに向かうか推定する(リサンプリング)
- 3.へ
今回やってみた内容もそれほど変わりませんが、少し工夫をしています。
特徴点の定義
グリーン背景などで、カラーボールを動かす場合は一定の輝度のときの色相を取り出して判定し、特徴点とすることが多いようです。この場合はこの方法はとても有用です(室内の光が少々変わっても追いかけられる)
しかし、車の場合は様々な色をしており、とりまく環境光は絶えず変化しています。加えて昼夜の違いによる大きな輝度変化などがあります。
MATLABでさまざまな画像を入力して、何をパラメータとするとよいか調べたところ、
輝度勾配と色相勾配の内積
が今のところ有用ではないかとみています。
具体的には画像から5x5ピクセル画像を走査し
それぞれの内積を考慮すると道路と車の区別がつきやすそう、という判断をしました。
これについてはもう少し調査が必要です。現在試験運用中ですが、かなり課題が見えてきています。追加のパラメータが必要だと思います。
パーティクルフィルタの実装
演算量が少ないのは良い事です。
車のドラレコ映像を例にあげます。
衝突回避の目的であれば、この場合必要なデータは
このように考えれば、入力映像のほぼ上半分は演算から除外することができます。
入力される映像は1000x1000ピクセルを想定しています。
特徴点を5x5で演算するので、パーティクルフィルタの演算に必要な領域は1000/5 x 100/5 = 200 x 200ピクセルです。
パーティクルフィルタの行列を以下のように定めます。
PM(x座標, y座標, 重み)
これが、パーティクル数Pだけ増えます。今回のアプローチではPは200~500くらいが良いかと思われます。あまり多いと演算に時間がかかりすぎます。少なすぎると追跡ができません。
PM(x, y, w, P)
ここではいきなりMATLABコードを出します。
別言語で実装を考えている方にもある程度判断がつくようにコメントしてます。私も最終的にSystemVerilog + C/C++での実装が必要なので。
パーティクルの初期化
% パーテクルフィルタ初期化 % xは1~(200-20)までのレンジ、yは画像下半分(100 ~ 200-20)までのレンジ % 中央の周辺にまずパーティクルを集めてばら撒く function PM = pm_init(PM,P) for m=1:1:P PM(1,1,m) = 100 + randi([-70 70]); PM(1,2,m) = 100 + randi([0 80]); end PM(1,3,P) = 1; end
先ほど述べたように、
パーティクルフィルタの動き回れる領域は200x200です。y座標だけはパーティクル フィルタの動き回れる領域を下半分にしています。(なので実質200x100)
尤度の計算
ランダムにおかれたパーティクルを左上端として、20x20ピクセルを走査し、合計値を格納します。行列Xaは特徴点が格納された200x200の行列です。
% 尤度計算 function PM = likelihood(PM,P,Xa) for m = 1:1:P x = PM(1,1,m); y = PM(1,2,m); A = sum(Xa(y:y+19, x:x+19),'all'); PM(1,3,m) = A; end end
リサンプリング
今回は単純ランダムサンプリングという手法を用います。パーティクルフィルタの重みの累積和を求めて格納し、重みに応じて乱数を生成し、次のサンプリング座標を決定します。重みが小さい=成績の悪いフィルタは、良いものに置き換えられます。
% リサンプリング function [last_w, new_PM] = resample(PM,P) new_PM = zeros(1,4,P); %新しい配列の作成 weights = cumsum(PM(1,3,:)); %重みの累積和 last_w = weights(length(weights)); if last_w == 0 last_w = 1; end for n = 1:1:P w = randi(last_w); for lp = 1:1:length(weights) if w < weights(lp) break; end end %disp(lp); % lp => index new_PM(1,3,n) = PM(1,3,lp); % ウェイトをコピー new_PM(1,1,n) = PM(1,1,lp); % xをコピー new_PM(1,2,n) = PM(1,2,lp); % yをコピー end end
期待値の計算
期待値の座標から推定して黄色い四角を描きます。
元の入力画像に書き戻す関係で、x,y比率を5倍にしています。
function picRGB = pm_draw(PM, P, picRGB) weight = sum(PM(1,3,:)); x = 0; y = 0; for m=1:1:P x = x + (PM(1,1,m) * PM(1,3,m)); y = y + (PM(1,2,m) * PM(1,3,m)); end cX = (x * 5 / weight); cY = (y * 5 / weight); picRGB = insertShape(picRGB,'Rectangle',[cX cY 100 100],'LineWidth',5); %黄色の四角を描画 end
次の状態の予測
トラッキングしたパーティクルフィルタが次にどこへ移動するかを予測します。ここでパーティクルが動ける領域を可変させると、より良い追跡が可能かもしれません。
function PM = pm_predict(PM,P) for m=1:1:P x = PM(1,1,m) + randi([-10 10]); if x < 1 % x座標の稼働領域を指定 x = 1; elseif x > (200-20) x = 200 - 20; end y = PM(1,2,m) + randi([-10 10]); if y < 100 % y座標の稼働領域を指定 y = 100; elseif y > (200-20) y = 200 - 20; end PM(1,1,m) = x; PM(1,2,m) = y; end end
作ってみた
重みの合計値などから、特徴点が異様に少ない時を判別できると思います。この時は全体的に暗めな場合が多いです。
もちろん特徴点が本当にない時と区別をつける必要があります。この辺りは工夫して実装すると面白いと思います。
youtu.be
参考
*1:あくまで例です。なにをもって尤度とするかは実装に依ります