Skip to content

Commit

Permalink
Additional documentation for open_path method.
Browse files Browse the repository at this point in the history
Resolves doceme#129
  • Loading branch information
tim-seoss committed Apr 24, 2023
1 parent a94c858 commit 63eeee0
Show file tree
Hide file tree
Showing 2 changed files with 257 additions and 0 deletions.
159 changes: 159 additions & 0 deletions 99-local-spi-example-udev.rules
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# An example udev rules file for the python spidev extension:
#
# https://pypi.org/project/spidev/

# This file gives examples for setting permissions on spidev device nodes, and
# creating symbolic links which allow consistent and/or logical device naming.
#
# This file would typically be adapted to fit a specific use-case, renamed, and
# then installed in the /etc/udev/rules.d/ directory.
#
# For more information on writing udev rules files, see:
#
# - The udev(7) man page.
# - Tutorial: https://www.reactivated.net/writing_udev_rules.html
# - Tutorial: https://linuxconfig.org/tutorial-on-how-to-write-basic-udev-rules-in-linux
# - Tutorial: https://wiki.archlinux.org/title/udev

# The udev rules which ship with your distribution may also provide useful
# examples. On most Linux distributions, these are located in:
# /lib/udev/rules.d/ (local custom rules should be installed in
# /etc/udev/rules.d instead - as noted above).

# An example of customising the permissions on all spidev files on the system -
# giving spidev file system device nodes group ownership by the `spi` group and
# granting read/write permission to user processes in the `spi` group.
#
# To use this, the group `spi` must exist on the system e.g.
#
# groupadd --system spi
#
#KERNEL=="spidev*", GROUP="spi", MODE="0660"

# Bind spidev driver to chip select 0 of the Beaglebone Black "McSPI0" SPI
# controller. This is needed for kernels > ~6.0+ - which do not automatically
# bind spidev based on the device tree 'compatible' property..
ACTION=="add", DEVPATH=="*/ocp/*48030000\.spi/spi*\.0", RUN+="/bin/sh -c 'SPI_TARGET_DEVICE=\"$$( basename $$DEVPATH )\" ; echo spidev > /sys/bus/spi/devices/$${SPI_TARGET_DEVICE}/driver_override ; echo $SPI_TARGET_DEVICE > /sys/bus/spi/drivers/spidev/bind'"

# As above, but for McSPI1, CS0.
ACTION=="add", DEVPATH=="*/ocp/*481a0000\.spi/spi*\.0", RUN+="/bin/sh -c 'SPI_TARGET_DEVICE=\"$$( basename $$DEVPATH )\" ; echo spidev > /sys/bus/spi/devices/$${SPI_TARGET_DEVICE}/driver_override ; echo $SPI_TARGET_DEVICE > /sys/bus/spi/drivers/spidev/bind'"

# As above, but for McSPI1, CS1.
#ACTION=="add", DEVPATH=="*/ocp/*481a0000\.spi/spi*\.1", RUN+="/bin/sh -c 'SPI_TARGET_DEVICE=\"$$( basename $$DEVPATH )\" ; echo spidev > /sys/bus/spi/devices/$${SPI_TARGET_DEVICE}/driver_override ; echo $SPI_TARGET_DEVICE > /sys/bus/spi/drivers/spidev/bind'"

# Import USB info for USB-attached SPI masters e.g. using the spi_ftdi_mpsse
# driver.
SUBSYSTEM=="spi", IMPORT{builtin}="usb_id"

# Bind spidev to SPI chip-select 0 of a specific USB-to-SPI controller based on
# its USB serial number ("123456789" in this example).
ACTION=="add", SUBSYSTEM=="spi", ENV{ID_USB_SERIAL_SHORT}="123456789", DEVPATH=="*/spi-ftdi-mpsse*/spi*\.0", RUN+="/bin/sh -c 'SPI_TARGET_DEVICE=\"$$( basename $$DEVPATH )\" ; echo spidev > /sys/bus/spi/devices/$${SPI_TARGET_DEVICE}/driver_override ; echo $SPI_TARGET_DEVICE > /sys/bus/spi/drivers/spidev/bind'"


# The following example creates deterministic (AKA "persistent") symlinks for
# spidev devices under the (created on-demand) directories:
#
# /dev/spi/by-*/
#
# First we use the udev 'path_id' builtin, to obtain the 'persistent device
# path' associated with each spidev.
#
# For information on other available udev "built in"s, see the output of:
#
# udevadm test-builtin --help
#
# To view the output of the command interactively, you can use a command like
# the following:
#
# udevadm test-builtin path_id $(udevadm info -q path -n /dev/spidev0.0)
#
# The following line makes $env{ID_PATH} available, so that we can use it later
# when creating our persistent symlinks:

SUBSYSTEM=="spidev", IMPORT{builtin}="path_id"

# Depending on the particular Linux driver implementation, some SPI controllers
# are abstracted as a single controller which multiple chip selects, whilst
# others are abstracted as multiple controllers, each of which acts on a single
# chip select. For our purposes we need to ensure the chip select is encoded
# in the deterministic/persistent symlink paths, so we create our own custom
# attribute "ID_PATH_WITH_CS"....

