Jetson Nano驱动开发避坑指南:如何高效编译内核并正确挂载驱动(实测有效)
Jetson Nano驱动开发实战:从内核编译到驱动装载的深度避坑手册
如果你刚拿到Jetson Nano,想在上面跑自己的第一个驱动程序,大概率会经历这么几个阶段:先是兴奋地按照官方教程搭建环境,然后发现编译内核要等上大半天,好不容易编译完了,驱动装载时又遇到各种权限问题、路径错误,最后测试应用时那个command not found的提示能让人瞬间崩溃。我去年接手一个边缘计算项目时,就在这块开发板上折腾了整整一周,光内核就重新编译了三次,才把整个流程跑通。今天这份指南,就是把我踩过的坑、总结的经验,以及那些官方文档里没写清楚的细节,全部整理出来。
Jetson Nano作为一款性价比极高的边缘AI开发板,其ARM架构和定制化的Tegra系统让很多从x86平台转过来的开发者不太适应。驱动开发更是如此——你不仅要熟悉Linux内核模块编程,还得搞定交叉编译环境、内核配置、模块装载等一系列嵌入式特有的流程。这篇文章面向的是有一定Linux基础,但嵌入式经验不多的开发者,我会用最直白的方式,带你走完从环境搭建到驱动测试的全过程,重点解决那些实际开发中真正卡住人的问题。
1. 环境准备:不只是安装工具链那么简单
很多人以为环境准备就是下载SDK、安装交叉编译器,其实远不止这些。Jetson Nano的开发环境涉及到主机(通常是x86的Ubuntu)和目标板(ARM架构的Nano)之间的协同,任何一个环节的配置错误,都会导致后续步骤全盘失败。
1.1 选择合适的主机系统版本
官方推荐使用Ubuntu 18.04或20.04作为主机开发环境,但我实测下来,Ubuntu 20.04 LTS是最稳定的选择。22.04版本虽然新,但有些依赖库的版本可能不兼容,会导致编译时出现奇怪的错误。如果你已经装了22.04,也不是不能用,只是需要多花时间处理依赖问题。
主机系统的磁盘空间也要提前规划好。完整的内核源码加上编译中间文件,大概需要30-40GB的空间。我建议专门分出一个至少100GB的分区给开发环境,避免编译到一半磁盘爆满的尴尬。
# 检查磁盘空间
df -h /home
# 建议可用空间 > 50GB
1.2 获取正确的源码和工具链
NVIDIA的开发者网站提供了多个版本的BSP(Board Support Package),你需要根据自己板子的具体型号和JetPack版本选择对应的源码包。这里有个关键点:一定要确保源码版本、工具链版本、板子当前系统版本三者一致。
我遇到过最头疼的情况就是版本不匹配——编译出来的内核能启动,但驱动加载时提示Invalid module format,查了半天才发现是内核版本号对不上。
注意:下载源码包时,除了kernel源码,还要下载对应的
kernel_src.tbz2和public_sources.tbz2。后者包含了设备树、配置文件等重要资源。
工具链的配置也有讲究。官方提供的Linaro GCC 7.3.1是经过NVIDIA测试的,虽然版本不算新,但稳定性最好。有些开发者喜欢用更新的GCC版本,这可能会引入一些未预期的行为。
# 解压工具链到/opt目录
sudo tar -xvf gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu.tar.xz -C /opt/
# 设置环境变量(永久生效)
echo 'export PATH=/opt/gcc-linaro-7.3.1-2018.05-x86_64_aarch64-linux-gnu/bin:$PATH' >> ~/.bashrc
echo 'export CROSS_COMPILE=aarch64-linux-gnu-' >> ~/.bashrc
echo 'export ARCH=arm64' >> ~/.bashrc
source ~/.bashrc
配置完环境变量后,务必验证一下:
aarch64-linux-gnu-gcc --version
# 应该显示:gcc version 7.3.1 20180425
1.3 解决常见的依赖问题
编译内核需要一堆开发库,如果缺了某个依赖,错误信息可能很隐晦。下面这个列表是我总结的必备依赖,安装它们能避免90%的编译错误:
- 基础构建工具:
build-essential、libncurses-dev、bison、flex - SSL相关:
libssl-dev(内核模块签名需要) - 设备树编译:
device-tree-compiler - Python3:
python3、python3-pip(一些配置脚本需要) - 其他工具:
git、wget、curl、lz4(镜像压缩)
安装命令很简单:
sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev \
device-tree-compiler python3 python3-pip git wget curl lz4 -y
2. 内核编译优化:速度提升与配置技巧
编译Jetson Nano的内核是个耗时活儿,在普通的开发机上可能要等上几个小时。但通过一些优化手段,我们可以把时间缩短到30分钟以内。
2.1 并行编译与资源分配
make命令的-j参数用来指定并行编译的作业数,很多人会设成CPU核心数,但这不一定是最优的。我的经验是:设为CPU核心数的1.5倍效果更好。比如8核CPU,就用-j12。
# 查看CPU核心数
nproc
# 假设输出是8,那么可以设置
export JOBS=$(( $(nproc) * 3 / 2 ))
sudo make ARCH=arm64 O=$TKOUT -j${JOBS}
但要注意,并行编译会占用大量内存。如果内存不足,系统会开始使用swap,反而拖慢速度。一个简单的判断方法是:确保可用内存大于CPU核心数 × 2GB。
2.2 配置文件的正确选择
NVIDIA为不同的硬件变体提供了多个默认配置。你需要根据自己板子的具体型号选择:
| 配置文件名 | 适用硬件 | 备注 |
|---|---|---|
tegra_defconfig | 标准Jetson Nano | 最常用的配置 |
tegra_defconfig_android | 运行Android的Nano | 如果需要Android支持 |
tegra_defconfig_debug | 带调试信息的版本 | 内核开发时使用,体积较大 |
配置命令:
cd /path/to/kernel/source
sudo make ARCH=arm64 O=$TKOUT tegra_defconfig
如果你需要添加自己的驱动支持,不要直接修改.config文件,而是用menuconfig:
sudo make ARCH=arm64 O=$TKOUT menuconfig
这里有个实用技巧:先保存当前的配置,再进行比较:
# 保存默认配置
cp $TKOUT/.config config.orig
# 修改配置后
diff config.orig $TKOUT/.config
2.3 只编译所需模块
如果你只是开发某个特定的驱动,不需要重新编译整个内核。可以先找出你的驱动依赖哪些模块:
# 假设你的驱动叫mydriver
grep -r "mydriver" drivers/ --include="*.c" --include="*.h"
# 查看依赖
modinfo mydriver.ko # 编译后才能用这个命令
然后只编译相关的目录:
# 只编译某个目录
sudo make ARCH=arm64 O=$TKOUT M=drivers/char modules
2.4 编译过程中的常见错误处理
我整理了几个编译时可能遇到的错误和解决方法:
-
"recipe for target 'scripts/mod/empty.o' failed"
- 原因:通常是因为
bc计算器工具缺失或版本问题 - 解决:
sudo apt install bc
- 原因:通常是因为
-
"openssl/opensslv.h: No such file or directory"
- 原因:缺少OpenSSL开发头文件
- 解决:
sudo apt install libssl-dev
-
"fatal error: gelf.h: No such file or directory"
- 原因:缺少libelf开发文件
- 解决:
sudo apt install libelf-dev
-
编译到一半卡住,内存占用很高
- 原因:可能是并行任务太多导致内存不足
- 解决:减少
-j参数的值,或者增加swap空间
3. 驱动开发实战:从Hello World到实际设备
驱动开发最难的不是写代码,而是调试。一个简单的字符设备驱动,可能因为一个小的配置错误就无法加载。
3.1 最简单的字符设备驱动示例
我们先从一个最简单的驱动开始,这个驱动只实现基本的打开、关闭、读写操作:
// hello_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "hello_dev"
static int major_num;
static int device_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Hello device opened\n");
return 0;
}
static int device_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "Hello device closed\n");
return 0;
}
static ssize_t device_read(struct file *filp, char *buffer, size_t len, loff_t *offset) {
char message[] = "Hello from kernel!\n";
int message_len = strlen(message);
if (*offset >= message_len) return 0;
if (len > message_len - *offset)
len = message_len - *offset;
if (copy_to_user(buffer, message + *offset, len))
return -EFAULT;
*offset += len;
return len;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = device_open,
.release = device_release,
.read = device_read,
};
static int __init hello_init(void) {
major_num = register_chrdev(0, DEVICE_NAME, &fops);
if (major_num < 0) {
printk(KERN_ALERT "Failed to register device\n");
return major_num;
}
printk(KERN_INFO "Hello driver loaded with major %d\n", major_num);
return 0;
}
static void __exit hello_exit(void) {
unregister_chrdev(major_num, DEVICE_NAME);
printk(KERN_INFO "Hello driver unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello world driver");
对应的Makefile也很关键:
# Makefile
obj-m := hello_drv.o
KDIR := /path/to/kernel/build/directory
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
3.2 编译驱动的注意事项
编译驱动时最容易出错的是内核路径的设置。KDIR必须指向你编译内核时使用的输出目录(就是O=$TKOUT指定的目录),而不是内核源码目录。
# 正确的方式
export KERNEL_OUT=/home/yourname/Jetson/Linux_for_Tegra/source/public/kernel/kernel-4.9/out
cd /path/to/your/driver
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -C $KERNEL_OUT M=$(pwd) modules
编译成功后,你会得到hello_drv.ko文件。用file命令检查一下:
file hello_drv.ko
# 应该显示:ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), BuildID[sha1]=...
如果显示的是x86_64,说明交叉编译环境没配置对。
3.3 驱动的装载与卸载
把.ko文件拷贝到Jetson Nano上后,装载前先检查依赖:
# 在Nano上执行
modinfo hello_drv.ko
# 查看vermagic是否与当前内核匹配
装载驱动:
sudo insmod hello_drv.ko
# 查看是否装载成功
lsmod | grep hello
dmesg | tail -20 # 查看内核日志
如果出现Invalid module format错误,通常是内核版本不匹配。检查vermagic:
# 在开发机上查看驱动信息
modinfo hello_drv.ko | grep vermagic
# 在Nano上查看内核版本
uname -r
两者必须完全一致,包括后面的-tegra后缀。
3.4 创建设备节点
驱动装载后还需要创建设备节点,应用程序才能访问:
# 查看驱动分配的主设备号
cat /proc/devices | grep hello
# 假设输出:250 hello_dev
# 创建设备节点
sudo mknod /dev/hello_dev c 250 0
sudo chmod 666 /dev/hello_dev
更规范的做法是在驱动里自动创建设备节点,这需要用到class_create()和device_create()函数,但作为入门,手动创建更直观。
4. 上层应用测试与调试技巧
驱动写好了,怎么测试它是否正常工作?这里有几个实用的测试方法和调试技巧。
4.1 最简单的测试程序
// test_driver.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd;
char buffer[100];
fd = open("/dev/hello_dev", O_RDONLY);
if (fd < 0) {
perror("Failed to open device");
return 1;
}
ssize_t bytes_read = read(fd, buffer, sizeof(buffer)-1);
if (bytes_read < 0) {
perror("Failed to read from device");
close(fd);
return 1;
}
buffer[bytes_read] = '\0';
printf("Read %zd bytes: %s", bytes_read, buffer);
close(fd);
return 0;
}
编译测试程序也要用交叉编译器:
aarch64-linux-gnu-gcc -o test_driver test_driver.c -static
4.2 权限问题的彻底解决
那个经典的command not found错误,我遇到过好几次。根本原因不是环境变量,而是文件权限和装载位置。
首先,确保测试程序有执行权限:
# 在Nano上
chmod +x test_driver
其次,如果你把程序放在SD卡或U盘里,这些文件系统可能默认挂载为noexec:
# 检查挂载选项
mount | grep /media
# 如果看到noexec,需要重新挂载
sudo mount -o remount,exec /media/yourusername/yourdrive
更好的做法是把测试程序放在Nano本地的文件系统里,比如/home/nano/tests/。
4.3 系统日志的查看技巧
驱动调试离不开内核日志。除了dmesg,还有几个有用的命令:
# 实时查看内核日志
sudo dmesg -w
# 查看特定驱动的日志
dmesg | grep hello_drv
# 查看更早的日志(如果系统有持久化日志)
sudo journalctl -k
# 设置日志级别,让驱动输出更多信息
echo 8 > /proc/sys/kernel/printk
4.4 常见测试问题与解决
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
open(): No such file or directory | 设备节点不存在 | 检查/dev/下是否有对应设备节点 |
open(): Permission denied | 权限不足 | sudo chmod 666 /dev/your_device |
read(): Invalid argument | 驱动read函数实现有误 | 检查驱动中的copy_to_user返回值 |
| 程序卡住不返回 | 驱动中有死锁或阻塞 | 检查驱动中的锁和等待队列 |
| 数据读取不正确 | 用户空间和内核空间数据拷贝错误 | 检查copy_to_user/copy_from_user的使用 |
4.5 性能测试与优化
对于简单的驱动,性能可能不是问题。但如果你开发的是需要处理大量数据的驱动(比如摄像头、网络设备),就需要关注性能。
几个基本的性能测试方法:
- 延迟测试:测量从用户空间调用到驱动返回的时间
- 吞吐量测试:测试大数据量的读写速度
- 并发测试:多个进程同时访问驱动时的表现
可以在驱动里添加时间统计:
#include <linux/ktime.h>
ktime_t start, end;
s64 elapsed;
start = ktime_get();
// 你的驱动代码
end = ktime_get();
elapsed = ktime_to_ns(ktime_sub(end, start));
printk(KERN_INFO "Operation took %lld ns\n", elapsed);
5. 生产环境部署考虑
开发测试通过的驱动,要部署到实际产品中还需要考虑更多因素。
5.1 驱动的自动加载
生产环境中不能每次重启都手动insmod。有几种自动加载的方式:
方法一:添加到/etc/modules
echo "hello_drv" | sudo tee -a /etc/modules
方法二:使用modprobe配置
# 创建配置文件
sudo sh -c 'echo "options hello_drv debug=1" > /etc/modprobe.d/hello_drv.conf'
方法三:systemd服务(更现代的方式)
# /etc/systemd/system/hello-drv.service
[Unit]
Description=Load Hello Driver
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/sbin/insmod /lib/modules/$(uname -r)/extra/hello_drv.ko
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
5.2 驱动的版本管理
随着内核更新,驱动也需要维护多个版本。我建议的版本管理策略:
- 使用Git管理驱动源码,每个内核版本一个分支
- 在驱动代码中定义版本号:
#define DRIVER_VERSION "1.0.2" MODULE_VERSION(DRIVER_VERSION); - 编译时自动包含版本信息:
EXTRA_CFLAGS += -DDRIVER_VERSION=\"$(shell git describe --tags)\"
5.3 错误处理与日志管理
生产环境的驱动需要有完善的错误处理和日志记录:
// 定义错误码
#define HELLO_ERR_NO_MEM -ENOMEM
#define HELLO_ERR_IO -EIO
// 带上下文的错误打印
#define hello_err(fmt, ...) \
pr_err("hello_drv: %s:%d: " fmt, __func__, __LINE__, ##__VA_ARGS__)
// 条件编译的调试信息
#ifdef DEBUG
#define hello_dbg(fmt, ...) \
pr_debug("hello_drv: %s:%d: " fmt, __func__, __LINE__, ##__VA_ARGS__)
#else
#define hello_dbg(fmt, ...)
#endif
5.4 安全性考虑
驱动运行在内核空间,一个bug就可能导致系统崩溃。一些基本的安全实践:
- 输入验证:所有从用户空间传来的数据都要验证
- 权限检查:重要的操作要检查调用者权限
- 资源限制:防止用户空间程序耗尽内核资源
- 内存安全:小心使用
copy_from_user和copy_to_user
static long device_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
// 检查权限
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
// 验证用户空间指针
if (_IOC_DIR(cmd) & _IOC_READ) {
if (!access_ok((void __user *)arg, _IOC_SIZE(cmd)))
return -EFAULT;
}
// ... 处理ioctl
}
5.5 性能监控与维护
驱动部署后还需要监控其运行状态。可以添加一些统计信息到/proc或sysfs:
// 在/proc中创建统计信息
static int hello_proc_show(struct seq_file *m, void *v) {
seq_printf(m, "Open count: %d\n", open_count);
seq_printf(m, "Read count: %d\n", read_count);
seq_printf(m, "Last error: %d\n", last_error);
return 0;
}
static int __init hello_init(void) {
// ... 其他初始化
proc_create_single("driver/hello_stats", 0, NULL, hello_proc_show);
return 0;
}
这样运维人员可以通过cat /proc/driver/hello_stats查看驱动状态。
驱动开发最难的部分其实不是技术本身,而是那些文档里没写的细节和坑。我记得第一次成功让驱动跑起来时,那种成就感比写完整个应用程序还要强烈。嵌入式驱动开发就是这样,你大部分时间都在和系统底层打交道,解决各种稀奇古怪的问题,但一旦打通了,你对整个计算机系统的理解就会上一个层次。
在实际项目中,我建议从最简单的驱动开始,每实现一个功能就充分测试,不要等到所有代码写完再测试。多用printk输出调试信息,但记得在生产版本中关掉调试输出。还有,一定要用版本控制工具,每次编译前确认内核版本和配置,这些看似琐碎的习惯能节省你大量的调试时间。
更多推荐



所有评论(0)