如何使用设备树叠加层在Beaglebone Black上添加i2c设备?

30

为什么要阅读这篇文章?

如果你拥有一块贝莱坞黑色版(Beaglebone Black,BBB)并且想要将自己的设备连接到它上面(而不是使用cape),那么你可能已经听说过设备树。在我的情况下,我想将一个RTC设备连接到BBB上的I2C总线上。网络上有很多零散的信息,本文旨在总结我所发现的内容,并作为指南来完成此操作。

因此,我将提供一个完整的示例,介绍如何激活BBB上的I2C总线以及如何使用内核中包含的设备驱动程序连接DS1308 RTC芯片。听起来很有趣吧?

接着请继续阅读,并在任何不清楚的地方留下评论。如果你比较匆忙,也可以直接从Github获取设备树叠加代码并开始尝试。

首先要做的事情。

我在我的BBB上使用ArchLinux ARM,主要是因为Arch Linux非常棒,而我可能太蠢了,无法使用类似Debian的发行版。 这是系统的screenfetch

ArchLinux screenfetch on BBB

作为您可能已经注意到的,内核版本已经超过了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 分页器。结果应如下所示。

enter image description here

您的设备树的所有部分也可以在内核源代码中进行调查,但由于还有一个包含机制,因此信息分散在几个文件中。

 <kernel-source>/arch/arm/boot/dts/.. 

一些相关文件是:
  • am335x-bone-common.dtsi
  • am335x-boneblack.dts
  • am33xx.dtsi
注意:.dtsi文件相当于C或C++中的.h文件,因为它们被.dts文件包含(因此以“i”结尾),并描述与处理器相关的设备,Beaglebone平台上的常见设备或仅适用于Beaglebone Black的设备。
你提到了覆盖层,那是什么?
好问题,我看到你还在跟着我。正如我之前所说,内核引导时会解析设备树BLOB。因此,当您的系统运行起来时,整个魔术已经结束了。在像BBB这样具有许多扩展板(Capes)的平台上,这将要求您每次使用另一个cape时重新编译设备树。
因此,您可以使用覆盖机制,在运行时添加或修改设备树中的设备!太神奇了。
注意:要能够编译设备树覆盖层,请确保安装适当的软件包,如上面所述(dtc-overlay)。

那么我要如何使用这些内容呢?

我来举个例子。由于BBB没有实时时钟(rtc),因此无法为测量等生成时间戳,我们将解决这个问题。

我们将使用ds1307实时时钟芯片(事实上我有一个ds1308rtc,但驱动程序是兼容的),并通过BBB上的I2C1总线与其通信。默认情况下,该总线在BBB上是禁用的,正如您可以从设备树源代码中看到的那样。

i2c1 node in am33xx.dtsi

那段代码中重要的信息是:
  • 定义了一个名为“i2c1”的节点
  • 将其定义为兼容omap4-i2c驱动程序
  • 根据处理器参考手册(第181页),该设备被分配了一个内存映射地址(0x4802a000)和适当的地址范围(0x1000)
  • 该设备的状态为禁用
现在我们将创建一个覆盖层来配置i2c1总线的GPIO引脚,激活该总线,然后添加rtc-device i2c1总线,以便自动加载适当的驱动程序并在/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标题表中找到它)和它的引脚配置作为第二个参数。

BBB gpio settings

我从Derek Molloy那里借用了这个概述来解释引脚模式。由于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文件,您可以看到该过程是否成功。它应该看起来像这样...

enter image description here

检查Beaglebone使用的设备树。
dtc -f -I fs /proc/device-tree | less

你会在覆盖层中找到所有的条目。

enter image description here

enter image description here

此外,您的文件系统中应该有一个新的I2C设备(/dev/i2c-1)和一个新的rtc设备(/dev/rtc1)。
要查看您的i2c总线,请安装i2c-tools软件包并使用...
i2cdetect -r 1

输出应该类似于这样。

enter image description here

如您所见,地址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。

对于冗长的帖子感到抱歉。


