ADS1256 với ESP32: tự làm data logger nhiều kênh ADC 24 bit, lệnh SPI tự viết, ghi CSV ra thẻ SD

Tự làm data logger nhiều kênh với ESP32 và ADC 24 bit ADS1256: nối dây chung SPI với thẻ SD, chọn PGA và tốc độ, lệnh SPI tự viết theo datasheet, quét kênh đơn và vi sai, ghi CSV.

ADC 12 bit của ESP32 đủ cho đo thô nhưng không đủ cho cầu điện trở, load cell, cảm biến mV hay nhiều kênh cần chính xác. ADS1256 của Texas Instruments là ADC 24 bit, 8 kênh đơn hoặc 4 kênh vi sai, có bộ khuếch đại PGA tới 64 lần, tốc độ từ 2,5 tới 30 000 mẫu/giây, module giá rẻ có sẵn. Bài này hướng dẫn làm một data logger nhiều kênh với ADS1256 và ESP32: nối dây, chọn PGA và tốc độ, các lệnh SPI tự viết theo datasheet (không cần thư viện), quét nhiều kênh, ghi CSV ra thẻ SD.

Chọn data logger, tính pin, bộ nhớ, chống sét có ở bài cùng cụm về data logger là gì. Bài này đi vào phần làm.

1. Nối ADS1256 và thẻ SD với ESP32

Sơ đồ nối module ADS1256 và module thẻ Micro SD với ESP32: chung SCLK D18, DOUT MISO D19, DIN MOSI D23; ADS1256 CS D25, DRDY D26; thẻ SD CS D5; nguồn 5 V từ VIN
Chung bus SPI, mỗi module một chân CS.
  • Module ADS1256 phổ biến (mạch xanh, 8 đầu vào AIN0–AIN7, AINCOM) cấp 5 V. Trước khi nối, kiểm tra chân DOUT ra mức 3,3 V (đo bằng đồng hồ khi chạy); module xuất 5 V thì cần mạch chuyển mức.
  • Chân DRDY báo có dữ liệu mới (xuống thấp); chương trình chờ chân này trước mỗi lần đọc.
  • Thẻ SD và ADS1256 dùng chung SCK, MISO, MOSI. ADS1256 dùng SPI chế độ 1, thẻ SD chế độ 0: chương trình đặt chế độ riêng cho mỗi lần giao tiếp (SPISettings), không lẫn nhau.

Cách dùng module thẻ nhớ, lỗi Card Mount Failed có ở bài dùng thẻ Micro SD với ESP32.

2. Chọn hệ số khuếch đại và tốc độ

Bảng PGA và tốc độ ADS1256: PGA x1 thang ±5 V LSB 0,596 µV, x8 ±0,625 V, x64 ±0,078 V LSB 0,009 µV; tốc độ 30000 SPS quét 5 kênh 8 mẫu mất 5 ms, 1000 SPS 44 ms, 100 SPS 404 ms, 10 SPS 4 giây
Bước 1 mã rất nhỏ, nhiễu thật lớn hơn nhiều.
  • PGA khuếch đại tín hiệu nhỏ trước khi chuyển đổi: cảm biến 0–5 V dùng x1; cầu điện trở, load cell, cặp nhiệt chỉ vài mV dùng x32–x64.
  • Bộ đệm đầu vào (bật trong chương trình) cho trở kháng vào rất cao, hợp với nguồn tín hiệu yếu, nhưng giới hạn điện áp vào khoảng 0 tới (AVDD − 2 V), tức 0–3 V với nguồn 5 V. Cần đo tới 5 V thì tắt bộ đệm.
  • Tốc độ càng thấp thì bộ lọc số càng mạnh, nhiễu càng nhỏ. Ở 10, 50 SPS bộ lọc có điểm không tại 50 Hz, chặn tốt nhiễu điện lưới (xem bài cùng cụm về nhiễu 50 Hz). Quét nhiều kênh thì mỗi lần đổi kênh phải chờ bộ lọc ổn định khoảng một chu kỳ, nên thời gian quét tăng theo số kênh.

