芯参谋(90):Linux内核开发指南(BSP Kernel)

第 1 章 文档总览

这一章先把边界划清楚:这份文档给谁用、基于什么平台与版本、用到哪些术语、内核源码从哪来。边界不清是 BSP 项目后期返工的头号原因。

1.1 编写目的、适用范围与读者

本指南用于规范嵌入式 Linux 产品内核层(BSP)开发的全流程动作与交付标准, 目标是让不同背景的工程师在同一套基线上协作,并把「个人经验」沉淀成「团队可复用的流程」。

角色 关注重点 必读章节
BSP 工程师 环境、配置裁剪、设备树、启动、镜像打包、版本发布 第 2、4、5、7、14 章
驱动工程师 总线模型、驱动框架、资源管理、调试定位 第 3、5、6、8 章
测试工程师 测试用例、性能基准、稳定性方法、回归范围 第 13 章、附录 D
硬件工程师 引脚复用、电源域、中断分配、启动介质、功耗 第 5.6、10、11 章
项目管理 / 评审 基线、交付物、检查清单、Known Issues 第 14 章、附录 F

适用范围:基于 Linux 内核的嵌入式产品(ARM32 / ARM64 / RISC-V 均可参照), 存储介质覆盖 SPI NOR、SPI NAND、eMMC、SD / SD NAND 等。 不适用范围:裸机 / RTOS 开发、纯用户态应用开发、芯片 RTL 设计。

1.2 平台与版本基线

版本基线必须写进文档的「第一页」,后续所有命令、补丁、配置都以此为准。 下表为示例基线,项目立项时按实际情况替换并冻结。

项目 基线选择 说明
内核版本 Linux 5.4 / 6.1(示例) 优先选 LTS 主线版本;厂商 SDK 内核需在 1.4 说明与主线差异
CPU 架构 ARM32(arm)/ ARM64(arm64) 决定 ARCH 与工具链前缀
芯片平台 i.MX6ULL / RK3568 / 全志 / STM32MP1 等 以项目选型为准,不写死本文档
Bootloader U-Boot(版本随 SDK) 与内核 DTB、bootargs 强耦合,必须配对升级
构建系统 Buildroot / Yocto / 厂商 SDK 见 2.4 三者差异
启动介质 SPI NOR / SPI NAND / eMMC / SD 决定分区表与文件系统选型,见第 7 章
工具链 gcc-linaro / arm-gnu-toolchain / SDK 自带 版本与内核版本强相关,见 2.2

版本基线冻结纪律

立项评审通过后,内核版本、工具链版本、U-Boot 版本三者必须同时冻结并写入本表;任一变更都要走变更评审,并在 14.5 Known Issues 中记录影响面。中途偷偷升级工具链是「昨天还能编译,今天编不过」最常见的来源。

1.3 术语、缩写与参考文档

缩写 全称 / 含义 备注
BSP Board Support Package,板级支持包 本文档主题
DTS / DTB / DTC 设备树源码 / 二进制 / 编译器 第 3.3、5 章
DTBO Device Tree Overlay 二进制 第 5.4 节
MTD Memory Technology Device,裸闪存子系统 第 6.6 节
UBI / UBIFS 闪存卷管理层 / 闪存文件系统 第 7 章
rootfs 根文件系统 第 7 章
bootargs U-Boot 传给内核的启动参数 第 7.3 节
Oops / panic 内核异常 / 致命错误 第 8.3 节
ko 可动态加载的内核模块 第 3.5、6.5 节
LTS Long Term Support,长期支持版本 第 1.2 节

参考文档(详见附录 E):内核源码树内 Documentation/、 README、各子系统 *-api.rst;芯片厂商 SDK Release Note 与 TRM; 器件 datasheet(以正式版本为准)。

1.4 内核源码来源与分支策略

内核源码只有两种来源,必须先定性再开发——它决定了后续所有补丁与升级方式。

来源 A

主线内核 mainline

从 kernel.org / 镜像站拉取官方 LTS 分支。优点:可长期维护、社区补丁可直接合入、安全修复及时。代价:芯片平台支持可能不完整,需自行补齐驱动与 DTS。

适合:平台主线支持好、产品生命周期长、有 upstream 诉求

来源 B

厂商 SDK 内核

芯片原厂提供的内核(如 NXP / Rockchip / 全志 SDK)。优点:外设驱动齐全、开箱即用。代价:版本往往落后、与主线差异大、升级困难、补丁难以上游化。

适合:量产时间紧、强依赖厂商私有驱动

混合策略

厂商 SDK 起步 + 逐步上游化

初期用 SDK 保证进度,同期把通用部分(DTS、存储驱动、通用外设)整理成可提交主线的补丁,长期降低维护成本。

推荐:有 2 年以上生命周期的产品

分支管理策略(建议)

