Cảm biến đọc giá trị nhảy lung tung: đo nhiễu, sửa nguồn, mass, dây, chọn bộ lọc trung vị hay EMA
Cảm biến analog đọc nhảy lung tung trên ESP32, Arduino: đo nhiễu bằng độ lệch chuẩn, tìm nguyên nhân ở nguồn, mass, dây, Wi-Fi, sửa phần cứng, rồi chọn bộ lọc trung vị, trung bình, EMA.
Cảm biến analog cho số đọc nhảy lên xuống liên tục, thỉnh thoảng vọt lên một giá trị vô lý rồi trở lại, là lỗi rất hay gặp với ESP32, Arduino. Cách vội nhất là lấy trung bình trong code, nhưng nhiều khi nguyên nhân nằm ở nguồn, dây mass hay dây tín hiệu, và lọc sai loại còn làm số đọc tệ hơn. Bài này chỉ cách tìm nguyên nhân cảm biến đọc giá trị nhảy lung tung theo thứ tự: đo nhiễu bằng con số, sửa nguồn, mass, dây, rồi mới chọn bộ lọc phù hợp với loại nhiễu (trung vị, trung bình trượt, EMA). Có chương trình chẩn đoán cho ESP32 và mô phỏng so sánh các bộ lọc.
1. Đo nhiễu bằng con số trước khi sửa
"Nhảy nhiều" khó so sánh; độ lệch chuẩn của 500 lần đọc liên tiếp thì so được. Chương trình dưới đây có chế độ đo nhiễu (gõ n): in trung bình, độ lệch chuẩn, giá trị nhỏ nhất, lớn nhất và số gai. Đo một lần trước khi sửa, đo lại sau mỗi thay đổi; con số giảm là sửa đúng chỗ.
// PYWORLD.VN - Chẩn đoán cảm biến analog đọc nhảy trên ESP32: đo 500 mẫu, in trung bình, độ lệch chuẩn, nhỏ nhất, lớn nhất, số gai
// PYWORLD.VN - Đo trước và sau mỗi lần sửa (thêm tụ, đổi nguồn, đi lại dây mass) để biết sửa có tác dụng không
// PYWORLD.VN - Chế độ đọc thường: lọc trung vị 5 rồi EMA; báo lỗi khi số đọc dính sát 0 hoặc sát thang đo (đứt dây, chập)
const int CHAN = 34; // ADC1: dùng được cả khi Wi-Fi đang chạy (ADC2 thì không)
const int SO_MAU_DO = 500;
const float ALPHA = 0.2f;
int mau[SO_MAU_DO];
void doNhieu() {
long tong = 0;
int nho = 4095, lon = 0;
for (int i = 0; i < SO_MAU_DO; i++) {
mau[i] = analogRead(CHAN);
tong += mau[i];
nho = min(nho, mau[i]);
lon = max(lon, mau[i]);
delayMicroseconds(500);
}
float tb = (float)tong / SO_MAU_DO, ps = 0;
for (int i = 0; i < SO_MAU_DO; i++) ps += (mau[i] - tb) * (mau[i] - tb);
float lech = sqrtf(ps / (SO_MAU_DO - 1));
int gai = 0;
for (int i = 0; i < SO_MAU_DO; i++)
if (fabsf(mau[i] - tb) > 5 * lech + 10) gai++; // mẫu lệch quá 5 lần độ lệch chuẩn: coi là gai
Serial.printf("TB %.1f, do lech chuan %.2f, nho nhat %d, lon nhat %d, gai %d / %d\n", tb, lech, nho, lon, gai, SO_MAU_DO);
}
float trungVi5(float moi) {
static float vung[5] = {0};
static int vt = 0, dem = 0;
vung[vt] = moi;
vt = (vt + 1) % 5;
if (dem < 5) dem++;
float s[5];
memcpy(s, vung, sizeof(s));
for (int i = 1; i < dem; i++)
for (int j = i; j > 0 && s[j - 1] > s[j]; j--) {
float t = s[j];
s[j] = s[j - 1];
s[j - 1] = t;
}
return s[dem / 2];
}
void setup() {
Serial.begin(115200);
analogSetPinAttenuation(CHAN, ADC_11db);
Serial.println("Lenh: n = do nhieu 500 mau. Mac dinh in gia tri da loc moi 100 ms");
}
void loop() {
if (Serial.available() && Serial.read() == 'n') doNhieu();
static uint32_t lan = 0;
static float loc = -1;
if (millis() - lan < 100) return;
lan = millis();
int tho = analogRead(CHAN);
float tv = trungVi5(tho);
loc = (loc < 0) ? tv : loc + ALPHA * (tv - loc); // EMA sau trung vị
const char *canh = (tho < 10) ? " (sat 0: dut day? chap mass?)" : (tho > 4085) ? " (sat thang do: vuot dai, chap nguon?)" : "";
Serial.printf("tho:%d,loc:%.1f%s\n", tho, loc, canh);
}
- Nối tín hiệu cảm biến vào GPIO34 của ESP32 DevKit V1. GPIO32–39 thuộc ADC1, dùng được khi Wi-Fi đang chạy; các chân ADC2 không đọc được khi bật Wi-Fi.
- Ở chế độ thường, chương trình in hai cột
thovàloccho Serial Plotter, và cảnh báo khi số đọc dính sát 0 (thường là đứt dây tín hiệu, chập mass) hoặc sát 4095 (vượt thang đo, chập nguồn). - Số đọc dao động vài đơn vị trên thang 4095 là bình thường với ADC của ESP32; độ lệch chuẩn vài chục đơn vị hoặc có nhiều gai mới cần sửa.
2. Tìm nguyên nhân

