JetsonXavierNX摄像头驱动开发指南

参考手册:NVIDIA Jetson Linux Developer Guide(下文中所指的参考手册均是指该手册)

Jetson资料下载中心:Jetson Download Center | NVIDIA Developer

Jetson Xavier NX 信息:

  • Linux jetson-desktop 4.9.253-tegra
  • L4T 32.7.2

platform代码:

  • t234:Jetson AGX Orin
  • t194:Jetson Xavier NX 系列、Jetson AGX Xavier 系列
  • t210:jetson nano、jetson tx1

内核定制化

下载编译内核

Jetson Linux | NVIDIA Developer处下载最新的(或者自己选L4T版本号)内核源码L4T Driver Package (BSP) Sources

注意:

  • 目前(2022/6/4)L4T 34.1之后,暂时只支持 Jetson AGX 系列和 Jetson Xavier 系列,其余暂不支持
  • 下载的是源码包(L4T Driver Package (BSP) Sources

解压TBZ2文件:

1
2
3
tar -xjf public_sources.tbz2
cd Linux_for_Tegra/source/public
tar –xjf kernel_src.tbz2

安装工具链:

注意:

  1. 如果在jetson nano上则不需要该步骤
  2. 官方推荐Linaro gcc 7.3.1 2018.05 aarch64

参考连接:本文最开始说明的参考手册中Kernel Customization -> Jetson Linux Toolchain

下载地址:http://releases.linaro.org/components/toolchain/binaries/7.3-2018.05/aarch64-linux-gnu/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz

解压:

1
2
3
mkdir $HOME/l4t-gcc
cd $HOME/l4t-gcc
tar xf gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz

编译内核:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 安装依赖
sudo apt install build-essential bc
# 指定输出目录
TEGRA_KERNEL_OUT=<outdir>
# 如果不在板子上编译则需要指定交叉编译工具链
export CROSS_COMPILE=$HOME/l4t-gcc/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
export LOCALVERSION=-tegra
# 创建 .config 文件
cd <kernel_source>
mkdir -p $TEGRA_KERNEL_OUT
make ARCH=arm64 O=$TEGRA_KERNEL_OUT tegra_defconfig
# 编译内核、dtb
make ARCH=arm64 O=$TEGRA_KERNEL_OUT -j<n>
make ARCH=arm64 O=$TEGRA_KERNEL_OUT dtbs
# 安装内核模块到指定位置
sudo make ARCH=arm64 O=$TEGRA_KERNEL_OUT modules_install INSTALL_MOD_PATH=<modules_install_path>
# 打包内核模块
cd <modules_install_path>
tar --owner root --group root -cjf kernel_supplements.tbz2 lib/modules

编译生成的内核以及设备树在:

  • 内核:TEGRA_KERNEL_OUT/arch/arm64/boot/Image
  • 设备树:TEGRA_KERNEL_OUT/arch/arm64/boot/dts/

编译内核模块

找到内核的头文件目录:

  • 对于在jetson上编译来说,头文件在/usr/src/linux-headers-$(uname -r)-ubuntu18.04_aarch64/lib/modules/$(uname -r)/build

  • 对于其他平台来说,需要下载内核源码并解压,按照上述操作,之后运行以下命令:

    1
    make ARCH=arm64 O=$TEGRA_KERNEL_OUT -j<n> modules_prepare

编译:

1
2
3
4
cd <path_to_module_source>
make ARCH=arm64 –C <kernel_directory> M=$(pwd)
# 执行以下命令从内核模块中去除不需要的符号:
<tool_chain_path>/aarch64-linux-gnu-strip -–strip-unneeded <path-of-kernel-module.ko>

摄像头驱动开发

官方提供两种开发方式:

  • Camera Core Library接口
  • 直接通过V4L2访问

对于Camera Core Library来说,Linux Driver Package (L4T)框架上的引用和内核模式的V4L2驱动之间的关系如下所示:

A screenshot of a cell phone Description generated with high confidence

对于直接通过V4L2访问和方式来说,其驱动于应用之间的关系为:

A screenshot of a cell phone Description generated with very high confidence

对于这种方式,需要:

  • 在内核中添加设备树节点
  • 开发对应的V4L2驱动,对于该驱动官方有两种版本,建议用最新的2.0。以下为可以参考的Sony IMX185驱动代码
    • Version 1.0 driver: imx185_v1.c
    • Version 2.0 driver: imx185.c

设备树

位置

参考手册中:Bootloader -> U-Boot Customization -> Environment Configuration -> exlinux.conf

在L4T中,一般最后的设备树启动文件*.dtb在根文件系统的/boot/dtb/下,同时可以通过以下命令找到具体位置:

1
2
3
cat /sys/firmware/devicetree/base/nvidia,dtsfilename
cat /proc/device-tree/nvidia,dtsfilename
# /dvs/git/dirty/git-master_linux/kernel/kernel-4.9/arch/arm64/boot/dts/../../../../../../hardware/nvidia/platform/t210/porg/kernel-dts/tegra210-p3448-0000-p3449-0000-b00.dts
  • 默认位置:
    • Jetson Nano:XXXX/hardware/nvidia/platform/t210/porg/kernel-dts/tegra210-p3448-0000-p3449-0000-b00.dts
    • Jetson Xavier NX:XXXX/hardware/nvidia/platform/t19x/jakku/kernel-dts/tegra194-p3668-all-p3509-0000.dts

同时通过/boot/extlinux/extlinux.conf配置文件(参见EXTLINUX - Syslinux Wiki)可以使得uboot通过dtb overlays的方式选取不同的设备树或者启动参数来启动系统,对Linux驱动编写与调试方便很多

如下所示为本文中对应的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TIMEOUT 30
DEFAULT primary

MENU TITLE L4T boot options

LABEL primary
MENU LABEL primary kernel
LINUX /boot/Image
INITRD /boot/initrd
FDT /boot/tegra194-p3668-all-p3509-0000-ice.dtb
APPEND ${cbootargs} root=/dev/mmcblk0p1 rw rootwait rootfstype=ext4 console=ttyTCU0,115200n8 console=tty0 fbcon=map:0 net.ifnames=0

LABEL backup
MENU LABEL backup dtb
LINUX /boot/Image
INITRD /boot/initrd
APPEND ${cbootargs} quiet root=/dev/mmcblk0p1 rw rootwait rootfstype=ext4 console=ttyTCU0,115200n8 console=tty0 fbcon=map:0 net.ifnames=0

其中:

  • TIMEOUT:默认等待选择时间
  • DEFAULT:默认启动方式
  • LINUX:Linux内核存放位置
  • FDT:设备树文件(不指定则用默认位置,即/boot/dtb/下文件)
  • FDTOVERLAYS:xxx.dtbo文件,用于补充或者覆盖设备树文件中的一些配置(uboot会根据该文件自动修改设备树)

通过上述命令编译设备树后,其生成的*.dtb文件会出现在output/arch/arm64/boot/dts下,

调试

设备树目录:/proc/device-tree(软连接)和/sys/firmware/devicetree/base/(实际位置)

1
2
3
4
5
6
# 调试
fdtdump /boot/tegra210-p3448-0000-p3449-0000-a02-fe-pi-audio.dtbo
# 反编译
dtc -I dtb -O dts <filename>
# 查看属性(hex)
hexdump -C /proc/device-tree/host1x/i2c@546c0000/tca9546@70/i2c@2/reg

基础含义

在设备树文件<top>/hardware/nvidia/platform/t19x/common/kernel-dts/t19x-common-modules/tegra194-camera-imx185-a00.dtsi中找到tegra-camera-platform节点

tegra-camera-platform节点中创建一个模块表(module table),每个模块必须包含其基本信息和该节点内设备的定义

如下所示为一种典型的节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
tegra-camera-platform {
compatible = "nvidia, tegra-camera-platform";
modules {
module0 {
badge = "imx185_bottom_liimx185";
position = "bottom";
orientation = "0";
drivernode0 {
pcl_id = "v4l2_sensor";
devname = "imx185 30-001a";
proc-device-tree =
"/proc/device-tree/i2c@3180000/tca9546@70/i2c@0/imx185_a@1a";
};
};
};
};

属性含义:

  • badge:标识此模块的唯一名称。
    • 名称必须由三部分组成,用下划线隔开:
      • 模组的摄像头板卡ID (camera_board_id)
      • 模块的位置,例如后置(rear)或前置(front)
      • 模块的零件号,可在模块数据表中找到。如果零件号不可用,请使用唯一标识符。只有零件号的最后六个字符是有效的
    • 例如,imx185_rear_liimx185代表一个模块,该模块带有一个后置imx185摄像头,零件号为“llimx185”
    • 如果系统有多个相同的模块,则每个模块必须具有不同的位置,从而使模块名称唯一
  • position:相机位置。支持的值取决于系统中的摄像机数量
    • 在双摄像头系统中:rear 和 front
    • 在三摄像头系统中:bottom、top 和 center
    • 在六摄像头系统中:bottomleft、bottomright、centerleft、centerright、topleft 和 topright
  • orientation:传感器方向;表示传感器方向的数字索引。索引和方向之间的关系是任意的,但通常在双摄像头系统中,0 是“后向”,1 是“前向”

CBoot

L4T 32.7.2中的bootloader存在问题,在修改上述/boot/extlinux/extlinux.conf配置文件后,boot阶段并不会生效,如下为对应的log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[0008.443] I> ########## Fixed storage boot ##########
[0008.448] I> Loading kernel-bootctrl from partition
[0008.453] I> Loading partition kernel-bootctrl at 0xa42e0000 from device(0x1)
[0008.467] W> tegrabl_get_kernel_bootctrl: magic number(0x00000000) is invalid
[0008.468] W> tegrabl_get_kernel_bootctrl: use default dummy boot control data
[0008.474] I> Already published: 00010003
[0008.477] I> Look for boot partition
[0008.481] I> Fallback: assuming 0th partition is boot partition
[0008.487] I> Detect filesystem
[0008.514] I> Loading extlinux.conf ...
[0008.514] I> Loading extlinux.conf binary from rootfs ...
[0008.514] I> rootfs path: /sdmmc_user/boot/extlinux/extlinux.conf
[0008.557] I> ext4_read_data_from_extent:298: Total file read should not be larger than file stat size
[0008.558] E> file /sdmmc_user/boot/extlinux/extlinux.conf read failed!!
[0008.558] W> Failed to load extlinux.conf binary from rootfs (err=202113305)
[0008.559] E> Failed to find/load /boot/extlinux/extlinux.conf
[0008.560] I> Loading kernel ...
[0008.563] I> No kernel binary path
[0008.566] I> Continue to load from partition ...
[0008.571] W> No valid slot number is found in scratch register

主要是其中的[0008.557] I> ext4_read_data_from_extent:298: Total file read should not be larger than file stat size,导致读取extlinux.conf失败

根据Cboot in 32.7.2 fails to read extlinux.conf中所述,需要手动修改并编译bootloader

下载编译CBoot

Jetson Linux R32.7.2 Release Page | NVIDIA Developer处下载CBoot Source T194(Jetson Xavier NX)

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 解压
mkdir cboot
tar -xjf cboot_src_t194.tbz2 -C cboot
cd cboot
# 2. 设置工具链(ARM64位工具链,形如aarch64-linux-gnu-)
export CROSS_COMPILE=$HOME/l4t-gcc/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
# 3. 设置相关环境变量
export TEGRA_TOP=$PWD
export TOP=$PWD
# 4. 编译,最终的二进制文件位于./out/build-t194/...
make -C ./bootloader/partner/t18x/cboot PROJECT=t194 TOOLCHAIN_PREFIX="${CROSS_COMPILE}" DEBUG=2 BUILDROOT="${PWD}"/out NV_TARGET_BOARD=t194ref NV_BUILD_SYSTEM_TYPE=l4t NOECHO=@
# 5. 将生成的lk.bin重命名为cboot_t194.bin
cp lk.bin cboot_t194.bin

如果系统的python版本为python3,则需要注意:

  1. 修改./bootloader/partner/t18x/cboot/build/get_branch_name.py中第24行,即print prj.getAttribute("revision")改为print(prj.getAttribute("revision"))
  2. 修改./bootloader/partner/t18x/cboot/scripts/add_version_info.py中第43行,即f.write(struct.pack('%ds' % (length), version_string))改为 f.write(struct.pack('%ds' % (length), version_string.encode('utf-8')))
  3. 编译中间会报错缺少.repo/manifest.xml文件,可以忽略

修改CBoot

修改CBoot,解决启动后读取extlinux.conf失败的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
diff --git a/lib/fs/ext4/ext4.c b/lib/fs/ext4/ext4.c
index c994c0f..6e8e2e7 100644
--- a/lib/fs/ext4/ext4.c
+++ b/lib/fs/ext4/ext4.c
@@ -244,6 +244,9 @@
uint8_t *buf_ptr = NULL;
off_t total_bytes_read = 0;
int err = 0;
+ uint32_t file_len_in_blocks = 0;
+ uint32_t total_blocks_read;
+ uint32_t block_size;

LTRACE_ENTRY;

@@ -254,6 +257,15 @@
}

