Select the version of your OS from the tabs below. If you don't know the version you are using, run the command cat /etc/os-release
or cat /etc/issue
on the board.
Remember that you can always refer to the Torizon Documentation, there you can find a lot of relevant articles that might help you in the application development.
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 how to install the I2C Tools and 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.
In order to take full advantage of this article, the following read is recommended:
Learn more about the tool and usage on I2C (Linux) - I2C Tools.
You can install the package i2c-tools from the Debian feeds by executing the following command in your Dockerfile:
RUN apt-get -y update && apt-get install -y \ i2c-tools \ && apt-get clean && apt-get autoremove && rm -rf /var/lib/apt/lists/*
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:
That being said there are some positives to this approach:
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:
Warning: On the example below, change the bus = smbus2.SMBus('/dev/verdin-i2c1')
line accordingly to your SoM.
#!python import smbus2 import time # Set the bus number to 0 # Change 0 to bus number where SHT31 is connected bus = smbus2.SMBus('/dev/apalis-i2c1') 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.
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.
--privileged
flag to any docker run
command gives the container full root access.
-v /dev:/dev
.--device
flag to any docker run
command gives container access to a specific device.
--device /dev/i2c-0
will give access to I2C bus 0 only.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
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.
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:
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.
CONFIG_SENSORS_SHT3x
as seen here.# 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:
/sys/bus/i2c/devices/*
, it is 0-0044
in the presented case./sys/class/hwmon/hwmon<x>
, it is exposed at hwmon1 in the presented case.hwmon<x>
directory temperature and humidity values written in temp1_input
and humidity1_input
files respectivelyAs 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.
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.
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.
In order to take full advantage of this article, the following read is recommended:
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:
That being said there are some positives to this approach:
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.
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.
--privileged
flag to any docker run
command gives the container full root access.
-v /dev:/dev
.--device
flag to any docker run
command gives container access to a specific device.
--device /dev/i2c-0
will give access to I2C bus 0 only.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
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.
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:
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.
CONFIG_SENSORS_SHT3x
as seen here.# 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:
/sys/bus/i2c/devices/*
, it is 0-0044
in the presented case./sys/class/hwmon/hwmon<x>
, it is exposed at hwmon1 in the presented case.hwmon<x>
directory temperature and humidity values written in temp1_input
and humidity1_input
files respectivelyAs 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.
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.