Encoder tương đối: cấu tạo, tín hiệu A B Z, cách chế tạo và đếm xung bằng PCNT của ESP32
Encoder tương đối hoạt động thế nào: đĩa mã, mặt nạ, kênh A B lệch 90°, vạch Z, đếm x4, ngõ ra NPN, HTL, RS-422, cách chế tạo đĩa mã và đếm encoder bằng PCNT của ESP32.
Encoder là cảm biến biến chuyển động quay (hoặc thẳng) thành xung điện. Mỗi khi trục quay qua một góc nhỏ, encoder phát ra một xung; đếm xung là biết trục đã quay bao nhiêu, đếm trong một khoảng thời gian là biết tốc độ. Encoder tương đối (incremental) chỉ báo phần thay đổi so với lúc bắt đầu đếm, khác với encoder tuyệt đối luôn báo đúng vị trí ngay khi cấp nguồn (bài tiếp theo của loạt). Bài viết giải thích cấu tạo encoder quang, hai kênh A, B lệch pha 90°, vạch Z, các kiểu ngõ ra, cách chế tạo đĩa mã và cách đếm xung encoder bằng bộ đếm phần cứng PCNT của ESP32.
1. Cấu tạo encoder quang

- Đĩa mã: gắn trên trục, có một vòng rãnh trong suốt và vạch tối xen kẽ đều nhau. Số cặp rãnh và vạch trên một vòng là độ phân giải, ghi là P/R hay PPR (xung mỗi vòng), ví dụ 100, 360, 600, 1024, 2500.
- Nguồn sáng và đi-ốt quang: LED hồng ngoại ở một bên, các đi-ốt quang ở bên kia. Rãnh đi qua thì đi-ốt nhận sáng, vạch đi qua thì bị che.
- Mặt nạ: tấm đứng yên có rãnh cùng bước với đĩa, đặt ngay trước đi-ốt quang để chỉ cho ánh sáng đi qua một vùng hẹp, giúp xung sắc nét. Hai cửa sổ A và B được đặt lệch nhau một phần tư bước.
- Vạch Z (index): một rãnh riêng ở vòng trong, cho đúng một xung mỗi vòng, dùng làm điểm gốc.
- Mạch so sánh: biến tín hiệu gần hình sin từ đi-ốt quang thành xung vuông sạch, rồi đưa ra tầng ngõ ra.

2. Hai kênh A, B lệch pha 90° và cách đếm x4

Nếu chỉ có một kênh A, ta biết trục đã quay bao nhiêu nhưng không biết quay chiều nào. Kênh B có cùng số xung nhưng lệch một phần tư chu kỳ (90°). Hai tín hiệu này gọi là tín hiệu cầu phương:
- Quay thuận: A đổi trạng thái trước B, thứ tự trạng thái (A, B) là 00 → 10 → 11 → 01 → 00.
- Quay nghịch: B đổi trước, thứ tự đảo lại 00 → 01 → 11 → 10 → 00.
- Đếm x1: chỉ đếm sườn lên của A. Đếm x2: cả sườn lên và xuống của A. Đếm x4: mọi sườn của cả A và B.
- Encoder 600 xung/vòng đếm x4 cho 2400 số đếm mỗi vòng, tức 0,15° mỗi số đếm. Nhà sản xuất đôi khi ghi con số sau khi nhân 4 (CPR, đếm mỗi vòng), nên đọc kỹ nhãn.
Đếm x4 cũng có lợi khi trục rung qua lại quanh một sườn xung: mỗi lần tiến là +1, mỗi lần lùi là −1, nên số đếm không bị trôi.

3. Các kiểu encoder và kiểu ngõ ra

