lynxeyedの電音鍵盤

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

MATLABでCIFAR-10のファイル読み込み(だけ)してみる

中身のない内容なんだけれども、いざ使う時に忘れるので覚書きです。
物体認識用のデータ・セットに使われるThe CIFAR-10 datasetをMATLABで表示する方法です。

www.cs.toronto.edu

準備

  1. MATLAB用のデータセット「CIFAR-10 Matlab version」をダウンロードし、展開
  2. cifar-10-batches-matというフォルダが解凍される
  3. フォルダごとMATLAB作業ディレクトリにコピー

中身

  • cifar-10-batches-mat

各データセットは32x32の画像が10000点含まれています。

読み込みと表示

画像を10000点分全て読み込み、ランダムな番号の画像ファイルと紐づく正解ラベルを表示します。

clear;
close all;
tic;
load('./cifar-10-batches-mat/data_batch_1.mat'); % これは1~5,test_batchどれでも良い
load('./cifar-10-batches-mat/batches.meta.mat');
imData = uint8(zeros(10000, 32, 32, 3));
dataColor = uint8(zeros(32, 32, 3));

%% 画像をロードする
for i = 1:10000

    data1 = data(i,:);
    dataColor = reshape(data1, [32, 32, 3]);
    
    imData(i,:,:,:) = dataColor;
end

%% ランダムに画像とタイトルを表示してみる
selectNo = randi(10000);
lab = labels(selectNo);
ti = label_names(lab + 1); % MATLABは1スタートなので+1する
IM1 = reshape(imData(selectNo,:,:,:),[32 32 3]);

figure(1);imshow(imrotate(IM1,270),'InitialMagnification',500);title(ti);

toc;

実行結果


f:id:Lynx-EyED:20210324204145p:plain
なるほど...?
32x32 ピクセルなんで仕方ないですね。

さてCNNのコード書くか〜

高度運転支援向け単眼カメラの実装(3.ニューラルネットワークの基本実装)

パーセプトロンの収束定理の壁

深度推定のフィルタを試作し様々な環境で調査して、ある程度どんな環境にも対応できそうなパラメータを決定しました。
が、すこし問題が出てきました。

  • すごく白い道路を道路と認識せず、壁と認識し、警告が止まらなくなる。(協業しているイスラエルの会社からの報告。こちらでも再現がある程度出来ている)
  • 昼間のフィルタパラメータでは薄暗い時に特徴点が減る。だが、深夜になると昼間と同等になったりする。薄暗い→夜の境目でのパラメータが全く不明。深度推定に問題が著しく出るわけではないが、運動視差などで補完できないレベルではある。

と、線形的にフィルタの閾値などを変更しても解決が難しい事象が出てきました。

この非線形な事象の解決にはニューラルネットワークが必要に思われます。ただしクラス分類は最小限にするなどして実時間での処理を目指します。
もちろん、高性能GPUで解決する方法はなくはありませんが、搭載するターゲット(軽車両、超小型車、既存車両への後付け)から考え、初期開発にお金をかけ、量産では費用をかけないものを目指します。


分類問題と回帰問題

行いたいのは深度推定とセマンティックセグメンテーションです。
この二者はディープラーニングでは少し異なる設計を必要とされます。

  • 深度推定は回帰問題
  • 車、人、二輪車、などは分類問題

これで出力層の設計が変わります。具体的には活性化関数が変わります。最終的にはあまり大した問題にはならないでしょう。

MATLABでNNの基本を設計する

実装にはMATLABを使います。Deep Lerning ToolboxやTensorFlowは使わず、バニラMATLABを使用します。それらのパッケージを使用しリソースリッチに実験するのも素敵ですが、その環境をFPGAや組み込みプロセッサへネイティブに動作させるとなると、移植に必要なエネルギーが膨大なのと、ハードリアルタイムに動作できなくなる可能性が高くなります。また、提供されている機能の大半は使用しないと思われます。今回利用するNNの機能は限定的であり、使用するニューロンの層数は2ないし3層と決定しています。FPGAに期待するのはTFが提供する機能が動くことではなく、行列積の演算を素早く終えること、ただそれだけです。逆にいうと、MATLABはその限られた環境に無理矢理押しこめるか見積もるための飛び道具です。

NN実装の素振り

CNNではなくNNから行います。非線形の分類=NNを試してみて、畳み込みNN(=CNN)のカーネルがどの程度必要とされているか見積もりたいからです。

スタンフォード大のニューラルネットワークの授業が公開されています。とても理解しやすく書かれています。実装例としてPythonを使っていますが、あまりPythonコードの解説にならないように努力が払われているように感じられます。NNはもとをただせば行列式の積和なので数式で説明された方が、証明もできますし、エンジニアとしては嬉しいです。ライセンスもアカデミックらしくMITです。

勉強がてらこのスタンフォード大の授業を和訳しています。(まだ途中)
github.com

Module.1の最終章「Putting it together: Minimal Neural Network Case Study」までやってみて、例題の線形分類と非線形分類をMATLABで実装しました。

線形分類

「Putting it together: Minimal Neural Network Case Study」の実践です。 線形分類スコアをS、分類付けをする重み行列をW、入力データをX(サンプル数i個、j番目まである行列)、バイアスをb、教師信号yと置くと、


S = WX+b
と記述できます。どれだけ教師信号yとかけ離れていたかを示す、損失関数はソフトマックス関数を用いて、下記の様に表せます。

L_i = -\log\left(\frac{e^{f_{y_i}}}{\sum_j e^{f_j}}\right)
ここから勾配を求め、重みを改良します。これを反復することによってどのくらい分類に成功しているかを求めます。詳細はこちらか英語の原文に当たってください。
MATLABで記述しました。コード全文は以下。
github.com
MATLAB homeをお使いの方でも基本パッケージだけで動くはずです。試したバージョンはMATLAB/Simulink R2021aです。

色分けしたスパイラル散布図を線形分類しました。黒丸の青、緑、赤を分類します。


f:id:Lynx-EyED:20210319192840p:plain
認識率は52%でした。大体48~53%くらいになります。視覚的にもわかる様に、ランダムな座標を生成し、分類がどの様にされているかをみてみました。「線形」分類なのでこんなもんですね。

ニューラルネットワーク

隠れ層を導入し、2層ニューラルネットワークを構築し活性化関数にReLUを導入したものです。


f:id:Lynx-EyED:20210319194200p:plain
98%~99%の分類精度を誇ります。やった。視覚的にも明らかですね。

MATLABコード全文は
github.com

これから

今回のトライアルは、入力データXは座標で、n番目データの前後(n-1),(n+1)などは独立していました。
ですが、入力するのは画像です。つまりあるピクセルの上下左右(もう少しマクロな視点で言うならば周囲数十ピクセル)、はかなり関連性の高い情報になるはずです。畳み込みが有効になってくる分野です。ここからCNNを実践していきます。

自作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生成時は含めて生成する

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