In a previous post, a light sensor, using a simple light dependant resistor connected to an analogue input pin the Seeduino XIAO, was connected to a Raspberry Pi with the I2C bus. There was a problem when an I2C display was added to make the light sensor into an independent device. Because the XIAO cannot act as both an I2C bus master and a slave a second I2C bus had to be created. But an obvious second solution would have been to use another serial bus between the Pi and the XIAO. That's exactly what will be done in this post, using the SPI (Serial Peripheral Interface) capabilities of both the XIAO and the Pi.
Table of contents
- Serial Peripheral Interface - SPI
- SPI on the Raspberry Pi
- SPI Circuit
- Data From the Raspberry Pi to the Seeeduino XIAO Using SPI
- Data From the Seeeduino XIAO to the Raspberry Pi Using SPI
- A 12 Bit SPI Light Sensor
- A 12 Bit SPI Light Sensor With Local Display
Serial Peripheral Interface - SPI
As is often the case on this site, this post is a learning tool as I become a little bit more familiar with a new-to-me technology. So far, my only contact with SPI had been a frustrating but ultimitaley successful attempt at connecting a relatively uncommon SPI LCD display with an ESP8266, an Arduino Uno and an Orange Pi. Here is how two devices, the XIAO and the LCD, could be connected to the Raspberry Pi using the SPI.

The table summarizes the logic signals.
| Label | Function | Alternate labels |
|---|---|---|
| SCLK | Serial Clock (output from master) | SCK, CLK |
| MOSI | Master Output Slave Input, or Master Out Slave In (data output from master) | SDI, DIN, SI |
| MISO | Master Input Slave Output, or Master In Slave Out (data output from slave) | SDO, DOUT, SO |
| SS | Slave Select (often active low, output from master) | CE, SSEL, CS |
There will be no further mention of the LCD in this post, but I thought it would be valuable to show the typical topology of a SPI bus. There can be only one master and multiple slaves. The master must enable any slave with a dedicated signal to communicate with it. 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.
- Should the clock signal be a 0 to 1 pulse or a 1 to 0 pulse? That is called its polarity.
- Should the trailing or leading edge of the clock pulse be used to latch the data? That is called the clock phase.
- What is the bit order? Least significant bit first or last ?
- How many bits in a single word transmitted ?
- How fast should the clock signal be?
- Is the slave select active low or can it be high?
- Is the slave select signal needed when there is only one slave?
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, one of the better articles I have read on Wikipedia.
SPI on the Raspberry Pi
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.
| Signal | GPIO pin | Physical pin |
|---|---|---|
| SPI_MOSI | 10 | 19 |
| SPI_MISO | 9 | 21 |
| SPI_SCLK | 11 | 23 |
| SPI_CEO_N | 8 | 24 |
| SPI_CE1_N | 7 | 26 |
Newer models with a 40 GPIO header have a second SPI bus which has up to three slave select signals.
| Signal | GPIO pin | Physical pin |
|---|---|---|
| SPI1_MOSI | 20 | 38 |
| SPI1_MISO | 19 | 35 |
| SPI1_SCLK | 21 | 40 |
| SPI1_CEO_N | 18 | 22 |
| SPI1_CE1_N | 17 | 11 |
| SPI1_CE2_N | 16 | 36 |
On the Raspberry Pi, bus is always 0 as there is only one bus. And device, which actually refers to the chip select signal, can be 0 (for CE0, GPIO8, physical pin 24) or 1 (for CE1, GPIO7, physical pin 26).
By default, the SPI kernel driver is not loaded in Raspbian Buster Lite. That can be done on a one-off basis with the dtparam utility.
Loopback test using Richard Hull version of spidev-test originally written by Anton Vorontsov. The hardware preparation is quite simple: connect pins 19 and 21 (GPIO10 = MOSI and GPIO 9 = MISO respectively) to form a loopback connection. I first ran the test without the loopback connection and then again after making the connection to see the difference.
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.
Let's do something similar with a Python script instead of a C++ program. First I created a virtual environment and then installed the Python spidev module by Stephen Caudle (doceme).
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.
Then I used nano to create the following script.
The script can be executed invoking the Python 3 interpreter directly.
Or, the script can be marked as executable and bash will start the interpreter automatically because of the first "shebang" line of the script.
Note that there are two equivalent ways to instantiate and initialize a SpiDev object:
On the Raspberry Pi, bus is always 0 as there is only one bus. And device, which actually refers to the chip select signal, can be 0 (for CE0, GPIO8, physical pin 24) or 1 (for CE1, GPIO7, physical pin 26).
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.
| Attribute | spidev.SpiDev() | spidev.SpiDev(0,x) |
|---|---|---|
| bits_per_word | 0 | 8 |
| cshigh | False | False |
| loop | False | False |
| lsbfirst | False | False |
| max_speed_hz | 0 | 125000000 |
| mode | 0 | 0 |
| no_cs | False | False |
| threewire | False | False |
At first I was mislead by the loop setting, which I thought needed to be set to true in a loopback 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.treewire and spi.loop are unsupported on the Raspberry Pi.
After looking at numerous examples on connecting analog to digital converters using SPI, I finally twigged on the fact that the default speed of 125 MHz was considerable 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.
The script output showed that a 32MHz frequency was possible but not 65MHz.
SPI Circuit
https://www.raspberrypi.org/documentation/hardware/raspberrypi/spi/README.md
SPI
Devices: /dev/spidev0.0 and /dev/spidev0.1
The SPI bus is available on the P1 Header:
MOSI P1-19
MISO P1-21
SCLK P1-23 P1-24 CE0 spidev0.0
GND P1-25 P1-26 CE1 spidev0.1
The hardware SPI controllers of both devices will be used. SPI is a two-wire bus but the slave and master must also share a common ground so a 3 wire connection must be made between the XIAO and the Raspberry Pi. It could not be simpler: connect the grounds together, connect the clock signals (SCL) together, and connect the data signals (SDA) together.

