1 实验环境和目的
这次还是用树莓派5吧,毕竟下一步还准备继续做UEFI的实验。
环境搭建刚刚写了一篇:树莓派5开发环境搭建基本操作-CSDN博客
现在准备把设备树的加载,详细的梳理一下。因为刚刚才看了索尼的光机,所以命名就直接用 ecx335c吧,不代表实际就是这个。

2 DTBO
2.1 DTS文件
首先新建一个名为 ecx335c-overlay.dts。将这个外设动态挂载到树莓派5的spi0总线上。
python
/dts-v1/;
/plugin/; /* 告诉DTC编译器,这是一个插件(Overlay) */
/ {
compatible = "brcm,bcm2712";
/* 目标:把内容注入到 &spi0 节点中 */
fragment@0 {
target = <&spi0>;
__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
/* 我们的测试外设 */
my_oled_panel@0 {
compatible = "sony,ecx335c";
reg = <0>; /* 对应树莓派物理Pin 24 (CE0) */
spi-max-frequency = <10000000>; /* 10 MHz */
/* 动态绑定的测试GPIO,这里用树莓派的GPIO 6 */
reset-gpios = <&rp1_gpio 6 1>; /* 1 代表 GPIO_ACTIVE_LOW */
};
};
};
};
树莓派的 Linux 系统自带了设备树编译器 dtc,所以直接编译即可。(注:-@ 参数至关重要,它负责生成符号表,允许动态解析类似 &spi0 这样的外部指针。)
bash
dtc -@ -I dts -O dtb -o ecx335c-overlay.dtbo ecx335c-overlay.dts
2.2 静态加载
拷贝到指定位置:
bash
sudo cp ecx335c-overlay.dtbo /boot/firmware/overlays/
然后增加加载功能sudo nano /boot/firmware/config.txt,在最后增加:
bash
dtoverlay=ecx335c-overlay
之后重启。
2.3 动态加载
在树莓派上,有一个单独工具dtoverlay,可以动态加载。
bash
sudo dtoverlay ecx335c-overlay.dtbo
其实这个是对标准Linux操作的封装。
正常流程如下:
1. 如果系统没有挂载 configfs,先挂载(通常 Android/Linux 开机已挂载)
mount -t configfs none /sys/kernel/config
2. 进入内核设备树 Overlay 的控制大本营
cd /sys/kernel/config/device-tree/overlays/
3. 创建一个你专属的动态插件目录(比如调 SPI10 屏幕)
mkdir my_panel_overlay
cd my_panel_overlay
4. 【核心命令】把编译好的 dtbo 灌进内核节点
cat /path/to/your/my_panel.dtbo > dtbo
5. 如果发现灌错了,直接删除
cd /sys/kernel/config/device-tree/overlays/
rmdir my_panel_overlay
简单试了一下,基本上有这个功能。不过确实没有树莓派的工具方便。
bash
tom@raspberrypi:/sys/kernel/config/device-tree/overlays$ sudo mkdir test
tom@raspberrypi:/sys/kernel/config/device-tree/overlays$ cd test/
tom@raspberrypi:/sys/kernel/config/device-tree/overlays/test$ ls
dtbo path status
2.4 加载后发生的事
此时主要由莓派 5 内部的 RP1 南桥芯片驱动(drivers/pinctrl/pinctrl-rp1.c) 和 DesignWare SPI 核心总线驱动(drivers/spi/spi-dw-core.c)进行处理。
内核解析你的dtbo,看到里面请求把GPIO 9、10、11复用。RP1的pinctrl驱动会立刻向物理引脚控制寄存器写入新的复用配置(Mux Value)。这几个脚的物理铜线在芯片内部瞬间与普通GPIO断开,直接挂接到了RP1内部的硬核SPI0控制器上。
Linux内核在系统树里动态创建出名为/sys/bus/spi/devices/spi10.0的新物理抽象节点。
内核SPI总线开始拿设备树里的compatible去匹配驱动。不过如果没匹配上,后面insmod的时候会继续匹配。
3 /sys/firmware/devicetree/base/
对于设备树来说,所有的内容都可以及时看到。不过这个文件夹是只读(Read-Only)"的,目录下的所有文件和文件夹,全都是内核只读暴露出来的。
bash
tom@raspberrypi:~/dttest$ cd /sys/firmware/devicetree/base/
tom@raspberrypi:/sys/firmware/devicetree/base$ ls
'#address-cells' cam0_reg clk-108M cpus i2c0mux model pwr_button sd-vcc-reg system
aliases cam1_clk clk-27M dummy interrupt-parent name reserved-memory serial-number thermal-zones
arm-pmu cam1_reg clocks firmwarekms leds __overrides__ rp1_firmware '#size-cells' timer
axi cam_dummy_reg compatible hvs@107c580000 memory@0 phy rp1_vdd_3v3 soc@107c000000 wl-on-reg
cam0_clk chosen cooling_fan i2c0if memreserve psci sd-io-1v8-reg __symbols__
只要一个设备树节点在内核初始化或动态加载时被成功解析,它就会原封不动地在 /sys/firmware/devicetree/base/ 目录下映射出一个一模一样的目录树。
这是由内核里的 OF 核心库(Drivers/of/base.c) 管理的。对内核而言,/sys/firmware/devicetree/base/ 就是整个硬件世界最原始的"数字孪生克隆体"。
不管这个硬件有没有驱动、芯片有没有坏、总线控制器有没有激活,只要DTB里写了,这里就一定会有。
目录的名字是按照节点名@物理地址拼出来的,比如my_oled_panel@0。
此时,可以发现已经创建了节点。
bash
tom@raspberrypi:~/dttest$ find /sys/firmware/devicetree/base/ -name "*my_oled_panel*"
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@50000/my_oled_panel@0
tom@raspberrypi:~/dttest$ find /sys/firmware/devicetree/base/ -name "spi@*"
/sys/firmware/devicetree/base/soc@107c000000/spi@7d004000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@60000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@58000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@68000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@4c000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@54000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@5c000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@64000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@50000
/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@6c000
下面是用grep -a '' *打印出来的内容(这个命令感觉有点哇塞):
bash
tom@raspberrypi:/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@50000/my_oled_panel@0$ grep -a '' *
compatible:sony,ecx335c
name:my_oled_panel
reg:
reset-gpios:.
spi-max-frequency:▒▒▒
能在树莓派/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@50000/my_oled_panel@0里看到compatible、reg、reset-gpios 这些虚拟文件,说明 DTBO 已经成功在内核启动时把静态账本塞进 spi0里面了! 此时,struct spi_device 对象已经在内核总线中创建完毕。
为什么是/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@50000/my_oled_panel@0呢?是因为使用的spi0,这个在上级就已经定义了。TODO
在 BCM2712 芯片设计时,就决定芯片内部的 AXI 总线外设物理基地址从 1000000000 开始。其中专门拉了一条 PCIe 专门用来连接 RP1 南桥芯片,这个 PCIe 控制器的物理配置寄存器基地址被接在了0x1000120000。
GPIO 控制器放在偏移地址 0x30000 的地方。第一个 I2C 控制器放在偏移地址 0x40000 的地方。第一个 SPI 控制器(也就是SPI0)的寄存器,做在了偏移地址0x50000的地方。
原始的SPI定义是在linux/arch/arm64/boot/dts/broadcom/rp1.dtsi at rpi-6.18.y · raspberrypi/linux · GitHub
python
rp1_spi8: spi@4c000 {
reg = <0xc0 0x4004c000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI8 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <1>;
#size-cells = <0>;
num-cs = <2>;
dmas = <&rp1_dma RP1_DMA_SPI8_TX>,
<&rp1_dma RP1_DMA_SPI8_RX>;
dma-names = "tx", "rx";
status = "disabled";
};
rp1_spi0: spi@50000 {
reg = <0xc0 0x40050000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI0 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <1>;
#size-cells = <0>;
num-cs = <2>;
dmas = <&rp1_dma RP1_DMA_SPI0_TX>,
<&rp1_dma RP1_DMA_SPI0_RX>;
dma-names = "tx", "rx";
status = "disabled";
};
rp1_spi1: spi@54000 {
reg = <0xc0 0x40054000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI1 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <1>;
#size-cells = <0>;
num-cs = <2>;
dmas = <&rp1_dma RP1_DMA_SPI1_TX>,
<&rp1_dma RP1_DMA_SPI1_RX>;
dma-names = "tx", "rx";
status = "disabled";
};
rp1_spi2: spi@58000 {
reg = <0xc0 0x40058000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI2 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <1>;
#size-cells = <0>;
num-cs = <2>;
dmas = <&rp1_dma RP1_DMA_SPI2_TX>,
<&rp1_dma RP1_DMA_SPI2_RX>;
dma-names = "tx", "rx";
status = "disabled";
};
rp1_spi3: spi@5c000 {
reg = <0xc0 0x4005c000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI3 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <1>;
#size-cells = <0>;
num-cs = <2>;
dmas = <&rp1_dma RP1_DMA_SPI3_TX>,
<&rp1_dma RP1_DMA_SPI3_RX>;
dma-names = "tx", "rx";
status = "disabled";
};
// SPI4 is a target/slave interface
rp1_spi4: spi@60000 {
reg = <0xc0 0x40060000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI4 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <0>;
#size-cells = <0>;
num-cs = <1>;
spi-slave;
dmas = <&rp1_dma RP1_DMA_SPI4_TX>,
<&rp1_dma RP1_DMA_SPI4_RX>;
dma-names = "tx", "rx";
status = "disabled";
slave {
compatible = "spidev";
spi-max-frequency = <1000000>;
};
};
rp1_spi5: spi@64000 {
reg = <0xc0 0x40064000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI5 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <1>;
#size-cells = <0>;
num-cs = <2>;
dmas = <&rp1_dma RP1_DMA_SPI5_TX>,
<&rp1_dma RP1_DMA_SPI5_RX>;
dma-names = "tx", "rx";
status = "disabled";
};
rp1_spi6: spi@68000 {
reg = <0xc0 0x40068000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI6 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <1>;
#size-cells = <0>;
num-cs = <2>;
dmas = <&rp1_dma RP1_DMA_SPI6_TX>,
<&rp1_dma RP1_DMA_SPI6_RX>;
dma-names = "tx", "rx";
status = "disabled";
};
// SPI7 is a target/slave interface
rp1_spi7: spi@6c000 {
reg = <0xc0 0x4006c000 0x0 0x130>;
compatible = "snps,dw-apb-ssi";
interrupts = <RP1_INT_SPI7 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rp1_clocks RP1_CLK_SYS>;
clock-names = "ssi_clk";
#address-cells = <0>;
#size-cells = <0>;
num-cs = <1>;
spi-slave;
dmas = <&rp1_dma RP1_DMA_SPI7_TX>,
<&rp1_dma RP1_DMA_SPI7_RX>;
dma-names = "tx", "rx";
status = "disabled";
板级的是在linux/arch/arm64/boot/dts/broadcom/bcm2712-rpi.dtsi at rpi-6.18.y · raspberrypi/linux · GitHub,这边有一层映射。
python
serial4 = &uart4;
spi0 = &spi0;
spi1 = &spi1;
spi10 = &spi10;
spi2 = &spi2;
spi3 = &spi3;
spi4 = &spi4;
spi5 = &spi5;
uart0 = &uart0;
uart1 = &uart1;
所以最后的地址就是/sys/firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@50000/my_oled_panel@0。
spi0.0 的由来:打印里是spi0.0。前一个 0 是因为在 DTBO 里把它套在了 &spi0 下面;后一个 .0 就是因为在 DTBO 里的 reg = <0>;(对应 @0)。这个就是总线命名规则。
4 编写驱动代码
文件名ecx335c_driver.c
cpp
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h> // 【核心修正】显式引入设备树结构体头文件
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tom");
MODULE_DESCRIPTION("Sony ECX335C Probe Tracker");
static int ecx335c_probe(struct spi_device *spi)
{
struct gpio_desc *reset_gpio;
// 打印 1:证明 Match 成功,内核已经调进了我们的 Probe
printk(KERN_INFO "========== [Tom-Debug] ecx335c_probe STARTED ==========\n");
printk(KERN_INFO "[Tom-Debug] Device matched on SPI bus: %s\n", dev_name(&spi->dev));
// 【安全检查】确认设备树节点确实挂载上去了
if (spi->dev.of_node) {
printk(KERN_INFO "[Tom-Debug] DT Node Name: %s\n", spi->dev.of_node->name);
} else {
printk(KERN_WARNING "[Tom-Debug] Warning: No Device Tree node found for this device!\n");
}
// 【核心修正】移除对 spi->chip_select 的直接整型打印,改用通用的 Max Speed 打印
printk(KERN_INFO "[Tom-Debug] Max Bus Speed configured: %d Hz\n", spi->max_speed_hz);
// 获取 GPIO 资源 (绑定设备树中的 reset-gpios)
reset_gpio = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(reset_gpio)) {
printk(KERN_ERR "[Tom-Debug] Failed to acquire reset-gpios from Device Tree!\n");
return PTR_ERR(reset_gpio);
}
// 打印 2:证明 GPIO 成功从设备树里解析并绑定
printk(KERN_INFO "[Tom-Debug] Successfully mapped reset-gpios!\n");
// 模拟硬件复位时序
gpiod_set_value(reset_gpio, 1); // 在 ACTIVE_LOW 机制下,传入 1 会驱使物理引脚输出低电平
printk(KERN_INFO "[Tom-Debug] MicroOLED hardware reset pin (XCLR) pulled LOW.\n");
return 0;
}
static void ecx335c_remove(struct spi_device *spi)
{
printk(KERN_INFO "========== [Tom-Debug] ecx335c_remove CALLED ==========\n");
}
/* 建立用来与 DTBO 匹配的兼容性字符串数组 */
static const struct of_device_id ecx335c_of_match[] = {
{ .compatible = "sony,ecx335c" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, ecx335c_of_match);
static struct spi_driver ecx335c_driver = {
.driver = {
.name = "ecx335c_panel_driver",
.of_match_table = ecx335c_of_match,
},
.probe = ecx335c_probe,
.remove = ecx335c_remove,
};
module_spi_driver(ecx335c_driver);
Makefile
obj-m += ecx335c_driver.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
直接在当前目录输入 make,就会在当前目录下生成一个驱动模块:ecx335c_driver.ko。
bash
tom@raspberrypi:~/dttest$ make
make -C /lib/modules/6.12.75+rpt-rpi-2712/build M=/home/tom/dttest modules
make[1]: Entering directory '/usr/src/linux-headers-6.12.75+rpt-rpi-2712'
CC [M] /home/tom/dttest/ecx335c_driver.o
MODPOST /home/tom/dttest/Module.symvers
CC [M] /home/tom/dttest/ecx335c_driver.mod.o
CC [M] /home/tom/dttest/.module-common.o
LD [M] /home/tom/dttest/ecx335c_driver.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.12.75+rpt-rpi-2712'
5 /sys/bus/spi/devices/spi0.0
可以看到,此时生成了/sys/bus/spi/devices/spi0.0。解释如下:
/sys/bus/ (总线设备大盘)是由Linux 内核设备驱动模型(Device-Driver Model)中的各总线核心层(SPI Core, Platform Core, I2C Core 等)管理。
它是功能分类。它不关心你的物理祖先是谁,这里只有"用什么协议通信"以及"归哪个驱动管"。
这里的spi0.0命名也有严格的含义:前面的0代表0号SPI 控制器(即某个 QUPv3 SE 引擎),后面的0代表片选引脚编号(Chip Select 0, 即 CS0)。
此时内容如下:
bash
tom@raspberrypi:/sys/bus/spi/devices/spi0.0$ ls -l
total 0
lrwxrwxrwx 1 root root 0 Jun 11 11:00 driver -> ../../../../../../bus/spi/drivers/ecx335c_panel_driver
-rw-r--r-- 1 root root 16384 Jun 11 11:00 driver_override
-r--r--r-- 1 root root 16384 Jun 11 11:00 modalias
lrwxrwxrwx 1 root root 0 Jun 11 11:00 of_node -> ../../../../../../firmware/devicetree/base/axi/pcie@1000120000/rp1/spi@50000/my_oled_panel@0
drwxr-xr-x 2 root root 0 Jun 11 11:00 power
drwxr-xr-x 2 root root 0 Jun 11 11:00 statistics
lrwxrwxrwx 1 root root 0 Jun 11 10:19 subsystem -> ../../../../../../bus/spi
lrwxrwxrwx 1 root root 0 Jun 11 11:00 supplier:platform:1f000d0000.gpio -> ../../../../../virtual/devlink/platform:1f000d0000.gpio--spi:spi0.0
-rw-r--r-- 1 root root 16384 Jun 11 10:19 uevent
SPI的调试方法
bash
# 1. 切换到 root
sudo su
# 2. 擦亮内核的追踪器镜片
echo 0 > /sys/kernel/tracing/tracing_on
echo "" > /sys/kernel/tracing/trace
# 3. 开启 SPI 模块搬运数据的事件监听(抓取发送和接收)
echo 1 > /sys/kernel/tracing/events/spi/spi_transfer_start/enable
# 4. 打开总开关
echo 1 > /sys/kernel/tracing/tracing_on
# 5. 此时去运行你的调屏测试程序(或者往 /dev/spidev10.0 灌数据)
# 6. 见证奇迹的时刻,查看内核抓到的 SPI 裸波形流
cat /sys/kernel/tracing/trace | tail -n 50
更具体的,打算单独写一篇来弄。。。TODO
6 时序观察
重启树莓派:sudo reboot
现在,我们手动塞入我们的驱动 .ko,来触发 Match 机制。
sudo insmod ecx335c_driver.ko
直接运行以下命令查看内核环形缓冲区的 Log:
bash
dmesg | grep "\[Tom-Debug\]"
会看到终端里以下时序Log。
[ 45.102345] ========== [Tom-Debug] ecx335c_probe STARTED ==========
[ 45.102350] [Tom-Debug] Device matched on SPI bus: spi0.0
[ 45.102353] [Tom-Debug] DT Node Name: my_oled_panel
[ 45.102355] [Tom-Debug] Chip Select configured: 0, Max Speed: 10000000 Hz
[ 45.105120] [Tom-Debug] Successfully mapped reset-gpios!
[ 45.105128] [Tom-Debug] MicroOLED hardware reset pin (XCLR) pulled LOW.
7 gpios的操作
在DST中,有这么一行。
cpp
reset-gpios = <&rp1_gpio 6 1>; /* 1 代表 GPIO_ACTIVE_LOW */
之后再代码中,可以直接使用。比如:
cpp
gpiod_set_value(reset_gpio, 0)
这个是怎么实现的呢?
C语言肯定是不能直接调用 DTS。而是 DTS 变二进制 -> 二进制变内核内存结构体 -> 驱动通过"compatible 匹配"拿到结构体 -> 通过 gpiod_get 函数把里面的引脚信息"提取"成 C 语言指针变量。
这样的好处就是这意味着驱动以后移植到高通或者全志平台,代码一行都不用改,只需要在对应平台的 DTBO 里换个引脚编号即可。