2020-10-04
md
Serial Peripheral Interface on the Raspberry Pi
<-I²C Light Sensor using a Seeeduino XIAO

As is often the case on this site, this post is a summary of what was learned as I have become a little more familiar with a new-to-me technology. So far, my only use of the Serial Peripheral Interface (SPI) had been a frustrating but ultimately successful attempt at connecting a relatively uncommon SPI LCD display with an ESP8266, then an Arduino Uno and, lately, an Orange Pi. This time, I wanted to look into using the SPI bus much as I used an I²C bus for arbitrary communication between a Raspberry Pi and a microcontroller. I haven't reached that goal yet, concentrating for the time being on "SPI on the Pi". The only devices harmed in the course of writing this post were a Raspberry Pi 3 B and a cheap, Saleae Logic knockoff, 8 channel logic analyzer. And ever mindful to conserve bits, only 3 channels of the latter were used.

The Raspberry Pi was running under the last version of Raspbian Buster when the original version of this post was written in early May 2020. Luckily, not much has changed with the newest version of the OS which now goes under the Raspberry Pi OS moniker. This revision remains a draft, because some parts may need to be updated and there may very well be some additional material in the future.

Table of contents

  1. Serial Peripheral Interface - SPI
  2. SPI Buses on the Raspberry Pi
    1. spidev0
    2. spidev1 and More
    3. Number of SPI Slave Devices
  3. SPI Loopback Test
    1. SPI Loopback Test in C
    2. SPI Loopback Test in Python
  4. The SPI Protocol in Action
  5. Documenting the Python SpiDev Module

Serial Peripheral Interface - SPI toc

Here is how two devices, an Lolin/Wemos D1 Mini and a colour LCD, could be connected to the Raspberry Pi using the SPI.

The table summarizes the logic signals.

LabelFunctionAlternate labels...
SCLKSerial Clock (output from master)SCK, CLK
MOSIMaster Output Slave Input, or Master Out Slave In (data output from master)SDI, DIN, SI
MISOMaster Input Slave Output, or Master In Slave Out (data output from slave)SDO, DOUT, SO
SSSlave Select (often active low, output from master)CE, SSEL, CS

There will be no further mention of the LCD or microcontroller in this post, but I thought it would be valuable to show the typical topology of a SPI bus where there can be only one master and one or more slaves. To communicate with a particular slave, the master must enable it with a dedicated signal (slave select, often called chip select).There is no concept of a bus address as in the I²C protocol. The master also supplies the clock to all the slaves. With each pulse of the clock, the master sends a bit of data down to the slave on the MOSI signal line and, simultaneously, the slave sends a bit of data up to the master on the MISO line. Thus SPI is a full-duplex communication protocol. Conceivably, a multi-function peripheral would not know what to send initially so that its data could be garbage until it has received some instructions from the master. And as can be seen above, some peripherals don't even have a slave output signal going to the master.

As one can imagine, there are a lot of details that need to be settled for communications to occur.

With those questions in mind, I feel comfortable enough to move on. Seriously, the article Serial Peripheral Interface is informative, thorough and nevertheless easily understood. It is, in fact, among the best technical articles I have read on Wikipedia.

SPI on the Raspberry Pi toc

Detailed information about Broadcom systems on chip (SoC) is hard to find. Here is what I have been able to piece together. The BCM2835 (used on the Raspberry Pi Model 1 and the Zero), the BCM2836 (used on Model 2) and the BCM2837 (used on Model 2 ver 1.2 and Model 3) systems on chip have 2 SPI master controllers each with 3 independent slave select signals (BCM2835 ARM Peripheral, Broadcom, 2012 p. 20). I have no information on the BCM2837B0 used on Models 3B+, 3A+ and the Compute Module 3+. The DATASHEET, Raspberry Pi Compute Module 3+, Raspberry Pi Compute Module 3+ Lite, Release 1, January 2019 lists 2xSPI as available peripherals (p. 6), which entails that the SoC must have at least two SPI controllers. The BCM2711, used with the Model 4, is more flexible and has 5 SPI Master interfaces SPI0, SPI3, SPI4, SPI5, SPI6 and two mini SPI interfaces, SP1 and SPI2 (BCM2711 ARM Peripherals, Raspberry Pi (Trading) Ltd., Version 1, 5th February 2020, p. 168).

Of course, not every SoC peripheral is connected to a header on the Raspberry Pi. However, all Raspberry Pi models have at least one hardware SPI bus (SPI0) with two associated slave select signals. It uses the following pins on the GPIO (P1) header.

SignalGPIO pinPhysical pin
SPI0_MOSI1019
SPI0_MISO921
SPI0_SCLK1123
SPI0_CEO_N824
SPI0_CE1_N726

Newer models with a 40 GPIO header have a second SPI bus (SPI1) which has up to three slave select signals.

