欢迎光临
我们一直在努力

设备树学习2--一个DTBO实验

1 实验环境和目的

这次还是用树莓派5吧,毕竟下一步还准备继续做UEFI的实验。

环境搭建刚刚写了一篇:树莓派5开发环境搭建基本操作-CSDN博客

现在准备把设备树的加载,详细的梳理一下。因为刚刚才看了索尼的光机,所以命名就直接用 ecx335c吧,不代表实际就是这个。

2 DTBO

2.1 DTS文件

首先新建一个名为 ecx335c-overlay.dts。将这个外设动态挂载到树莓派5的spi0总线上。

/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 这样的外部指针。)

dtc -@ -I dts -O dtb -o ecx335c-overlay.dtbo ecx335c-overlay.dts

2.2 静态加载

拷贝到指定位置:

sudo cp ecx335c-overlay.dtbo /boot/firmware/overlays/

然后增加加载功能sudo nano /boot/firmware/config.txt,在最后增加:

dtoverlay=ecx335c-overlay

之后重启。

2.3 动态加载

在树莓派上,有一个单独工具dtoverlay,可以动态加载。

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

简单试了一下,基本上有这个功能。不过确实没有树莓派的工具方便。

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)”的,目录下的所有文件和文件夹,全都是内核只读暴露出来的。

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。

此时,可以发现已经创建了节点。

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 '' *打印出来的内容(这个命令感觉有点哇塞):

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

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,这边有一层映射。

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

#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。

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)。

此时内容如下:

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的调试方法

# 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:

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中,有这么一行。

reset-gpios = <&rp1_gpio 6 1>; /* 1 代表 GPIO_ACTIVE_LOW */

之后再代码中,可以直接使用。比如:

gpiod_set_value(reset_gpio, 0)

这个是怎么实现的呢?

C语言肯定是不能直接调用 DTS。而是 DTS 变二进制 -> 二进制变内核内存结构体 -> 驱动通过“compatible 匹配”拿到结构体 -> 通过 gpiod_get 函数把里面的引脚信息“提取”成 C 语言指针变量。

这样的好处就是这意味着驱动以后移植到高通或者全志平台,代码一行都不用改,只需要在对应平台的 DTBO 里换个引脚编号即可。

赞(0)
未经允许不得转载:171主机测评 » 设备树学习2--一个DTBO实验
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址