analogRead Arduino: điện áp tham chiếu AREF, đo Vcc thật, lấy trung bình, oversampling, giảm nhiễu
analogRead trên Arduino Uno, Nano: công thức đổi điện áp, chọn AREF DEFAULT, INTERNAL, EXTERNAL, đo Vcc thật bằng 1,1 V, lấy trung bình có bù nửa bậc, oversampling 12 bit, giảm nhiễu.
analogRead() là lệnh đọc điện áp analog trên Arduino: Uno, Nano (chip ATmega328P) trả về số từ 0 đến 1023 ứng với 0 V đến điện áp tham chiếu. Đọc được thì dễ, đọc đúng thì khó hơn: số đọc nhảy vài đơn vị, điện áp tính ra lệch vài chục mV khi cắm cổng USB khác, lấy trung bình nhiều mẫu mà không chính xác thêm. Bài này giải thích analogRead trên Arduino hoạt động thế nào, chọn điện áp tham chiếu AREF, đo điện áp nguồn thật bằng tham chiếu 1,1 V bên trong chip, lấy trung bình đúng cách (có bù nửa bậc), oversampling lên 12 bit, và các nguồn nhiễu hay gặp. Có chương trình cho Arduino Uno, Nano và mô phỏng bằng Python.
1. analogRead hoạt động thế nào
số đọc = điện áp vào × 1024 / Vref (làm tròn xuống, 0 đến 1023)
điện áp = (số đọc + 0,5) × Vref / 1024

- Với Vref 5 V, mỗi bậc (1 LSB) là 4,88 mV.
- Số đọc k nghĩa là điện áp nằm đâu đó từ k đến k + 1 LSB; giá trị giữa khoảng là k + 0,5. Nhiều ví dụ chia cho 1023 thay vì 1024: sai rất nhỏ, nhưng công thức đúng theo datasheet ATmega328P là 1024.
- Mỗi lần đọc mất khoảng 100 µs (bộ chia clock ADC mặc định 128 ở 16 MHz, 13 chu kỳ mỗi lần chuyển đổi).
- Nguồn tín hiệu nên có trở kháng dưới 10 kΩ để tụ lấy mẫu trong chip nạp kịp. Cầu chia 100 kΩ thì thêm tụ 100 nF tại chân vào.
2. Điện áp tham chiếu: DEFAULT, INTERNAL, EXTERNAL

- DEFAULT: tham chiếu là nguồn 5 V của bo. Cảm biến kiểu tỷ lệ (biến trở, cảm biến nuôi cùng 5 V) không bị ảnh hưởng khi nguồn thay đổi, vì tín hiệu và tham chiếu đổi cùng tỷ lệ. Cảm biến cho điện áp tuyệt đối (LM35, cảm biến có ổn áp riêng) thì bị lệch theo nguồn.
- INTERNAL: 1,1 V bên trong chip, độ phân giải 1,07 mV, hợp tín hiệu nhỏ. Giá trị thật mỗi chip lệch vài phần trăm quanh 1,1 V: đo một lần bằng đồng hồ (đo tại chân AREF khi đã chọn INTERNAL) rồi sửa trong code.
- EXTERNAL: lấy điện áp ở chân AREF, thường từ IC tham chiếu chính xác (2,5 V, 4,096 V). Phải gọi
analogReference(EXTERNAL)trước mọi lệnhanalogRead; nếu không, tham chiếu bên trong bị nối thẳng vào điện áp ngoài.
3. Điện áp nguồn thật không phải 5,00 V
Cổng USB máy tính cho 4,6–5,2 V tùy máy, qua diode và cầu chì trên bo còn sụt thêm. Nếu nguồn thật là 4,72 V mà code vẫn tính theo 5,00 V, một điện áp 2,501 V sẽ thành 2,649 V, lệch gần 6 %. Cách sửa không cần linh kiện: cho ADC đo chính tham chiếu 1,1 V bên trong với AVcc làm chuẩn, từ đó suy ra AVcc:
Vcc (mV) = 1100 × 1024 / số đọc của kênh 1,1 V
Hàm docVccMv() trong chương trình ở mục 5 làm việc này bằng thanh ghi ADMUX. Số 1100 sửa theo số đo thật của chip để có kết quả chính xác nhất.
4. Lấy trung bình và oversampling
Script dưới đây mô phỏng ADC 10 bit đo 2,5013 V khi tín hiệu có nhiễu 2 LSB, lấy trung bình từ 1 đến 256 mẫu, mỗi trường hợp 300 lần:
# PYWORLD.VN - Mô phỏng ADC 10 bit của Arduino: lấy trung bình N mẫu giảm nhiễu theo căn bậc hai của N
# PYWORLD.VN - ADC làm tròn xuống nên trung bình lệch thấp 0,5 LSB: so sánh có và không cộng bù 0,5; oversampling cần có nhiễu
# PYWORLD.VN - Và sai số khi Vcc thật là 4,72 V (cổng USB) nhưng code vẫn tính theo 5,00 V
"""ADC lý tưởng: số đọc = floor(V / Vref × 1024), kẹp 0..1023."""
from __future__ import annotations
import math
import random
LSB = 5.0 / 1024
def adc(v: float, vref: float = 5.0) -> int:
return max(0, min(1023, int(v / vref * 1024)))
def tb_ma(v_that: float, nhieu_lsb: float, n: int, rnd: random.Random) -> float:
"""Trung bình n số đọc (đơn vị LSB) khi tín hiệu có nhiễu chuẩn nhieu_lsb."""
return sum(adc(v_that + rnd.gauss(0, nhieu_lsb * LSB)) for _ in range(n)) / n
def main() -> None:
rnd = random.Random(2)
v = 2.5013
print(f"Dien ap that {v} V, 1 LSB = {LSB * 1000:.2f} mV")
for n in (1, 4, 16, 64, 256):
ma = [tb_ma(v, 2.0, n, rnd) for _ in range(300)]
rms_khong = math.sqrt(sum((m * LSB - v) ** 2 for m in ma) / len(ma)) * 1000
rms_bu = math.sqrt(sum(((m + 0.5) * LSB - v) ** 2 for m in ma) / len(ma)) * 1000
print(f" nhieu 2 LSB, TB {n:3d} mau: RMS khong bu {rms_khong:5.2f} mV, co bu 0,5 LSB {rms_bu:5.2f} mV"
f" (ly thuyet {2 * LSB * 1000 / math.sqrt(n):5.2f})")
for nhieu in (0.0, 0.5):
kq = (tb_ma(v, nhieu, 64, rnd) + 0.5) * LSB
print(f" TB 64 mau co bu, nhieu {nhieu} LSB: {kq * 1000:7.2f} mV (sai {abs(kq - v) * 1000:.2f} mV)")
vcc_that, so = 4.72, adc(v, 4.72)
print(f"Vcc that 4,72 V: so doc {so} -> tinh theo 5,00 V = {(so + 0.5) * 5 / 1024:.3f} V,"
f" theo Vcc do duoc = {(so + 0.5) * vcc_that / 1024:.3f} V")
if __name__ == "__main__":
main()

