Medición de CO₂ con Sensor SCD41 y ESP32
📋 Descripción del Proyecto
Sistema de monitoreo de calidad del aire diseñado para aulas escolares que mide la concentración de CO₂ en tiempo real y proporciona alertas visuales y recomendaciones para mantener un ambiente de aprendizaje saludable.
🎯 Aplicación
La calidad del aire interior está directamente relacionada con el rendimiento cognitivo de los estudiantes. Niveles elevados de CO₂ pueden causar:
- Disminución de la concentración
- Somnolencia
- Reducción del rendimiento académico
- Problemas de salud a largo plazo
Este monitor ayuda a tomar decisiones para mantener los niveles óptimos de CO₂ para un ambiente de aprendizaje saludable.
📈 Beneficios Educativos
Para Estudiantes
- Concienciación sobre calidad del aire
- Comprensión de conceptos de química y física
- Aprendizaje sobre sistemas embebidos y sensores
Para Docentes
- Herramienta objetiva para gestión del aula
- Datos para proyectos STEAM
- Mejora del ambiente de aprendizaje
Para el Centro Educativo
- Cumplimiento de normativas de calidad del aire
- Reducción de enfermedades respiratorias
- Optimización del rendimiento académico
🔧 Hardware Utilizado
En la imagen de la izquierda, el sensor SCD41 y el led neopixel sobre la controladora Lolin D32 (ESP32)
A la derecha, pantalla OLED
A la derecha, pantalla OLED
Componentes Principales
| Componente | Modelo/Especificaciones | Función |
|---|---|---|
| Microcontrolador | Lolin D32 (ESP32) | Control principal del sistema |
| Sensor de CO2 | SCD41 (Sensirion) | Medición de CO2, temperatura y humedad |
| Pantalla | OLED SH1106 (128x64 px) | Visualización de datos |
| Indicador Visual | LED NeoPixel WS2812B | Alertas de color por estado |
Especificaciones del Sensor SCD41
- Rango de medición CO2: 400 - 40,000 ppm
- Precisión: ±50 ppm + 5% de la lectura
- Interfaz: I2C
- Consumo: <0.5 mA (modo bajo consumo)
- Tiempo de respuesta: <60 segundos
- Calibración: Automática con referencia de 400 ppm
🔌 Esquema de Conexionado
Bus I2C Principal (Bus #2)
ESP32 → Dispositivos I2C
Pin 19 (SDA) → SDA Sensor SCD41 + OLED
Pin 22 (SCL) → SCL Sensor SCD41 + OLED
3.3V → VCC Sensor + OLED
GND → GND Sensor + OLED
LED de Estado
ESP32 → LED NeoPixel
Pin 4 → DIN (Entrada de datos)
3.3V → VCC
GND → GND
Direcciones I2C
- Sensor SCD41: 0x62
- Pantalla OLED: 0x3C
- Velocidad del bus: 50 kHz (optimizada para estabilidad)
⚙️ Funciones del Sistema
Estados de Monitoreo
| Estado | Rango CO2 | LED | Pantalla | Acción Recomendada |
|---|---|---|---|---|
| NORMAL | ≤ 1000 ppm | 🟢 Verde (brillo bajo) | Apagada | Aire óptimo |
| ADVERTENCIA | 1001-1600 ppm | 🟠 Naranja (brillo medio) | Encendida | Abrir puerta |
| CRÍTICO | > 1600 ppm | 🔴 Rojo fijo (brillo alto) | Alerta visible | Ventilar urgente |
Gestión Inteligente de Energía
- Intervalos de medición adaptativos:
- Normal: 180 segundos
- Advertencia: 60 segundos
- Crítico: 20 segundos
📊 Interpretación de Mediciones
Niveles de CO2 en Ambientes Escolares
| Concentración | Calidad del Aire | Efectos en el Aprendizaje |
|---|---|---|
| 350-400 ppm | Aire exterior | Referencia natural |
| 400-1000 ppm | Excelente | Condiciones óptimas |
| 1000-1600 ppm | Aceptable | Ligera disminución concentración |
| 1600-5000 ppm | Pobre | Somnolencia, fatiga |
| >5000 ppm | Muy pobre | Problemas de salud |
Recomendaciones de Ventilación
- < 800 ppm: Mantener ventilación normal
- 800-1200 ppm: Aumentar ventilación natural (ventanas semiabiertas)
- 1200-1600 ppm: Abrir puerta prioritariamente, alternativamente ventanas
- > 1600 ppm: Ventilación cruzada urgente (puerta y ventanas) o evacuación temporal
🔋 Características de Eficiencia Energética
Optimizaciones Implementadas
- Reducción de frecuencia CPU (240MHz → 3MHz)
- Comunicaciones inalámbricas desactivadas
- Modo de suspensión inteligente (light sleep)
- Pantalla apagada en estado normal
- LEDs con brillo optimizado
Consumo Estimado
- Modo activo: ~15 mA
- Modo suspensión: ~2 mA
- Autonomía con batería 2000mAh: ~120 horas (5 días)
🛠️ Instalación y Configuración
Requisitos de Software
- Arduino IDE 1.8.19 o superior
- Bibliotecas requeridas:
SparkFun_SCD4x_Arduino_LibraryAdafruit_NeoPixelAdafruit_SH110XAdafruit_GFX
/*
* Monitor de CO2 con sensor SCD41 y ESP32
* wwww.acusticaescolar.com
* Creative commons
*
*Este código implementa un sistema de monitoreo de CO2 que:
* - Lee datos del sensor SCD41 a través de I2C
* - Muestra información en pantalla OLED
* - Indica niveles de CO2 con LED NeoPixel
* - Optimizado para bajo consumo energético
*
* Estados del sistema basados en niveles de CO2:
* - NORMAL: ≤1000 ppm (LED verde)
* - WARNING: 1000-1600 ppm (LED naranja fijo)
* - CRITICAL: ≥1600 ppm (LED rojo fijo)
*/
#include <Wire.h>
#include <Adafruit_NeoPixel.h>
#include "SparkFun_SCD4x_Arduino_Library.h"
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include "esp_wifi.h"
#include "esp_bt.h"
// ================================
// CONFIGURACIÓN DEL SISTEMA
// ================================
// Configuración de debug - cambiar a 1 para activar mensajes de depuración
#define DEBUG_MODE 0 // 0 = producción, 1 = debug
// Definición de pines para los buses I2C duales
// Bus I2C #1 (actualmente no utilizado en este código)
#define I2C_SDA_1 33
#define I2C_SCL_1 32
// Bus I2C #2 - usado para sensor SCD41 y pantalla OLED
#define I2C_SDA_2 19
#define I2C_SCL_2 22
// Direcciones I2C de los dispositivos
#define OLED_ADDR 0x3C // Dirección de la pantalla OLED
#define SCD41_ADDR 0x62 // Dirección del sensor SCD41
// Configuración de la pantalla OLED
#define SCREEN_WIDTH 128 // Ancho en píxeles
#define SCREEN_HEIGHT 64 // Alto en píxeles
#define OLED_RESET -1 // Pin de reset (-1 si no se usa)
// ================================
// DEFINICIÓN DE ESTADOS DEL SISTEMA
// ================================
// Enumeración de estados basados en niveles de CO2
typedef enum {
STATE_NORMAL, // CO2 <= 1000 ppm - Aire bueno
STATE_WARNING, // 1000 < CO2 < 1600 ppm - Ventilación recomendada
STATE_CRITICAL // CO2 >= 1600 ppm - Ventilación urgente
} co2_state_t;
// ================================
// CREACIÓN DE INSTANCIAS DE OBJETOS
// ================================
// Crear los buses I2C separados
TwoWire I2C_MPU = TwoWire(0); // Bus I2C #1 (no usado actualmente)
TwoWire I2C_SCD41_OLED = TwoWire(1); // Bus I2C #2 para sensor y OLED
// Crear instancia de la pantalla OLED SH1106
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &I2C_SCD41_OLED, OLED_RESET);
// Crear instancia del sensor SCD41
SCD4x mySensor;
// ================================
// VARIABLES GLOBALES
// ================================
// Variables para almacenar las lecturas del sensor
uint16_t co2 = 0; // Concentración de CO2 en ppm
float temperature = 0.0; // Temperatura en °C (no utilizada actualmente)
float humidity = 0.0; // Humedad relativa en % (no utilizada actualmente)
// Variables de temporización
unsigned long lastUpdate = 0; // Última actualización
const int updateInterval = 5000; // Intervalo de actualización en ms
// Configuración del LED NeoPixel
#define LED_PIN 4 // Pin donde está conectado el LED
#define LED_COUNT 1 // Número de LEDs (solo uno)
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
// Variables de estado del sistema
co2_state_t current_state = STATE_NORMAL; // Estado actual del sistema
co2_state_t previous_state = STATE_NORMAL; // Estado anterior (para detectar cambios)
// ================================
// FUNCIÓN DE CONFIGURACIÓN INICIAL
// ================================
void setup()
{
// Reducir frecuencia del CPU para ahorrar energía (de 240MHz a 3MHz)
setCpuFrequencyMhz(3);
// Inicializar comunicación serie solo en modo debug
#if DEBUG_MODE
Serial.begin(115200);
Serial.println("ESP32 Dual I2C - OLED y Sensor CO2 SCD41 [DEBUG MODE]");
#endif
// Desactivar WiFi y Bluetooth para reducir consumo energético
esp_wifi_deinit(); // Desactivar WiFi completamente
esp_bt_controller_disable(); // Desactivar Bluetooth
// Inicializar el bus I2C #2 con velocidad reducida para estabilidad
// Velocidad reducida de 100kHz a 50kHz para mejor estabilidad
I2C_SCD41_OLED.begin(I2C_SDA_2, I2C_SCL_2, 50000);
// Inicializar el LED NeoPixel
strip.begin(); // Configurar el LED
strip.show(); // Asegurar que inicie apagado
// ================================
// INICIALIZACIÓN DE LA PANTALLA OLED
// ================================
display.begin(OLED_ADDR, true); // Inicializar con dirección I2C
display.clearDisplay(); // Limpiar pantalla
display.setTextSize(1); // Tamaño de texto pequeño
display.setTextColor(SH110X_WHITE); // Color blanco
display.setCursor(0, 0); // Posición inicial
display.println("Iniciant..."); // Mensaje de inicio
display.display(); // Mostrar en pantalla
// ================================
// INICIALIZACIÓN DEL SENSOR SCD41
// ================================
#if DEBUG_MODE
Serial.println("Inicializando SCD41...");
#endif
// Intentar conectar con el sensor SCD41
if (mySensor.begin(I2C_SCD41_OLED, SCD41_ADDR) == false) {
// Si no se puede conectar al sensor, mostrar error
#if DEBUG_MODE
Serial.println("Error: Sensor SCD41 no encontrado. Verifique conexiones.");
#endif
// Mostrar mensaje de error en pantalla
display.clearDisplay();
display.setCursor(0, 0);
display.println("Error: SCD41");
display.println("no encontrado");
display.display();
// Bucle infinito con modo de bajo consumo en caso de error
while (1) {
esp_sleep_enable_timer_wakeup(5 * 1000000); // Despertar cada 5 segundos
esp_light_sleep_start(); // Entrar en modo de bajo consumo
}
}
// ================================
// CONFIGURACIÓN DEL SENSOR SCD41
// ================================
// Detener mediciones periódicas para configurar el sensor
if (mySensor.stopPeriodicMeasurement() == true) {
#if DEBUG_MODE
Serial.println(F("Medidas períódicas desactivadas"));
#endif
}
// Configurar offset de temperatura (compensación por calentamiento del sensor)
#if DEBUG_MODE
Serial.print(F("Offset de Temperatura actual: "));
Serial.println(mySensor.getTemperatureOffset(), 2);
#endif
mySensor.setTemperatureOffset(0); // Sin offset de temperatura
#if DEBUG_MODE
Serial.print(F("Ahora el offset de Temperature es: "));
Serial.println(mySensor.getTemperatureOffset(), 2);
Serial.print(F("Altitud actual del sensor: "));
Serial.println(mySensor.getSensorAltitude());
#endif
// Configurar altitud del sensor (Els Pallaresos = 121m sobre nivel del mar)
// La altitud afecta la calibración de CO2
mySensor.setSensorAltitude(121);
#if DEBUG_MODE
Serial.print(F("Sensor altitude ahora es: "));
Serial.println(mySensor.getSensorAltitude());
#endif
// Configurar presión atmosférica ambiente (en Pascales)
// 99787 Pa aproximadamente corresponde a la presión a 121m de altitud
bool success = mySensor.setAmbientPressure(99787);
#if DEBUG_MODE
if (success) {
Serial.println(F("setAmbientPressure configurada"));
}
#endif
// Guardar configuración en la memoria no volátil del sensor
mySensor.persistSettings();
// Mostrar número de serie del sensor (solo en modo debug)
#if DEBUG_MODE
char serialNumber[13];
if (mySensor.getSerialNumber(serialNumber) == true) {
Serial.print(F("El número de serie del sensor es: 0x"));
Serial.println(serialNumber);
}
#endif
// ================================
// ACTIVAR MODO DE BAJO CONSUMO
// ================================
// Asegurar que las mediciones periódicas estén detenidas
if (mySensor.stopPeriodicMeasurement() == true) {
#if DEBUG_MODE
Serial.println(F("Medición periódica desactivada"));
#endif
}
// Activar modo de medición periódica de bajo consumo
// Este modo toma mediciones cada 30 segundos automáticamente
if (mySensor.startLowPowerPeriodicMeasurement() == true) {
#if DEBUG_MODE
Serial.println(F("Bajo consumo activado"));
#endif
}
// ================================
// MENSAJE FINAL DE INICIALIZACIÓN
// ================================
// Mostrar mensaje de inicialización completa en OLED
display.clearDisplay();
display.setCursor(0, 0);
display.println("SCD41 CO2 Monitor");
display.println("----------------");
display.println("Calentando sensor...");
display.display();
// Esperar 3 segundos usando modo de bajo consumo en lugar de delay()
// Esto permite que el sensor se estabilice
esp_sleep_enable_timer_wakeup(3 * 1000000); // 3 segundos en microsegundos
esp_light_sleep_start();
}
// ================================
// BUCLE PRINCIPAL DEL PROGRAMA
// ================================
void loop()
{
// Verificar si el sensor tiene nuevos datos disponibles
if (mySensor.readMeasurement()) {
// Leer valor de CO2 del sensor
co2 = mySensor.getCO2();
#if DEBUG_MODE
Serial.print("CO2(ppm): ");
Serial.print(co2);
Serial.println();
#endif
// Actualizar el estado del sistema basado en el nivel de CO2
updateCO2State();
// Manejar LED y pantalla según el estado actual
handleDeviceStates();
}
#if DEBUG_MODE
else {
// Mostrar punto si no hay datos disponibles (solo en debug)
Serial.print(F("."));
}
#endif
// ================================
// GESTIÓN INTELIGENTE DEL CONSUMO
// ================================
// Determinar tiempo de suspensión basado en el estado actual
// En estados críticos, despertarse más frecuentemente
uint32_t sleep_time;
switch (current_state) {
case STATE_NORMAL:
sleep_time = 180 * 1000000; // 180 segundos - aire bueno, menos urgencia
break;
case STATE_WARNING:
sleep_time = 60 * 1000000; // 60 segundos - monitoreo más frecuente
break;
case STATE_CRITICAL:
sleep_time = 20 * 1000000; // 20 segundos - monitoreo urgente
break;
default:
sleep_time = 180 * 1000000; // Valor por defecto
break;
}
// Configurar y entrar en modo de bajo consumo
esp_sleep_enable_timer_wakeup(sleep_time);
esp_light_sleep_start();
}
// ================================
// FUNCIONES DE GESTIÓN DE ESTADOS
// ================================
/**
* Actualiza el estado del sistema basado en el nivel de CO2
*
* Rangos de CO2:
* - 0-1000 ppm: NORMAL (aire exterior, ambientes bien ventilados)
* - 1001-1600 ppm: WARNING (ventilación recomendada)
* - >1600 ppm: CRITICAL (ventilación urgente requerida)
*/
void updateCO2State() {
// Guardar el estado anterior para detectar cambios
previous_state = current_state;
// Determinar nuevo estado basado en nivel de CO2
if (co2 <= 1000) {
current_state = STATE_NORMAL;
} else if (co2 < 1600) {
current_state = STATE_WARNING;
} else {
current_state = STATE_CRITICAL;
}
}
/**
* Maneja los dispositivos (LED y pantalla) según el estado actual
* Llama a la función específica para cada estado
*/
void handleDeviceStates() {
switch (current_state) {
case STATE_NORMAL:
handleNormalState();
break;
case STATE_WARNING:
handleWarningState();
break;
case STATE_CRITICAL:
handleCriticalState();
break;
}
}
/**
* Maneja el estado NORMAL (CO2 ≤ 1000 ppm)
* - LED verde con brillo bajo para ahorrar energía
* - Pantalla apagada para máximo ahorro energético
*/
void handleNormalState() {
// Configurar LED verde con brillo reducido
strip.setPixelColor(0, 0, 254, 0); // RGB: Verde
strip.setBrightness(10); // Brillo muy bajo (de 255 máximo)
strip.show(); // Aplicar cambios al LED
// Apagar pantalla OLED para ahorrar energía
display.oled_command(SH110X_DISPLAYOFF);
}
/**
* Maneja el estado WARNING (1000 < CO2 < 1600 ppm)
* - LED naranja fijo para indicar precaución
* - Pantalla encendida solo cuando cambia de estado
*/
void handleWarningState() {
// LED naranja fijo para indicar precaución
strip.setPixelColor(0, 254, 191, 0); // RGB: Naranja
strip.setBrightness(25); // Brillo medio para alerta visible pero eficiente
strip.show();
// Actualizar pantalla solo si es la primera vez en este estado
// (para ahorrar energía)
if (previous_state != current_state) {
updateDisplay();
}
}
/**
* Maneja el estado CRITICAL (CO2 ≥ 1600 ppm)
* - LED rojo fijo con brillo medio-alto
* - Pantalla siempre encendida mostrando alerta
*/
void handleCriticalState() {
// LED rojo fijo para indicar urgencia
strip.setPixelColor(0, 254, 0, 0); // RGB: Rojo
strip.setBrightness(50); // Brillo más alto para urgencia
strip.show();
// Mantener pantalla actualizada en estado crítico
updateDisplay();
}
/**
* Actualiza el contenido de la pantalla OLED
* Muestra información diferente según el estado actual
*/
void updateDisplay() {
// Encender la pantalla
display.oled_command(SH110X_DISPLAYON);
display.clearDisplay();
// ================================
// ENCABEZADO DE LA PANTALLA
// ================================
display.setTextSize(1); // Texto pequeño para encabezado
display.setCursor(0, 0); // Esquina superior izquierda
display.println("Midiendo CO2:");
display.println(""); // Línea en blanco
// ================================
// VALOR PRINCIPAL DE CO2
// ================================
display.setTextSize(2); // Texto grande para valor principal
display.setCursor(10, 20); // Centrado horizontalmente
display.print(co2);
display.println(" ppm");
// ================================
// MENSAJES ESPECÍFICOS POR ESTADO
// ================================
if (current_state == STATE_WARNING) {
// Mensaje de advertencia con instrucciones
display.setTextSize(1);
display.setCursor(0, 45);
display.print("ALERTA! Abrir");
display.setCursor(0, 55);
display.print("puerta o ventana");
} else if (current_state == STATE_CRITICAL) {
// Mensaje crítico urgente
display.setTextSize(2);
display.setCursor(10, 45);
display.print("VENTILAR");
}
// Aplicar todos los cambios a la pantalla
display.display();
}
🔬 Posibles Extensiones del Proyecto
- Conectividad IoT: WiFi para monitoreo remoto
- Datalogger: Almacenamiento de datos históricos
- Alertas sonoras: Buzzer para avisos audibles
- Múltiples sensores: Red de monitores en diferentes aulas con app Sensirion MyAmbience
- Interfaz web: Dashboard para administración escolar
- Máxima autonomia: Reducir consumos
Las que tienen enlace estan desarrolladas.
📋 Lista de Materiales y Costes
| Componente | Cantidad | Precio Aprox. |
|---|---|---|
| ESP32 DevKit | 1 | 8€ |
| Sensor SCD41 | 1 | 45€ (15€ unidades remanufacturadas) |
| OLED SH1106 | 1 | 6€ |
| LED NeoPixel | 1 | 2€ |
| Cables y caja | - | 3€ |
| TOTAL | ≈ 64€ (35€ según precio del sensor) |
Proyecto desarrollado por www.acusticaescolar.com para mejorar la calidad del aire en ambientes educativos. Junio 2025.




No hay comentarios:
Publicar un comentario
Nota: solo los miembros de este blog pueden publicar comentarios.