lynxeyedの電音鍵盤

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

MATLABによる軽量NNの開発(1:環境の整備)

ゼロから作るDeepLearning

前回の話題からの続きになります。
こんな本をいただきました。有難うございます。




5章の誤差逆伝播を重点的にお勉強して、4~7章を読みました。SURF/SIFT/k-means/KL変換など古典的な画像処理をしていた人は4,5章だけでもいいかもしれません。それにしてもとても良い本ですね。この本。最近のNN論文を読むスピードが格段に上がります。

そういえば学生時代はまだ勾配から誤差を求めていて遅すぎて死にかけたことがありました。懐かしい。

本のサンプルコード

おそらくCS231n授業のAssignmentから引用しているのかと思われます。
Qiitaなどにコードの解説記事が乱立しているところを見ると、あー、あのPythonコードの記述やっぱ難しいんだな。という気持ちになります。
しかし、6章までの解説はかなり丁寧ですし、コードも各レイヤの順伝播、逆伝播をしっかり明記しているのはとても良いです。

この本を基にして軽量NNを実装していこうと思います。

とはいえ、Python/Numpyのお作法にリソース割かれたくないなーという気持ちが強くなります。ファー

MATLABを使用

MATLAB Deep Learning Toolboxいいですよね。僕もライセンス持ってて、よく使います。
しかし今回は、論文を見ながら各層をプリミティブに変更していくと思うので、自前で用意してしまってもいいかなという気持ちです。
今回の最終的に実装するエッジデバイスFPGAですので、GPUとアプローチがかなり異なります。
そう考えると最終的にフレームワーク以外全部自前、ということになりそうです。もちろん、ある程度レイヤが固まったら、DLTつかってもいいかも。高速に動作するはずだし。兎にも角にも、Simulinkとの連携への期待も込めてMATLABで設計していきます。

雛形を作る

ニューラルネットワークの各レイヤの理論はすごいですが、プログラム上はそんなに複雑ではない*1ので、ほぼフルスクラッチでも大変ではないと思います。

上記の「ゼロD」本のコードをそのままMATLABに逐次翻訳は、できなくはないと思いますが、やる意味はなさそうです。
PythonMATLABは1:1で逐次翻訳できそうでいて、全く思想が違う言語同士なので、動きを理解したら、1から書いた方が早いでしょう。

とはいえ、全部1から書くのも骨が折れます。雛形を用意し改造していく方がいいでしょう。今回のエンジニアリングの目的はコードを書くことではなく、論文を実機レベルに落とし込む設計をし、検証、実装することです。

で、いいものをMATLAB File Exchangeで見つけました。フルスクラッチの単純なMNIST向けCNNです。
jp.mathworks.com


ここの要となるコードは勾配を求めるnet_grad.mですが、ニューラルネットワーク各レイヤの伝播はこうなってました。

       %%順伝播
	z = w3*fc+b3;     % Affine
	z = relu(z);		% ReLu
	out = w4*z + b4;	%Affine
	out = relu(out);
	probs = softmax(out);
	L = cross_entropy(probs, lab);

%%------(中略)------------
	%% 逆伝播
	dw4= dout*z';	
	db4 = sum(dout,2); 
	dz = w4'*dout;          % ん?どこからどこまでが一つのレイヤかがわからん
 
	dw4 = sum(dw4,5);
	dz= diff_relu(z).*dz;
 
	dw3 = dz*fc';
	db3 = dz;
	db3 = sum(db3,2); 
 
	dfc = w3'*dz;

%% うんこうんこ

ちょっと順伝播、逆伝播がわかりづらかったので書き直しました。
例えば、Affineレイヤを「ゼロD」本の様に記述すると、MATLABではこうなると思います。

%% Affineレイヤ
classdef my_affine
    properties
        W
        b
        x
        dW
        db
        
    end
    methods
        function obj = my_affine(W, b)
            if nargin == 2
                
                obj.W = W;
                obj.b = b;
                obj.dW = [];
                
            end
        end
        %% 順方向    
        function [obj, out] = forward(obj, x)
            
            obj.x = x;
            out = obj.W * obj.x + obj.b;
            
        end
        %% 逆方向
        function [obj, dx] = backward(obj, dout)
            %size(obj.x)
            dx = obj.W' * dout;
            obj.dW = dout * obj.x';
            obj.db = sum(dout, 2);
            
        end
    end
end
 

こんな感じで各レイヤを記述すると、先程のnet_grad.mのコードは以下の様になります。

	%% 順伝播
	[Affine1,out]       = Affine1.forward(fc);
	[Relu3,out]         = Relu3.forward(out);
	[Affine2,out]       = Affine2.forward(out);
	[lastLayer, ~]      = lastLayer.forward(out, label);

%%------(中略)------------

 	%% 逆伝播
	[lastLayer, dout]   = lastLayer.backward(dout);
	[Affine2, dout]     = Affine2.backward(dout);
	[Relu3, dout]       = Relu3.backward(dout);
	[Affine1, dfc]      = Affine1.backward(dout);


スッキリ書けます。今回各レイヤ(Convolution/Affine/ReLu/Softmax/Cross-entropy-error)を書き直し、この雛形に押し込んだ形にしました。
コード全文は以下から。
github.com

MNISTの文字の学習です。最初は文字認識率が80%後半台と畳み込みをしてる割にあまり向上しません。
4~5epochくらい回せば、95%付近まで改善します。暇な時に回してみるのもいいかもしれません。
↓ 半日くらい根気強く回せば、このくらい(96~97%ほど)
f:id:Lynx-EyED:20210422164608p:plain

これから

FPGA向けBinaryNetの検証と、その派生論文を調べ、実装していきます。畳み込み層の計算量の低減を当面の課題とします。
このネタも、去年のRISCVネタ並みに引き摺りまくっていく次第ですのでよろしくお願いします。

*1:カルマンフィルタなどもそうですね

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"

おしまい。