はじめに
今回は、FPGAとソフトの両方を開発しようという内容になります。
XilinxにはZynqというCPUとFPGAが1つのパッケージ化されたSoCがあります。
今回はそれを使用します。
作るモノは前からカメラ関連をやりたいと思っていたので、それを題材とします。
さすがに1からシステムを作るのは骨が折れるので、ベースを使用してそこから簡単な機能を追加という形にしていきます。
ベースはDigilentのpcam-5cというプロジェクトを使用します。
動作環境
PC:M4 Macbook Pro(Sequoia 15.1)
SW IDE:Vitis 2023.1
FPGA IDE:Vivado 2023.1
ディスプレイ:HDMI接続できるやつ
評価ボード:Zybo Z-10(今買うと5万超えるんですね…めっちゃ値上がりしてる)
カメラモジュール:Pcam 5C(OV5640)
カメラモジュール仕様↓
実施する場合の注意点
・IDEはWindows版かLinux版しかないためどちらかで実施してください。
また、そこそこのPCスペック(特にメモリ)を要求してきますのでご注意を。
(今回に関して言うと、16GBあれば動くと思います。)
私みたいにMac(Appleシリコン)でどうしてもやりたいんだ!という方は以下を実施すればできます。
※ただし全然おすすめはしないです
既存システムの概要
ざっくりとしたシステム構成を書きます。
システム構成
処理の流れとしては以下ですね。
・OV5640からMIPIでカメラデータを受け取る
・カメラデータをPL部で画像処理する
・DMAを使用しカメラデータをDDRへ保存する
・出力タイミングにあわせ、DDRからデータを抜き出し、TMDSでディスプレイに送信する
クロック&リセット系統
バス構成
モジュール詳細(PL)
MIPI_D_PHY_RX/MIPI_CSI_2_RX
カメラからのMIPI信号を受信するモジュール。
D_PHYの仕様
・AXI4-Liteよりレジスタアクセス可能
・リファレンスクロックとして200MHzを入力
CSI_2の仕様
・AXI4-Liteよりレジスタアクセス可能
・出力のデータタイプはRAW10(データの並びはBGBG/GRGR)
・MIPI信号をAXI4-Stream(150MHz)へ変換し出力
・AXI4-Streamのフォーマット
AXI_BayreToRGB
ベイヤー配列をRGBに変換する(デモザイク)モジュール。
モジュールの仕様
・入力データサイズ:40bit(RBG10bit)
・入力データサンプル数:4/clk
・出力データサイズ:32bit(RBG10bit + 2bitsのパディング)
・出力データサンプル数:1/clk
・最大画素数:2048pix
・ラインバッファを持つ(1ライン分)
・TUSERはフレームスタートの意味を付与する
・TLASTはライン終了の意味を付与する
入力データのイメージ
デモザイクの処理のイメージ
GammaCorrection
ガンマ補正を行うモジュール。
モジュールの仕様
・入出力IF:AXI4-Stream
・入力データサイズ:32bit(RBG10bits + 2bitsのパディング)
・入力データサンプル数:1/clk
・出力データサイズ:24bit(RGB8bits)
・出力データサンプル数:1/clk
・AXI4-Liteよりレジスタアクセス可能
レジスタでガンマ補正値を変更できる(補正値はROM持ち)
設定可能ガンマ補正値:1.0、1.5、1.8、2.2
axi_vdma(VDMA)
PLからのデータをPS制御でメモリへ送る(逆も然り)モジュール。
モジュールの仕様
・S2MMのデータ幅:24bit
バーストサイズ:8
ラインバッファサイズ:1024
メモリマップのデータ幅:64
・MM2Sのデータ幅:24bit
バーストサイズ:8
ラインバッファサイズ:1024
メモリマップのデータ幅:64
・フレームバッファを持つ(3つ)
・アドレス幅は32bit
・AXI4-Liteよりレジスタアクセス可能
v_tc(Video Timing Controller)
ビデオタイミングの生成/検出を行うモジュール。
モジュールの仕様
・ビデオタイミングの検出モードは使用せず、生成モードのみ使用
・ビデオフォーマットは720p
・AXI4-Liteよりレジスタアクセス可能
v_axi4s_vid_out(Video Out)
AXI4-Streamデータをビデオデータ(RGB)に変換し、出力するモジュール。
上記のvtcとセットで使用する方がいい。
モジュールの仕様
・AXI4-Streamのデータフォーマットは「RBG」
・FIFOの深さは1024
rgb2dvi
ビデオデータをTMDSに変換し、出力するモジュール。
モジュールの仕様
・リセット信号(aRst_n)は前段のv_axi4s_vid_outモジュールのロック信号を入力とする
・PixelClkとSerialClkを入力する必要があり、以下の関係であること
SerialClk = PixelClk × 5
※SerialClkは内部で作成も可
モジュール詳細(PS)
ファイル構成
フローチャート
シーケンス図(画像フォーマット変更時)
追加機能
ここからが本題になります。
簡単な機能を1つ追加したいと思います。
既存のシステムがカラー画像なので、これをグレースケール化する機能を追加します。
仕様
・入出力のデータフォーマットはAXI4-Streamとする。
中のデータ構成は前後のモジュールと同じ。
・クロックリセット構成は前後のモジュールと同じ。
・AXI4-Liteよりレジスタアクセス可能。
・グレースケール化の方法は以下の通り。とりあえず楽な最大値/最小値方法で実装する。
方法 | 内容 |
---|---|
最大値/最小値 | RGBの最大値・最小値を取得し設定する方法 |
平均値 | RGBを単純に平均し設定する方法 |
NTSC重み付け | NTSCという規格に準拠し設定する方法 |
チャネル選択 | RGBのどれか1つのチャネルを取得し設定する方法 |
ブロック図
上記が機能追加後のブロック図になります。
BayerToRGBモジュールとGammmaCorrectionモジュールの間に入れました。
クロック&リセット系統
バス接続
フローチャート
シーケンス図(画像フォーマット変更時)
実装
ソースコード
PL部
module grayscale_top #(
parameter integer C_S_AXIS_DATA_WIDTH = 32
) (
input wire CLK,
input wire RESET,
// S AXI-Stream interface
output wire S_AXIS_VIDEO_TREADY,
input wire [C_S_AXIS_DATA_WIDTH-1:0] S_AXIS_VIDEO_TDATA,
input wire S_AXIS_VIDEO_TVALID,
input wire S_AXIS_VIDEO_TUSER,
input wire S_AXIS_VIDEO_TLAST,
// M AXI-Stream interface
input wire M_AXIS_VIDEO_TREADY,
output wire [C_S_AXIS_DATA_WIDTH-1:0] M_AXIS_VIDEO_TDATA,
output wire M_AXIS_VIDEO_TVALID,
output wire M_AXIS_VIDEO_TUSER,
output wire M_AXIS_VIDEO_TLAST,
// Grayscale
input wire GRAYSCALE_EN,
input wire GRAYSCALE_PTN
);
wire [ 9: 0] w_red;
wire [ 9: 0] w_green;
wire [ 9: 0] w_blue;
reg [ 9: 0] r_max;
reg r_axis_video_tready;
reg r_axis_video_tvalid;
reg r_axis_video_tuser;
reg r_axis_video_tlast;
assign w_green = S_AXIS_VIDEO_TDATA[ 9: 0];
assign w_blue = S_AXIS_VIDEO_TDATA[19:10];
assign w_red = S_AXIS_VIDEO_TDATA[29:20];
always @(posedge CLK or posedge RESET) begin
if (RESET) begin
r_max <= 10'd0;
end else begin
if (S_AXIS_VIDEO_TVALID) begin
r_max <= (w_red > w_green && w_red > w_blue) ? w_red : (w_green > w_red && w_green > w_blue) ? w_green : w_blue;
end else begin
r_max <= 10'd0;
end
end
end
always @(posedge CLK or posedge RESET) begin
if (RESET) begin
r_axis_video_tready <= 1'b0;
r_axis_video_tvalid <= 1'b0;
r_axis_video_tuser <= 1'b0;
r_axis_video_tlast <= 1'b0;
end else begin
r_axis_video_tready <= M_AXIS_VIDEO_TREADY;
r_axis_video_tvalid <= S_AXIS_VIDEO_TVALID;
r_axis_video_tuser <= S_AXIS_VIDEO_TUSER;
r_axis_video_tlast <= S_AXIS_VIDEO_TLAST;
end
end
assign S_AXIS_VIDEO_TREADY = (GRAYSCALE_EN) ? r_axis_video_tready : M_AXIS_VIDEO_TREADY;
assign M_AXIS_VIDEO_TDATA = (GRAYSCALE_EN) ? {2'b00, r_max[9:0], r_max[9:0], r_max[9:0]} : S_AXIS_VIDEO_TDATA;
assign M_AXIS_VIDEO_TVALID = (GRAYSCALE_EN) ? r_axis_video_tvalid : S_AXIS_VIDEO_TVALID;
assign M_AXIS_VIDEO_TUSER = (GRAYSCALE_EN) ? r_axis_video_tuser : S_AXIS_VIDEO_TUSER;
assign M_AXIS_VIDEO_TLAST = (GRAYSCALE_EN) ? r_axis_video_tlast : S_AXIS_VIDEO_TLAST;
endmodule
解説(PL部)
まず、レジスタは以下になります。
GRAYSCALE_EN:0:OFF、1:ON
GRAYSCALE_PTN:使用していない
PTNはグレースケール化のパターンを割り振りたいと思って一応持っておきました。
実際は使ってないです。
処理としてはマスターからのTVALID信号で最大値を取得し、他の信号も一段ずらしてスレーブに投げているだけになります。
波形
以下が、シミュレーションの波形になります。
最大値が選ばれて出ていますね。
PS部
void pipeline_mode_change(AXI_VDMA<ScuGicInterruptController> &vdma_driver,
OV5640 &cam, VideoOutput &vid, Resolution res,
OV5640_cfg::mode_t mode)
{
// Bring up input pipeline back-to-front
{
vdma_driver.resetWrite();
MIPI_CSI_2_RX_mWriteReg(XPAR_MIPI_CSI_2_RX_0_S_AXI_LITE_BASEADDR,
CR_OFFSET, (CR_RESET_MASK & ~CR_ENABLE_MASK));
MIPI_D_PHY_RX_mWriteReg(XPAR_MIPI_D_PHY_RX_0_S_AXI_LITE_BASEADDR,
CR_OFFSET, (CR_RESET_MASK & ~CR_ENABLE_MASK));
cam.reset();
}
{
vdma_driver.configureWrite(timing[static_cast<int>(res)].h_active,
timing[static_cast<int>(res)].v_active);
Xil_Out32(GAMMA_BASE_ADDR, 3); // Set Gamma correction factor to 1/1.8
Xil_Out32(GRAYSCALE_BASE_ADDR, 0); // Set GrayScale mode to disabled
// TODO CSI-2, D-PHY config here
cam.init();
}
{
vdma_driver.enableWrite();
MIPI_CSI_2_RX_mWriteReg(XPAR_MIPI_CSI_2_RX_0_S_AXI_LITE_BASEADDR,
CR_OFFSET, CR_ENABLE_MASK);
MIPI_D_PHY_RX_mWriteReg(XPAR_MIPI_D_PHY_RX_0_S_AXI_LITE_BASEADDR,
CR_OFFSET, CR_ENABLE_MASK);
cam.set_mode(mode);
cam.set_awb(OV5640_cfg::awb_t::AWB_ADVANCED);
}
// Bring up output pipeline back-to-front
{
vid.reset();
vdma_driver.resetRead();
}
{
vid.configure(res);
vdma_driver.configureRead(timing[static_cast<int>(res)].h_active,
timing[static_cast<int>(res)].v_active);
}
{
vid.enable();
vdma_driver.enableRead();
}
}
解説1(PS部)
pipeline_mode_change()関数にグレースケールレジスタ制御を追加しただけになります。
case 'i':
xil_printf(
" Please press the key corresponding to the desired GrayScale "
"change:\r\n");
xil_printf(" 1. Enable GrayScale Mode\r\n");
xil_printf(" 2. Disable GrayScale Mode\r\n");
read_char1 = getchar();
getchar();
xil_printf("Read: %d\r\n", read_char1);
switch (read_char1)
{
case '1':
Xil_Out32(GRAYSCALE_BASE_ADDR, 1);
xil_printf("Enabled GrayScale Mode\r\n");
break;
case '2':
Xil_Out32(GRAYSCALE_BASE_ADDR, 0);
xil_printf("Disabled GrayScale Mode\r\n");
break;
default:
xil_printf(
" Selection is outside the available options! Please "
"retry...\r\n");
}
default:
xil_printf(
"\r\n Selection is outside the available options! Please "
"retry...");
解説2(PS部)
main関数のswitch文に”i”条件を追加しただけになります。
実機動作
最後に
今回は以上になります。
グレースケール化のみの追加でしたので、然程難しくはなかったと思います。
次はHLSを絡ませたり、ソフト側でなにかをやりたいと思っています。