2020-04-13
md
SPI Light Sensor using a Seeeduino XIAO
<-I²C Light Sensor using a Seeeduino XIAO
<-Overview of the SAMD21 Arm Cortex-M0+ Based Seeeduino XIAO

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

  1. Serial Peripheral Interface - SPI
  2. SPI on the Raspberry Pi
    1. spidev0
    2. spidev1
    3. SPI Loopback Test in C
    4. SPI Loopback Test in Python
  3. SPI Circuit
  4. Data From the Raspberry Pi to the Seeeduino XIAO Using SPI
    1. The Seeeduino XIAO as an SPI Slave Receiving Data
    2. The Raspberry Pi as the SPI Master Sending Data
  5. Data From the Seeeduino XIAO to the Raspberry Pi Using SPI
    1. The Seeeduino XIAO as an SPI Slave Sending Data
    2. The Raspberry Pi as the SPI Master Requesting Data
  6. A 12 Bit SPI Light Sensor
  7. A 12 Bit SPI Light Sensor With Local Display

Serial Peripheral Interface - SPI toc

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.

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

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 toc

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
SPI_MOSI1019
SPI_MISO921
SPI_SCLK1123
SPI_CEO_N824
SPI_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

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

(MOSI, MISO and SCLK)

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.

woopi@goldserver:~ $ ls /dev/spi* ls: cannot access '/dev/spi*': No such file or directory woopi@goldserver:~ $ sudo dtparam spi=on 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
spi-gpio40-45

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.

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

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

woopi@goldserver:~/spidev_test $ cd ~ woopi@goldserver:~ $ mkvenv xiao_spi ... woopi@goldserver:~ $ ve xiao_spi (xiao_spi) 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 xiao_spi woopi@goldserver:~ $ cd xiao (xiao_spi) woopi@goldserver:~/xiao_spi $ nano spi_loopback_test.py

#!/usr/bin/env python # loopback test script # connect MOSI and MISO import spidev import time spi = spidev.SpiDev(0, 0) spi.max_speed_hz=1000000 #necessary! try: v = 0 while True: v1 = v + 1 send = [v, v1] print("") print("TX:", send) print("RX:", spi.xfer(send)) time.sleep(0.5) if v1 >= 255: v = 0 else: v = (v1+1) except KeyboardInterrupt: spi.close()

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

(xiao_spi) woopi@goldserver:~/xiao_spi $ 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.

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

Note that there are two equivalent ways to instantiate and initialize a SpiDev object:

spi = spidev.SpiDev() spi.open(bus, device) or spi = spidev.SpiDev(bus, device)

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.

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

#!/usr/bin/env python # SPI loopback speed test script # Connect MOSI and MISO (physical pins 9 and 11 (GPIO10 = MOSI and GPIO9 = MISO) on Raspberry Pi) 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 except KeyboardInterrupt: spi.close()

The script output showed that a 32MHz frequency was possible but not 65MHz.

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

SPI Circuit toc

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 toc

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 toc

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.

/* * i2c_slave_rx.ino */ #include <Wire.h> #define I2C_SLAVE_ADDRESS  4 #define BAUD 115200 void setup() {  // waiting for the serial monitor    Serial.begin(BAUD);  while (!Serial) delay(10);  // setup the Wire libray with device as SPI slave  Wire.begin(I2C_SLAVE_ADDRESS);    // join i2c bus  Wire.onReceive(receiveEvent);     // assign data received handler  Serial.println("\nI2C Slave");  Serial.print("Listening "); } void loop() {  delay(100);  Serial.print("."); } // received data block handler void receiveEvent(int count) {  int reg = Wire.read();  Serial.printf("\nReceived %d bytes for register %d: \"", count-1, reg);  // loop through all but the last byte  while(Wire.available() > 1) {    char c = Wire.read(); // receive each byte as a character    if (c >= ' ')                     Serial.print(c);                   // print the received character c    else      Serial.printf("(#%d)", (byte) c);  // print received ord(c) - should not occur  }  int x = Wire.read();        // receive the last byte as an integer  Serial.printf("%d\"", x);   // print the integer }

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 toc

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.