Script dưới đây đổi mã 24 bit sang volt (cùng công thức với chương trình), tính bước 1 mã theo PGA, mã thanh ghi MUX, DRATE và thời gian quét:

# PYWORLD.VN - Tính nhanh cho ADS1256: đổi mã 24 bit sang volt theo PGA, bước nhỏ nhất (LSB), mã thanh ghi MUX, DRATE và thời gian quét nhiều kênh
# PYWORLD.VN - Cùng công thức với hàm sangVon() trong sketch ESP32_ADS1256_LOGGER; dùng để kiểm tra số đọc hoặc chọn tốc độ, hệ số khuếch đại
# PYWORLD.VN - Chạy: python ads1256_tinh.py
VREF = 2.5
DRATE = {30000: 0xF0, 15000: 0xE0, 7500: 0xD0, 3750: 0xC0, 2000: 0xB0, 1000: 0xA1, 500: 0x92, 100: 0x82,
         60: 0x72, 50: 0x63, 30: 0x53, 25: 0x43, 15: 0x33, 10: 0x23, 5: 0x13, 2.5: 0x03}     # bảng DRATE trong datasheet


def sang_von(ma: int, pga: int = 1) -> float:
    if ma & 0x800000:
        ma -= 0x1000000
    return ma * (2 * VREF / pga) / 2 ** 23


def lsb_uv(pga: int = 1) -> float:
    return 2 * VREF / pga / 2 ** 23 * 1e6


def mux(ain_duong: int, ain_am: int | None = None) -> int:
    """ain_am = None: đơn cực so với AINCOM (mã 8)."""
    return (ain_duong << 4) | (8 if ain_am is None else ain_am)


def thoi_gian_quet_ms(so_kenh: int, so_mau: int, toc_do: float, them_ms: float = 0.1) -> float:
    """Mỗi lần đổi kênh phải chờ bộ lọc ổn định khoảng 1/tốc độ; cộng thời gian SPI."""
    return so_kenh * so_mau * (1000 / toc_do + them_ms)


if __name__ == "__main__":
    for ma in (0x7FFFFF, 0x400000, 0x000001, 0xFFFFFF, 0x800000):
        print(f"ma 0x{ma:06X} -> {sang_von(ma):+.7f} V (PGA 1)")
    for pga in (1, 8, 64):
        print(f"PGA {pga:2d}: thang +/-{2 * VREF / pga:.4f} V, LSB {lsb_uv(pga):.4f} uV")
    print("MUX AIN0 don:", hex(mux(0)), "| AIN6-AIN7 vi sai:", hex(mux(6, 7)), "| DRATE 1000 SPS:", hex(DRATE[1000]))
    for toc_do in (30000, 1000, 100, 10):
        print(f"5 kenh x 8 mau o {toc_do} SPS: {thoi_gian_quet_ms(5, 8, toc_do):.0f} ms moi lan quet")

3. Các lệnh SPI

Bảng trình tự lệnh SPI ADS1256: RESET FE và SDATAC 0F khi khởi động, ghi STATUS ADCON DRATE, SELFCAL F0, ghi MUX chọn kênh, SYNC FC và WAKEUP 00 bắt đầu đo kênh mới, RDATA 01 chờ 7 µs đọc 3 byte
Không cần thư viện: vài lệnh là đủ.

Điểm quan trọng khi quét nhiều kênh: sau khi đổi MUX phải gửi SYNC và WAKEUP để bộ lọc số bắt đầu lại, nếu không kết quả đầu tiên lẫn dữ liệu của kênh trước. Lệnh RDATA phải chờ khoảng 50 chu kỳ thạch anh (gần 7 µs với 7,68 MHz) rồi mới đọc 3 byte. Kết quả là số bù 2: mã từ 0x800000 tới 0x7FFFFF ứng với −2·Vref/PGA tới gần +2·Vref/PGA.

4. Chương trình data logger

Chương trình quét 4 kênh đơn cực AIN0–AIN3 (so với AINCOM) và 1 kênh vi sai AIN6–AIN7, mỗi kênh lấy trung bình 8 lần đọc, mỗi giây ghi một dòng CSV (giây, các điện áp) ra thẻ SD và Serial. Đổi kênh, PGA, tốc độ ở đầu chương trình.