- Nhảy theo nhịp tải (bơm, relay, LED công suất đóng là nhảy): dòng tải đi chung dây mass với cảm biến, làm điện áp mass của cảm biến lệch đi. Đi dây mass cảm biến riêng về chân GND của bo. Relay đóng làm mạch khởi động lại là cùng gốc: xem bài đóng relay làm ESP32 bị reset.
- Đổi nguồn là đổi mức nhiễu (cổng USB máy tính, cục sạc rẻ, mạch hạ áp xung): đo bằng chế độ
nvới nguồn pin để so; thêm tụ lọc tại nguồn cảm biến. - Dây dài: dây tín hiệu như một ăng-ten nhận nhiễu 50 Hz từ dây điện. Rút ngắn, xoắn dây tín hiệu với dây GND.
- Chạm tay vào dây thì số đổi: chân đang thả nổi hoặc dây lỏng.
3. Sửa phần cứng

Tụ 100 nF đặt sát chân ADC vừa lọc nhiễu tần số cao vừa giúp ADC lấy mẫu chính xác hơn khi nguồn tín hiệu có trở kháng lớn. Lọc RC 1 kΩ và 100 nF có tần số cắt khoảng 1,6 kHz: đủ nhanh cho cảm biến nhiệt độ, áp suất, độ ẩm, chặn được gai ngắn do Wi-Fi và tải đóng cắt.
4. Chọn bộ lọc theo loại nhiễu
Sau khi sửa phần cứng, phần nhiễu còn lại mới cần lọc bằng code. Script dưới đây so sánh bốn cách lọc trên một tín hiệu thử có nhiễu đều, 4 % mẫu là gai lớn và một bậc thay đổi thật:
# PYWORLD.VN - So sánh bộ lọc cho số đọc cảm biến bị nhảy: trung vị 5, trung bình trượt 10, EMA alpha 0,2, trung vị rồi EMA
# PYWORLD.VN - Tín hiệu thử có nhiễu ngẫu nhiên, gai (Wi-Fi phát, relay đóng) và một bậc thay đổi thật; đo sai số RMS và độ trễ theo bậc
# PYWORLD.VN - Chạy: python so_sanh_bo_loc.py
"""Sai số RMS tính trên đoạn ổn định; độ trễ = số mẫu từ lúc bậc xảy ra tới khi đi được 90 % độ cao bậc."""
from __future__ import annotations
import math
import random
from statistics import median
N, BAC_O, CAO = 300, 150, 200.0 # bậc 1000 → 1200 ở mẫu 150
def tin_hieu(seed: int = 8):
rnd = random.Random(seed)
that, do = [], []
for i in range(N):
v = 1000.0 + (CAO if i >= BAC_O else 0)
x = v + rnd.gauss(0, 8)
if rnd.random() < 0.04: # 4 % mẫu là gai lớn
x += rnd.choice([-1, 1]) * rnd.uniform(150, 400)
that.append(v)
do.append(x)
return that, do
def trung_vi(x, n=5):
return [median(x[max(0, i - n + 1):i + 1]) for i in range(len(x))]
def trung_binh(x, n=10):
return [sum(x[max(0, i - n + 1):i + 1]) / len(x[max(0, i - n + 1):i + 1]) for i in range(len(x))]
def ema(x, a=0.2):
y, ra = x[0], []
for v in x:
y += a * (v - y)
ra.append(y)
return ra
def danh_gia(that, y):
on_dinh = [i for i in range(20, N) if not BAC_O <= i < BAC_O + 30]
rms = math.sqrt(sum((y[i] - that[i]) ** 2 for i in on_dinh) / len(on_dinh))
tre = next((i - BAC_O for i in range(BAC_O, N) if y[i] >= 1000 + 0.9 * CAO), None)
return rms, tre
BO_LOC = {"Khong loc": lambda x: x, "Trung vi 5": trung_vi, "Trung binh 10": trung_binh, "EMA 0,2": ema,
"Trung vi 5 + EMA 0,2": lambda x: ema(trung_vi(x))}
def main() -> None:
that, do = tin_hieu()
for ten, f in BO_LOC.items():
rms, tre = danh_gia(that, f(do))
print(f"{ten:22s} sai so RMS {rms:6.1f} tre theo bac {tre} mau")
if __name__ == "__main__":
main()


