All 11 I/O pins of the Seeduino XIAO can be used as 12-bits analogue inputs. In section 7.2 a previous post, I showed how one of these pins could be connected to voltage divider made from a fixed resistor and a light dependant resistor (LDR) to measure light levels. In this post, the I²C capabilities of the XIAO will be exploited to make the light measure available to a Raspberry Pi which notoriously lacks analogue inputs.
In the previous post, instead of sending the light levels to a Raspberry Pi, they were displayed on an OLED display. I thought it could be worthwhile to continue to display the light levels on an LCD, independently of the Raspberry Pi. This is a bit tricky because the display is a I²C slave device and the Xiao I²C is also in slave mode to communicate with the Raspberry Pi which, as far as I know, can only act as master. The problem, of course, is that two slaves cannot communicate directly with each other. The last section of this post shows how to get around this difficulty using the U8g2 software I²C capabilities.
Table of contents
- I²C on the Raspberry Pi
- I²C Circuit
- Data From the Raspberry Pi to the Seeeduino XIAO Using I²C
- Data From the Seeeduino XIAO to the Raspberry Pi Using I²C
- A 12 Bit I²C Light Sensor
- A 12 Bit I²C Light Sensor With Local Display
By default, the hardware I²C bus controller is not enabled in
Raspbian Buster Lite. It must be enabled by including one, and only one, of the following three lines in the
/boot/config.txt configuration file.
Usually, the first
dtparam line is used unless an I²C real-time clock is also connected to the I²C bus. In that case, the second line which enables the I²C bus controller and loads a driver is used. Of course
ds3231 could be replaced by any supported RTC chip. The last option enables the I²C bus controller as the first one, but it offers more flexibility.
Without options, the third line enables the hardware I²C bus controller on the standard I/O pins just like the first line.
The Raspberry Pi must be rebooted to activate the I²C controller. Indeed, until the added line is removed from the configuration file, the
/dev/i2c-1 device will be available each time the Pi is booted. If you only want to experiment with the I²C bus for a short while, the controller can be added manually:
Once the I²C device
i2cdetect utility can be used to scan the I²C bus.
As can be seen, there are currently no I²C devices attached to the bus. If
i2cdetect is not available on the Raspberry Pi, it is easily installed with the command sudo apt install i2c-tools -y. By the way, if the default user is part of the
i2c group, it will not be necessary to use the
Since Python is the "recommended" language for the Raspberry Pi, I created a virtual Python3 environment in order to install the prerequisite
smbus module often used to communicate over the I²C bus with Python. See the section 11. Working Directories for a short presentation of how I work with virtual environments in Raspbian Buster.
If you do not want to use a virtual environment, I believe that the required
smbus module can be loaded into the default Python 3 installation with the bash command sudo apt install python3-smbus.
The hardware I²C controllers of both devices will be used. I²C 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 I²C 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).
Although not typical of I²C 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 I²C 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 first step is to verify that the XIAO can be used as an I²C slave. In the following sketch, the XIAO will echo to the Arduino IDE serial monitor the string and integer data sent by the I²C 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 I²C device. I will come back to the sketch later.
Once the sketch is running on the XIAO, it was possible to check that it showed up as a device on the Raspberry Pi I²C 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 I²C 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 I²C 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 I²C register to which the following data stream is written. I have arbitrarily set the register value at 1 in the Python script. The XIAO I²C 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
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 I²C Communication by Mike Murray. My thanks to both these sources.
The XIAO is more likely to be a peripheral device that will be sending data to the Rapsberry Pi. It will nevertheless remain an I²C slave and only send data to the Raspberry Pi in response to a request from the later which remains the I²C master.
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.
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 I²C 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 I²C 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.
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 I²C 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.
In my previous post about the Seeeduino XIAO, the light levels as measured by the LDR were shown on an I²C display. I wanted to do something similar here, but there is a complication. The I²C bus is already being used to send the LDR measurements to the Raspberry Pi. The XIAO hardware I²C controller is thus started in slave mode. However the OLED display is itself an I²C bus slave which means only an I²C 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 I²C 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 I²C 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 I²C bus,
u8x8 object was created without specifying the SDA and SCL pins.
However, in this script using a software I²C 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 I²C request in a
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 I²C bus does work, it is noticeably slower. I can envision four ways of avoiding this problem.
- Connect the OLED display to the hardware I²C bus and use a software I²C bus to connect to the Raspberry Pi.
Unfortunately, I have not found a software I²C implementation for the Sam D21 that would allow the device to run as an I²C slave. The U8g2 library contains at least part of such an implementation so it could perhaps be a starting point.
- Use a second hardware I²C bus for the OLED display.
I have verified that it is possible to set up a I²C bus on pins A6 and A7. So far, my solution, which requires modyfing the
variant.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 I²C bus as another I²C 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 I²C bus and let the XIAO act as a slave I²C device most of the time but become an I²C 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 I²C 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 I²C such as a real time clock and so on. In the circumstances, I will move on to enabling a second hardware I²C bus which does work.