buf_ptr = (uint8_t *)buf;
+ block_size = E2FS_BLOCK_SIZE(ext2->super_blk);
+
+ if (len != 0UL) {
+ file_len_in_blocks = len/block_size;
+ if ((len % block_size) != 0U) {
+ file_len_in_blocks++;
+ }
+ LTRACEF("file_len_in_blocks: %u\n", file_len_in_blocks);
+ }

/* Extract extents info */
extent_header = (struct ext4_extent_header *)inode->e2di_blocks;
@@ -284,21 +296,37 @@
err = ext4_read_extent(ext2, (struct ext4_extent_header *)buf2, buf_ptr, &bytes_read);
total_bytes_read += bytes_read;
buf_ptr += bytes_read;
- if ((len != 0) && (total_bytes_read > len)) {
- TRACEF("Total file read should not be larger than file stat size\n");
- err = ERR_NOT_VALID;
- goto fail;
- }
+
+ total_blocks_read = total_bytes_read/block_size;
+ if ((total_bytes_read % block_size) != 0U) {
+ total_blocks_read++;
+ }
+
+ /* Check if extra block isn't read */
+ if ((len != 0) && (total_blocks_read > file_len_in_blocks)) {
+ TRACEF("More blocks are read (%u) than file block count (%u)\n",
+ total_blocks_read, file_len_in_blocks);
+ err = ERR_NOT_VALID;
+ goto fail;
+ }
}
} else {
/* Read leaf node */
err = ext4_read_extent(ext2, extent_header, buf_ptr, &bytes_read);
total_bytes_read += bytes_read;
- if ((len != 0) && (total_bytes_read > len)) {
- TRACEF("Total file read should not be larger than file stat size\n");
- err = ERR_NOT_VALID;
- goto fail;
- }
+
+ total_blocks_read = total_bytes_read/block_size;
+ if ((total_bytes_read % block_size) != 0U) {
+ total_blocks_read++;
+ }
+
+ /* Check if extra block isn't read */
+ if ((len != 0) && (total_blocks_read > file_len_in_blocks)) {
+ TRACEF("More blocks are read (%u) than file block count (%u)\n",
+ total_blocks_read, file_len_in_blocks);
+ err = ERR_NOT_VALID;
+ goto fail;
+ }
}

