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
Bậc thang ADC 10 bit phóng to 8 bậc đầu: số đọc k ứng với điện áp từ k đến k cộng 1 LSB, giữa khoảng là k cộng 0,5 LSB; 1 LSB bằng Vref chia 1024 bằng 4,88 mV với Vref 5 V
Mỗi số đọc là một khoảng điện áp rộng 1 LSB.
  • 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

Bảng chọn điện áp tham chiếu trên Arduino Uno: DEFAULT dùng AVcc khoảng 5 V mỗi LSB 4,88 mV; INTERNAL 1,1 V mỗi LSB 1,07 mV; EXTERNAL dùng điện áp ở chân AREF; cảnh báo gọi analogReference EXTERNAL trước khi đọc
Tham chiếu quyết định thang đo và độ chính xác.
  • 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ệnh analogRead; 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()
Đồ thị sai số RMS theo số mẫu lấy trung bình từ 1 đến 256: có bù 0,5 LSB giảm theo căn bậc hai của số mẫu xuống 0,60 mV ở 256 mẫu, khớp lý thuyết; không bù thì dừng ở khoảng 2,5 mV
Không cộng bù nửa bậc, lấy thêm mẫu cũng vô ích.
  • 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ượngNguyên nhânCách xử lý
Chân để trống đọc lung tungChân analog không nối gì thì nhận nhiễuBì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 caoBỏ 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ácVcc 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ệchQuên bù 0,5 LSB, sai VrefCộng 0,5 trước khi đổi ra volt, đo Vref thật

8. Xem thêm

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.


Tin tức liên quan

Arduino IDE 2: tự gợi ý code, quản lý board và thư viện, gỡ lỗi từng bước, log khi không có JTAG
Arduino IDE 2: tự gợi ý code, quản lý board và thư viện, gỡ lỗi từng bước, log khi không có JTAG

Dùng Arduino IDE 2 hiệu quả: tự gợi ý code, nhảy tới định nghĩa, quản lý board và thư viện, thư mục của IDE, gỡ lỗi từng bước, macro log có mức cho ESP32.

Trẻ mấy tuổi nên học lập trình? Lộ trình từ 4 đến 15 tuổi cho phụ huynh
Trẻ mấy tuổi nên học lập trình? Lộ trình từ 4 đến 15 tuổi cho phụ huynh

Trẻ mấy tuổi nên học lập trình: lộ trình năm bước từ trò chơi không màn hình, ScratchJr, Scratch đến mạch ESP32 và Python, kèm dấu hiệu trẻ sẵn sàng và câu hỏi thường gặp.

Các nguồn sai số khi đo lực bằng load cell: lệch tâm, rão, trễ, nhiệt độ và ngân sách sai số
Các nguồn sai số khi đo lực bằng load cell: lệch tâm, rão, trễ, nhiệt độ và ngân sách sai số

Vì sao đo lực thực tế sai hơn phiếu thông số load cell: lệch tâm và gá lắp, rão, trễ, phi tuyến, nhiệt độ, cáp và bộ đọc; ví dụ ngân sách sai số và danh sách kiểm tra nhanh.

Hiệu chuẩn đồng hồ so theo ISO 463 và đồng hồ chân gạt ISO 9493: đặc tính sai số và sai số cosin
Hiệu chuẩn đồng hồ so theo ISO 463 và đồng hồ chân gạt ISO 9493: đặc tính sai số và sai số cosin

Hiệu chuẩn đồng hồ so theo ISO 463: sai số trong 1/10 vòng, 1 vòng, toàn dải, trễ, lặp lại; trình tự trên máy hiệu chuẩn, ví dụ kết quả, sai số cosin của đồng hồ chân gạt.

Học lập trình có lợi gì cho trẻ? Lợi ích thật và những hiểu lầm thường gặp
Học lập trình có lợi gì cho trẻ? Lợi ích thật và những hiểu lầm thường gặp

Học lập trình có lợi gì cho trẻ: sáu lợi ích có cơ sở, nghiên cứu nói gì, sáu hiểu lầm thường gặp và cách phụ huynh nhìn ra tiến bộ thật của con qua năm bậc.

Cài ESP-IDF trên Windows và dự án đầu tiên: idf.py, app_main, menuconfig, nạp và xem nhật ký
Cài ESP-IDF trên Windows và dự án đầu tiên: idf.py, app_main, menuconfig, nạp và xem nhật ký

Cài ESP-IDF trên Windows bằng bộ cài hoặc tiện ích VS Code, cấu trúc dự án, các lệnh idf.py, dự án đầu tiên với app_main và ESP_LOGI, tệp nạp và địa chỉ, so với Arduino.

LM2596 là gì: sơ đồ chân, thông số, cách chỉnh 5 V – 3,3 V, dòng tải thực tế và nhiệt độ
LM2596 là gì: sơ đồ chân, thông số, cách chỉnh 5 V – 3,3 V, dòng tải thực tế và nhiệt độ

LM2596 là IC hạ áp buck 150 kHz: sơ đồ chân, thông số datasheet, sơ đồ nguyên lý, công thức Vout = 1,23 × (1 + R2/R1), cách chỉnh module ra 5 V, 3,3 V, dòng tải, nhiệt và lỗi hay gặp.

Đầu công cụ của máy CNC và máy laser: spindle, trimmer, mũi phay, module laser diode, thấu kính
Đầu công cụ của máy CNC và máy laser: spindle, trimmer, mũi phay, module laser diode, thấu kính

Các loại spindle cho máy CNC mini, kiểu mũi phay xoắn lên, xoắn xuống, mũi V, đầu cầu và cách chọn, collet ER11, cấu tạo module laser diode, tín hiệu PWM và chọn công suất laser.


Bình luận
Đã thêm vào giỏ hàng