main            # 基线分支,只接受经过评审的合入,每个发布版本打 tag
release/vX.Y    # 发布分支,从 main 切出,只接受 bugfix,不再合入新特性
dev/<feature>   # 特性分支,开发者个人分支,完成后提 MR/PR 合入 main
hotfix/<issue>  # 紧急修复分支,修完同时回流 main 与对应 release 分支

与上游同步的建议节奏

无论选哪种来源,每季度至少同步一次上游 LTS 的安全补丁;厂商 SDK 内核则跟踪原厂 Release Note 的 CVE 修复。同步前先在 dev/ 分支验证编译与启动,再合入 main。

第 2 章 开发环境搭建

环境是后面所有工作的地基。这一章的目标是:任何一个新人在一台干净的机器上,照着命令跑完就能编出可启动的内核。

2.1 宿主机系统与依赖包

推荐 Ubuntu 20.04 / 22.04 LTS(64 位),磁盘预留 100 GB 以上(内核源码 + 构建产物体积可观)。 以下为最小依赖集,已覆盖内核编译、menuconfig、设备树编译、模块签名等常见需求。

sudo apt update
sudo apt install -y build-essential libncurses-dev flex bison libssl-dev libelf-dev \
    bc cpio kmod rsync wget git python3 python3-pip device-tree-compiler \
    u-boot-tools zlib1g-dev liblz4-tool zstd file cscope exuberant-ctags \
    picocom minicom tftpd-hpa nfs-kernel-server openssh-server

# 调试与性能工具(第 8、9 章会用到)
sudo apt install -y trace-cmd kernelshark linux-tools-common linux-tools-generic \
    gdb-multiarch stress stress-ng iperf3 sysstat

不要用 root 直接编译内核

用普通用户编译,需要 root 权限的动作前加 sudo。以 root 编译会导致源码树文件属主混乱,后期 git clean、补丁生成都会踩坑;内核构建本身也不推荐用 sudo make。

2.2 交叉编译工具链选型

工具链必须与内核版本和目标架构同时匹配,尤其注意浮点 ABI (ARM32 的 gnueabihf 与 gnueabi 不兼容)。

目标架构 典型前缀 说明
ARM32 arm-linux-gnueabihf- 带硬浮点,i.MX6ULL / STM32MP1 等常用
ARM64 aarch64-linux-gnu- RK3568 及多数 64 位平台
RISC-V riscv64-linux-gnu- 注意 ISA 扩展与 -march 设置
SDK 自带 以厂商 Release Note 为准 与 SDK 内核版本绑定,不建议混用外部工具链
# 方式一:发行版仓库安装(够用,版本较固定)
sudo apt install -y gcc-arm-linux-gnueabihf gcc-aarch64-linux-gnu

# 方式二:官方工具链(推荐,版本可控)
wget https://developer.arm.com/-/media/Files/downloads/gnu/.../arm-gnu-toolchain-*.tar.xz
sudo tar -xf arm-gnu-toolchain-*.tar.xz -C /opt
export PATH=/opt/arm-gnu-toolchain-*/bin:$PATH

# 验证(必须看到版本号与正确的 target)
arm-linux-gnueabihf-gcc --version
aarch64-linux-gnu-gcc --version

# 写入环境脚本,避免每次手敲(放在 build/env.sh,团队共用)
cat > build/env.sh <<'EOF'
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
export PATH=/opt/arm-gnu-toolchain-13.2/bin:$PATH
export KBUILD_OUTPUT=${PWD}/build/out      # 可选:把编译产物放到独立目录
EOF
source build/env.sh

工具链版本不匹配的典型症状