- Có gai: trung vị 5 mẫu giảm sai số từ 50 xuống 4,3 và chỉ trễ 2 mẫu. Trung bình trượt và EMA không bỏ được gai mà trải gai ra nhiều mẫu (sai số còn khoảng 19).
- Nhiễu đều (không gai): trung bình trượt hoặc EMA làm mượt tốt và tốn ít bộ nhớ.
- Kết hợp: trung vị trước để bỏ gai, EMA sau để làm mượt; sai số nhỏ nhất (2,5) nhưng trễ nhiều nhất. Chương trình ESP32 ở mục 1 dùng cách này.
Lý thuyết và cách chọn hệ số cho từng bộ lọc có ở các bài cùng cụm về lọc trung bình trượt, lọc EMA và bộ lọc Kalman.
5. Cảm biến số cũng có thể đọc sai
- Cảm biến giao tiếp số (DHT22, DS18B20, cảm biến I2C) không bị nhiễu kiểu analog, nhưng đọc lỗi sẽ cho giá trị vô lý (−127, 85, 0). Kiểm tra mã kiểm tra (checksum, CRC) và bỏ lần đọc lỗi thay vì đưa vào trung bình.
- Dây I2C dài, kéo lên yếu làm đọc lúc được lúc không: xem bài cùng cụm về I2C không thấy thiết bị.
6. Bảng tra nhanh
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Gai lớn từng lúc | Tải đóng cắt, Wi-Fi phát | Mass riêng, tụ, lọc trung vị |
| Gợn đều liên tục | Nguồn nhiễu, nhiễu 50 Hz trên dây dài | Tụ nguồn, dây xoắn, lọc trung bình |
| Số trôi chậm theo thời gian | Nhiệt độ, cảm biến chưa ổn định | Chờ ổn định, bù nhiệt; không phải nhiễu |
| Dính 0 hoặc dính tối đa | Đứt dây, chập, vượt thang đo | Kiểm tra dây, cầu chia điện áp |
| Đọc được khi tắt Wi-Fi, hỏng khi bật | Dùng chân ADC2 | Chuyển sang GPIO32–39 |
Bài cùng cụm chủ đề: lọc trung bình trượt; lọc EMA; bộ lọc Kalman một chiều; nhiễu 50 Hz và cách loại bỏ; vòng đất (ground loop); I2C không thấy thiết bị.
Xem thêm