How to add i2c devices on the Beaglebone Black using device tree overlays?

IlikePepsi picture IlikePepsi · Nov 5, 2015 · Viewed 17.5k times · Source

Why should I read this?

If you have a Beaglebone Black (BBB) and you want to wire up your own devices to it (not capes), you might already have heard about the device tree. In my case I wanted to connect a RTC device to the I2C bus on the BBB. There is lots of information scattered around the web and this article is meant to be a summary of what I found as also a guide to get it done.

So I'll give a full example of activating the I2C bus on the BBB as well as hooking up a DS1308 RTC chip using the device drivers included in the kernel. Sounds interesting?

Then read on and please leave comments if anything is not clear. If you are in a bit of a hurry you can also just grab the device tree overlay code on Github and fly away.

First things first.

I'm using ArchLinux ARM on my BBB mainly because Arch Linux is awesome and I am possibly too stupid to use debianoid distributions. Here's a screenfetch of the system..

ArchLinux screenfetch on BBB

As you might notice the kernel version is already above that 3.x stuff. What you can not see in the screenfetch is that the kernel supports device tree overlays using the Capemgr utility.

What is the device tree?

I'll just do it quick, you can find deeper knowledge here, here, here and here. The device tree is a structure describing the underlying hardware on your platform. It's heavily used in embedded devices since SOCs and stuff don't have buses like PCI where devices can be discovered. They have to be defined statically and are attached to a "platform bus" to give a handle to the device drivers shipped with the kernel.

Before the device tree was introduced to Linux all that work had to be done with specific C header files and custom implementations which then all had to be merged into the mainline kernel. Thus that being a imaginable exhaustive task it came to the famous Linus Torvalds rant. Here you go with still some more device tree background.

Yeah nice, but how does it work?

To describe the device tree we're using .dts (device tree source) files, which are human readable and get compiled by the device tree compiler (dtc) into device tree blobs (.dtb), a binary format. When the system boots the bootloader (e.g. u-boot) hands over that blob to the kernel. The kernel parses it and creates all the devices as given by the device tree.

If you're not believing me, use the device tree compiler to peak into the device tree your BBB is using right now.

If you haven't installed it yet, get the appropriate package..

pacman -Sy dtc-overlay
dtc -f -I fs /proc/device-tree | less

That pipe to the pager less is recommended due to a lot of output generated by that command. The result should look something like this..

enter image description here

All the parts of your device tree could also be investigated within the kernel source but since there is also an include mechanism the information is split among several files in

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

Some relevant files are:

  • am335x-bone-common.dtsi
  • am335x-boneblack.dts
  • am33xx.dtsi

Note: The .dtsi files are equivalent to .h files in C or C++ because they get included (therefore the 'i' at the end) by .dts files

They all describe devices related to the processor, common devices on the Beaglebone platform or devices only suitable on the Beaglebone Black.

You mentioned overlays, what's that?

Good question, I see you're still with me. As I said before, the device tree blob is parsed when the kernel boots. So when your system is up and running the whole magic is already over. On a platform like the BBB with a whole bunch of expansion boards (Capes) this would require you to recompile the device tree every time you go for another cape to use.

Therefore you have the overlay mechanism that allows you to add or modify devices in your device tree AT RUNTIME! Amazing.

Note: to be able to compile device tree overlays make sure to install the appropriate package like above (dtc-overlay)

And how am I going to use all this?

I'll give you an example. Since the BBB has no real time clock (rtc) which would be useful to generate timestamps for measurements etc., we are going to fix that.

We'll use a ds1307 real time clock chip (in fact I have a ds1308 rtc but the driver is compatible) and communicate with it over the I2C1 bus on the BBB. By default that bus is disabled on the BBB as you can see from the device tree sources..

i2c1 node in am33xx.dtsi

The important information in that snippet is:

  • a node named 'i2c1' is defined
  • it is defined as compatible to the omap4-i2c driver
  • the device gets assigned a memory mapped address (0x4802a000) and an appropriate address range (0x1000) according to the processors reference manual (page 181)
  • the devices status is disabled

Now we'll create an overlay to configure the GPIO pins for the i2c1 bus, activate that bus and afterwards we'll add the rtc-device i2c1 bus so that the appropriate driver is automatically loaded and the rtc-device gets created in /dev.

