Thực hành hiệu chuẩn thước điện trở bằng căn mẫu với ADS1115, ESP32: đo tỉ lệ, độ tuyến tính, độ trễ
Hiệu chuẩn thước điện trở 50 mm bằng chồng căn mẫu: ADS1115 đo tỉ lệ, ESP32 ghi điểm đi lên và đi xuống, khớp đa thức, tính sai số tuyến tính, độ trễ và độ không đảm bảo.
Phần thực hành của nhóm bài chuyển vị: hiệu chuẩn một thước điện trở (biến trở thẳng) hành trình 50 mm bằng chồng căn mẫu trên bàn đá, đọc bằng ADC 16 bit ADS1115 và ESP32. Chương trình trong bài đo theo kiểu tỉ lệ để không phụ thuộc nguồn 3,3 V, ghi các điểm theo chiều cao căn mẫu cả khi đi lên và đi xuống, khớp đa thức bậc 1 và bậc 2 bằng bình phương tối thiểu, tính sai số tuyến tính theo phần trăm toàn thang và lưu hệ số vào bộ nhớ. Cuối bài là cách đọc phần dư để thấy độ cong và độ trễ, và ngân sách độ không đảm bảo của phép đo vị trí sau hiệu chuẩn.
1. Chuẩn bị
- ESP32 DevKit V1 30 chân.
- Mạch chuyển đổi ADC ADS1115 16 bit.
- Thước điện trở (biến trở thẳng có cần đẩy lò xo) hành trình 50 mm.
- Bộ căn mẫu song phẳng (cấp 1 hoặc 2 là đủ), bàn đá hoặc tấm phẳng, giá kẹp thước cứng.
- Dây cắm test board.
Căn mẫu là chuẩn của phép hiệu chuẩn: sai số của căn mẫu cấp 1 cỡ một micromét, nhỏ hơn hàng chục lần so với sai số của thước điện trở, đúng nguyên tắc chuẩn phải tốt hơn nhiều so với thiết bị được hiệu chuẩn.
2. Nối dây và gá

