Interface誌2024年12月号の別冊付録「Gowin Vol.4」にGowin FPGAを使ってLUT-Networkを動かす記事が掲載された.その記事ではFPGAボードとしてTang Nano 4K(Sipeed)が使われており,LUT-Networkを使って,カメラ画像を元に手書き数字を認識する内容となっている.デザインは,Sipeedのサンプル・プロジェクトの1つである,「カメラ画像をDVI出力するもの」がベースとして利用されている.
4Kという容量の小さなLUTだけで,MNISTを利用した推論処理を実現しているが,さすがに4Kというのは画像認識処理を行うには小さいため,認識精度も限定的であり,拡張する余裕も少ない(4Kに収まったことがすごいが…).もう少しLUT容量に余裕があれば,認識精度を調整したり,追加の機能を盛り込む余地も生まれ,さらに遊べそうである.
●機能豊富なTang Primer 20K
Tang Primer 20K(Sipeed)はRJ45やUSB,Pmod×4,ミニフォンジャック,HDMIコネクタ,フラットケーブル・コネクタ(LCD),フラットケーブル・コネクタ(カメラ)などをボード上に搭載しており,このボードだけで一通り遊べるので,初心者にもお勧めできるFPGAボードである.
色々な機能を持ったこのボードでLUT-Networkを動かせれば,機能を組み合わせて,面白い機械学習処理を試せるかもしれない.
と,いうことで,Sipeedの提供するTang Primer 20K向けのサンプル・プロジェクトをベースに,Gowin Vol.4 第3部の記事を見ながら,LUT-Networkによる画像識別処理をポーティングした.その際,ベースとして,Sipeedが提供するサンプル・プロジェクトの中から,「カメラ画像をHDMI出力するもの(OV5640_HDMI1024_DDR3)」を利用した.
7000円程度で買えるFPGAボード.色々なI/Fがそろっている.
カメラとしてOV5640(Aliexpressで買える)を使う.これはTang Primer 20Kとはフラットケーブルで接続する.
●Tang Nano 4KとTang Primer 20Kの違い
2つのボード間でポーティングする上で,把握しなければならない違いを挙げる.
・カメラ
OV2640(OmniVision)を使うTang Nano 4Kに対して,Tang Primer 20KはOV5640(OmniVision)を使う.これは流通しているカメラ・デバイスとフラットケーブル・コネクタの関係によるものなので,Tang Primer 20Kに接続できるOV2640があれば,それを使ってもよい.ちなみに,Tang Nano 4Kの画像データ線は10bitであるのに対して,Tang Primer 20Kのそれは8bitである.今回は1ピクセルのデータを2クロックに分けて伝送するモードを利用したので,FPGA側では,16bit/ピクセルで画像データを受け取っている.OV5640の初期化はSCCBで行う.この処理には,サンプル・プロジェクトの1つ「cam2lcd」の「OV5640_LCD480_FIFO」の中に含まれる lut_ov5640_rgb565_480_272.v を利用した.
・RAM
PSRAMを使っているTang Nano 4Kに対して,Tang Primer 20KはDDR3 SDRAMを使っている.ただし,これらはフレームバッファのIPに接続されており,ユーザが違いを意識する必要はない.
●ベースとして使うサンプル・プロジェクト(OV5640_HDMI1024_DDR3)の仕様
▲カメラの初期化
OV5640の初期化モジュールとして,SVGA解像度(1024×768)に対応したものが利用されている.
Gowin Vol.4記事では,カメラの入力解像度はSVGA?になっており,それを間引きすることで,推論用の28×28の画像を得ている.どのみち間引きするので,できることならカメラから取得する画像そのものの解像度を落としたい.Tang Primer 20Kのサンプル・プロジェクト群を調べてみると,480×272の解像度でカメラを利用しているデザインがあったので,そのデザインからOV5640の初期化シーケンス(モジュール)を拝借する.サンプル・プロジェクト群を見ると,OV5640については他にも,800×480(RGB565),1024×768(RGB565),1280×720(RGB565)などを利用できる.
カメラ・デバイスのデータシートは一般には公開されていない場合がほとんどである.従って,カメラの利用シーンでは,このように確実に動く初期化シーケンスが用意されたものの中から,目的に一番近い設定のものを選ぶのが,ホビー用途では現実的な方法であろう.
今回は,推論用として28×28の画像を使うので,その元となるカメラ画像も高い解像度は不要であり,OV5640の初期化には別なモジュールを利用する.
▲DVI出力解像度の違い
ベースとして使ったTang Primer 20Kのサンプル・プロジェクトではSVGA(1024×768)の解像度で,カメラ入力画像を画面一杯にDVI出力している.これを改造して,DVIの出力解像度を1280×720にする.
サンプル・プロジェクトでは,カメラ画像をそのまま画面一杯にDVI出力しているが,今回はDVI出力(1280×720)の一部にカメラ画像をはめ込む.加えて,推論処理の前処理において2値化した28×28の画像も画面に表示する.さらに,MNISTで分類した推論結果(数字の0~9)も画面に簡易表示する.このあたりの処理は,Gowin Vol.4記事の筆者(渕上氏)のプロジェクトをおおむねそのまま使うが,一部改変が必要である.
▲Tang Primer 4K用LUT-Network
移植元として参考にする(パクる)Gowin Vol.4で紹介されたデザインを概観する.主要な処理を右図にまとめた.多くの処理はトップ・モジュールに記述されている.一部は別モジュールに記述されており,それをトップ・モジュールでインスタシエイトして利用している.
右図は作図の都合で「クラス」となっているが,全てトップ・モジュールに記述された処理である.矢印は処理の流れを示す.それぞれの処理グループが含むメンバは,□が参照するだけの信号を表し,〇が駆動する信号を表している(主要な信号のみを抜粋).
DVI同期信号生成の処理では,DVIのIPに送る同期信号と,フレームバッファから画像を引き出すための出力指示信号(syn_de)を生成している.Tang Primer 20K版では,このモジュールは利用せず,Sipeedのサンプル・プロジェクトに含まれている vga_timing.v を改造して使った(どちらを使ってもよいが).
●改変のポイント
ソースコードやIPは,Gowin,Sipeedなど提供元のライセンスを遵守すること.渕上 竜司氏のRTLコードはMITライセンスで公開されている.
▲カメラ入力
Gowin Vol.4のデザインではカメラ画像が,各ピクセル10ビットモノクロとなっている.Tang Primer 20K用のサンプル・プロジェクトは各ピクセル16ビット・カラーとなっているので,2値化処理のあたりは調整が必要(次項の内容)だった.
カメラ初期化用のモジュール lut_ov5640_rgb565_480_272.v は,Sipeedのサンプルからコピーして,あらかじめプロジェクトに追加しておく.
変更後のRTLコード(top.sv)の抜粋
//configure look-up table
lut_ov5640_rgb565_480_272 lut_ov5640_rgb565_480_272_m0(
.lut_index (lut_index ),
.lut_data (lut_data )
);
//CMOS sensor 8bit data is converted to 16bit data
wire cmos_16bit_vsync;
cmos_8_16bit cmos_8_16bit_m0(
.rst (~rst_n ),
.pclk (cmos_pclk ), //カメラへ送るピクセルクロック
.pdata_i (cmos_db ), //カメラから来る画像データ(8bit)
.de_i (cmos_href ), //カメラ水平同期
.pdata_o (cmos_16bit_data ), //出力画像データ(16bit)
.hblank (cmos_16bit_wr ), //水平同期出力
.de_o (cmos_16bit_clk ) //データ・イネーブル出力
);
▲縮小&二価化
カメラ画像から画素を間引いて28×28の画像を作り,各ピクセルを2値化する.
変更後のRTLコード(top.sv)の抜粋
// -----------------------------
// 縮小&二値化
// -----------------------------
logic prev_href,prev2_href,prev3_href, prev_vsync,prev2_vsync;
logic [10:0] cam_x;
logic [9:0] cam_y;
always @(posedge cmos_clk)begin
prev_href <= cmos_href;
prev2_href <= prev_href;
prev3_href <= prev2_href;
prev_vsync <= cmos_vsync;
prev2_vsync <= prev_vsync;
if(prev2_vsync)begin // 行カウンタ
cam_y <= 0;
// ブランキングでラッチ
bin_img <= bin_shr;
end else begin
if({prev3_href, prev2_href} == 2'b01) begin
cam_y <= cam_y +1;
end
end
end
always_ff @(posedge cmos_16bit_clk ) begin
if ( ~cmos_16bit_wr ) begin // 列カウンタ
cam_x <= 0;
end
else begin
cam_x <= cam_x + 1;
end
end
logic [27:0][27:0] bin_shr;
logic [27:0][27:0] bin_img;
always_ff @(posedge cmos_16bit_clk) begin
// 間引いてシフトレジスタにサンプリング
if ( cmos_16bit_wr ) begin
if ( cam_x[8:3] < 28 && cam_y[7:3] < 28 && cam_x[2:0] == 0 && cam_y[2:0] == 0 ) begin
bin_shr <= (28*28)'({(write_data[15:11]+write_data[10:5]+write_data[4:0]) < 30, bin_shr} >> 1); //2値化閾値を変更
end
end
end
▲ LUT-Network 画像認識
ほぼ変更は無し.
LUT-Networkを表すモジュールは,Gowin Vol.4の筆者プロジェクトから取得し,MnistLutSimple.vをあらかじめこのプロジェクトに追加しておく. もちろんBinaryBrainを使って独自に学習し,学習済みモデルを用意してもよい.
変更後のRTLコード(top.sv)の抜粋
// -----------------------------
// LUT-Network 画像認識
// -----------------------------
logic [9:0] mnist_class;
MnistLutSimple // MNIST学習済みのLUT-Network
#(
.USE_REG (0 ),
.USER_WIDTH (0 ),
.DEVICE ("RTL" )
)
u_MnistLutSimple
(
.reset (~rst_n ),
.clk (cmos_16bit_clk ), // 16bit化後のカメラ画像クロック
.cke (1'b1 ),
.in_user ('0 ),
.in_data (bin_img ), // 2価化した画像データ
.in_valid (1'b1 ),
.out_user ( ),
.out_data (mnist_class ), // 推論結果を格納するレジスタ
.out_valid ( )
);
▲DVI画面出力用シグナル・ジェネレータ
syn_genモジュールに代わり,Tang Primer 20Kのサンプル・プロジェクトに含まれている vga_timing モジュールを使う(top.sv).
logic cmos_16bit_clk_half;
//The video output timing generator and generate a frame read data request
wire out_de;
vga_timing vga_timing_m0(
.clk (video_clk),
.rst (~rst_n),
.hs(syn_off0_hs),
.vs(syn_off0_vs),
.de(out_de),
.rd(camera_de)
);
今回作るものは,カメラ入力解像度とDVI出力解像度が異なるので,その制御のために vga_timing モジュールの中に信号を追加する.このモジュールは1280×720を走査し,DVI出力に必要な同期信号を生成するものだが,追加する信号はカメラ画像有効領域の時のみEnableになる信号(rd)である.この信号をフレームバッファに接続することで,フレームバッファからの画像出力を制御する.
vga_timingモジュールの変更部分(vga_timing.v)
省略
output reg rd // 追加する信号
);
parameter H_ACTIVE = 16'd1280; //1280 1920
parameter H_FP = 16'd110; //110 88
parameter H_SYNC = 16'd40; //40 44
parameter H_BP = 16'd220; //220 148
parameter V_ACTIVE = 16'd720; //720 1080
parameter V_FP = 16'd5; //5 4
parameter V_SYNC = 16'd5; //5 5
parameter V_BP = 16'd20; //20 36
parameter HS_POL = 1'b1;
parameter VS_POL = 1'b1;
parameter RD_H = 16'd480;
parameter RD_V = 16'd272;
parameter H_TOTAL = H_ACTIVE + H_FP + H_SYNC + H_BP;//horizontal total time (pixels)
parameter V_TOTAL = V_ACTIVE + V_FP + V_SYNC + V_BP;//vertical total time (lines)
reg hs_reg; //horizontal sync register
reg vs_reg; //vertical sync register
reg[11:0] h_cnt; //horizontal counter
reg[10:0] v_cnt; //vertical counter
reg h_active; //horizontal video active
reg v_active; //vertical video active
assign hs = hs_reg;
assign vs = vs_reg;
assign de = h_active & v_active;
/*有効領域 de信号生成*/
always@ (posedge clk)begin
if(rst == 1'b1)
rd <= 0;
else begin
rd <= (h_cnt > (H_BP + H_SYNC -2)) & (h_cnt < (H_BP + H_SYNC + RD_H -1)) &
(v_cnt > (V_BP + V_SYNC -2)) & (v_cnt < (V_BP + V_SYNC + RD_V -1));
end
end
省略
▲フレームバッファ
良く出来ていて,入出力のクロックドメインが違っても,このIPが自動で対応してくれる.今回入力については,カメラから来る画像データをひたすら流し込むだけだったので,特に難しさはなかった.出力については,ピクセルクロックに同期して画像データを出力してくれるが,画像データを止めたり出したりする信号(rd,DVI画面出力用シグナル・ジェネレータで作る)の扱い方だけ分かれば,他に難しいところはない.まぁそこが分からずハマったのだが…
フレームバッファ・モジュールはメモリIPに接続されている.物理メモリはTang Nano 4KではPS RAMだったが,Tang Primer 20KではDDR3 SDRAMとなっている.この違いはメモリIPが吸収するので特に意識する必要はない.
変更後のRTLコード(top.sv)の抜粋
wire camera_de; // シグナル・ジェネレータに駆動されるデータ出力指示
Video_Frame_Buffer_Top Video_Frame_Buffer_Top_inst
(
.I_rst_n (init_calib_complete ),//rst_n ),
.I_dma_clk (dma_clk ), //sram_clk ),
`ifdef USE_THREE_FRAME_BUFFER
.I_wr_halt (1'd0 ), //1:halt, 0:no halt
.I_rd_halt (1'd0 ), //1:halt, 0:no halt
`endif
// video data input カメラ画像入力
.I_vin0_clk (cmos_16bit_clk ), // 入力クロック
.I_vin0_vs_n (~cmos_vsync ),//只接收负极性
.I_vin0_de (cmos_16bit_wr ), // 入力イネーブル
.I_vin0_data (write_data ), // 入力データ(16bit)
.O_vin0_fifo_full ( ),
// 画像出力
.I_vout0_clk (video_clk ), // 出力クロック
.I_vout0_vs_n (~syn_off0_vs ),//只接收负极性
.I_vout0_de (camera_de ), // Enableの時のみ画像データを出力
.O_vout0_den (off0_syn_de ), // 出力イネーブル
.O_vout0_data (off0_syn_data ), // 出力データ
.O_vout0_fifo_empty ( ),
// ddr write request
.I_cmd_ready (cmd_ready ),
.O_cmd (cmd ),//0:write; 1:read
.O_cmd_en (cmd_en ),
.O_app_burst_number (app_burst_number ),
.O_addr (addr ),//[ADDR_WIDTH-1:0]
.I_wr_data_rdy (wr_data_rdy ),
.O_wr_data_en (wr_data_en ),//
.O_wr_data_end (wr_data_end ),//
.O_wr_data (wr_data ),//[DATA_WIDTH-1:0]
.O_wr_data_mask (wr_data_mask ),
.I_rd_data_valid (rd_data_valid ),
.I_rd_data_end (rd_data_end ),//unused
.I_rd_data (rd_data ),//[DATA_WIDTH-1:0]
.I_init_calib_complete(init_calib_complete)
);
localparam N = 7; //delay N clocks
reg [N-1:0] Pout_hs_dn ;
reg [N-1:0] Pout_vs_dn ;
reg [N-1:0] Pout_de_dn ;
always@(posedge video_clk or negedge rst_n)
begin
if(!rst_n)
begin
Pout_hs_dn <= {N{1'b1}};
Pout_vs_dn <= {N{1'b1}};
Pout_de_dn <= {N{1'b0}};
end
else
begin
Pout_hs_dn <= {Pout_hs_dn[N-2:0],syn_off0_hs};
Pout_vs_dn <= {Pout_vs_dn[N-2:0],syn_off0_vs};
Pout_de_dn <= {Pout_de_dn[N-2:0],out_de};
end
end
//---------------------------------------------
wire lcd_vs,lcd_de,lcd_hs,lcd_dclk;
assign lcd_vs = Pout_vs_dn[4]; //syn_off0_vs;
assign lcd_hs = Pout_hs_dn[4]; //syn_off0_hs;
assign lcd_de = Pout_de_dn[4]; //off0_syn_de;
assign lcd_dclk = video_clk; //video_clk_phs;
▲表示要素のオーバーレイ描画
Gowin Vol.4を参考に画面描画部分をポーティングした.DVI出力画面の桁と行(x,y)を走査し,ピクセル位置に応じて,
- 2値化画像
- 推論結果
の画素値をDVIのIPに送るため,レジスタに入れる.
カメラ解像度に合わせて多少変更(top.sv).
// -----------------------------
// 表示画像オーバーレイ
// -----------------------------
logic prev_de;
logic [11:0] dvi_x;
logic [10:0] dvi_y;
always_ff @(posedge video_clk ) begin
prev_de <= lcd_de;
if ( ~lcd_de ) begin // 桁カウンタ
dvi_x <= 0;
end
else begin
dvi_x <= dvi_x + 1;
end
if ( lcd_vs ) begin // 行カウンタ
dvi_y <= 0;
end
else begin
if ( {prev_de, lcd_de} == 2'b10 ) begin
dvi_y <= dvi_y + 1;
end
end
end
localparam int BIN_X = 50;
localparam int BIN_Y = 1;
logic bin_en;
logic bin_view;
always_ff @(posedge video_clk ) begin
if ( dvi_x[10:4] >= BIN_X && dvi_x[10:4] < BIN_X+28
&& dvi_y[9:4] >= BIN_Y && dvi_y[9:4] < BIN_Y+28 ) begin
bin_en <= 1;
bin_view <= bin_img[dvi_y[9:4]-BIN_Y][dvi_x[10:4]-BIN_X];
end
else begin
bin_en <= 0;
bin_view <= 0;
end
end
localparam int MNIST_X = 1;
localparam int MNIST_Y = 18;
logic mnist_en;
logic mnist_view;
always_ff @(posedge video_clk ) begin
if ( dvi_x[10:5] >= MNIST_X && dvi_x[10:5] < MNIST_X+10
&& dvi_y[9:5] >= MNIST_Y && dvi_y[9:5] < MNIST_Y+1 ) begin
mnist_en <= 1;
mnist_view <= mnist_class[dvi_x[10:5]-MNIST_X];
end
else begin
mnist_en <= 0;
mnist_view <= 0;
end
end
▲GowinのDVI用IP
特にDVIであることを意識しなくても,必要な信号をつなぐだけでちゃんと動くので使いやすい.
このIPは2つのクロックを必要とする.CLKDIVは分周器である.DVI出力用のシリアル・クロック(tmds_clk)をPLLで生成し,それを分周してピクセル・クロックを生成している(top.sv).
画像データは,ピクセル位置(桁と行)に応じて,次の4つのソースからピクセルデータを選択し,DVIのIPの .I_rgb_r,.I_rgb_g,.I_rgb_bに入力する.
- カメラの元画像
- 2値化画像
- 推論結果
- 背景
CLKDIV u_clkdiv // 分周器
(.RESETN(hdmi4_rst_n)
,.HCLKIN(serial_clk) // clk x5 シリアルクロック入力
,.CLKOUT(video_clk) // clk x1 DVIビデオクロック出力
,.CALIB (1'b1)
);
defparam u_clkdiv.DIV_MODE="5";
defparam u_clkdiv.GSREN="false";
DVI_TX_Top DVI_TX_Top_inst
(
.I_rst_n (hdmi4_rst_n ), //asynchronous reset, low active
.I_serial_clk (serial_clk ), // DVI伝送用シリアル・クロック
.I_rgb_clk (lcd_dclk ), // 遅延させたpixel clock
.I_rgb_vs (lcd_vs ), // 遅延させた垂直同期信号
.I_rgb_hs (lcd_hs ), // 遅延させた水平同期信号
.I_rgb_de (lcd_de ), // 遅延させた画像データ・イネーブル
.I_rgb_r ( off0_syn_de? {off0_syn_data[4:0],3'b0}: bin_en?{8{bin_view}}: mnist_en? {8{mnist_view}}: dvi_x), //tp0_data_r
.I_rgb_g ( off0_syn_de? {off0_syn_data[10:5],2'b0}: bin_en?{8{bin_view}}: mnist_en? {8{mnist_view}}: dvi_y), //,
.I_rgb_b ( off0_syn_de? {off0_syn_data[15:11],3'b0}: bin_en?{8{bin_view}}: mnist_en? {8{mnist_view}}: 8'hff), //,
.O_tmds_clk_p (O_tmds_clk_p ),
.O_tmds_clk_n (O_tmds_clk_n ),
.O_tmds_data_p (O_tmds_data_p ), //{r,g,b}
.O_tmds_data_n (O_tmds_data_n )
);
●形になった
画面出力できて,何とか形になりました.差し当たって,Tang Nano 4K版と同じ動きができています.
ベースとなるデザインができました.LUTが20Kあるので,色々遊べそうです.
・BinaryBrainを使ってもっと認識率の高い学習済みモデルを作る
・カメラ画像以外のデータと合わせた認識
参考文献
(1)リポジトリ
https://github.com/Lathe-Mariel/TangPrimer20K_LUT-Network/tree/main/primer20k/OV5640_HDMI1024_DDR3