SignalGPIO pinPhysical pin
SPI1_MOSI2038
SPI1_MISO1935
SPI1_SCLK2140
SPI1_CEO_N1822
SPI1_CE1_N1711
SPI1_CE2_N1636

Bus SPI2 is available on the Compute module only.

I do not have a Model 4 so cannot experiment with the other SPI interfaces on that model. The pinout for these other interfaces can be found in section 5.3 Alternative Function Assignment on page 98 of the data sheet. There is a clear presentation of the pinout for SPI3, SPI4, SPI5 and SPI6 on the 40 pin header of the Model 4 on the SPI documentation at the Raspberry Pi Foundation.

In BCM2835 ARM Peripheral, there is mention of a BSC/SPI slave controller on page 160 (BSC for Broadcom Serial Controller appears to be the Broadcom implementation of the I2C protocol). It shows up in the BCM2711 ARM Peripherals as the ALT3 function of GPIO8, 9, 10 and 11 pins (pages 98 and 99). To the best of my knowledge, drivers for that peripheral were never developed. There seems to be a consensus that the available hardware SPI interfaces on the Raspberry Pi function exclusively in master mode.

spidev0 toc

By default, the SPI kernel drivers are not loaded in the lite versions of Raspbian Buster or its more recent replacement Raspberry Pi OS. That can be done on a one-off basis with the dtparam utility.

woopi@goldserver:~ $ lsmod | grep spi nothing found! woopi@goldserver:~ $ ls /dev/spi* ls: cannot access '/dev/spi*': No such file or directory woopi@goldserver:~ $ sudo dtparam spi=on or woopi@goldserver:~ $ sudo dtparam spi woopi@goldserver:~/spidev_test $ lsmod | grep spi spidev 20480 0 spi_bcm2835 20480 0 woopi@goldserver:~ $ ls -l /dev/spi* crw-rw---- 1 root spi 153, 0 Apr 28 13:13 /dev/spidev0.0 crw-rw---- 1 root spi 153, 1 Apr 28 13:13 /dev/spidev0.1

While "everything is a file in Linux", spidev0.0 and spidev0.1 are called "user space device nodes" in the help files. Interpret them as the SPI0 controller with slave select SPIO0_CEO_N which is GPIO8 in the case of spidev0.0 and the same SPI0 controller with slave select SPI0_CE1_N, GPIO7 for spidev0.1. Unfortunately the third slave select, SPIO_CE2_N on GPIO45 is not available on the GPIO header.

There are two ways to enable the SPI0 hardware interface permanently. I prefer to edit the hardware configuration file /boot/config.txt.

woopi@goldserver:~ $ sudo nano /boot/config.txt

I found the following bit in the file.

... # Uncomment some or all of these to enable the optional hardware interfaces #dtparam=i2c_arm=on #dtparam=i2s=on #dtparam=spi=on ...

As instructed, I removed the leading "#" at the start of the #dtparam=spi=on line to enable SPI0. This change will take effect when the Raspberry Pi is rebooted. The other way to accomplish the same thing is to use the raspi-config utility.

woopi@goldserver:~ $ sudo raspi-config

Here is the list of menu choices that have to be made.

To be accurate, using the configuration utility not only modifies the boot/config.txt to enable the SPI interface at boot time, but it also loads the overlay and drivers immediately.

Additionally, there are two device-tree overlays, spi0-cs and spi0-hw-cs, that can be used to load the SPI0 kernel driver.

woopi@goldserver:~ $ dtoverlay -h spi0-cs Name: spi0-cs Info: Allows the (software) CS pins for SPI0 to be changed Usage: dtoverlay=spi0-cs,<param>=<val> Params: cs0_pin GPIO pin for CS0 (default 8) cs1_pin GPIO pin for CS1 (default 7) woopi@goldserver:~ $ dtoverlay -h spi0-hw-cs Name: spi0-hw-cs Info: Re-enables hardware CS/CE (chip selects) for SPI0 Usage: dtoverlay=spi0-hw-cs Params: <None>

These overlays can be used to load the kernel module as with dtparam spi, but with more flexibility such as selecting which pins to use for slave select signals. Below I will install the driver using GPIO23 and GPIO24 (physical pins 16 and 18 respectively) as the slave select pins

woopi@goldserver:~ $ ls /dev/spi* ls: cannot access '/dev/spi*': No such file or directory woopi@goldserver:~ $ sudo dtoverlay spi0-cs cs0_pin=23 cs1_pin=24 woopi@goldserver:~ $ ls /dev/spi* /dev/spidev0.0 /dev/spidev0.1 woopi@goldserver:~ $ dtoverlay -l Overlays (in load order): 0: spi0-cs woopi@goldserver:~ $ sudo dtoverlay -r 0 woopi@goldserver:~ $ dtoverlay -l No overlays loaded woopi@goldserver:~ $ ls /dev/spi* ls: cannot access '/dev/spi*': No such file or directory woopi@goldserver:~ $ sudo dtoverlay spi0-hw-cs woopi@goldserver:~ $ ls /dev/spi* /dev/spidev0.0 /dev/spidev0.1

