芯参谋(89):Linux内核开发指南(BSP Kernel)
读者路径
BSP 工程师:第 2、4、5、7、14 章是主战场;附录 A/B/C 是日常手册。
驱动工程师:第 3、5、6 章 + 对应专项子文档;第 8 章决定你定位问题的速度。
测试工程师:第 13 章全套用例 + 第 15 章排查入口;附录 D 的脚本可直接复用。
硬件工程师:第 5.6 节(GPIO 复用 / 电源域 / 中断冲突)与第 10、11、17 章必须看, 多数「驱动调不通」最后都定位到硬件设计。
新板首次点亮:直接翻第 17 章,按六步顺序走,不要凭感觉跳级。
要交付量产 / 要过安全合规:第 19、20、21、23 章是一组,缺一项都会在后期付出代价。
这份文档的三条硬约束
| 约束 | 具体要求 | 落地位置 |
|---|---|---|
| 配置基线 | 永不直接提交 .config,一律 make savedefconfig 生成 defconfig 入仓 |
第 4.2 节 |
| 源码基线 | 内核源码来源、分支、补丁集三要素可追溯,每个发布版本有 tag | 第 1.4、14.1 节 |
| 交付基线 | 发布前过一遍检查清单,产出 changelog + Known Issues + 交付物清单 | 第 14.4、14.5 节、附录 F |
与其它文档的分工
| 文档层级 | 覆盖范围 | 典型例子 |
|---|---|---|
| 本指南(总文档) | 内核全流程:环境、配置、设备树、驱动、文件系统、调试、调优、测试、发布 | 本文档 |
| 驱动专项子文档 | 单一器件的驱动实现细节:寄存器、命令序列、MTD 对接、时序 | SPI NAND 驱动开发指南 |
| 硬件设计指南 | 原理图、电源、封装、信号完整性 | SD NAND 电路设计指南 |
| 市场 / 选型报告 | 容量档位、应用场景、厂商格局 | 存储芯片应用市场分析 |
BSP 内核开发主流程:从环境搭建到版本发布阶段一环境搭建宿主机 / 工具链源码 / 构建系统第 2 章阶段二配置与裁剪defconfig 基线menuconfig 裁剪第 4 章阶段三内核与驱动开发设备树 DTS驱动 probe 适配第 5–7 章阶段四调试与调优Oops 定位 / ftrace启动与存储调优第 8–11 章阶段五测试与发布压力 / 稳定 / 回归补丁与版本基线第 12–15 章贯穿全程的三条基线源码基线Git 分支策略 + tag上游 / 厂商 SDK 同步第 1.4、14 章配置基线defconfig 入仓不直接提交 .config第 4.2 节交付基线检查清单 + 交付物版本号 changelog附录 F、第 14.4 节说明:五阶段是主线,三条基线是约束——任何阶段改动都要回到基线上留痕,否则版本无法追溯。图示:阶段顺序不强制串行,实项目中阶段三与阶段四通常多轮迭代。
图 1 · BSP 内核开发主流程与贯穿全程的三条基线
文档体系分层:本指南是总文档,驱动专项是子文档Linux 内核开发指南BSP 总文档 · 覆盖内核全流程环境构建与配置裁剪第 2、4 章设备树与启动流程第 3、5 章驱动开发第 6 章调试 · 调优 · 发布第 8–15 章第 6 章下挂的驱动专项子文档(各自独立成文,接口规范由本指南统一约束)SPI NAND 驱动开发SPI NOR 驱动开发eMMC / 块设备I2C / ADC / 其它外设
图 2 · BSP 总文档与驱动专项子文档的层级关系
第 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 配置基线管理规范
- 选起点:以最接近目标硬件的
*_defconfig为起点,不要从零开始。 - 调配置:用
menuconfig调整,每改一批就记录「改了什么、为什么」。 - 存基线:
make savedefconfig产出 defconfig,与源码同仓提交。 - 编译验证:
make -j$(nproc)全量编译,确认无警告转错误。 - 度量收益:记录镜像体积、启动时间、内核内存占用三项指标。
- 写 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)
相关报告 · 1
- 凝血分析仪(Coagulation Analyzer)_市场分析市场应用分析
- 榨汁机(Juicer)_市场分析市场应用分析
- 联咏(Novatek)平台_SPI_NOR_eMMC_样机排查调试&解决方案
- NAND多平面Cache命令与Die交错_技术探讨原理方案设计
- 恒玄(BES)平台_SPI_NOR_样机排查调试&解决方案
相关报告 · 2
- AI算力硬件模块(AIHW)_市场分析市场应用分析
- 智能闹钟(Smart Alarm Clock)_市场分析市场应用分析
- 海栿创(Hynitron)_开发指南原理方案设计
- 炬芯(Actions)平台_SPI_NOR_量产排查调试&解决方案
- LEDDriver_调光与恒流异常_样机排查调试&解决方案

浙公网安备 33010602011771号