3. SDK架构解析:Processor SDK结构、文件系统布局、内核源码树、U-Boot源码树、设备树(Device Tree)概念

好,咱们进入第三章。这一章我打算把TI的Processor SDK掰开揉碎了讲清楚。说实话,我刚接触TI平台那会儿,第一次解压完SDK,看着满屏的文件夹,心里是有点懵的。但后来做项目多了,发现这套结构其实设计得挺讲究。你只要摸清了它的骨架,后面开发就会顺风顺水。

3.1 Processor SDK整体结构

TI的Processor SDK,说白了就是一个超级大礼包。它把Linux内核、U-Boot、文件系统、工具链、还有各种例程,全给你打包好了。我个人习惯把它理解成「三件套」:

  • 引导加载程序 —— 就是U-Boot,负责把硬件初始化好,然后跳进内核。
  • 操作系统内核 —— Linux内核,负责管理所有资源。
  • 根文件系统 —— 提供用户空间的程序和库。

你下载下来的SDK,解压后大概长这样:

ti-processor-sdk-linux-am335x-evm-XX.XX.XX.XX/
├── bin/                  # 一些辅助脚本和工具
├── board-support/        # 板级支持包,U-Boot和内核源码都在这里
├── example-applications/ # 例程,新手必看
├── filesystem/           # 文件系统相关
├── linux-devkit/         # 交叉编译工具链
├── Makefile              # 顶层构建脚本
├── Rules.make            # 公共编译规则
└── setup.sh              # 环境配置脚本

嗯,这里要注意:board-support/ 这个目录是核心。你后面要改内核、改U-Boot,基本都在这里折腾。

核心要点:SDK的设计哲学是「开箱即用」。你执行 setup.sh 配置好环境,然后 make 一下,就能生成完整的系统镜像。但真正做产品时,我建议你还是得把每个组件单独拎出来理解。

3.2 文件系统布局

TI的SDK提供了两种文件系统:一种是基于BusyBox的最小系统,另一种是完整的Tiny Yocto文件系统。我在项目中遇到过,很多新手上来就选大的,结果发现很多用不到的包占空间,还拖慢启动速度。

咱们先看看文件系统里都有啥:

filesystem/
├── arago-base-tisdk-image-am335x-evm.tar.xz  # 基础文件系统
├── tisdk-rootfs-image-am335x-evm.tar.xz      # 完整文件系统
└── tisdk-rootfs-image-am335x-evm.ubi         # UBI镜像,用于NAND Flash

解压后,根文件系统的典型布局是这样的:

目录 用途 我的经验
/bin 用户命令(如ls, cp) BusyBox的软链接都在这里
/sbin 系统管理命令 ifconfig、reboot这些
/etc 配置文件 网络、启动脚本都在这里改
/lib 共享库和内核模块 驱动ko文件放这里
/usr 用户程序和数据 你写的应用建议放/usr/bin
/var 可变数据(日志等) 调试时经常看/var/log

小技巧:调试阶段,我习惯用NFS挂载根文件系统。这样在PC上改完代码,目标板重启就能生效,省去了反复烧写的麻烦。具体做法是在U-Boot里设置 root=/dev/nfs nfsroot=...

3.3 内核源码树

内核源码在 board-support/linux-XX.XX.XX+gitXXXX 目录下。你想想看,Linux内核有几万甚至几十万个文件,但咱们做嵌入式开发,其实只需要关注几个关键目录:

linux-XX.XX.XX/
├── arch/                # 体系结构相关代码
│   └── arm/             # ARM架构
│       ├── boot/dts/    # 设备树文件(重点!)
│       ├── mach-omap2/  # TI OMAP/AM平台相关
│       └── plat-omap/   # OMAP平台通用代码
├── drivers/             # 设备驱动
│   ├── net/             # 网络驱动
│   ├── mmc/             # MMC/SD卡驱动
│   ├── spi/             # SPI驱动
│   └── i2c/             # I2C驱动
├── include/             # 头文件
├── kernel/              # 内核核心代码
└── Documentation/       # 文档,没事多翻翻

我个人习惯,拿到一个新板子,第一件事就是去 arch/arm/boot/dts/ 目录下看看。因为设备树文件直接决定了你的硬件能不能正常工作。

避坑指南:我曾经在某个项目里,因为内核版本和SDK自带的工具链不匹配,编译出来的内核死活起不来。后来发现,TI的SDK对内核版本和工具链版本有严格的对应关系。所以,不要轻易升级内核版本,除非你清楚自己在做什么。

3.4 U-Boot源码树