// PYWORLD.VN - Data logger nhiều kênh: ESP32 đọc ADC 24 bit ADS1256 qua SPI (tự viết lệnh theo datasheet, không cần thư viện), ghi CSV ra thẻ SD
// PYWORLD.VN - Quét 4 kênh đơn (AIN0-AIN3 so với AINCOM) và 1 kênh vi sai (AIN6-AIN7), lấy trung bình, mỗi giây ghi một dòng có thời gian
// PYWORLD.VN - Chung bus SPI: SCK 18, MISO 19, MOSI 23; ADS1256 CS 25, DRDY 26; thẻ SD CS 5. Vref 2,5 V, thạch anh 7,68 MHz

#include <SPI.h>
#include <SD.h>

const int ADS_CS = 25, ADS_DRDY = 26, SD_CS = 5;
const float VREF = 2.5;
const uint8_t PGA_BIT = 0;                       // 0 = x1 (±5 V), 1 = x2, ... 6 = x64
const uint8_t DRATE_1000SPS = 0xA1;              // bảng DRATE trong datasheet
const int SO_MAU_TB = 8;                         // trung bình 8 lần đọc mỗi kênh
const uint8_t KENH[] = {0x08, 0x18, 0x28, 0x38, 0x67};   // MUX: AINP ở 4 bit cao, AINN ở 4 bit thấp (8 = AINCOM)
const char *TEN_KENH[] = {"ain0", "ain1", "ain2", "ain3", "ain6_7"};
const int SO_KENH = sizeof(KENH);

SPISettings caiDatAds(1000000, MSBFIRST, SPI_MODE1);     // ADS1256: SPI chế độ 1, tối đa fCLKIN/4
bool coThe = false;
uint32_t lucGhi = 0;

void choDrdy() {
  uint32_t t0 = micros();
  while (digitalRead(ADS_DRDY) == HIGH && micros() - t0 < 200000) {}
}

void lenh(uint8_t c) {
  SPI.beginTransaction(caiDatAds);
  digitalWrite(ADS_CS, LOW);
  SPI.transfer(c);
  digitalWrite(ADS_CS, HIGH);
  SPI.endTransaction();
}

void ghiThanhGhi(uint8_t dc, uint8_t giaTri) {
  SPI.beginTransaction(caiDatAds);
  digitalWrite(ADS_CS, LOW);
  SPI.transfer(0x50 | dc);                       // WREG
  SPI.transfer(0x00);                            // ghi 1 thanh ghi
  SPI.transfer(giaTri);
  digitalWrite(ADS_CS, HIGH);
  SPI.endTransaction();
}

// Đổi kênh rồi đọc một lần chuyển đổi đã ổn định của kênh mới
int32_t docKenh(uint8_t mux) {
  choDrdy();
  ghiThanhGhi(0x01, mux);                        // thanh ghi MUX
  lenh(0xFC);                                    // SYNC: bắt đầu lại bộ lọc với kênh mới
  delayMicroseconds(5);
  lenh(0x00);                                    // WAKEUP
  choDrdy();
  SPI.beginTransaction(caiDatAds);
  digitalWrite(ADS_CS, LOW);
  SPI.transfer(0x01);                            // RDATA
  delayMicroseconds(7);                          // t6 = 50 chu kỳ thạch anh 7,68 MHz
  int32_t b2 = SPI.transfer(0);                  // đọc lần lượt: byte cao trước
  int32_t b1 = SPI.transfer(0);
  int32_t b0 = SPI.transfer(0);
  int32_t v = (b2 << 16) | (b1 << 8) | b0;
  digitalWrite(ADS_CS, HIGH);
  SPI.endTransaction();
  if (v & 0x800000) v -= 0x1000000;              // số bù 2, 24 bit
  return v;
}

double sangVon(int32_t ma) {
  return ma * (2.0 * VREF / (1 << PGA_BIT)) / 8388608.0;   // 2^23
}

