このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
こんにちは、フィックスターズ新規事業推進室の大澤です。
前回の記事では、Ultra96 ボード上でカメラ画像を取得する環境の構築方法と簡単なテストの動かし方についてご紹介しました。今回は、技術的な観点からこのシステムの説明をしていきたいと思います。いくつかポイントを絞って説明します。
まずはブロックデザインの作り方です。
今回のシステムに必要な IP は何かと考えてみると、MIPI CSI のレシーバー、受けた画像データを DRAM に転送する DMA が最低限必要です。我々は、Xilinx IP の MIPI CSI-2 Rx Subsystem と AXI Video Direct Memory Access を使うことにしました。
カメラからは RAW 画像が送られてくるので、これを RGB (もしくは YUYV など) に変換する必要があります。ここは Halide で記述し、我々の Halide FPGA バックエンドと使用して FPGA IP を生成しました。この IP は Demosaic_root という名前の IP です。
これら IP をメインにブロックをデザインします。
Add IP… で Zynq UltraScale+ MPSoC を追加し、Customize IP… で設定変更を行います。
PS は起動時に FSBL によって設定が行われるのでここできっちりやる必要はないのですが、ブロックデザインを構築するために必要な項目だけ設定しておきます。
変更場所 | 変更項目 | 変更内容 | 変更理由 |
---|---|---|---|
PS-PL Configuration | General -> Interrupts -> PL to PS の IRQ0[0-7] | 0 から 1 に変更 | VDMA 割り込みを受けるため |
PS-PL interfaces | Mater Interface の AXI HPM0 FPD | チェックを入れる | AXI-Lite 設定用 |
PS-PL interfaces | Slave Interface の AXI HP の AXI HPC0 FPD | チェックを入れる | VDMA から DRAM へ画像データを転送させるため |
Add IP… で MIPI CSI-2 Rx Subsystem を追加し、Customize IP… で設定変更を行います。Ultra96 では CSI0, CSI1 の2系統が使えますが、このうち CSI0 系統の信号を使おうと思いますので、Pin Assignment で CSI0 系統の信号を指定します。Pixel Format に関しては、Pcam-5C カメラと Raspberry Pi カメラ v2 の共通フォーマットは RAW8 もしくは RAW10 であり、RAW8 か RAW10 かどちらでも良さそうですが、Digilent 社の Pcam-5C reference design を流用している事情もあり、RAW10 を採用します。
変更場所 | 変更項目 | 変更内容 | 変更理由 |
---|---|---|---|
Configuration タブの Subsystem Options | Pixel Format | RAW8 から RAW10 に変更 | ソースコード流用の都合上 |
Configuration タブの Subsystem Options | Serial Data Lanes | 1 から 2 に変更 | MIPI ボードの制約のため、2レーンまでしか使えない |
Configuration タブの VFB Options | Pixels Per Clock | 1 から 4 に変更 | カメラは 4 x RAW10 / pclk でデータを送信するため |
Shared Loig タブの Shared Logic | Select whether … | Include Shared Logic in core を選択 | MMCM と PLL を持たせるため |
Pin Assignment タブ | Clock Lane の Pin Loc | T3 から N2 に変更 | CSI0 系統の信号を使うため |
Data Lane0 の Pin Loc | P3 から N5 に変更 | ||
Data Lane1 の Pin Loc | U2 から M2 に変更 | ||
Data Lane2 の Pin Loc | R4 から M5 に変更 | ||
Data Lane3 の Pin Loc | R1 から L2 に変更 |
Add IP… で AXI Video Direct Memory Access を追加し、Customize IP… で設定変更を行います。
変更場所 | 変更項目 | 変更内容 | 変更理由 |
---|---|---|---|
Basic タブの Write Channel | Memory Map Data Width | 64 から 128 に変更 | PS の S_AXI_HPC0_FPD0 のデータ幅に合わせるため |
Basic タブの Read Channel | Enable Read Channel | チェックを外す | Read は不要なため |
Add IP… で Demosaic_root を追加します。設定変更は必要ありません。
MIPI CSI-2 Rx Subsystem は 200MHz クロックを必要とするので、Clocking Wizard で 200MHz を作ります。Add IP… で Clocking Wizard を追加し、Customize IP… で設定変更を行います。
変更場所 | 変更項目 | 変更内容 | 変更理由 |
---|---|---|---|
Output Clocks タブ | clk_out1 の Output Freq (MHz) の Requested | 100.000 から 200.000 に変更 | 200MHz クロックを生成するため |
Clocking Wizard は active high な reset を必要とするので、active low な reset を反転させて作ります。Add IP… で Utility Vector Logic を追加し、Customize IP… で設定変更を行います。
変更場所 | 変更項目 | 変更内容 | 変更理由 |
---|---|---|---|
トップ画面 | C_SIZE | 8 から 1 に変更 | 1 ビットしか使わないため |
トップ画面 | C_OPERATION | not を選択 | 反転ロジックとするため |
以下のようにポート間を手動で接続します。
ポート | ポート |
---|---|
mipi_csi2_rx_subsyst_0 の video_out | demosaic_root_0 の p_idata |
demosaic_root_0 の p_odata | axi_vdma_0 の S_AXIS_S2MM |
axi_vdma_0 の M_AXI_S2MM | zynq_ultra_ps_e_0 の S_AXI_HPC0_FPD |
axi_vdma_0 の s2mm_introut | zynq_ultra_ps_e_0 の pl_ps_irq[0:0] |
zynq_ultra_ps_e_0 の pl_resetn0 | util_vector_logic_0 の Op1 |
util_vector_logic_0 の Res | clk_wiz_0 の reset |
clk_wiz_0 の clk_out1 | mipi_csi2_rx_subsyst_0 の dphy_clk_200M |
この後は、Run Connection Automation で、信号線をすべて選んで自動的に接続します。
最後に、mipi_csi2_rx_subsyst_0 の mipi_phy_if で Make external して、ブロックデザインは完成です。
AISTARVISION MIPI Adapter V2.1 ボード にはカメラコネクタが複数あります。Pcam-5C カメラ、Raspberry Pi カメラ v2 の場合は、J5 コネクタもしくは J9 コネクタのどちらかに接続して使います。カメラをどちらのコネクタで使うかによって、ジャンパーピンの設定が必要です。ところが、「J5 コネクタにカメラを繋いだ場合はこんな風にジャンパーピンを刺してください」というような親切な説明がありません。回路図があるので、それを読んで適切にやりなさい、ということのようです。
それではMIPI ボードの回路図を見てみましょう。Ultra96 の Hardware User Manual も併せて見ます。
MIPI ボードは、Ultra96 ボードと HS コネクタ、LS コネクタを経由して接続されています。HS コネクタの方を見てみましょう。
MIPI1_XX とか MIPI2_XX とかが MIPI の信号です。Ultra96 側の HS コネクタ(下の表)と照らし合わせてみると、MIPI1 系統が Ultra96 上の CSI0 系統に、MIPI2 系統が同 CSI1 系統に対応することが分かります。
Ultra96 って CSI1 系統は 2レーンまでのサポートなんですね。我々が使おうと思っている CSI0 系統は 4レーンまでいけます。ただ、今回は 2レーンしか使いませんが。
Ultra96 上では CSI0_C+, CSI0_C-, CSI0_D0+, CSI0_D0-, CSI0_D1+, CSI1_D1- の信号を使うことになります。MIPI ボード上では、それらはそれぞれ MIPI1_CP, MIPI1_CN, MIPI1_D0P, MIPI1_D0N, MIPI1_D1P, MIPI1_D1N が対応します。
MIPI ボード上で、MIPI1 系統がどこに繋がっているか、回路図を調べてみると、J10 コネクタと J5 コネクタであることが分かります。
Pcam-5C カメラ、Raspberry Pi カメラ v2 は J5 コネクタか J9 コネクタのどちらかに刺すので、J5 コネクタを使うことになります。
J5 コネクタには、データレーンは 2レーンしかありません。ブロックデザインの際、MIPI の Serial Data Lanes の値を 2 にしましたが、4 ではなく 2 にした理由はこれです。
J5 コネクタの MIPI 以外の信号を見てみると、SCL, SDA の信号と GPIO の信号があります。SCL, SDA は I2C のクロック信号とデータ信号です。
カメラ自体のクロック設定だとか解像度設定だとかいったカメラに対する制御は I2C で行いますので、PI_SCL_1, PI_SDA_1 を Ultra96 側の I2C バスに繋げてやる必要があります。また、GPIO0 は、Power enable 制御を行う信号で、これを high にしないとカメラが動きません。
これらの信号を適切に繋ぐために、J13, J15 のジャンパー設定が必要になります。
まず、SCL, SDA 信号を見てみましょう。
J5 コネクタの PI_SCL_1, PI_SDA_1 がどこに繋がっているか調べてみると、U18 PCA9306 を経由して J13 ピンヘッダの 20番、22番に繋がっていることが分かります。
20 番 CAM1_SCL を IC2X_SCL に、22 番 CAM1_SDA を IC2X_SDA に繋げれば良さそうです。J13 ピンヘッダには I2C0 系統、I2C1 系統、I2C2 系統が、J15 ピンヘッダには I2C3 系統があります。どこに繋いでも大丈夫と思いますが、ジャンパーピンで簡単にショートできる I2C2 系統にしましょう。ジャンパーピンを使って、19番と20番、21番と22番をショートさせましょう。
さて、続きです。I2C2_SCL, I2C2_SDA がどこに繋がっているか調べましょう。調べてみると、HS コネクタの 32 番、34 番に繋がっていることが分かります。
この先は Ultra96 ボードの HS コネクタに繋がります。
下の表を見ると、Ultra96 上では、I2C2_SCL, I2C2_SDA です。 Ultra96 上の HS コネクタの先はハードワイヤードされていて、HS コネクタの 32番、34番は Zynq の外部ピン経由で I2C バスに繋がります。
Zynq PS からは I2C2 を制御すればよいということになります。
J5 コネクタの PI_GPIO0 の方も見てみましょう。
PI_GPIO0 がどこに繋がっているか見てみると、U4 74AVC4T245PW を経由して J15 ピンヘッダの 2 番に繋がっています。
2番をどこかと繋げないといけませんが、ジャンパーピンで簡単にショートできる 1番を選びましょう。というわけで、次は 1番 APQ_GPIO12 を調べます。APQ_GPIO12 が MIPI ボード上のどこに繋がっているか調べてみると、LS コネクタの 24 番であることが分かります。
この先は Ultra96 ボードの LS コネクタに繋がります。
下の表を見ると、Ultra96 上では MIO37 となっています。Ultra96 上の LS コネクタの先はハードワイヤードされていて、LS コネクタの 24番は Zynq の外部ピン経由で MIO37 に繋がります。
Zynq PS からは MIO37 を制御すればよいということになります。
—
ということで、まとめると、J5 コネクタに刺したカメラを I2C と GPIO で制御するために、J13 と J15 を写真のようにショートさせて、後は Zynq PS で I2C2 と MIO37 を制御すればよい、ということになります。
ここからは SW の話になります。
カメラに対する制御は、これまで見てきたように、I2C および GPIO を使って行います。
I2C 制御は、I2C2 に対して行えばよいことが分かっています。でも、I2C2 って何でしょうか?
Ultra96 に搭載されている Zynq UltraScale+ MPSoC には I2C0, I2C1 の 2つの I2C コントローラがありますが、I2C2 なんてありません。
実は、Ultra96 には TCA9548A なる I2C スイッチが搭載されていて、一つの I2C コントローラから複数の I2C バスを制御できるようになっています。(↓の図では左の Zynq 側が I2C0_SDA, I2C0_SCL となっていますが、I2C1_SDA, I2C1_SCL の間違いではないかなあと思います。)
TCA9548A は 8チャネル分の I2C バスを配下に持てますが、Ultra96 上では I2C0, I2C1, I2C2, I2C3 の 4つを I2C サブバスとして制御できます。我々が制御したい I2C2 とは、TCA9548A のチャネル 2 のことです。
TCA9548A は Ultra96 ボード上に実装されていて、PS からは MIO 経由でアクセスできます。PS の I2C1 コントローラから、IC2 バス上にある I2C スレーブデバイスとして見えます。スレーブアドレスは 0x70 から 0x77 のうちのいずれかになるのですが、Ultra96 ボード上では 0x75 になっています。
TCA9548A は IC2 レジスタを 1つしか持っておらず、I2C コントローラから制御する際は、レジスタアドレスを指定する必要がありません。[スレーブアドレス][データ] というようなフォーマットのコマンドを送信して制御します。
[データ] には、開きたいチャンネルを指定します。チャネル 0 を開きたければ 0x1 を、チャネル 1 を開きたければ 0x2 を指定します。
チャネル 2 であれば 0x4 ですね。なので [0x75][0x4] というような I2C コマンドを送信すると TCA9548A はチャネル 2 を開けてくれ、それ以降 I2C1 コントローラが発行する I2C コマンドはチャネル 2 に流れます。
というのが TCA9548A の制御方法なのですが、Linux の場合は i2c mux ドライバというありがたいドライバがあって、TCA9548A を意識しないで済むように隠蔽してくれています。
ikwzm さんの Linux を起動すると、/dev/ 下に i2c-0 から i2c-9 までの 10個のデバイスファイルがあります。このうち、i2c-2 から i2c-9 がチャネル 0 – 7 に対応するデバイスファイルです。
我々が制御したい I2C2 は、/dev/i2c-4 を使ってアクセスできます。TCA9548A に対して明示的に何らかの制御をする必要はなく、/dev/i2c-4 に対して読み書きを行えば、TCA9548A のチャネル 2, 即ち I2C2 バスにアクセスできます。
具体的な I2C 制御に関しては、Pcam-5C カメラに関しては Digilent 社の Pcam-5C reference design を、Raspberry Pi カメラ v2 に関しては Sony IMX219 Raspberry Pi V2 CMOS Datasheet and Source Code を参考にしています。
ultra96_design/src/linux/caminit/src/ov5640.cc::init_ov5640() もしくは ultra96_design/src/linux/caminit/src/imx219.cc::init_imx219() が当該コードです。適切な設定ができているかどうか、細かい検証はしていないのですが、一応それらしく動いています。
カメラの Power enable をオンにするため、MIO37 を制御します。MIO37 には、PS の GPIO が割り当てられています。
Linux からは sysfs 経由で GPIO を制御できるので、sysfs の中から当該 GPIO を探してみましょう。
/sys/kernel/debug/gpio を見てみると、↓のようになっています。
:
gpio-370 (PS_FP_PWR_EN )
gpio-371 (PL_PWR_EN )
gpio-372 (POWER_KILL )
gpio-373 ( )
gpio-374 (GPIO-A )
gpio-375 (GPIO-B )
gpio-376 (SPI0_SCLK )
gpio-377 (GPIO-C )
gpio-378 (GPIO-D )
gpio-379 (SPI0_CS )
gpio-380 (SPI0_MISO )
:
どうやら gpio-375 が該当するようです。
それでは gpio-375 を high にしてあげましょう。
# echo 375 > /sys/class/gpio/export
# echo out > /sys/class/gpio/gpio375/direction
# echo 1 > /sys/class/gpio/gpio375/value
これでカメラの Power enable 信号が high になります。
ソースコードとしては、上記のようなシェルスクリプトは用意していなくて、ultra96_design/src/linux/caminit/src/caminit.cc の set_cam_gpio() で同等のことを行っています。
—
説明が I2C, GPIO の順番になっていますが、実際の制御は GPIO 設定をしてから I2C 設定をする必要があります。GPIO 設定をしないとカメラの電源が入らないので、I2C デバイスとして見えません。
今回公開した SW では、GPIO 設定、I2C 設定はユーザーランドのプログラムに行わせています。カーネルモジュールでやってもいいのですが、ちょっと手を抜いてやってないです。
ユーザーランドからカメラ画像を取得する方法を提供するために、V4L2 インターフェースを持つカーネルモジュールを作ることにしました。
ただし、V4L2 の全機能をカバーするのは大変だし、機能的に実現できないところもあるので一部機能のみサポートすることにしました。一部機能のみ、というか、簡単なサンプルプログラムが動くような最低限度のサポート、といったところです。
実装にあたっては、Xilinx SDK が生成する BSP ドライバを可能な限りそのまま使うことにしました。
ただし、素直に必要な BSP ドライバ C ファイルをカーネルモジュールビルドに組み込むと、ヘッダー関連でコンパイルエラーが発生してしまいます。
そこで、ultra96_design/src/linux/driver/ に dummy ディレクトリを用意して、そこにダミーヘッダーを置くことによって、カーネルモジュールビルドが通るようにしました。こうすることにより、カーネルドライバから Xilinx SDK Driver API を呼び出すことができます。
本ドライバは基本的なキャラクタ型ドライバインターフェースを備えた実装であり、特に説明することもないのですが、初期化と画像取得時の動作だけ、説明しておきます。
カメラ ⇒ MIPI ⇒ demosaic ⇒ VDMA というパイプラインを、後ろから初期化していきます。即ち、VDMA, demosaic, MIPI, カメラの順に初期化します。
先に少し触れましたが、カメラの初期化はユーザーランドで行いますので、カーネルモジュールでの初期化は VDMA, demosaic, MIPI の 3つです。
VDMA の制御に関しては、Digilent 社の Pcam-5C reference design の AXI_VDMA.h を参考にしました。
実装は、ultra96_design/src/linux/driver/vdma.c にあります。AXI_VDMA.h 内の Write Channel 側の初期化処理を参考に、初期化関数 zynq_v4l2_vdma_init を記述しています。
この初期化により、VDMA は 3つの write buffer を循環しながらひたすら書き続けるようになります。SW が画像を取得する際には、3つの write buffer から最新のバッファを選んで、そこから画像データをコピーするようにします。
基本、AXI_VDMA.h の初期化と同じような処理を行っているのですが、XAxiVdma_DmaConfig を呼び出す際の記述において、一カ所変更する必要がありました。
:
WriteCfg.EnableCircularBuf = 1;
WriteCfg.EnableSync = 0; //Gen-Lock <- ココを 1 から 0 に変更!!
WriteCfg.PointNum = 0;
WriteCfg.EnableFrameCounter = 0;
WriteCfg.FixedFrameStoreAddr = 0; //ignored, since we circle through buffers
Status = XAxiVdma_DmaConfig(&inst[minor], XAXIVDMA_WRITE, &WriteCfg);
:
これを行わないと、VDMA が現在どこを書き込んでいるか調べるための XAxiVdma_CurrFrameStore() が正常に動作しませんでした。VDMA Write Channel は同期を取る相手がいないので EnableSync は確かに不要そうですが、XAxiVdma_CurrFrameStore() に影響する理由はよく分かりません。
後は割り込み設定です。
本ドライバは device tree overlay を使ってロードしますが、dts 記述 (overlay/v4l2.dts) で割り込み番号 89 を指定しています。request_irq() では、dts の記述に対応すべく、irq_of_parse_and_map() を使っています。
:
v4l2 {
compatible = "fixstars,zynq-v4l2-1.0";
#interrupt-cells = <0x3>;
device-name="v4l2";
interrupt-parent = <&gic>;
interrupts = <0x0 0x59 0x4>; <- 割り込み番号は 0x59 (89)
};
:
割り込み番号がなぜ 89 番と分かったか、経緯を説明します。
Xilinx SDK が生成する xparameters.h を見ると、VDMA の割り込み番号を 121 としていることが分かります。
/* Definitions for Fabric interrupts connected to psu_acpu_gic */
#define XPAR_FABRIC_AXI_VDMA_0_S2MM_INTROUT_INTR 121U
dts にも 121 を記述したのですが割り込みが上がらず、少々ハマりました。色々調べるうち、割り込みを発生させて GIC のレジスタの値を観察すると、Shared Peripheral Interrupt 89 が上がっていることが分かりました。そして dts に 89 を記述すると割り込みが取れるようになりました。
外部割込み番号は 89 で、CPU には 32 の下駄をはかせて 121 番の割り込みが上がってくるのだと思いますが、xparameters.h に記述される割り込み番号は、外部割込み番号ではなく、CPU に上がってくる割り込み番号であるようです。
なので、dts に記述する割り込み番号は xparameters.h 内の定義値 – 32 なのですね。
Demosaic は特に注意事項はありません。一回だけ start をかければいいように、auto_restart を有効にしていることくらいです。
どのような初期化をすればよく分からなかったのですが、Xilinx SDK Drivers API csi を見て、適当に、
とやってみたら動きました。適当でスミマセン。
画像取得は、VDMA からの割り込み発生時に行います。割り込み発生のタイミングは、VDMA が write buffer に 1フレーム書き終わった時です。実際に画像を取得する処理は、割り込みハンドラではなく、割り込みハンドラから起床された work queue スレッドで行っています。
work queue スレッドは XAxiVdma_CurrFrameStore() を呼び出して VDMA が現在書き込んでいる write buffer の番号を取得します。XAxiVdma_CurrFrameStore() は VDMA が現在作業中のバッファの番号を返す API です。
ところで、割り込み発生時に XAxiVdma_CurrFrameStore() を呼び出した場合、今書き込み終わった write buffer の番号を返すでしょうか?それとも次の write buffer の番号を返すでしょうか?VDMA がどこかの write buffer に書き込んでいる最中に XAxiVdma_CurrFrameStore() を呼び出せば、その write buffer を返すでしょうが、割り込みのタイミングはちょうど write buffer に書き終えたタイミングであり、現在作業中の write buffer ってどれになるんだろう?という疑問が出てきます。
これに関する文書は見つけられていませんが、実験してみた限りでは次の write buffer の番号を返すように見えました。試しに XAxiVdma_CurrFrameStore() が返した write buffer から画像データをコピーすると、タイミングによっては、あたかも VDMA によって一部上書きされたかのような画像データを取得することがありました。
現実装では、XAxiVdma_CurrFrameStore() の返り値の一つ前の write buffer からコピーするようにしています。
コピーは、ユーザーがキューイングしたバッファに対して行われます。キューイングされているバッファが複数ある場合は、古いバッファを選択して書き込むようにしています。
本システムを作るにあたって、いくつかハマりポイントがありました。
1つ目、2つ目については既出ですので、ここでは最後の事例についてご紹介します。
結論から言えば、VDMA の M_AXI_S2MM のデータ幅が 64bit だったのに対し、受け側の PS の S_AXI_HPC0_FPD のデータ幅が 128bit だった、というものです。
分かってしまえばそりゃダメだよね、なのですがこれがなかなか分かりませんでした。
2番目の、PS の S_AXI_HPC0_FPD のデータ幅を 128 から 64 にしても現象が変わらなかった理由は、以下です。
FPGA を構成するのに、device tree overlay を使用しており、PS の設定を変更して bitstream を生成して、device tree overlay によって FPGA を再構成していました。(これで PS の設定を変更したつもりになっていました。)
ただ、PS の設定変更は device tree overlay による方法ではできません。PS の設定は FSBL が行うのにも関わらず、FSBL の更新はせず、FPGA の再構成のみを行っていたからです。
これも分かってしまえばそりゃダメだよね、なのですが、なかなか分かりませんでした。
VDMA の M_AXI_S2MM のデータ幅を 128 に変更することにより、この問題を解消しています。
コンピュータビジョンセミナーvol.2 開催のお知らせ - ニュース一覧 - 株式会社フィックスターズ in Realizing Self-Driving Cars with General-Purpose Processors 日本語版
[…] バージョンアップに伴い、オンラインセミナーを開催します。 本セミナーでは、...
【Docker】NVIDIA SDK Managerでエラー無く環境構築する【Jetson】 | マサキノート in NVIDIA SDK Manager on Dockerで快適なJetsonライフ
[…] 参考:https://proc-cpuinfo.fixstars.com/2019/06/nvidia-sdk-manager-on-docker/ […]...
Windowsカーネルドライバを自作してWinDbgで解析してみる① - かえるのほんだな in Windowsデバイスドライバの基本動作を確認する (1)
[…] 参考:Windowsデバイスドライバの基本動作を確認する (1) - Fixstars Tech Blog /proc/cpuinfo ...
2021年版G検定チートシート | エビワークス in ニューラルネットの共通フォーマット対決! NNEF vs ONNX
[…] ONNX(オニキス):Open Neural Network Exchange formatフレームワーク間のモデル変換ツー...
YOSHIFUJI Naoki in CUDAデバイスメモリもスマートポインタで管理したい
ありがとうございます。別に型にこだわる必要がないので、ユニバーサル参照を受けるよ...