U-Boot在 board-support/u-boot-XXXX.XX+gitXXXX 目录下。U-Boot的源码结构跟内核有点像,但更轻量:

u-boot-XXXX.XX/
├── arch/                # 架构相关
│   └── arm/             # ARM相关
│       ├── cpu/         # CPU初始化代码
│       └── dts/         # U-Boot自己的设备树
├── board/               # 板级代码
│   └── ti/              # TI系列板子
│       ├── am335x/      # AM335x EVM
│       └── am437x/      # AM437x EVM
├── common/              # 通用代码
├── drivers/             # 驱动
├── include/             # 头文件
│   └── configs/         # 板级配置头文件
└── cmd/                 # 命令行命令实现

U-Boot的配置方式跟内核不一样。它用的是 include/configs/ 下的头文件来配置。比如 am335x_evm.h 这个文件,里面定义了内存大小、环境变量位置、启动参数等等。

核心要点:U-Boot的启动流程大致是:SPL(第一级引导)→ U-Boot完整版 → 加载内核。SPL负责初始化DDR和时钟,然后跳转到U-Boot。如果SPL出了问题,板子连灯都不亮。我调试时,习惯在SPL里加串口打印,确认硬件初始化是否通过。

3.5 设备树(Device Tree)概念

设备树,英文叫Device Tree,简称DT。这东西刚出来的时候,我其实挺抵触的。因为以前写内核驱动,直接在C代码里写死硬件地址,多直接啊。但后来发现,设备树确实是个好东西。

说白了,设备树就是用来描述硬件信息的。它告诉内核:你的CPU是什么型号、内存多大、I2C控制器在哪个地址、GPIO怎么用……等等。这样内核就不用把硬件信息写死在代码里了,换一块板子,换个设备树就行。

设备树的文件后缀是 .dts(源文件)和 .dtsi(包含文件)。编译后生成 .dtb(二进制文件)。

咱们看一个简单的例子:

/dts-v1/;

#include "am33xx.dtsi"

/ {
    model = "TI AM335x EVM";
    compatible = "ti,am335x-evm", "ti,am33xx";

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

    &i2c0 {
        status = "okay";
        clock-frequency = <400000>;

        eeprom@50 {
            compatible = "atmel,24c256";
            reg = <0x50>;
        };
    };
};

你看,这个文件描述了:

  • 板子型号是AM335x EVM
  • 内存起始地址0x80000000,大小256MB
  • I2C0控制器使能,频率400KHz
  • I2C总线上挂了一个EEPROM,地址0x50

调试技巧:内核启动时,你可以通过 cat /proc/device-tree/ 查看当前使用的设备树。如果某个外设没工作,先检查设备树里有没有对应的节点。我曾经花了一整天查一个SPI驱动的问题,最后发现是设备树里忘了加 status = "okay"

3.6 设备树的核心语法

设备树的语法其实不复杂,记住几个关键点就行:

语法元素 说明 示例
/ 根节点 / { ... };
node-name 节点名称 cpu@0 { ... };
property 属性,键值对 reg = <0x1000 0x100>;
&label 引用标签 &i2c0 { ... };
#include 包含其他dtsi文件 #include "am33xx.dtsi"

嗯,这里要特别说一下 compatible 属性。这个属性是用来匹配驱动的。内核在启动时,会遍历设备树里的每个节点,然后根据 compatible 的值去找对应的驱动。比如 compatible = "ti,am335x-uart",内核就会去驱动列表里找名字匹配的驱动来初始化这个串口。

避坑指南:我曾经在移植一个老驱动时,发现设备树里配了 compatible,但驱动就是加载不了。后来查了半天,发现是驱动里的 of_match_table 写错了,跟设备树里的字符串不完全一致。记住,字符串必须完全匹配,大小写、连字符都不能错

3.7 小结

这一章我们走了一遍SDK的骨架。从整体结构到文件系统,再到内核和U-Boot的源码树,最后重点讲了设备树。你想想看,这些组件是怎么串起来的?

U-Boot启动后,从Flash里读出设备树文件(.dtb)和内核镜像(zImage),然后把设备树的地址传给内核。内核解析设备树,知道有哪些硬件,然后加载对应的驱动。驱动初始化完成后,挂载根文件系统,最后启动用户空间的init进程。

这个流程,说白了就是嵌入式Linux的「三板斧」。后面几章,我们会逐一深入每个环节。下一章,咱们聊聊如何搭建开发环境,以及怎么用SDK编译出第一个能跑的系统。

课后思考:如果你拿到一块新的TI板子,但SDK里没有对应的设备树文件,你会怎么创建?从哪里入手?