☰
树莓派Linux内核4.19.y定制实战:从交叉编译到设备树与驱动
2026/10/23 13:14:44 网站建设 项目流程

简介:面向树莓派开发者的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树莓派3B+3B+的板级设备描述
bcm2711_defconfig树莓派4B默认配置BCM2711 平台的内核功能开关
bcmrpi3_defconfig树莓派3B/3B+3代芯片的默认配置文件

常见误用是改完设备树忘了重新编译.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告诉内核构建系统目标架构是 ARM,CROSS_COMPILE指定交叉编译器的前缀。漏掉任何一个,make大概率会在本地 x86 环境下试图用gcc编译 ARM 代码,直接报unknown architecture之类的错误。

export ARCH=arm export CROSS_COMPILE=arm-bcm2708-linux-gnueabihf- make bcm2711_defconfig # 生成 .config,4B 用 bcm2711_defconfig make -j4 zImage modules dtbs

bcm2711_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 menuconfig

menuconfig 是终端里的图形化配置界面,按/键可以搜索某个配置项。比如想去掉蓝牙,直接搜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 dtbs

grep 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 -20

modprobe和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-dev,Fedora 系装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。 解决:在启动时按Ctrl+C或查看最终进入的 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_DRV=y。

# 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.o

6.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 源码树上找到自己的节奏。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询