Because the Raspberry Pi 3 has integrated pull-up resistors on the SPI bus, none needed to be added. Not shown is the USB cable from a desktop computer to the XIAO which provides power to the latter as well as enabling serial communication and firmware downloads (or uploads depending on the point of view).
Data From the Raspberry Pi to the Seeeduino XIAO Using SPI
Although not typical of SPI data flows, I decided to start by having the Raspberry Pi send data to the XIAO for the simple reason that one of the first sources on the subject of SPI communication I read was the Master Writer / Slave Receiver tutorial by Nicholas Zambetti on the Arduino site. That tutorial, like many others on the Web, uses two Arduinos but it is not too difficult to use a Raspberry Pi as the master as shown below.
The Seeeduino XIAO as an SPI Slave Receiving Data
The first step is to verify that the XIAO can be used as an SPI slave. In the following sketch, the XIAO will echo to the Arduino IDE serial monitor the string and integer data sent by the SPI master. The sketch is quite simple.
The sketch must be running on the XIAO so that the Raspberry Pi will find the XIAO as a SPI device. I will come back to the sketch later.
The Raspberry Pi as the SPI Master Sending Data
Once the sketch is running on the XIAO, it was possible to check that it showed up as a device on the Raspberry Pi SPI bus.
As can be seen, the XIAO was found at the address specified in the Arduino sketch running on the XIAO. It is now possible to create a Python script will repeatedly send a string followed by a small integer over the SPI bus.
Once the script is created, it can be launched. Quickly, dots should appear on the screen after each chunk of data is sent to the XIAO over the SPI bus.
If a virtual environment is not used, the appropriate command would be python3 i2c_tx.py. Here is what the XIAO displays in the Arduino IDE serial monitor. Here is the output from the XIAO sent to the Arduino IDE serial monitor.
If the XIAO sketch is examined, it is clear that the handler is told that 7 bytes have been received not 6. The first byte identifies the SPI register to which the following data stream is written. I have arbitrarily set the register value at 1 in the Python script. The XIAO SPI bus is not a hardware device with multiple registers such as a real-time clock, and that first destination address can be ignored. If the smbus function write_byte had been used in the Python script to write the data one bite at a time, there would be no destination register address, but it would have become necessary to send a guard byte (say 0x00) to signal the end of the string.
To stop the sender script, press the CtrlC keyboard combination.
The two main references for the Arduino sketch and Python script are Wire Slave Receiver by Nicholas Zambetti already mentioned and Raspberry Pi to Arduino SPI Communication by Mike Murray. My thanks to both these sources.
Data From the Seeeduino XIAO to the Raspberry Pi Using SPI
The XIAO is more likely to be a peripheral device that will be sending data to the Rapsberry Pi. It will nevertheless remain an SPI slave and only send data to the Raspberry Pi in response to a request from the later which remains the SPI master.
The Seeeduino XIAO as an SPI Slave Sending Data
At first glance, the sketch appears simpler than the previous one. Actually it is rather similar with the exception that a handler for the Wire.onRequest event is assigned instead of the Wire.onReceive event. The Raspberry Pi will initiate the transaction by requesting a specific number of bytes from the XIAO and the latter will respond by sending a random 16 bit integer as two distinct bytes.
There is another change in the sketch which is rather important and thus worth pointing out. The setup function does not wait indefinitely for the Serial object to be created. After 20 seconds the rest of the script executed even without Serial in place. That way, the script can be executed without the XIAO being connected to the desktop computer. The numerous Serial.print statements will not cause a problem.
It was then possible to verify that the 5 volt output from the Raspberry Pi GPIO header (pin 2 or 4) can power the XIAO through its 5 volt input pin.
The Raspberry Pi as the SPI Master Requesting Data
As can be seen, the script is simple.
This is the output when running the script on the Raspberry Pi.
And this is the serial output from the other end.
Testing shows that the SPI register parameter in the read_i2c_block_data() function, which was set at 0x00 in the script, has no impact. Again the XIAO has not SPI register. As for the byte count parameter, as long as it is at least as large as the data begin sent by the slave, the function should work. However, if the 2 is replaced with a 1 in the function there will be an error because the list returned by the function, which is assigned to data, will contain only one element.
A 12 Bit SPI Light Sensor
It is a simple matter to transform the previous example into a useful project. Instead of generating random values to send to the Raspberry Pi over the SPI bus, the data sent will be voltage levels across a voltage divider in which one resistor is a light dependant resistor. This method of measuring light levels with the Seeeduino XIAO has already been investigated so there is nothing really new here.