LTRACEF("err %d, bytes_read %lu\n", err, total_bytes_read);

重新烧录

找到SDK MANGER下的对应目录,一般是形如~/nvidia/nvidia_sdk/JetPack_4.6.2_Linux_JETSON_XAVIER_NX_TARGETS/Linux_for_Tegra,将上述编译生成的cboot_t194.bin放入并替换Linux_for_Tegra/bootloader/cboot_t194.bin

1
2
3
cd bootloader
cp ~/cboot_t194.bin ./
chmod +x cboot_t194.bin

之后可以通过SDK MANGER重新烧录整个系统

或者在Linux_for_Tegra下执行以下命令单独烧写CBoot:

1
sudo ./flash.sh -r -k cpu-bootloader jetson-xavier-nx-devkit-emmc mmcblk0p1
  • -r:跳过构建并重用现有的 system.img
  • -k :flash.cfg 中指定的分区名称或编号
  • jetson-xavier-nx-devkit-emmc:即同文件夹下的jetson-xavier-nx-devkit-emmc.conf配置文件

注意:烧录时需要板卡通过micro usb和PC相连,且板卡处于recovery模式(一般在关机时短接底板上的rec和gnd,上电后松开即可)

测试

参考链接:linux/v4l2 – Gateworks

参考手册中:

  • Multimedia -> Accelerated GStreamer -> Camera Capture with GStreamer-1.0
  • Camera Development -> Sensor Software Driver Programming -> Device Registration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 安装依赖
