第二章 驱动开发环境搭建:交叉编译工具链安装、Linux内核源码获取、设备树(DTS)基础、TFTP/NFS网络启动环境配置

说实话,环境搭建这一步,看着琐碎,但真能卡住不少人。我见过太多新手,代码写得挺溜,结果卡在编译不过、板子起不来这种基础问题上。咱们今天就把这些坑一个个填平。

2.1 交叉编译工具链安装

嵌入式开发嘛,目标芯片性能有限,咱们得在PC上编译好,再把二进制丢到板子上跑。这个在PC上编译、在ARM上运行的编译器,就叫交叉编译工具链。

为什么不用PC自带的gcc? 你想想看,PC的gcc编译出来的是x86指令,ARM芯片根本看不懂。所以必须用专门为ARM定制的gcc。

2.1.1 获取工具链

我个人习惯用Linaro提供的工具链,稳定且社区活跃。以ARM Cortex-A系列为例:

# 下载ARM GCC工具链(以gcc-arm-9.2-2019.12为例)
wget https://releases.linaro.org/components/toolchain/binaries/9.2-2019.12/arm-linux-gnueabihf/gcc-arm-9.2-2019.12-x86_64-arm-linux-gnueabihf.tar.xz

# 解压到指定目录
tar -xvf gcc-arm-9.2-2019.12-x86_64-arm-linux-gnueabihf.tar.xz -C /opt/

# 添加环境变量
export PATH=/opt/gcc-arm-9.2-2019.12-x86_64-arm-linux-gnueabihf/bin:$PATH
小提示: 建议把export命令写到~/.bashrc里,这样每次打开终端自动生效。我刚开始总忘记加,每次都要手动export,烦得很。

2.1.2 验证安装

装完得验证一下,别白忙活:

arm-linux-gnueabihf-gcc --version

如果看到版本号输出,恭喜你,第一步成了。写个简单的hello world试试:

# 编译测试
arm-linux-gnueabihf-gcc -o hello hello.c

# 查看文件类型
file hello
# 输出:hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)

看到ARM字样,说明确实是交叉编译出来的。

注意: 我曾经遇到过工具链版本不匹配导致内核编译失败的情况。比如Linux 4.19内核用gcc 8.x没问题,但用gcc 10.x就报错。建议先查一下内核文档推荐的gcc版本范围。

2.2 Linux内核源码获取

驱动开发离不开内核源码。你想想看,驱动本质上就是内核的一个模块,不了解内核结构怎么行?

2.2.1 从官方仓库克隆

# 克隆主线内核(以5.10 LTS为例)
git clone --depth 1 -b v5.10 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git

# 或者只克隆最新版本,省空间
git clone --depth 1 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git

为什么用--depth 1? 完整的内核仓库有好几个G,咱们做驱动开发一般不需要看历史提交,只取最新代码就够了。省时间也省硬盘。

2.2.2 芯片厂商提供的内核

实际项目中,我建议直接用芯片厂商(如NXP、TI、Rockchip)提供的BSP内核。这些内核已经针对特定芯片做了适配,省去很多移植工作。

# 以NXP i.MX系列为例
git clone https://source.codeaurora.org/external/imx/linux-imx -b imx_5.10.52_2.1.0
核心观点: 主线内核适合学习,厂商内核适合干活。别搞反了。

2.3 设备树(DTS)基础

设备树,说白了就是描述硬件信息的配置文件。以前的内核用大量C代码描述硬件,改个引脚都要重新编译内核。有了设备树,改硬件配置就像改文本文件一样简单。

2.3.1 设备树文件结构

一个典型的设备树文件长这样:

/dts-v1/;

/ {
    model = "My AI Board";
    compatible = "mycompany,myboard";

    chosen {
        stdout-path = &uart1;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  // 512MB内存
    };

    soc {
        uart1: serial@10000000 {
            compatible = "ns16550";
            reg = <0x10000000 0x1000>;
            interrupts = <0 33 4>;
            clock-frequency = <24000000>;
        };

        i2c1: i2c@10020000 {
            compatible = "mycompany,i2c";
            reg = <0x10020000 0x1000>;
            interrupts = <0 34 4>;
            #address-cells = <1>;
            #size-cells = <0>;

            ai_accelerator: ai-chip@10 {
                compatible = "mycompany,ai-chip";
                reg = <0x10>;
                interrupt-parent = <&gpio1>;
                interrupts = <5 2>;
            };
        };
    };
};

