Skip to content

Accessing Devices (e.g. I2C) from Inside the Container

sakaki edited this page Mar 28, 2020 · 1 revision

Easily enable access to the RPi's device interfaces, such as I2C, for the 64-bit guest OS!

Introduction

One of the nice features of systemd-nspawn is that it prevents processes running inside the container from performing a number of potentially harmful actions. For example, kernel modules may not be loaded, device nodes may not be created (and many existing device nodes are by default invisible), the host system cannot be rebooted etc.

These restrictions, however, can sometimes get in the way, for example if one wants to allow a 64-bit program to access the Pi's extensive I/O interfaces via their device nodes in /dev/.

Accordingly, in this short note I'll show how to enable access to system devices for the 64-bit environment. To keep things concrete, we'll look at how to make the Pi's I2C (inter integrated circuit) interface available to the pi user inside the container. However, the approach taken is general (and can easily be transposed to SPI etc).

Prerequisites

It's important you verify that you can access the target device interface (here, I2C) from within the regular, 32-bit Raspbian environment first, before trying to extend this to the 64-bit nspawn container.

For the case of I2C, begin by selecting the PreferencesRaspberry Pi Configuration tool, Interfaces tab, and making sure that the I2C radio button is enabled:

Ensuring I2C Interface is Enabled in Raspbian

Setting this ensures that the relevant kernel module, here i2c_bcm2835, is autoloaded; this has to be arranged in the 'host' system, because systemd-nspawn containers aren't allowed to modprobe (but do share the same kernel with the host). If your target device requires a kernel module whose loading isn't controlled by the RPi configuration tool, you can still make it autoload on boot, by simply adding the module name to /etc/modules instead.

That done, check you can view the I2C bus from within a (again regular, 32-bit) console. The (bundled) i2cdetect tool can be used for this; issue:

pi@raspberrypi:~ $ i2cdetect -y 1 
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                         

The output you get may differ from the above, if your RPi has devices on the I2C bus, but the the important thing is to check that a table is displayed.

Assuming that works you are good to proceed!

Gathering Device and Group Information from the Host

The next step is to work out the name of the relevant /dev/ node for your target device, its type, and any group used to access it.

In the case of I2C, you can use the i2cdetect tool again to find the node name. Issue:

pi@raspberrypi:~ $ i2cdetect -l
i2c-1	i2c       	bcm2835 I2C adapter             	I2C adapter

For other devices, you'll need to use appropriate tools to find the corresponding device node(s) under /dev/.

So we have one I2C adaptor available, with device node /dev/i2c-1.

Next (still working at the regular 32-bit terminal) we can find the (id and name of the) group with access to this node. Issue:

pi@raspberrypi:~ $ stat --format "%g %G" /dev/i2c-1
998 i2c

The above shows that the node's group is i2c, with numerical group id (gid) 998.

Next, let's see which users belong to the i2c group. Issue:

pi@raspberrypi:~ $ getent group i2c
i2c:x:998:pi

Here, the pi user is a member (which is why it was possible to use the i2cdetect -y 1 command as that user above, incidentally), and there are (currently) no other members.

You'll need to make the obvious modifications to these instructions if not using the pi user, of course.

Finally, let's discover the device node's type, major number and (device group) name:

pi@raspberrypi:~ $ cat /proc/devices
Character devices:
  1 mem
...
 89 i2c
...
254 gpiochip

Block devices:
  1 ramdisk
...
259 blkext

From the output, we can see that the device type is char (character), it's device group name is i2c, and it major device number is 89.

Your output may vary slightly from the above, depending on system type. If you are dealing with a different target device, the major number etc. will of course also be different.

Incidentally, the device group has (confusingly!) nothing to do with the group owning the device's /dev/ node (in this case, they happen to be textually identical (i.e., both are "i2c")).

We now have the necessary information to expose the I2C interface in the container, viz:

  • Device node: /dev/ic2-1
    • Owning group / gid: i2c / 998
  • Device info: device group name: i2c, major number: 98, type: char

Required Modifications to the Host System

By default, /dev/i2c-1 is not visible within the container. To make it so, we need to arrange for it to be bind mounted (much like /home is) at container startup.