As shown above there are four ways to set up the first SPI bus on any Raspberry Pi model.

spidev1 and more toc

Lets use the dtoverlay utility to list all SPI related overlays.

woopi@goldserver:~ $ dtoverlay --all | grep spi anyspi (*) enc28j60-spi2 jedec-spi-nor sc16is752-spi1 sh1106-spi (*) spi-gpio35-39 spi-gpio40-45 spi-rtc spi0-cs spi0-hw-cs spi1-1cs spi1-2cs spi1-3cs spi2-1cs spi2-2cs spi2-3cs spi3-1cs spi3-2cs spi4-1cs spi4-2cs spi5-1cs spi5-2cs spi6-1cs spi6-2cs ssd1306-spi (*) ssd1351-spi (*)

Modules marked with an asterisk were added in the newer Raspberry Pi OS. Some overlays are obviously drivers for SPI devices such as an Ethernet controller, a real-time clock, flash memory and display controllers. The spi-gpioXX-YY overlays are to "move [the] SPI function block to GPIO[XX-YY]". Since these GPIO pins are not available on the Raspberry Pi, I assume they are for the Compute module. The spi0-xxx overlays have already been seen. The spiB-Ncs overlays are pretty much self-explanatory: it is a driver for SPI bus B with N slave select signals. Let's look at the documentation for one of them and install it (after spi0-cs was installed).

woopi@goldserver:~ $ dtoverlay -h spi1-3cs Name: spi1-3cs Info: Enables spi1 with three chip select (CS) lines and associated spidev dev nodes. The gpio pin numbers for the CS lines and spidev device node creation are configurable. N.B.: spi1 is only accessible on devices with a 40pin header, eg: A+, B+, Zero and PI2 B; as well as the Compute Module. Usage: dtoverlay=spi1-3cs,<param>=<val> Params: cs0_pin GPIO pin for CS0 (default 18 - BCM SPI1_CE0). cs1_pin GPIO pin for CS1 (default 17 - BCM SPI1_CE1). cs2_pin GPIO pin for CS2 (default 16 - BCM SPI1_CE2). cs0_spidev Set to 'disabled' to stop the creation of a userspace device node /dev/spidev1.0 (default is 'okay' or enabled). cs1_spidev Set to 'disabled' to stop the creation of a userspace device node /dev/spidev1.1 (default is 'okay' or enabled). cs2_spidev Set to 'disabled' to stop the creation of a userspace device node /dev/spidev1.2 (default is 'okay' or enabled). woopi@goldserver:~ $ sudo dtoverlay spi1-3cs woopi@goldserver:~ $ ls /dev/spi* /dev/spidev0.0 /dev/spidev0.1 /dev/spidev1.0 /dev/spidev1.1 /dev/spidev1.2

Of course the above will install the kernel driver temporarily, and when the Pi is rebooted /dev/spidev1.x will no longer exist. The /boot/config.txt file has to be modified to load the driver automatically at boot time.

woopi@goldserver:~ $ sudo nano /boot/config.txt

... # Uncomment some or all of these to enable the optional hardware interfaces #dtparam=i2c_arm=on #dtparam=i2s=on dtparam=spi=on # To enable the SPI1 hardware interface, uncomment one and only one of the # following optional overlays depending on the number of slave devices # (1 to 3) to be connected to the bus #dtoverlay=spi1-1cs dtoverlay=spi1-2cs #dtoverlay=spi1-3cs ...

Shown is bold red are the changes to the file that will load two SPI controllers and create four devices whenever the Raspberry Pi is booted.

There is were no SPI2 to SPI6 interfaces before the Raspberry 4 (and Compute Module). So one should not be surprised if the installation of any of these fail on a Raspberry Pi 3 B or earlier model.

woopi@goldserver:~ $ sudo dtoverlay spi2-1cs woopi@goldserver:~ $ sudo dtoverlay spi4-1cs * Failed to apply overlay '0_spi4-1cs' (kernel)

The failure may be flagged on the console as shown above or it may not be reported at all.

Number of SPI Slave Devices toc

All Raspberry Pi with a 40 pin GPIO header can handle up to five SPI slave devices as shown in the following figure.

In parentheses are the GPIO pin number of each Raspberry Pi signal, while the physical header number of each signal is in square brackets. PJRC would call that a simple but poor SPI bus design, see Better SPI Bus Design in 3 Steps.

It will be possible to have even more SPI slaves connected to a Compute module and to a Raspberry Pi 4. Without these devices, I can't say more with any certainty. Using the loop back tests described in the next section, it will be easy to verify if a kernel module is correctly loaded.

SPI Loopback Test toc