The GPIO pins on the P8 and P9 headers on the BBB have multiple functionalities which are muxed together and therefore we have to adjust the pinmux settings to use them for I2C communication. As you can see in this table for the I2C1 bus we'll have to use the header pins 17 and 18 in mux mode 2. To get more information on the GPIO handling on the BBB look 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 */

OMG what just happend?

At a first glance the overlay syntax looks quite weird but it's basically made of so called fragments that target an already existing device node and modify that node (and it's children).

In this case we target the am33xx_pinmux device node that is defined in the processors device tree (am33xx.dtsi). Within that node we add a new child node called pinmux_i2c1_pins which was not existent before (take a look at am335x-bone-common.dtsi to verify) and the label i2c1_pins.

The next part is a bit more complex and if you are interested read this. Every GPIO pin is configured by a single register with several bits to control it's behavior and all the registers are controlled by the pinctrl-single driver. To set a specific pin just use it's address offset from the base address (you will find that in the P9 header table above) and it's pin configuration as second parameter..

BBB gpio settings

I borrowed this overview from Derek Molloy to explain the pin mode. Since 0x72 is equivalent to 01110010b we have both pins configured as inputs with enabled pullup resistor and active slew control in mux mode 2.

And mux mode 2 for these pins means pin 17 on header P9 is the clock line SCL and pin 18 on header P9 is the data line SDA.

But we still have to enable I2C1?

That's absolutely correct so let's extend our overlay as follows..

/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 */

In the code above we added a new fragment that targets the i2c1 device node and tell's it to use our previously defined pin configuration. We set a I2C clock frequency of 100kHz and activate the device.

Furthermore the rtc clock was added as a child to the i2c1 node. The important information for the kernel is the compatible statement, naming the driver to use (ds1307) and the devices address on the I2C bus (0x68). The rtc's I2C address can be obtained from the datasheet.

And how do I get that code into the kernel?

At first the device tree source has to be compiled. Use the dtc compiler with the following call..

dtc -O dtb -o <filename>-00A0.dtbo -b 0 -@ <filename>.dts

Caution! The filename must be a concatenation of the name you desire plus the version tag as seen above (-00A0) otherwise you'll have a hard time.

The resulting .dtbo file should be copied into /lib/firmware and I really have no idea where that "-00A0" naming convention comes from but there are other files in the firmware directory using it as well.

From now on you can load your overlay dynamically by using Capemgr. To do so move into /sys/devices/platform/bone_capemgr/ and then execute..

echo <filename> > slots

Capemgr will then look for your .dtbo file in the firmware directory and load it if possible. By looking into the slots file you can see if the procedure was successful. It should look something like this..

enter image description here

Examine the device tree used by the Beaglebone.

dtc -f -I fs /proc/device-tree | less

You'll find all the entries from the overlay..

enter image description here

enter image description here

Furthermore there should be a new I2C device (/dev/i2c-1) and a new rtc device (/dev/rtc1) in your filesystem.

To have a look on your i2c buses install the package i2c-tools and use..

i2cdetect -r 1

Output should be something like this..

enter image description here

As you can see address 0x68 is occupied by a device.

To query your rtc use..

hwclock -r -f /dev/rtc1

But that's not all, or is it?

No, there is one more option you have, loading device tree overlays at boot. AWESOME!

To do so open /boot/uEnv.txt and add bone_capemgr.enable_partno=<filename> to the optargs statement. That's what it looks on my BBB

optargs=coherent_pool=1M bone_capemgr.enable_partno=bbb-i2c1

Confusingly the filename is used in optargs not the part-number tag defined in the device tree overlay.

You can get my example code aside a useful Makefile on github if you like.

Sorry for long post.

Answer

Bwani picture Bwani · Nov 24, 2016

This is very helpful and valuable information. I wrote an i2c kernel driver which I can load dynamically to talk to a custom chip at address 0x77. I have been successful in communicating with the chip in the past by instantiating the device manually as follows :echo act2_chip 0x77 > /sys/bus/i2c/devices/i2c-1/new_device. After the device is instantiated, I can see it using i2cdetect tools and my loadable kernel driver can communicate with the chip.

Now I am trying to instantiate the device using the device tree method. So following your lead, I changed some parameters in your dtsi file like below:

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>;
      };

I connected the chip at pins 17 and 18 for scl and sda. Here is the dmesg output I get after echo > slots : image

But while inserting the driver into the kernel, I see the probe function being called. this means the driver is able to see the device as far as I think.

And when I try to write to the kernel driver, I get the following message : omap_i2c 4802a000.i2c: controller timed out