This is achieved by editing the file /etc/systemd/nspawn/debian-buster-64.nspawn. Issue:

pi@raspberrypi:~ $ sudo nano -w /etc/systemd/nspawn/debian-buster-64.nspawn

and append the following line at the end of the [Files] section (you'll see other Bind entries there already):

Bind=/dev/i2c-1

Leave the rest of the file as-is. Save, and exit nano. /dev/i2c-1 will now appear inside the container's filesystem, as soon as the container is restarted (we'll do that shortly).

Next, we need to allow control group access to the i2c device group inside the container (as by default systemd-nspawn restricts all but a few essential devices, meaning that even where a device node is made visible, as we have just arranged, no processes — even those belonging to root — are able to use it).

To do so, we create a drop-in unit, augmenting the startup of the relevant container. Issue:

pi@raspberrypi:~ $ sudo systemctl edit systemd-nspawn@debian-buster-64

and in the (initially empty) file that opens, enter:

[Service]
DeviceAllow=char-i2c rwm

Save, and exit the editor.

Note: in the above, the char-i2c specifier is made up of the device class (here char = a character device) and device group name (here i2c), as determined earlier; the hyphen between them is a systemd convention. rwm means to allow read, write and create (mknod) control group access to devices belonging to this device group. See the systemd.resource-control manpage for further details.

If you already have a drop-in created for this target (and for avoidance of doubt, this won't apply to most readers), with a [Service] section, just append the DeviceAllow=char-i2c rwm there.

All the 'external' changes have now been made, so now we can restart the Debian Buster container to have them 'take', then make the final necessary changes from a 64-bit shell.

To restart the container, issue:

pi@raspberrypi:~ $ ds64-stop && ds64-start

Make sure you have saved any work in applications launched from the 64-bit container before issuing the above, as all processes within the container will be force-closed by ds64-stop.

Required Changes Inside the Guest Container

Once the container restarts, spawn a 64-bit shell within it; issue:

pi@raspberrypi:~ $ ds64-shell
pi@debian-buster-64:~ $ 

We next need to create the matchingi2c (account) group, with gid 998, to tally with the owning group of the bind-mounted /dev/i2c-1, and make sure the pi user within the container belongs to this group.

To create the group (with forced specific gid of 998), in the 64-bit container shell issue:

pi@debian-buster-64:~ $ sudo addgroup --gid 998 i2c

NB: if this command fails, it may be that you already have an i2c group present within the container, most likely as a result of having installed software packages that use this bus. In that case, issue sudo groupmod --gid 998 i2c to remap it.

Now add the pi user to that group:

pi@debian-buster-64:~ $ sudo usermod -a -G i2c pi

Obviously adapt as required, if you are not using the pi user, or are targeting a different device.

Check that this has all worked; issue:

pi@debian-buster-64:~ $ getent group i2c
i2c:x:998:pi

Looks good: we now have a group with name i2c gid 998 inside the container, mirroring that outside, and the pi user (inside) is a member (again, mirroring the situation outside).

That's the one-off preparation complete; conclude by exiting and re-entering the 64-bit shell, to have the group changes for pi taken up. Issue:

pi@debian-buster-64:~ $  exit
pi@raspberrypi:~ $ ds64-shell
pi@debian-buster-64:~ $ 

Testing

We can now test that the I2C interface can be accessed from within the container! To do so, let's install a (64-bit) version of i2cdetect there. This lives in the i2c-tools package in Debian Buster, so issue:

pi@debian-buster-64:~ $ sudo apt-get update
pi@debian-buster-64:~ $ sudo apt-get install -y i2c-tools

NB: you need to be careful not to install tools like this into the container prior to setting up the named account group (here, i2c), as they will often create this group if it does not exist; the problem being that the gid used in that case will most likely not match the desired numeric value (here, 998) from the host system.

Once that completes, you can test out access! Issue:

pi@debian-buster-64:~ $ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --                         

Your output may vary slightly from the above, if you have active devices on the I2C bus.

Assuming you see a table outputted as above, congratulations! You can now access I2C devices successfully (as the regular, pi user) from within the container.

The same approach may be used to easily add access to other common devices, such as SPI etc.

Have fun ^-^