- Có bù 0,5 LSB, sai số giảm theo căn bậc hai số mẫu: 64 mẫu còn 1,18 mV, 256 mẫu còn 0,60 mV.
- Không bù, sai số dừng ở khoảng 2,5 mV dù lấy 256 mẫu: trung bình các số đã làm tròn xuống luôn thấp hơn điện áp thật nửa bậc.
- Oversampling: cộng 4ⁿ mẫu rồi chia 2ⁿ để thêm n bit (16 mẫu cho 12 bit). Cách này chỉ có tác dụng khi tín hiệu có nhiễu khoảng nửa bậc trở lên: tín hiệu quá sạch thì mọi mẫu giống nhau, trung bình bao nhiêu cũng vậy. Mô phỏng: 64 mẫu với tín hiệu sạch lệch 1,14 mV (nằm giữa bậc), với nhiễu 0,5 LSB chỉ lệch 0,31 mV.
- Lấy trung bình làm chậm việc đo: 64 mẫu mất khoảng 7 ms. Tín hiệu đổi nhanh thì dùng lọc trượt thay vì đọc nhiều lần một chỗ.
5. Chương trình cho Arduino Uno, Nano
Chạy trên Arduino Uno hoặc Arduino Nano (cùng chip ATmega328P). Nối biến trở hoặc cảm biến analog vào A0, A1. Cách dùng biến trở, quang trở, NTC làm tín hiệu thử có ở bài biến trở, quang trở và nhiệt điện trở NTC.
// PYWORLD.VN - analogRead chính xác hơn trên Arduino Uno: đo điện áp nguồn thật bằng tham chiếu 1,1 V bên trong chip
// PYWORLD.VN - Bỏ mẫu đầu sau khi đổi kênh, lấy trung bình 64 mẫu, oversampling 16 mẫu lên 12 bit; in điện áp A0 và A1
// PYWORLD.VN - Biến trở hoặc cảm biến analog vào A0, A1 (trở kháng nguồn nên dưới 10 kΩ); mở Serial Monitor 115200
const int SO_MAU_TB = 64;
// Đo Vcc: cho ADC đo tham chiếu 1,1 V bên trong, lấy Vcc làm chuẩn. Vcc = 1,1 V × 1024 / số đọc (ATmega328P).
long docVccMv() {
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1); // chuẩn AVcc, kênh vào = tham chiếu 1,1 V
delay(2); // chờ điện áp tham chiếu ổn định
ADCSRA |= _BV(ADSC);
while (bit_is_set(ADCSRA, ADSC)) {}
uint16_t so = ADC;
return 1126400L / so; // 1100 mV × 1024; sửa 1100 theo số đo thật của chip
}
// Đổi kênh: mẫu đầu tiên thường lệch vì tụ lấy mẫu còn giữ điện áp của kênh trước, nên bỏ đi.
// ADC làm tròn xuống: trung bình nhiều mẫu lệch thấp 0,5 LSB, nên cộng bù 0,5.
float docTrungBinh(int chan) {
analogRead(chan);
long tong = 0;
for (int i = 0; i < SO_MAU_TB; i++) tong += analogRead(chan);
return (float)tong / SO_MAU_TB + 0.5f;
}
// Oversampling: cộng 16 mẫu 10 bit rồi chia 4 được số 12 bit (0–4095); cần có chút nhiễu ngẫu nhiên mới hiệu quả.
// +8 = bù 0,5 LSB của mỗi mẫu (16 × 0,5).
float doc12Bit(int chan) {
analogRead(chan);
uint16_t tong = 0;
for (int i = 0; i < 16; i++) tong += analogRead(chan);
return (tong + 8) / 4.0f;
}
void setup() {
Serial.begin(115200);
analogReference(DEFAULT); // dùng AVcc làm chuẩn (5 V trên Uno)
Serial.println("Vcc_mV,A0_tho,A0_TB64_V,A0_12bit_V,A1_TB64_V");
}
void loop() {
long vcc = docVccMv();
analogRead(A0); // sau khi đổi ADMUX cho phép đo Vcc, đọc bỏ một lần
int tho = analogRead(A0);
float a0 = docTrungBinh(A0) * vcc / 1024.0 / 1000.0;
float a0_12 = doc12Bit(A0) * vcc / 4096.0 / 1000.0;
float a1 = docTrungBinh(A1) * vcc / 1024.0 / 1000.0;
Serial.print(vcc);
Serial.print(',');
Serial.print(tho);
Serial.print(',');
Serial.print(a0, 4);
Serial.print(',');
Serial.print(a0_12, 4);
Serial.print(',');
Serial.println(a1, 4);
delay(500);
}
- Mỗi dòng in: Vcc đo được, số đọc thô của A0, điện áp A0 trung bình 64 mẫu, A0 oversampling 12 bit, A1.
- Sau khi đo Vcc, ADC vừa đổi kênh nên chương trình đọc bỏ một lần; mỗi hàm đọc cũng bỏ mẫu đầu.
- Mở Serial Plotter để thấy cột số thô nhảy còn cột trung bình gần như phẳng.
6. Các nguồn nhiễu và cách giảm
- Nguồn USB nhiễu: dùng nguồn ngoài ổn định, thêm tụ 100 nF và 10 µF gần chân cảm biến.
- Dây tín hiệu dài: dây xoắn với GND, tụ 100 nF tại chân analog, tránh đi cạnh dây động cơ, relay.
- Chung mass với tải lớn: dòng của động cơ, LED công suất chạy qua dây GND chung làm lệch điện áp đất; đi dây GND riêng cho cảm biến về chân GND của bo.
- Đọc ngay khi relay, động cơ vừa đóng: chờ vài ms sau khi đóng tải mới đo.
- Cần chính xác hơn 10 bit: dùng ADC ngoài như module ADS1115 16 bit, có tham chiếu riêng và đo vi sai.
ADC của ESP32 khác Arduino ở thang đo (độ suy giảm), tính phi tuyến và hàm analogReadMilliVolts(): xem bài đọc ADC trên ESP32.
7. Lỗi hay gặp
| Hiện tượng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Chân để trống đọc lung tung | Chân analog không nối gì thì nhận nhiễu | Bình thường; nối tín hiệu hoặc điện trở xuống GND |
| Đọc A1 bị ảnh hưởng bởi A0 | Đổi kênh liên tục, nguồn tín hiệu trở kháng cao | Bỏ mẫu đầu, giảm trở kháng nguồn, thêm tụ |
| Cùng điện áp, cắm máy khác ra số khác | Vcc thay đổi theo cổng USB | Đo Vcc bằng tham chiếu 1,1 V hoặc dùng INTERNAL, EXTERNAL |
| Luôn đọc 1023 | Điện áp vượt Vref (đặc biệt khi dùng INTERNAL 1,1 V) | Cầu chia điện áp, đổi tham chiếu |
| Trung bình nhiều mẫu vẫn lệch | Quên bù 0,5 LSB, sai Vref | Cộng 0,5 trước khi đổi ra volt, đo Vref thật |
8. Xem thêm
- Arduino là gì: bo mạch, Arduino IDE, setup() và loop()
- Bắt đầu với Arduino Nano CH340
- Cảm biến chuyển vị: biến trở, LVDT
Bài cùng cụm chủ đề: sơ đồ chân Arduino Uno; millis() thay cho delay(); độ phân giải ADC và ENOB; lọc trung bình trượt.
Xem thêm