树莓派Linux内核4.19.y定制实战:从交叉编译到设备树与驱动 简介面向树莓派开发者的Linux内核4.19.y分支完整源码包适合需要在内核层进行驱动开发、性能调优或功能定制的嵌入式爱好者、树莓派玩家及系统工程师。压缩包约193.93MB内含数万份内核源文件其中C源码超过2.6万份、头文件超过1.9万份另有makefile、设备树dts/dtsi、Kconfig、defconfig等构建与硬件描述文件便于按框架快速定位特定驱动、总线和外设配置。目前已有598人浏览学习对入门者和进阶者都有参考价值。内容涵盖从获取源码、配置menuconfig、编译内核与模块到更新config.txt/cmdline.txt并重启验证的完整思路同时涉及GPIO、I2C、GPU、SPI等树莓派常用外设的驱动编写与内核裁剪要点。学习这份源码既能理解Linux内核的编译流程也能掌握针对树莓派的定制化改造方法为进一步编写硬件驱动、优化系统性能以及开展嵌入式项目打下扎实基础。1. 树莓派Linux源码4.19.y从读代码到改内核这一步才是关键树莓派的Linux内核镜像并不是什么黑匣子但很多人拿到4.19.y这份源码包后第一个动作往往是在drivers目录里搜驱动名搜到几个c文件就以为自己看懂了。实际上下载这份源码的真正价值不在“读”而在“改”——裁剪掉用不到的功能、给某个外设重新写驱动、调整设备树让系统认出一块新板子。树莓派4B、3B、Zero W 这些板子的差异就藏在这一棵内核树和几十个.dts设备树文件里。适合两类人一类是做嵌入式产品原型、需要一个干净且私有化内核的开发者另一类是遇到官方内核无法驱动某个外设、只能自己上手改源码的折腾派。2. 源码结构拆解BCM283x驱动、设备树与4.19.y的目录主线拿到源码包后先别急着敲make花十分钟把目录主线过一遍后面编译和改驱动时能少走很多弯路。4.19.y 这版源码是标准的 Linux 内核树加上树莓派官方补丁你既能看到上游内核的影子也能发现大量只属于树莓派的私有代码。2.1 顶层目录先分清哪些是树莓派私有、哪些是通用内核源码根目录下的结构对做过内核编译的人并不陌生但有几处需要单独圈出来。arch/arm是树莓派4B和3B这类32位ARM板卡的内核架构目录包含启动相关代码、系统调用入口和内存管理底层drivers是驱动的大杂烩树莓派官方新增的驱动绝大多数都挂在drivers下对应的子系统里Documentation里是内核文档开发时遇到 Kconfig 选项拿不准、设备树 binding 写不对先翻这里比上论坛发帖高效得多。linux-rpi-4.19.y/ ├── arch/arm/ # ARM架构代码树莓派默认32位内核 ├── arch/arm/boot/dts/ # 设备树源文件树莓派各型号的硬件描述 ├── drivers/ # 驱动代码bcm2835相关驱动散落在各子系统 ├── include/ # 内核头文件写驱动时经常要引用的路径 ├── kernel/ # 内核核心代码调度、进程管理等 ├── scripts/ # 编译脚本mkknlimg 就在这里面 ├── Documentation/ # 内核文档设备树binding、API说明 ├── Kconfig # 顶层配置菜单入口 └── Makefile # 顶层构建脚本注意几个容易被当成普通目录的地方。arch/arm/boot/dts下不仅有bcm2711-rpi-4-b.dts这类板级文件还有一个overlays子目录里面是几十个.dtbo设备树插件树莓派的 GPIO、I2C、SPI 扩展全靠它们改设备树时这里几乎是必动的。scripts/mkknlimg是树莓派特有的内核镜像转换脚本上游内核没有编译完 zImage 后必须用这个脚本转出能被 bootloader 识别的kernel7l.img。2.2 BCM283x 系芯片的内核侧代码从 machine 目录到驱动子系统树莓派用的是博通的 BCM2835、BCM2836、BCM2837、BCM2711 这一系 SoC在 4.19.y 里这些芯片对应的平台代码并不集中在一个叫“bcm283x”的大目录里而是按职责拆开。arch/arm/mach-bcm283x/里是芯片级初始化代码4.19 时代已经大量让位于设备树这个目录里剩的东西不多真正干活的代码在drivers下面。// drivers/pinctrl/bcm/pinctrl-bcm2835.c 是GPIO引脚控制的驱动入口 static const struct of_device_id bcm2835_pinctrl_match[] { { .compatible brcm,bcm2835-gpio }, { .compatible brcm,bcm2711-gpio }, {} };这段代码展示了树莓派驱动的典型写法通过设备树里的compatible字符串把硬件描述和驱动绑定起来。brcm,bcm2835-gpio对应老款芯片brcm,bcm2711-gpio对应树莓派4B同一个驱动文件兼容多个芯片型号靠的就是设备树声明。编译内核前打开arch/arm/boot/dts/bcm283x.dtsi看看这些节点是怎么写的能帮你理解“为什么改一个引脚要动设备树而不是动 C 代码”。2.3 设备树、defconfig 与配置文件三者的关系一张表说清树莓派的内核定制绕不开三样东西设备树描述硬件、defconfig 决定内核功能、config.txt 告诉 bootloader 启动参数。它们之间的边界经常被新手搞混简单记你能插什么外设主要由设备树决定内核支不支持这个外设的驱动主要由 defconfig 决定启动时加载哪份内核镜像和设备树由 boot 分区里的 config.txt 决定。表4.19.y 源码中与树莓派型号对应的关键配置文件文件/配置对应板卡主要作用bcm2711-rpi-4-b.dts树莓派4B定义4B的引脚、SD卡、HDMI、PCIe等硬件节点bcm2837-rpi-3-b-plus.dts树莓派3B3B的板级设备描述bcm2711_defconfig树莓派4B默认配置BCM2711 平台的内核功能开关bcmrpi3_defconfig树莓派3B/3B3代芯片的默认配置文件常见误用是改完设备树忘了重新编译.dtb或者改完.dts直接把它拷到 boot 分区用这都不会生效。4.19.y 的编译流程会把.dts编成.dtb而 bootloader 实际读取的是.dtb不是源文件。这个环节后面编译时会再验证一遍。3. 交叉编译实战一条命令链生成可启动的kernel7l.img源码树在手里下一步要做的是把.c文件变成树莓派 SD 卡上能跑的kernel7l.img。在不熟悉交叉编译的人眼里这一步玄学很多其实拆开看就三件事选对工具链、设对环境变量、按序执行几个 make 目标。3.1 工具链选择官方 tools 仓库和发行版自带交叉编译器怎么取舍交叉编译树莓派内核有两个常用选择一个是树莓派官方tools仓库里现成的arm-bcm2708-linux-gnueabihf-工具链另一个是 Ubuntu、Debian 上通过 apt 安装的arm-linux-gnueabihf-gcc。两条路都能编出可运行的内核但血泪经验是官方工具链和 4.19.y 的源码配合最稳发行版自带编译器版本太新编老内核时偶尔会报一些莫名的语法警告虽然多数不影响产物但排查起来费时间。# 下载官方工具链并解压到工作目录 wget https://github.com/raspberrypi/tools/archive/master.zip unzip master.zip export PATH$PATH:$PWD/tools-master/arm-bcm2708-linux-gnueabihf/bin环境变量PATH加了之后先用arm-bcm2708-linux-gnueabihf-gcc -v验证一下能不能跑。注意这里的前缀arm-bcm2708-linux-gnueabihf-虽然带着bcm2708字样但工具链本身是支持后续芯片的这个命名是历史遗留不影响你编译树莓派4B的内核。3.2 一次完整的编译命令链ARCH、CROSS_COMPILE、make 目标逐个说清在源码根目录执行下面这组命令前先想清楚两个变量ARCH告诉内核构建系统目标架构是 ARMCROSS_COMPILE指定交叉编译器的前缀。漏掉任何一个make大概率会在本地 x86 环境下试图用gcc编译 ARM 代码直接报unknown architecture之类的错误。export ARCHarm export CROSS_COMPILEarm-bcm2708-linux-gnueabihf- make bcm2711_defconfig # 生成 .config4B 用 bcm2711_defconfig make -j4 zImage modules dtbsbcm2711_defconfig是一个默认配置目标执行后会生成.config文件这个文件是后续所有内核编译的起点zImage是压缩后的内核镜像最终要转成kernel7l.img才能被树莓派引导modules是把配置里标记为m的功能编成.ko文件驱动里很多非必须功能都走这一步dtbs是把设备树源码.dts编译成.dtb。-j4是并行编译的作业数一般取 CPU 核心数加一。注意如果你的板子是 3B、3B把bcm2711_defconfig换成bcmrpi3_defconfig再执行。4B 和 3 代的内核配置差异不小混用虽然偶尔也能启动但容易出现某个外设驱动缺失的怪问题。3.3 编译产物核对kernel7l.img、modules、dtbs 各要拷到哪编译完成后arch/arm/boot/zImage是裸内核镜像不能直接烧录需要用源码根目录scripts/下的mkknlimg工具包装一下。./scripts/mkknlimg arch/arm/boot/zImage kernel7l.img make modules_install INSTALL_MOD_PATH/media/pi/rootfs cp arch/arm/boot/dts/*.dtb /media/pi/boot/ cp arch/arm/boot/dts/overlays/*.dtbo /media/pi/boot/overlays/mkknlimg会在 zImage 头部加上树莓派 bootloader 需要的标记信息转换后得到kernel7l.img才该拷进 boot 分区。modules_install的关键参数INSTALL_MOD_PATH指向你开发板 SD 卡的根文件系统目录驱动模块会被安装到该目录的lib/modules/4.19.y下面。设备树文件分两处拷板级.dtb直接放 boot 分区overlays子目录里的.dtbo插件文件要放进 boot 分区的同名子目录。如果你的 SD 卡是直接插在读卡器上boot 分区就是那个能看到config.txt的 FAT 分区rootfs 是那个能看到etc/的 ext4 分区别拷反了。4. 内核裁剪与模块化defconfig、menuconfig、ko文件怎么配合编译一次内核的时间不短每次改配置全量重编更让人头疼。4.19.y 给了一个折中方案核心功能编进内核镜像非关键功能编成.ko模块用到时再用modprobe加载。这一章把配置裁剪和模块加载的完整链路串一遍。4.1 defconfig 是基线menuconfig 做微调两者配合省时间bcm2711_defconfig生成的是树莓派官方认为“通常够用”的配置它开启了绝大多数硬件驱动但很多你用不到的功能也在里面比如蓝牙、Wi-Fi、各种显示驱动这些会拖慢编译并让内核体积变大。你需要的是在官方配置基础上做减法。make menuconfigmenuconfig 是终端里的图形化配置界面按/键可以搜索某个配置项。比如想去掉蓝牙直接搜BT找到Bluetooth subsystem support按空格把*改成保存退出后.config就被改掉了。这比手动编辑.config靠谱得多因为 menuconfig 会自动处理依赖关系避免出现“关了 A 但 A 的依赖项还开着”这种不一致。4.2 一个真实的裁剪场景关掉蓝牙和相关模块后重新编译以树莓派4B为例如果做的是工业数据采集设备蓝牙基本用不上。裁剪步骤如下先make menuconfig在搜索框输入BT逐一把Bluetooth subsystem support、RFCOMM、BNEP等从[*]改为[ ]再搜BT_HCIUART这是板载蓝牙串口驱动一并关掉保存退出后重新编译。grep CONFIG_BT .config make -j4 zImage modules dtbsgrep CONFIG_BT .config用来确认蓝牙相关的配置确实被关掉了。重新编译后对比 zImage 体积通常会小上几 MB对启动速度影响不大但对内存占用和内核安全面有实际意义。注意裁剪时保留CONFIG_BT_HCIBTUSB这类通用 USB 蓝牙驱动的m属性因为 USB 蓝牙适配器可能还在用保险起见按模块编译而不是彻底关掉。4.3 从配置项到.ko文件一个模块的诞生和加载路径内核配置里m表示编译成模块y表示编进内核镜像。选择m后驱动代码不会进入 zImage而是被编译成drivers/net/wireless/xxx.ko这样的独立文件。很多新手分不清这两者的适用场景放在启动时必须用到的驱动比如 SD 卡控制、串口调试口建议y偶尔插拔的外设驱动比如 USB 转串口、Wi-Fi 网卡用m更灵活。make modules_install INSTALL_MOD_PATH/path/to/rootfs # 在开发板上执行 modprobe xxx # 查看模块是否加载成功 lsmod | grep xxx dmesg | tail -20modprobe和insmod的区别在于modprobe会自动解析模块依赖加载前先把依赖的其他.ko一起载入insmod则只加载指定文件依赖需要你手动处理。日常开发建议用modprobe。模块加载失败时dmesg里通常会有具体原因比如依赖的符号找不到、设备树节点没匹配上按日志排查即可。5. 编译烧录避坑指南启动失败时按这三条线索排查树莓派内核编译翻车的场景多数集中在编译环境、镜像转换、分区内容这三处。这一章把最常见的坑列出来每条都按“现象、原因、解决”三部分记录都是我实际踩过的路。5.1 现象编译中期报错openssl/evp.h: No such file or directory原因4.19.y 内核在生成某些模块时需要 OpenSSL 头文件宿主系统没有安装开发库。不装库而反复make clean重建只是浪费时间。 解决Debian/Ubuntu 系统执行sudo apt install libssl-devFedora 系装openssl-devel。装完从上次失败的目标继续编不用全量重来。5.2 现象编译通过、烧录后启动到一半panic或者手动加载.ko时报version magic不匹配原因内核镜像和模块不是同一套配置、同一次编译出来的。常见场景是更新了内核镜像但lib/modules/里还是老模块或者用了两个不同的工具链分别编译内核和模块模块的 vermagic 信息和内核对不上。 解决内核和模块必须用同一棵源码树、同一次make modules_install装到 rootfs。重新安装模块后再执行depmod -a重建依赖表避免加载时找不到符号。5.3 现象烧录后卡在开机彩虹屏进不了系统原因boot 分区里的内核镜像名和config.txt指定的不一致或者kernel7l.img没有经过mkknlimg转换直接复制了 zImage。树莓派 bootloader 按文件名找内核找不到或格式不对就在彩虹屏原地转圈。 解决检查config.txt里kernel行指向的是kernel7l.img还是kernel.img确认实际文件存在重新用./scripts/mkknlimg转换后再覆盖 boot 分区sync 后断电再试别用热重启。5.4 现象新内核进去了但 Wi-Fi 或蓝牙消失lsmod看不到固件加载日志原因裁剪 defconfig 时把无线网卡驱动相关的CONFIG_BRCMFMAC、CONFIG_BT_HCIUART关成了n或者 rootfs 的/lib/firmware/brcm固件目录缺失。启动时驱动在modprobe阶段找不到固件文件外设就静默失败。 解决裁剪前先grep -E BRCM|BT_HCI .config确认哪些是硬件必须项补回驱动并重新编译后从官方根文件系统镜像里把/lib/firmware/brcm整个拷过来。这类问题不会报 panic容易让人误以为是硬件坏了。5.5 现象内核替换后卡在Waiting for root device ...或直接进入 initramfs shell原因cmdline.txt里的root参数指向的根文件系统分区和实际 SD 卡分区对不上。比如原先系统是/dev/mmcblk0p2换了一片分区布局不同的卡内核根据参数找不到 rootfs。 解决在启动时按CtrlC或查看最终进入的 shell 提示符用blkid查 rootfs 分区的 UUID然后把cmdline.txt里的root改成 UUID 形式。这是树莓派官方系统支持的方式比依赖/dev/mmcblk0pN这种不稳定命名稳得多。6. 二次开发实践把第一个字符驱动编进树莓派内核最后落到最实际的改造动作写一个最小字符设备驱动然后编译进内核而不是作为一个外面加载的模块。这样可以直观感受“改一个文件、编一次内核、在开发板上验证”的完整闭环。6.1 驱动代码、Kconfig、Makefile 三个文件的联动在内核源码drivers/char/下新建hello_drv.c实现最基本的读写操作和设备节点创建。注意这里用的是 4.19.y 的老接口风格register_chrdev和class_create在该版本中仍然有效不依赖新内核的复杂框架。#include linux/init.h #include linux/module.h #include linux/fs.h #include linux/device.h #include linux/uaccess.h #define DEVICE_NAME hello_drv static int major; static struct class *hello_class; static ssize_t hello_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { char data[] hello from kernel\n; /* 把内核缓冲区的数据复制到用户空间 */ if (copy_to_user(buf, data, sizeof(data))) return -EFAULT; return sizeof(data); } static ssize_t hello_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) { char data[64]; /* 把用户空间的数据复制到内核缓冲区 */ if (copy_from_user(data, buf, count)) return -EFAULT; pr_info(hello_drv: received %zu bytes\n, count); return count; } static const struct file_operations hello_fops { .owner THIS_MODULE, .read hello_read, .write hello_write, }; static int __init hello_init(void) { /* 注册字符设备0 表示让内核自动分配主设备号 */ major register_chrdev(0, DEVICE_NAME, hello_fops); if (major 0) return major; hello_class class_create(THIS_MODULE, DEVICE_NAME); device_create(hello_class, NULL, MKDEV(major, 0), NULL, DEVICE_NAME); pr_info(hello_drv: registered, major%d\n, major); return 0; } static void __exit hello_exit(void) { device_destroy(hello_class, MKDEV(major, 0)); class_destroy(hello_class); unregister_chrdev(major, DEVICE_NAME); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL);代码里major是主设备号0 表示由内核动态分配避免了手动指定时和其他驱动冲突copy_to_user和copy_from_user是内核态与用户态交换数据的标准接口不能用memcpy直接拷指针跨权限域时会出问题pr_info会输出到内核日志验证时用dmesg查看。要让内核构建系统认识这个文件需要改两处drivers/char/Kconfig里加一段配置项drivers/char/Makefile里加一行编译规则。这样make menuconfig就能搜到它也可以直接在.config里手动加CONFIG_HELLO_DRVy。# drivers/char/Kconfig 追加 config HELLO_DRV tristate Hello world driver default y help A simple character device for kernel development practice. # drivers/char/Makefile 追加 obj-$(CONFIG_HELLO_DRV) hello_drv.o6.2 验证路径重新编译、替换镜像、确认设备节点重新执行交叉编译流程make bcm2711_defconfig这步会把你手动加的配置覆盖掉所以直接make -j4 zImage modules dtbs然后mkknlimg转换、拷贝 boot 分区、重启。重启后执行下面几个操作验证结果ls /dev/hello_drv # 确认设备节点存在 echo test /dev/hello_drv # 触发 write 回调 dmesg | grep hello_drv # 看内核日志中的登记信息 cat /dev/hello_drv # 读取驱动返回的字符串cat返回hello from kernel就说明整个链路通了。如果设备节点不存在先看dmesg里注册是否报错再确认.config里CONFIG_HELLO_DRV确实变y或m了经常有人改了 Kconfig 但没重新生成.config这个步骤最容易翻车。6.3 一个让我少折腾半天的习惯每次改配置后固定走一遍验证循环驱动能跑只是第一步后面你大概率会反复调file_operations里的函数、改设备树引脚、调整模块依赖。从那以后我每次改动都会强制自己走完一遍“备份旧镜像 → 编译 → mkknlimg → 拷贝 dtb 和 modules → sync → 重启 → dmesg 验证”的流程不做完不进行下一个修改。这套循环虽然看起来机械但能精准定位问题环节省去了“改了但不知道改没生效”的反复猜测。希望帮到你在这棵 4.19.y 源码树上找到自己的节奏。本文还有配套的精品资源点击获取