Being a full-duplex communication protocol, it should be possible to perform a loop back test with a SPI interface. The only prerequisite for such tests is to connect the MOSI and MISO lines together. There are plenty of references to a C program called spidev-test, so I first tried that. Then I moved on to a similar test but using a Python script.

SPI Loopback Test in C toc

In my experience, not all versions of spidev-test originally written in C by Anton Vorontsov that can be found on the Web work on the Raspberry Pi. Presumably the version provided by Raspberry Pi team does function, but I used the Richard Hull version. I have not compared these versions. The test is first run without the loopback connection and then again after making the connection to see the difference.

woopi@goldserver:~ $ mkdir spidev_test woopi@goldserver:~ $ wget https://raw.githubusercontent.com/rm-hull/spidev-test/master/spidev_test.c -O spidev_test/spidev_test.c --2020-04-28 13:55:21-- https://raw.githubusercontent.com/rm-hull/spidev-test/master/spidev_test.c ... 2020-04-28 13:55:22 (7.19 MB/s) - ‘spidev_test/spidev_test.c’ saved [8511/8511] woopi@goldserver:~ $ cd spidev_test woopi@goldserver:~/spidev_test $ gcc -o spidev_test spidev_test.c woopi@goldserver:~/spidev_test $ ls -l total 32 -rwxr-xr-x 1 woopi woopi 18124 Apr 28 13:56 spidev_test -rw-r--r-- 1 woopi woopi 8511 Apr 28 13:55 spidev_test.c woopi@goldserver:~/spidev_test2 $ ./spidev_test -v spi mode: 0x0 bits per word: 8 max speed: 500000 Hz (500 KHz) TX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....�..................�. RX | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................................

Obviously, the data send out of the MOSI signal line was not read. Connect the MOSI and MISO pins (GPIO 10 and 9 which are header pins 19 and 21 respectively) together and rerun the script.

woopi@goldserver:~/spidev_test $ ./spidev_test -v spi mode: 0x0 bits per word: 8 max speed: 500000 Hz (500 KHz) TX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....�..................�. RX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....�..................�.

It should not be necessary to use the sudo prefix to obtain root privileges to run the test because the default user should be a member of the spi group.

woopi@goldserver:~ $ groups woopi adm dialout cdrom sudo audio video plugdev games users input netdev gpio i2c spi

The test works with other hardware SPI channels if in place.

woopi@goldserver:~ $ ls /dev/spidev1* /dev/spidev1.0 /dev/spidev1.1 /dev/spidev1.2 woopi@goldserver:~ $ cd spidev_test/ woopi@goldserver:~/spidev_test $ ./spidev_test -v -D /dev/spidev1.1 spi mode: 0x0 bits per word: 8 max speed: 500000 Hz (500 KHz) TX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....�..................�. RX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....�..................�. woopi@goldserver:~/spidev_test $ ./spidev_test -v -D /dev/spidev1.2 spi mode: 0x0 bits per word: 8 max speed: 500000 Hz (500 KHz) TX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....�..................�. RX | FF FF FF FF FF FF 40 00 00 00 00 95 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF F0 0D | ......@....�..................�.

Of course, the MOSI and MISO SPI1 signals (GPIO 20 and 19 respectively) have to be connected for this test to work as shown. Also it does not matter which slave select pin is used for this test as can be seen from the two commands above.

SPI Loopback Test in Python toc

Let's do something similar with a Python script instead of a C/C++ program. First I created a virtual Python3 environment and then installed the Python spidev module by Stephen Caudle (doceme).

woopi@goldserver:~/spidev_test $ cd ~ woopi@goldserver:~ $ mkvenv spipy a Bash script to create a virtual environment ... woopi@goldserver:~ $ ve spipy a Bash macro to activate the virtual environment> (spipy) woopi@goldserver:~ $ pip install spidev Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple Collecting spidev Downloading https://www.piwheels.org/simple/spidev/spidev-3.4-cp37-cp37m-linux_armv7l.whl (39 kB) Installing collected packages: spidev Successfully installed spidev-3.4

Then I used nano to create the following script.

woopi@goldserver:~ $ ve spipy woopi@goldserver:~ $ cd spipy (spipy) woopi@goldserver:~/spipy $ nano spi_loopback_test.py

import spidev import time SPI_BUS = 0 # spidev0 SPI_SS = 0 # spidev0.0 SPI_CLOCK = 1000000 # 1 Mhz # setup SPI spi = spidev.SpiDev(SPI_BUS, SPI_SS) spi.max_speed_hz = SPI_CLOCK # transfer 2 bytes at a time, ^C to exit try: v = 0 while True: send = [v, v+1] print("") print("TX:", send) print("RX:", spi.xfer(send)) time.sleep(0.5) if v >= 254: v = 0 else: v = (v+2) finally: spi.close()

