-
Notifications
You must be signed in to change notification settings - Fork 24
Accessing Devices (e.g. I2C) from Inside the Container
Easily enable access to the RPi's device interfaces, such as I2C, for the 64-bit guest OS!
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).
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 Preferences→Raspberry Pi Configuration tool, Interfaces tab, and making sure that the I2C
radio button is enabled:
Setting this ensures that the relevant kernel module, here
i2c_bcm2835
, is autoloaded; this has to be arranged in the 'host' system, becausesystemd-nspawn
containers aren't allowed tomodprobe
(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!
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
- Owning group / gid:
- Device info: device group name:
i2c
, major number:98
, type:char
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 (herechar
= a character device) and device group name (herei2c
), as determined earlier; the hyphen between them is asystemd
convention.rwm
means to allow read, write and create (mknod
) control group access to devices belonging to this device group. See thesystemd.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 theDeviceAllow=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
.
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, issuesudo 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:~ $
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 thegid
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 ^-^
Wiki content license: Creative Commons Attribution-ShareAlike 4.0 International License