# If the ID_PATH includes the CS number already, just use that verbatim:
SUBSYSTEM=="spidev", ENV{ID_PATH}=="?*.spi-cs*", ENV{ID_PATH_WITH_CS}="$env{ID_PATH}"

# ... otherwise append the CS from the spidev number attribute to it.
SUBSYSTEM=="spidev", ENV{ID_PATH}=="?*.spi", ENV{ID_PATH_WITH_CS}="$env{ID_PATH}-cs-$number"

# Finally create symbolic links under /dev/spi/by-path/ using our custom
# attribute:
SUBSYSTEM=="spidev", ENV{ID_PATH_WITH_CS}=="?*", SYMLINK+="spi/by-path/$env{ID_PATH_WITH_CS}"


# An alternative example which uses a shell fragment to remove the '.spi' from
# the persistent device ID (for aesthetic reasons) is included below:
#SUBSYSTEM=="spidev", ENV{ID_PATH}=="?*-cs*", PROGRAM="/bin/sh -c 'echo ${ID_PATH%%.spi-cs-*}.cs-${DEVNAME#*spidev*.}'", SYMLINK+="spi/by-path/$result"
#SUBSYSTEM=="spidev", ENV{ID_PATH}=="?*.spi", PROGRAM="/bin/sh -c 'echo ${ID_PATH%%.spi}.cs-${DEVNAME#*spidev*.}'", SYMLINK+="spi/by-path/$result"

# Here we match the spidev device for CS0 of a memory mapped SPI controller (TI
# AM335x McSPI0 peripheral at 0x48030000), and create a symbolic link which
# identifies the hardware device which is attached to that spidev:
#SUBSYSTEM=="spidev", ENV{ID_PATH_WITH_CS}=="platform-48030000.spi-cs-0", SYMLINK+="spi/by-function/opcn3_optical_partical_counter_0"

# As above, but for TI AM33xx McSPI1 peripheral at 0x481a0000.
#SUBSYSTEM=="spidev", ENV{ID_PATH_WITH_CS}=="platform-481a0000.spi-cs-0", SYMLINK+="spi/by-function/alphasense_ndir_co2_sensor_0"

# A similar pair of rules which instead create symlinks identifying spidev
# nodes by their physical PCB connector silkscreen labels (i.e. it encodes
# knowledge of the way that a particular PCB has been designed).
SUBSYSTEM=="spidev", ENV{ID_PATH_WITH_CS}=="platform-48030000.spi-cs-0", SYMLINK+="spi/by-connector/H1"
SUBSYSTEM=="spidev", ENV{ID_PATH_WITH_CS}=="platform-481a0000.spi-cs-0", SYMLINK+="spi/by-connector/H3"

# An example for the Raspberry Pi 3, which creates a symbolic link which
# encodes the pins used on the SBC's 40 pin header (when used with the
# 'spi0-1cs' or 'spi-2cs' device tree overlays with the default CS0 pin
# assignment):
SUBSYSTEM=="spidev", ENV{ID_PATH_WITH_CS}=="platform-3f204000.spi-cs-0", SYMLINK+="spi/by-connector/MOSI19_MISO21_CLK23_CS24"


# An example which uses `compat` device tree entries to add symlinks to spidev
# devices:
#
# First import the COMPATITBLE_0 attribute from the parent SPI device:
SUBSYSTEM=="spidev", IMPORT{parent}="OF_COMPATIBLE_0"

# ...then create symlinks using the imported 'compatible' attribute. Some older
# device trees use a compatible string 'spidev', which is not useful for our
# purposes, so we ignore those cases.
SUBSYSTEM=="spidev", ENV{OF_COMPATIBLE_0}!="spidev", ENV{OF_COMPATIBLE_0}=="?*", SYMLINK+="spi/by-compat/$env{OF_COMPATIBLE_0}"


# An example of using the device tree node name of the parent SPI device to
# create `by-name/` symlinks.
SUBSYSTEM=="spidev", IMPORT{parent}="OF_NAME"

# ....then use the imported name attribute when creating the symlinks. As
# above, a device tree name of 'spidev' isn't descriptive, so skip those.
SUBSYSTEM=="spidev", ENV{OF_NAME}!="spidev", ENV{OF_COMPATIBLE_0}=="?*", SYMLINK+="spi/by-name/$env{OF_NAME}"


# Another set of examples for USB-attached SPI controllers...

# Import USB device info strings for spidev devices which have parent SPI
# controllers that are USB attached.
SUBSYSTEM=="spidev", IMPORT{builtin}="usb_id"

# For USB devices using the spi-ftdi-mpsse driver, append the chipselect from
# the ID_PATH attribute to the USB serial number (which is stored in the FTDI
# eeprom).

# Derive and store FTDI MPSSE device chip select string for use by subsequent
# rules as ENV{FTDI-MPSSE-CHIPSELECT}
SUBSYSTEM=="spidev", ENV{ID_PATH}=="?*spi-ftdi-mpsse*-cs-*", PROGRAM="/bin/sh -c 'echo ${ID_PATH#*mpsse.*-}'", ENV{FTDI-MPSSE-CHIPSELECT}="$result"

