Search by Tags

How to use I2C on Torizon

 
Applicable for

Tags

Article updated at 17 Sep 2020
Compare with Revision

Subscribe for this article updates

Select the version of Torizon from the tabs below. If you don't know the version you are using, run the command cat /etc/os-release on the board.

Torizon 5.0.0

Introduction

I2C is a common interface for a variety of hardware peripherals, as such it helps to know how to integrate I2C devices with Torizon. It should be noted that Torizon at its core is a Linux operating system, as such some of the information here is general Linux knowledge.

What this article will ultimately help with is how to give containers access to I2C devices in Torizon. This article will cover the two general ways to access peripherals. The method depends on your specific needs and the software support of your specific I2C device:

This article complies to the Typographic Conventions for Torizon Documentation.

Prerequisites

In order to take full advantage of this article, the following read is recommended:

I2C With User-space Drivers

User-space Introduction

The idea with user-space is to access and manipulate the I2C interface from your application code. This means you will usually work with the generic default I2C-dev interface provided by the kernel via /dev.

Since this approach is dependent on one's application, this means exact steps will depend on whatever language/framework is used. However in the next section we will show a general example using python.

Before the example here are some points to keep in mind when working in user-space:

  • With user-space you take on the burden of the device's behavior. This means writing code to read, write and interpret data from your peripheral.
  • This will lead to initial development overhead, however if your device lacks proper software support it may be your only option.

That being said there are some positives to this approach:

  • By virtue of writing your own code it should be easier to debug issues compared to some outside driver.
  • Also you can define and add your functionality to better serve your application.

User-space Example

  • I2C device being used: SHT31 sensor.
  • Dummy Python application to show the concept.
  • Python module smbus2 for providing I2C interfacing capabilities.
    • This library is language specific. Do some research on what your chosen language's equivalent is.

Warning: Keep in mind that you'll need to adapt this for whatever device and language/framework you are using for your project.

The purpose of the simplified version is purely explanatory for the sake of this article:

#!python
import smbus2
import time
 
# Set the bus number to 0
# Change 0 to bus number where SHT31 is connected
bus = smbus2.SMBus(0)
 
while True:
    # SHT31 address, measurement command, repeatability
    # Address, usually 0x44(68), is configurable to 0x45(69) by pulling up ADDR pin
    # Single shot mode measurement command with clock stretching enabled 0x2C(44)
    # High repeatability measurement 0x06(06)   
    bus.write_i2c_block_data(0x44, 0x2C, [0x06])
 
    time.sleep(0.5)
 
    # SHT31 address, register address, number of bytes to read
    # Read from 0x00(00), this is ignored by the sensor, still need to specify for SMBus
    # Read 6 bytes: Temp MSB, Temp LSB, Temp CRC, Humididty MSB, Humidity LSB, Humidity CRC
    data = bus.read_i2c_block_data(0x44, 0x00, 6)
 
    # Process data received from the sensor
    temp = data[0] * 256 + data[1]
    cTemp = -45 + (175 * temp / 65535.0)
    fTemp = -49 + (315 * temp / 65535.0)
    humidity = 100 * (data[3] * 256 + data[4]) / 65535.0
 
    # Print processed data to stdout
    print ("Temperature in Celsius is : %.2f C" %cTemp)
    print ("Temperature in Fahrenheit is : %.2f F" %fTemp)
    print ("Relative Humidity is : %.2f %%RH\n" %humidity)

As you can see from the application it simply reads values from the sensor, and calculates real-world numbers for temperature and humidity. Even though we use smbus2 for I2C interfacing, we still had to create code to read, write, and process the values from the device. We also had to know information such as I2C address, register address, and how to interpret values from the sensor. These are all common things that must be figured out when working with I2C devices in user-space.

User-space Container Access

Now for Torizon we must overcome an additional issue of how to provide a container access to I2C devices. With the default I2C-dev interface the system requires root privileges in order to access entries in /dev. This restriction also applies to containers.

Fortunately with containers we can grant them additional access and permissions at runtime in a couple of ways.

  • Adding --privileged flag to any docker run command gives the container full root access.
    • Includes any devices that were bind mounted in via -v /dev:/dev.
    • Should not be used in production, a container with full root access is insecure.
  • Adding --device flag to any docker run command gives container access to a specific device.
    • For example --device /dev/i2c-0 will give access to I2C bus 0 only.
    • Still requires permissions within the container to access I2C devices (More info below).

