From 957584247e80f75ff64d4215458d885f4034a8bf Mon Sep 17 00:00:00 2001 From: Tim Small Date: Mon, 24 Apr 2023 08:59:20 +0100 Subject: [PATCH 1/5] Split SpiDev_open to prepare for open-by-path functionality. Prep for adding open_path Python method in #129 --- spidev_module.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/spidev_module.c b/spidev_module.c index ad3c342..95068fd 100644 --- a/spidev_module.c +++ b/spidev_module.c @@ -1335,6 +1335,9 @@ static PyGetSetDef SpiDev_getset[] = { {NULL}, }; +static PyObject * +SpiDev_open_dev(SpiDevObject *self, char *dev_path); + PyDoc_STRVAR(SpiDev_open_doc, "open(bus, device)\n\n" "Connects the object to the specified SPI device.\n" @@ -1345,8 +1348,6 @@ SpiDev_open(SpiDevObject *self, PyObject *args, PyObject *kwds) { 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; @@ -1355,7 +1356,15 @@ SpiDev_open(SpiDevObject *self, PyObject *args, PyObject *kwds) "Bus and/or device number is invalid."); return NULL; } - if ((self->fd = open(path, O_RDWR, 0)) == -1) { + return SpiDev_open_dev(self, path); +} + +static PyObject * +SpiDev_open_dev(SpiDevObject *self, char *dev_path) +{ + uint8_t tmp8; + uint32_t tmp32; + if ((self->fd = open(dev_path, O_RDWR, 0)) == -1) { PyErr_SetFromErrno(PyExc_IOError); return NULL; } From 5cb64055c4f9a10d437ca993be85d790b7df0e98 Mon Sep 17 00:00:00 2001 From: Tim Small Date: Mon, 24 Apr 2023 09:02:44 +0100 Subject: [PATCH 2/5] Reorder declarations to avoid function prototype. Prep for adding open_path Python method in #129 --- spidev_module.c | 46 ++++++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/spidev_module.c b/spidev_module.c index 95068fd..cf12a1d 100644 --- a/spidev_module.c +++ b/spidev_module.c @@ -1335,30 +1335,6 @@ static PyGetSetDef SpiDev_getset[] = { {NULL}, }; -static PyObject * -SpiDev_open_dev(SpiDevObject *self, char *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.\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 PyObject * SpiDev_open_dev(SpiDevObject *self, char *dev_path) { @@ -1388,6 +1364,28 @@ SpiDev_open_dev(SpiDevObject *self, char *dev_path) return Py_None; } + +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.\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) { From 05e381097b7899ca167eb33240d16f763c5f24c9 Mon Sep 17 00:00:00 2001 From: Tim Small Date: Mon, 24 Apr 2023 14:56:16 +0100 Subject: [PATCH 3/5] Add open_path() Python method to permit persistent device symlinks. Adds to the functionality provided by the open() call (which hard-codes the opening of /dev/spidev. device file paths). Because the bus number allocation may not be knowable in advance (bus numbers are dynamically allocated by the Linux kernel), open_path() allows the user to employ udev rules to deterministically create a symlink to the correct hardware device at boot and/or hot-plug time. Part of #129 --- README.md | 13 ++++++++++--- spidev_module.c | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5f272d6..9d06436 100644 --- a/README.md +++ b/README.md @@ -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) ``` @@ -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 @@ -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.` +Equivalent to calling `open_path("/dev/spidev.")`. n.b. **Either** +`open_path` or `open` should be used. readbytes(n) diff --git a/spidev_module.c b/spidev_module.c index cf12a1d..a7eddc3 100644 --- a/spidev_module.c +++ b/spidev_module.c @@ -1365,6 +1365,28 @@ SpiDev_open_dev(SpiDevObject *self, char *dev_path) } +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 (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" @@ -1441,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, From 75695f3e54a231f0a685f9d3e9610f60ac1e43fe Mon Sep 17 00:00:00 2001 From: Tim Small Date: Mon, 24 Apr 2023 15:45:17 +0100 Subject: [PATCH 4/5] Additional documentation for open_path method. Resolves #129 --- 99-local-spi-example-udev.rules | 159 ++++++++++++++++++++++++++++++++ README.md | 98 ++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 99-local-spi-example-udev.rules diff --git a/99-local-spi-example-udev.rules b/99-local-spi-example-udev.rules new file mode 100644 index 0000000..4740024 --- /dev/null +++ b/99-local-spi-example-udev.rules @@ -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" diff --git a/README.md b/README.md index 9d06436..f4361d1 100644 --- a/README.md +++ b/README.md @@ -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. 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). From 969fc4be65ae2fdd21db24d5f15249a10bb1db5d Mon Sep 17 00:00:00 2001 From: Tim Small Date: Mon, 24 Apr 2023 10:41:08 +0100 Subject: [PATCH 5/5] Add changelog entry. --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80c0ea2..13866f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Changelog --------- +Unreleased +==== + +* Added open_path method to accommodate dynamic SPI bus number allocation + 3.6 ====