MQ-135 là cảm biến “đời phổ thông” dùng trong DIY để theo dõi xu hướng chất lượng không khí (air quality) như: khói nhẹ, hơi cồn, VOC, ammonia… Bài này tập trung vào cách làm ổn định – dễ hiển thị dashboard – cảnh báo theo ngưỡng, thay vì cố ra “ppm chuẩn” (vì MQ-135 muốn ppm chuẩn cần hiệu chuẩn nghiêm túc).
1) MQ-135 đo gì? Dùng đúng kỳ vọng
- MQ-135 nhạy với nhiều loại khí (VOC, NH3, khói, hơi dung môi…).
- Module thường có:
- AO (Analog Out): giá trị analog thể hiện “mức khí tổng hợp” (không khí bẩn hơn ⇒ AO thay đổi).
- DO (Digital Out): bật/tắt theo ngưỡng biến trở trên module.
Kết luận thực tế: MQ-135 phù hợp nhất cho:
- Theo dõi chỉ số tương đối (index 0–100), phát hiện bất thường theo baseline.
- Cảnh báo “không khí đang xấu đi” theo thời gian.
2) Chuẩn bị phần cứng
- ESP32 (DevKit / ESP32-S3/C3 đều được)
- Module MQ-135
- Dây jumper
- Nguồn 5V ổn định (heater của MQ tiêu thụ dòng tương đối)
MQ-135 có heater nên ấm khi chạy. Đặt nơi thoáng, tránh sát nhựa dễ biến dạng.
3) Nối dây chuẩn (dùng AO)
Sơ đồ nối dây
| MQ-135 | ESP32 |
|---|---|
| VCC | 5V |
| GND | GND |
| AO | GPIO34 (ADC) (hoặc 32/33/35) |
| DO (tuỳ chọn) | GPIO25 (input) |
Lưu ý điện áp AO
- ESP32 ADC tối đa ~3.3V (tùy config).
- Nhiều module MQ-135 có AO không lên tới 5V (do mạch chia áp/LM393), nhưng không phải module nào cũng giống nhau. ➡️ Cách an toàn: đo AO bằng đồng hồ khi chạy, nếu AO có thể >3.3V thì cần chia áp trước khi đưa vào ESP32.
4) Chiến lược đọc “realtime nhưng không nhiễu”
MQ sensor thường nhiễu và trôi, nên bạn nên làm 3 lớp:
- Warm-up: chờ ổn định (tối thiểu vài phút; lần đầu có thể lâu hơn).
- Averaging: đọc nhiều mẫu, lấy trung bình.
- Baseline + Index: chuyển sang thang 0–100 để dễ hiển thị.
Ý tưởng:
- aq_raw: ADC trung bình (0–4095)
- aq_index: 0–100 (chuẩn hóa theo min/max thực nghiệm)
- aq_state: Normal / Warning / Danger (cảnh báo theo ngưỡng)
5) Code ESP32 (Arduino) – đọc MQ-135 + publish MQTT realtime
Ví dụ này:
- Đọc AO bằng ADC
- Lọc bằng trung bình nhiều mẫu
- Map ra aq_index 0–100 theo min/max bạn tự chỉnh sau khi chạy thử
- Publish MQTT mỗi 2 giây theo topic chuẩn IoTLabs
Bạn thay cấu hình Wi-Fi/MQTT cho hệ của bạn.
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#define PIN_MQ135_AO 34
#define ADC_SAMPLES 30
#define PUBLISH_EVERY_MS 2000
// --- WiFi/MQTT
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_PASS";
const char* MQTT_HOST = "broker.iotlabs.vn"; // ví dụ
const int MQTT_PORT = 1883; // TLS: 8883
const char* MQTT_USER = "YOUR_USER";
const char* MQTT_PASS = "YOUR_PASS";
const char* PROJECT_ID = "demo_project";
const char* DEVICE_ID = "esp32_mq135_01";
WiFiClient espClient;
PubSubClient mqtt(espClient);
unsigned long lastPub = 0;
// Bạn chỉnh 2 giá trị này dựa trên quan sát thực tế
// - AQ_MIN: giá trị raw khi không khí sạch tương đối
// - AQ_MAX: giá trị raw khi có mùi/VOC/khói nhẹ tăng rõ rệt
int AQ_MIN = 900; // ví dụ
int AQ_MAX = 2800; // ví dụ
uint32_t nowSeconds() {
// Nếu bạn có time sync chuẩn (NTP/time service) thì dùng unix seconds thật.
return (uint32_t)(millis() / 1000);
}
int readMq135Avg() {
uint32_t sum = 0;
for (int i = 0; i < ADC_SAMPLES; i++) {
sum += analogRead(PIN_MQ135_AO);
delay(5);
}
return (int)(sum / ADC_SAMPLES);
}
int clampInt(int x, int lo, int hi) {
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
int toIndex(int raw) {
raw = clampInt(raw, AQ_MIN, AQ_MAX);
long idx = (long)(raw - AQ_MIN) * 100L / (long)(AQ_MAX - AQ_MIN);
return clampInt((int)idx, 0, 100);
}
const char* toState(int idx) {
if (idx >= 80) return "danger";
if (idx >= 60) return "warning";
return "normal";
}
void wifiConnect() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(300);
}
void mqttConnect() {
mqtt.setServer(MQTT_HOST, MQTT_PORT);
while (!mqtt.connected()) {
String clientId = String("iotlabs-") + DEVICE_ID;
mqtt.connect(clientId.c_str(), MQTT_USER, MQTT_PASS);
delay(500);
}
}
void publishTelemetry(int raw, int idx, const char* state) {
String topic = String("iotlabs/") + PROJECT_ID + "/" + DEVICE_ID + "/telemetry";
StaticJsonDocument<256> doc;
doc["ts"] = nowSeconds();
JsonObject metrics = doc.createNestedObject("metrics");
metrics["aq_raw"] = raw;
metrics["aq_index"] = idx;
JsonObject tags = doc.createNestedObject("tags");
tags["state"] = state;
tags["sensor"] = "mq135";
char payload[256];
size_t n = serializeJson(doc, payload);
mqtt.publish(topic.c_str(), payload, n);
}
void setup() {
Serial.begin(115200);
analogReadResolution(12);
wifiConnect();
mqttConnect();
Serial.println("MQ-135 warm-up... (60s demo)");
delay(60000); // demo; thực tế nên 3-5 phút để ổn định hơn
}
void loop() {
if (WiFi.status() != WL_CONNECTED) wifiConnect();
if (!mqtt.connected()) mqttConnect();
mqtt.loop();
if (millis() - lastPub >= PUBLISH_EVERY_MS) {
lastPub = millis();
int raw = readMq135Avg();
int idx = toIndex(raw);
const char* state = toState(idx);
Serial.printf("AQ raw=%d | index=%d | state=%s\n", raw, idx, state);
publishTelemetry(raw, idx, state);
}
}
6) Cách chỉnh AQ_MIN/AQ_MAX nhanh (thực tế nhất)
- Để cảm biến chạy ổn định 3–5 phút.
- Ghi lại raw trong môi trường bình thường (đóng cửa vừa phải) ⇒ set AQ_MIN quanh giá trị đó.
- Tạo tình huống “không khí xấu hơn” nhưng an toàn:
- mở nắp chai cồn/hand sanitizer gần cảm biến vài giây (không đổ, không hít sát)
- dùng nước hoa/xịt khử mùi nhẹ ở xa Ghi lại raw khi tăng rõ ⇒ set AQ_MAX quanh giá trị đó.
Mục tiêu: để aq_index chạy 10–40 ở môi trường bình thường, và tăng mạnh khi có VOC/mùi.
7) Dashboard realtime gợi ý (chuẩn IoTLabs)
Card 1: Last value
- AQ Index: 0–100
- State badge: normal/warning/danger
Chart 1h / 24h
- Line chart aq_index
- Có vùng ngưỡng (>=60 warning, >=80 danger)
Rule cảnh báo
- Warning nếu aq_index >= 60 liên tục 15s
- Danger nếu aq_index >= 80 liên tục 10s
- Cooldown 2–5 phút để tránh spam
8) Khi nào nên nâng cấp cảm biến “chuẩn” hơn?
Nếu bạn cần số liệu “đáng tin” và dễ so sánh:
- CO2 chuẩn: SCD41 / MH-Z19B
- Bụi mịn: PMS5003/PMS7003 (PM2.5/PM10)
- TVOC/IAQ hiện đại: SGP40 / ENS160
MQ-135 vẫn ok cho DIY, nhưng production/khuyến nghị lâu dài thì 3 nhóm trên “đáng tiền” hơn.