| Kiểu | Nguyên lý | Dùng ở đâu |
|---|---|---|
| Quang học | Đĩa có rãnh, LED và đi-ốt quang | Độ phân giải cao (tới hàng chục nghìn xung/vòng), máy CNC, servo; sợ bụi, dầu và rung mạnh |
| Từ tính | Vòng nam châm nhiều cực hoặc bánh răng thép, cảm biến Hall hoặc từ trở | Động cơ giảm tốc có encoder cho robot, môi trường bẩn, nhiều rung |
| Cơ khí | Lá đồng trượt trên đĩa có vạch dẫn điện | Núm xoay chỉnh âm lượng, menu; rẻ nhưng tiếp điểm dội, mòn dần |

Về ngõ ra điện:
- NPN cực thu hở: transistor bên trong chỉ kéo dây xuống 0 V; mức cao do điện trở kéo lên ở bên nhận. Rất tiện cho ESP32 vì kéo lên 3,3 V là an toàn, bất kể encoder chạy nguồn 5 V hay 24 V. Tần số cao thì dùng điện trở kéo lên nhỏ hơn (1 đến 2,2 kΩ) để sườn xung dốc.
- Đẩy kéo (HTL, TTL): ngõ ra lên thẳng bằng điện áp nguồn của encoder; loại 10–30 V phải qua mạch hạ mức hoặc opto trước khi vào ESP32.
- Vi sai RS-422 (line driver): mỗi kênh hai dây ngược pha (A và A đảo); bên nhận dùng IC thu vi sai. Chống nhiễu tốt, dây đi được hàng chục mét trong tủ điện có biến tần.
4. Encoder được chế tạo như thế nào

- Đĩa mã: loại độ phân giải cao dùng đĩa thủy tinh phủ lớp crôm mỏng, quang khắc như làm mạch in để tạo vạch cỡ vài micromet. Loại vừa dùng lá thép không gỉ khắc axit; loại rẻ dùng đĩa nhựa in vạch.
- Mặt nạ và đầu đọc: mặt nạ có rãnh cùng bước, cửa sổ B lệch cửa sổ A đúng 1/4 bước. Encoder hiện đại thường dùng một chip có sẵn mảng đi-ốt quang được bố trí để tự tạo hai tín hiệu lệch 90°.
- Trục và vòng bi: đĩa phải đồng tâm với trục, khe hở giữa đĩa và mặt nạ đều suốt một vòng, nếu không xung sẽ lúc rộng lúc hẹp.
- Căn chỉnh: cho trục quay đều, quan sát A, B trên máy hiện sóng, xoay nhẹ đầu đọc tới khi hai kênh lệch đúng 90° và mỗi xung có tỉ lệ cao thấp gần 50 %.
- Encoder từ: một đầu từ hóa ghi lên vòng nam châm hàng chục đến hàng trăm cặp cực xen kẽ; chip Hall hoặc từ trở đọc và nội suy ra nhiều xung hơn.
- Kiểm tra: đếm đủ số xung mỗi vòng, vị trí vạch Z, sai số chia vạch, thử rung, nhiệt độ và độ kín.
Tự chế một encoder đơn giản: in hoặc cắt một đĩa có rãnh đều (in 3D, cắt mica), gắn lên trục, dùng hai cảm biến quang chữ U đặt lệch nhau một phần tư bước rãnh. Độ phân giải thấp nhưng đủ để đo tốc độ bánh xe robot.
5. Thực hành: đếm encoder bằng PCNT của ESP32
Encoder 600 xung/vòng quay 3000 vòng/phút cho 30 kHz mỗi kênh. Đếm bằng ngắt GPIO sẽ chiếm nhiều thời gian của CPU và dễ sót xung. ESP32 có khối PCNT (bộ đếm xung) đếm cầu phương hoàn toàn bằng phần cứng; chương trình chỉ cần đọc số đếm khi cần.
Linh kiện
- ESP32 DevKit V1 30 chân.
- Một encoder quay ngõ ra NPN cực thu hở, nguồn 5–24 V, có kênh A, B, Z (ví dụ 600 xung/vòng).
- Ba điện trở 4,7 kΩ; breadboard và dây cắm.
Sơ đồ nối