The spi_loopback_test.py script can be executed invoking the Python 3 interpreter directly.

(spipy) woopi@goldserver:~/spipy $ python spi_loopback_test.py

If you do not want to use a virtual Python environment, the required spidev module can be loaded into the default Python 3 installation and then the script can be executed using the Python 3 interpreter.

woopi@goldserver:~ $ sudo apt install python3-spidev woopi@goldserver:~ $ python3 spi_loopback_test.py

It is also possible to mark the script as executable and bash will start the interpreter automatically if the correct "shebang" line is added to the script. In the the virtual environment that first line should be

#!/usr/bin/env python

while it should be

#!/usr/bin python3

if the default Python3 interpreter is used. No matter how the script is started, this is its output to the console if everything works correctly.

TX: [0, 1] RX: [0, 1] TX: [2, 3] RX: [2, 3] ... TX: [254, 255] RX: [254, 255] TX: [0, 1] RX: [0, 1] TX: [2, 3] RX: [2, 3] ...

Use the CtrlC key combination to exit the loop.

It was not easy to get that script to work, which forced me to look at spidev with more attention. Unfortunately, I could not find a "SPI on the Pi with Python" tutorial suitable for a newbie like me, so I had to rely on the README.md on the project GitHub (or project description at pypi.org) and the bits of the source code that I could fathom. Not unexpectedly, the SpiDev class has a number of settings or attributes. Here they are with their values after instantiation and initialization of a SpiDev object.

Attributespidev.SpiDev()spidev.SpiDev(0,x)
bits_per_word08
cshighFalseFalse
loopFalseFalse
lsbfirst FalseFalse
max_speed_hz0125000000
mode00
no_csFalseFalse
threewire FalseFalse

At first I was misled by the loop setting, which I thought needed to be set to true in a loop back test. It was a logical conclusion but it was wrong. After some searching I found the following issues Setting lsbfirst = True breaks access to GPIO on Raspberry Pi 1/2 with 3.18 kernel #18 and spi.lsbfirst = True fails with [Errno 22] Invalid argument #49 about a similar problem. In the last comment of the last issue, Gadgetoid confirms that spi.lsbfirst, spi.loop, and spi.threewire are unsupported on the Raspberry Pi. I am not positive that the last option remains unsupported especially given the Raspberry Pi SPI page. Of course, even if the Linux driver has some capability, it does not necessarily follow that the Python module supports it. Further testing is needed.

After looking at numerous examples on connecting analogue to digital converters using SPI, I finally twigged on the fact that the default speed of 125 MHz was considerably higher than the typical 1 Mhz in those examples. So I added the spi.max_speed_hz=1000000 and the script worked! A short script gives a ballpark indication of the maximum SPI speed.

import spidev import time spi = spidev.SpiDev(0, 0) send = [0, 1, 2, 4, 8, 16, 32, 64, 128, 255] freq = 250000 ok = True try: while ok: spi.max_speed_hz = freq print() print("spi.max_speed_hz:", spi.max_speed_hz) print("TX:", send) recvd = spi.xfer( [0, 1, 2, 4, 8, 16, 32, 64, 128, 255] ) print("RX:", recvd) ok = recvd == send if ok: print("Success") else: print("Failed") freq = 2*freq finally: spi.close()

The spi_loopback_speed.py script output showed that a 32MHz frequency was possible but not 64MHz.

spi.max_speed_hz: 250000 TX: [0, 1, 2, 4, 8, 16, 32, 64, 128, 255] RX: [0, 1, 2, 4, 8, 16, 32, 64, 128, 255] Success ... spi.max_speed_hz: 32000000 TX: [0, 1, 2, 4, 8, 16, 32, 64, 128, 255] RX: [0, 1, 2, 4, 8, 16, 32, 64, 128, 255] Success spi.max_speed_hz: 64000000 TX: [0, 1, 2, 4, 8, 16, 32, 64, 128, 255] RX: [0, 0, 1, 2, 4, 8, 16, 32, 64, 127] Failed

The SPI Protocol in Action toc

Here is an even simpler Python script that will create a SpiDev object, have it open the /dev/spidev0.1 file and write a byte (0x3A) on the MOSI signal line every tenth of a second until the CtrlC keyboard combination is pressed.

import spidev import time spi = spidev.SpiDev(0, 1) # create spi object connecting to /dev/spidev0.1 spi.max_speed_hz = 250000 # set speed to 250 Khz try: while True: # endless loop, press Ctrl+C to exit spi.writebytes([0x3A]) # write one byte time.sleep(0.1) # sleep for 0.1 seconds finally: spi.close() # always close the port before exit

Three SPI signals were captured with a logic analyzer. The following image is a screen capture of the transmission of one byte as decoded by the analyzer.

As can be seen the data was sent bit by bit (which is what is meant by serial protocol) starting with the most significant bit. The eight clock cycles needed to send a byte of data took about 32 microseconds which translates to the set clock frequency of 250,000 Hz, give or take the measurement error.

