Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp

Hướng dẫn đọc điện áp analog bằng ADC của ESP32 trên Arduino IDE, dùng analogReadMilliVolts, làm cầu phân áp đo pin Li-ion 18650 và ước lượng phần trăm pin còn lại.

Thiết bị chạy pin nào cũng cần biết pin còn bao nhiêu để kịp báo sạc hoặc tự tắt an toàn. ESP32 có sẵn bộ chuyển đổi tương tự – số (ADC) để đo điện áp, nhưng ADC này có vài giới hạn cần hiểu trước khi dùng. Bài viết hướng dẫn đọc điện áp analog trên ESP32 bằng Arduino IDE, sau đó dùng cầu phân áp để đo điện áp pin Li-ion 18650 và ước lượng phần trăm pin còn lại.

1. ADC của ESP32: những điều cần biết

  • Độ phân giải 12 bit: analogRead() trả về số từ 0 đến 4095.
  • Chỉ đo được đến khoảng 3,1 V ở mức suy giảm lớn nhất (11 dB). Không bao giờ đưa điện áp cao hơn 3,3 V vào chân GPIO.
  • Hai nhóm chân: ADC1 gồm GPIO32–39, dùng được mọi lúc. ADC2 gồm GPIO0, 2, 4, 12–15 và 25–27, không đọc được analog khi đang bật Wi-Fi. Nên chọn chân thuộc ADC1.
  • Không tuyến tính ở hai đầu dải đo (hình dưới): điện áp rất thấp đọc ra 0, điện áp gần 3,3 V đọc ra 4095 từ sớm.
Đồ thị minh họa ADC của ESP32 so với đường lý tưởng: vùng chết dưới 0,1 V, bão hòa gần 3,3 V và vùng đo chính xác 0,15 đến 2,45 V
ADC của ESP32 đo chính xác nhất ở khoảng giữa dải (hình vẽ minh họa xu hướng, không phải số đo).

Từ gói esp32 phiên bản 3.x, cách đọc nên dùng là analogReadMilliVolts(). Hàm này trả về thẳng số milivôn, đã hiệu chỉnh theo dữ liệu mà nhà sản xuất ghi sẵn trong từng chip, nên chính xác hơn việc tự nhân giá trị thô với 3,3 / 4095.

2. Linh kiện cần chuẩn bị

3. Chương trình 1: đọc điện áp từ biến trở

Nối hai chân ngoài của biến trở vào 3V3 và GND, chân giữa vào D34 (GPIO34). Chương trình in cả giá trị thô lẫn số milivôn để so sánh:

/*
  PYWORLD.VN - ESP32 đọc điện áp analog: giá trị thô và milivôn
  Biến trở 10 kΩ: hai chân ngoài nối 3V3 và GND, chân giữa nối GPIO34
  Không cần cài thêm thư viện (gói esp32 phiên bản 3.x)
*/

#define ADC_PIN 34   // Chân thuộc nhóm ADC1, dùng được cả khi bật Wi-Fi

void setup() {
  Serial.begin(115200);
  delay(500);

  analogReadResolution(12);                     // 12 bit: giá trị thô từ 0 đến 4095
  analogSetPinAttenuation(ADC_PIN, ADC_11db);   // Thang đo rộng nhất của ADC
}

void loop() {
  int raw = analogRead(ADC_PIN);                    // Giá trị thô
  uint32_t miliVon = analogReadMilliVolts(ADC_PIN); // Đã hiệu chỉnh theo dữ liệu riêng của từng chip

  Serial.printf("raw = %4d   U = %4lu mV\n", raw, (unsigned long)miliVon);
  delay(500);
}

Chọn board DOIT ESP32 DEVKIT V1, nạp chương trình rồi mở Serial Monitor ở 115200 baud. Ví dụ kết quả khi vặn biến trở từ đầu GND lên giữa hành trình (số minh họa):

