Introduzione alla Stazione di Qualità dell’Aria
Ecco uno sketch Arduino completo per una “stazione qualità aria” con MQ‑135, MQ‑2, BME280 e display, che include: riscaldamento e calibrazione dei sensori MQ (R0 in aria pulita), letture periodiche, stima gas index da MQ, conversione in AQI stimato (PM2.5-like) tramite breakpoints EPA, più visualizzazione su display.
L’AQI è una stima euristica perché MQ‑135/MQ‑2 non misurano direttamente PM2.5.
Hardware e librerieBME280 via I2C per temperatura/umidità/pressione; sostituire SEALEVELPRESSURE_HPA con il valore locale per migliore altitudine.
MQ‑135 e MQ‑2 su moduli analogici: serve riscaldamento iniziale e calibrazione in aria pulita per stimare R0; poi Rs viene ricavata da Vout e convertita in rapporto Rs/R0 per stimare concentrazione relativa.
AQI: calcolo tramite formula a tratti con breakpoints PM2.5 EPA (24h). Nel progetto si usa una media mobile/NowCast semplice per l’ora corrente per un “AQI stimato”.
Wiring tipicoBME280: VCC 3.3V, GND, SCL-A5, SDA-A4 (Arduino Uno) oppure pin I2C della vostra board.
MQ‑135 su A0, MQ‑2 su A1; usare 5V e rispettare i tempi di preriscaldamento indicati in datasheet/modulo.
Codice Arduino (commentato in italiano)Nota: usare un display I2C 128×64 (SSD1306) o 1602 I2C (modificare se diverso). Include: calibrazione R0, letture, stima gas-index, NowCast semplice e conversione in AQI stimato con breakpoints EPA.
/* Stazione Qualità Aria – MQ-135, MQ-2, BME280, Display I2C
Funzioni:
- Calibrazione R0 (aria pulita) per MQ-135 e MQ-2
- Letture periodiche con preriscaldamento
- Gas index (relativo) da Rs/R0
- PM2.5 stimato euristico dai gas + umidità (proxy)
- AQI stimato (EPA PM2.5) con media mobile/NowCast semplice
Avvertenza: MQ non misurano PM2.5; AQI qui è una stima indicativa, non certificabile.
*/
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
#include <Adafruit_BME280.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Adafruit_BME280 bme;
// Pin analogici per MQ
const int PIN_MQ135 = A0;
const int PIN_MQ2 = A1;
// Costanti elettriche: misurate il resistore di carico RL del vostro modulo (in ohm)
const float RL_MQ135 = 10000.0; // es. 10k, misurare sul vostro modulo
const float RL_MQ2 = 5000.0; // es. 5k, misurare sul vostro modulo
// Calibrazione: R0 in aria pulita (verrà appreso in setup)
float R0_MQ135 = -1.0;
float R0_MQ2 = -1.0;
// Parametri ADC
#if defined(ARDUINO_ARCH_ESP32)
const float ADC_MAX = 4095.0;
const float VREF = 3.3;
#else
const float ADC_MAX = 1023.0;
const float VREF = 5.0;
#endif
// BME280: pressione livello mare locale (hPa) per altitudine corretta
#define SEALEVELPRESSURE_HPA (1013.25) // sostituire con valore locale meteo per accuratezza [vedi note]
// Tempi preriscaldamento/calibrazione
const unsigned long MQ_PREHEAT_MS = 3UL * 60UL * 1000UL; // 3 min minimo moduli; meglio 24h per stabilità
const unsigned long CALIB_WINDOW_MS = 30UL * 1000UL; // 30s media in aria pulita
// Campionamento
const unsigned long SAMPLE_MS = 2000;
// Smoothing per AQI (NowCast semplice: EMA)
float pm25_est_ema = -1.0;
const float NOWCAST_ALPHA = 0.2;
// Utility: calcola Rs dal valore ADC
float rs_from_adc(int adc, float RL) {
float vout = (adc / ADC_MAX) * VREF;
if (vout <= 0.001) vout = 0.001;
float rs = RL * ((VREF / vout) - 1.0);
return rs;
}
// Stima euristica PM2.5 (µg/m3) da MQ e BME: proxy grezza, da tarare sul campo
float estimate_pm25_from_gas(float rsr0_135, float rsr0_2, float humidity) {
// Maggiore inquinamento → Rs/R0 più basso tipicamente per molti target gas
// Combina due sensori e aggiusta per umidità alta che può aumentare letture apparentemente “sporche”.
float gas_index = ( (1.0 / max(rsr0_135, 0.1)) * 0.6 + (1.0 / max(rsr0_2, 0.1)) * 0.4 );
// Correzione umidità: riduce leggermente a RH molto alta
float hum_factor = 1.0 - constrain((humidity - 50.0) * 0.002, 0.0, 0.2);
float pm = gas_index * 15.0 * hum_factor; // fattore scala da tarare localmente
return constrain(pm, 0.0, 500.0);
}
// AQI EPA PM2.5 con breakpoint e formula piecewise
int aqi_from_pm25(float pm) {
// Breakpoints EPA PM2.5 (µg/m3) e AQI [web:11][web:12][web:14]
struct BP { float Cl; float Ch; int Il; int Ih; };
static const BP bps[] = {
{0.0, 12.0, 0, 50},
{12.1, 35.4, 51, 100},
{35.5, 55.4,101, 150},
{55.5,150.4,151, 200},
{150.5,250.4,201,300},
{250.5,350.4,301,400},
{350.5,500.4,401,500}
};
if (pm > 500.4) return 500;
for (auto &bp : bps) {
if (pm >= bp.Cl && pm <= bp.Ch) {
float aqi = ( (bp.Ih - bp.Il) / (bp.Ch - bp.Cl) ) * (pm - bp.Cl) + bp.Il;
return (int)round(aqi);
}
}
return 0;
}
// Calibrazione R0 in aria pulita: media su finestra
float calibrate_R0(int pin, float RL) {
unsigned long t0 = millis();
double acc = 0; int n = 0;
while (millis() - t0 < CALIB_WINDOW_MS) {
int adc = analogRead(pin);
float rs = rs_from_adc(adc, RL);
acc += rs;
n++;
delay(100);
}
float rs_avg = (n > 0) ? (acc / n) : RL;
// R0 definito come Rs in aria pulita (approssimazione comune per MQ) [datasheet]
return rs_avg;
}
void draw_screen(float tempC, float hum, float pres_hPa, float alt_m,
float rsr0_135, float rsr0_2, float pm25_est, int aqi) {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print("T:"); display.print(tempC,1); display.print("C ");
display.print("RH:"); display.print(hum,0); display.print("%");
display.setCursor(0,10);
display.print("P:"); display.print(pres_hPa,1); display.print("hPa ");
display.print("Alt:"); display.print(alt_m,0); display.print("m");
display.setCursor(0,20);
display.print("MQ135 Rs/R0: "); display.print(rsr0_135,2);
display.setCursor(0,30);
display.print("MQ2 Rs/R0: "); display.print(rsr0_2,2);
display.setCursor(0,40);
display.print("PM2.5 est: "); display.print(pm25_est,1); display.print(" ug/m3");
display.setCursor(0,54);
display.print("AQI stimato: "); display.print(aqi);
display.display();
}
void setup() {
Serial.begin(115200);
Wire.begin();
// Display
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
// fall back silenzioso se non presente
} else {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0,0);
display.print("Stazione Qualita Aria");
display.setCursor(0,10);
display.print("Riscaldamento MQ...");
display.display();
}
// BME280
bool ok = bme.begin(0x76); // cambiare a 0x77 se necessario
if (!ok) {
Serial.println("BME280 non trovato!");
}
// Preriscaldamento MQ
Serial.println("Preriscaldamento MQ in corso...");
unsigned long t0 = millis();
while (millis() - t0 < MQ_PREHEAT_MS) {
delay(250);
}
// Calibrazione in aria pulita (portare i sensori all'esterno lontano da inquinanti)
Serial.println("Calibrazione R0 in aria pulita (30s)...");
if (display.width()) {
display.clearDisplay();
display.setCursor(0,0);
display.print("Calibrazione R0...");
display.display();
}
R0_MQ135 = calibrate_R0(PIN_MQ135, RL_MQ135);
R0_MQ2 = calibrate_R0(PIN_MQ2, RL_MQ2);
Serial.print("R0_MQ135 = "); Serial.println(R0_MQ135, 1);
Serial.print("R0_MQ2 = "); Serial.println(R0_MQ2, 1);
pm25_est_ema = -1.0;
}
unsigned long lastSample = 0;
void loop() {
if (millis() - lastSample < SAMPLE_MS) return;
lastSample = millis();
// Letture BME280
float tempC = NAN, hum = NAN, pres_hPa = NAN, alt_m = NAN;
if (bme.sensorID()) {
tempC = bme.readTemperature();
hum = bme.readHumidity();
pres_hPa = bme.readPressure() / 100.0;
alt_m = bme.readAltitude(SEALEVELPRESSURE_HPA);
}
// Letture MQ
int adc135 = analogRead(PIN_MQ135);
int adc2 = analogRead(PIN_MQ2);
float rs135 = rs_from_adc(adc135, RL_MQ135);
float rs2 = rs_from_adc(adc2, RL_MQ2);
float rsr0_135 = (R0_MQ135 > 0) ? (rs135 / R0_MQ135) : NAN;
float rsr0_2 = (R0_MQ2 > 0) ? (rs2 / R0_MQ2) : NAN;
// Stima PM2.5 grezza dai gas e umidità
float pm25_est = estimate_pm25_from_gas(rsr0_135, rsr0_2, isfinite(hum)?hum:50.0);
// NowCast/EMA semplice
if (pm25_est_ema < 0) pm25_est_ema = pm25_est;
else pm25_est_ema = NOWCAST_ALPHA * pm25_est + (1.0 - NOWCAST_ALPHA) * pm25_est_ema;
// AQI da PM2.5 stimato
int aqi = aqi_from_pm25(pm25_est_ema);
// Serial log
Serial.print("T="); Serial.print(tempC,1);
Serial.print("C RH="); Serial.print(hum,0);
Serial.print("% P="); Serial.print(pres_hPa,1);
Serial.print("hPa Alt="); Serial.print(alt_m,0); Serial.print("m | ");
Serial.print("MQ135 Rs/R0="); Serial.print(rsr0_135,2);
Serial.print(" MQ2 Rs/R0="); Serial.print(rsr0_2,2); Serial.print(" | ");
Serial.print("PM2.5_est="); Serial.print(pm25_est_ema,1);
Serial.print(" ug/m3 AQI="); Serial.println(aqi);
// Display
draw_screen(tempC, hum, pres_hPa, alt_m, rsr0_135, rsr0_2, pm25_est_ema, aqi);
}