woopi@goldserver:~ $ ls /dev/i2* /dev/i2c-1 woopi@goldserver:~ $ i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- 04 -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: -- -- -- -- -- -- -- --

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.

(xiao_spi) woopi@goldserver:~/xiao_spi $ nano i2c_tx.py

''' i2c_tx.py ''' import smbus # XIAO Slave Addresses XIAO_ADDRESS = 0x04 # Create the SPI bus I2Cbus = smbus.SMBus(1) x = 0 # This function converts a string to an array of bytes. def ConvertStringToBytes(src): converted = [] for b in src: converted.append(ord(b)) converted.append(x) return converted counter = 0 while True: BytesToSend = ConvertStringToBytes("x is ") x = (x+1) % 256 I2Cbus.write_i2c_block_data(XIAO_ADDRESS, 0x01, BytesToSend) counter = counter + 1 print('.', end='', flush=True) if counter % 80 == 0: print('')

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.

(xiao_spi) woopi@goldserver:~/xiao_spi $ python i2c_tx.py ................................................................................ ................................................................................ ................................................................................ ................................................................................

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.

I2C Slave Listening ..................................................... Received 6 bytes for register 1: "x is 0" Received 6 bytes for register 1: "x is 1" Received 6 bytes for register 1: "x is 2" Received 6 bytes for register 1: "x is 3" Received 6 bytes for register 1: "x is 4" Received 6 bytes for register 1: "x is 5" Received 6 bytes for register 1: "x is 6" Received 6 bytes for register 1: "x is 7" Received 6 bytes for register 1: "x is 8" Received 6 bytes for register 1: "x is 9" Received 6 bytes for register 1: "x is 10" Received 6 bytes for register 1: "x is 11"

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.

.................^CTraceback (most recent call last): File "i2c_tx.py", line 46, in <module> I2Cbus.write_i2c_block_data(XIAO_ADDRESS, 0x01, BytesToSend) KeyboardInterrupt (xiao_spi) woopi@goldserver:~/xiao_spi $

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 toc

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 toc

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.

/* * i2c_slave_tx.ino */ #include <Wire.h> #define I2C_SLAVE_ADDRESS  4 #define BAUD 115200 void setup() {  // start serial port waiting up to 20 seconds  Serial.begin(BAUD);  unsigned long startserial = millis();  while (!Serial && (millis() - startserial < 20000)) delay(10); // Waiting for Serial Monitor      // setup the Wire libray with device as SPI slave  Wire.begin(I2C_SLAVE_ADDRESS);    // join i2c bus  Wire.onRequest(requestEvent);     // assign data request handler  Serial.println("\nI2C Slave");  Serial.print("Waiting for data request "); } void loop() {  delay(100);  Serial.print("."); } // request data block handler void requestEvent(void) {  int x = 128 + random(4023 - 128);       // generate random 16 bit value in the 128..4023 range  Wire.write((x >> 8) & 0xFF);            // send higer 8 bits of data  Wire.write(x & 0xFF);                   // send lower 8 bits of data  Serial.printf("\nSent data %d\n", x);   // print the integer }

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 toc

As can be seen, the script is simple.

''' i2c_rx.py ''' import smbus import time # XIAO Slave Addresses XIAO_ADDRESS = 0x04 # Create the SPI bus I2Cbus = smbus.SMBus(1) while True: data = I2Cbus.read_i2c_block_data(XIAO_ADDRESS, 0x00, 2) value = (data[0] << 8) + data[1]; print("Received value {}".format(value)) time.sleep(5)

This is the output when running the script on the Raspberry Pi.

(xiao_spi) woopi@goldserver:~/xiao_spi $ python i2c_rx.py Received value 2896 Received value 1066 Received value 2360 Received value 742 Received value 1448 Received value 3926 ^CTraceback (most recent call last): File "i2c_rx.py", line 37, in time.sleep(5) KeyboardInterrupt

And this is the serial output from the other end.