raw =    0   U =  142 mV
raw =  871   U =  850 mV
raw = 1924   U = 1650 mV
  • analogReadResolution(12): chọn độ phân giải 12 bit (đây cũng là giá trị mặc định).
  • analogSetPinAttenuation(ADC_PIN, ADC_11db): chọn mức suy giảm 11 dB để đo được dải điện áp rộng nhất. Các mức nhỏ hơn (ADC_0db, ADC_2_5db, ADC_6db) chỉ đo được điện áp thấp hơn nhưng mịn hơn.
  • analogRead() cho giá trị thô; analogReadMilliVolts() cho số milivôn đã hiệu chỉnh.

Để ý dòng đầu của ví dụ: khi chân nối GND, giá trị thô bằng 0 nhưng số milivôn vẫn khác 0. Đó là vùng chết ở đầu dưới của ADC, vì vậy không nên dùng ADC của ESP32 để đo các điện áp dưới khoảng 0,15 V.

4. Vì sao phải dùng cầu phân áp để đo pin?

Pin Li-ion 18650 có điện áp từ khoảng 3,0 V lúc cạn đến 4,2 V lúc đầy, cao hơn mức 3,3 V mà chân GPIO chịu được. Cầu phân áp gồm hai điện trở mắc nối tiếp sẽ chia điện áp pin xuống trước khi đưa vào chân ADC:

Sơ đồ nguyên lý cầu phân áp hai điện trở 100 kΩ và tụ 100 nF đưa điện áp pin Li-ion vào chân GPIO35 của ESP32
Cầu phân áp hai điện trở 100 kΩ chia đôi điện áp pin (hình vẽ sơ đồ nguyên lý).
Nối cầu phân áp đo pin với ESP32
Điểm nốiNối tớiGhi chú
Cực dương của pinMột đầu của R1 (100 kΩ)Không nối thẳng cực dương pin vào chân GPIO.
Điểm giữa R1 và R2D35 / GPIO35Chân ADC1, chỉ là ngõ vào.
Đầu còn lại của R2 (100 kΩ)GNDGND của ESP32 phải nối chung với cực âm của pin.
Tụ 100 nFGiữa GPIO35 và GNDĐặt sát chân ESP32.
  • Điện trở lớn để đỡ hao pin: với hai điện trở 100 kΩ, cầu phân áp chỉ tiêu thụ khoảng 0,02 mA.
  • Tụ 100 nF: điện trở lớn làm ADC lấy mẫu không ổn định; tụ giữ điện áp tại chân đứng yên trong lúc ADC đọc.
  • Đo đúng chỗ: lấy điện áp ở cực dương của pin (cọc B+ hoặc chân pin của mạch). Điện áp ở chân 5V hay 3V3 đã qua mạch ổn áp nên không thay đổi theo pin.
  • An toàn: chập hai cực pin Li-ion gây dòng rất lớn, có thể cháy. Kiểm tra dây nối trước khi lắp pin vào khay.

5. Chương trình 2: đo điện áp pin và ước lượng phần trăm

/*
  PYWORLD.VN - ESP32 đo điện áp pin Li-ion 18650 qua cầu phân áp và ước lượng phần trăm pin
  Cầu phân áp: cực dương pin -> R1 100 kΩ -> GPIO35 -> R2 100 kΩ -> GND; tụ 100 nF song song với R2
  Không cần cài thêm thư viện (gói esp32 phiên bản 3.x)
*/

#define ADC_PIN      35
#define R1_OHM       100000.0   // Điện trở nối lên cực dương của pin
#define R2_OHM       100000.0   // Điện trở nối xuống GND
#define HE_SO_CHINH  1.000      // = điện áp đo bằng đồng hồ / điện áp chương trình in ra
#define SO_MAU       16         // Số lần đọc để lấy trung bình