void setup() {
  Serial.begin(115200);
  pinMode(ADS_CS, OUTPUT);
  digitalWrite(ADS_CS, HIGH);
  pinMode(ADS_DRDY, INPUT);
  SPI.begin(18, 19, 23);
  lenh(0xFE);                                    // RESET
  delay(10);
  lenh(0x0F);                                    // SDATAC: dừng chế độ đọc liên tục để ghi thanh ghi
  ghiThanhGhi(0x00, 0x06);                       // STATUS: bật bộ đệm đầu vào, tự hiệu chuẩn
  ghiThanhGhi(0x02, PGA_BIT);                    // ADCON: PGA
  ghiThanhGhi(0x03, DRATE_1000SPS);
  lenh(0xF0);                                    // SELFCAL
  choDrdy();
  coThe = SD.begin(SD_CS, SPI, 4000000);
  if (coThe && !SD.exists("/adc_log.csv")) {
    File f = SD.open("/adc_log.csv", FILE_WRITE);
    f.print("giay");
    for (int k = 0; k < SO_KENH; k++) f.printf(",%s_V", TEN_KENH[k]);
    f.println();
    f.close();
  }
  Serial.println(coThe ? "The SD OK, ghi /adc_log.csv" : "Khong co the SD: chi in ra Serial");
}

void loop() {
  if (millis() - lucGhi < 1000) return;
  lucGhi = millis();
  double ketQua[8];
  for (int k = 0; k < SO_KENH; k++) {
    int64_t tong = 0;
    for (int i = 0; i < SO_MAU_TB; i++) tong += docKenh(KENH[k]);
    ketQua[k] = sangVon((int32_t)(tong / SO_MAU_TB));
  }
  char dong[160];
  int n = snprintf(dong, sizeof(dong), "%lu", (unsigned long)(millis() / 1000));
  for (int k = 0; k < SO_KENH; k++) n += snprintf(dong + n, sizeof(dong) - n, ",%.6f", ketQua[k]);
  Serial.println(dong);
  if (coThe) {
    File f = SD.open("/adc_log.csv", FILE_APPEND);
    if (f) { f.println(dong); f.close(); }        // đóng file sau mỗi dòng: mất điện không mất dữ liệu cũ
  }
}
Dữ liệu CSV ghi ra từ data logger ADS1256: cột giây, ain0 đến ain3 và kênh vi sai ain6_7 với 6 chữ số thập phân
Ví dụ minh họa.
  • File đóng lại sau mỗi dòng để mất điện không hỏng dữ liệu cũ. Cần thời gian thực thì thêm module đồng hồ RTC và ghi ngày giờ thay cho số giây.
  • Tín hiệu mV (cầu điện trở) nên nối vi sai giữa hai chân AIN, dây xoắn đôi, và dùng tốc độ thấp. Chi tiết về hiệu chuẩn cảm biến có ở bài cùng cụm về hiệu chuẩn bằng hồi quy.

5. Lỗi hay gặp

Hiện tượngNguyên nhânCách xử lý
Luôn đọc 0 hoặc giá trị không đổiDRDY không xuống, SPI sai chế độ, CS saiKiểm tra dây DRDY, CS; SPI chế độ 1
Đọc ra số bão hòa (gần ±5 V)Điện áp vào vượt thang của PGA, hoặc vượt giới hạn bộ đệmGiảm PGA; tắt bộ đệm nếu đo trên 3 V
Kênh này lẫn số của kênh trướcKhông SYNC sau khi đổi MUXGửi SYNC, WAKEUP như chương trình
Số nhảy nhiều ở 6 chữ số cuốiNhiễu, tốc độ cao, dây dàiGiảm tốc độ, đo vi sai, dây xoắn, lấy trung bình
Thẻ SD lỗi khi thêm ADS1256Module thẻ SD giữ chân MISO khi CS caoDùng module thẻ SD loại khác, hoặc bus SPI riêng

Mạch dùng trong bài: ESP32 DevKit V1 30 chân, module thẻ Micro SD. Cần ADC 16 bit đơn giản hơn (I2C, 4 kênh) thì dùng ADS1115, xem bài đo điện áp 16 bit bằng ADS1115.

