En este proyecto construiremos un medidor de temperatura y humedad basado en el conocido sensor DHT11 conectado a una placa XIAO RP2040. Para diferenciarlo de otros proyectos similares le daremos algunas funciones adicionales, como la capacidad de mostrar valores característicos de las magnitudes medidas y gráficos para analizar su evolución en el tiempo.
Introducción
La familia XIAO de placas microcontroladoras ofrece una amplia variedad de opciones. En un tamaño compacto de apenas 20 x 17,5 mm, podemos encontrar modelos equipados con diferentes versiones de ESP32, los micros de Raspberry (RP2040 y RP2350), el RA4M1 de Renesas y el MG24 de Silicon Labs, entre otros. Estos modelos varían en cuanto a la cantidad de memoria RAM y Flash, interfaces de comunicación como WiFi, Bluetooth o Zigbee, y periféricos para interactuar con el mundo real, como micrófonos, cámaras y acelerómetros. Las posibilidades de programación también son amplias, pudiendo emplearse Arduino, Micropython o CircuitPython para un enfoque algoritmico tradicional o modelos de Machine Learning si quieres incursionar en el campo de la IA.
En este proyecto utilizaremos la XIAO RP2040 junto con dos placas de expansión que facilitarán las conexiones: la Expansion Board Base, que incluye un display OLED al que le sacaremos provecho visualizando valores numéricos e imágenes, y una placa con interfaz Grove equipada con el sensor DHT11 para medir temperatura y humedad.
Para añadir originalidad al proyecto y aprovechar las capacidades de la Expansion Board, además de practicar programación en MicroPython, implementaremos diversas formas de visualizar las mediciones. Incluiremos funciones para mostrar valores máximos, mínimos y promedios, y también para graficar los últimos registros de temperatura y humedad.
La placa de expansión
Para probar nuestros prototipos y programas con los distintos modelos de placas XIAO, Seeed Studio ofrece la Expansion Board Base, una placa multifunción muy completa que incorpora varios accesorios, entre ellos:
- Display OLED de 64 x 128 pixeles.
- Zócalo de conexión para la XIAO.
- Conectores de acceso a todos los pines de la XIAO.
- Conectores tipo Grove (4).
- Cargador de batería Lipo, con su conector y led indicador de carga.
- Conector para servomotor.
- RTC (reloj de tiempo real) alimentado con una pila CR1220.
- Zócalo para tarjeta SD.
- Botón de RESET.
- Botón de usuario para usos múltiples.
- Buzzer pasivo.
- Conectores para depuración (SWD).
Conexiones
En este medidor multifunción utilizaremos el display OLED, el buzzer y el botón de usuario incluidos en la placa de expansión. Para medir los valores de temperatura y humedad añadiremos un modulo Grove que incorpora el sensor DHT11. Todos estos elementos se conectan como se puede apreciar en la siguiente imagen.
Los conectores Grove de la Expansion board tienen funciones diferentes y están conectados a distintos pines de la XIAO, así que para que el código que te presentaré luego funcione correctamente, debes respetar el conexionado de la imagen anterior.
Programa
El programa está escrito en Micropython, pero antes de cargar el código en la XIAO debemos hacer algunas preparaciones.
Instalación de Micropython
Antes de poder programar la XIAO en MicroPython es necesario instalar el firmware correspondiente. Si ya sabes como hacerlo o si tu XIAO ya tiene Micropython instalado, puedes saltear esta sección y seguir adelante. Si no es así sigue estas instrucciones para preparar la placa.
Existen dos métodos para instalar el firmware: puedes descargarlo e instalarlo manualmente, o utilizar Thonny para que se encargue del trabajo. En esta ocasión, optaremos por la segunda opción.
Necesitarás una computadora con la última versión de Thonny instalado (yo usé la 4.1.6) y un cable USB tipo C que permita transferir datos (no sólo de alimentación). Puedes descargar Thonny desde su sitio web.
El procedimiento a seguir es el siguiente: Teniendo la XIAO desconectada de cualquier fuente de alimentación, presiona y mantén presionado el botón BOOT (B) que está sobre ella. Conecta el cable USB en la XIAO y sin soltar el botón, inserta el otro extremo del cable en la computadora. La XIAO será reconocida como un dispositivo llamado RPI-RP2. Luego de eso ya puedes soltar BOOT.
A continuación abre Thonny y busca en el menú principal Ejecutar – Configurar intérprete. Verás un cuadro de diálogo como el siguiente:
Donde dice “¿Qué tipo de intérprete debe utilizar Thonny para ejecutar su código?” selecciona MicroPython (RP2040).
Luego haz click en la parte inferior donde dice Instala o actualiza MicroPython. A continuación verás otro diálogo donde tienes que elegir la versión específica de Micropython a grabar:
Busca los valores mostrados en la imagen. Automáticamente Thonny te elegirá la última versión estable del firmware (1.24.1 a la fecha). Pulsa Instalar y luego de unos instantes el firmware estará listo para usar. 👍
Librerías
En este proyecto se usan varias librerías (o módulos) que ya vienen incluidas en el firmware. Ya hablaremos sobre algunas de ellas mas adelante. La que no está incluida es la librería que controla el display OLED, que usa como controlador el chip SSD1306.
Como suele ocurrir con algunos chips o sensores, hay mas de una librería disponible para manejar el SSD1306. Tal vez la mas conocida sea la librería “oficial” que puedes descargar desde el repositorio micropython-lib. Esta librería, que ya analicé en un artículo anterior, contiene las funciones básicas para controlar un display basado en el SSD1306 pero se queda corta en la parte de gráficos. Por esta razón y para dotar de mas funciones al medidor que te presento en este proyecto, he decidido usar otra libreria mas completa desarrollada por rdagger y que puedes encontrar en este otro repositorio.
Esta otra librería es mucho mas completa en primitivas gráficas y tiene la posibilidad de mostrar valores o textos usando distintos tipos de letras o fuentes, algo que aprovecharemos en este medidor.
Todas estas funciones están contenidas en dos archivos, uno llamado ssd1306.py y el otro xglcd_font.py. Puedes instalarlos usando mpremote como lo explico en este artículo anterior o simplemente descargandolos y copiandolos luego al sistema de archivos de la XIAO en una carpeta de nombre lib
Imágenes
Para hacer la visualización de datos de temperatura y humedad algo mas llamativa, el medidor muestra unos íconos relacionados con estas magnitudes sobre los valores numéricos. Esto también sirve para indicar las unidades de medida (grados y porcentajes).
La librería para el OLED elegida permite cargar imágenes de tipo bitmap y transferirlas al display. Estas imágenes deben estar en formato monoHMSB, es decir deben ser monocromáticas y la información de color debe estar ordenada de manera horizontal con el primer pixel asociado con el bit mas significativo de cada byte (Horizontal Most Significant Bit).
Teniendo las imágenes monocromáticas (por ejemplo en BMP) se pueden convertir al formato usado por la librería con el programa img2monoHMSB.py que se encuentra en la carpeta utils del repositorio original y en la carpeta images del repositorio de este proyecto.
Las imágenes usadas en el medidor son HumIcon.BMP y TempIcon.BMP y también están en mi repositorio. Para realizar la conversión debes escribir (teniendo Python instalado) lo siguiente en una ventana de comandos:
python img2monoHMSB.py HumIcon.bmp
Haciendo esto con las dos imágenes originales obtendrás los archivos HumIcon.mono y TempIcon.mono que son las imágenes convertidas.
Si quieres ahorrarte este trabajo, las imágenes ya convertidas están en mi cuenta de Github en el repositorio de este proyecto dentro de la carpeta images.
Finalmente, debes subir estas imágenes convertidas a la carpeta images del sistema de archivos de la XIAO para que el código las pueda cargar y luego mostrar en el display.
Código
Analicemos ahora el funcionamiento del código.
Como es habitual, en el inicio se importan las librerías o módulos necesarios. Aquí podemos destacar que se importan las clases Display y XglcdFont de las librerías del OLED que mencionamos antes.
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
Luego se importa la clase deque de la librería collections. Esto requiere de alguna explicación adicional.
El término “deque” proviene de “double-ended queue” y da nombre a una estructura de datos denominada en español “cola de dos entradas“.
Esta estructura de datos es muy flexible y puede considerarse como una evolución de otras estructuras como son las pilas (stacks) y las colas (queues) que se utilizan en distintos lenguajes de programación.
El funcionamiento de estas tres estructuras se explica gráficamente en la siguiente imagen:
Como se puede apreciar, la deque permite meter y sacar datos por ambos extremos.
En este proyecto usaremos la deque para almacenar los últimos valores medidos de temperatura y humedad. La cola tendrá una capacidad de 100 elementos e ingresaremos los datos por un lado y a medida que se vaya llenando, se irán cayendo por el otro. De esta manera siempre tendremos almacenados en la cola los útimos 100 valores medidos.
Siguiendo con el código vemos que luego se definen algunos valores constantes, como los pines de la XIAO (para referenciarlos por el nombre y no el número de GPIO) y los pines de conexión del OLED, el DHT11, el buzzer y el botón de usuario de la Expansion base.
La lista modes define los distintos modos de visualización de datos del medidor, como analizaremos mas adelante.
################ Constantes #################### #Equivalencias entre nombres de pines y GPIOs D0 = 26 D1 = 27 D2 = 28 D3 = 29 D4 = 6 D5 = 7 D6 = 0 D7 = 1 D8 = 2 D9 = 4 D10 = 3 # Pines de I2C de la RP2040 SDA_PIN = D4 SCL_PIN = D5 # Pin de conexion del DHT11 DHT11_PIN = D7 #Buzzer BUZZER_PIN = D3 #Botón BUTTON_PIN = D1 # Cantidad de valores a graficar maxValues = 100 # Tiempo entre mediciones (seg) sampleTime = 1 # Modos de visualizacion modes = ["Values", "Min", "Max", "Avg", "PlotTemp", "PlotHum"]
A continuación se define una serie de funciones de uso general para mostrar valores con caracteres grandes en el display, mostrar bitmaps, hacer sonar el buzzer y leer el botón de usuario de la placa de expansión.
Las funciones plotHum y plotTemp muestran en un gráfico de barras todos los valores almacenados en la deque de humedad y temperatura respectivamente. En cambio showTH muestra los valores actuales de ambas magnitudes y showMin, showMax y showAvg buscan entre los valores almacenados los valores mínimos, máximos y promedio respectivamente y los muestran en pantalla.
La función printError imprime un sencillo mensaje de error que es generado si al intentar medir el DHT11 se produce algún error, como por ejemplo si el módulo está desconectado o no responde por un malfuncionamiento.
#################### Funciones ################### def printBig (temp, hum): #Imprime dos valores con font grande tempStr = f"{temp:.1f}" humStr = f"{hum:.0f}" #Imprime valores con numeros grandes #Borra antes porque el "1" no tapa 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 (): #Mostrar 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 (): # Lee el User Button de la placa de expansion en D1 # Retorna el valor leido return (button.value()) def beep(): # Hace un beep en el buzzer pasivo buzzer = PWM(Pin(BUZZER_PIN)) buzzer.freq(1000) buzzer.duty_u16(32768) # 50% Duty Cycle sleep(0.1) buzzer.deinit() #Libera recursos def showTH (temp, hum): # Muestra TyH actual con bitmaps # Imprime valores con font grande printBig (temp, hum) # Muestra bitmaps de temp y hum showBitmaps () #Actualizar pantalla display.present() def plotHum (values): # Grafica los valores de humedad maxHum = 100 display.draw_rectangle(20, 4, 104, 56, invert=False) #Escala de valores de humedad display.draw_text(0, 0, "100", fixed, False) display.draw_text(5, 28, "50", fixed, False) display.draw_text(10, 54, "0", fixed, False) #Titulo display.draw_text(5, 15, "H", fixed, False) #Plotear los valores almacenados x = 22 for i in values: h = int (i[1]*56/maxHum) #Escalar 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): # Grafica los valores de temperatura maxTemp = 50 display.draw_rectangle(20, 4, 104, 56, invert=False) #Escala de valores de humedad display.draw_text(5, 0, "50", fixed, False) display.draw_text(5, 28, "25", fixed, False) display.draw_text(10, 54, "0", fixed, False) #Titulo display.draw_text(5, 15, "T", fixed, False) #Plotear los valores almacenados x = 22 for i in values: h = int (i[0]*56/maxTemp) #Escalar display.draw_vline(x, 60-h, h, invert=False) x=x+1 display.present () def showMin (values): #Muestra valores mínimos de las últimas 100 mediciones #Buscar los mínimos tempMin = 50 humMin = 100 for value in values: if (value[0] < tempMin): #Temperatura tempMin = value[0] if (value[1] < humMin): #Humedad humMin = value[1] # Imprime valores con font grande printBig (tempMin, humMin) # Muestra bitmaps showBitmaps () display.draw_text(0, 0, "MIN", fixed, False) #Actualizar pantalla display.present() def showMax (values): #Muestra valores máximos de las últimas 100 mediciones tempMax = 0 humMax = 0 for value in values: if (value[0] > tempMax): #Temperatura tempMax = value[0] if (value[1] > humMax): #Humedad humMax = value[1] # Imprime valores con font grande printBig (tempMax, humMax) # Muestra bitmaps showBitmaps () display.draw_text(0, 0, "MAX", fixed, False) #Actualizar pantalla display.present() def showAvg (values): #Muestra valores promedio de las últimas 100 mediciones tempSum = 0 humSum = 0 for value in values: tempSum = tempSum + value[0] #Temperatura humSum = humSum + value[1] #Humedad tempAvg = tempSum / len(values) humAvg = humSum / len(values) # Imprime valores con font grande printBig (tempAvg, humAvg) # Muestra bitmaps showBitmaps () display.draw_text(0, 0, "AVG", fixed, False) #Actualizar pantalla display.present() def printError (): display.clear() # Mensaje de error display.draw_text(0, 0, "ERROR Sensor!", fixed, False) #Actualizar pantalla display.present()
Luego de estas definiciones de funciones viene el código principal. Al inicio se crean los objetos del bus i2c para el display, el display, el botón de usuario y el sensor DHT11. Luego se cargan en memoria los fonts que se van a utilizar: PerfectPixel_23x32 tiene un tamaño grande y se usa para los valores de temperatura y humedad y FixedFont5x8 para mostrar algunos textos pequeños.
#################### Código principal ################### #Crear objeto I2C i2c = SoftI2C(freq=400000, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN)) #Crear objeto display display = Display(i2c=i2c, width=128, height=64) #Crear objeto sensor sensorTH = DHT11 (Pin(DHT11_PIN)) # Crear objeto para el User button de la placa button = Pin (D1, Pin.IN, Pin.PULL_UP) # Carga fonts perfect = XglcdFont('fonts/PerfectPixel_23x32.c', 23, 32) fixed = XglcdFont('fonts/FixedFont5x8.c',5,8)
A continuación se crea o instancia listTH, la deque que va a almacenar los últimos 100 valores medidos (la cantidad está definida por la variable maxValues que se carga al principio con el valor de 100).
listTH = deque ([], maxValues) #No usa maxlen=
Para finalizar, ya se prepara el loop o bucle que se va a repetir indefinidamente.
modeIndex es la variable que indica que se debe visualizar en pantalla. Sirve para indexar la lista modes. Como empieza con el valor cero y modes[0] es “Values”, el programa arranca mostrando los valores actuales de temperatura y humedad.
Luego se pone en False errorFlag, indicando que (por ahora) no se ha producido una condición de error.
Dentro del bucle se intenta leer el sensor. Si hay un error porque el sensor no responde, se muestra un mensaje en pantalla, se levanta errorFlag y con el continue se saltea todo el código que sigue, ya que tenemos valores inválidos de temperatura y humedad.
Si por el contrario no se produce ningún error, se guardan los valores medidos como una tupla dentro de la deque y se los imprime en la consola.
En esta parte también se comprueba si hubo un error de medición antes de una lectura correcta. Si es así se borra el mensaje de error de la pantalla.
Finalmente, se lee el botón de usuario de la placa. Cada vez que se lo pulsa se cambia el modo de visualización. El último segmento comprueba el modo actual y según su valor muestra la pantalla correspondiente y hace un retardo antes de la próxima lectura.
# Arranca mostrando valores modeIndex = 0 # Reset condicion de error errorFlag = False # Loop while (True): # Medir temperatura y humedad try: sensorTH.measure () # Separar valores temp = sensorTH.temperature() hum = sensorTH.humidity() except Exception as e: print(f"Error al leer el sensor: {e}") printError () sleep (5) errorFlag = True continue # Saltar lo que sigue # Sigue si no hay error de lectura if (errorFlag==True): #Viene de condicion de error errorFlag = False print ("Limpiar!") display.clear () # Guardarlo en la lista (deque) listTH.append ((temp,hum)) # Imprimir por consola print (temp,"grados") print (hum ,"%") # Cambiar la visualizacion si pulsan boton if (readButton()==0): beep () modeIndex = (modeIndex + 1) % len (modes) display.clear() # Mostrar la visualizacion activa 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) # Esperar para la próxima medición sleep(sampleTime)
Funcionamiento
En el siguiente video se puede apreciar el funcionamiento del medidor.
Mejoras y modificaciones
Como cualquier otro proyecto, a este también se le pueden hacer muchas modificaciones y mejoras. Por ejemplo algunas que se me ocurrieron a mi:
- Posibilidad de seleccionar grados Celsius o Farenheit para los valores de temperatura.
- Determinar valores máximos, mínimos y promedios con todos los valores que se vayan midiendo y no solo los últimos 100.
- Alarmas para valores extremos (máximos o mínimos) tanto de temperatura como de humedad.
Repositorio
Puedes encontrar el código y todos los archivos de este proyecto en mi Github. Dentro del repositorio XIAO está publicado este proyecto y otros que están relacionados.
Proyectos con distintas placas de la familia XIAO – Projects with different boards from the XIAO family
Conclusiones
En este artículo te compartí un proyecto sencillo que implementa un medidor de temperatura y humedad basado en la placa XIAO RP2040 y el sensor DHT11. Si bien hay muchos proyectos similares traté de diferenciarlo dandole mas funciones al software, como el uso de bitmaps, fuentes grandes y la capacidad de almacenar valores medidos y encontrar y mostrar en ellos los máximos, mínimos y promedios. El medidor también tiene la posibilidad de mostrar la evolucion de las mediciones de forma gráfica en tiempo real. La intención fué aprovechar algunas funciones de la XIAO y de la placa de expansión y utilizar una librería para el display OLED que tiene mucho potencial por la cantidad de funciones que incluye. Además, me xtendí en la explicación de algunos temas no tan comunes en Micropython, como el uso de colas.
Espero que el proyecto te haya servido para aprender cosas nuevas, reforzar algunas que ya sabías y darte ideas para que hagas tus propios proyectos. Cualquier duda o sugerencia puedes dejarla mas abajo en la sección de comentarios.
Hasta la próxima!