// Bảng ước lượng phần trăm pin theo điện áp lúc pin nghỉ (pin Li-ion 3,7 V)
const float BANG_VON[] = {4.20, 4.06, 3.98, 3.92, 3.87, 3.82, 3.79, 3.77, 3.74, 3.68, 3.45, 3.00};
const int BANG_PHAN_TRAM[] = {100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 5, 0};
const int SO_MUC = sizeof(BANG_PHAN_TRAM) / sizeof(BANG_PHAN_TRAM[0]);

float docDienApPin() {
  uint32_t tong = 0;
  for (int i = 0; i < SO_MAU; i++) {
    tong += analogReadMilliVolts(ADC_PIN);
    delay(2);
  }
  float vonTaiChan = tong / (float)SO_MAU / 1000.0;               // Điện áp tại chân ADC
  return vonTaiChan * (R1_OHM + R2_OHM) / R2_OHM * HE_SO_CHINH;   // Điện áp của pin
}

int phanTramPin(float von) {
  if (von >= BANG_VON[0]) {
    return 100;
  }
  for (int i = 1; i < SO_MUC; i++) {
    if (von >= BANG_VON[i]) {
      // Nội suy tuyến tính giữa hai mức liền nhau trong bảng
      float tiLe = (von - BANG_VON[i]) / (BANG_VON[i - 1] - BANG_VON[i]);
      return BANG_PHAN_TRAM[i] + (int)(tiLe * (BANG_PHAN_TRAM[i - 1] - BANG_PHAN_TRAM[i]) + 0.5);
    }
  }
  return 0;
}

void setup() {
  Serial.begin(115200);
  delay(500);

  analogSetPinAttenuation(ADC_PIN, ADC_11db);
}

void loop() {
  float von = docDienApPin();
  Serial.printf("Pin: %.2f V, khoang %d %%\n", von, phanTramPin(von));
  delay(2000);
}
Hình minh họa Serial Monitor in điện áp pin 18650 khoảng 3,90 V và phần trăm pin ước lượng 66 phần trăm
Kết quả mong đợi trên Serial Monitor khi pin ở khoảng 3,9 V (hình minh họa theo định dạng in của chương trình).

6. Giải thích các lệnh chính

  • docDienApPin() đọc 16 lần rồi lấy trung bình để giảm nhiễu, đổi sang vôn, sau đó nhân với (R1 + R2) / R2 để ra điện áp thật của pin. Với hai điện trở bằng nhau, hệ số này bằng 2.
  • HE_SO_CHINH bù sai số của điện trở và của ADC (xem mục 7).
  • phanTramPin() tra điện áp trong bảng BANG_VON, rồi nội suy tuyến tính giữa hai mức liền nhau. Ví dụ 3,90 V nằm giữa 3,92 V (70 %) và 3,87 V (60 %), kết quả là khoảng 66 %.
Đường cong ước lượng điện áp pin Li-ion theo phần trăm dung lượng còn lại, từ 4,2 V lúc đầy đến 3,0 V lúc cạn
Đường cong điện áp – dung lượng dùng trong chương trình (hình vẽ theo bảng ước lượng trong code).

Điện áp pin không giảm đều theo dung lượng, nên công thức chia đều từ 3,0 V đến 4,2 V cho kết quả sai nhiều. Bảng trong chương trình là giá trị ước lượng cho pin Li-ion 3,7 V lúc nghỉ; phần trăm tính ra chỉ nên dùng để tham khảo.

7. Hiệu chỉnh bằng đồng hồ đo

  1. Đo điện áp pin bằng đồng hồ, ví dụ được 3,95 V.
  2. Xem số chương trình in ra cùng lúc đó, ví dụ 3,90 V.
  3. Tính HE_SO_CHINH = 3,95 / 3,90 ≈ 1,013, sửa vào chương trình rồi nạp lại.

Điện trở thường có sai số 1–5 %, nên bước này giúp số đo sát thực tế hơn hẳn. Chỉ cần làm một lần cho mỗi mạch.