Bài cùng cụm chủ đề: data logger là gì; nhiễu 50 Hz; hiệu chuẩn cảm biến bằng hồi quy.


Tin tức liên quan

Chuông cửa ESP32-CAM gửi ảnh qua Telegram: bấm chuông là chụp, đèn flash ban đêm, lưu thẻ nhớ
Chuông cửa ESP32-CAM gửi ảnh qua Telegram: bấm chuông là chụp, đèn flash ban đêm, lưu thẻ nhớ

Làm chuông cửa bằng ESP32-CAM: bấm nút thì chụp ảnh gửi về điện thoại qua Telegram kèm giờ, tự bật đèn flash ban đêm, lưu ảnh vào thẻ nhớ, nhắn /anh để xem cửa bất cứ lúc nào.

Hướng dẫn dùng PWM trên ESP32: điều chỉnh độ sáng LED bằng analogWrite và LEDC
Hướng dẫn dùng PWM trên ESP32: điều chỉnh độ sáng LED bằng analogWrite và LEDC

Hướng dẫn tạo PWM trên ESP32 bằng Arduino IDE: analogWrite, ledcAttach và ledcWrite của gói esp32 3.x, chỉnh độ sáng LED bằng biến trở, chọn tần số và độ phân giải.

Cân IoT theo dõi bình gas bằng ESP32 và HX711: còn bao nhiêu kg, ước tính ngày hết, báo qua Telegram
Cân IoT theo dõi bình gas bằng ESP32 và HX711: còn bao nhiêu kg, ước tính ngày hết, báo qua Telegram

Làm cân IoT đặt dưới bình gas: ESP32 đọc load cell qua HX711, trừ vỏ bình ra số kg gas còn lại, hồi quy tính mức dùng mỗi ngày, ước tính ngày hết và nhắn Telegram khi sắp hết.

ESP32 gửi cảnh báo Telegram, nhận lệnh bật tắt relay: tạo bot, lấy chat id, không cần thư viện bot
ESP32 gửi cảnh báo Telegram, nhận lệnh bật tắt relay: tạo bot, lấy chat id, không cần thư viện bot

Tạo bot Telegram, lấy token và chat id, cho ESP32 gửi cảnh báo cửa mở, báo có điện lại và nhận lệnh /bat, /tat relay bằng HTTPClient và ArduinoJson.

Hướng dẫn đo điện áp 16 bit bằng module ADS1115 với ESP32
Hướng dẫn đo điện áp 16 bit bằng module ADS1115 với ESP32

Hướng dẫn nối module ADC 16 bit ADS1115 với ESP32 DevKit V1 qua I2C, cài thư viện Adafruit ADS1X15, đọc 4 kênh ra vôn, đo vi sai và chọn gain an toàn.

ESP32 đọc MPU-6050 tính góc nghiêng bằng bộ lọc bù: nền tảng để flycam tự cân bằng
ESP32 đọc MPU-6050 tính góc nghiêng bằng bộ lọc bù: nền tảng để flycam tự cân bằng

Nối MPU-6050 với ESP32, đọc thanh ghi không cần thư viện, tính góc roll, pitch từ gia tốc kế, ghép với con quay bằng bộ lọc bù và kiểm tra dấu trước khi dùng cho flycam.

Xe tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 trên servo quét, tự chọn đường
Xe tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 trên servo quét, tự chọn đường

Làm xe robot tự tránh vật cản với ESP32: cảm biến siêu âm HC-SR04 gắn trên servo quét trái phải, cầu phân áp cho chân ECHO, thuật toán chọn đường, lưu đồ và thí nghiệm STEM.

LED ma trận MAX7219 và LED 7 đoạn TM1637 với ESP32: chữ chạy 32 × 8 và đồng hồ, không cần thư viện
LED ma trận MAX7219 và LED 7 đoạn TM1637 với ESP32: chữ chạy 32 × 8 và đồng hồ, không cần thư viện

So sánh LED ma trận MAX7219 và LED 7 đoạn TM1637, nối với ESP32 an toàn mức điện áp, giải thích khung 16 bit và ba lệnh TM1637, hai chương trình chữ chạy và đồng hồ không cần thư viện.


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