为什么要阅读这篇文章?
如果你拥有一块贝莱坞黑色版(Beaglebone Black,BBB)并且想要将自己的设备连接到它上面(而不是使用cape),那么你可能已经听说过设备树。在我的情况下,我想将一个RTC设备连接到BBB上的I2C总线上。网络上有很多零散的信息,本文旨在总结我所发现的内容,并作为指南来完成此操作。
因此,我将提供一个完整的示例,介绍如何激活BBB上的I2C总线以及如何使用内核中包含的设备驱动程序连接DS1308 RTC芯片。听起来很有趣吧?
接着请继续阅读,并在任何不清楚的地方留下评论。如果你比较匆忙,也可以直接从Github获取设备树叠加代码并开始尝试。
首先要做的事情。
我在我的BBB上使用ArchLinux ARM,主要是因为Arch Linux非常棒,而我可能太蠢了,无法使用类似Debian的发行版。 这是系统的screenfetch。 作为您可能已经注意到的,内核版本已经超过了3.x。在screenfetch中看不到的是,该内核支持使用Capemgr实用程序的设备树叠加。什么是设备树?
我会简单介绍一下,你可以在这里, 这里, 这里和这里找到更深入的知识。 设备树是描述平台上底层硬件的结构。它在嵌入式设备中被广泛使用,因为SOC等设备没有像PCI那样的总线可以发现设备。它们必须被静态定义,并附加到“平台总线”上,以便给内核附带的设备驱动程序提供一个句柄。
在设备树被引入Linux之前,所有这些工作都必须使用特定的C头文件和自定义实现来完成,然后将它们全部合并到主线内核中。因此,这是一项难以想象的繁琐任务,这就导致了著名的 Linus Torvalds rant。这里还有更多设备树背景知识。是的,很好,但它是如何工作的?
为了描述设备树,我们使用
.dts
(设备树源)文件,这些文件是人类可读的,并且由设备树编译器(dtc
)编译成设备树块(.dtb
),即二进制格式。当系统启动时,引导加载程序(例如u-boot)将该块交给内核。内核解析它并按照设备树所给出的方式创建所有设备。如果您不相信我,请使用设备树编译器查看您的BBB正在使用的设备树。
如果您还没有安装它,请获取相应的软件包。
pacman -Sy dtc-overlay
dtc -f -I fs /proc/device-tree | less
由于该命令生成大量输出,建议将管道连接至 less
分页器。结果应如下所示。
您的设备树的所有部分也可以在内核源代码中进行调查,但由于还有一个包含机制,因此信息分散在几个文件中。
<kernel-source>/arch/arm/boot/dts/..
一些相关文件是:
am335x-bone-common.dtsi
am335x-boneblack.dts
am33xx.dtsi
你提到了覆盖层,那是什么?
好问题,我看到你还在跟着我。正如我之前所说,内核引导时会解析设备树BLOB。因此,当您的系统运行起来时,整个魔术已经结束了。在像BBB这样具有许多扩展板(Capes)的平台上,这将要求您每次使用另一个cape时重新编译设备树。
因此,您可以使用覆盖机制,在运行时添加或修改设备树中的设备!太神奇了。
注意:要能够编译设备树覆盖层,请确保安装适当的软件包,如上面所述(dtc-overlay)。
那么我要如何使用这些内容呢?
我来举个例子。由于BBB没有实时时钟(rtc),因此无法为测量等生成时间戳,我们将解决这个问题。
我们将使用ds1307实时时钟芯片(事实上我有一个ds1308rtc,但驱动程序是兼容的),并通过BBB上的I2C1总线与其通信。默认情况下,该总线在BBB上是禁用的,正如您可以从设备树源代码中看到的那样。
那段代码中重要的信息是:- 定义了一个名为“i2c1”的节点
- 将其定义为兼容omap4-i2c驱动程序
- 根据处理器参考手册(第181页),该设备被分配了一个内存映射地址(0x4802a000)和适当的地址范围(0x1000)
- 该设备的状态为禁用
/dev
中创建rtc-device。在BBB上,P8和P9引脚上的GPIO引脚具有多个功能,这些功能被混合在一起,因此我们必须调整引脚复用设置以将它们用于I2C通信。如this table所示,对于I2C1总线,我们将不得不在mux模式2中使用头部引脚17和18。要获取有关BBB上GPIO处理的更多信息,请查看here。
/dts-v1/;
/plugin/;
/{ /* this is our device tree overlay root node */
compatible = "ti,beaglebone", "ti,beaglebone-black";
part-number = "BBB-I2C1"; // you can choose any name here but it should be memorable
version = "00A0";
fragment@0 {
target = <&am33xx_pinmux>; // this is a link to an already defined node in the device tree, so that node is overlayed with our modification
__overlay__ {
i2c1_pins: pinmux_i2c1_pins {
pinctrl-single,pins = <
0x158 0x72 /* spi0_d1.i2c1_sda */
0x15C 0x72 /* spi0_cs0.i2c1_sdl */
>;
};
};
};
}; /* root node end */
天啊,刚才发生了什么?
乍一看,叠加语法看起来相当奇怪,但它基本上由所谓的片段组成,这些片段针对已经存在的设备节点并修改该节点(及其子节点)。
在这种情况下,我们针对处理器设备树(am33xx.dtsi
)中定义的am33xx_pinmux
设备节点。在该节点内,我们添加一个名为pinmux_i2c1_pins的新子节点,该节点以前不存在(请查看am335x-bone-common.dtsi
进行验证),并且标签为i2c1_pins。
下一部分有点复杂,如果您感兴趣,请阅读this。每个GPIO引脚都由单个寄存器配置,其中有几个位用于控制其行为,并且所有寄存器都由pinctrl-single
驱动程序控制。要设置特定引脚,只需使用它相对于基地址的地址偏移量(您将在上面的P9标题表中找到它)和它的引脚配置作为第二个参数。
0x72
等同于01110010b
,因此我们将这两个引脚配置为带有启用上拉电阻和在mux模式2中激活缓冲控制的输入。
对于这些引脚,mux模式2表示P9头上的引脚17是时钟线SCL,引脚18是数据线SDA。
但我们仍然需要启用I2C1吗?
完全正确,因此让我们扩展我们的覆盖层如下..
/dts-v1/;
/plugin/;
/{ /* this is our device tree overlay root node */
compatible = "ti,beaglebone", "ti,beaglebone-black";
part-number = "BBB-I2C1"; // you can choose any name here but it should be memorable
version = "00A0";
fragment@0 {
target = <&am33xx_pinmux>; // this is a link to an already defined node in the device tree, so that node is overlayed with our modification
__overlay__ {
i2c1_pins: pinmux_i2c1_pins {
pinctrl-single,pins = <
0x158 0x72 /* spi0_d1.i2c1_sda */
0x15C 0x72 /* spi0_cs0.i2c1_sdl */
>;
};
};
};
fragment@1 {
target = <&i2c1>;
__overlay__ {
pinctrl-0 = <&i2c1_pins>;
clock-frequency = <100000>;
status = "okay";
rtc: rtc@68 { /* the real time clock defined as child of the i2c1 bus */
compatible = "dallas,ds1307";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x68>;
};
};
};
}; /* root node end */
在上面的代码中,我们添加了一个新的片段,针对i2c1设备节点,并告诉它使用我们之前定义的引脚配置。我们设置了100kHz的I2C时钟频率并激活了该设备。
此外,rtc时钟被添加为i2c1节点的子级。内核的重要信息是兼容语句,用于命名要使用的驱动程序(
ds1307
)和I2C总线上设备的地址(0x68
)。可以从数据表获取rtc的I2C地址。那么如何将该代码加入内核呢?
首先,必须编译设备树源代码。使用以下调用dtc编译器...
dtc -O dtb -o <filename>-00A0.dtbo -b 0 -@ <filename>.dts
注意!文件名必须是您想要的名称和版本标记(如上所示的-00A0)的连接,否则您将会遇到麻烦。 生成的.dtbo文件应复制到/lib/firmware中,但我真的不知道这个“-00A0”命名约定是从哪里来的,但固件目录中还有其他使用它的文件。 从现在开始,您可以通过使用Capemgr动态加载覆盖。为此,请移至/sys/devices/platform/bone_capemgr/,然后执行...
echo <filename> > slots
Capemgr会在固件目录中寻找您的
.dtbo
文件,并在可能的情况下加载它。通过查看slots文件,您可以看到该过程是否成功。它应该看起来像这样...
检查Beaglebone使用的设备树。dtc -f -I fs /proc/device-tree | less
你会在覆盖层中找到所有的条目。
此外,您的文件系统中应该有一个新的I2C设备(/dev/i2c-1
)和一个新的rtc设备(/dev/rtc1
)。要查看您的i2c总线,请安装
i2c-tools
软件包并使用...i2cdetect -r 1
输出应该类似于这样。
如您所见,地址0x68被某个设备占用。
要查询您的RTC,请使用以下方法..
hwclock -r -f /dev/rtc1
但这还不是全部,或者说是吗?
不,您还有一种选择,在启动时加载设备树叠加层。太棒了!
要这样做,请打开/boot/uEnv.txt
并将bone_capemgr.enable_partno=<filename>
添加到optargs
语句中。这是我BBB上的样子。
optargs=coherent_pool=1M bone_capemgr.enable_partno=bbb-i2c1
令人困惑的是,在设备树叠加中定义的part-number
标签没有在optargs中使用,而是使用了文件名。
如果您喜欢,可以在github上获取我的示例代码和有用的Makefile。
对于冗长的帖子感到抱歉。
./arch/arm/boot/dts
中找到它),并添加自定义配置。这是容易的部分。之后,您需要重新编译适用于您的板子的设备树,并替换旧的设备树二进制文件,通常附加在内核uimage或zimage文件中。最好的方法可能是为您的板子重新编译内核。在这样做的同时,您也可以将驱动程序嵌入内核中。问候 - IlikePepsi