The XIAO family of microcontroller boards offers a wide range of options. In a compact size of just 20 x 17.5 mm, you can find models equipped with various versions of ESP32, Raspberry microcontrollers (RP2040 and RP2350), the Renesas RA4M1, and Silicon Labs' MG24, among others. These models differ in RAM and Flash memory capacity, communication interfaces like WiFi, Bluetooth, or Zigbee, and peripherals for real-world interaction, such as microphones, cameras, and accelerometers. Programming options are also versatile, allowing the use of Arduino, Micropython, or CircuitPython for traditional algorithmic approaches or Machine Learning models for those venturing into AI.
In this project, we will use the XIAO RP2040 along with two expansion boards to simplify connections: the Expansion Board Base, which includes an OLED display that we will use to show numerical values and images, and a Grove interface board equipped with the DHT11 sensor for measuring temperature and humidity.
To make the project unique and take full advantage of the Expansion Board's capabilities, while also practicing programming in Micropython, we will implement various ways to visualize the measurements. This will include functions to display maximum, minimum, and average values, as well as graphs of recent temperature and humidity readings.
The Expansion BoardTo test prototypes and programs with various XIAO models, Seeed Studio offers the Expansion Board Base, a versatile multifunction board equipped with several accessories, including:
- 64 x 128 pixel OLED display.
- Socket for connecting the XIAO board.
- Connectors to access all XIAO pins.
- Four Grove connectors.
- Lipo battery charger with connector and charge indicator LED.
- Servo motor connector.
- RTC (real-time clock) powered by a CR1220 battery.
- SD card slot.
- RESET button.
- Multi-purpose user button.
- Passive buzzer.
- Debug connectors (SWD).
In this multifunction monitor, we will use the OLED display, the buzzer, and the user button included on the Expansion Board. To measure temperature and humidity, we will add a Grove module with the DHT11 sensor. All these components are connected as shown in the following image.
The Grove connectors on the Expansion Board serve different functions and are connected to various pins on the XIAO. To ensure the code provided later works correctly, you must follow the connections shown in the previous image exactly.Program
The program is written in MicroPython, but before uploading the code to the XIAO, we need to perform some initial setup.
Installing MicroPython
Before programming the XIAO with MicroPython, you need to install the appropriate firmware. If you already know how to do this or if your XIAO already has MicroPython installed, you can skip this section. Otherwise, follow these steps to prepare your board.
There are two methods to install the firmware: manually downloading and installing it or using Thonny to handle the process. In this guide, we will use the second option.
What You Need
- A computer with the latest version of Thonny installed (I used version 4.1.6).
- A USB Type-C cable capable of data transfer (not just power).
You can download Thonny from its official website.
Installation Procedure
- Ensure the XIAO is disconnected from any power source.
- Press and hold the BOOT (B) button on the XIAO.
- Connect the USB cable to the XIAO while keeping the button pressed, then plug the other end of the cable into your computer.
- The XIAO will be recognized as a device named RPI-RP2.
- Release the BOOT button.
Next, open Thonny and navigate to Run > Configure Interpreter in the main menu. You will see a dialog box similar to this:
In the list that says "Which kind of interpreter should Thonny use for running code?" select MicroPython (RP2040).
Click on the bottom section labeled Install or update Micropython. Next, you'll see another dialog where you need to select the specific version of MicroPython to flash:
Look for the values shown in the dialog. Thonny will automatically select the latest stable firmware version (1.24.1 as of now). Click Install, and after a few moments, the firmware will be ready to use. 👍
This project uses several libraries (or modules) already included in the MicroPython firmware. We will discuss some of them later. However, the library to control the OLED display, which uses the SSD1306 chip, is not included by default.
As is often the case with some chips or sensors, more than one library is available to manage the SSD1306. Perhaps the most well-known is the "official" library, which you can download from the MicroPython-lib repository. This library provides basic functions to control an SSD1306-based display but lacks advanced graphic capabilities.
For this reason, and to enhance the features of the monitor in this project, I decided to use a more comprehensive library developed by rdagger, which you can find in this repository.
This alternative library offers richer graphical primitives and the ability to display values or text using different fonts, something we will take full advantage of in this monitor.
All these functions are contained in two files: ssd1306.py
and xglcd_font.py
. You can install them using mpremote, or simply download and copy them to the XIAO's file system in a folder named lib
.
To make the temperature and humidity data display more visually appealing, the monitor shows icons related to these measurements above the numerical values. These icons also serve to indicate the measurement units (degrees and percentages).
The chosen OLED library allows loading bitmap images and transferring them to the display. These images must be in monoHMSB format, meaning they need to be monochromatic, with the color information arranged horizontally, where the first pixel corresponds to the most significant bit of each byte (Horizontal Most Significant Bit).
If you have monochromatic images (e.g., in BMP format), you can convert them into the format required by the library using the img2monoHMSB.py
script. This script is located in the utils
folder of the original repository and the images
folder of this project's repository.
The images used in this monitor are HumIcon.BMP
and TempIcon.BMP
, which are also available in my repository. To perform the conversion, open a command prompt (with Python installed) and type the following:
python img2monoHMSB.py HumIcon.bmp
By doing this with the two original images, you will get the files HumIcon.mono
and TempIcon.mono
, which are the converted images.
If you want to skip this step, the pre-converted images are already available in my GitHub repository for this project, within the images
folder.
Finally, you need to upload these converted images to the images
folder in the XIAO's file system so the code can load and display them on the screen.
Let’s now analyze how the code works.
As usual, the necessary libraries or modules are imported at the beginning. Notably, the Display and XglcdFont classes from the previously mentioned OLED libraries are imported here.
from time import sleep
from machine import Pin, SoftI2C, PWM
from ssd1306 import Display
from xglcd_font import XglcdFont
from collections import deque #doble-ended queue
from dht import DHT11
The deque
class from the collections
library is also imported, which warrants some additional explanation.
The term "deque" comes from "double-ended queue". This data structure is very flexible and can be seen as an evolution of other common data structures like stacks and queues used in various programming languages.
The operation of these three structures is illustrated graphically in the following image:
As you can see, the deque
allows adding and removing data from both ends.
In this project, we will use the deque
to store the latest temperature and humidity readings. The queue will have a capacity of 100 elements, where data is added to one end, and as it fills up, older values are removed from the other end. This way, the queue will always hold the most recent 100 measurements.
Continuing with the code, some constant values are defined, such as the XIAO pins (referenced by name rather than GPIO number) and the connection pins for the OLED, DHT11, buzzer, and the Expansion Board's user button.
The modes
list defines the various data visualization modes for the monitor, which we will analyze later.
################ Constants ####################
# Pin name and GPIO mappings
D0 = 26
D1 = 27
D2 = 28
D3 = 29
D4 = 6
D5 = 7
D6 = 0
D7 = 1
D8 = 2
D9 = 4
D10 = 3
# RP2040 I2C pins
SDA_PIN = D4
SCL_PIN = D5
# DHT11 connection pin
DHT11_PIN = D7
# Buzzer
BUZZER_PIN = D3
# Button
BUTTON_PIN = D1
# Number of values to plot
maxValues = 100
# Time between measurements (sec)
sampleTime = 1
# Display modes
modes = ["Values", "Min", "Max", "Avg", "PlotTemp", "PlotHum"]
A series of general-purpose functions are defined to display values with large characters on the OLED, show bitmap images, sound the buzzer, and read the user button on the Expansion Board.
The functions plotHum
and plotTemp
display bar graphs of the values stored in the deque
for humidity and temperature, respectively. The function showTH
shows the current temperature and humidity values, while showMin
, showMax
, and showAvg
display the minimum, maximum, and average values from the stored measurements.
The function printError
displays a simple error message on the screen if an issue occurs while measuring with the DHT11 sensor, such as when the module is disconnected or malfunctioning.
#################### Functions ###################
def printBig(temp, hum):
# Prints two values with a large font
tempStr = f"{temp:.1f}"
humStr = f"{hum:.0f}"
# Prints values with large numbers
# Clears before because the "1" doesn't cover completely
display.draw_text(5, 31, " ", perfect, False)
display.draw_text(5, 31, tempStr, perfect, False)
display.draw_text(85, 31, " ", perfect, False)
display.draw_text(85, 31, humStr, perfect, False)
def showBitmaps():
# Displays bitmaps
display.draw_bitmap("images/TempIcon.mono", 25, 0, 32, 32, True)
display.draw_bitmap("images/HumIcon.mono", 85, 0, 32, 32, True)
def readButton():
# Reads the User Button on the expansion board at D1
# Returns the read value
return (button.value())
def beep():
# Makes a beep on the passive buzzer
buzzer = PWM(Pin(BUZZER_PIN))
buzzer.freq(1000)
buzzer.duty_u16(32768) # 50% Duty Cycle
sleep(0.1)
buzzer.deinit() # Releases resources
def showTH(temp, hum):
# Displays current temperature and humidity with bitmaps
# Prints values with a large font
printBig(temp, hum)
# Displays temp and hum bitmaps
showBitmaps()
# Update screen
display.present()
def plotHum(values):
# Plots humidity values
maxHum = 100
display.draw_rectangle(20, 4, 104, 56, invert=False)
# Humidity scale values
display.draw_text(0, 0, "100", fixed, False)
display.draw_text(5, 28, "50", fixed, False)
display.draw_text(10, 54, "0", fixed, False)
# Title
display.draw_text(5, 15, "H", fixed, False)
# Plot the stored values
x = 22
for i in values:
h = int(i[1] * 56 / maxHum) # Scale
display.draw_vline(x, 5, 56 - h, invert=True)
display.draw_vline(x, 60 - h, h, invert=False)
x = x + 1
display.present()
def plotTemp(values):
# Plots temperature values
maxTemp = 50
display.draw_rectangle(20, 4, 104, 56, invert=False)
# Temperature scale values
display.draw_text(5, 0, "50", fixed, False)
display.draw_text(5, 28, "25", fixed, False)
display.draw_text(10, 54, "0", fixed, False)
# Title
display.draw_text(5, 15, "T", fixed, False)
# Plot the stored values
x = 22
for i in values:
h = int(i[0] * 56 / maxTemp) # Scale
display.draw_vline(x, 60 - h, h, invert=False)
x = x + 1
display.present()
def showMin(values):
# Displays minimum values from the last 100 measurements
# Find the minimums
tempMin = 50
humMin = 100
for value in values:
if (value[0] < tempMin): # Temperature
tempMin = value[0]
if (value[1] < humMin): # Humidity
humMin = value[1]
# Prints values with a large font
printBig(tempMin, humMin)
# Displays bitmaps
showBitmaps()
display.draw_text(0, 0, "MIN", fixed, False)
# Update screen
display.present()
def showMax(values):
# Displays maximum values from the last 100 measurements
tempMax = 0
humMax = 0
for value in values:
if (value[0] > tempMax): # Temperature
tempMax = value[0]
if (value[1] > humMax): # Humidity
humMax = value[1]
# Prints values with a large font
printBig(tempMax, humMax)
# Displays bitmaps
showBitmaps()
display.draw_text(0, 0, "MAX", fixed, False)
# Update screen
display.present()
def showAvg(values):
# Displays average values from the last 100 measurements
tempSum = 0
humSum = 0
for value in values:
tempSum = tempSum + value[0] # Temperature
humSum = humSum + value[1] # Humidity
tempAvg = tempSum / len(values)
humAvg = humSum / len(values)
# Prints values with a large font
printBig(tempAvg, humAvg)
# Displays bitmaps
showBitmaps()
display.draw_text(0, 0, "AVG", fixed, False)
# Update screen
display.present()
def printError():
display.clear()
# Error message
display.draw_text(0, 0, "ERROR Sensor!", fixed, False)
# Update screen
display.present()
After these function definitions comes the main code. At the beginning, the objects for the I2C bus for the display, the display itself, the user button, and the DHT11 sensor are created. Then, the fonts to be used are loaded into memory: PerfectPixel_23x32 is large and used for temperature and humidity values, while FixedFont5x8 is used to display small texts.
#################### Main Code ###################
# Create I2C object
i2c = SoftI2C(freq=400000, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN))
# Create display object
display = Display(i2c=i2c, width=128, height=64)
# Create sensor object
sensorTH = DHT11(Pin(DHT11_PIN))
# Create object for the User button on the board
button = Pin(D1, Pin.IN, Pin.PULL_UP)
# Load fonts
perfect = XglcdFont('fonts/PerfectPixel_23x32.c', 23, 32)
fixed = XglcdFont('fonts/FixedFont5x8.c', 5, 8)
Next, the listTH
deque is created or instantiated. This deque will store the last 100 measured values (the quantity is defined by the variable maxValues
, which is initialized at the beginning with the value of 100).
# Create object list to store values for the graph
listTH = deque([], maxValues) # Does not use maxlen=
The loop that will run indefinitely is then prepared.
modeIndex
is the variable that indicates what should be displayed on the screen. It is used to index the modes
list. Since it starts with the value 0
and modes[0]
is "Values", the program begins by displaying the current temperature and humidity values.
Next, errorFlag
is set to False
, indicating that (for now) no error condition has occurred.
Inside the loop, the program attempts to read the sensor. If an error occurs because the sensor does not respond, a message is displayed on the screen, errorFlag
is raised, and the continue
statement skips all subsequent code since the temperature and humidity values are invalid.
If no error occurs, the measured values are stored as a tuple inside the deque
and printed to the console.
This section also checks if there was an error before a successful reading. If so, the error message is cleared from the screen.
Finally, the user button on the board is read. Each time it is pressed, the display mode changes. The last segment checks the current mode and, based on its value, shows the corresponding screen and adds a delay before the next reading.
# Start by showing values
modeIndex = 0
# Reset error condition
errorFlag = False
# Loop
while (True):
# Measure temperature and humidity
try:
sensorTH.measure()
# Separate values
temp = sensorTH.temperature()
hum = sensorTH.humidity()
except Exception as e:
print(f"Error reading the sensor: {e}")
printError()
sleep(5)
errorFlag = True
continue # Skip the following code
# Proceed if no reading error
if (errorFlag == True): # Comes from error condition
errorFlag = False
print("Clear!")
display.clear()
# Save to the list (deque)
listTH.append((temp, hum))
# Print to console
print(temp, "degrees")
print(hum, "%")
# Change display mode if button is pressed
if (readButton() == 0):
beep()
modeIndex = (modeIndex + 1) % len(modes)
display.clear()
# Show the active display
if (modes[modeIndex] == "Values"):
showTH(temp, hum)
elif (modes[modeIndex] == "Min"):
showMin(listTH)
elif (modes[modeIndex] == "Max"):
showMax(listTH)
elif (modes[modeIndex] == "Avg"):
showAvg(listTH)
elif (modes[modeIndex] == "PlotTemp"):
plotTemp(listTH)
else:
plotHum(listTH)
# Wait for the next measurement
sleep(sampleTime)
OperationThe following video demonstrates how the monitor works.
Improvements and ModificationsLike any other project, this one can also be modified and improved in many ways. Here are some ideas I came up with:
- Add the option to select between Celsius or Fahrenheit for temperature values.
- Calculate maximum, minimum, and average values for all measured data, not just the last 100.
- Implement alarms for extreme values (both maximum and minimum) for temperature and humidity.
You can find the code and all the files for this project on my GitHub. This project, along with others related to it, is available in the XIAO
repository.
In this article, I shared a simple project that implements a temperature and humidity monitor based on the XIAO RP2040 board and the DHT11 sensor. While there are many similar projects, I aimed to differentiate this one by adding more features to the software, such as the use of bitmaps, large fonts, and the ability to store measured values and calculate and display their maximum, minimum, and average. The meter also has the ability to show the evolution of the measurements graphically in real time.
The goal was to take advantage of some features of the XIAO and its expansion board and to use a library for the OLED display that has great potential due to the number of functions it offers. Additionally, I went into detail about some less common topics in MicroPython, such as using deques.
I hope this project has helped you learn new things, reinforce what you already know, and spark ideas for your own projects. If you have any questions or suggestions, feel free to leave them below.
For more information and projects, you can check out my blog and social media.
See you next time!
Comments
Please log in or sign up to comment.