lynxeyedの電音鍵盤

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

QuickLogic EOS S3基板を起こすハメになった話

ついに半導体枯渇が死活問題になってきた

ある程度の在庫を抱えてはいたのですが、Cyclone 10LP/MAX10が枯渇しそうです。
少なくともトレイに入った状態のintel FPGAはここ半年くらいお目にかかっていません。在庫しているボードものこりがわずかで、使い終わったらおしまい。商社によると年末に各社から不良在庫回避放出イベントが多少あるんじゃないか、と言われてましたがそうでもなさそう。

そんな経緯で、代替デバイスを検討すべく小規模FPGAに限って新興メーカ数社を中心に評価ボード、供給体制をチェックしていました。とはいえ弊社との取引先に無断でメインコアを変えるわけにも行かないので、対応を長いこと協議していました。
ADAS向けとシニアカーなどの福祉車両向けがメインです。車載グレードが必ずしも要求されない用途に限ります。

複数のデバイスをチェックし、早い段階からチェックしていたQuickLogic EOS S3は、私(ハードウェア担当)が選択肢から外そうとしていました。詳細は後述しますが、いわゆるFPGAに求める機能があまりにも欠落しているかな、、、という判断でした。

ですが、取引先の大半のエンジニア(ソフトウェア担当)が違う観点でした。
下記は協議中に挙げられたメリットデメリットです。

設計検討

EOS S3の何がいいのか、会議を重ねました。

ハードウェアサイド(基板設計とFPGA側)

メリット
  • 値段にしてはロジック規模大きめ
  • CPUとのバスブリッジがあり、128kBのアドレス空間
  • ハードマクロでSRAMがある
  • SRAMは非同期デュアルポート
デメリット
  • 基板設計時、EOS S3のパッケージが小さく(0.4mm BGA 64pin)、ビルドアップがほぼ前提となってる
  • FPGAの(単なるマルチプレクサではなく)GPIOとして使えるピンが32ピンしかなく、そのうち6ピン以上が起動時のデバッグトリガーやデバッグ端子や状態遷移につかわれていてめんどくさい(少ピンFPGAあるある)
  • ハードマクロSRAMが8kBしかない
  • ちょっとしたロジックを組むと50MHz超えたあたりでFPGA部の動作が怪しい(Fmax=40MHzくらいがせいぜいではないか?)
  • PLL(というかクロック)が足りない
  • DDRない
  • LVDSない

ソフトウェアサイド(CPUプログラミングチーム)