The active low slave select signal was asserted about 7 microseconds before the transmission started, and remained active for approximately 24 microseconds after the last bit was sent.

Documenting the Python SpiDev Module toc

There is an hour-long video by Tony DiCola from Adafruit, Raspberry Pi & Python SPI Deep Dive with TonyD! @adafruit LIVE which is informative even if a little dated (February 2016). At around the 23-minute mark of the video, Mr DiCola refers to a PDF file "that someone put together" to document the SpiDev module. Unfortunately the link to it is no longer valid. I was able to find SpiDev_Doc.pdf on the Wayback Machine. According to the document properties, it was written by Thomas Baumann and it dates from Dec 21, 2013. As could be expected, the document was not up to date. For reasons which remain mysterious to me and my analyst, I decided to update it. In order to do that, I wrote a Python script to test the various functions and settings of the library.

#!/usr/bin/env python3 # Version: July 10, 2020 import spidev import time import argparse DEFAULT_SPI_BUS = 0 # spidev0 DEFAULT_SPI_CS = 0 # spidev0.0 DEFAULT_SPI_CLOCK = 1000000 # 1 Mhz DEFAULT_SPI_MODE = 0 DEFAULT_SPI_BITS = 8 DEFAULT_BUFFER_SIZE = 4 # 32 bytes or less DEFAULT_FIRST_BYTE = 254 DEFAULT_TEST = "writebytes" # write Bytes as hex bytes def BytesToHex(Bytes): return ''.join(["%02X " % x for x in Bytes]).strip() def dumpAttributes(msg): print() print(msg) print(" bits_per_word: {}".format(spi.bits_per_word)) print(" cshigh: {} ".format(spi.cshigh)) print(" loop: {}".format(spi.loop)) print(" lsbfirst: {}".format(spi.lsbfirst)) print(" max_speed_hz: {}".format(spi.max_speed_hz)) print(" mode: {}".format(spi.mode)) print(" nc_cs: {}".format(spi.no_cs)) print(" threewire: {}".format(spi.threewire)) parser = argparse.ArgumentParser() parser.add_argument("-b", "--bus", type=int, default=DEFAULT_SPI_BUS, metavar='', help="SPI bus (0 for spidev0, 1 for spidev1 ...)") parser.add_argument("-c", "--cs", type=int, default=DEFAULT_SPI_CS, choices=range(0, 3), metavar='', help="Chip select (0, 1 or 2)") parser.add_argument("-i", "--cshigh",default=False, action='store_true', help="Chip select active high") parser.add_argument("-n", "--nocs", default=False, action='store_true', help="No chip select signal") parser.add_argument("-s", "--speed", type=int, default=DEFAULT_SPI_CLOCK, metavar='', help="Maximum speed (Hz)") parser.add_argument("-m", "--mode", type=int, default=DEFAULT_SPI_MODE, choices=range(0, 4), metavar='', help="Mode (0,1,2 or 3)") parser.add_argument("-w", "--word", type=int, default=DEFAULT_SPI_BITS, choices=range(8, 17), metavar='', help="Bits per word (8 to 16) (read-only)") parser.add_argument("-l", "--lsb", default=False, action='store_true', help="Least significant bits sent first, (read-only)") parser.add_argument("-o", "--loop", default=False, action='store_true', help="Loop test, (read-only)") parser.add_argument("-3", "--threewire", default=False, action='store_true', help="Three wire mode") parser.add_argument("-t", "--test", type=str, default=DEFAULT_TEST, choices=["writebytes", "writebytes2", "xfer", "xfer2", "xfer3"], metavar='', help="Test: ('writebytes', 'writebytes2', 'xfer', 'xfer2' or 'xfer3')") parser.add_argument("-r", "--repeat", type=int, default=0, metavar='', help="number of repeat transmissions, 0 (default) for endless repetitions") parser.add_argument("-C", "--xclock", type=int, default=0, metavar='', help="xfer speed (Hz)") parser.add_argument("-D", "--xdelay", type=int, default=0, metavar='', help="xfer delay before disactivating slave select (microseconds)") parser.add_argument("-W", "--xword", type=int, default=8, metavar='', help="xfer bits per word, (8 only)") parser.add_argument("-L", "--length", type=int, default=DEFAULT_BUFFER_SIZE, metavar='', help="Buffer length") parser.add_argument("-F", "--first", type=int, default=DEFAULT_FIRST_BYTE, choices=range(0, 256), metavar='', help="First byte in buffer (0 to 256)") parser.add_argument("-v", "--verbose", default=False, action='store_true', help="Display object attributes") args = parser.parse_args() # setup SPI if args.verbose: spi = spidev.SpiDev() dumpAttributes("Attributes after spidev.SpiDev()") spi.open(args.bus, args.cs) dumpAttributes("Attributes after open({}, {})".format(args.bus, args.cs)) else: spi = spidev.SpiDev(args.bus, args.cs) # set attributes spi.bits_per_word = args.word spi.cshigh = args.cshigh spi.lsbfirst = args.lsb spi.loop = args.loop spi.max_speed_hz = args.speed spi.mode = args.mode spi.no_cs = args.nocs spi.threewire = args.threewire dumpAttributes("Attributes for test") print() print("Test parameters") print(" Buffer size: {} bytes".format(args.length)) print(" First byte in buffer: {}".format(args.first)) print(" Testing method: {}".format(args.test)) if args.repeat == 0: print(" Repeat transmission endlessly") else: print(" Repeat transmission {} times".format(args.repeat)) # set up buffer bufferSize = args.length bufferFirst = args.first send = [*range(bufferSize)] for x in range(bufferSize): send[x] = bufferFirst bufferFirst += 1 if bufferFirst > 255: bufferFirst = 0 sentdata = BytesToHex(send) # common xfer execution def perform(trsfer): if args.xword: resp = trsfer(list(send), args.xclock, args.xdelay, args.xword) elif args.xdelay: resp = trsfer(list(send), args.xclock, args.xdelay) elif args.xclock: resp = trsfer(list(send), args.xclock) else: resp = trsfer(list(send)) # transfer a copy of send, otherwise xfer(send) will overwrite send loopcount = 0 maxcount = args.repeat try: while (maxcount == 0) or (loopcount < maxcount): print("") print("TX:", sentdata) # select how to write list of bytes if args.test == "writebytes": spi.writebytes(send) elif args.test == "writebytes2": spi.writebytes2(send) elif args.test == "xfer": perform(spi.xfer) elif args.test == "xfer2": perform(spi.xfer2) elif args.test == "xfer3": perform(spi.xfer3) else: print("how did this happen?") if (loopcount == 0) and args.verbose: dumpAttributes("Attributes after first transmission") firstLoop = False time.sleep(0.2) loopcount += 1 finally: spi.close()