| Từ | Nối tới |
|---|---|
| Thước: đầu 1 | GND |
| Thước: đầu 2 | 3V3, và AIN1 của ADS1115 |
| Thước: con trượt | AIN0 của ADS1115 |
| ADS1115 VDD, GND | ESP32 3V3, GND |
| ADS1115 SCL, SDA | ESP32 GPIO22, GPIO21 |
| ADS1115 ADDR | GND (địa chỉ 0x48) |
Đo tỉ lệ: thước là bộ chia áp, điện áp con trượt tỉ lệ với cả vị trí lẫn điện áp nguồn. Chương trình đo cả con trượt (AIN0) lẫn chính điện áp đặt lên thước (AIN1) rồi lấy tỉ số, nên nguồn 3,3 V dao động không làm sai vị trí.
Gá: kẹp thước vuông góc với bàn đá, cần đẩy thẳng đứng; nếu cần đẩy nghiêng, mỗi chồng căn mẫu sẽ bị đo thành hình chiếu (sai số cosin). Đầu cần đẩy tì vào giữa mặt căn mẫu.
3. Chương trình
Chương trình đọc ADS1115 trực tiếp qua thanh ghi bằng thư viện Wire, lưu hệ số bằng Preferences; cả hai có sẵn trong gói esp32, không cần thư viện ngoài. Lệnh gõ trong Serial Monitor (115200 baud, kiểu xuống dòng Newline):
| Lệnh | Việc làm |
|---|---|
| d20 | Ghi một điểm khi cần đẩy đang tì trên chồng căn mẫu cao 20 mm (32 lần đọc tỉ số) |
| l | Liệt kê các điểm đã ghi |
| x | Xóa các điểm |
| k | Khớp bậc 1, bậc 2, in phần dư, sai số tuyến tính, chọn bậc và lưu |
| m | Bật hoặc tắt chế độ đo vị trí liên tục |
/*
PYWORLD.VN - Thực hành hiệu chuẩn thước điện trở bằng căn mẫu: ADS1115 đo tỉ lệ, ESP32 khớp đa thức và tính độ tuyến tính
Thước: một đầu GND, đầu kia 3V3, con trượt vào AIN0; 3V3 nối thêm AIN1. ADS1115: SDA GPIO21, SCL GPIO22, ADDR nối GND
Lệnh Serial: d<mm> ghi điểm theo chiều cao căn mẫu, l liệt kê, x xóa điểm, k khớp và lưu, m bật/tắt đo
*/
#include <Wire.h>
#include <Preferences.h>
#define CHAN_SDA 21
#define CHAN_SCL 22
#define SO_DIEM_TOI_DA 12
#define SO_MAU_DIEM 32 // Số lần đọc tỉ số cho mỗi điểm hiệu chuẩn
const uint8_t DIA_CHI_ADS = 0x48;
const uint8_t THANH_GHI_KET_QUA = 0x00;
const uint8_t THANH_GHI_CAU_HINH = 0x01;
// Đo một lần, dải ±4,096 V, 128 mẫu/s, tắt bộ so sánh: AIN0 so với GND (con trượt) và AIN1 so với GND (nguồn)
const uint16_t CAU_HINH_AIN0 = 0xC383;
const uint16_t CAU_HINH_AIN1 = 0xD383;
Preferences boNho;
float viTriDiem[SO_DIEM_TOI_DA]; // Chiều cao căn mẫu của từng điểm, mm
double tiSoDiem[SO_DIEM_TOI_DA]; // Tỉ số Vcon trượt / Vnguồn trung bình
double lechChuanDiem[SO_DIEM_TOI_DA];
int soDiem = 0;
double heSo[3] = {0.0, 50.0, 0.0}; // mm = a0 + a1*r + a2*r*r; giá trị đầu gần đúng cho thước 50 mm
int bacDaThuc = 1;
bool dangDo = false;
int16_t docAds(uint16_t cauHinh) {
Wire.beginTransmission(DIA_CHI_ADS);
Wire.write(THANH_GHI_CAU_HINH);
Wire.write(cauHinh >> 8);
Wire.write(cauHinh & 0xFF);
Wire.endTransmission();
delay(9); // Một lần chuyển đổi ở 128 mẫu/s mất khoảng 7,8 ms
Wire.beginTransmission(DIA_CHI_ADS);
Wire.write(THANH_GHI_KET_QUA);
Wire.endTransmission();
if (Wire.requestFrom(DIA_CHI_ADS, (uint8_t)2) != 2) {
return 0;
}
uint8_t cao = Wire.read();
uint8_t thap = Wire.read();
return (int16_t)((cao << 8) | thap);
}
// Tỉ số con trượt / nguồn: nguồn 3V3 dao động thì cả hai cùng đổi, tỉ số giữ nguyên
bool docTiSo(double *tiSo) {
int16_t conTruot = docAds(CAU_HINH_AIN0);
int16_t nguon = docAds(CAU_HINH_AIN1);
if (nguon < 1000) {
return false; // Không có nguồn ở AIN1 hoặc mất kết nối I2C
}
*tiSo = (double)conTruot / nguon;
return true;
}
bool docNhieuMau(int n, double *trungBinh, double *lechChuan) {
double tong = 0, tongBinhPhuong = 0;
for (int i = 0; i < n; i++) {
double r;
if (!docTiSo(&r)) {
return false;
}
tong += r;
tongBinhPhuong += r * r;
}
*trungBinh = tong / n;
double phuongSai = (tongBinhPhuong - tong * tong / n) / (n - 1);
*lechChuan = phuongSai > 0 ? sqrt(phuongSai) : 0;
return true;
}
// Khử Gauss có chọn phần tử trụ cho hệ n ẩn (n ≤ 3)
bool giaiHe(double a[3][4], int n, double nghiem[3]) {
for (int cot = 0; cot < n; cot++) {
int hangTru = cot;
for (int h = cot + 1; h < n; h++) {
if (fabs(a[h][cot]) > fabs(a[hangTru][cot])) {
hangTru = h;
}
}
if (fabs(a[hangTru][cot]) < 1e-12) {
return false;
}
for (int k = 0; k <= n; k++) {
double tam = a[cot][k];
a[cot][k] = a[hangTru][k];
a[hangTru][k] = tam;
}
for (int h = 0; h < n; h++) {
if (h != cot) {
double f = a[h][cot] / a[cot][cot];
for (int k = cot; k <= n; k++) {
a[h][k] -= f * a[cot][k];
}
}
}
}
for (int i = 0; i < n; i++) {
nghiem[i] = a[i][n] / a[i][i];
}
return true;
}
// Bình phương tối thiểu: vị trí theo tỉ số, đa thức bậc 1 hoặc 2
bool khopDaThuc(int bac, double c[3]) {
int n = bac + 1;
double a[3][4] = {{0}};
for (int i = 0; i < soDiem; i++) {
double r = tiSoDiem[i];
double luyThua[5] = {1, r, r * r, r * r * r, r * r * r * r};
for (int h = 0; h < n; h++) {
for (int k = 0; k < n; k++) {
a[h][k] += luyThua[h + k];
}
a[h][n] += luyThua[h] * viTriDiem[i];
}
}
c[2] = 0;
return giaiHe(a, n, c);
}
double tinhViTri(double r, const double c[3]) {
return c[0] + c[1] * r + c[2] * r * r;
}
void luuHeSo() {
boNho.begin("thuocdientro", false);
boNho.putBytes("heso", heSo, sizeof(heSo));
boNho.putInt("bac", bacDaThuc);
boNho.end();
}
void napHeSo() {
boNho.begin("thuocdientro", true);
if (boNho.getBytesLength("heso") == sizeof(heSo)) {
boNho.getBytes("heso", heSo, sizeof(heSo));
bacDaThuc = boNho.getInt("bac", 1);
Serial.println("Da nap he so hieu chuan da luu");
} else {
Serial.println("Chua co he so luu: can hieu chuan bang can mau");
}
boNho.end();
}
void lenhGhiDiem(float mm) {
if (soDiem >= SO_DIEM_TOI_DA) {
Serial.println("Da du so diem toi da");
return;
}
double tb, s;
if (!docNhieuMau(SO_MAU_DIEM, &tb, &s)) {
Serial.println("Khong doc duoc ADS1115 hoac thieu nguon o AIN1");
return;
}
viTriDiem[soDiem] = mm;
tiSoDiem[soDiem] = tb;
lechChuanDiem[soDiem] = s;
soDiem++;
Serial.printf("Diem %d: %.3f mm -> ti so %.6f (s = %.6f)\n", soDiem, mm, tb, s);
}
void lenhLietKe() {
for (int i = 0; i < soDiem; i++) {
Serial.printf("Diem %d: %.3f mm -> ti so %.6f (s = %.6f)\n", i + 1, viTriDiem[i], tiSoDiem[i], lechChuanDiem[i]);
}
Serial.printf("Tong %d diem\n", soDiem);
}
void lenhKhop() {
if (soDiem < 3) {
Serial.println("Can it nhat 3 diem, nen phu ca hanh trinh");
return;
}
double c1[3], c2[3];
if (!khopDaThuc(1, c1)) {
Serial.println("Khong khop duoc: kiem tra cac diem");
return;
}
bool coBac2 = soDiem >= 4 && khopDaThuc(2, c2);
double tong1 = 0, tong2 = 0, nhoNhat = viTriDiem[0], lonNhat = viTriDiem[0];
Serial.println("Diem | Can mau (mm) | Ti so | Du bac 1 (mm) | Du bac 2 (mm)");
for (int i = 0; i < soDiem; i++) {
double du1 = viTriDiem[i] - tinhViTri(tiSoDiem[i], c1);
double du2 = coBac2 ? viTriDiem[i] - tinhViTri(tiSoDiem[i], c2) : 0;
tong1 += du1 * du1;
tong2 += du2 * du2;
if (viTriDiem[i] < nhoNhat) {
nhoNhat = viTriDiem[i];
}
if (viTriDiem[i] > lonNhat) {
lonNhat = viTriDiem[i];
}
Serial.printf("%4d | %12.3f | %8.6f | %13.4f | %13.4f\n", i + 1, viTriDiem[i], tiSoDiem[i], du1, du2);
}
double s1 = sqrt(tong1 / (soDiem - 2));
double s2 = coBac2 ? sqrt(tong2 / (soDiem - 3)) : 0;
Serial.printf("Do lech chuan phan du: bac 1 = %.4f mm, bac 2 = %.4f mm\n", s1, s2);
// Bậc 2 chỉ dùng khi giảm phần dư rõ rệt, tránh khớp theo nhiễu và độ trễ
bacDaThuc = (coBac2 && s2 < 0.5 * s1) ? 2 : 1;
double duLonNhat = 0;
for (int k = 0; k < 3; k++) {
heSo[k] = bacDaThuc == 2 ? c2[k] : c1[k];
}
for (int i = 0; i < soDiem; i++) {
double du = fabs(viTriDiem[i] - tinhViTri(tiSoDiem[i], heSo));
if (du > duLonNhat) {
duLonNhat = du;
}
}
Serial.printf("Sai so tuyen tinh lon nhat: %.3f %% toan thang (%.2f mm)\n", duLonNhat / (lonNhat - nhoNhat) * 100, lonNhat - nhoNhat);
luuHeSo();
Serial.printf("Chon bac %d: a0 = %.4f mm, a1 = %.4f mm, a2 = %.4f mm (da luu)\n", bacDaThuc, heSo[0], heSo[1], heSo[2]);
}
void inHuongDan() {
Serial.println("Lenh: d<mm> ghi diem | l liet ke | x xoa diem | k khop va luu | m bat/tat do");
}
void xuLyLenh() {
if (!Serial.available()) {
return;
}
String lenh = Serial.readStringUntil('\n');
lenh.trim();
if (lenh.length() == 0) {
return;
}
char ma = lenh.charAt(0);
String thamSo = lenh.substring(1);
thamSo.trim();
if (ma == 'd') {
lenhGhiDiem(thamSo.toFloat());
} else if (ma == 'l') {
lenhLietKe();
} else if (ma == 'x') {
soDiem = 0;
Serial.println("Da xoa cac diem");
} else if (ma == 'k') {
lenhKhop();
} else if (ma == 'm') {
dangDo = !dangDo;
Serial.println(dangDo ? "Bat che do do" : "Tat che do do");
} else {
inHuongDan();
}
}
void setup() {
Serial.begin(115200);
Wire.begin(CHAN_SDA, CHAN_SCL, 400000);
delay(200);
Serial.println();
Wire.beginTransmission(DIA_CHI_ADS);
if (Wire.endTransmission() != 0) {
Serial.println("Khong thay ADS1115 o dia chi 0x48: kiem tra SDA, SCL va chan ADDR");
}
napHeSo();
inHuongDan();
}
void loop() {
xuLyLenh();
if (!dangDo) {
return;
}
double tb, s;
if (!docNhieuMau(8, &tb, &s)) {
Serial.println("Khong doc duoc ADS1115 hoac thieu nguon o AIN1");
delay(1000);
return;
}
Serial.printf("Ti so %.6f | Vi tri %.3f mm\n", tb, tinhViTri(tb, heSo));
delay(200);
}
- Thanh ghi cấu hình 0xC383 và 0xD383: đo một lần, AIN0 hoặc AIN1 so với GND, dải ±4,096 V, 128 mẫu/giây. Mỗi lần chuyển đổi khoảng 7,8 ms nên chương trình chờ 9 ms rồi đọc.
- Khớp vị trí theo tỉ số: vị trí = a₀ + a₁·r + a₂·r². Tỉ số nằm trong khoảng 0–1 nên hệ phương trình không bị lệch thang.
- Sai số tuyến tính in theo phần trăm của khoảng đã hiệu chuẩn (từ điểm thấp nhất tới cao nhất), cùng cách nhà sản xuất công bố độ tuyến tính của thước.
4. Trình tự hiệu chuẩn

