Ultra96 Linux で MIPI カメラから画像を取得する (解説編)

2019年1月22日

前回の続きから

こんにちは、フィックスターズ新規事業推進室の大澤です。
前回の記事では、Ultra96 ボード上でカメラ画像を取得する環境の構築方法と簡単なテストの動かし方についてご紹介しました。今回は、技術的な観点からこのシステムの説明をしていきたいと思います。いくつかポイントを絞って説明します。

1. ブロックデザイン

まずはブロックデザインの作り方です。
今回のシステムに必要な 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 をメインにブロックをデザインします。

1.1 Zynq UltraScale+ MPSoC の追加

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 へ画像データを転送させるため

1.2 MIPI CSI-2 Rx Subsystem の追加

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 に変更



1.3 AXI Video Direct Memory Access の追加

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 は不要なため

1.4 Demosaic_root の追加

Add IP… で Demosaic_root を追加します。設定変更は必要ありません。

1.5 Clocking Wizard の追加

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 クロックを生成するため

1.6 Utility Vector Logic の追加

Clocking Wizard は active high な reset を必要とするので、active low な reset を反転させて作ります。Add IP… で Utility Vector Logic を追加し、Customize IP… で設定変更を行います。

変更場所 変更項目 変更内容 変更理由
トップ画面 C_SIZE 8 から 1 に変更 1 ビットしか使わないため
トップ画面 C_OPERATION not を選択 反転ロジックとするため

1.7 IP 間の接続

以下のようにポート間を手動で接続します。

ポート ポート
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 して、ブロックデザインは完成です。

2. MIPI ボード設定

AISTARVISION MIPI Adapter V2.1 ボード にはカメラコネクタが複数あります。Pcam-5C カメラ、Raspberry Pi カメラ v2 の場合は、J5 コネクタもしくは J9 コネクタのどちらかに接続して使います。カメラをどちらのコネクタで使うかによって、ジャンパーピンの設定が必要です。ところが、「J5 コネクタにカメラを繋いだ場合はこんな風にジャンパーピンを刺してください」というような親切な説明がありません。回路図があるので、それを読んで適切にやりなさい、ということのようです。

2.1 カメラ接続

それでは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 のジャンパー設定が必要になります。

2.2 ジャンパー設定

まず、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 を制御すればよい、ということになります。

3. カメラ制御

ここからは SW の話になります。
カメラに対する制御は、これまで見てきたように、I2C および GPIO を使って行います。

3.1 I2C 制御

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() が当該コードです。適切な設定ができているかどうか、細かい検証はしていないのですが、一応それらしく動いています。

3.2 GPIO 制御

カメラの 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 設定はユーザーランドのプログラムに行わせています。カーネルモジュールでやってもいいのですが、ちょっと手を抜いてやってないです。

4. Linux V4L2 ドライバ

ユーザーランドからカメラ画像を取得する方法を提供するために、V4L2 インターフェースを持つカーネルモジュールを作ることにしました。
ただし、V4L2 の全機能をカバーするのは大変だし、機能的に実現できないところもあるので一部機能のみサポートすることにしました。一部機能のみ、というか、簡単なサンプルプログラムが動くような最低限度のサポート、といったところです。

実装にあたっては、Xilinx SDK が生成する BSP ドライバを可能な限りそのまま使うことにしました。
ただし、素直に必要な BSP ドライバ C ファイルをカーネルモジュールビルドに組み込むと、ヘッダー関連でコンパイルエラーが発生してしまいます。
そこで、ultra96_design/src/linux/driver/ に dummy ディレクトリを用意して、そこにダミーヘッダーを置くことによって、カーネルモジュールビルドが通るようにしました。こうすることにより、カーネルドライバから Xilinx SDK Driver API を呼び出すことができます。

本ドライバは基本的なキャラクタ型ドライバインターフェースを備えた実装であり、特に説明することもないのですが、初期化と画像取得時の動作だけ、説明しておきます。

4.1 初期化

カメラ ⇒ MIPI ⇒ demosaic ⇒ VDMA というパイプラインを、後ろから初期化していきます。即ち、VDMA, demosaic, MIPI, カメラの順に初期化します。
先に少し触れましたが、カメラの初期化はユーザーランドで行いますので、カーネルモジュールでの初期化は VDMA, demosaic, MIPI の 3つです。

4.1.1 AXI Video Direct Memory Access の初期化

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 なのですね。

4.1.2 Demosaic_root の初期化

Demosaic は特に注意事項はありません。一回だけ start をかければいいように、auto_restart を有効にしていることくらいです。

4.1.3 MIPI CSI-2 Rx Subsystem の初期化

どのような初期化をすればよく分からなかったのですが、Xilinx SDK Drivers API csi を見て、適当に、

  • XCsi_LookupConfig
  • XCsi_CfgInitialize
  • XCsi_Reset
  • XCsi_Activate

とやってみたら動きました。適当でスミマセン。

4.2 画像取得時の動作

画像取得は、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 からコピーするようにしています。

コピーは、ユーザーがキューイングしたバッファに対して行われます。キューイングされているバッファが複数ある場合は、古いバッファを選択して書き込むようにしています。

5. はまり事例

本システムを作るにあたって、いくつかハマりポイントがありました。

    • カメラの Power enable をオンしない状態で i2c コマンドを発行していて、カメラが全然見つからなかった
    • AXI Video Direct Memory Access の割り込み番号が分からず、割り込みが拾えなかった
    • 取得画像が謎の縦縞模様になってしまった

1つ目、2つ目については既出ですので、ここでは最後の事例についてご紹介します。
結論から言えば、VDMA の M_AXI_S2MM のデータ幅が 64bit だったのに対し、受け側の PS の S_AXI_HPC0_FPD のデータ幅が 128bit だった、というものです。
分かってしまえばそりゃダメだよね、なのですがこれがなかなか分かりませんでした。

  • データ幅が異なるポートが繋がると思っていなかった
  • データ幅が異なることに気づき、PS の S_AXI_HPC0_FPD のデータ幅を 128 から 64 にしてみても現象は変わらなかった
  • VDMA の M_AXI_S2MM のデータ幅を 64 から 128 に変更すると解消できることが分かり、あれ??と思った

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 に変更することにより、この問題を解消しています。

Tags

About Author

takashi.osawa

Leave a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください

Recent Comments

Social Media