现象:编译报 undefined reference to `__aeabi_...'、unrecognized command line option '-mno-unaligned-access';或者编出来了但内核启动即崩溃(浮点 ABI 不一致)。处置:不要临时换工具链硬编,回到基线表确认版本,必要时连内核一起升/降。

2.3 内核源码获取与补丁管理

# 主线内核(浅克隆加速,够编译即可)
git clone --depth 1 --branch v6.1 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
cd linux && git log --oneline -1

# 厂商 SDK:以原厂文档为准,一般提供完整 git 仓库或 tarball + 补丁集
# 补丁管理:所有本地修改一律做成 patch 入仓,禁止直接改源码不留痕
git format-patch -1 <commit> -o patches/       # 生成补丁
git am patches/0001-*.patch                    # 应用补丁
quilt series                                   # 若 SDK 使用 quilt 管理补丁,按其规范操作

2.4 构建系统:Buildroot / Yocto / 厂商 SDK

三者不是互斥关系,而是不同规模产品的不同选择。

维度 Buildroot Yocto / OpenEmbedded 厂商 SDK
定位 轻量根文件系统与工具链生成器 完整发行版构建框架 原厂一键编译脚本
上手成本 低(menuconfig 一套配置) 高(layer / recipe 概念多) 最低(照文档跑脚本)
灵活性 中,适合单一产品形态 高,适合多产品线与长期演进 低,绑定原厂目录结构
可复现性 好(defconfig + 版本锁定) 最好(recipe 精确到 checksum) 差(依赖 SDK 包本身)
构建耗时 分钟级到半小时级 首次数小时(有 sstate 缓存后快) 视 SDK 而定
推荐场景 中小项目 / 单一产品 / 快速验证 多产品线 / 需要 OTA / 需要长期维护 前期验证 / 原厂方案直出

选型建议

做单一产品、团队人手少:选 Buildroot,把 buildroot 的 defconfig、外部树(BR2_EXTERNAL)与内核 defconfig 一起入仓,整机构建可复现。多条产品线、要做 OTA 与长期演进:选 Yocto,把自定义层单独建仓。厂商 SDK 只建议在前期验证阶段用,量产项目应把关键配置抽出来落到 Buildroot/Yocto。

2.5 内核、设备树与模块的独立编译

日常开发中,全量重编太慢,掌握「单独编」能省大量时间。

source build/env.sh

# 1) 配置(首次)
make <soc>_defconfig            # 载入芯片默认配置,例如 imx_v6_v7_defconfig
make menuconfig                 # 交互调整
make savedefconfig              # 生成精简 defconfig(提交这一个,不要提交 .config)
cp defconfig arch/arm/configs/myboard_defconfig

# 2) 编译内核与模块(-j 按 CPU 核数调整)
make -j$(nproc) zImage          # ARM32;ARM64 通常是 Image
make -j$(nproc) modules
make -j$(nproc) dtbs            # 编译全部设备树
make mysoc-myboard.dtb          # 只编指定的一个 dtb(快)

# 3) 单独编译某个外部模块
make -C /path/to/kernel M=$(pwd) modules
make -C /path/to/kernel M=$(pwd) modules_install INSTALL_MOD_PATH=/path/to/rootfs

# 4) 清理(注意别误删 .config)
make clean                      # 清目标文件
make mrproper                   # 连 .config 一起清,慎用

2.6 调试通道:串口 / TFTP / NFS / SSH

开发期强烈建议内核走 TFTP、rootfs 走 NFS,把「改一行代码」到「看到结果」压缩到一分钟内, 同时避免反复擦写 Flash 导致器件损耗。

通道 用途 关键配置
串口控制台 看 U-Boot / 内核启动日志、进 U-Boot 命令行 波特率常见 115200 8N1,按板卡确认
TFTP 下载 zImage / dtb 到内存运行 服务端 /srv/tftp,U-Boot 设 serverip
NFS 挂载根文件系统,改文件即时生效 内核需开 CONFIG_ROOT_NFS,/etc/exports 配权限
SSH 登录后跑性能工具、拷贝日志 rootfs 内置 dropbear / openssh
# U-Boot 侧:网络启动内核 + NFS 根文件系统(开发期最常用)
setenv ipaddr 192.168.1.100; setenv serverip 192.168.1.10
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.10:/srv/nfs/rootfs,v3,tcp rw ip=dhcp'
tftpboot 0x80800000 zImage
tftpboot 0x83000000 myboard.dtb
bootz 0x80800000 - 0x83000000        # ARM64 用 booti

# 宿主机:把编译产物同步到 TFTP 目录
cp arch/arm/boot/zImage /srv/tftp/ && cp arch/arm/boot/dts/myboard.dtb /srv/tftp/

2.7 推荐工具集

类别 工具 用途
编辑 / 浏览 vim / VS Code + clangd 配合 compile_commands.json 获得跳转补全
代码索引 cscope / ctags make cscope 生成索引,查函数调用极快
版本管理 git + gitk / tig 分支、补丁、bisect 定位引入问题的提交
静态检查 make checkstack / sparse / coccinelle 编译期发现隐患
动态调试 trace-cmd / kernelshark / perf 第 8.5 节
内核调试 gdb-multiarch + KGDB 第 8.4 节
串口终端 picocom / minicom picocom -b 115200 /dev/ttyUSB0
压力测试 stress-ng / fio / iperf3 第 13 章

开发环境链路:宿主机编译 → 产物 → 目标板 → 调试通道回传宿主机开发机· Ubuntu 20.04 / 22.04 LTS· 内核源码树 + 补丁· Buildroot / Yocto / SDK· TFTP·NFS·SSH 服务端交叉编译· arm-linux-gnueabihf-· aarch64-linux-gnu-· ARCH=arm / arm64· CROSS_COMPILE= 前缀编译产物· zImage / uImage· *.dtb 设备树· *.ko 内核模块· rootfs 镜像目标板· U-Boot 引导· 内核 + DTB 加载· rootfs 挂载· 业务进程运行调试通道(宿主机 ↔ 目标板)· 串口控制台 UART· TFTP 下载内核 / DTB· NFS 挂载 rootfs· SSH 网络调试建议:开发期一律走 TFTP + NFS,避免反复烧写 Flash;量产前再切回本地 Flash 启动。

图 3 · 宿主机交叉编译到目标板的完整链路与调试通道

第 3 章 内核基础原理

这一章是给新人的理论底座:搞清楚内核怎么启动、设备树起什么作用、子系统怎么分层、模块怎么加载。理解了这些,后面调驱动才知道自己在改哪一层。

3.1 内核空间与用户空间、系统调用

CPU 以特权级区分两种运行态:用户空间跑应用程序,各自拥有独立虚拟地址空间, 一旦越界访问或执行特权指令会被 MMU / CPU 拦截;内核空间所有进程共享同一份地址空间, 驱动代码就运行在这里,一处出错会影响整机。

用户态进入内核态的合法途径只有三类:系统调用(open/read/write/ioctl 等)、 异常(缺页、未定义指令)、中断(外设触发)。 驱动开发者打交道最多的是 ioctl 与 read/write 在内核侧的 file_operations 实现。

内核空间与用户空间的边界:系统调用是唯一合法通道用户空间(User Space,CPU 低特权级,地址空间隔离)应用程序业务进程 / 守护进程C 库 glibc / musl封装系统调用调试工具perf / strace / fio系统调用接口 syscall(svc / swi)· 异常 · 中断 —— 唯一合法进入内核的通道内核空间(Kernel Space,高特权级,共享同一地址空间)系统调用层open / read / write / ioctl 的内核实现核心子系统VFS · 网络协议栈 · 进程调度 · 内存管理设备驱动层字符设备 · 块设备 · MTD · 网络驱动 · 总线驱动体系相关层中断控制器 · 时钟 · GPIO · DMA · 电源域硬件:CPU · 内存 · 存储器件(SPI NOR / SPI NAND / eMMC)· 外设

图 4 · 内核空间与用户空间的分层及系统调用通道

对驱动开发的直接含义

内核态没有内存保护:一个野指针写坏的是整个内核,现象往往是随机的死机而不是立刻崩溃。因此第 6.7 节的资源自动释放(devm_*)与第 8.6 节的内存排查不是可选项,而是必备动作。

3.2 内核启动全流程

启动链路是排障的总地图。拿到一块不启动的板子,第一件事永远是看串口停在哪一步, 而不是猜。下图给出七阶段与每一步的日志特征。

从上电到用户进程:七个阶段与各自的「日志特征」ROM / SPL片内 Boot ROM 加载第一阶段引导程序到 SRAM无串口输出 = 卡在这一步1U-Boot SPL初始化 DDR、时钟、引脚,为加载 U-Boot 主体做准备U-Boot SPL banner2U-Boot 主体加载 zImage / dtb,装配 bootargs 并跳转内核bootargs 在此定型3内核入口自解压(zImage)→ 跳转到内核入口 → 早期汇编初始化Uncompressing Linux...4内核初始化setup_arch 解析 dtb → 各子系统 initcall 分级初始化大量 printk 输出5挂载 rootfs按 bootargs 的 root= 挂载根文件系统VFS: Mounted root6用户态 init启动 init / systemd → 拉起业务进程login / 业务进程日志7排障入口:先看串口停在哪一步,再跳到第 15.1 节的对应分支;第 9 章的启动耗时优化就是逐段量测这七步。注:阶段 1–3 属 Bootloader 范畴,本文只界定接口边界(dtb 传递、bootargs 装配)。

图 5 · 嵌入式 Linux 启动七阶段与日志特征

阶段 关键动作 常见失败点
ROM / SPL 片内 ROM 加载 SPL 到 SRAM 启动引脚电平、Boot 介质焊接、SPL 镜像不对
U-Boot SPL 初始化 DDR / 时钟 / 串口 DDR 参数不匹配 → 卡死无输出
U-Boot 主体 加载 zImage + dtb,装配 bootargs bootargs 写错、dtb 地址冲突、网口不通
内核入口 自解压并跳转到内核入口 镜像损坏、加载地址与链接地址不一致
内核初始化 解析 dtb、子系统 initcall 分级初始化 dtb 不匹配、时钟/电源域未开导致驱动崩溃
挂载 rootfs 按 root= 参数挂载根文件系统 root 参数错、文件系统驱动未编译进内核
用户态 init 启动 init / systemd 并拉起业务 rootfs 缺库、init 路径不存在

3.3 设备树 DTS 原理

设备树解决的是「同一份内核如何描述不同硬件」的问题: 硬件描述(DTS)从内核代码中剥离出来,编译成 DTB 后由 Bootloader 传给内核, 内核展开成 device_node,再按 compatible 与驱动的 of_match_table 做匹配,匹配成功才调用 probe()。

DTS 如何从源文件变成驱动 probe:编译 → 传递 → 展开 → 匹配*.dts / *.dtsi板级与 SoC 公共描述dtc 编译器make dtbs 或手动 dtc*.dtb / *.dtbo扁平二进制设备树U-Boot 加载 dtb传给内核(寄存器/约定)解析为 device_node/proc/device-tree 可查按 compatible 匹配与驱动的 of_match_table 比对调用驱动 probe()拿到资源并注册设备匹配失败的典型后果· 驱动不 probe,设备不出现· dmesg 无相关日志· /sys、/dev 下找不到节点· 排查入口见 5.5 / 15.3compatible 写法(从具体到通用,驱动按最前匹配优先)compatible = "xtx,xt25w1g", "jedec,spi-nor";

图 6 · 设备树从编译到驱动 probe 的完整链路

最容易误解的一点:compatible 不是「驱动名」

compatible 是硬件的标识字符串,格式惯例为 "厂商,型号",可带多个从具体到通用的候选(如 "xtx,xt25w1g", "jedec,spi-nor")。驱动侧在自己的 of_match_table 里声明支持哪些字符串,两边能对上才会 probe。驱动调不通时先核对这一对字符串,比看寄存器快得多。

3.4 内核子系统地图

下表是子系统清单与本文定位——每个子系统单独写专项文档,这里只给出职责边界与常用接口入口。

子系统 职责 驱动常用接口 / 文档入口
Platform 总线 无物理总线的片上外设挂接点 platform_driver_register、of_device_id
SPI / I2C / UART 串行总线与控制器、从设备驱动 spi_driver、i2c_driver、serdev
MTD 裸闪存(NOR / NAND)统一抽象 mtd_info、nand_controller、spi_nand
块设备 eMMC / SD / UBI 之上的块接口 gendisk、blk_mq
字符设备 按字节流访问的设备 cdev、file_operations
时钟 CLK 时钟树与门控 clk_get / clk_prepare_enable
电源管理 PM suspend/resume、Runtime PM、电源域 dev_pm_ops、pm_runtime_*
中断 IRQ 中断号映射、上下半部、线程化中断 request_threaded_irq、devm_request_irq
DMA 外设与内存之间的直接搬运 dmaengine、dma_map_single
GPIO / Pinctrl 引脚复用与电平控制 gpiod_get、pinctrl

3.5 内核模块:内置 vs 动态加载

方式 配置形态 优点 代价 / 注意
内置编译 CONFIG_XXX=y 启动即可用,无加载顺序问题;rootfs 挂载前必需的驱动(存储、文件系统)必须内置 镜像变大,改动需重编内核
动态模块 CONFIG_XXX=m 可现场加载调试,迭代快,镜像小 需要处理依赖与加载顺序,modules_install 到 rootfs
# 模块常用操作
insmod  demo.ko                 # 加载(不处理依赖)
modprobe demo                   # 加载(自动处理依赖,需先 depmod)
lsmod                           # 查看已加载模块
modinfo demo.ko                 # 查看模块信息(依赖、参数、版本)
rmmod demo                      # 卸载
cat /proc/modules               # 内核视角的模块列表

# 模块参数(驱动内 module_param 声明)
insmod demo.ko debug=1

经验法则

启动链路上必需的驱动一律内置:存储控制器、MTD/块设备、根文件系统类型、以及它们依赖的时钟与 GPIO/pinctrl。调试期才用得上的驱动做成模块,量产后按需关闭。把存储驱动做成模块而文件系统内置(或反之)是「VFS: Unable to mount root fs」的高频成因。

第 4 章 内核配置与裁剪

配置裁剪是 BSP 工程师的核心工作:把几千个 CONFIG 收敛成一份可维护、可追溯、体积与启动时间都达标的基线。

4.1 .config 与 Kconfig 原理

内核配置由 Kconfig(各级目录下的配置描述)生成菜单,选择结果落在源码树根目录的 .config(CONFIG_XXX=y/m/n)。常见命令:

make <soc>_defconfig        # 载入芯片/板级默认配置(起点)
make menuconfig             # 图形化调整(需要 ncurses)
make oldconfig              # 沿用旧 .config,只提示新增项(升级内核时用)
make olddefconfig           # 同上,新增项取默认值,不交互(CI 里用这个)
make savedefconfig          # 生成精简 defconfig(只保留非默认项)→ 入仓
make nconfig / xconfig      # 其它交互前端

不要直接把 .config 提交进仓库

.config 包含上千项(绝大多数是默认值),提交它会导致:① 评审时看不出真正改了什么;② 升级内核时冲突一大片;③ 别人 make oldconfig 后产生隐性差异。正确做法:只提交 make savedefconfig 产出的 defconfig(通常几十行),并在 arch/<arch>/configs/ 或项目 build/ 目录下管理。

4.2 配置基线管理规范

  1. 选起点:以最接近目标硬件的 *_defconfig 为起点,不要从零开始。
  2. 调配置:用 menuconfig 调整,每改一批就记录「改了什么、为什么」。
  3. 存基线:make savedefconfig 产出 defconfig,与源码同仓提交。
  4. 编译验证:make -j$(nproc) 全量编译,确认无警告转错误。
  5. 度量收益:记录镜像体积、启动时间、内核内存占用三项指标。
  6. 写 changelog:配置变更与代码变更一样要有评审与记录(见 14.2)。
# 推荐:把 defconfig 与构建脚本一起放进项目的 build/ 目录
build/
├── env.sh                  # ARCH / CROSS_COMPILE / PATH
├── myboard_defconfig       # 配置基线(唯一来源)
├── build_kernel.sh         # 一键编译:载入 defconfig → 编译 → 拷贝产物
└── patches/                # 内核补丁集

# build_kernel.sh 关键片段
#!/bin/sh
set -e
source "$(dirname "$0")/env.sh"
cp "$1" .config || cp build/myboard_defconfig .config
make olddefconfig
make -j"$(nproc)" zImage dtbs modules
make modules_install INSTALL_MOD_PATH=../rootfs

4.3 裁剪原则与收益

裁剪的三条原则:没有对应硬件的驱动全删;量产版本关闭调试组件; 按需保留文件系统与协议栈。收益体现在体积、启动时间、内存占用与攻击面四个方面。

配置基线的闭环:改配置 → 存基线 → 编译 → 度量 → 回归选基线芯片 defconfig调配置menuconfig存基线savedefconfig 入仓编译验证zImage / dtb / ko度量收益体积 / 启动 / 内存任一环节不满足目标 → 回到「调配置」重来,并把结论记入 changelog裁剪优先级(按「收益 ÷ 风险」排序,从上往下做)第一优先 · 收益高· 用不到的设备驱动· 多余的文件系统· 不用的网络协议栈体积下降最明显第二优先 · 收益中· 调试组件与 tracing· 内核符号表 / kgdb· 多余的字符设备量产前必关第三优先 · 收益低· 调度 / 内存参数微调· initcall 层级调整影响启动时间与内存

图 7 · 内核配置基线闭环与裁剪优先级

裁剪对象 典型 CONFIG 收益 风险提示
无用驱动 CONFIG_DRM、CONFIG_SND 等未使用子系统 体积下降最明显 删错会导致外设不可用,逐项验证
文件系统 CONFIG_ISO9660、CONFIG_NFS_FS 等 体积 + 启动时间 根文件系统类型必须保留且内置
网络协议栈 CONFIG_IPV6、无关隧道与过滤模块 体积 + 攻击面 确认业务是否用到
调试组件 CONFIG_DEBUG_KERNEL、CONFIG_KGDB、CONFIG_FTRACE 体积 + 性能 开发期保留,量产前关闭
模块符号表 CONFIG_MODULES 相关 体积 关掉后无法加载任何 ko

裁剪必须配合回归测试

裁剪后必须跑一遍第 13 章的基础功能用例。「编过了」不等于「能用」——很多配置项编译期无依赖,运行期才发现某个驱动被间接依赖(例如 UBI 依赖 MTD、MTD 依赖特定的 NAND ECC 引擎)。裁剪前后各跑一次 lsmod 与 /sys 节点对比最稳妥。

4.4 内核编译产出

产出 路径 用途
内核镜像 arch/<arch>/boot/zImage 或 Image 被 U-Boot 加载运行
uImage 由 mkimage 在 zImage 上加 64 字节头 老版本 U-Boot 需要
设备树 arch/<arch>/boot/dts/*.dtb 描述硬件,与内核配对加载
内核模块 *.ko,安装到 /lib/modules/<ver>/ 可动态加载的驱动
符号表 System.map、vmlinux 定位 Oops 地址(第 8.3 节必备)

一定要保留 vmlinux 与 System.map

发布版本把 vmlinux、System.map、.config 与源码 commit id 一起归档。没有它们,后面出现 Oops 时无法用 addr2line 还原调用栈,只能靠猜。归档成本几乎为零,缺了它排查成本极高。

4.5 镜像烧录与启动介质

烧录方式取决于介质与阶段:开发期用 TFTP/NFS 避免擦写; 小批量用 U-Boot 命令或 USB 烧写工具;量产用编程器或产线烧录工装。

编译产物落到哪里:分区表是产物与介质之间的契约编译产物zImage / uImage压缩内核镜像*.dtb设备树二进制rootfs 镜像rootfs.squashfs / ubi / ext4*.ko 模块已编入 rootfs 的 /lib/modules典型分区表(示例偏移,按器件容量调整)U-Boot0 ~ 1 MBenv(建议冗余双份)2 × 擦除块DTB几十 KBKernel3 ~ 8 MBrootfs视文件系统而定data / OTA剩余空间分区必须与 U-Boot、内核 MTD 分区、OTA 三方一致启动介质差异SPI NOR容量小 · XIP 可行常用 squashfs + jffs2只读 rootfs + 小数据区SPI NAND需坏块与 ECC 管理常用 UBI/UBIFS量产成本低,注意坏块表eMMC / SD有 FTL,块设备接口常用 ext4 / f2fs容量大,适合 A/B 分区 OTA

图 8 · 内核 / DTB / rootfs 产物与分区表、启动介质的对应关系

# U-Boot 下烧录到 SPI NOR(示例,偏移按分区表调整)
sf probe 0
sf erase 0x100000 0x400000
tftpboot 0x80800000 zImage
sf write 0x80800000 0x100000 ${filesize}

# U-Boot 下烧录到 SPI NAND(需先确认坏块与 ECC 布局)
mtd list
nand erase.spread 0x200000 0x600000
nand write 0x80800000 0x200000 ${filesize}

# eMMC:通常在能启动的系统中用 dd 或专用升级工具写入分区
dd if=rootfs.ext4 of=/dev/mmcblk0p3 bs=4M conv=fsync

烧录前必须核对三件事

① 分区偏移与大小与 U-Boot、内核 MTD 分区表一致;② SPI NAND 必须使用 skip-bad-block 的写入方式(nand write.trimffs / erase.spread),否则坏块会推移后续数据;③ 烧录后做一次回读校验(CRC 或 sha256),不要烧完就断电。写错偏移覆盖掉 U-Boot 会导致板子变砖,只能重新用编程器救。

第 5 章 设备树 DTS 开发

设备树是硬件与驱动之间的契约。这一章给出写法规范、属性速查、调试手段,以及最容易踩的六个坑。

5.1 DTS 编写与命名规范

规范项 要求 示例 / 说明
节点命名 <类型>@<地址>,地址写实际基址或片选 flash@0、i2c@40001000
label 可选,供其它节点引用,用冒号后缀 spi0: spi@40000000
compatible "厂商,型号",从具体到通用排列 "xtx,xt25w1g", "jedec,spi-nor"
层级 SoC 公共部分放 .dtsi,板级差异放 .dts 避免板级文件复制整份 SoC 描述
status 默认关闭的外设在板级显式 okay status = "okay";
引用 复用用 &label 覆盖,不要重复定义 &spi0 { status = "okay"; };

5.2 常用属性速查

DTS 是树:根节点下挂总线,总线节点下挂从设备节点树(缩进表示父子关系)/ 根节点chosenbootargs / stdout-pathsoccompatible = "simple-bus"spi0: spi@40000000#address-cells / #size-cellsflash@0compatible / reg / spi-max-frequencyi2c0: i2c@40001000总线控制器节点sensor@48compatible / reg(I2C 地址)常用属性速查compatible驱动匹配字符串,从具体到通用reg地址 / 片选 / I2C 地址#address-cells子节点 reg 的地址字段数量interrupts中断号 + 触发类型clocks时钟源引用(phandle)gpiosGPIO 引用与有效电平statusokay / disabled 控制启用power-domains所属电源域,未开会挂死注:节点名格式为 <类型>@<地址>(如 flash@0),label(如 spi0:)供其它节点引用。

图 9 · DTS 节点树结构与常用属性速查

属性 作用 典型写法 易错点
compatible 驱动匹配 compatible = "vendor,chip"; 与驱动 of_match_table 对不上则不 probe
reg 地址 / 片选 / I2C 地址 reg = <0>; 或 reg = <0x40000000 0x1000>; 位宽由父节点 #address-cells 决定
#address-cells / #size-cells 定义子节点 reg 的字段数 #address-cells = <1>; 总线节点必填,漏了子节点解析全错
interrupts 中断号与触发类型 interrupts = <0 34 IRQ_TYPE_LEVEL_HIGH>; 触发类型与实际电路反了会丢中断或中断风暴
clocks 时钟源引用 clocks = <&ccm CLK_SPI0>; 时钟未开 → 访问寄存器挂死
gpios GPIO 引用与有效电平 reset-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; 有效电平写反 → 器件一直处于复位
power-domains 所属电源域 power-domains = <&pd_spi>; 电源域未开 → 读写超时或死机
spi-max-frequency SPI 最大速率 spi-max-frequency = <50000000>; 超器件规格会偶发数据错误

5.3 总线节点与子节点写法

SPI / I2C 设备挂在控制器节点下,控制器节点必须声明 #address-cells 与 #size-cells,子节点用 reg 表示片选(SPI)或设备地址(I2C)。

/* SPI 挂一个存储器件 */
&spi0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi0_pins>;
    #address-cells = <1>;
    #size-cells = <0>;

    flash@0 {
        compatible = "xtx,xt25w1g", "jedec,spi-nor";
        reg = <0>;                        /* CS0 */
        spi-max-frequency = <50000000>;
        spi-rx-bus-width = <4>;           /* 使能 Quad 读(器件与控制器均需支持) */
        #address-cells = <1>;
        #size-cells = <1>;
        partitions {
            compatible = "fixed-partitions";
            #address-cells = <1>;
            #size-cells = <1>;
            partition@0 { label = "uboot";  reg = <0x0 0x100000>; };
            partition@100000 { label = "kernel"; reg = <0x100000 0x500000>; };
        };
    };
};

/* I2C 挂一个传感器 */
&i2c0 {
    status = "okay";
    clock-frequency = <100000>;
    sensor@48 {
        compatible = "vendor,model";
        reg = <0x48>;                     /* 7-bit 地址 */
        interrupt-parent = <&gpio1>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
    };
};

storage 器件上 fixed-partitions 与 UBI 的选择

SPI NOR 上常用 fixed-partitions 直接静态分区;SPI NAND 上更推荐用 partitions { compatible = "fixed-partitions"; } 划出 UBI 容器分区,再在系统内用 ubiattach / UBI 卷管理,避免手工管理坏块。

5.4 DTS overlay 动态设备树

Overlay 用于在运行时叠加一段设备树片段,适合同一主板配多种扩展板的场景 (如树莓派 HAT、可插拔模组)。

# 编译 overlay
dtc -@ -I dts -O dtb -o ext-board.dtbo ext-board.dts     # -@ 生成 __symbols__,必需

# U-Boot 加载并应用
setenv bootargs ${bootargs} dtoverlay=/ext-board.dtbo
# 或 U-Boot 命令行:fdt apply 后再 boot

# 运行时查看
ls /sys/kernel/config/device-tree/overlays/       # 需开启 CONFIG_OF_OVERLAY

Overlay 的两条限制

① 基树必须带 __symbols__ 节点(编译时加 -@),否则无法解析 &label 引用;② Overlay 不能修改已经 probe 成功的设备——它只能添加新节点或覆盖尚未使用的节点,对已在运行的关键设备(如存储控制器)无效。量产产品慎用,优先用静态 DTS + 多份板级文件。

5.5 DTS 调试方法

DTS 调试四步闭环:编译期排语法,运行期验结果1 · 编译期dtc 检查语法与引用是否成立2 · 运行期看树/proc/device-tree确认节点是否生效3 · 看匹配dmesg 过滤关键字确认 probe 是否触发4 · 回读 dtbfdtget / fdtdump核对真实生效值闭环说明:任一步失败都要回到上一步——看不到节点先查编译,节点在但没 probe 就查 compatible。编译期dtc -I dts -O dtb -o out.dtb board.dtsmake board.dtb看设备树ls /proc/device-tree/cat /proc/device-tree/soc/spi@.../compatible看匹配与 probedmesg | grep -i -E 'spi|nand|probe'ls /sys/bus/spi/drivers/

图 10 · 设备树调试的四步闭环与对应命令

# 1) 编译期:单独编译一个 dts,语法与引用错误会在这里暴露
dtc -I dts -O dtb -o /tmp/board.dtb arch/arm/boot/dts/board.dts
make board.dtb

# 2) 反编译已生效的 dtb,确认最终值(含包含文件的展开结果)
dtc -I dtb -O dts -o /tmp/board_decompiled.dts /boot/board.dtb

# 3) 板端核对
ls /proc/device-tree/
find /proc/device-tree -name compatible -exec sh -c 'echo "$1:"; cat "$1"; echo' _ {} \;
cat /sys/firmware/devicetree/base/soc/spi@*/compatible

# 4) 看驱动是否匹配成功
dmesg | grep -iE "of_|probe|spi|nand"
ls /sys/bus/spi/drivers/ /sys/bus/platform/drivers/

5.6 高频坑位

坑 现象 排查与处置
地址 / 单元数不匹配 子节点 reg 解析错误,驱动拿到错误地址 核对父节点 #address-cells、#size-cells
中断冲突或类型反了 中断不触发 / 触发一次后再无响应 / 中断风暴 cat /proc/interrupts;核对触发类型与实际电平
GPIO 复用未配置 片选无效、复位脚拉不住 检查 pinctrl-0 是否占用了该引脚
电源域 / 时钟未开 访问寄存器死机或读回全 0 / 全 F 先开 clocks 与 power-domains 再访问
compatible 写错 设备完全不出现,dmesg 无任何相关日志 与驱动 of_match_table 逐字比对
status 未改 okay SoC dtsi 里默认 disabled,板级忘了开 板级文件里显式 status = "okay";

时钟与电源域是最隐蔽的一类坑

典型现象:驱动代码完全正确,一读寄存器就死机(不是返回错误,是整机卡死),或者读回来全是 0x00 / 0xFF。这几乎总是时钟未使能或电源域未打开导致的总线挂起。排查顺序:先确认 DTS 的 clocks / power-domains,再确认 probe 里调用了 clk_prepare_enable() 与 pm_runtime_get_sync(),最后才怀疑硬件。

后续章节请下载「芯参谋」查看

……(篇幅原因, 还有25章· 详见芯参谋→方案设计)

打开路径:🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> Linux内核开发指南(BSP Kernel)

📥 下载芯参谋,免费查看完整报告

网址:www.myxincm.com

相关报告 · 1

相关报告 · 2

posted @ 2026-11-03 22:47  在路上163  阅读(3)  评论(0)    收藏  举报