Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add open_path method as an alternative to open (to allow use of udev symlinks). #130

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
---------

Unreleased
====

* Added open_path method to accommodate dynamic SPI bus number allocation

3.6
====

Expand Down
111 changes: 108 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Usage
```python
import spidev
spi = spidev.SpiDev()
spi.open(bus, device)
spi.open_path(spidev_devicefile_path)
to_send = [0x01, 0x02, 0x03]
spi.xfer(to_send)
```
Expand All @@ -21,7 +21,7 @@ Settings
```python
import spidev
spi = spidev.SpiDev()
spi.open(bus, device)
spi.open_path("/dev/spidev0.0")

# Settings (for example)
spi.max_speed_hz = 5000
Expand All @@ -43,9 +43,16 @@ spi.mode = 0b01
Methods
-------

open_path(filesystem_path)

Connects to the specified SPI device special file, following symbolic links if
appropriate (see note on deterministic SPI bus numbering in the Linux kernel
below for why this can be advantageous in some configurations).

open(bus, device)

Connects to the specified SPI device, opening `/dev/spidev<bus>.<device>`
Equivalent to calling `open_path("/dev/spidev<bus>.<device>")`. n.b. **Either**
`open_path` or `open` should be used.

readbytes(n)

Expand Down Expand Up @@ -83,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).
65 changes: 48 additions & 17 deletions spidev_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -1335,27 +1335,12 @@ static PyGetSetDef SpiDev_getset[] = {
{NULL},
};

PyDoc_STRVAR(SpiDev_open_doc,
"open(bus, device)\n\n"
"Connects the object to the specified SPI device.\n"
"open(X,Y) will open /dev/spidev<X>.<Y>\n");

static PyObject *
SpiDev_open(SpiDevObject *self, PyObject *args, PyObject *kwds)
SpiDev_open_dev(SpiDevObject *self, char *dev_path)
{
int bus, device;
char path[SPIDEV_MAXPATH];
uint8_t tmp8;
uint32_t tmp32;
static char *kwlist[] = {"bus", "device", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii:open", kwlist, &bus, &device))
return NULL;
if (snprintf(path, SPIDEV_MAXPATH, "/dev/spidev%d.%d", bus, device) >= SPIDEV_MAXPATH) {
PyErr_SetString(PyExc_OverflowError,
"Bus and/or device number is invalid.");
return NULL;
}
if ((self->fd = open(path, O_RDWR, 0)) == -1) {
if ((self->fd = open(dev_path, O_RDWR, 0)) == -1) {
PyErr_SetFromErrno(PyExc_IOError);
return NULL;
}
Expand All @@ -1379,6 +1364,50 @@ SpiDev_open(SpiDevObject *self, PyObject *args, PyObject *kwds)
return Py_None;
}


PyDoc_STRVAR(SpiDev_open_path_doc,
"open_path(spidev_path)\n\n"
"Connects the object to the specified SPI device.\n"
"open_path(X) will open the spidev character device <X> (following symbolic links if necessary).\n");

static PyObject *
SpiDev_open_path(SpiDevObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"path", NULL};
PyObject *py_dev_path;
char *dev_path;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O&:open", kwlist, PyUnicode_FSConverter, &py_dev_path))
return NULL;
if (py_dev_path == NULL)
return NULL;
dev_path = PyBytes_AsString(py_dev_path);
if (dev_path == NULL)
return NULL;
return SpiDev_open_dev(self, dev_path);
}


PyDoc_STRVAR(SpiDev_open_doc,
"open(bus, device)\n\n"
"Connects the object to the specified SPI device.\n"
"open(X,Y) will open /dev/spidev<X>.<Y>\n");

static PyObject *
SpiDev_open(SpiDevObject *self, PyObject *args, PyObject *kwds)
{
int bus, device;
char path[SPIDEV_MAXPATH];
static char *kwlist[] = {"bus", "device", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii:open", kwlist, &bus, &device))
return NULL;
if (snprintf(path, SPIDEV_MAXPATH, "/dev/spidev%d.%d", bus, device) >= SPIDEV_MAXPATH) {
PyErr_SetString(PyExc_OverflowError,
"Bus and/or device number is invalid.");
return NULL;
}
return SpiDev_open_dev(self, path);
}

static int
SpiDev_init(SpiDevObject *self, PyObject *args, PyObject *kwds)
{
Expand Down Expand Up @@ -1434,6 +1463,8 @@ PyObject *SpiDev_exit(SpiDevObject *self, PyObject *args)
static PyMethodDef SpiDev_methods[] = {
{"open", (PyCFunction)SpiDev_open, METH_VARARGS | METH_KEYWORDS,
SpiDev_open_doc},
{"open_path", (PyCFunction)SpiDev_open_path, METH_VARARGS | METH_KEYWORDS,
SpiDev_open_path_doc},
{"close", (PyCFunction)SpiDev_close, METH_NOARGS,
SpiDev_close_doc},
{"fileno", (PyCFunction)SpiDev_fileno, METH_NOARGS,
Expand Down