这是非常棒的解释!你能否发布更多有关如何接口GPIO设备的细节呢?比如说需要5个GPIO?另外,如果你要为你的设备编写一个绑定,比如RTC,它会是什么样子?你将如何插入它并进行构建/测试?我知道你对这个问题的答案非常了解!!真的很期待你的回复! - Raulp
简而言之,如果您必须使用设备树而不是覆盖层来运行驱动程序 - Raulp
1
如果您不想使用覆盖机制,您需要从内核源代码中获取设备树源代码(在内核源代码目录中,您可以在./arch/arm/boot/dts中找到它),并添加自定义配置。这是容易的部分。之后,您需要重新编译适用于您的板子的设备树,并替换旧的设备树二进制文件,通常附加在内核uimage或zimage文件中。最好的方法可能是为您的板子重新编译内核。在这样做的同时,您也可以将驱动程序嵌入内核中。问候 - IlikePepsi
1
@Raulp 关于您的GPIO请求,请看一下这个帖子,也许您可以提取一些有用的信息。 - IlikePepsi
@guru_florida 我已经在那个部分添加了一个小警告,指出命名问题。感谢你的提示。 - IlikePepsi
显示剩余2条评论
1个回答

2
这是非常有用和有价值的信息。我编写了一个i2c内核驱动程序,可以动态加载它以与地址0x77的自定义芯片通信。过去,我通过手动实例化设备来成功地与芯片通信,如下所示:echo act2_chip 0x77 > /sys/bus/i2c/devices/i2c-1/new_device。 设备实例化后,我可以使用i2cdetect工具看到它,并且我的可加载内核驱动程序可以与芯片通信。
现在,我正在尝试使用设备树方法实例化设备。因此,按照您的建议,我更改了dtsi文件中的一些参数,如下所示:
fragment@1 {
    target = <&i2c1>;

    __overlay__ {
      pinctrl-0 = <&i2c1_pins>;

      clock-frequency = <100000>;
      status = "okay";

      act2_chip: act2_chip@77 { /* the real time clock defined as child of the i2c1 bus */
        compatible = "xx,act2_chip";
        #address-cells = <1>;
        #size-cells = <0>;
        reg = <0x77>;
      };

我将芯片连接到了scl和sda的17号和18号引脚。在执行echo > slots命令后,这是我所得到的dmesg输出:image 但是,在将驱动程序插入内核时,我发现探测函数被调用了。这意味着,据我所知,驱动程序能够看到设备。
当我尝试向内核驱动程序写入时,我收到以下消息:omap_i2c 4802a000.i2c: 控制器超时。

请确认特定的引脚没有被其他驱动程序占用。看起来I2C驱动程序的引脚请求失败了。为此,在加载驱动程序之前,请查看位于“/sys/kernel/debug/pinctrl/..”中的文件。我现在手头没有Beaglebone,无法告诉您确切的路径,但有些文件可以显示哪个驱动程序占用了哪些引脚。然后,您需要卸载该驱动程序并重试。 - IlikePepsi
好的,我找到了路径。它是/sys/kernel/debug/pinctrl/44e10800.pinmux。有几个文件,你应该查看pimux-pinspins以获取有关引脚状态和占用引脚的驱动程序的信息。 - IlikePepsi
这些引脚没有被其他任何东西使用。我试图把输出复制到这里,但被版主删除了。一个问题:由于你正在注册的设备已经在内核中有一个驱动程序,我猜测这是rtc-ds1307.c,那么我需要将我的驱动程序静态编译到内核中,还是insmod就可以工作了?另外看一下你提供的git代码,你将两个udev规则复制到/etc/udev/rules.d/。这样做的目的是什么?我没有这样做。而且17号和18号引脚上有拉高电阻吗? - Bwani
不,您不需要将其编译到内核中。将驱动程序作为内核模块加载即可。I2C文件夹中的udev规则只是告诉系统将生成的i2c设备的权限设置为“0666”。因此,您无需成为root用户或使用sudo来访问i2c总线。rtc的udev规则几乎相同。访问权限已更改,但此外还有一个名为“rtc”的设备文件,默认情况下链接到“rtc0”(即CPU中的rtc时钟),我只是将链接弯曲到新创建的“rtc1”文件,这是i2c-rtc。 - IlikePepsi
所有GPIO引脚默认情况下应使用上拉电阻作为保护措施,但您可以在内核模块代码中更改它。您可以使用内核函数来操作引脚配置(尽管我有时会发现它不像预期的那样工作),或者直接访问引脚配置寄存器并操作相应的位。我建议您查看Derek Molloys页面,深入了解GPIO编程。祝好! - IlikePepsi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接