メリット
  • Cortex-M4F(CMSISライブラリが使える)
  • 組み込みにしては大容量の512kB SRAM
  • dockerですぐ環境構築できる
  • Zephyrつかえる、FreeRTOSつかえる
  • 提供されている音声認識エンジンが優秀
  • エッジデバイス用AIソリューションが豊富
  • I2Cセンサーに限るが、Arduinoサンプルソースコードがある場合、小変更で動かせる事が多い。
  • スタートアップ時にCPUがまず起動するのはありがたい
  • 気に入らなかったらFPGA側をとめられる(おいやめろ
デメリット
  • コントロールしているペリフェラルがどのサブモジュールに属しているか意識しなければならない事がある
  • 他デバイスへの移植性がちょっと悪いかも

ソフトウェアサイドのメリットが目立ったので、QuickLogic EOS S3を続行することにしました。導入コストが低い*1というのはどのエンジニアからも出たのでそうなんだろうなー。
音声認識は現状は行っていないのですが、直近で発生しそうな案件ではあるので用意しておいてもよいでしょう。音声でなくても低周波の波形解析に応用できそうとのこと。

基板の設計にあたって

チップの在庫確保もでき問題なさそうです。
問題はハードウェアチームです。基板製造コストがintelFPGAに比べかなり上がるのと、intelFPGAほどの機能が見込めないので妥協案を探っていきます。

  • PLLがない問題

頼りにしていたSi5351Aがしばらく枯渇しています。L/Tみても絶望しかありません。
これについてはPLLで何がしたいか、という問題を検討しました。多くの場合数百MHzのクロックが欲しいのではなく、数十MHzのクロックとその90度位相が遅れたクロック(I-Q)が欲しいだけです。ですので、ロジック回路で解決できそうです。高速動作する74AVC74などに欲しい周波数の4倍を加え、I,Qを取得する算段にします。なお、FPGA側でそのくらいは作れそうに思えますが、ループエラーで論理合成は成功しないと思われます。そもそもFmaxが低いのでそれ以前の問題ですが。

  • 基板製造価格

製造費用増は仕方がありません。事前検討の結果、EOS S3のBGA64ピンのうち62ピンはどうしても必要なことがわかっています。
パッドオンビアかつビルドアップを選択しますが、べリードビアを避ける、フルスタックではなく貫通viaにすることによりなるべく費用を下げます*2

  • SRAMが足りない問題

外付けでQSPI IoT SRAMを使用します。EOS S3のFmaxがかなり低いことも検討の結果わかっていますので、帯域を確保するため2個パラレルで使用します。この時必要になるGPIOは12pin(=6pin x 2個)です。パラレルSRAMを使用する場合より格段に少ないピン数です。

基板外観

こんな感じ。アートワークは2日ほどで終わりました。部品選定は選定中に在庫が消えるという体験を何回もやっているので4日ほどかかっています。もうやだー。
f:id:Lynx-EyED:20211206021621p:plain

裏面
何も部品は配置しません。シルクで電源端子の説明だけ書いておきます。
f:id:Lynx-EyED:20211206022856p:plain


部品点数自体は少ないので、4層もあれば広々と配置できます。
f:id:Lynx-EyED:20211206021949p:plain

見ての通り、Raspberry Pi ZeroのHATと同じフットプリントです。デバッグはすべてラズパイから行います。
部品も確保済み。ガーバーアウトしたので、あとは製造実装まちです。

*1:ジョインしてきた企業や人員に教育するコストが低い

*2:基板製造業者によります。この方法でも価格は変わらない場合も十分あり得ます

QuickLogic EOS S3でCMOSカメラOV5642を駆動する

小規模FPGA+小規模マイコンならではの使い方としてイメージセンサの駆動は最適かもしれません。
マイコンだけだと、駆動ロジック自体は単純な割に処理速度がギリギリになってしまうからです。外部メモリーIFを持っているマイコンであれば駆動は簡単ですが、そのような品種は比較的高価で多ピンの場合が多いので、EOS S3はそれらに対するアドバンテージと言える、かも。知らんけど。

※ QuickLogic EOS S3評価ボードはスイッチサイエンスさんでも発売中。
www.switch-science.com

この記事は、イメージセンサの選定から、ロジックの検討、HDL記述、CPUからFPGAへのアクセス、とりだした画像データのMATLABによるデコードまで行います。

イメージセンサ検討

扱いやすく安価なイメージセンサがあります。
akizukidenshi.com
外部から24MHzでクロックを入れる必要があります。
MIPIインターフェースのみを持つカメラが多い昨今で、OmniVision OV5642はDVP(=8bitパラレルIF)を持ちVGAサイズ時60fpsまで駆動できるこのデバイスはかなり重宝します。EOS S3のようにカメラ用IFハードマクロを持たない古典的なCPLD/FPGAでも駆動できるからです。

イメージセンサ初期化

SCCB(I2C)インタフェースで設定をします。今回VGAサイズ、RGB565フォーマット出力に設定しています。この記事でSCCB設定方法について書くつもりはありません。OV5642はイメージセンサにしては結構「枯れた」デバイスですので、検索すると設定方法のブログや記事などが結構見つかります。実際に記述したコードはこちらをご覧ください。

画像データのストア方法検討

EOS S3のハードマクロSRAMは8kbyteあります。もちろんこれを画像データの蓄積に利用するのですが、VGAサイズでRGB565=16bit/ピクセルの場合、640x480x2[byte/pixel]=600kbyteとなるので、必要データ数の75分の1しかありません。対策は以下のいずれかを選ぶと思います。

  1. 外部大容量RAMを接続する
  2. CPU側はトータル512kbyteのSRAMを持っているのでFPGA/CPU協調動作と気合で頑張る
  3. 時代はミニマリスト。潔く画像全部を取り込むのは諦める

1は真っ当な解決方法です。ほとんどの組み込みエンジニアは異論は無いでしょう。2もできるならしたいところです。やってみて出来そうだな、という手応えはありましたが、それ以外のこと(例えば画像処理など)を並行してさせると一気に破綻しそうなので、今回は採用しませんでした。3は一見すると意味不明ですが、事と次第においては最適解です。例えば画像処理において、取得できた画像のすべての要素がROI*1ということはまずあり得ません。画像中でROIとなるオブジェクトは多くて数点、画像中の占める割合で言うと25%~30%くらいでしょう。
加えて量産の場合、せっかく格安FPGAを採用したのにRAMを外付けするというと稟議で「PoCまではRAM付けていいけど、量産(前試作)までに外してね 」と返ってくることもあります。量産だと一個百円のデバイスでも苦い顔される。購買サイドで考えればわかんなくないけど。
というわけで、今回は3を試してみます。1でもQSPI IoT SRAMで実験したので、また後日ブログにしようかと思います。

FPGAロジック設計方針

eFPGAでOV5642からのピクセルクロック、VSYNC、HREF、8bit信号を取得するロジックを記述します。データはハードマクロ非同期SRAMに記録しAHB to WishboneでCPUメモリ空間からリードできるようにします。
問題はWishboneバスブリッジが最大10MHzであるということです。お世辞にも高速とは言えません。転送帯域を稼ぐため、8bitデータを4回分(=32bit)をFPGA側で受信後に整形し、RAMに記録する事にしました。下図に示します。
f:id:Lynx-EyED:20211014125523p:plain

こうすると、ピクセルクロック(PCLK)=24MHzでデータが転送される場合、RAMへのアクセススピードは24/4=6MHz(<10MHz)となり帯域を十分確保できます。詳細は後述しますが、指定された分の記録が終わるとステートマシンは動作を止め、CPUへ完了した旨をステータスビットを1にして知らせます。CPUから解除信号を受信するとステートマシンはリセットされ動作を再開します。
ステートマシンを記述します。

localparam		CRSET = 2'd0;  // RESET
localparam		CB08F = 2'd1;  // Camera buffer 8bit Full 
localparam		CB16F = 2'd2;  // Camera buffer 16bit full
localparam		CB24F = 2'd3;  // Camera buffer 24bit full

localparam  	RST32 = 32'hFFFF_FFFF;
localparam  	RST11 = 11'h7ff;

assign cam_data_valid	= HREFI & VSYNCI ; 

always @( posedge PCLKI or posedge WBs_RST_i)
begin
    if(WBs_RST_i)
    begin
        cam_reg1		<= RST32;	
        cam_reg_out		<= 32'h00;
        cam_status		<= CRSET;
        cam_reg_rdy		<= 1'b0;
        cam_ram_cnt		<= RST11; 	
        cam_freerun		<= RST32;
    end
    else begin // PCLK
		if(cam_data_valid & cam_go_flag)begin
			case(cam_status)
			CRSET: begin
				cam_reg1	<= {24'h00, CAM_DAT[7:0]};
				cam_reg_rdy <= 1'b0; 
				cam_status	<= (cam_state_stop)? cam_status : CB08F;
			end
			CB08F: begin	
				cam_reg1	<= {16'h00,cam_reg1[7:0],CAM_DAT[7:0]};
				cam_freerun	<= cam_freerun + 32'h01;
				cam_ram_cnt	<= (cam_ram_cnt + 11'h01);
				cam_reg_rdy <= 1'b0;
				cam_status	<= CB16F;
			end
			CB16F: begin	
				cam_reg1	<= {8'h00,cam_reg1[15:0],CAM_DAT[7:0]};
				cam_reg_rdy <= 1'b0;
				cam_status	<= CB24F;
			end
			CB24F: begin
				cam_reg_out	<= {cam_reg1[23:0],CAM_DAT[7:0]}; 
				cam_reg1	<= 32'h0;
				cam_reg_rdy	<= 1'b1;    // 非同期SRAM書き込み信号
				
				cam_status	<= CRSET;
			end
			endcase
		end
		else if(~cam_go_flag)begin
			cam_reg1		<= RST32;
			cam_reg_out		<= cam_reg_out;
			cam_status		<= CRSET;
			cam_reg_rdy		<= 1'b0;
			cam_ram_cnt		<= RST11;
			cam_freerun		<= RST32;
		end
		else begin //!cam_data_valid
			cam_reg_rdy	<= 1'b0;
			cam_status	<= cam_status;
			cam_reg_out <= cam_reg_out;
		end
	end	// PCLK
end

SRAMが足りない問題

この問いに一言で回答するなら「画像データの一部分だけをスキャンして残りは捨てる」です。前述したようにROIが判明している場合は他の要素が邪魔になるのでよくやる手法です。メモリが足りないという理由でやることはあまりしませんが…。
OV5642からは1秒あたり数フレームから数十フレーム相当のデータが絶えず流れてきます。今回は8kBしかRAMがないので、

  • 1フレーム目は最初から8kB分を取得しCPUへ転送。フレーム中の残りのデータは捨てる
  • 2フレーム目は8kB~16kBまでの8kB分を取得しCPUへ転送。フレーム中の残りのデータは捨てる
  • 3フレーム目は16kB~24kBまでの8kB分を取得しCPUへ転送。フレーム中の残りの(以下略

.....

  • 75フレームでVGAサイズ1枚分データがようやく取得完了

と、75回繰り返してデータを取得することになります。このやり方だと動く物体を撮像するのは少し無理があります。
今回はUSBシリアルで取得データをゆっくり転送する都合上、さらに時間を要するのでさらに遅くなります。しかたないね。
ROIを考慮する場合はデータは少なくて済むはずなのでここまで手間ではないと思います。

CPU側のプログラム

ハードマクロSRAMは32bit x 512ワードの非同期SRAMを4つ接続しています。4つのアドレスは決まっているため、C側では以下のように宣言しています。

volatile uint32_t **fb_ram0		= 0x40022000;
volatile uint32_t **fb_ram1		= 0x40024000; 
volatile uint32_t **fb_ram2		= 0x40026000; 
volatile uint32_t **fb_ram3		= 0x40028000; 
volatile uint32_t **fb_status	 	= 0x4002a000;

最後のfb_statusはステータスレジスタです。ステータスレジスタ読み書きによってFPGA側のモジュールの起動・停止などを行います。ステータスの詳細は下記の通りです。read only / write onlyはそれぞれCPU側(=Cソースコード)から見た時の状態です。
f:id:Lynx-EyED:20211014161318p:plain
以上を踏まえVGAサイズのデータ1枚分を取得するコードを記述すると、以下のように記述できます。

uint32_t fboundary = 0;
uint32_t a[512*4];  // 8kb分のデータ保管用

for(uint32_t ii = 1 ; ii < 76; ii++) {
        fboundary = ii;// 
        *(volatile uint32_t *)fb_status = (0x00000 | fboundary); // リセット
        *(volatile uint32_t *)fb_status = (0x10000 | fboundary); // fboundary番目のデータリードスタート
        
        while( 0x08 != (*(volatile uint32_t *)fb_status & 0x08) ); //転送は完了したか
        *(volatile uint32_t *)fb_status = (0x00000 | fboundary); // ステートマシンリセット

        memcpy(&a[512*0], fb_ram0, (512 * sizeof(uint32_t))); // ram0 -> a
        memcpy(&a[512*1], fb_ram1, (512 * sizeof(uint32_t))); // ram1 -> a
        memcpy(&a[512*2], fb_ram2, (512 * sizeof(uint32_t))); // ram2 -> a
        memcpy(&a[512*3], fb_ram3, (512 * sizeof(uint32_t))); // ram3 -> a

        for(uint32_t i = 0 ; i < 512*4 ; i++) {
            dbg_hex32(a[i]);
            dbg_str("\n"); // シリアルデータ送信
        }
    }

ハードウェア

QuickFeatherに直接はんだづけ。こんな雑な配線でもデータが化けないので感動します。ボードのリセットボタンとユーザーボタンがすり減って反応が鈍くなってきたので、そろそろ基板を起こそうかと考えています。
カメラデータ取得・通信用にUSBシリアルを接続しています。
f:id:Lynx-EyED:20211014133256j:plain
結線図を書く気力がないので、カメラとQuickFeatherとの配線情報はpcfを参照ください。

プロジェクト

下記githubからcloneしてください
github.com

git clone -b v0.0.5 https://github.com/panda5mt/qf_wbfpga_pio/

動作方法

書き込み後、リセットするとスタートします。ボーレートはかなり早めの921600baud、8N1でデータがシリアルから取得できます。
ターミナルを使っているならば、パイプで一度テキストファイル等に保存した方が取り扱いが楽だと思われます。
下記例はpicture.txtにデータを保存しています。/dev/ttyUSB0はUSBシリアルのポートです。各自の環境で変更してください。
例:

$ cu -l /dev/ttyUSB0 -s 921600 > picture.txt

RGB565をプレビューしたい

ビットマップのヘッダとかよくわからなかったのと、わかったところで真面目に実装する気力もないのでMATLABの力を借りました。先ほど取得できたpicture.txtを読み込みます。
数十行で書けてしまう。MATLABはいいぞ。

clc;
format long;

RGB_img = zeros(480,640,3,'uint8');
img = zeros(480,640,'uint32');
lower5 = hex2dec('1f') .* ones(480,640,'uint32'); % 0x1f 0x1f ....
lower6 = hex2dec('3f') .* ones(480,640,'uint32'); % 0x3f 0x3f ....
lower8 = hex2dec('ff') .* ones(480,640,'uint32'); % 0xff 0xff ....
lower16 = 65535 .* ones(480,640,'uint32'); % 0xffff 0xffff ....

fileID = fopen('picture.txt');


while (true)
    tline = fgetl(fileID);
    disp(tline);
    if contains(string(tline),'!st')
        disp("start signal received...");
        break;
    end
end
    

 for HGT = 1:480 
     for WID = 1:2:640
        
        data = fgetl(fileID);
        if -1 ~= data
            
            img(HGT, WID) = hex2dec(data);
            img(HGT, WID+1) = bitshift(hex2dec(data),-16);
            
        end
        
     end
 end

img = bitand(lower16, img);
%img = RGB565
imgR = (255/63) .* bitand(lower5, bitshift(img,-11));    % Red component
imgG = (255/127).* bitand(lower6, bitshift(img,-5));    % Green component
imgB = (255/63) .* bitand(lower5, img);	% Blue component

RGB_img(:,:,1) = imgR;
RGB_img(:,:,2) = imgG;
RGB_img(:,:,3) = imgB;

imshow(RGB_img);

動作風景

できました!
f:id:Lynx-EyED:20211014162932p:plain
埼玉県新座市イメージキャラクタ 「ゾウキリン」 (C)2010 新座市

MATLABのプロジェクトはこちらです(上記「プロジェクト」に含まれています)
https://github.com/panda5mt/qf_wbfpga_pio/tree/v0.0.5/matlab

おしまい。

*1:Region of Interest:関心領域

QuickLogic EOS S3のハードマクロFIFOを使う

EOS S3はPSoC3/5LPと比較すると、アーキテクチャの差があるので単純比較できないものの、およそ3~4.5倍ほどの論理ゲート規模を有します。

それはそうと、ロジック規模だけで優劣をつけてしまうので有れば15年以上前のCPLDと同等になってしまいます。それ以上の価値としてEOS S3の特徴は、小規模組み込みデバイスとしては大きめのRAMが搭載されていてサブモジュールごとに割り振られている点です。Cortex-M4Fには512KB、eFPGAには8kBのRAMが割り振られています。

FPGA側から8KBのRAMはハードマクロRAMまたはFIFOとしてインスタンスできます。8kBはちょっと少ないんじゃないかという感じもしますが、EOS S3はセンサーマネジメントSoCなので、FPGA IPでなんでも作り込むような古典的思想ではなく、飽くまでセンサーIFのみを用意するにとどめ、取得したデータはDMAでCortex-M4F側のSRAMに転送する前提なのでこれはこれでいいのかなと。DMAのDestinationはCortex-M4FサブモジュールのSRAMだけでなくSPIマスタにも切り変え可能なため、IoT RAMのような高速大容量SPI RAMがDestination側で使えたらいいなと考えているところです。

下記の順番でこの記事は構成されています。

使用可能なハードマクロ

FPGAが利用できるハードマクロは

  • RAM (8 x 8Kbits)
  • FIFO (8 x 8Kbits)
  • 乗算器 (32bit x 32bit x 2)

です。小規模FPGAらしく必要最小限でとどめていて好印象。

参考:https://quicklogic-fpga-tool-docs.readthedocs.io/en/latest/ram/S3BDeviceHardmacroResources.html#macro-usage-and-examples

ハードマクロFIFOインスタンス

Design example Using FIFOsにハードマクロFIFOインスタンス事例と、VerilogのexampleコードYour-Installed_Path/quicklogic-arch-defs/tests/fifo_testがあります。Wishbone bus readyなコードになっているので、自作プロジェクト内で動作できるように若干の修正は行いますが、ほぼそのまま使おうとおもいます。CPU側のコードだけ追記するような感じです。
FIFOは8kbit、16kbitのブロックを使用することができ、それぞれのブロックを並列、直列接続することによってビット長を増やしたり、単純にFIFO深さをn倍にしたりすることもできます。

下記例は8kbitのFIFOを1ブロック分インスタンスしています。

(前略)
 FIFO_8K_BLK  # (.data_depth_int(data_depth_int),.data_width_int(data_width_int),.reg_rd_int(reg_rd_int),.sync_fifo_int(sync_fifo_int)
             )
      FIFO_INST  ( .DIN(DIN), .PUSH(PUSH), .POP(POP), .Fifo_Push_Flush(Fifo_Push_Flush), .Fifo_Pop_Flush(Fifo_Pop_Flush), .Push_Clk(Push_Clk),
            .Pop_Clk(Pop_Clk),.PUSH_FLAG(PUSH_FLAG), .POP_FLAG(POP_FLAG), .Push_Clk_En(Push_Clk_En), .Pop_Clk_En(Pop_Clk_En),
            .Fifo_Dir(Fifo_Dir),  .Async_Flush(Async_Flush), .Almost_Full(Almost_Full), .Almost_Empty(Almost_Empty), .DOUT(DOUT));

それぞれの信号の詳細は、FIFO Usage — QuickLogic-FPGA-Toolchainを参照ください。

Cortex-M4F側のプログラム

Exampleでは3つ、タイプの異なるFIFOが用意されています。
メモリマップのとおり、構造体を用意しました。
fpga/inc/fpga_fifoctrl.h

typedef struct fpga_fifoctrl {
    uint32_t    device_id;		// 0x00
    uint32_t    rev_num;		// 0x04
    uint32_t    gpio_in;		// 0x08
    uint32_t    gpio_out;		// 0x0C
    uint32_t    gpio_oe;		// 0x10
    uint32_t    reserved1[60-1];
    uint32_t    fifo1_acc;		// 0x100
    uint32_t    fifo1_flags;		// 0x104
    uint32_t    reserved2[63-1];
    uint32_t    fifo2_acc;		// 0x200
    uint32_t	fifo2_flags;		// 0x204
    uint32_t    reserved3[127-1];
    uint32_t	fifo3_acc;		// 0x400
    uint32_t	fifo3_flags;		// 0x404
} fpga_fifoctrl_t;


fpga_fifoctrl_t* fifoctrl_regs = (fpga_fifoctrl_t*)(FPGA_PERIPH_BASE);

このままだとちょっとアクセスが面倒な感じがするので、それぞれのFIFOやフラグにアクセスできるように関数を用意します。

// FIFO APIs
uint32_t    fpga_getflag(uint8_t ch);
uint32_t    fpga_getfifo(uint8_t ch); 
void        fpga_setfifo(uint8_t ch, uint32_t value);

フラグ取得API,FIFO読み込みAPI,FIFO書き込みAPIです。
main関数で下記の手順を実行するテストコードを書きます。

  1. ライト動作
    1. 特定のChannelのFIFOに深さ512だけ書き込む
  2. リード動作
    1. ステータスを読み込む
    2. FIFOをリードする
    3. 上記2ステップをFIFO深さ512だけ実行する
  3. Channel1~3まで上記ステップを繰り返し


コードです。
src/main.c

    // (中略) 
    // test each FIFOs(FIFO1~3)
    for (uint8_t ch=FIFO_CH1 ; ch<=FIFO_CH3 ; ch++) {
      dbg_str("\r\n\r\n------------------ CHANNEL "); dbg_int(ch); dbg_str(" ------------------");

      for(uint32_t i=0 ; i<512 ; i++) {
        fpga_setfifo(ch,i);
      }
      for(uint32_t i=0 ; i<512 ; i++) {
        dbg_str("\r\nstatus = 0x");
        dbg_hex32(fpga_getflag(ch));
        dbg_str("....fifo = 0x");
        dbg_hex32(fpga_getfifo(ch));
      }

      // 最後のFIFOをReadした後(=FIFOはすべてEmpty)のFlagを確認する  
      dbg_str("\r\nstatus = 0x");
      dbg_hex32(fpga_getflag(ch));
      dbg_str(".\r\n");
      
    }

動作確認

QuickFeatherにUSB-UARTを接続していると動作確認ができます。

------------------ CHANNEL 1 ------------------
status = 0x00000f80....fifo = 0x00000000
status = 0x00000e8f....fifo = 0x00000001
status = 0x00000e0e....fifo = 0x00000002
status = 0x00000e0e....fifo = 0x00000003
status = 0x00000e0e....fifo = 0x00000004

()

status = 0x00008102....fifo = 0x000001ff
status = 0x00008001.

------------------ CHANNEL 2 ------------------
status = 0x00000e03....fifo = 0x00000000
status = 0x00000d02....fifo = 0x00000001
status = 0x00000d02....fifo = 0x00000002
status = 0x00000d02....fifo = 0x00000003
status = 0x00000d02....fifo = 0x00000004

()

status = 0x00000302....fifo = 0x000001fc
status = 0x00000202....fifo = 0x000001fd
status = 0x00000202....fifo = 0x000001fe
status = 0x00008102....fifo = 0x000001ff
status = 0x00008001.


------------------ CHANNEL 3 ------------------
status = 0x00000f80....fifo = 0x00000000
status = 0x00000e8f....fifo = 0x00000001
status = 0x00000e0e....fifo = 0x00000002
status = 0x00000e0e....fifo = 0x00000003

()

status = 0x00000302....fifo = 0x000001fb
status = 0x00000302....fifo = 0x000001fc
status = 0x00000202....fifo = 0x000001fd
status = 0x00000202....fifo = 0x000001fe
status = 0x00008102....fifo = 0x000001ff
status = 0x00008001.

こちらも適宜参照ください

Channel1、FIFOリード動作の初めの部分

深さ512分だけFIFOを書き込んだ後からスタートしています。Status(Flag1)=0x0f80ですので、PUSH,POPフラグともにFullを示しています。
Almost Fullフラグも立っています。

Channel1、FIFOリード動作の終盤部分

Status(Flag1)=0x81xxとなっておりAlmost Emptyフラグが立っていることがわかります。

Channel2、FIFOリード動作の初めの部分

Status(Flag2) = 0x00000e03となっています。
Channel2のみFIFO深さが1024となっていますが、FIFOライトしたのは512分だけなので、PUSH,POPフラグともに「半分かそれ以上埋まっている」「半分かそれ以上空いている」=「半分空いている」を示しています。
Channel3は情報は1と似ているため説明は省略致します。

正しく動作していることを確認できました。

コード全体

QORC-SDKプロジェクト、
qorc-sdk/qf_appsフォルダ内で、以下のプロジェクトをcloneしてください。

github.com

$ git clone -b v0.0.2 https://github.com/panda5mt/qf_wbfpga_pio.git

所感

ハードマクロFIFOを体験できました。今回はCPU側からR/Wしましたが、CPU側からはリードオンリーにして、FPGAに接続したイメージセンサなどから所得したデータをDMAで取得したりするのに使えそうです。


QuickLogic EOS S3をDockerで使う

前回の記事のキャッチアップです。Dockerが使えると環境整備の負担が少なくて良いので、作ってみました。

脱・開発環境汚染

現在、複数の大学と企業と連携したプロジェクトで開発をしているのですが、環境構築やってらんない、CI/CD考慮した環境にすべきとの指摘があり、移行可能なプロジェクトから順次Dockerを採用しています。そこで使用中のQORC-SDKもDockerに移行しました。その時のメモとなります。
QuickLogic公式にもql_symbiFlowのみのDockerfileがあるのですが、どうせなら開発環境全部用意して欲しいのだが、というところがこの記事の始まりです。
この記事はDockerの初歩的な知識があり、セットアップも終えている前提で始まります。

Dockerfile

Macbook Late 2020(intel)、WSL2上での動作を確認しています。M1 Macでも一応動作しそうですがかなり遅いです。CI/CD考慮しているのにM1に拘ることもないでしょう。x86_64使いましょ。
任意のディレクトリを作成しDockerfileを作成します。

$ mkdir docker-qorc
$ cd docker-qorc
$ touch Dockerfile

Dockerfileは以下の内容です。Docker imageはあまり軽くない(2.0GBほど)ですが、
成果物と開発環境の両方が欲しいので今のところはこの方針でいきます。

FROM debian:buster-slim 
RUN apt update && apt -y install --no-install-recommends sudo make libtbb2 wget coreutils udev curl time tar\
    && apt -y install git gcc-arm-none-eabi\
    && apt clean && rm -rf /var/lib/apt/lists/*
RUN git clone -b v1.10.0 https://github.com/QuickLogic-Corp/qorc-sdk.git
SHELL ["/bin/bash", "-c"]
# 以下3行はtarからarm-none-eabiを導入する場合は不要。(armからのダウンロードがすごく遅いのでaptで導入してシンボリックリンクを貼っている)
WORKDIR /qorc-sdk 
RUN mkdir -p arm_toolchain_install/gcc-arm-none-eabi-9-2020-q2-update/
RUN ln -s /usr/bin/ arm_toolchain_install/gcc-arm-none-eabi-9-2020-q2-update/
#RUN sed -i -e 's/v1.3.1/v1.3.2/g' envsetup.sh 
#RUN source envsetup.sh
WORKDIR /root
RUN echo -e '#!/bin/bash\n\ncd /qorc-sdk\nsource envsetup.sh\ncd -\nmake -C GCC_Project\nscp GCC_Project/output/bin/*.bin pi@raspberrypi.local:/home/pi\n' > qlogic_build.sh 
RUN sed -i 's/\r//' *.sh
RUN chmod +x qlogic_build.sh
WORKDIR /qorc-sdk/qf_apps
# 以下はキャッシュされると最新版がgit cloneできないための対策
ARG CACHEBUST=1
RUN git clone https://github.com/panda5mt/qf_wbfpga_pio.git

Dockerイメージのビルド

保存したら、Dockerイメージをビルドします。イメージ名は適当にtestにしました。Docker hubを使うのであればイメージ名は考慮した方がいいでしょう。この記事の趣旨ではないですがDockerイメージをVSCodeを使っているならばこの項目での作業は全く必要ありません。

$ docker build -t test --build-arg CACHEBUST=$(date +%s)  .

所要時間はそんなにいらないはずです。5分ほどで仕上がるでしょう。
イメージを起動します。

$ docker run -it test 

ログインできたら下記のようになると思います。

[root@123456789abc:/qorc-sdk/qf_apps# 

QORCプロジェクトのビルド

例題としてqf_advancedfpgaをビルドします。Dockerイメージを作った直後の初回だけanacondaが走って環境を構築するので10分くらいかかるかも。次回以降は純粋にプロジェクトのビルドのみです。

$ cd qf_advancedfpga/
$ ~/qlogic_build.sh

エラーなく終わればOK*1GCC_Project/output/qf_advancedfpga.binがビルドバイナリです。qf_apps内の他のプロジェクトも同様の方法でビルドできます。

Dockerイメージ状態の保存

Dockerイメージ起動中に別ターミナルでコンテナIDの確認をします。

$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED          STATUS          PORTS     NAMES
123456789abc   test      "bash"    11 minutes ago   Up 11 minutes             foobar

コンテナID=123456789abcをコミットします。

$ docker commit 123456789abc test

docker commitはあまり使ったことはないですが...
成果物をgit pushした後はイメージごと捨ててしまうので。
個人、チームでやりやすい開発スタイルで使えばいいんじゃないかな。

おしまい。

*1:先程作成したDockerfileでqlogic_build.shというスクリプトを作成しています

小規模マイコン+FPGA、QuickLogic EOS S3の試食

小さめなマイコン+グルーロジックの代表格はPSoC5 LPなどが挙げられます。回路規模でその上になるとXilinxであればZynq、IntelであればCyclone V SoCのような比較的中規模デバイスでしょう。

www.quicklogic.com
昨年の11月に購入して積んでいました。
f:id:Lynx-EyED:20201106100600j:plain
必要に駆られて使ってみることに。
f:id:Lynx-EyED:20201106100719j:plain
Quicklogic EOS-S3はこの両者の中間のデバイスと言えるかもしれません。このデバイスは80MHzで動作するCortex-M4Fを軸に小規模DSPとeFPGAを搭載した安価なデバイスです。少量購入でも単価1000円くらい。FPGA側はCPU(バスマスタ)にぶら下がるメモリマップドスレーブとして開発できます。(Wishboneバス)

Vivado IPインテグレータやPlatform DesignerみたいなGUIはありません。GUIのアシストが必要になるレベルの大規模なものを作る前提ではないのでしょう。いずれにせよ、今回のようなデバイスは使い捨てができる回路規模であり、そういう前提で使うのが吉です。末長く使う物でもないので、これを大黒柱にした開発は避けるべきでしょう。

今回の記事は以下のような構成です。前半は開発環境の整備。中盤はプログラミングに必要なGPIOやFPGA内部とのアクセスに必要なメモリマップの情報です。また後半ではオリジナルコードを記述しながら、FPGAのプログラミング時に必要なピンアサイン情報の設定方法も取り上げます。



開発環境の整備

(Dockerを使用する場合の記事を書きました。私のように開発環境が多く、使用後はイメージごと使い捨てるスタイルの方はこちらの方が便利だと思います)


Quicklogicの提供するFPGA開発環境(ql_symbiflow)が、x86_64 Linuxを前提にしています。他はarm-none-eabi-gccおよびPython3が動作すればアーキテクチャはなんでも動くのですが、ここでCPUのアーキテクチャが固定されてしまいます。SymbiFlow本体にはそのような制約はないようですし、そのうち改善されるといいですね。

今回はWSL1を使いました。WSL1/2はこのブログ執筆時点でUSBが動作しませんが、私の場合はWindows(WSL)はオンプレのコンパイラ専用機として使用するので問題ありません。開発は手元のmacVS Code Remote Developmentで行い、EOS-S3への書き込みはラズパイから実行します。

WSL1はubuntu-20.04を使用しています。諸事情*1でWSL2からダウングレードしています。

QORC-SDK導入

以下のgithubからcloneしてきます。
github.com

$ git clone -b v1.10.0 https://github.com/QuickLogic-Corp/qorc-sdk.git
$ cd qorc-sdk/

コンパイラ導入

諸々のコンパイラ環境を一気に導入します。

$ source envsetup.sh

これだけです。
ここでanacondaをいきなりインストールされて、環境構築が始まります。私の環境ではタイムアウトに起因するエラーが何回か出ました。根気よく何回かトライする必要があるかも。

インストール時のエラーについて
  • apioに関するエラー:WSLはUSB(シリアル)が動かないのでエラーで構わない。
  • fasmに関するエラー:論理合成後のバイナリストリームをC言語ヘッダファイルに変換する部分がエラーを起こしています。再インストールが必要かもしれません。

加えて、gcc-arm-none-eabiはQORC-SDKが提供するバージョンを使うのが良いと思われます。バージョンによってはサンプルプログラムをビルドした際にRAM領域がオーバーフローすることがあります。

githubのREADMEをみるといろいろPATHを通すように指示がありますが、私の場合はログイン後に毎回source envsetup.shを走らせています。ログインと同時にPATHを通されてしまうとQORC-SDKのconda環境が別に構築したpip環境や、既存のconda環境を破壊することがあるので嫌だっただけです…pyenvを真面目に設定すればいいのでしょうが。

サンプルプロジェクトのお試しビルド

前節までで導入した環境の動作確認のためサンプルプロジェクトをビルドしてみます。ARMコンパイラFPGA論理合成ソフトウェアql_symbiflowの両方の動作確認ができるサンプルコードを選択します。 qf_apps/qf_helloworldhwqf_apps/qf_advancedfpga といったサンプルプロジェクトが最適と思われます。
例:qf_helloworldhwのサンプルをビルド

cd qf_apps/qf_helloworldhw
make clean -C GCC_Project
make -C GCC_Project

clean後のビルドはCコンパイルの他、論理合成も走るので5分くらい気長に待ちます。

(中略)
make[1]: Entering directory '/mnt/c/qorc-sdk/qf_apps/qf_helloworldhw/GCC_Project'
Linking ...
Convert ELF to BIN
Create text symbol table.
make[1]: Leaving directory '/mnt/c/qorc-sdk/qf_apps/qf_helloworldhw/GCC_Project'
Copy output files ...
make: Leaving directory '/mnt/c/qorc-sdk/qf_apps/qf_helloworldhw/GCC_Project'

こんな感じで終了していれば成功。
もし

ERROR: 'AL4S3B_FPGA_top_bit.h' not found.

のようなエラーが出ている場合、ql_symbiflowのインストールに失敗しています。前節で実行したsource envsetup.shの際のエラーを確認することをお勧めします。

おまけ:ラズパイでの書き込み

Raspbianでもubuntuでも同様の手法です。下記のREADMEの通りに導入します。
github.com

$ git clone --recursive https://github.com/QuickLogic-Corp/TinyFPGA-Programmer-Application.git
$ pip3 install tinyfpgab

毎回コマンド打つのは面倒ですので、スクリプトを書きます。書き込みするbinファイルは大抵同じファイル名だと思いますので、下記のようにしています。ttyポート名は各自の環境に合わせてください。

#!/bin/bash
#qfprog --port /dev/ttyACM0 --bootloader *.bin --mode fpga-m4
BINFILE=qf_advancedfpga.bin
qfprog="python3 /(your-installed-directory)/TinyFPGA-Programmer-Application/tinyfpga-programmer-gui.py"
$(qfprog) --port /dev/ttyACM0  --m4app $(BINFILE) --mode fpga-m4

上記テキストを保存し、実行権限を付与すればOK.

GPIOとピンアサイ

閑話休題。フレキシブルなマイコンのイメージが先行し、I2CやSPI、UARTなどの特定機能は任意のピンに割り当てられそうに思いますが、残念ながらEOS-S3はあまり自由度はありません。Cortex-M4FやSensor Manager側に接続されている機能はピンアサインがほぼ固定されています。FPGAからの入出力であれば電源、クロック、ADC入力を除くほぼ全てのピンに割り当て可能なはず(執筆時点で解決できていない部分があります。後述する「コード作成」セクションの「ピンへの反映」をご覧ください)です。
下図は、それぞれのパッケージにおける、個々のピンにアサイン可能な特定機能一覧の抜粋です。
f:id:Lynx-EyED:20210805130637p:plain
FBIOというのはFPGA Fabric IOのことです。アサイン可能なピンの詳細はEOS-S3データシートのp.99~p.102をご覧ください。


https://www.quicklogic.com/wp-content/uploads/2020/12/QL-EOS-S3-Ultra-Low-Power-multicore-MCU-Datasheet-2020.pdf


どのピンに何を割り当てたかはテーブルでソースコードに記述します。
サンプルプロジェクトの場合、src/pincfg_table.cに記述されています。

PadConfig pincfg_table[] = 
{
  { // setup UART TX
    .ucPin = PAD_44,
    .ucFunc = PAD44_FUNC_SEL_UART_TXD,
    .ucCtrl = PAD_CTRL_SRC_A0,
    .ucMode = PAD_MODE_OUTPUT_EN,
    .ucPull = PAD_NOPULL,
    .ucDrv = PAD_DRV_STRENGHT_4MA,
    .ucSpeed = PAD_SLEW_RATE_SLOW,
    .ucSmtTrg = PAD_SMT_TRIG_DIS,
  },
  { // setup UART RX
    .ucPin = PAD_45,                     // Options: 14, 16, 25, or 45
    .ucFunc = PAD45_FUNC_SEL_UART_RXD,
    .ucCtrl = PAD_CTRL_SRC_A0,
    .ucMode = PAD_MODE_INPUT_EN,
    .ucPull = PAD_NOPULL,
  },

   { // Pad 17 -- clock out from FPGA
    .ucPin = PAD_17,
    .ucFunc = PAD17_FUNC_SEL_FBIO_17,
    .ucCtrl = PAD_CTRL_SRC_FPGA,
    .ucMode = PAD_MODE_OUTPUT_EN,
    .ucPull = PAD_NOPULL,
    .ucDrv = PAD_DRV_STRENGHT_4MA,
    .ucSpeed = PAD_SLEW_RATE_FAST,
    .ucSmtTrg = PAD_SMT_TRIG_EN,
  },

   //(中略).....

}

ピンアサインはパッケージのピン番号ではなく信号名(PAD_17 etc..)で記述します。
なお、FPGAの入出力の場合、Verilog側で記述した信号名と対応させる必要があるため、さらに*.pcfファイルを記述する必要があります。詳細はコード作成のセクションで扱います。

メモリマップについて

EOS-S3は様々なサブシステムが接続されており、メモリマップを網羅するのは大変です。この記事では今回使う範囲のメモリマップに限定します。
メモリマップ詳細はTRMのp.38~p.43をご覧ください。
https://www.quicklogic.com/wp-content/uploads/2020/06/QL-S3-Technical-Reference-Manual.pdf
プログラミングでよく使うのはPeripheralの0x4000_0000~0x5fff_ffff番地でしょう。
f:id:Lynx-EyED:20210805171116j:plain

上記アドレスのうち特にFPGAへインターフェースするメモリ領域は0x4002_0000~の128kB分です。
この番地はAHB Slave-Wishbone Masterブリッジが接続されている領域です。
f:id:Lynx-EyED:20210805171809j:plain
詳細はTRMのp.275~ご覧ください。また、後述する、コードの作成時にも触れますので参考になれば。

コード作成

今回、FPGA領域に簡単なGPIOポートを作成します。
Cortex-M4F側から32bitのデータを受け取り、下位4bitだけをGPIOポートに反映するという簡単な物です。一連の動作確認には必要十分だと思います。

既存のサンプルコードに直接手を加えるのはあまり賢明ではないでしょう。サンプルを雛形として新規にプロジェクトを作成することにします。すでにQORC-SDK自体にその仕組みがありますので利用しましょう。

既存プロジェクトを雛形に新規プロジェクトを作成

qf_appsディレクトリに戻ります。
既存プロジェクトqf_mqttsn_ai_appを雛形にして、新しいプロジェクトqf_wbfpga_pioを作ります

$ python create_newapp.py --source qf_mqttsn_ai_app --dest qf_wbfpga_pio

CPUからFPGAへアクセスするには

メモリマップのセクションでも取り上げた通り、AHB Slave-Wishbone Masterブリッジが接続されています。FPGA側に作成したオリジナル回路とCPU間でデータの送受信をする際には、オリジナル回路にWishbone Slaveを接続する必要があります。

これもサンプルがありますので、それを基に作ってしまいますFPGA側の参考になるサンプルプロジェクトはqf_apps/qf_advancedfpgaです。
このプロジェクト内のfpga/rtl/AL4S3B_FPGA_Rgisters.vを抜粋してみます。

Cortex-M4F -> FPGA

Wishboneマスタによるデータライト部分です。Cortex-M4Fからはデータ代入動作でこの部分にライトされます。

assign FB_COLORS_REG_Wr_Dcd   = (WBs_ADR_i == FPGA_COLORS_ADR) & WBs_CYC_i & WBs_STB_i & WBs_WE_i   & (~WBs_ACK_o);

// 中略

        if (FB_COLORS_REG_Wr_Dcd) begin
            color0 <= WBs_BYTE_STB_i[0] ? WBs_DAT_i[3:0]    : color0;
            color1 <= WBs_BYTE_STB_i[1] ? WBs_DAT_i[10:8]   : color1;
            color2 <= WBs_BYTE_STB_i[2] ? WBs_DAT_i[18:16]  : color2;
            color3 <= WBs_BYTE_STB_i[3] ? WBs_DAT_i[26:24]  : color3;
        end
FPGA -> Cortex-M4F

Wishboneマスタによるデータリード部分です。今回該当部分は0xDEADBEEFがリードできるようにします。

always @( * )
 begin
    case(WBs_ADR_i[ADDRWIDTH-1:0])
    FPGA_REG_ID_VALUE_ADR    : WBs_DAT_o <= Device_ID_o;
    FPGA_REV_NUM_ADR         : WBs_DAT_o <= Rev_Num;  
    FPGA_SCRATCH_REG_ADR     : WBs_DAT_o <= { 16'h0, Scratch_reg }; 
    FPGA_COLORS_ADR          : WBs_DAT_o <= 32'hDEADBEEF; //{ 5'b0, color3, 5'b0, color2, 5'b0, color1, 5'b0, color0};
    FPGA_DURATION0_ADR       : WBs_DAT_o <= { 20'b0, duration0};
    FPGA_DURATION1_ADR       : WBs_DAT_o <= { 20'b0, duration1};
    FPGA_DURATION2_ADR       : WBs_DAT_o <= { 20'b0, duration2};
    FPGA_DURATION3_ADR       : WBs_DAT_o <= { 20'b0, duration3};
	default                  : WBs_DAT_o <= AL4S3B_DEF_REG_VALUE;
	endcase
end

Wishboneのアドレスによって、データを振り分けています。たくさんレジスタがありますが、今回の試行ではcolor0レジスタだけを利用し、GPIOに反映するロジックを記述しようと思います。
ヘッダに記述された構造体も見てみます。fpga/inc/fpga_ledctlr.hにあります。
便宜上2つ用意されています。この部分はqf_advancedfpgaのサンプルの便宜上の都合ですので、1つにシュリンクしても良いでしょう。

// 構造体1つ目
typedef struct fpga_ledctlr_regs {
    uint32_t    device_id;			// 0x00
    uint32_t    rev_num;			// 0x04
    uint16_t    scratch_reg;		// 0x08
    uint16_t    reserved1;			// 0x0A
    uint32_t    reserved2;			// 0x0C
    uint8_t    	color0; 			// 0x10
    uint8_t    	color1; 			// 0x11
	uint8_t    	color2; 			// 0x12
	uint8_t    	color3; 			// 0x13
    uint32_t    reserved7[3];		// 0x14
	uint32_t	duration0;			// 0x20
	uint32_t	duration1;			// 0x24
	uint32_t	duration2;			// 0x28
	uint32_t	duration3;			// 0x2C
} fpga_ledctlr_regs_t;

//構造体2つ目
typedef struct fpga_ledctlr_regs2 {
    uint32_t    device_id;			// 0x00
    uint32_t    rev_num;			// 0x04
    uint16_t    scratch_reg;		// 0x08
    uint16_t    reserved1;			// 0x0A
    uint32_t    reserved2;			// 0x0C
    uint32_t    colors;				// 0x10
    uint32_t    reserved7[3];		// 0x14
	uint32_t	duration0;			// 0x20
	uint32_t	duration1;			// 0x24
	uint32_t	duration2;			// 0x28
	uint32_t	duration3;			// 0x2C
} fpga_ledctlr_regs2_t;
Cortex-M4FからFPGAへデータライトするには
#define FPGA_PERIPH_BASE (0x40020000)

// 中略

fpga_ledctlr_regs_t* pledctlr_regs = (fpga_ledctlr_regs_t*)(FPGA_PERIPH_BASE);
pledctlr_regs->color0 = 0xXX; // 任意の数

のように変数に代入するだけでOKです。

Cortex-M4FへFPGAからデータリードするには
#define FPGA_PERIPH_BASE (0x40020000)

// 中略
int32_t read_data;

fpga_ledctlr_regs2_t* pledctlr_regs2 = (fpga_ledctlr_regs2_t*)(FPGA_PERIPH_BASE);
read_data = pledctlr_regs2->colors; // read_dataに格納

これも簡単です。インターフェース部分が理解できてきました。
この情報をもとに、以下の方針で修正していきます。

pledctlr_regs->color0 = 任意の数値;
↓
FPGAのcolor0レジスタに反映
↓
color0の下位4bitを{p3_o,p2_o,p1_o,p0_o}に反映
↓
GPIOに反映

まず、qf_advancedfpga/fpga/の部分を作成中のプロジェクトqf_wbfpga_pioにコピーし、makefileを修正します。


makefileの修正は本質ではないものの面倒で時間が取られる部分です。修正後のプロジェクトをgithubに上げております。詳細はこのセクションの最後をご覧ください。


RTL修正部分:詳細は割愛しますが、レジスタcolor0を4bit幅に直し、出力ポートp0,p1,p2,p3を追加するコードを追記しました。color0,p0,p1,p2,p3といったキーワードでfpga/ディレクトリをgrepするとわかると思います。

GPIOに反映するロジックは簡単です。Wishboneクロックに同期して出力を反映しているだけです。
fpga/rtl/LED_controller.v

reg [3:0] port_l;

always @(posedge rst or posedge clk)
    if (rst)begin
        port_l <= 4'h0;
    end else begin
        port_l <= color0;
    end
assign p0 = port_l[0];
assign p1 = port_l[1];
assign p2 = port_l[2];
assign p3 = port_l[3];

endmodule

ピンへの反映

p0~p3までの4つの出力をIOに反映します。
fpga/rtl/quickfeather.pcfを記述します。
*.pcfは

set_io  信号名 「このパッケージでのピン番号」または「IOエイリアス(IO_10など)」

という文法で記述します。
使用しているボード(QuickFeather)に搭載されているのはQFN(PU64)パッケージです。

set_io p0_o	    40
set_io p1_o     42
set_io p2_o	    37
set_io p3_o     28

したがって、BGA(PD64)パッケージの場合は以下のように記述します。

set_io p0_o	    E7
set_io p1_o     D7
set_io p2_o	    G8
set_io p3_o     H5

WCLSP(WR42)の場合は割愛します。
冒頭で述べた懸念点です。どこのFBIOでも指定できるはず、と述べたのですが、以下のピン番号は属性がIOTYPE=SDIOMUXであり、IOに指定するとエラーが発生し論理合成を終えることができませんでした。このピンを避けた方が良いと思われます。

QFN(PU64)パッケージ: 22,21,20,18,17,15,16,11,13,14,10,9,8,7 番ピン

詳細は、Symbiflow Installation Guide and TutorialのPcfの記述方法をご覧ください。

Cソースコードの記述

FreeRTOSが移植されているので、そのまま使いました。
必要な部分のみの抜粋です。

#define FPGA_PERIPH_BASE (0x40020000)
fpga_ledctlr_regs_t* pledctlr_regs = (fpga_ledctlr_regs_t*)(FPGA_PERIPH_BASE);
fpga_ledctlr_regs2_t* pledctlr_regs2 = (fpga_ledctlr_regs2_t*)(FPGA_PERIPH_BASE);


void vTask1(void *pvParameters);
void vTask2(void *pvParameters);
int main(){

  
    xTaskCreate(vTask1,"Task1", 100, NULL, 1, NULL);
    xTaskCreate(vTask2,"Task2", 100, NULL, 1, NULL);
    vTaskStartScheduler();

   while(1);
}


void vTask1(void *pvParameters){
  while(1){
    pledctlr_regs->color0 = 0x05;
    vTaskDelay(500);
    pledctlr_regs->color0 = 0x0a;
    vTaskDelay(500);
  }
}

void vTask2(void *pvParameters){
  while(1){
    vTaskDelay(1000);
    dbg_str("\r\n\r\nRead data from FPGA=0x");
    dbg_hex32(pledctlr_regs2->colors); // データリード
  }
}

1つ目のスレッドvTask1でFPGAのメモリ空間の指定した領域へ500msec毎に0x05、0x0Aを記述しています。
2つ目のスレッドはUARTに1秒間隔でFPGAからリードしたデータを(=0xdeadbeefになるはずです)表示しています。UARTは115200baudで、QuickFeather基板の場合J3ピンヘッダの2(TX),3(RX)にUSB-シリアルを接続する必要があります。

動作風景

youtu.be

プロジェクト

qorc-sdk/qf_appsフォルダで、以下のプロジェクトをcloneしてください。
github.com

$ git clone -b v0.0.1 https://github.com/panda5mt/qf_wbfpga_pio.git

バイスのパッケージについて(QFNは売ってない?)

2021年8月現在、Mouserで評価ボード、チップ単体を入手できます。
www.mouser.jp
BGA(PD64)、WCLSP(WR42)、QFN(PU64)の3タイプのパッケージがありますが、QFNは残念ながら量産計画はなく、評価ボードのみの供給になるようです。
EOS-S3データシートのp.98。

NOTE: The QFN pinout information is provided as reference for QuickFeather board only. The QFN package is not in mass production. 

このPD64パッケージの基板を製作したので、後日記事にできればと思います。

おわりに

すごく使いやすいわけではありませんが、必要十分なデバイスと思います。
FPGAもLVDSやDDRなどがあるわけではありませんが、最大100MHz動作で、80MHz動作のCortex-M4Fのサブシステムと協業するのには十分な性能だと思います。

小さい、安価であるところと、メモリが豊富なところをうまく使えないか考えているところです。


*1:intel FPGAのnios2開発環境が2021年6月時点でwsl1しか対応していないため

intelHLSプロトタイプの設計(Cyclone10GX基板の設計)

最近では、安全運転支援のカメラ開発も乗用車から船舶向け、最近では庫内の運搬用群ロボットまで引き合いをいただいております。
ありがとうございます。

開発スピード向上

上記のような簡易プロトタイプ用途としてCyclone10LPのボードを1年前に開発しました。基板単体を販売するほど販路やマーケが大きい会社ではないのでそのようなことはしていませんが、弊社の提供するサービスが動くハードウェアの一部として提供しており、すでにH社製ADASの補助、無人バイク制御などで活躍しています。

MATLAB/SimulinkをはじめとしたMBD開発に軸足を置いているため開発効率はそこそこ高い*1ものの、GPU開発に比べると遅いのも事実です。FPGAを利用した画像処理、深層学習、あるいは軽量NNの論文でも明らかなように1GFLOPSあたりの電力効率は2~10倍ほどアドバンテージはあり、単位処理能力あたりのデバイス単価は圧倒的に安いですが、開発速度がおそいと研究開発の分野では厳しいものがあります。
開発初期のプロト以降では開発コスト高めのFPGA内部はどうしても固定化されてしまう問題がありました。このためFPGA内部の開発も流動化できるような高位言語プロトタイプFPGAボードを設計しました。

高位言語を使おう

上記の問題はCPU/GPU/FPGAアーキテクチャの差異によるものですし、intelが力を入れて取り組んでいる分野でもあるので、intel oneAPIが次第に解決してくれるだろうとぼんやり考えていたのですが、その橋渡しになるであろうintelHLSが、お世辞にも開発に使うには厳しすぎるだろというものでした。

例えば、VS2010が必要だったり、MAX10とかその規模でHLSいらないよね?みたいな状況でした。*2

この記事を書いている時点で、Quartus Prime Pro v21.1ではVS2017になっています。また、HLSが使用できるデバイスが、ALMアーキテクチャFPGAに限定されています。
やっとか。という思いもありますが…時間をかけてインテルのマーケが市場調査を行い、HLSを周回軌道に乗せたようにも見えます。

intel HLSについてはまた次の記事で扱います。
弊社の場合、HLSはHLAC特徴量の演算モジュール、FCNの畳み込み層の高速化モジュールの設計のみに使用します。
手っ取り早くAvalon-MM/STモジュールにすればこっちのもの。
もちろんいずれもHDLでの記述ができないものではありません。が、この分野は年々更新されるため新しい手法にも追いつきつつ検証、試行するにはHLSが不可欠となってきています。

バイスの選定

Cyclone V EとCyclone10GXを天秤にかけました。
Cyclone10GXは(ほぼ)Arria 10と言えます。ウェハもおそらく同じものでしょう。フリーライセンスで使えるArria10。
フリーライセンスでDDR3/DDR4使う、高速トランシーバGXBを使うとなるとCyclone10GXが魅力的です。

また、車載に使う場合も考慮します。電力バカ食いされるとポンコツ欧州車でECUエラーになりやすいので、LEは控えめなデバイスを選定しました。(86kLE~105kLE)
対策すりゃいいんだけど、車載で電力消費が多いのはそもそも歓迎されません。車に収まるレベルの小さいバッテリーで動いてるわけだし。

その他のFA用途でも、消費電力は少ないに越したことはありません。
ちょうどCyclone10LPとロジック規模がクロスする、85kLE~105kLEのCyclone 10GXを採用しています。
Cyclone10GX基板、公式も含め評価ボードがすごく少ないんですよね。あと220kLEの最大規模のデバイスを採用している場合がほとんどで消費電力はそれなりに大きく、電源回路も電力消費も豪華。笑

基板設計も製造も今年は、(来年も期待はできませんが)設計サイド、量産サイドで考えると、気が遠くなるような芳しくない条件が重なります。

半導体の供給が遅れている

半導体がない、互換品ももちろん売り切れ、生産目処が立たないためベンダーは製品ラインナップから終息製品に突如移行。水晶発振器は旭化成の火災の影響で欲しいスペックのが突然のディスコン。今日あっても明日あるかわからない。
というわけで、設計始める前に100pcs~1000pcs単位で材料を集めました。ほんとこれからどうするんだろう。この設計スタイルはギャンブルでしかない。
今回、電圧も電流も異なる電源を複数必要としているため、本来であればそれぞれ最適な定格のものを選ぶのですが、大量に在庫しなくてはならない都合上、最大スペックに合わせています。電源ICもバリアントが増えると調達リスクが嵩みます。リスクとコストのギリギリを攻めています。

ロームさん、および代理店のご厚意により弊社でも実績のあるFPGA用超低リップルDCDCをサンプル提供いただきました。
また水晶発振器は調達の関係でルネサス大真空、京セラとすべて国産品になってます。

ハーネスが資材供給の遅れから作れない

また、ハーネスの問題も無視できません。如何に深刻なのか、少し例をあげます。
以下は公然の秘密で、スズキ本社に問い合わせたところ「隠しているつもりはない、書いて構わない」とのことでしたので例を挙げます。

ワゴンR
・SWIFT

これらは2021年春にフルモデルチェンジの新型が出る予定でした。ワゴンRはスライド式自動ドアがつきます。今までのワゴンRの立ち位置はハスラーが今後担います。(荷室が大きい、普通の4ドア)

SWIFTは5ナンバーを死守しつつの完全なモデルチェンジでした(除くスイスポ)。
SWIFTに関しては5年が経過しており、全車速対応の車線変更支援などADASを強化した完全モデルチェンジが出る予定でした。またADASのメインはデュアルカメラでしたが、こちらも挑戦的な変更がされるはずでした。我々アフターにも他に色々情報だけきておりますが、言って良いのはここまでとのことです。

これに関して、もうカタログまで印刷、プロトは倉庫にある状態です。取りやめたのは、

  • Renesasのデバイスの供給停止
  • BOSCHのあんちくしょうが(ry
  • ハーネス、主に車載向けのコネクタ樹脂がない

実は1番目は今年中に解決されそうです(また火災が起きたりしなければ)。B社も今年中にはどうにかしてくれそうとのこと。ですが、3番目が来年末にも解決が難しそうとのこと。

すくなくとも今年は専用ハーネス作ったり、設計するのは本当にやめた方がいい


あ、そうそう。ジムニーの納車が遅いのは仕様です。別に特段遅れてはいないそうですよ。うちもまだ来ないんだけど。。。


というわけで、LVDS、高速トランシーバともにUSB Type-Cコネクタを設け、そこから入出力しています。
このようにすれば、大量に家電量販店に出回っているUSB Type-C(USB3.0用)ケーブルを流用できます。残念ながらUSBとしてのプロトコルスタックは用意するつもりがないので互換性はありませんが、事故を避けるため、cc端子は5.1kΩでプルダウンして、5V,3AをUSB PDに則って取得しています。Type-C対応のモバイルバッテリーがあれば給電できます。15W有れば今回使用するCyclone10GXの電力を十分賄えます。

FAギョーカイだとよくやるパターンですね。USB3に対応しているType-Cコネクタを使用すると最大で、差動高速トランシーバTX2ch、RX2ch、差動1ch、GPIO2chを引き伸ばす事ができます。もちろん、そのようにすると代償としてコネクタの表裏が決まってしまうのでそこは対策する必要があるかもしれません(表裏逆の場合は給電止めるなど)。PCIe Gen2 x2を引き回すのに重宝しますね。

基板

仕事後にダラダラ設計やってたので1ヶ月くらいかかってますね。。
Raspberry Pi 3Aと同じフットプリントにしました。前回のCyclone10LPと同じく、Raspiからコンフィグできます。
部品面
f:id:Lynx-EyED:20210704215353p:plain
ハンダ面
f:id:Lynx-EyED:20210704222658p:plain
6層基板です。ドリル径0.3、L/S=5mil、内層クリアランスは10milとしました。2018年ごろから中国、台湾のPCB製造業社でも外層3mil/内層4milくらいまでは可能になってきました。以前は国外のPCB製造業社だと内層はとんでもなくL/Sを緩くしないといけないイメージでしたが、良くなっていますね。板厚は1.6mmくらいであれば、ドリル径0.3mmもあれば特注扱いにはならないと思います。
DDR3Lを搭載しました。データバスはインテルのガイドを参考に3.5mm以上ズレる場合だけ配線長を調整しています。
下記は90%程配線が終わった図。
f:id:Lynx-EyED:20210703223232p:plain
ベタを貼ると内層は見えなくなってしまいますがこんな感じ。
f:id:Lynx-EyED:20210704220503p:plain
Cyclone10GXの5つのバンク*3をフルに使い、4つの機能に分けています。

  • GPIO
    • Raspberry Piとの通信用です。これのために1.8V(FPGA)-3.3V(RasPi)間でレベル変換を使用しています。
  • LVDS
    • ディスプレーなど、低速ではないものの引き回す必要がある通信用
  • DDR3L
    • PCBレイアウトの都合でbankを2つ分跨いでいます。構想段階ではCyclone10LPの時のようにSDR SDRAMを搭載する予定でした。想定している場面が画像データのたたみ込まれたデータの保存だったので、そこまでスピードが要求されないのですが、SDR SDRAMより大容量の方が処理は楽になるため*4採用しました。DDR3の配線はどのピンでも配置できるわけではないので、試しに使用するDDR3のバス幅、CLなどを確認、pin plannerでバンクを指定、合成結果を確認するなどしてデータシートだけではなく合成結果を見た方が良いと思われます。
  • GXB
    • 言わずもがな、高速差動信号用です。車載、FAのフィールドで、以下の使用を想定しています。USB Type-Cケーブルを使って引き回します。
    • PCI Express Gen.2 (x2~)
    • SFP+



次回は、基板完成して、到着しているといいな。

*1:手戻りが少ない

*2:製薬系ではGMPにおけるソフトウェア開発で認証が降りるのはいまだにVS2010のみだったりするので…これはこれで、正しい選択ではあるのかもしれません

*3:4bank + GXB専用bank

*4:たとえば、同一データでも台形補正したもの、オリジナルのデータと2つ以上欲しい場合があります。両者を保存できるとFPGAが他の処理に専念できるため、見た目の処理性能が向上します

高度運転支援向け単眼カメラの実装(4.道路認識エンジンの実装)

前回は全結合ニューラルネットワークの試作までをしました。通常は畳み込み層を多段追加したCNNを利用して画像認識を行います。
一方で、ハードリアルタイムの要件を満たすには、この畳み込み層のえげつない量の行列のドット積を次フレームが来る前に素早く計算していかなければなりません。
学習を済ませた重み係数だけ使うとしてもそれだけは変わりません。計算量が多すぎます。それで、本当に深層学習に頼る必要があるか、それとも一部だけ軽量NNにするのか、線形解、k-NNやSVMで済むのか、といった問題を再考慮しました。

軽量特徴量の調査

画像認識も条件を追加すればもう少し楽になるはずと色々調査しました。
例として、画像認識における回転不変性。これもCNNは各層のフィルタが学習することによって実装されていきます。
車載カメラの場合は物体識別の際に、回転不変性はあまり必要ではないかもしれません。例えば立っている人を認識するのと、倒れている人を認識するのは別フィルタを使用した方が良いでしょう。(後者の事故危険性が高いため、別の認識が必要)

www.aist.go.jp
2005年のニュースであるので、深層学習がはやる前にはなりますが、このHLAC、とても軽量です。
JSで実装されている例
rest-term.com

視点の変更

車、人、自転車、バイク、大型車両....
とセマンティックセグメンテーション方式で認識するのは大変です。
まずは道路とそれ以外を認識すべきと思いました。単純に考えて、前方が道路ではない=これ以上進んではいけないということです。

道路の識別

さきほどのHLACを用いました。先程のブログも参照するとわかる通り、1次のフィルタは画像の明度勾配になっています。2次はエッジの向きが読み取れます。
道路データはドラレコのデータの下1/4くらいを切り取り、(一応、前もって「道路」以外が映り込んでいないことを確認。) 永延とフレームごとにHLAC特徴量を流してヒストグラムを記録して閾値を設定しました。道路条件であまりにもヒストグラムがかけ離れている場合はk-NNなどの手法で、それでも無理な場合は全結合型の少ない段数のNNで分類を行います。
幸い、いまのところ舗装路ではそれほどかけ離れたデータはないため、1つの閾値だけ(つまり線形解) で「道路」を認識しています。

FPGA実装

人為的なバグを減らすため、カメラをFPGA直結でデータを取得するのではなく、PCからストリームを流し込み、計算のみCyclone10LP FPGA上で行い、また計算結果をPC上のSimulinkに返す、Simulinkは計算結果をビデオストリームに重ね合わせて着色する。という方式にしています。

www.youtube.com
青で着色:道路と認識
ピンクで着色:それ以外
撮影区間さいたま市別所坂上(国道17号線:武蔵浦和駅から北浦和方面)

平面テクスチャが多い車両も「道路」とみなされているきらいがありますね...。でもこのくらいであれば認識に問題はなさそうです。
下記の、ビデオのデータと合成していないデータも見ると納得いただけるかと思います。
www.youtube.com

進行方向に車やバイクが現れた場合、大きい「ノイズ」が現れるので、速度抑止制御はそれほど難しくないかと思います。

これから

道路状況の変化をもろに受けているので「ちょっとノイズが強いかな」という感想です。
また、未舗装路の場合も調査しようと思います。
この辺りをもう少し閾値制御でどうにかなるのか調査してみます。