| Dây encoder | Nối tới ESP32 |
|---|---|
| Nguồn (5–24 V) | VIN (5 V khi cắm USB) |
| 0 V | GND |
| A, B, Z | GPIO25, GPIO26, GPIO27; mỗi chân thêm điện trở 4,7 kΩ lên 3V3 |
Chương trình
Chương trình dùng trình điều khiển driver/pulse_cnt.h có sẵn trong gói esp32 3.x, không cần thư viện ngoài. Sửa XUNG_MOI_VONG theo nhãn encoder.
/*
PYWORLD.VN - Encoder tương đối (A, B, Z) với ESP32: đếm cầu phương x4 bằng bộ đếm xung phần cứng PCNT
Kênh A vào GPIO25, kênh B vào GPIO26, vạch Z vào GPIO27; encoder ngõ ra cực thu hở NPN, kéo lên 3V3 bằng điện trở 4,7 kΩ
In số đếm, số vòng, góc, tốc độ (vòng/phút) và chiều quay; báo mỗi lần qua vạch Z
*/
#include "driver/pulse_cnt.h"
#define CHAN_A 25
#define CHAN_B 26
#define CHAN_Z 27
const int XUNG_MOI_VONG = 600; // Số xung của mỗi kênh trong một vòng (ghi trên nhãn: 600 P/R)
const int DEM_MOI_VONG = XUNG_MOI_VONG * 4; // Đếm cả sườn lên và xuống của A và B: x4
const int CHU_KY_MS = 200;
pcnt_unit_handle_t boDem = NULL;
volatile bool quaZ = false;
void IRAM_ATTR ngatZ() {
quaZ = true;
}
void caiDatBoDem() {
pcnt_unit_config_t cauHinh = {};
cauHinh.low_limit = -30000;
cauHinh.high_limit = 30000;
cauHinh.flags.accum_count = 1; // Cộng dồn khi chạm giới hạn, số đếm không bị quay về 0
pcnt_new_unit(&cauHinh, &boDem);
pcnt_glitch_filter_config_t locNhieu = {};
locNhieu.max_glitch_ns = 1000; // Bỏ qua xung nhiễu ngắn hơn 1 µs
pcnt_unit_set_glitch_filter(boDem, &locNhieu);
// Kênh 1: đếm theo sườn của A, chiều đếm do mức của B quyết định
pcnt_chan_config_t kenhA = {};
kenhA.edge_gpio_num = CHAN_A;
kenhA.level_gpio_num = CHAN_B;
pcnt_channel_handle_t hA = NULL;
pcnt_new_channel(boDem, &kenhA, &hA);
pcnt_channel_set_edge_action(hA, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE);
pcnt_channel_set_level_action(hA, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE);
// Kênh 2: đếm theo sườn của B, chiều đếm do mức của A quyết định
pcnt_chan_config_t kenhB = {};
kenhB.edge_gpio_num = CHAN_B;
kenhB.level_gpio_num = CHAN_A;
pcnt_channel_handle_t hB = NULL;
pcnt_new_channel(boDem, &kenhB, &hB);
pcnt_channel_set_edge_action(hB, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE);
pcnt_channel_set_level_action(hB, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE);
// Cần điểm theo dõi ở hai giới hạn thì chế độ cộng dồn mới hoạt động
pcnt_unit_add_watch_point(boDem, cauHinh.low_limit);
pcnt_unit_add_watch_point(boDem, cauHinh.high_limit);
pcnt_unit_enable(boDem);
pcnt_unit_clear_count(boDem);
pcnt_unit_start(boDem);
}
void setup() {
Serial.begin(115200);
caiDatBoDem();
pinMode(CHAN_Z, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(CHAN_Z), ngatZ, FALLING);
Serial.printf("\nEncoder %d xung/vong, dem x4 = %d dem/vong\n", XUNG_MOI_VONG, DEM_MOI_VONG);
}
void loop() {
static int demTruoc = 0;
static uint32_t mocTruoc = millis();
if (millis() - mocTruoc < CHU_KY_MS) {
return;
}
uint32_t bayGio = millis();
int dem = 0;
pcnt_unit_get_count(boDem, &dem);
float giay = (bayGio - mocTruoc) / 1000.0f;
int thayDoi = dem - demTruoc;
float vongPhut = thayDoi / (float)DEM_MOI_VONG / giay * 60.0f;
float soVong = dem / (float)DEM_MOI_VONG;
float goc = (dem % DEM_MOI_VONG) * 360.0f / DEM_MOI_VONG;
if (goc < 0) {
goc += 360.0f;
}
const char *chieu = thayDoi > 0 ? "thuan" : (thayDoi < 0 ? "nghich" : "dung yen");
Serial.printf("Dem %d | %.3f vong | Goc %.1f do | %.1f vong/phut | %s\n", dem, soVong, goc, vongPhut, chieu);
if (quaZ) {
quaZ = false;
Serial.printf("Da qua vach Z (so dem hien tai %d)\n", dem);
}
demTruoc = dem;
mocTruoc = bayGio;
}
Giải thích
- Một đơn vị PCNT có hai kênh: kênh 1 đếm theo sườn của A và lấy mức của B để quyết định cộng hay trừ; kênh 2 làm ngược lại. Kết quả là đếm x4 có dấu.
- Bộ lọc 1 µs bỏ qua gai nhiễu ngắn. Giới hạn ±30000 kèm chế độ cộng dồn giúp số đếm tiếp tục tăng khi vượt giới hạn phần cứng.
- Mỗi 200 ms chương trình đọc số đếm, tính số vòng, góc trong vòng (0 đến 360°), tốc độ từ phần thay đổi và chiều quay từ dấu của phần thay đổi.
- Vạch Z được báo bằng ngắt GPIO; có thể dùng để đặt lại số đếm về 0 sau khi tìm gốc.
Kết quả mong đợi