关键点解释:

  • compatible:驱动匹配的关键字,驱动通过它找到对应的硬件
  • reg:寄存器地址和大小
  • interrupts:中断号、触发方式
  • &uart1:引用其他节点,类似C语言的指针

2.3.2 编译设备树

设备树源文件(.dts)需要编译成二进制(.dtb)才能被内核使用:

# 编译设备树
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 反编译(查看二进制内容)
dtc -I dtb -O dts -o myboard_decompiled.dts myboard.dtb
经验之谈: 我在项目中遇到过设备树写错导致外设无法识别的情况。排查方法很简单:用ls /dev看设备节点有没有生成,或者用cat /proc/device-tree/查看内核实际解析的设备树。

2.4 TFTP/NFS网络启动环境配置

开发阶段,总不能每次改个驱动就烧写一次Flash吧?太慢了。网络启动可以让你从PC加载内核和文件系统,改完直接重启板子就能看到效果。

2.4.1 TFTP服务器配置(传输内核和设备树)

TFTP用于从PC下载内核镜像和设备树到板子内存:

# 安装TFTP服务器(Ubuntu)
sudo apt-get install tftpd-hpa

# 配置TFTP目录
sudo mkdir -p /srv/tftp
sudo chmod 777 /srv/tftp

# 编辑配置文件 /etc/default/tftpd-hpa
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="--secure"

# 重启服务
sudo systemctl restart tftpd-hpa

把内核镜像zImage和设备树myboard.dtb放到/srv/tftp目录:

cp arch/arm/boot/zImage /srv/tftp/
cp arch/arm/boot/dts/myboard.dtb /srv/tftp/

2.4.2 NFS服务器配置(共享根文件系统)

NFS让板子通过网络挂载PC上的文件系统,这样修改文件系统内容也不用重新烧写:

# 安装NFS服务器
sudo apt-get install nfs-kernel-server

# 创建根文件系统目录
sudo mkdir -p /srv/nfs/rootfs

# 解压根文件系统
sudo tar -xvf rootfs.tar.gz -C /srv/nfs/rootfs/

# 编辑 /etc/exports,添加共享目录
/srv/nfs/rootfs *(rw,sync,no_subtree_check,no_root_squash)

# 重启NFS服务
sudo systemctl restart nfs-kernel-server
注意: no_root_squash这个选项很重要。如果不加,板子上的root用户会被映射成nobody,导致很多操作权限不足。我曾经因为这个排查了半天,最后发现是权限问题。

2.4.3 U-Boot启动参数设置

在板子的U-Boot命令行中设置启动参数:

# 设置IP地址(根据实际网络修改)
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.10

# 设置启动参数
setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.10:/srv/nfs/rootfs rw ip=192.168.1.100:192.168.1.10:192.168.1.1:255.255.255.0::eth0:off'

# 从TFTP加载内核和设备树
tftp 0x42000000 zImage
tftp 0x43000000 myboard.dtb

# 启动内核
bootz 0x42000000 - 0x43000000

参数说明:

  • root=/dev/nfs:告诉内核从NFS挂载根文件系统
  • nfsroot=...:NFS服务器地址和路径
  • ip=...:板子IP、服务器IP、网关、子网掩码
调试技巧: 如果网络启动失败,先ping一下服务器IP确认网络通不通。我习惯在U-Boot里先执行ping 192.168.1.10,通了再继续。

2.5 环境验证

全部配置完成后,重启板子。如果看到内核启动日志,最后出现类似这样的提示:

VFS: Mounted root (nfs filesystem) on device 0:13
Freeing unused kernel memory: 1024K
Run /sbin/init as init process

恭喜你,环境搭建成功了!现在你可以愉快地修改驱动、重新编译、重启板子,整个过程不超过1分钟。

嗯,环境搭建这部分确实繁琐,但这是后面所有工作的基础。我建议你按照这个流程走一遍,遇到问题别慌,多半是网络配置或者路径写错了。下一章咱们开始写第一个真正的AI芯片驱动。