md
Serial Peripheral Interface on the Raspberry Pi
May 10, 2020
<-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.

Consider this a draft, because some parts will certainly need to be updated and there may very well be some additional material in the future. Changes will have to wait until I have a bit more time and until the box of microcontrollers carefully put aside when preparing to move the office is found. Question: What does one do during a pandemic induce lockdown? Answer: Switch offices with one's spouse. That may sound like a small job, but a lot of things have accumulated over the years. That included books which occupy 32 metres (105 feet) of shelves that had to be brought from an upstairs office to a basement office and workshop. Moving them was not much fun, but organizing them is some sort of reasonable order is even less enjoyable. The final result should prove that the effort was worthwhile.

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 SpiDev

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 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 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

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.

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 Raspbian Buster Lite. 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

Interpret these files ("everything is a file in Linux") 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 /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. 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.

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 enc28j60-spi2 jedec-spi-nor sc16is752-spi1 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

Some overlays are obviously drivers for SPI devices such as an Ethernet controller, a real-time clock and flash memory. 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 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. So what happens if one tries to install one of those drivers on a Raspberry Pi 3 B?

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

The installation will fail and it may be flagged as such on the console 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

The Richard Hull version of the spidev-test originally written in C by Anton Vorontsov is used. 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 | ................................ connect MOSI and MISO together 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.0 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.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 | ......@....�..................�.

Of course, 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

If you do not want to use a virtual environment, I believe that the required spidev module can be loaded into the default Python 3 installation with the bash command sudo apt install python3-spydev based on the following, but I have not tested that.

woopi@goldserver:~ $ sudo apt-cache policy python3-spidev python3-spidev: Installed: (none) Candidate: 20190221~182651-1 Version table: 20190221~182651-1 500 500 http://archive.raspberrypi.org/debian buster/main armhf Packages

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

Or, the script can be marked as executable and bash will start the interpreter automatically because of the first "shebang" line of the script.

(spipy) woopi@goldserver:~/spipy $ chmod +x spi_loopback_test.py (spipy) woopi@goldserver:~/spipy $ ./spi_loopback_test.py 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] ...

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, but I have not yet tested that possibility.

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 ball-park 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 SpiDev 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, but I was not able to identify the original author. 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 that allows me to test the various functions and settings of the library.

#!/usr/bin/env python # spi_explore.py 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("-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)) # 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 firstLoop = True try: while True: 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 firstLoop and args.verbose: dumpAttributes("Attributes after first transmission") firstLoop = False time.sleep(0.2) finally: spi.close()

As usual, this script (spi_explore.py) is running in a virtual Python3 environment. After making it executable, I printed out the help 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') -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. Just 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. As far as I can tell, the buffer size is 4,096 bytes. The three functions writebytes, xfer and xfer2 write out the any list of values of that 4,096 bytes 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 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. The functions are identical.

The maximum size of the SPI buffer is seemingly specified in the file /sys/module/spidev/parameters/bufsiz.

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 would fail with any list bigger than 4,096 bytes just as before. As expected, writebytes2 and xfer3 would work with bigger lists. The strange thing is that these functions broke the large list in chunks of 8,192 bytes. The first three functions ignore the buffer size parameter while the last two abide by it even if it is less important in their case.

A first draft of my revised SpiDev Documentation 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, an ESP8266 perhaps?

<-I²C Light Sensor using a Seeeduino XIAO --