sudo apt install v4l-utils
# 检测摄像头
v4l2-ctl --list-devices
v4l2-ctl --list-formats-ext
# 检测摄像头支持的格式
v4l2-ctl -d /dev/video1 --list-formats-ext
# 检查摄像头设置
v4l2-ctl -d /dev/video1 --list-ctrls
v4l2-ctl -d /dev/video1 --all

# 输出内核信息
dmesg | grep -i imx219

# 重启nvargus服务 防止gst-launch-1.0导致摄像头打开失败
sudo service nvargus-daemon restart

# 直接显示在屏幕上
nvgstcapture
# 或者 设置对应的摄像头
gst-launch-1.0 nvarguscamerasrc sensor-id=0 sensor-mode=3 ! 'video/x-raw(memory:NVMM),width=3820, height=2464, framerate=21/1, format=NV12' ! nvvidconv flip-method=0 ! 'video/x-raw,width=960, height=616' ! nvvidconv ! nvegltransform ! nveglglessink -e

# https://www.linuxtv.org/wiki/index.php/V4l-utils
# 测试摄像头
v4l2-compliance -d /dev/video<n>
# 验证
v4l2-ctl --set-fmt-video=width=1920,height=1080,pixelformat=RG12 --stream-mmap --set-ctrl=sensor_mode=0 --stream-count=100 -d /dev/video<n>