# Create the symlink by combining the USB serial number and the chipselect.
SUBSYSTEM=="spidev", ENV{ID_USB_SERIAL_SHORT}=="?*", SYMLINK+="spi/by-usb-sernum/$env{ID_USB_SERIAL_SHORT}-$env{FTDI-MPSSE-CHIPSELECT}"

# A similar rule which adds a symlink for spi-ftdi-mpsse devices under
# /dev/spi/by-path as a one-liner:
SUBSYSTEM=="spidev", ENV{ID_PATH}=="?*spi-ftdi-mpsse*-cs-*", PROGRAM="/bin/sh -c 'echo ${ID_USB_SERIAL_SHORT}-${ID_PATH#*mpsse.*-}'", SYMLINK+="spi/by-path/usb-sernum-$result"
98 changes: 98 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,101 @@ data will be split into smaller chunks and sent in multiple operations.
close()

Disconnects from the SPI device.

The Linux kernel and SPI bus numbering and the role of udev
-----------------------------------------------------------

### Summary

If your code may interact with an SPI controller which is attached to the
system via the USB or PCI buses, **or** if you are maintaining a product which
is likely to change SoCs **or upgrade kernels** during its lifetime, then you
should consider using one or more udev rules to create symlinks to the SPI
controller spidev, and then use `open_path`, to open the device file via the
symlink in your code.

Consider allowing the end-user to configure their choice of full spidev path -
for example with the use of a command line argument to your Python script, or
an entry in a configuration file which your code reads and parses.

Additional udev actions can also set the ownership and file access permissions
on the spidev device node file (to increase the security of the system). In
some instances, udev rules may also be needed to ensure that spidev device
nodes are created in the first place (by triggering the Linux `spidev` driver
to "bind" to an underlying SPI controller).

### Detailed Information

This section provides an overview of the Linux APIs which this extension uses.

**If your software might be used on systems with non-deterministic SPI bus
numbering**, then using the `open_path` method can allow those maintaining the
system to use mechanisms such as `udev` to create stable symbolic links to the
SPI device for the correct physical SPI bus.

See the example udev rule file `99-local-spi-example-udev.rules`.

This Python extension communicates with SPI devices by using the 'spidev'
[Linux kernel SPI userspace
API](https://www.kernel.org/doc/html/next/spi/spidev.html).

'spidev' in turn communicates with SPI bus controller hardware using the
kernel's internal SPI APIs and hardware device drivers.

If the system is configured to expose a particular SPI device to user space
(i.e. when an SPI device is "bound" to the spidev driver), then the spidev
driver registers this device with the kernel, and exposes its Linux kernel SPI
bus number and SPI chip select number to user space in the form of a POSIX
"character device" special file.

A user space program (usually 'udev') listens for kernel device creation
events, and creates a file system "device node" for user space software to
interact with. By convention, for spidev, the device nodes are named
/dev/spidev<bus>.<device> is (where the *bus* is the Linux kernel's internal
SPI bus number (see below) and the *device* number corresponds to the SPI
controller "chip select" output pin that is connected to the SPI *device* 'chip
select' input pin.

The Linux kernel **may assign SPI bus numbers to a system's SPI controllers in
a non-deterministic way.** In some hardware configurations, the SPI bus number
of a particular hardware peripheral is:

- Not guaranteed to remain constant between different Linux kernel versions.
- Not guaranteed to remain constant between successive boots of the same kernel
(due to race conditions during boot-time hardware enumeration, or dynamic
kernel module loading).
- Not guaranteed to match the hardware manufacturer's SPI bus numbering scheme.

In the case of SPI controllers which are themselves connected to the system via
buses that are subject to hot-plug (such as USB, Thunderbolt, or PCI), the
SPI bus number should usually be expected to be non-deterministic.

The supported Linux mechanism which allows user space software to identify the
correct hardware, it to compose "udev rules" which create stable symbolic links
to device files. For example, most Linux distributions automatically create
symbolic links to allow identification of block storage devices e.g. see the
output of `ls -alR /dev/disk`.

`99-local-spi-example-udev.rules` included with py-spidev includes example udev
rules for creating stable symlink device paths (for use with `open_path`).

e.g. the following Python code could be used to communicate with an SPI device
attached to chip-select line 0 of an individual FTDI FT232H USB to SPI adapter
which has the USB serial number "1A8FG636":


```
#!/usr/bin/env python3
import spidev
spi = spidev.SpiDev()
spi.open_path("/dev/spi/by-path/usb-sernum-1A8FG636-cs-0")
# TODO: Useful stuff here
spi.close()
```

In the more general case, the example udev file should be modified as
appropriate to your needs, renamed to something descriptive of the purpose
and/or project, and placed in `/etc/udev/rules.d/` (or `/lib/udev/rules.d/` in
the case of rules files included with operating system packages).

0 comments on commit 63eeee0

Please sign in to comment.