- Gá và ổn định: để căn mẫu, thước và giá cân bằng nhiệt với phòng; dùng găng hoặc kẹp khi cầm căn mẫu.
- Điểm 0: cần đẩy tì thẳng lên bàn đá, gõ
d0. Mọi chiều cao căn mẫu tính từ mặt bàn này. - Đi lên: ghép chồng căn mẫu 10, 20, 30, 40, 50 mm (dùng ít miếng nhất), nâng cần đẩy, đặt chồng vào, hạ cần đẩy nhẹ nhàng, chờ vài giây, gõ
dkèm chiều cao. - Đi xuống: lặp lại 40, 30, 20, 10, 0 mm. So sánh hai chiều cho biết độ trễ của thước.
- Khớp và lưu: gõ
k. - Kiểm tra: gõ
m, đặt chồng 25 mm (20 + 5 mm) không dùng khi khớp, so số đọc với 25,000 mm.
5. Kết quả mong đợi


- Phần dư bậc 1 có dạng vòm: thước không hoàn toàn thẳng, hai đầu và giữa lệch khoảng 0,04–0,06 mm. Khớp bậc 2 giảm độ lệch chuẩn phần dư từ 0,035 mm xuống 0,0065 mm, nên chương trình chọn bậc 2.
- Sau khi khớp bậc 2, các điểm đi lên lệch dương nhẹ, đi xuống lệch âm nhẹ: đó là độ trễ khoảng 0,013 mm, do ma sát con trượt và khe hở cơ khí. Bậc đa thức nào cũng không bỏ được trễ.
- Sai số tuyến tính sau hiệu chính chỉ khoảng 0,016 % toàn thang; chồng 25 mm đọc được 24,995 mm.
6. Độ không đảm bảo sau hiệu chuẩn