# gst-launch-1.0方式
# 采集摄像头图像保存为本地视频
gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! 'video/x-raw(memory:NVMM), width=(int)1920, height=(int)1080,format=(string)NV12, framerate=(fraction)30/1' ! nvv4l2h264enc bitrate=8000000 ! h264parse ! qtmux ! filesink location=filename_h264.mp4 -e

gst-launch-1.0 nvarguscamerasrc sensor-id=1 ! 'video/x-raw(memory:NVMM), width=(int)1920, height=(int)1080,format=(string)NV12, framerate=(fraction)30/1' ! nvv4l2h264enc bitrate=8000000 ! h264parse ! qtmux ! filesink location=filename_h264_1.mp4 -e

gst-launch-1.0 nvarguscamerasrc sensor-id=2 ! 'video/x-raw(memory:NVMM), width=(int)1920, height=(int)1080,format=(string)NV12, framerate=(fraction)30/1' ! nvv4l2h264enc bitrate=8000000 ! h264parse ! qtmux ! filesink location=filename_h264_2.mp4 -e

# v4l2-ctl方式
# 保存为图片
v4l2-ctl --device /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=RG10 --set-ctrl bypass_mode=0 --stream-mmap --stream-to=frame.raw --stream-count=1
convert -size 1920x1080 -depth 16 uyvy:frame.raw frame.png

v4l2-ctl --device /dev/video1 --set-fmt-video=width=1920,height=1080,pixelformat=RG10 --set-ctrl bypass_mode=0 --stream-mmap --stream-to=frame.raw --stream-count=1
convert -size 1920x1080 -depth 16 uyvy:frame.raw frame.png
# 默认格式
v4l2-ctl --device /dev/video0 --stream-mmap --stream-to=frame.raw --stream-count=1
convert -size 640x480 -depth 16 uyvy:frame.raw frame.png

# 连续采集测试
v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080,pixelformat=RG10 --set-ctrl bypass_mode=0 --stream-mmap --stream-count=100