I2C Slave Waiting for data request ................................................................ Sent data 2896 .................................................. Sent data 1066 .................................................. Sent data 2360 .................................................. Sent data 742 .................................................. Sent data 1448 ................................................... Sent data 3926 ................

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.

(xiao_spi) woopi@goldserver:~/xiao_spi $ python i2c_rx.py Traceback (most recent call last): File "i2c_rx.py", line 35, in value = (data[0] << 8) + data[1]; IndexError: list index out of range

A 12 Bit SPI Light Sensor toc

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.

/* * i2c_slave_ldr_sensor.ino */ #include <Wire.h> #define BAUD 115200             // Baud for serial port (/dev/ttyACM0 on Linux) #define I2C_SLAVE_ADDRESS  4    // arbitray choice in range (0x03 to 0x77) different from other devices on bus #define LDR 3                   // Analog input pin void setup() {  Serial.begin(BAUD);  Wire.begin();  while (!Serial) delay(10); // Waiting for Serial Monitor    pinMode(LDR, INPUT);  analogReadResolution(ADC_RESOLUTION);  // #define ADC_RESOLUTION 12 in header file       Serial.println("\nI2C Slave");  Serial.print("Waiting for data request ");  Wire.begin(I2C_SLAVE_ADDRESS);    // join i2c bus  Wire.onRequest(requestEvent);     // assign data request handler } void loop() {  delay(100);  Serial.print("."); } // request data block handler void requestEvent(void) {  int level = analogRead(LDR);                  // LDR value between 0 and 4095 = (2^12)-1  Wire.write((level >> 8) & 0xFF);              // send higer 8 bits of data  Wire.write(level & 0xFF);                     // send lower 8 bits of data  Serial.printf("\nLight level: %d\n", level);  // print the LDR value }

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.

''' i2c_light_sensor.py ''' import smbus import time # XIAO Slave Addresses XIAO_ADDRESS = 0x04 # Create the SPI bus I2Cbus = smbus.SMBus(1) # average of 3 readings of the LDR def readSensor(): value = 0; for i in range(3): data = I2Cbus.read_i2c_block_data(XIAO_ADDRESS, 0x00, 2) value = value + (data[0] << 8) + data[1]; return value // 3 while True: print("LDR value {}".format(readSensor())) time.sleep(5)

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 toc

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.

// Using hardware IIC bus (SDA: 4, SCL: 5) with noname 128x64 0.96" OLED display U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* reset=*/ U8X8_PIN_NONE);

However, in this script using a software SPI bus, the SDA and SCL pins need to be specified.

#define SOFT_I2C_SDA 6 // I/O pin connected to the display SPI SDA pin #define SOFT_I2C_SCL 7 // I/O pin connected to the display SPI SCL pin // Using software IIC bus with noname 128x64 0.96" OLED display U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(/*clock*/ SOFT_I2C_SCL, /*data*/ SOFT_I2C_SDA, /* reset=*/ U8X8_PIN_NONE);

Here is the complete sketch.