The sketch to run on the XIAO is almost the same as that presented above. The random data generator is replace by a simple analogue read of an input signal connected to the voltage divider.
Similarly, the Python script on the Raspberry Pi is almost the same as before. The only new element, is that three light level measurements are made and the average is printed out.
This is just a "proof of concept" script. In practice, the script is too brittle; a minimum of error handling needs to be introduced.
A 12 Bit SPI Light Sensor With Local Display
In my previous post about the Seeeduino XIAO, the light levels as measured by the LDR were shown on an SPI display. I wanted to do something similar here, but there is a complication. The SPI bus is already being used to send the LDR measurements to the Raspberry Pi. The XIAO hardware SPI controller is thus started in slave mode. However the OLED display is itself an SPI bus slave which means only an SPI bus master can send data to it. It was not clear from the little bit of research I did on the subject that the XIAO could alternate between being a bus slave and a bus master. And, nominally there is only one SPI on the Xiao (this is not quite true and the possibility of adding a second bus will be discussed in a later post). So for a first attempt at this, I took the easy way out and connected the display to two other I/O lines and used the U8g2 software SPI driver.

As can be seen, pins A6 and A7, which can be TX and RX signals of a UART, are used to talk to the display, but any other two pins out of the seven that were free could have been used. When using the hardware SPI bus, u8x8 object was created without specifying the SDA and SCL pins.
However, in this script using a software SPI bus, the SDA and SCL pins need to be specified.
Here is the complete sketch.
In the previous section, the Raspberry Pi averaged three consecutive readings. In this newer version, I decided that the XIAO had plenty of power to do the averaging on its own. So if you look at the loop() function, you will see that two things are being done at regular intervals. The voltage at the LDR junction is being read once a second and a running average value, avgLdrLevel is updated at each reading. Less frequently, every two seconds, the current average value is displayed on the OLED screen. Requests for the current average LDR value from the Raspberry Pi are being answered in the background as they come in.
The function that updates the average LDR value, addLdrValue is pretty simple. The latest values read from the LDR are kept in a FIFO (first in, first out) queue. The size of the queue is defined by the LDR_VALUES_SIZE macro. When a new value is read from the sensor, it replaces the oldest value in the queue and then the average of all the valid values is returned. The counter ldrCount keeps track of the number of valid entries in the queue. Once the queue is filled, ldrCount=LDR_VALUES_SIZE will remain true. There is no need to calculate the average ex nihilo. Instead, there is a running sum of all valid values in the queue, ldrSum which gets updated at each new reading. It is not much of a saving if the queue is short because ldrCount additions are being replaced with one addition and one subtraction, but I think it looks neat and when the buffer is made larger, the averaging will make the sensor a better low light level indicator for the home automation system.
The Raspberry Pi script can be slightly simplified because there is no longer any need to do any averaging at this end. The other change made to the script improves its reliability. By enclosing the SPI request in a try except block, execution is not interrupted should the sensor be disconnected.
Do not forget to enable the /dev/i2c-1 controller and to start the Python virtual environment, if you are using the latter, before executing the script.
While the software SPI bus does work, it is noticeably slower. I can envision four ways of avoiding this problem.
- Connect the OLED display to the hardware SPI bus and use a software SPI bus to connect to the Raspberry Pi.
Unfortunately, I have not found a software SPI implementation for the Sam D21 that would allow the device to run as an SPI slave. The U8g2 library contains at least part of such an implementation so it could perhaps be a starting point.
- Use a second hardware SPI bus for the OLED display.
I have verified that it is possible to set up a SPI bus on pins A6 and A7. So far, my solution, which requires modyfing the
variant.handvariant.cppfiles, is not elegant and not thoroughly tested. I hope to return to this topic in a future post. - Connect the OLED display to the hardware SPI bus as another SPI slave alongside the XIAO and let the Raspberry Pi update the display.
I am sure it would work, but it defeats my goal as I wanted to offload the Raspberry Pi and have the XIAO act as an independent light meter.
- Connect the OLED display to the hardware SPI bus and let the XIAO act as a slave SPI device most of the time but become an SPI master when the display needs to be updated.
So far, I have not managed to get this to work reliably, except if the Raspberry Pi requests for light levels occur on a strict schedule which is not a general enough solution. From what I have been able to gather, the SAM D21 can handle an SPI bus with multiple masters but not the Raspberry Pi, but I am not sure that is the problem. Even if this did work, there would have to be tests with other slave devices on the SPI such as a real time clock and so on. In the circumstances, I will move on to enabling a second hardware SPI bus which does work.
I²C Light Sensor using a Seeeduino XIAO