设备树修改实验演示
341.4.2 设备树修改与实验演示
/* imx6ull-14x14-evk.dts:本节实际修改并由课程提供的最终片段。 */
&i2c1 { // 引用 SoC 公共设备树中已经定义的 I²C1 控制器节点。
clock-frequency = <100000>; // 将本总线的目标时钟频率设为 100000 Hz,即 100 kHz。
pinctrl-names = "default"; // 声明默认状态对应一组引脚复用配置。
pinctrl-0 = <&pinctrl_i2c1>; // 默认状态引用 pinctrl_i2c1,引出 I²C1 的 SCL 和 SDA。
status = "okay"; // 明确表示 I²C1 控制器可以投入使用。
mpu6050@68 { // 声明 I²C1 上 7 位地址为 0x68 的 MPU6050 设备。
compatible = "mpu6050,INV_MPU6050"; // 必须与课程 MPU6050 驱动的 OF 匹配字符串一致。
//interrupt-parent = <&gpio5>; // 课程最终源码注释了原有的中断控制器配置。
//interrupts = <0 0>; // 课程最终源码未给当前 MPU6050 驱动配置中断输入。
reg = <0x68>; // 在 I²C 总线节点下,reg 表示不含读写位的从设备地址。
status = "okay"; // 课程显式标记该设备节点可用。
//position = <1>; // 课程最终源码没有让驱动使用原有的安装方位属性。
}; // MPU6050 设备节点结束。
oled@3c { // 声明 I²C1 上 7 位地址为 0x3c 的 OLED 设备。
compatible = "i2c_oled"; // 必须与课程 OLED 驱动的 OF 匹配字符串一致。
reg = <0x3c>; // 这里必须写 7 位地址 0x3c,不能写含写方向位的 0x78。
status = "okay"; // 课程显式标记 OLED 节点可用。
}; // OLED 设备节点结束。
}; // 本节摘录的 I²C1 节点结束。
/* mpu6050_drv.c:视频实际查看的设备树匹配表。 */
static const struct of_device_id mpu6050_of_match_table[] = { // 定义 MPU6050 驱动支持的 OF 设备列表。
{.compatible = "mpu6050,INV_MPU6050"}, // 与设备树中 MPU6050 节点的 compatible 完全对应。
{/* sentinel */} // 空表项是结束标记,内核遍历到这里停止。
}; // MPU6050 的 OF 匹配表结束。
/* oled_drv.c:视频实际查看的设备树匹配表。 */
static const struct of_device_id oled_of_match_table[] = { // 定义 OLED 驱动支持的 OF 设备列表。
{.compatible = "i2c_oled"}, // 与设备树中 OLED 节点的 compatible 完全对应。
{/* sentinel */}}; // 空表项结束数组;此处保留课程源码原有的括号写法。
本节接续上一课的失败现场:两个 .ko 模块已经进入内核,但应用仍提示 can't open /dev/oled。这一课补上活动设备树中的 I²C 设备描述,重新生成并部署 DTB,再用驱动日志、设备节点、传感器数据和 OLED 显示逐层验证。上面的 DTS 片段与两个匹配表均来自本节视频实际展示的课程配套源码,没有补写后续驱动业务代码。
一、设备树修改
1.1 上节回顾
上一课的终端状态是:
# lsmod
Module Size Used by
oled_drv 6932 0
mpu6050_drv 3814 0
# ./app
can't open /dev/oled
lsmod 只能证明模块入口已经执行、模块代码已驻留内核。它不能证明设备与驱动完成匹配,也不能证明 probe() 已执行。课程应用首先打开 /dev/oled,失败后立即返回,因此当时甚至还没有继续打开 /dev/I2C1_mpu6050。
完整因果链是:
活动 DTB 没有课程所需的 I²C 设备描述
↓
内核没有为它们实例化对应的 I²C client
↓
驱动模块虽已加载,却没有匹配对象,probe() 不执行
↓
probe() 内的 device_create() 不执行
↓
/dev/oled 与 /dev/I2C1_mpu6050 不会生成
↓
应用 open() 失败
视频先卸载上一课加载的模块:
lsmod
rmmod oled_drv.ko
lsmod
rmmod mpu6050_drv.ko
lsmod
画面中的 BusyBox rmmod 接受了带 .ko 的写法。日常更通用的写法是按 lsmod 第一列给出的模块名执行 rmmod oled_drv 和 rmmod mpu6050_drv;insmod 则需要模块文件路径,如 insmod oled_drv.ko。
上一课的其他结论仍然成立:应用必须用 ARM 交叉编译器构建,链接数学库的参数是小写 -lm;外部模块通过 Kbuild 生成 .ko;应用和模块放进 Ubuntu 的 NFS 根目录后,开发板能直接看到,无需反复烧写 eMMC。本节要更换的是启动阶段使用的 DTB,走的是 TFTP 下载链路,不是 NFS 文件共享链路。
1.2 设备树修改
1.2.1 设备树的功能与路径
设备树用于描述操作系统无法像 PCI、USB 那样自动枚举的板级硬件,包括控制器、总线连接、地址、时钟、引脚和中断等。它告诉内核“硬件按什么方式连接”,并不等同于设备驱动,也不会凭空使未接线的硬件出现。Linux 官方文档指出,I²C 设备不能在硬件层自动枚举,软件必须知道设备位于哪条总线以及使用什么地址;在设备树平台上,固定 I²C 设备通常作为控制器节点的子节点声明,参见 Linux I²C 设备实例化文档。
课程内核源码中的常见文件是:
| 名称 | 含义 | 本节用途 |
|---|---|---|
.dtsi | 可被多个板级文件复用的设备树源包含文件 | 提供 i.MX6ULL SoC 的公共控制器定义 |
.dts | 某块板或某个产品配置的设备树源入口/板级增量 | 在 imx6ull-14x14-evk.dts 中修改 I²C1 |
.dtb | DTC 编译生成的二进制设备树 | U-Boot 实际下载并交给内核 |
| DTC | Device Tree Compiler | 把预处理后的 DTS 编译为 DTB |
这些文件位于内核源码的 arch/arm/boot/dts/。DTC 是工具名称,不是把 .dtc 当作一种设备树源码扩展名。
1.2.2 找到并打开设备树文件
本节实际编辑:
arch/arm/boot/dts/imx6ull-14x14-evk.dts
要注意源文件与启动文件的包含关系:
topeet_emmc_hdmi.dts
└── imx6ull-14x14-evk-emmc.dts
└── imx6ull-14x14-evk.dts ← 本节修改处
└── imx6ull.dtsi
因此,修改通用板级文件后,需要重新构建并部署最外层入口对应的 topeet_emmc_hdmi.dtb。直接把 imx6ull-14x14-evk.dts 放进 TFTP 目录没有作用;U-Boot 当前请求的是 topeet_emmc_hdmi.dtb。
视频通过 Windows 映射到 Ubuntu 的源码目录,用 VS Code 打开并搜索。按文件修改日期只能辅助定位,不能用来确认启动配置。更可靠的方式是按完整文件名搜索,再沿 #include 关系核对最终 DTB;VS Code 可用 Ctrl+P,终端可用:
find arch/arm/boot/dts -name 'imx6ull-14x14-evk.dts'
grep -R 'imx6ull-14x14-evk.dts' arch/arm/boot/dts
1.2.3 设备树文件中的设备信息
视频在约第 389 行找到 &i2c1。开头的 & 表示通过标签引用 imx6ull.dtsi 中已经存在的 I²C1 控制器节点,并在板级文件中补充或覆盖属性,不是创建第二个 I²C1 控制器。
| 属性 | 本节含义 |
|---|---|
clock-frequency = <100000>; | I²C1 目标总线频率为 100 kHz |
pinctrl-names = "default"; | 声明默认 pinctrl 状态 |
pinctrl-0 = <&pinctrl_i2c1>; | 默认状态使用 I²C1 的 SCL/SDA 引脚复用组 |
status = "okay"; | I²C1 控制器可用 |
pinctrl_i2c1 负责把相应 SoC 焊球切换到 I²C 功能并配置电气参数。它更接近单片机中的“引脚复用与 pad 配置”,但设备树是在描述配置数据,真正写寄存器的是内核 pinctrl 驱动。课程前面已经确认扩展接口上的 OLED 和 MPU6050 共用 I²C1 的 SCL、SDA;同一总线允许多个从设备共线,靠不同地址区分。
1.2.4 修改设备树中的 I²C 设备信息
课程源码原有 mpu6050@68 节点。本节保留并核对两项关键属性:
compatible = "mpu6050,INV_MPU6050":供驱动的 OF 匹配表识别;reg = <0x68>:MPU6050 在该 I²C 总线上的 7 位地址。
视频把 interrupt-parent、interrupts 和 position 注释掉。准确原因是本课程当前驱动没有申请或处理这些属性,而且原节点的 GPIO 中断配置并未按本实验接线核实。不能概括成“I²C 设备不需要中断”:不少 I²C 传感器会用额外的 IRQ 引脚通知数据就绪、动作检测或故障;总线通信和中断通知是两条不同信号链。需要中断时,应依据原理图、芯片手册和绑定文档写入真实 GPIO/触发类型,并让驱动使用它。
课程还加入 status = "okay"。它能明确表达节点可用,但对这个新建子节点并非绝对必需:Linux 的设备树 API 把 status 缺省、"okay" 或 "ok" 都视为可用。只有某层原先写了 status = "disabled" 时,板级覆盖为 "okay" 才是启用它的关键动作,参见 Linux DeviceTree API 对可用状态的定义。
1.2.5 添加新设备信息
OLED 同样是固定连接在 I²C1 上且不能自动枚举的设备,所以在 &i2c1 下增加 oled@3c 子节点。节点名由两部分组成:
oled@3c
│ └── unit-address,应与 reg 的 0x3c 一致
└─────── 便于人阅读的节点名称
复制相邻 I²C 设备节点只能作为语法起点,随后必须按目标硬件逐项核对 compatible、reg、供电、中断、复位等属性。不能把 AP3216C 或 MPU6050 的地址和中断配置原样留给 OLED,也不能据此推导“任何硬件都必须有一个设备树节点”。本实验中这两个固定、不可枚举的 I²C 设备采用设备树声明;其他总线或系统还可能通过 ACPI、板级表、总线枚举或用户空间接口实例化设备。
1.2.6 确定 compatible 值
视频分别打开两份驱动,在 of_device_id 表中查到:
MPU6050 设备树:compatible = "mpu6050,INV_MPU6050"
MPU6050 驱动表:.compatible = "mpu6050,INV_MPU6050"
OLED 设备树:compatible = "i2c_oled"
OLED 驱动表:.compatible = "i2c_oled"
设备树字符串与驱动 of_match_table 中的某一项匹配后,驱动核心才会尝试绑定并调用 probe()。Linux 驱动模型的绑定文档明确区分了“找到匹配项”和“调用 probe() 完成绑定”。因此:
.ko文件名、i2c_driver.driver.name、i2c_device_id名称与 OFcompatible是不同字段,不要混为一谈;compatible必须逐字符一致,大小写也属于字符串内容;- 现代设备树规范建议使用
"manufacturer,model"形式。课程的"i2c_oled"没有厂商前缀,"mpu6050,INV_MPU6050"也不是规范的新绑定命名,但它们是配套驱动已经采用的私有字符串,本实验必须原样匹配,不能单独改一边; - 日志中的
match successed是课程驱动在probe()中自行打印的英文字符串,不是内核统一提示。
compatible 匹配只能证明软件描述与驱动表相符。课程的 probe() 主要注册字符设备,没有先通过一次可靠 I²C 事务确认芯片身份,所以即使出现匹配日志,也不能单独证明实物已接好或芯片在总线上应答。
1.2.7 确定设备地址
视频先从旧的 51 单片机 OLED 代码中看到 0x78,暂时把它写进设备树,随后马上说明该值不适用于 Linux 设备树并改为 0x3c。因此本节的最终结论是:
| 设备 | 设备树 reg | 含义 |
|---|---|---|
| MPU6050 | 0x68 | 本实验接线所用的 7 位 I²C 地址 |
| OLED | 0x3c | 本实验 OLED 的 7 位 I²C 地址 |
工程中应优先从具体模块原理图、控制器数据手册和地址选择脚状态确认地址。旧代码里的常量可能表示 7 位地址、已经左移的地址字节,甚至是某个库自己的参数格式,不能只看到数值就直接复制。
1.2.8 保存修改
VS Code 标签上的白点表示文件尚未保存。按 Ctrl+S 后,修改才写回 Ubuntu 上的内核源码文件。视频使用 Windows 端访问 Linux 源码目录;无论底层是共享目录、SFTP 映射还是远程编辑,最终都要确认 Linux 构建目录中的文件内容和修改时间已经更新。
建议保存后立即核对关键行,防止在错误的同名源码树中修改:
grep -n -A14 'mpu6050@68' arch/arm/boot/dts/imx6ull-14x14-evk.dts
1.2.9 编译内核
保存 DTS 后必须重新生成 DTB;编辑文本本身不会改变下一次启动使用的二进制设备树。视频使用课程厂商脚本构建,输出中同时出现内核和多种 DTB 的生成过程。此次没有修改 C 内核代码,运行整套脚本主要是沿用课程环境,最终部署时只替换了 DTB。
通用内核树也常用 make dtbs 或指定某个 DTB 目标,但具体交叉编译器、配置和目标名称依赖当前厂商内核。不能把课程的 create.sh 当作所有 Linux 内核都自带的标准命令。
1.2.10 确定正确的设备地址
课程用二进制对比说明 0x78 与 0x3c 的关系:
0x3c = 0b0111100 ← 7 位设备地址
0x78 = 0b01111000 ← (0x3c << 1) | 0,最低位为写方向
0x79 = 0b01111001 ← (0x3c << 1) | 1,最低位为读方向
因此:
0x78 >> 1 = 0x3c
右移不是随意换算,而是从某些旧资料写出的“8 位地址字节”中去掉最低位 R/W。设备树描述设备身份和总线位置,不预先固定下一笔传输是读还是写,所以 reg 应写 0x3c。
1.2.11 I²C 协议与设备地址
对本实验采用的普通 7 位寻址,一次地址阶段可按下面理解:
bit 7 bit 1 bit 0
┌──────────────────────────────────┬─────┐
│ 7 位从设备地址 │ R/W │
└──────────────────────────────────┴─────┘
0:写
1:读
Linux I²C API 和设备树节点保存的是地址部分;控制器驱动根据具体消息方向,在总线上生成相应的 R/W 位。Linux 文档的设备树示例也直接把 EEPROM 的 7 位地址写成 reg = <0x50>,参见 I²C 设备实例化文档。
这里还要区分三种常见写法:
| 资料中的写法 | 对本实验 OLED 的值 | 能否直接写入 reg |
|---|---|---|
| 7 位从设备地址 | 0x3c | 可以 |
| 8 位写地址字节 | 0x78 | 不可以,应右移一位 |
| 8 位读地址字节 | 0x79 | 不可以,应右移一位 |
I²C 也定义了 10 位寻址,但本节两个设备都按普通 7 位地址处理;不能把“高 7 位加 R/W”错误推广到所有 I²C 地址形式。
1.3 编译内核
1.3.1 编译内核前的准备
视频在 Ubuntu Shell 中进入当前课程内核根目录:
~/Linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga
构建前至少确认三点:DTS 已保存、当前路径是启动配置对应的内核树、交叉编译环境仍然可用。若在另一份解压目录中修改或构建,即使命令成功,TFTP 中仍可能被复制成旧产物。
1.3.2 编译内核的步骤
视频实际执行:
./create.sh
./ 表示从当前目录运行脚本;.sh 是文件名的一部分。画面中的脚本先更新 .config,随后构建内核和一批板级 DTB。若出现权限错误,可以先检查 ls -l create.sh,再决定是否需要赋予执行权限;若出现 DTC 语法错误,应回到 DTS 报错行修正,不能继续部署旧 DTB。
构建结束后,本节真正需要的产物是:
arch/arm/boot/dts/topeet_emmc_hdmi.dtb
1.3.3 新生成设备树的放置位置
量产或离线启动时,内核与 DTB 通常要写入板上 eMMC 的启动分区或约定位置。本课程已在第 339 课把 U-Boot bootcmd 配置为每次启动通过 TFTP 下载:
zImage → DDR 0x80800000
topeet_emmc_hdmi.dtb → DDR 0x83800000
所以当前调试只需更新 Ubuntu 的 TFTP 根目录:
/home/wfeng/tftpfile
TFTP 在启动前下载 zImage 和 DTB;Linux 启动后才通过 NFS 使用 /home/wfeng/nfs 作为根文件系统。两者职责不同,不能称为“TFTP 挂载设备树”或“TFTP 挂载根文件系统”。本节没有修改内核代码,视频保留原 zImage,只更新了 topeet_emmc_hdmi.dtb。
1.3.4 更新设备树的具体操作
根据视频路径整理后的命令如下:
# 进入第 339 课配置的 TFTP 根目录。
cd ~/tftpfile/
# 确认目录中原有 zImage 和旧 DTB。
ls
pwd
# 视频先删除旧 DTB。
rm topeet_emmc_hdmi.dtb
# 从当前课程内核构建目录复制新 DTB;末尾的点表示当前目录。
cp ~/Linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/boot/dts/topeet_emmc_hdmi.dtb .
# 确认文件名、大小和修改时间均符合预期。
ls -lh zImage topeet_emmc_hdmi.dtb
其实 cp 默认即可覆盖同名普通文件,视频的“先删再复制”不是必要步骤。关键是 TFTP 目录中的文件名必须与 U-Boot bootcmd 请求的 topeet_emmc_hdmi.dtb 完全一致,并且复制命令成功。若希望进一步确认最终二进制确实包含新节点,可在主机上反编译检查:
dtc -I dtb -O dts -o /tmp/running-candidate.dts ~/tftpfile/topeet_emmc_hdmi.dtb
grep -n -A12 'mpu6050@68' /tmp/running-candidate.dts
反编译能证明候选 DTB 中包含节点;是否真正启动了这份文件,还要看 U-Boot 的 TFTP 文件名、传输日志和启动参数。
1.4 使用新设备树和内核
视频给开发板断电再上电。因为 U-Boot 环境变量已保存,启动时会重新从 ~/tftpfile 下载现有 zImage 和刚更新的 topeet_emmc_hdmi.dtb。严格说,这次实验验证的是“原有内核镜像 + 新设备树”,并没有部署新的内核逻辑。
登录后进入 NFS 根文件系统中的实验目录并加载模块:
cd /oled_6050
ls
insmod mpu6050_drv.ko
insmod oled_drv.ko
lsmod
这次日志与上一课明显不同:
mpu6050_driver_init
mpu6050 match successed
oled match successed
alloc device number : major:[247], minor:[0] succeed!
create device file 'oled' succeed!
get i2c_client, client name = i2c_oled, addr = 0x3c
get i2c_adapter, adapter name = 21a0000.i2c
oled probe
oled i2c_driver was added into the system.
动态主设备号如 247 由当次运行分配,不应写进应用或脚本。真正有诊断价值的是:两个匹配提示出现,OLED probe() 执行,日志读到 addr = 0x3c,并创建了字符设备。
从设备树到应用可用的顺序是:
DTS 修改
→ DTC 生成 topeet_emmc_hdmi.dtb
→ U-Boot 通过 TFTP 下载并把 DTB 交给内核
→ 内核为 I²C1 子节点建立 I²C 设备对象
→ insmod 注册 i2c_driver
→ compatible 与 of_match_table 匹配
→ 调用 probe()
→ 课程驱动注册字符设备并调用 device_create()
→ /dev/oled 与 /dev/I2C1_mpu6050 可供应用打开
Linux 的 I²C 客户端驱动文档说明,I²C 驱动通过 probe() 与已实例化的设备绑定,驱动本身不会仅凭 i2c_driver 注册就自动创造对应 client。可用下面的检查把“模块存在”和“设备可用”分开:
lsmod
dmesg | tail -n 80
ls -l /dev/oled /dev/I2C1_mpu6050
ls /sys/bus/i2c/devices
lsmod 中 Used by 0 主要表示模块依赖引用计数,不等于驱动没有绑定设备,也不能替代 dmesg、sysfs、设备节点和真实 I/O 验证。
1.5 运行 APP
驱动匹配后,视频执行:
./app
终端先出现两个驱动的打开日志,然后持续打印 MPU6050 原始数据,例如:
oled open
mpu6050_open
AX=270, AY=-156, AZ=4308
GX=-59, GY=-1, GZ=1
同时,实物 OLED 开始刷新数据显示。这一结果比单独看到 match successed 更完整,因为它证明了以下链路在本次演示中已经贯通:
- 应用成功打开两个
/dev节点; - MPU6050 驱动能够从 I²C1 读取数据;
- 应用能够把数据交给 OLED 驱动;
- OLED 实物能够显示更新内容。
视频中第一次展示时数据短暂停止,教师移动连线后重新运行 ./app,显示恢复,并判断是接触不良。这个现象只能说明当时的故障与线缆接触相关,不能形成“程序无输出都重跑一次”的通用结论。若实验失败,应按层排查:
| 现象 | 优先检查 |
|---|---|
没有 match successed | 启动的 DTB、节点 status、compatible、模块是否为最新版本 |
有匹配日志但没有 /dev 节点 | probe() 后续报错、字符设备注册、当前根文件系统的设备管理机制 |
| 节点存在但 I²C 读写失败 | 7 位地址、供电、共地、SCL/SDA 是否接反、上拉和接触情况 |
| MPU6050 有数据但 OLED 不显示 | OLED 地址、写事务、显示初始化、屏幕供电和连接 |
| 数据出现但角度不准 | 传感器量程、零偏、安装方向、滤波和应用算法;属于后续代码分析范围 |
本节完成的是从设备树声明到实物运行的闭环。必须同时记住两个证据边界:设备树描述不等于硬件自动探测成功;一组会变化的原始值和 OLED 刷新能证明链路工作,却不能单凭这一段演示证明传感器已经完成标定或姿态算法绝对准确。