/* * i2c_slave_ldr_sensor.ino */ #include <Wire.h> #include <U8g2lib.h> #define BAUD          115200    // Baud for serial port (/dev/ttyACM0 on Linux) #define I2C_SLAVE_ADDRESS  4    // arbitray choice in range (0x03 to 0x77) different from other devices on bus #define LDR                3    // Analog input pin #define LDR_DELAY       1000    // Delay (ms) between readings of LDR value #define LDR_VALUES_SIZE   10    // Number of old values retained #define REFRESH_DELAY   2000    // Delay (ms) between updates of LDR value on display #define SOFT_I2C_SDA       6    // I/O pin connecete to the display SPI SDA pin #define SOFT_I2C_SCL       7    // I/O pin connecete to the display SPI SCL pin // Using software IIC bus with noname 128x64 0.96" OLED display U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(/*clock*/ SOFT_I2C_SCL, /*data*/ SOFT_I2C_SDA, /* reset=*/ U8X8_PIN_NONE); // variables used for rolling average LDR value int ldrValues[LDR_VALUES_SIZE] {0}; int ldrIndex = 0; int ldrCount = 0; int ldrSum = 0; // adds the a new LDR value and returns the new average int addLdrValue(int newValue) {  ldrSum = ldrSum - ldrValues[ldrIndex] + newValue;  ldrValues[ldrIndex] = newValue;  if (ldrCount < LDR_VALUES_SIZE) ldrCount++;  ldrIndex = (ldrIndex + 1) % LDR_VALUES_SIZE;  return (int) ldrSum / ldrCount; } // returns the last LDR value added to queue int lastLdrValue(void) {  if (ldrCount < 1) {    return -1;    } else {    int i = ldrIndex - 1;    if (i < 0) i = LDR_VALUES_SIZE - 1;    return ldrValues[i];  }      } void setup() {  // Start serial port waiting up to 20 seconds  Serial.begin(BAUD);  unsigned long startserial = millis();  while (!Serial && (millis() - startserial < 20000)) delay(10);  // setup the Wire libray with device as SPI slave  Wire.begin(I2C_SLAVE_ADDRESS);    // join i2c bus  Wire.onRequest(requestEvent);     // assign data request handler  // setup the analog input for the LDR voltage divider  pinMode(LDR, INPUT);  analogReadResolution(ADC_RESOLUTION);  // #define ADC_RESOLUTION 12 in header file     // setup the OLED display  u8x8.begin();  u8x8.setPowerSave(0);  u8x8.setFont(u8x8_font_chroma48medium8_r);      Serial.println("Test of SPI light sensor with concurrent display using software SPI");  Serial.printf("Hardware SPI bus: SDA: A%d, SCL: A%d - XIAO is a bus slave\n", SDA, SCL);    Serial.printf("Software SPI bus: SDA: A%d, SCL: A%d - XIAO is a bus master\n", SOFT_I2C_SDA, SOFT_I2C_SCL);    Serial.print("Waiting for data request "); } int avgLdrLevel; char buf[200]; unsigned long ldrReadTime = 0; unsigned long refreshTime = 0; void loop(){  if (millis() - ldrReadTime >= LDR_DELAY) {    avgLdrLevel = addLdrValue(analogRead(LDR));    ldrReadTime = millis();  }  if (millis() - refreshTime >= REFRESH_DELAY) {    sprintf(buf, "LDR level: %d", avgLdrLevel);    u8x8.clearLine(0);    u8x8.drawString(0,0, buf);    Serial.printf("Last:%d Average:%d\n", lastLdrValue(), avgLdrLevel);    refreshTime = millis();  } } // request data block handler void requestEvent(void) {  int level = avgLdrLevel;                      // LDR value between 0 and 4095 = (2^12)-1  Wire.write((level >> 8) & 0xFF);              // send higer 8 bits of data  Wire.write(level & 0xFF);                     // send lower 8 bits of data }

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.

''' light_sensor.py ''' import smbus import time # XIAO Slave Addresses XIAO_ADDRESS = 0x04 # Create the SPI bus I2Cbus = smbus.SMBus(1) def readSensor(): try: data = I2Cbus.read_i2c_block_data(XIAO_ADDRESS, 0x00, 2) return (data[0] << 8) + data[1]; except OSError as e: print(str(e)) while True: print("Average LDR value {}".format(readSensor())) time.sleep(5)

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.

(xiao_spi) woopi@goldserver:~/xiao_spi $ python light_sensor.py Average LDR value 1035 Average LDR value 1069 Average LDR value 1222 Average LDR value 1970 Average LDR value 2577 Average LDR value 2567 ... disconnecting the USB cable to the XIAO ... [Errno 121] Remote I/O error Average LDR value None [Errno 121] Remote I/O error Average LDR value None ... connecting the Raspberry Pi 5 volt output to the XIAO 5 volt input ... don't forget the 20 seconds wait while opening the serial port times out ... Average LDR value 709 Average LDR value 704 Average LDR value 704 Average LDR value 699

While the software SPI bus does work, it is noticeably slower. I can envision four ways of avoiding this problem.

<-I²C Light Sensor using a Seeeduino XIAO
<-Overview of the SAMD21 Arm Cortex-M0+ Based Seeeduino XIAO