In Torizon we made an i2cdev group with id 51 to allow the users in this group to access I2C devices without root privileges. A similar group was added to Toradex's Debian base container (see here) so that non root users in this group may also enjoy the same benefit.

Knowing this when building a container image based off of a Toradex Debian container image you need just add the following to your Dockerfile.

  • RUN usermod -a -G i2cdev torizon
    • If building a container from scratch additionally add RUN groupadd --gid 51 i2cdev

The command will add the Torizon user to the i2cdev group allowing access to I2C devices in /dev without root privileges. Now you just need run your container with the appropriate --device flag.

I2C With Kernel-space Drivers

Kernel-space Introduction

As for the other method of integrating I2C devices we now move to kernel-space drivers. The idea here is to leverage device specific driver software that is either builtin or loaded into the Linux kernel as a kernel module.

Compared to user-space access:

  • Software comes from open-source community or device manufacturer.
  • Kernel-space software may have little to no documentation.
    • This can lead to more difficult issues when it comes to debugging.
  • Driver software is a part of the Kernel system rather than the application code.
    • Errors/issues with the driver can affect the entire system.
  • Method is not available if kernel-space software doesn't exist.
  • You have to write a Device Tree Overlay, which may not be trivial for newcomers to Linux.

Kernel-space Example

  • Same SHT31 sensor as before.
  • First conduct some research if kernel-space software is available.
    • Driver for SHT31 sensor found in mainline kernel here.

Now that we've confirmed a kernel-space approach is possible, next we must find out if this software is even included in Torizon's kernel. If your device's driver software is part of the mainline Linux Kernel like the SHT31 sensor you can check this in a couple of steps.

  • Find driver's corresponding config variable via kernel source code or the "Linux Kernel Driver Database".
  • Config variable found to be CONFIG_SENSORS_SHT3x as seen here.
  • Search if this config variable is part of your Torizon system. Run on the computer on module:
# zcat /proc/config.gz | grep CONFIG_SENSORS_SHT3x
CONFIG_SENSORS_SHT3x=m

This shows that this config is part of this specific Torizon build. However the "m" shows that the driver was built in as a loadable module rather than just builtin and active by default.

Referencing the "Linux Kernel Driver Database" again we see that if built as a module the driver will be named sht3x. Using the name we can simply load the module into the running system like so.

# modprobe sht3x

Depending on your device it might just work now. Though it is often the case the driver may need some additional information. Also the issue with this method is that is is not persistent, meaning the driver must be loaded on each boot. This is not very ideal for a production system.

In order to pass required information to the driver and have this configuration be persistent, it is often the case to do so via the device tree. For more information on what a device tree is and some general tips on customizing one please see the article Device Tree Customization.

Additionally with Torizon we will also leverage something called "device tree overlays" in order to make this customization without recompiling the whole kernel. For more information on device tree overlays and the Torizon tooling for it please see the article Device Tree Overlays.

Now that we've introduced those concepts see below the sample device tree overlay.

/dts-v1/;
/plugin/;
 
/ {
	compatible = "toradex,apalis_imx6q"; //choose board in use
	fragment@0 {
		target = <&i2c1>; // select interface as per board
		__overlay__ {
			#address-cells = <1>;
			#size-cells = <0>;
			status = "okay";
 
			sht3x: sht3x@44 {
				compatible = "sensirion,sht3x";
				reg = <0x44>; // address of the sensor
				status = "okay";
			};
		};
	};
};

This overlay can be applied using the dtconf tool as explained in the device tree overlay article.

We reference the i2c1 interface of the Apalis i.MX6 module and add an additional entry representing our sensor. For this device the syntax and information passed was rather simple, only requiring a compatible string which references the driver and an I2C address. For other devices you may need to find additional documentation or examples on exact syntax.