As usual, this script (spi_explore.py) is running in a virtual Python3 environment although that is not mandatory as explained above. However this script will not work in Python 2.x which, in any case, is deprecated. Here is the help screen which hopefully is self-explanatory.

woopi@goldserver:~ $ ve spipy (spipy) woopi@goldserver:~ $ sudo chmod +x spipy/spi_explore.py (spipy) woopi@goldserver:~ $ spipy/spi_explore.py -h usage: spi_explore.py [-h] [-b] [-c] [-i] [-n] [-s] [-m] [-w] [-l] [-o] [-3] [-t] [-C] [-D] [-W] [-L] [-F] [-v] optional arguments: -h, --help show this help message and exit -b , --bus SPI bus (0 for spidev0, 1 for spidev1 ...) -c , --cs Chip select (0, 1 or 2) -i, --cshigh Chip select active high -n, --nocs No chip select signal -s , --speed Maximum speed (Hz) -m , --mode Mode (0,1,2 or 3) -w , --word Bits per word (8 to 16) (read-only) -l, --lsb Least significant bits sent first, (read-only) -o, --loop Loop test, (read-only) -3, --threewire Three wire mode -t , --test Test: ('writebytes', 'writebytes2', 'xfer', 'xfer2' or 'xfer3') -r , --repeat number of repeat transmissions, 0 (default) for endless repetitions -C , --xclock xfer speed (Hz) -D , --xdelay xfer delay before disactivating slave select (microseconds) -W , --xword xfer bits per word, (8 only) -L , --length Buffer length (1 to 32 bytes) -F , --first First byte in buffer (0 to 256) -v, --verbose Display object attributes

By default the script will write 4 bytes to spidev0.0 using the writebytes function. None of the SpiDev attributes will be changed from their default value except for the speed which is set at 1 MHz for the reason explained above. In the following commands a different slave select is chosen, and the stream of bytes is first written in SPI mode 0 and then again in SPI mode 2.

(spipy) woopi@goldserver:~ $ spipy/spi_explore.py -c 1

(spipy) woopi@goldserver:~ $ spipy/spi_explore.py -c 1 -m 2

This verifies that in mode 0 (and mode 1) the clock pulses are transitions from low to high while in mode 2 (and 3) the pulses are transitions from high to low. Here is a somewhat more detailed explanation of the mode.

ModeCPOLCPHADescription
000data is sampled at the leading rising edge of the clock
101data is sampled on the trailing falling edge of the clock
210data is sampled on the leading falling edge of the clock
311data is sampled on the trailing rising edge of the clock

The script shows that certain attributes cannot be changed.