Encoder tương đối mất vị trí khi mất điện. Máy CNC, máy in 3D dùng encoder tương đối hoặc động cơ bước luôn chạy về công tắc gốc (và vạch Z) khi khởi động. Nếu cần biết vị trí ngay khi bật máy, dùng encoder tuyệt đối.
6. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Số đếm không đổi | Thiếu điện trở kéo lên; encoder không có nguồn | Thêm điện trở 4,7 kΩ lên 3V3; đo điện áp nguồn encoder |
| Đếm ngược chiều mong muốn | Đổi chỗ A và B | Đổi hai dây A, B hoặc đổi số chân trong chương trình |
| Số đếm mỗi vòng ít hơn 2400 | Quay quá nhanh so với điện trở kéo lên; bộ lọc quá lớn | Giảm điện trở kéo lên còn 1–2,2 kΩ; giảm max_glitch_ns |
| Số đếm nhảy khi động cơ chạy | Nhiễu từ dây động lực, biến tần | Dùng cáp có chống nhiễu, đi riêng dây; dùng encoder ngõ ra vi sai |
| Núm xoay cơ khí nhảy hai, ba số mỗi nấc | Tiếp điểm dội | Thêm tụ 10 nF ở mỗi kênh, tăng bộ lọc lên vài µs, chia số đếm cho số sườn mỗi nấc |
7. Xem thêm
- Dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung.
- Mức logic 3,3 V và 5 V: khi nào cần chuyển mức.
- Thanh cảm biến dò line TCRT5000 với ESP32 (cảm biến quang phản xạ, dùng được làm encoder bánh xe đơn giản).
- Các bài khác trong loạt: encoder tuyệt đối, cảm biến tiệm cận cảm ứng từ.
Xem thêm