Jetsonのカメラドライバを書く[RAW撮影編]

# はじめに

前の記事: [Jetsonのカメラドライバを書く[I2C編]](https://qiita.com/uzuna/items/693cabb800173f2175f5)
次の記事: [Jetsonのカメラドライバを書く[Argus撮影編]](https://qiita.com/uzuna/items/675d4f43e2bd97e6d0b9)

前の記事ではカーネルモジュールが読み込まれるまでをやりました。
制御用のI2C通信が出来たのでカメラを制御をして絵をとります。

# RAW画像を得る

USBカメラなら繋いだらすぐに映像が得られますし、スマホとかは当たり前にカメラがついて最近は画質も良いです。
Jetsonもコマンドが流せてるし、後は簡単に...とは行きません、本当に大変なのはここからです。

IMX708が出力するのはよく見るYUVでもRGBでもMJPEGでもない、Bayerです。
[SSSの技術サイトquad-bayer-coding](https://www.sony-semicon.com/ja/technology/mobile/quad-bayer-coding.html)を引用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/8b0e424e-c9a4-7467-c72e-bd18fed13f5d.png)

CVによく使うRGB画像は1画素にRGBの3色の情報を持ち、ディスプレイでは加色法によって任意の色を作ります。イメージセンサーに1画素で3色を分離して撮像する技術は存在しないため、RGB特定の色のみを通すフィルターをつかい1画素=特定色の光の強さをデータにします。
これがBayer形式、あるいはRAW画像などと呼ばれ、1画素=RGBの画像にするには隣接する他の色を適切に混色が必要です。このDebayerのプロセスはたいてい専用のHWが載っていることが多くISP(image signal processor)と呼ばれます。実際にISPを使う場合はMIPI-CSI2のデータに加えて、ISP変換のためのメモリやら処理のスループットも気にしなければならず、原因の特定が困難になります。
まずはRAW画像を得ることから始めましょう。

## 駆動モードを決める

イメージセンサーの制御をする上ではじめに考える必要があるので駆動モードの設定です。
イメージセンサーは数メガピクセルの画素を持っていますが、それを保持するメモリを持っていないことがほとんどです。写真撮影のように一枚撮って読み出すというものではなくストリームで、撮れたものから即座にMIPIを通じてシステム側に転送します。この転送するストリームを作るのに大きく影響するパラメータは**VSYNCとHSYNC**といった画素読み出しの周期に関わるもの、**画像の大きさ**や**転送レート**といった受け側に対応するリソースを用意しなければならないものがあります。これらストリームを開始したら変更できない要素を基準にモードを決めます。
RaspberryPiの`imx708.c`を見ると4つのモードが定義されています。

|Mode|resolution|description|
|---|---|---|
|0|4608x2592|Full Resolution|
|1|2304x1296|2x2binning|
|2|1536x864|2x2binning and cropped for 720p||
|3|2304x1296|2x2binning with HDR|

まずは撮影モードを1つに決めてそれを実装します。
今回はmode0の全画素読み出しのものにします。

```imx708.c
static const struct imx708_mode supported_modes_10bit_no_hdr[] = {
	{
		/* Full resolution. */
		.width = 4608,
		.height = 2592,
		.line_length_pix = 0x3d20,
		.crop = {
			.left = IMX708_PIXEL_ARRAY_LEFT,
			.top = IMX708_PIXEL_ARRAY_TOP,
			.width = 4608,
			.height = 2592,
		},
		.vblank_min = 58,
		.vblank_default = 58,
		.reg_list = {
			.num_of_regs = ARRAY_SIZE(mode_4608x2592_regs),
			.regs = mode_4608x2592_regs,
		},
		.pixel_rate = 595200000,
		.exposure_lines_min = 8,
		.exposure_lines_step = 1,
		.hdr = false
	},
```

受け側がメモリを確保する必要があるためか、設定はV4L2が知る必要があるためカーネルモジュールにも詳しく定義しています。
これをもとにJetsonに必要な情報を整理します。

|item|value|
|---|---|
|width|4608|
|height|2592|
|HSYNC(line_length)|0x3d20 -> 15648|
|VSYNC|2592 + 58 = 2650|
|pixel_rate(pix/s)|595200000|
|fps|595200000 / (15648 * 2650) = 14.35|

おさらいで再びMACNICA社のひろみんさんの記事、[【徹底解説】MIPI なんて難しくない！新人がゼロから学ぶ MIPI 規格 基礎編](https://www.macnica.co.jp/business/semiconductor/articles/lattice/142604/)からビデオフレーム図を引用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/d6650688-1969-0188-bf03-f3d6ba76b91b.png)

今回のモードを雰囲気でスケール合わせると以下のような感じです。line-blankingの感覚が長く、frame-blankingの間隔が短いですね。画素のデータ自体は7.7usで読めますが、転送には20us程度はかかってしまうので十分なline-blankingがなければならないため、この設定になっていると考えられます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/58e3a74f-9b85-b85e-1755-bd42f9032cef.png)

これで流し撮りなんかしたらすごく歪むでしょう。動きモノで使うのは向かないですね。
一方MIPIの伝送レートが低くても良くなるため、長いFFCを使っても撮影できる可能性は高いです。

### モードを作る

駆動モードが決まったのでJetsonに移植をします。
先の通りJetsonの場合、駆動モード設定の多くはデバイスツリーに書き、レジスタ設定はカーネルモジュールに書きます。
なので作業内容は以下になります。

1. デバイスツリーのmode0の設定をRaspberryPiのものに合わせる
1. カーネルモジュールの画面設定とモード設定をデバイスツリーに合わせる
1. カーネルモジュールのレジスタ設定をコピーする

### デバイスツリーのmode0の設定をRaspberryPiのものに合わせる

まずは`tegra234-camera-rbpcv3-imx708.dtsi`を開きます。
デバイスツリーを読むと大体以下のようになっているのがわかります。

```tegra234-camera-rbpcv3-imx708.dtsi
- tegra-capture-vi
    - ...
- host1x@13e00000
    - ...
- cam_i2cmux
    - i2c@0
        - rbpcv3_imx708_a@1a
            - mode0
            - mode1
            - ports
    - i2c@1
        - rbpcv3_imx708_c@1a
            - mode0
            - mode1
            - ports
```

今回変更が必要なモード設定の記述があるのはのは`cam_i2cmux`の下のノードです。
まず`i2c@0`と`i2c@1`で別れており、だいたい同じ内容が書かれています。基本的には片方で書き換えたら、もう一方にもコピーするのが良いです。今回はCAM1なので`i2c@1`で書き換えをします。
`mode0`と`mode1`がありますが、`mode1`は今は使わないので削除しましょう。

#### mode0の書き換え

デバイスツリーは先程整理した、ストリーム中に変更できない重要なパラメータの指定の他、露出に関する変更可能な値についてはレンジを指定します。
スペースで区切られて3段(+1行)に分かれていると思いますが、これは大雑把に以下のような項目となってます

1. センサー及びCAMポート固有の項目。基本的には変更しなくていい
1. モード固有の固定値。モードごとに適切な値にしなければならない
1. ストリーミング中に変更可能な項目。レンジを指定する。RAW撮影のときは影響しない

画面の大きさやラインの長さは読み取れた設定どおりの設定で良いです。
`mclk_multiplier`は非推奨になっているので削除します。
`pix_clk_hz`はそのままにしてください。
変更可能な露出関係の設定の内、フレームレートだけは判明しているので設定しておきます。
最後におまじないとして`embedded_metadata_height = "4"`を設定しておきます。
変更すると以下のようになります。

```tegra234-camera-rbpcv3-imx708.dtsi
mode0 { /* imx708_MODE_4608x2592 */
	mclk_khz = "24000";
	num_lanes = "2";
	tegra_sinterface = "serial_c";
	phy_mode = "DPHY";
	discontinuous_clk = "no";
	dpcm_enable = "false";
	cil_settletime = "0";
	lane_polarity = "0";

	active_w = "4608";
	active_h = "2592";
	mode_type = "bayer";
	pixel_phase = "rggb";
	csi_pixel_bit_depth = "10";
	readout_orientation = "0";
	line_length = "15648";
	inherent_gain = "1";
	pix_clk_hz = "300000000";

	gain_factor = "16";
	framerate_factor = "1000000";
	exposure_factor = "1000000";
	min_gain_val = "16"; /* 1.00x */
	max_gain_val = "356"; /* 22x */
	step_gain_val = "1";
	default_gain = "16"; /* 1.00x */
	min_hdr_ratio = "1";
	max_hdr_ratio = "1";
	min_framerate = "14350000"; /* 14.35 fps */
	max_framerate = "14350000";
	step_framerate = "1";
	default_framerate = "14350000";
	min_exp_time = "13"; /* us */
	max_exp_time = "683709"; /* us */
	step_exp_time = "1";
	default_exp_time = "2495"; /* us */

	embedded_metadata_height = "4";
};
```

RAWを撮影するのに重要なのはストリーミング中に変更不可能な値、すなわちスペースで分けられた2の段が主なので、レンジ設定の項目はまだ適当で大丈夫です。
設定変更できたらこの2,3段の設定をCAM0側にもコピーしてCAM1と同じにします。

### ドライバの画面設定とモード設定をデバイスツリーに合わせる

modeを1つにしてモード固有値を設定しました。
カーネルモジュールにもこのモードに対応する設定が`imx708_mode_tbls.h`のファイルがあり、enumと配列を使って対応関係を作っています。
これをデバイスツリーに合わせます。

1. enumのモードに関する設定を1つだけ残し、適切な名前`IMX708_MODE_4608x2592_14FPS`にする
1. `mode_table`も不要なenumに該当する行を消す
1. フレームレートを14とする
1. `imx708_frmfmt`に画像の大きさ、とモードenumをmode0に該当するものにする

すると以下のようになると思います。
不要なレジスタ設定定義は消しましょう。
基本的には`imx708_frmfmt`の配列がデバイスツリーのmodeXと対応関係になります。
変更結果を抜粋すると以下のようになっているはずです。

```imx708_mode_tbls.h
enum {
	IMX708_MODE_4608x2592_14FPS,

	IMX708_MODE_COMMON,
	IMX708_START_STREAM,
	IMX708_STOP_STREAM,
};

static const imx708_reg *mode_table[] = {
	[IMX708_MODE_4608x2592_14FPS] = imx708_mode_4608x2592_14fps,

	[IMX708_MODE_COMMON] = imx708_mode_common,
	[IMX708_START_STREAM] = imx708_start,
	[IMX708_STOP_STREAM] = imx708_stop,
};

static const int imx708_14_fr[] = {
	14,
};

static const struct camera_common_frmfmt imx708_frmfmt[] = {
	{{4608, 2592}, imx708_14_fr, 1, 0, IMX708_MODE_4608x2592_14FPS},
};
#endif /* __IMX708_I2C_TABLES__ */
```

### カーネルモジュールのレジスタ設定書き換え

モード固有値をデバイスツリーに設定してきました、この設定値はイメージセンサーでどのように設定するのでしょうか?
これらはすべてI2Cと通してレジスタに書き込むことでセンサーに反映します。
イメージセンサーは電源投入後は基本`Standbyモード`で、ピクセルのスキャン動作などは行っておらず設定を待っている状態です。
使うときにはまず、上のモード固有のような値を事前に該当するレジスタに適切に書き込みます。
レジスタ設定が出来たら`Stanbyモード`から、データを読み出し続ける`Streamingモード`に切り替えることでセンサーから画像のデータが得られます。

まずは設定値群である`mode_common`と`mode0`のレジスタ設定をコピーしてください。(コピペなのでここに差分は貼りません)
Jetsonはレジスタリストの末尾にセンチネル`IMX708_TABLE_END`を置いているので、間違えて消さないように注意しましょう。

次に開始と終了について、IMX477と共通で`0x0100`を`0x01`にすることで`Streamingモード`になるようです。
変えなくても問題ないですが、わかりやすさの点では定数にしたほうがいいかもしれません。

```imx708_mode_tbls.h
#define IMX708_REG_MODE_SELECT	0x0100
#define IMX708_MODE_STANDBY		0x00
#define IMX708_MODE_STREAMING	0x01

#define imx708_reg struct reg_8

static const imx708_reg imx708_start[] = {
	{IMX708_REG_MODE_SELECT, IMX708_MODE_STREAMING},
	{IMX708_TABLE_WAIT_MS, IMX708_WAIT_MS*3},
	{IMX708_TABLE_END, 0x00}
};

static const imx708_reg imx708_stop[] = {
	{IMX708_REG_MODE_SELECT, IMX708_MODE_STANDBY},
	{IMX708_TABLE_END, 0x00}
};
```

これでデバイスツリーとカーネルモジュールが出来ました。
ビルドしてorin-nanoに持ち込み、デバイスツリーを上書きして再起動しましょう。

## 動作させてみる

再起動したところで再び`make insmod`します。
問題なく設定できていれば、設定したとおりの解像度とfpsになっているはずです。
`v4l2-ctl`を使って期待通りになっているか確認しましょう。
以下のように`imx708 10-001a`が見つかり解像度が`4608x2592`ならばOKです。

```
jetson@orin-nano:~/imx708$ v4l2-ctl --list-devices
NVIDIA Tegra Video Input Device (platform:tegra-camrtc-ca):
        /dev/media0

vi-output, imx708 10-001a (platform:tegra-capture-vi:2):
        /dev/video0

jetson@orin-nano:~/imx708$ v4l2-ctl --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
        Type: Video Capture

        [0]: 'RG10' (10-bit Bayer RGRG/GBGB)
                Size: Discrete 4608x2592
                        Interval: Discrete 0.071s (14.000 fps)
```

早速RAW画像を取得しましょう

```
jetson@orin-nano:~/imx708$ make check0
v4l2-ctl -d /dev/video0 --set-ctrl bypass_mode=0 --stream-mmap --stream-count=1 --stream-to=test.raw
<
```

`<`がでて、エラーが発生したり処理が終わらないということがなければ成功です :tada::tada::tada:
`test.raw`というバイナリファイルが出来て、おそらく撮像できているというのがわかります。

うまく動かない場合は`dmest`でerr_codeにヒントがあります。
NVIDIAのforumで同じ問題の人がいないか検索するのがいいです。


```
$ hexdump test.raw | head 
0000000 1685 2108 16c5 2088 1685 21c8 16c5 2148
0000010 16c5 2188 1685 22c8 1705 2148 1785 21c8
0000020 1705 2108 1785 2108 1745 21c8 1745 20c8
0000030 1785 2108 1806 21c8 1745 2148 1745 2208
0000040 1745 22c8 1705 2288 16c5 2188 1705 2108
0000050 1745 2348 16c5 22c8 1705 2288 1705 2208
0000060 16c5 2308 16c5 2248 1745 2208 1705 22c8
0000070 1785 2248 1705 2308 1685 21c8 1745 21c8
0000080 1785 2288 1745 23c8 1785 2308 1745 2348
0000090 1785 2208 1685 2248 1705 22c8 1745 23c8
```

さっぱり値の意味はわかりませんが、光が当たらないように覆い隠したり、明るく照らすと値が変わるので、何かを撮影しているっぽいことはわかります。

暗くした結果。

```
$ hexdump test.raw | head 
0000000 1004 1004 1004 1044 1004 1004 1004 1004
*
0000020 1004 1004 1004 1004 1004 1004 1004 1004
0000030 1004 1044 1044 1004 1044 1004 1004 1044
0000040 1004 1004 1004 1044 1004 1004 1004 1004
0000050 1044 1004 1044 1004 1004 1004 1004 1004
0000060 1004 1004 1004 1004 1004 1004 1004 1004
0000070 1044 1004 1044 1004 1044 1004 1004 1044
0000080 1004 1004 1004 1004 1004 1004 1004 1004
0000090 1004 1004 1044 1044 1044 1004 1044 1004
```

ついでにstream-countを増やしてfps計測もしてみましょう。

```
jetson@orin-nano:~/imx708$ v4l2-ctl -d /dev/video0 --set-ctrl bypass_mode=0 --stream-mmap --stream-count=60
<<<<<<<<<<<<<<<< 14.36 fps
<<<<<<<<<<<<<< 14.36 fps
<<<<<<<<<<<<<<< 14.36 fps
<<<<<<<<<<<<<< 14.36 fps
```

わずかに違いますが、だいたい良さそうですね。
これでセンサーが期待通りにデータを送り出していることがしていることがわかりました。

## まとめ

MIPI-CSI2のイメージセンサーの出力はたいていBayerで変換が必要という前提を確認しました。
駆動モードという概念が存在し、センサーは事前にI2Cで設定されたレジスタの値に従って連続的にデータを送って来るため、受信側も出力されるデータ形式を設定を知っておく必要があることもわかりました。
ところでBayerから変換しないと絵が見れないですよね、次はJetsonのISPを通して画像を得るところをやります。

次の記事: [Jetsonのカメラドライバを書く[Argus撮影編]](https://qiita.com/uzuna/items/675d4f43e2bd97e6d0b9)