(spipy) woopi@goldserver:~ $ spipy/spi_explore.py --word 9 Traceback (most recent call last): File "spipy/spi_explore.py", line 79, in spi.bits_per_word = args.word OSError: [Errno 22] Invalid argument (spipy) woopi@goldserver:~ $ spipy/spi_explore.py --lsb Traceback (most recent call last): File "spipy/spi_explore.py", line 88, in dumpAttributes("Attributes for test") File "spipy/spi_explore.py", line 34, in dumpAttributes print() OSError: [Errno 22] Invalid argument (spipy) woopi@goldserver:~ $ spipy/spi_explore.py --loop Traceback (most recent call last): File "spipy/spi_explore.py", line 88, in dumpAttributes("Attributes for test") File "spipy/spi_explore.py", line 34, in dumpAttributes print() OSError: [Errno 22] Invalid argument

There's a vexing problem with the SpiDev module. Exactly what is the difference between the two transfer functions xfer and xfer2? The GitHub documentation says that it is at the level of the chip select signal: xfer releases the signal between blocks while xfer2 asserts the signal throughout the complete transaction. That raises the question of what is a block of data. The default buffer size is 4,096 bytes. The three functions writebytes, xfer and xfer2 write out any list of values of 4,096 bytes or less in one chunk and the chip select signal is asserted throughout the transmission. Any attempt to transmit a list of bytes longer than 4,096 bytes with these three functions fails. In other words, xfer and xfer2 only ever send out a single block of data, so there is never a reason to release the chip select signal. As far as I can tell, these functions are identical.

The value stored in /sys/module/spidev/parameters/bufsiz is apparently the maximum size of the SPI buffer.

woopi@goldserver:~ $ cat /sys/module/spidev/parameters/bufsiz 4096

This value is easily changed by added a spidev.bufsiz=xxxx option (where xxxx is the desired buffer size) in the /boot/cmdline.txt file. Root privileges are needed to edit this file and remember to keep the content of the file on a single line. The system has to be rebooted for this change to take effect. Doubling the size of the buffer limit had surprising consequences.

woopi@goldserver:~ $ cat /sys/module/spidev/parameters/bufsiz 8192

Even after this change, the writebytes, xfer and xfer2 functions fail with any list bigger than 4,096 bytes just as before.

woopi@goldserver:~ $ ./spi_explore.py -b 0 -c 0 -t xfer2 -r 1 -L 5012 Attributes for test bits_per_word: 8 cshigh: False loop: False lsbfirst: False max_speed_hz: 1000000 mode: 0 nc_cs: False threewire: False Test parameters Buffer size: 5012 bytes First byte in buffer: 254 Testing method: xfer2 Repeat transmission 1 times TX: FE FF 00 01 02 03 04 05 06 07 0 ...D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD Traceback (most recent call last): File "./spi_explore.py", line 141, in <module> perform(spi.xfer2) File "./spi_explore.py", line 116, in perform resp = trsfer(list(send), args.xclock, args.xdelay, args.xword) OverflowError: Argument list size exceeds 4096 bytes.

In fact, the three functions completely ignore the spidev.bufsize parameter and it remains possible to send lists containing 4096 bytes using any one of them even when buffer size parameter is set to a smaller value such as 1024. On the other hand, writebytes2 and xfer3 which work with arbitrarily sized lists, will break large list in chunks sized according to the value of the spidev.bufsize parameter. The folowing three images show how the 12,000 bytes were broken up into chuncks of the specified spidev.bufsize by the xfer3 functions.

Here is the command used to capture the above images.

woopi@goldserver:~ $ ./spi_explore.py -b 0 -c 0 -t xfer3 -r 1 -L 12000

In my opinion, it would make more sense if the writebytes, xfer and xfer2 functions abided by the buffer size parameter. It is less important that the writebytes2 and xfer3 functions follow the buffer size parameter since they can handle any size list with a fixed buffer size of 4096. As can be seen, these latter two functions do release the chip select signals between blocks of data. In other words, xfer3 behaves as xfer2 should behave according to the library documentation.

The graph, which has a completely wrong horizontal scale, and the table shows the default timing of the chip select(/SS) signal when sending a block of 1024 bytes of data and when a 250 microsecond delay is specified.

Chip (Slave) SelectTime (microseconds)
PhaseSignalDefaultDelay
1. before the data blockLOW (0 V)76
2. transmission of the data blockLOW (0 V)82098201
3. after the data blockLOW (0 V)16298
4. release periodHIGH (3.3V)191183

The following command was used to add the 250 microsecond delay to generate the second set of data. These measurements, done by hand and only once, are not very accurate but they do show where the delay is added when specified in a xfer3 command.

woopi@goldserver:~ $ ./spi_explore.py -b 0 -c 0 -t xfer3 -r 1 -L 12000 -D 250

A second draft of my revised SpiDev Documentation (2020-07-12) is available. This is necessarily a draft as I have done nothing with the incoming data stream. For that I need a slave SPI device that I can control.

<-I²C Light Sensor using a Seeeduino XIAO