If all goes well the driver should create the following files/directories on the Torizon system:

  • Creation of a device entry in /sys/bus/i2c/devices/*, it is 0-0044 in the presented case.
  • Data from sensor exposed at /sys/class/hwmon/hwmon<x>, it is exposed at hwmon1 in the presented case.
  • Within the hwmon<x> directory temperature and humidity values written in temp1_input and humidity1_input files respectively

As you can see compared to user-space we now have the data from the sensor given to us by the driver, rather than having to code this behavior ourselves. Since the information is available via the filesystem entries your application just now needs to read files to get the values.

Kernel-space Container Access

Here's where a kernel-space approach pays off in terms of simplicity. A container while by definition is a contained sub-system from the host, it does inherit some aspects from the host. Most importantly it inherits many aspects of the host's kernel.

What this ends up meaning is that the same data can also be accessed within a container without giving the --privileged option. So for the above example you should be able to access the sensor's data via the same /sys entries from within a container.

Torizon 4.0.0

Introduction

I2C is a common interface for a variety of hardware peripherals, as such it helps to know how to integrate I2C devices with Torizon. It should be noted that Torizon at its core is a Linux operating system, as such some of the information here is general Linux knowledge.

What this article will ultimately help with is how to give containers access to I2C devices in Torizon. This article will cover the two general ways to access peripherals. The method depends on your specific needs and the software support of your specific I2C device:

This article complies to the Typographic Conventions for Torizon Documentation.

Prerequisites

In order to take full advantage of this article, the following read is recommended:

I2C With User-space Drivers

User-space Introduction

The idea with user-space is to access and manipulate the I2C interface from your application code. This means you will usually work with the generic default I2C-dev interface provided by the kernel via /dev.

Since this approach is dependent on one's application, this means exact steps will depend on whatever language/framework is used. However in the next section we will show a general example using python.

Before the example here are some points to keep in mind when working in user-space:

  • With user-space you take on the burden of the device's behavior. This means writing code to read, write and interpret data from your peripheral.
  • This will lead to initial development overhead, however if your device lacks proper software support it may be your only option.

That being said there are some positives to this approach:

  • By virtue of writing your own code it should be easier to debug issues compared to some outside driver.
  • Also you can define and add your functionality to better serve your application.

User-space Example

  • I2C device being used: SHT31 sensor.
  • Dummy Python application to show the concept.
  • Python module smbus2 for providing I2C interfacing capabilities.
    • This library is language specific. Do some research on what your chosen language's equivalent is.

Warning: Keep in mind that you'll need to adapt this for whatever device and language/framework you are using for your project.

The purpose of the simplified version is purely explanatory for the sake of this article:

#!python
import smbus2
import time
 
# Set the bus number to 0
# Change 0 to bus number where SHT31 is connected
bus = smbus2.SMBus(0)
 
while True:
    # SHT31 address, measurement command, repeatability
    # Address, usually 0x44(68), is configurable to 0x45(69) by pulling up ADDR pin
    # Single shot mode measurement command with clock stretching enabled 0x2C(44)
    # High repeatability measurement 0x06(06)   
    bus.write_i2c_block_data(0x44, 0x2C, [0x06])
 
    time.sleep(0.5)
 
    # SHT31 address, register address, number of bytes to read
    # Read from 0x00(00), this is ignored by the sensor, still need to specify for SMBus
    # Read 6 bytes: Temp MSB, Temp LSB, Temp CRC, Humididty MSB, Humidity LSB, Humidity CRC
    data = bus.read_i2c_block_data(0x44, 0x00, 6)
 
    # Process data received from the sensor
    temp = data[0] * 256 + data[1]
    cTemp = -45 + (175 * temp / 65535.0)
    fTemp = -49 + (315 * temp / 65535.0)
    humidity = 100 * (data[3] * 256 + data[4]) / 65535.0
 
    # Print processed data to stdout
    print ("Temperature in Celsius is : %.2f C" %cTemp)
    print ("Temperature in Fahrenheit is : %.2f F" %fTemp)
    print ("Relative Humidity is : %.2f %%RH\n" %humidity)

As you can see from the application it simply reads values from the sensor, and calculates real-world numbers for temperature and humidity. Even though we use smbus2 for I2C interfacing, we still had to create code to read, write, and process the values from the device. We also had to know information such as I2C address, register address, and how to interpret values from the sensor. These are all common things that must be figured out when working with I2C devices in user-space.

User-space Container Access

Now for Torizon we must overcome an additional issue of how to provide a container access to I2C devices. With the default I2C-dev interface the system requires root privileges in order to access entries in /dev. This restriction also applies to containers.

Fortunately with containers we can grant them additional access and permissions at runtime in a couple of ways.

  • Adding --privileged flag to any docker run command gives the container full root access.
    • Includes any devices that were bind mounted in via -v /dev:/dev.
    • Should not be used in production, a container with full root access is insecure.
  • Adding --device flag to any docker run command gives container access to a specific device.
    • For example --device /dev/i2c-0 will give access to I2C bus 0 only.
    • Still requires permissions within the container to access I2C devices (More info below).

In Torizon we made an i2cdev group with id 51 to allow the users in this group to access I2C devices without root privileges. A similar group was added to Toradex's Debian base container (see here) so that non root users in this group may also enjoy the same benefit.

Knowing this when building a container image based off of a Toradex Debian container image you need just add the following to your Dockerfile.

  • RUN usermod -a -G i2cdev torizon
    • If building a container from scratch additionally add RUN groupadd --gid 51 i2cdev

The command will add the Torizon user to the i2cdev group allowing access to I2C devices in /dev without root privileges. Now you just need run your container with the appropriate --device flag.

I2C With Kernel-space Drivers

Kernel-space Introduction

As for the other method of integrating I2C devices we now move to kernel-space drivers. The idea here is to leverage device specific driver software that is either builtin or loaded into the Linux kernel as a kernel module.

Compared to user-space access:

  • Software comes from open-source community or device manufacturer.
  • Kernel-space software may have little to no documentation.
    • This can lead to more difficult issues when it comes to debugging.
  • Driver software is a part of the Kernel system rather than the application code.
    • Errors/issues with the driver can affect the entire system.
  • Method is not available if kernel-space software doesn't exist.
  • You have to write a Device Tree Overlay, which may not be trivial for newcomers to Linux.

Kernel-space Example

  • Same SHT31 sensor as before.
  • First conduct some research if kernel-space software is available.
    • Driver for SHT31 sensor found in mainline kernel here.

Now that we've confirmed a kernel-space approach is possible, next we must find out if this software is even included in Torizon's kernel. If your device's driver software is part of the mainline Linux Kernel like the SHT31 sensor you can check this in a couple of steps.

  • Find driver's corresponding config variable via kernel source code or the "Linux Kernel Driver Database".
  • Config variable found to be CONFIG_SENSORS_SHT3x as seen here.
  • Search if this config variable is part of your Torizon system. Run on the computer on module:
# zcat /proc/config.gz | grep CONFIG_SENSORS_SHT3x
CONFIG_SENSORS_SHT3x=m

This shows that this config is part of this specific Torizon build. However the "m" shows that the driver was built in as a loadable module rather than just builtin and active by default.

Referencing the "Linux Kernel Driver Database" again we see that if built as a module the driver will be named sht3x. Using the name we can simply load the module into the running system like so.

# modprobe sht3x

Depending on your device it might just work now. Though it is often the case the driver may need some additional information. Also the issue with this method is that is is not persistent, meaning the driver must be loaded on each boot. This is not very ideal for a production system.

In order to pass required information to the driver and have this configuration be persistent, it is often the case to do so via the device tree. For more information on what a device tree is and some general tips on customizing one please see the article Device Tree Customization.

Additionally with Torizon we will also leverage something called "device tree overlays" in order to make this customization without recompiling the whole kernel. For more information on device tree overlays and the Torizon tooling for it please see the article Device Tree Overlays.

Now that we've introduced those concepts see below the sample device tree overlay.

/dts-v1/;
/plugin/;
 
/ {
	compatible = "toradex,apalis_imx6q"; //choose board in use
	fragment@0 {
		target = <&i2c1>; // select interface as per board
		__overlay__ {
			#address-cells = <1>;
			#size-cells = <0>;
			status = "okay";
 
			sht3x: sht3x@44 {
				compatible = "sensirion,sht3x";
				reg = <0x44>; // address of the sensor
				status = "okay";
			};
		};
	};
};

This overlay can be applied using the dtconf tool as explained in the device tree overlay article.

We reference the i2c1 interface of the Apalis i.MX6 module and add an additional entry representing our sensor. For this device the syntax and information passed was rather simple, only requiring a compatible string which references the driver and an I2C address. For other devices you may need to find additional documentation or examples on exact syntax.

If all goes well the driver should create the following files/directories on the Torizon system:

  • Creation of a device entry in /sys/bus/i2c/devices/*, it is 0-0044 in the presented case.
  • Data from sensor exposed at /sys/class/hwmon/hwmon<x>, it is exposed at hwmon1 in the presented case.
  • Within the hwmon<x> directory temperature and humidity values written in temp1_input and humidity1_input files respectively

As you can see compared to user-space we now have the data from the sensor given to us by the driver, rather than having to code this behavior ourselves. Since the information is available via the filesystem entries your application just now needs to read files to get the values.

Kernel-space Container Access

Here's where a kernel-space approach pays off in terms of simplicity. A container while by definition is a contained sub-system from the host, it does inherit some aspects from the host. Most importantly it inherits many aspects of the host's kernel.

What this ends up meaning is that the same data can also be accessed within a container without giving the --privileged option. So for the above example you should be able to access the sensor's data via the same /sys entries from within a container.