NVIDIA Jetson Orin Nano Developer Kitの開発環境構築

Orin Nanoが買えたので環境構築しました。

## Setup Jetson

セットアップや接続に迷ったときは**Jetson Getting Started**からたどると解決しやすい。

https://developer.nvidia.com/embedded/learn/getting-started-jetson

追加で用意する必要があるのは

1. L4Tを書き込むためのLocalMachine
1. USB-Cのケーブル: LocalMachineと接続するため
1. microSD or NVMe(M.2) SSD: 読み書きが早いNVMeのほうがおすすめ
1. Keyboard + Mouse: セットアップの最後にGUIが出てくるため

### Install L4T(R35.3.1)

大まかに3つの方法がある
- [microSDカードにイメージを書き込む手順](https://developer.nvidia.com/embedded/learn/get-started-jetson-orin-nano-devkit#write)
- [SDKManagerを使った書き込み方](https://developer.nvidia.com/sdk-manager)
- [Driver Package (BSP)を使ってflushする方法(e.g. R35.3.1)](https://developer.nvidia.com/embedded/jetson-linux-r3531)

microSDを使う場合、SDカードを作れば使い回せること、起動しなくてもLocalMachineから修正が容易なのは利点。
一方でmicroSDは転送速度が遅いので画像、機械学習モデルを動かす上ではボトルネックとなる。
またLinuxより前のBootLoaderなどは更新されないため今後バージョンを上げたときに不整合で機能しないHWが出る可能性がある。

SDKManagerを使うとGUIから操作できること、Jetpackまでセットアップできることが利点の一つ。
何度もセットアップする場合はネットワークから落とすよりも早くなると思われる。
デメリットとしてはLocalMachineのディストロに制限があったり、ストレージを圧迫すること。

ここではBSPからflushする方法を取る。
Jetpackなどは手動セットアップになるが作業の再現性がよい。

#### ForceRecovery Modeで起動

1. NVMeなどのストレージを接続する
1. J5のUSB-CとLocalMachineを接続する
1. キーボードとマウスは適当に接続
1. DisplayPortを適当なディスプレイに接続
1. 可能なら有線LANをつなぐ
1. J14の左から3,4番目PIN#10,#9を短絡させながら電源を入れる(J16にDCジャックを接続)
![Jetson Orin Nano Developer Kit Carrier Board SP-11324-001_v1.0 | 5より](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/6f9533fb-0b19-0d8a-f449-b43fce9cebf4.png)

DS1のLEDが光るがつないだディスプレイには何も写っていない状態になる。
LocalMachineからは`lsusb`でNVIDIAのデバイスが見えていればflushに進む。

#### Flush

FlushではLinuxストレージ以外にも書き込みが行われる。
以下にMakefileを用意しているので接続ができているなら `make flush` で書き込みができます。
初回のセットアップが面倒な場合は `add_user` で事前にアカウントとホスト名の設定が出来ます。

```Makefile
MAJOR:=35
MINOR:=3.1
L4T_RELEASE_PACKAGE:=Jetson_Linux_R${MAJOR}.${MINOR}_aarch64.tbz2
SAMPLE_FS_PACKAGE:=Tegra_Linux_Sample-Root-Filesystem_R${MAJOR}.${MINOR}_aarch64.tbz2
RELEASE_ADDR:=https://developer.download.nvidia.com/embedded/L4T/r${MAJOR}_Release_v${MINOR}/release

# STORAGE_DEVIVE:=sda1  # USB Memory or microSD
STORAGE_DEVIVE:=nvme0n1p1  # NVMe SSD

# BOARD:=p3737-0000+p3701-0000-maxn  # Jetson AGX Orin DK MAXN
# BOARD:=p3509-a02+p3767-0000  # Jetson OrinNX + XavierNX DK
BOARD:=jetson-orin-nano-devkit  # Jetson Orin Nano DK

HOSTNAME:=orin-nano  # Need set your hostname
USERNAME:=jetson
PASSWORD:=jetson

.PHONY: flush
flush:
	cd Linux_for_Tegra \
	&& sudo ./tools/kernel_flash/l4t_initrd_flash.sh --external-device ${STORAGE_DEVIVE} \
		-c tools/kernel_flash/flash_l4t_external.xml -p "-c bootloader/t186ref/cfg/flash_t234_qspi.xml" \
		--showlogs --network usb0 ${BOARD} internal

# L4Tにデフォルトユーザーを設定する
.PHONY: add_user
add_user:
	cd Linux_for_Tegra \
	&& sudo ./tools/l4t_create_default_user.sh -u ${USERNAME} -p ${PASSWORD} --autologin --accept-license --hostname ${HOSTNAME}

extract: Linux_for_Tegra/rootfs
	cd Linux_for_Tegra/ \
		&& sudo ./apply_binaries.sh \
		&& sudo ./tools/l4t_flash_prerequisites.sh

Linux_for_Tegra: download
	tar xf ${L4T_RELEASE_PACKAGE}

Linux_for_Tegra/rootfs: Linux_for_Tegra
	sudo tar xpf ${SAMPLE_FS_PACKAGE} -C Linux_for_Tegra/rootfs/

download: ${L4T_RELEASE_PACKAGE} ${SAMPLE_FS_PACKAGE}

${L4T_RELEASE_PACKAGE}:
	wget ${RELEASE_ADDR}/${L4T_RELEASE_PACKAGE}

${SAMPLE_FS_PACKAGE}:
	wget ${RELEASE_ADDR}/${SAMPLE_FS_PACKAGE}
```

microSDに書く方法を使わないのはNVMeだからflushするしかないのもあるが、カメラなどデバイスの認識はLinuxのBoot以前のステップで行われており、BSPのバージョン不一致が大きくなると期待通りに認識できなくなることもあるのでflushするほうがトラブルが起きにくい。[Boot Architecture](https://docs.nvidia.com/jetson/archives/r35.3.1/DeveloperGuide/text/AR/BootArchitecture.html)にBoot時に何が行われているのかが書かれている。

#### Setup by GUI

(事前に`make add_user`をした場合はこのステップは不要)
Flushが成功したらUBuntu20.04のセットアップ画面が開いているはずなので、適当に設定をする。
セットアップが終わったら、無線の場合は無線の接続設定をしてPowerModeを15Wなどに設定して再起動。
mDNSによって設定した`<hostnane>.local`でアクセスできるはずなので以降SSHで作業をする。
USB経由で仮想ネットワーク接続もあるのでそちらでつないでも構わない(があまり安定していないように見受けられるので有線LANを推奨)

この記事では以降`orin-nano`というhostname `jetson`というユーザーで作ったという前提で進める

### Setup SSH with VSCode

GUIを使った開発をすることもあると思うのでVSCode Remote SSHを利用したときに、LocalMachine側にGUI表示が出るように設定をする。

ssh configの設定を以下のように設定する。
頻繁に再インストールしてfingerprintが変わる場合はIPチェックは外しておくのが楽。
ForwardX11はGUIアプリケーションを開発する場合に必要な設定。

```~/.ssh/config(LocalMachine)
Host orin-nano
	HostName orin-nano.local
	User jetson
	StrictHostKeyChecking no
	UserKnownHostsFile=/dev/null
	ForwardX11 yes
	ForwardX11Trusted yes
```

ホスト側もX11 forwardを許可してsshdを再起動する

```diff: /etc/ssh/sshd_config
+ X11Forwarding yes
+ X11DisplayOffset 10
+ X11UseLocalhost yes
```

SSHのたびにパスワードを入れなくて良いようにSSH鍵登録をする

```sh
# まだ鍵を作っていない場合
ssh-keygen -t ed25519

# orin-nano鍵を登録
ssh-copy-id orin-nano
```


この時点でVSCodeから`Ctrl+Shift+P` -> `Remote SSH: Connect to Host` -> `orin-nano` で接続ができる。
VSCode Remote SSHはデフォルトでX11 Forwardingをしているので`xeyes`などを実行するとLocalMachineに画面が出てくる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92809/92119e82-28f4-ec79-c73b-78b77063b887.png)


#### 注意点

xeyesなどの代表的なX11を使うアプリケーションは動作するが、NVIDIAのEGLを使うような処理の場合、例えば`nvarguscamerasrc`などはDISPLAYのEGL初期化に失敗して動かせなくなる場合がある。

```sh
gst-launch-1.0 nvarguscamerasrc ! fakesink
libEGL warning: DRI3: failed to query the version
libEGL warning: DRI2: failed to authenticate
Setting pipeline to PAUSED ...
Pipeline is live and does not need PREROLL ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
GST_ARGUS: Creating output stream
(Argus) Error BadParameter:  (propagating from src/eglstream/FrameConsumerImpl.cpp, function initialize(), line 89)
(Argus) Error BadParameter:  (propagating from src/eglstream/FrameConsumerImpl.cpp, function create(), line 44)
Error generated. /dvs/git/dirty/git-master_linux/multimedia/nvgstreamer/gst-nvarguscamera/gstnvarguscamerasrc.cpp, threadInitialize:320 Failed to create FrameConsumer
Error generated. /dvs/git/dirty/git-master_linux/multimedia/nvgstreamer/gst-nvarguscamera/gstnvarguscamerasrc.cpp, threadFunction:241 (propagating)
Error generated. /dvs/git/dirty/git-master_linux/multimedia/nvgstreamer/gst-nvarguscamera/gstnvarguscamerasrc.cpp, waitRunning:203 Invalid thread state 3
Error generated. /dvs/git/dirty/git-master_linux/multimedia/nvgstreamer/gst-nvarguscamera/gstnvarguscamerasrc.cpp, execute:806 (propagating)
Got EOS from element "pipeline0".
Execution ended after 0:00:00.092742811
Setting pipeline to NULL ...
Freeing pipeline ...
```

そういうときはX11Forwardを諦めてDISPLAYをunsetすると動作することがある。

```sh
unset DISPLAY

gst-launch-1.0 nvarguscamerasrc ! fakesink
Setting pipeline to PAUSED ...
Pipeline is live and does not need PREROLL ...
Setting pipeline to PLAYING ...
New clock: GstSystemClock
GST_ARGUS: Creating output stream
CONSUMER: Waiting until producer is connected...
GST_ARGUS: Available Sensor modes :
...
```

### install multimedia-api and nvidia-docker

ここからはアプリケーション向けのセットアップ。
このままJetson上で構築してももちろんいいが、実運用を考えるとコンテナで扱える方法を知っていると助かるケースがあるので、コンテナでの開発環境をセットアップする。

```mermaid
flowchart LR

L[LocalMachine]
subgraph Jetson
W[WorkDir]
C[WorkContainer]
end


L --SSH--> W
W --mount--> C
L --vscode:open-->C
```

まずはdockerとnvidiaのruntime、そしてJetsonの特徴の一つであるmultimedia-apiをインストールする。
デフォルトのruncではL4T関連のリソースが使えないのでruntimeのデフォルトを変更する

```sh
sudo apt install -y \
    libnvidia-container-tools \
    libnvidia-container0 \
    nvidia-container-runtime \
    nvidia-docker2 \
    nvidia-l4t-jetson-multimedia-api \
    curl

# sudo lessで使えるようにする
sudo usermod -aG docker $USER

# nvidia runtimeをデフォルトにする
sudo vi /etc/docker/daemon.json
```


```diff: /etc/docker/daemon.json
{
+    "default-runtime": "nvidia",
    "runtimes": {
        "nvidia": {
            "path": "nvidia-container-runtime",
            "runtimeArgs": []
        }
    }
}
```

再起動したら以下のようになっていればOK

```sh
docker info | grep Runtime
 Runtimes: io.containerd.runc.v2 io.containerd.runtime.v1.linux nvidia runc
 Default Runtime: nvidia
```

#### buildx

DevContainerが使うためbuildxをinstall

```sh
wget https://github.com/docker/buildx/releases/download/v0.10.4/buildx-v0.10.4.linux-arm64
mkdir -p $HOME/.docker/cli-plugins
cp buildx-v0.10.4.linux-arm64 $HOME/.docker/cli-plugins/docker-buildx
chmod +x ~/.docker/cli-plugins/docker-buildx
```

docker buildxコマンドが使えるようになる

```sh
fmy:~ $ docker buildx

Usage:  docker buildx [OPTIONS] COMMAND

Extended build capabilities with BuildKit
```

### Setup VSCode Dev Container with Remote SSH

RemoteSSHからさらにVSCodeの`Dev Container`を使って開発する方法を紹介する。
コンテナの中でもローカル環境とほとんど操作感が変わらず作業ができてとても助かる。

設定のテンプレートは[このリポジトリ](https://github.com/uzuna/jetson-devenv-with-vscode)にPushしてある。

DockerFileはコメントを入れているので`devcontainer.json`について解説をする

:warning: X11の動作についてはコンテナ内では不安定な(displayが開けない、Nv関係のエラーが出る)ためまだ検証中

```json-doc
{
    // buildの設定
    "build": {
        "dockerfile": "Dockerfile",
        // Jetpackにあわせたベースコンテナを使う指定をする
        // CUDA_VERなどはビルド時に指定が必要なことが多いので入れておくと後が楽
        "args": {
            "L4T_VERSION": "r35.3.1",
            "BASE_IMAGE_NAME": "nvcr.io/nvidia/l4t-base:35.3.1",
            "DS_VERSION": "6.2",
            "CUDA_VER": "11.4"
        }
    },
    // 前者はJetson固有のリソース
    // L4Tコンテナ内ではL4Tに関するパッケージのinstallができないため
    // Hostにインストールした後bindingする形になる
    "mounts": [
        {
            "type": "bind",
            "source": "/tmp/argus_socket",
            "target": "/tmp/argus_socket"
        },
        {
            "type": "bind",
            "source": "/usr/src/jetson_multimedia_api",
            "target": "/usr/src/jetson_multimedia_api"
        },
        // HostのX11の設定を引き継ぐ
        {
            "type": "bind",
            "source": "${env:HOME}/.Xauthority",
            "target": "/home/vscode/.Xauthority"
        }
    ],
    // Userの指定をしなければrootがownerのファイルができてしまうため
    // コンテナ内のユーザーのUIDをhostのUIDで上書きしてowner不一致をさせない
    "remoteUser": "vscode",
    "updateRemoteUserUID": true,
    // X11を接続するためにhostネットワークを使い、hostのDISPLAY環境変数を使う
    // DISPLAYは今はdevcontainer.envで決め打ちしている
    "runArgs": [
        "--env-file",
        ".devcontainer/devcontainer.env",
        "--network",
        "host"
    ]
}
```

上記リポジトリの`.devcontainer`以下を作業ディレクトリにコピーし、DockerFileを用途に合わせて修正したら `Ctrl+Shift+P` -> `Dev Container: Reopen in Container`でコンテナ内でも作業ができる。


## 最後に

軽くDeepStreamなどを見たところXavierNXでは変換に時間がかかりすぎていたものがOrinNanoではすぐに終わるなど部分的には上位互換と言えるような雰囲気でした。
今後カメラ周りとかも今後触っていきたい。