8. Lưu ý khi dùng thực tế

  • Đo lúc tải nhẹ: khi ESP32 đang phát Wi-Fi, điện áp pin sụt tạm thời nên phần trăm tính ra thấp hơn thật. Nên đo trước khi bật Wi-Fi hoặc lấy trung bình nhiều lần đo.
  • Ngưỡng cảnh báo: báo pin yếu ở khoảng 3,4–3,5 V và ngừng dùng ở khoảng 3,3 V. Xả pin Li-ion xuống quá thấp làm pin chai nhanh; mạch bảo vệ pin là lớp an toàn cuối cùng, không nên dựa hẳn vào nó.
  • Pin nhiều cell nối tiếp (2S, 3S): điện áp cao hơn nhiều, phải tính lại cầu phân áp sao cho điện áp tại chân ADC không vượt 3,1 V khi pin đầy.
  • Cần chính xác hơn: dùng ADC ngoài như trong bài đo điện áp 16 bit bằng ADS1115 với ESP32.

9. Xử lý lỗi thường gặp

  • Số đo luôn bằng 0 hoặc luôn rất cao: nối sai điểm giữa của cầu phân áp, hoặc GND của ESP32 chưa nối chung với cực âm của pin.
  • Số đo nhảy lung tung: thiếu tụ 100 nF, dây nối dài hoặc lỏng. Thêm tụ và tăng SO_MAU.
  • Số đo lệch đều so với đồng hồ: hiệu chỉnh HE_SO_CHINH theo mục 7; kiểm tra giá trị thật của hai điện trở.
  • Đọc analog được lúc chưa bật Wi-Fi, bật Wi-Fi thì sai: đang dùng chân thuộc ADC2. Chuyển sang một chân trong nhóm GPIO32–39.
  • Lỗi biên dịch "analogReadMilliVolts was not declared": gói esp32 quá cũ. Cập nhật gói esp32 by Espressif Systems trong Boards Manager.
  • Không nạp được chương trình: dùng cáp USB có truyền dữ liệu, chọn đúng cổng COM và giữ nút BOOT khi IDE dừng ở dòng Connecting...

Xem thêm: đo điện áp 16 bit bằng ADS1115 với ESP32, mạch hạ áp LM2596 và MP1584: cách chọn và cấp nguồn cho ESP32 và mô đun pin Lithium-ion 18650 để cấp nguồn di động cho mạch.


Tin tức liên quan

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

3 Lượt xem

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch
Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch

3 Lượt xem

Hướng dẫn ESP32 lấy giờ Việt Nam từ Internet bằng NTP trên Arduino IDE, in ra Serial Monitor và làm đồng hồ hiển thị thứ, ngày, giờ trên màn hình OLED 0.96 inch SSD1306.

Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient

3 Lượt xem

Hướng dẫn dùng MQTT với ESP32 trên Arduino IDE: broker, topic, publish, subscribe, cài Mosquitto và PubSubClient, gửi dữ liệu và điều khiển module relay qua mạng nội bộ.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

3 Lượt xem

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

3 Lượt xem

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

3 Lượt xem

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.

Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn
Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn

4 Lượt xem

Hướng dẫn đưa ESP32 vào Deep Sleep trên Arduino IDE để tiết kiệm pin: các chế độ nguồn, đánh thức bằng timer và nút nhấn, giữ biến trong bộ nhớ RTC và ước tính thời lượng pin.

Hướng dẫn kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng
Hướng dẫn kết nối Wi-Fi trên ESP32: quét mạng, tự kết nối lại, IP tĩnh và phát Wi-Fi riêng

4 Lượt xem

Hướng dẫn các thao tác Wi-Fi cần dùng nhất trên ESP32 với Arduino IDE: quét mạng, kết nối có thời gian chờ, xem IP và cường độ sóng, tự kết nối lại, đặt IP tĩnh, phát Wi-Fi riêng.


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