Độ không đảm bảo mở rộng khoảng 0,02 mm trên hành trình 50 mm. Căn mẫu chỉ góp phần không đáng kể; các nguồn chính là phần dư còn lại, nhiệt độ, độ trễ và tiếp xúc của con trượt. Thước điện trở dùng một chiều (ví dụ chỉ đo khi đẩy vào) và trong phòng ổn định nhiệt sẽ có độ không đảm bảo nhỏ hơn. Cũng nên nhớ: thước điện trở mòn dần ở đoạn hay dùng, nên hiệu chuẩn lại định kỳ và sau khi thấy số đọc nhảy.
7. Xử lý lỗi
| Hiện tượng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Khong thay ADS1115 o dia chi 0x48 | Sai SDA, SCL, ADDR chưa nối GND | Kiểm tra dây GPIO21, GPIO22 và chân ADDR |
| Khong doc duoc ADS1115 hoac thieu nguon o AIN1 | Chưa nối đầu 3V3 của thước vào AIN1 | Nối AIN1 với đầu 3V3 của thước |
| Tỉ số giảm khi cần đẩy đi lên | Đảo hai đầu thước | Đổi chỗ hai đầu, hoặc để nguyên vì phép khớp vẫn đúng |
| s của mỗi điểm lớn, số nhảy | Con trượt bẩn, mòn, dây lỏng | Vệ sinh, dây ngắn, thay thước nếu mòn |
| Phần dư lớn ở một điểm | Nhập sai chiều cao, bụi dưới căn mẫu | Lau căn mẫu và bàn đá, ghi lại điểm đó |
8. Xem thêm
- Cảm biến chuyển vị: biến trở, LVDT, điện dung, dòng xoáy, laser – cấu tạo, nguyên lý và chế tạo.
- Hướng dẫn đo điện áp 16 bit bằng module ADS1115 với ESP32.
- Các bài khác trong loạt: chuẩn chiều dài và căn mẫu ISO 3650, sai số Abbe, cosin và giãn nở nhiệt.
Xem thêm