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

- 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 độ

- 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

Đ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ũ
}
}

- 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ượng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Luôn đọc 0 hoặc giá trị không đổi | DRDY không xuống, SPI sai chế độ, CS sai | Kiể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ộ đệm | Giả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ước | Không SYNC sau khi đổi MUX | Gửi SYNC, WAKEUP như chương trình |
| Số nhảy nhiều ở 6 chữ số cuối | Nhiễu, tốc độ cao, dây dài | Giảm tốc độ, đo vi sai, dây xoắn, lấy trung bình |
| Thẻ SD lỗi khi thêm ADS1256 | Module thẻ SD giữ chân MISO khi CS cao | Dù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.
Xem thêm