Đo tốc độ động cơ bằng encoder và bộ đếm xung PCNT của ESP32: vòng/phút và số vòng
Hướng dẫn đọc encoder Hall hai kênh của động cơ DC bằng bộ đếm xung PCNT của ESP32: đếm 4 cạnh, tính vòng/phút và số vòng, lập bảng tốc độ theo duty trong giờ học STEM.
Động cơ DC chạy bằng PWM chỉ biết "được cấp bao nhiêu điện", chứ không biết thực tế quay nhanh hay chậm. Khi gắn thêm encoder ở đuôi động cơ, ESP32 đếm được số xung để biết trục đã quay bao nhiêu vòng và quay nhanh bao nhiêu. Đây là bước đầu tiên để điều khiển vòng kín, chẳng hạn giữ tốc độ ổn định bằng PID hay cho xe robot chạy thẳng.
Bài viết giải thích encoder hai kênh A, B, cách dùng bộ đếm xung phần cứng PCNT của ESP32 để đếm mà không tốn CPU, chương trình đo tốc độ (vòng/phút) và vị trí (số vòng), cùng thí nghiệm STEM lập bảng tốc độ theo duty.
1. Encoder hai kênh hoạt động thế nào

- Encoder trên động cơ giảm tốc thường có một đĩa nam châm gắn vào đuôi trục và hai cảm biến Hall đặt lệch nhau. Mỗi vòng trục động cơ, mỗi kênh cho ra một số xung cố định, ví dụ 11 xung.
- Hai kênh lệch nhau một phần tư chu kỳ (90°). Quay thuận thì A đi trước B, quay nghịch thì B đi trước A. Nhờ vậy biết được chiều quay.
- Đếm 4 cạnh: đếm cả cạnh lên và cạnh xuống của cả hai kênh, mỗi xung của một kênh cho 4 đơn vị đếm.
- Encoder gắn ở trục động cơ, trước hộp số. Muốn tính cho trục ra phải nhân thêm tỉ số hộp số. Với 11 xung, hộp số 30 : 1, một vòng trục ra là 11 × 4 × 30 = 1320 đơn vị đếm.
2. Vì sao dùng PCNT thay cho ngắt
Ở 200 vòng/phút trục ra, encoder trên tạo 1320 × 200 / 60 ≈ 4400 cạnh mỗi giây. Đếm bằng ngắt GPIO vẫn được, nhưng mỗi cạnh là một lần ngắt, tốn CPU và dễ sót khi chương trình bận. ESP32 có bộ PCNT (Pulse Counter) đếm xung bằng phần cứng:
- tự giải mã hai kênh A, B và cộng trừ theo chiều quay;
- có bộ lọc nhiễu bỏ qua các gai xung ngắn;
- chương trình chỉ cần đọc số đếm khi cần.
Gói esp32 3.x dùng bộ thư viện ESP-IDF 5, nên chỉ cần #include "driver/pulse_cnt.h", không cài thêm thư viện.
3. Linh kiện cần chuẩn bị
- ESP32 DevKit V1 30 chân.
- Động cơ DC giảm tốc có encoder Hall hai kênh. Ghi lại số xung mỗi vòng và tỉ số hộp số in trên nhãn hoặc ghi trong mô tả của động cơ.
- Module L298N, khay pin 18650 loại 2S và hai pin.
- Dây cắm test board.
4. Sơ đồ nối dây

- Phần động cơ và L298N nối giống bài ESP32 điều khiển động cơ DC với L298N trong cùng loạt.
- Cấp cho encoder 3,3 V từ chân 3V3 của ESP32, để xung ra ở mức 3,3 V. Nếu encoder bắt buộc dùng 5 V và có điện trở kéo lên 5 V trên mạch, phải chuyển mức trước khi vào ESP32.
- Đầu cắm 6 dây của động cơ có encoder không có thứ tự chung. Xem chữ in trên mạch encoder (thường là M1, GND, C1 hoặc A, C2 hoặc B, VCC, M2).
5. Chương trình đo tốc độ và vị trí
Sửa hai hằng số XUNG_MOI_VONG_DONG_CO và TI_SO_HOP_SO theo động cơ của bạn. Chương trình in kết quả mỗi 200 ms và nhận lệnh trên Serial Monitor (115200 baud, kiểu xuống dòng "New Line"): t 60 tiến 60 %, l 60 lùi 60 %, d dừng, q quét duty để lập bảng.
/*
PYWORLD.VN - Đo tốc độ và vị trí động cơ DC có encoder bằng bộ đếm xung PCNT của ESP32 (đếm 4 cạnh)
Encoder: A -> GPIO32, B -> GPIO33, VCC -> 3V3, GND -> GND. L298N như bài động cơ DC: ENA GPIO27, IN1 GPIO26, IN2 GPIO25
Lệnh (115200, New Line): t 60 = tiến 60 %, l 60 = lùi 60 %, d = dừng, q = quét duty 0 -> 100 % để lập bảng tốc độ
*/
#include "driver/pulse_cnt.h"
#define ENC_A 32
#define ENC_B 33
#define ENA_PIN 27
#define IN1_PIN 26
#define IN2_PIN 25
// Đọc trên nhãn động cơ: số xung mỗi vòng trục động cơ (một kênh) và tỉ số hộp số
const float XUNG_MOI_VONG_DONG_CO = 11;
const float TI_SO_HOP_SO = 30;
// Đếm cả cạnh lên và cạnh xuống của hai kênh: nhân 4
const float DEM_MOI_VONG = XUNG_MOI_VONG_DONG_CO * 4 * TI_SO_HOP_SO;
const int CHU_KY_MS = 200; // chu kỳ tính tốc độ
pcnt_unit_handle_t donVi = NULL;
int dutyHienTai = 0;
void caiDatPCNT() {
pcnt_unit_config_t cauHinh = {};
cauHinh.high_limit = 30000;
cauHinh.low_limit = -30000;
cauHinh.flags.accum_count = 1; // cộng dồn số đếm khi chạm giới hạn
ESP_ERROR_CHECK(pcnt_new_unit(&cauHinh, &donVi));
pcnt_glitch_filter_config_t locNhieu = {};
locNhieu.max_glitch_ns = 1000; // bỏ qua xung nhiễu ngắn hơn 1 micro giây
ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(donVi, &locNhieu));
pcnt_chan_config_t kA = {};
kA.edge_gpio_num = ENC_A;
kA.level_gpio_num = ENC_B;
pcnt_channel_handle_t kenhA;
ESP_ERROR_CHECK(pcnt_new_channel(donVi, &kA, &kenhA));
pcnt_chan_config_t kB = {};
kB.edge_gpio_num = ENC_B;
kB.level_gpio_num = ENC_A;
pcnt_channel_handle_t kenhB;
ESP_ERROR_CHECK(pcnt_new_channel(donVi, &kB, &kenhB));
// Giải mã cầu phương: chiều đếm phụ thuộc mức của kênh còn lại
pcnt_channel_set_edge_action(kenhA, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE);
pcnt_channel_set_level_action(kenhA, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE);
pcnt_channel_set_edge_action(kenhB, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE);
pcnt_channel_set_level_action(kenhB, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE);
pcnt_unit_add_watch_point(donVi, cauHinh.high_limit);
pcnt_unit_add_watch_point(donVi, cauHinh.low_limit);
ESP_ERROR_CHECK(pcnt_unit_enable(donVi));
ESP_ERROR_CHECK(pcnt_unit_clear_count(donVi));
ESP_ERROR_CHECK(pcnt_unit_start(donVi));
}
int docDem() {
int dem = 0;
pcnt_unit_get_count(donVi, &dem);
return dem;
}
void quay(int tocDo) {
tocDo = constrain(tocDo, -100, 100);
digitalWrite(IN1_PIN, tocDo > 0 ? HIGH : LOW);
digitalWrite(IN2_PIN, tocDo < 0 ? HIGH : LOW);
ledcWrite(ENA_PIN, (uint32_t)abs(tocDo) * 1023 / 100);
dutyHienTai = tocDo;
}
// Đo tốc độ trong khoảng thời gian ms (vòng/phút của trục ra)
float doTocDo(int ms) {
int dem0 = docDem();
delay(ms);
return (docDem() - dem0) / DEM_MOI_VONG * 60000.0f / ms;
}
void quetDuty() {
Serial.println("duty (%) ; toc do (vong/phut)");
for (int d = 0; d <= 100; d += 10) {
quay(d);
delay(1500); // chờ động cơ ổn định
Serial.printf("%3d ; %6.1f\n", d, doTocDo(1000));
}
quay(0);
}
void setup() {
Serial.begin(115200);
pinMode(IN1_PIN, OUTPUT);
pinMode(IN2_PIN, OUTPUT);
ledcAttach(ENA_PIN, 1000, 10);
caiDatPCNT();
quay(0);
Serial.printf("Encoder: %.0f dem moi vong truc ra\n", DEM_MOI_VONG);
}
void loop() {
if (Serial.available()) {
String s = Serial.readStringUntil('\n');
s.trim();
char c = s.length() ? tolower(s.charAt(0)) : ' ';
int so = s.substring(1).toInt();
if (c == 't') quay(so);
else if (c == 'l') quay(-so);
else if (c == 'd') quay(0);
else if (c == 'q') quetDuty();
}
static unsigned long tTruoc = 0;
static int demTruoc = 0;
if (millis() - tTruoc >= CHU_KY_MS) {
int dem = docDem();
float vongPhut = (dem - demTruoc) / DEM_MOI_VONG * 60000.0f / (millis() - tTruoc);
tTruoc = millis();
demTruoc = dem;
Serial.printf("duty %4d %% | dem %7d | vi tri %8.2f vong | toc do %6.1f vong/phut\n",
dutyHienTai, dem, dem / DEM_MOI_VONG, vongPhut);
}
}

6. Giải thích các lệnh chính
pcnt_new_unit()tạo một bộ đếm với giới hạn ±30000. Cờaccum_countcùng hai điểm theo dõi ở hai giới hạn giúp số đếm tiếp tục cộng dồn khi vượt ±30000, không bị quay về 0.pcnt_unit_set_glitch_filter()bỏ qua các xung ngắn hơn 1 µs, thường là nhiễu từ động cơ.- Hai kênh của bộ đếm: kênh thứ nhất đếm cạnh của A và xem mức của B; kênh thứ hai đếm cạnh của B và xem mức của A. Các lệnh
set_edge_actionvàset_level_actiontạo ra cách giải mã ở hình 1. - Tốc độ được tính bằng số đếm tăng thêm trong một khoảng thời gian, chia cho số đếm mỗi vòng, rồi đổi ra vòng/phút.
Chu kỳ đo dài thì tốc độ ổn định hơn nhưng phản ứng chậm hơn. Với 1320 đơn vị đếm mỗi vòng, ở tốc độ thấp nên đo trong 200–500 ms.
7. Gợi ý dùng trong giờ học STEM
Mục tiêu: học sinh làm quen với cảm biến vị trí, phân biệt điều khiển vòng hở và vòng kín, lập bảng và vẽ đồ thị từ số liệu đo thật.
- Đồ thị tốc độ theo duty. Chạy lệnh
q, chép bảng vào bảng tính và vẽ đồ thị. Nhận xét: vùng nào động cơ chưa quay? Đồ thị có thẳng không? Thử lại khi pin còn đầy và khi pin đã yếu. - Ảnh hưởng của tải. Ở duty 60 %, dùng tay hãm nhẹ trục ra và quan sát tốc độ giảm. Từ đó dẫn tới câu hỏi: làm sao giữ tốc độ không đổi khi tải thay đổi? (Bài PID trong cùng loạt trả lời câu hỏi này.)
- Kiểm chứng tỉ số hộp số. Đánh dấu trục ra, quay bằng tay đúng 10 vòng, xem số đếm. Chia cho 10 × 4 × số xung mỗi vòng để ra tỉ số hộp số thật.
- Đo quãng đường. Gắn bánh xe đường kính D lên trục: quãng đường = số vòng × π × D. Cho xe chạy trên sàn và so với thước dây.
8. 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 khi động cơ quay | Encoder chưa có nguồn; sai chân A, B | Kiểm tra VCC 3,3 V và GND của encoder; đo xung bằng đồng hồ hoặc LED |
| Tiến mà số đếm giảm | Hai kênh A, B đổi chỗ so với chiều quay | Đổi chỗ dây A và B, hoặc đổi hai số ENC_A, ENC_B |
| Số đếm nhảy lung tung, cả khi đứng yên | Nhiễu từ động cơ chạy dọc dây | Dây encoder ngắn, tách xa dây động cơ; hàn tụ 100 nF giữa hai chân động cơ |
| Tốc độ chỉ bằng một nửa hoặc gấp đôi | Sai số xung mỗi vòng hoặc tỉ số hộp số | Kiểm chứng bằng thí nghiệm 3 ở mục 7 |
| Lỗi khi khởi động, chương trình dừng | Chân không hỗ trợ hoặc đang dùng cho việc khác | Dùng chân GPIO thường (không dùng chân 6–11); xem dòng báo lỗi trên Serial Monitor |
9. Xem thêm
- Ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung
- PWM trên ESP32
- Mức logic 3,3 V và 5 V: khi nào cần chuyển mức
Bài tiếp theo trong loạt dùng số đo từ encoder để giữ tốc độ động cơ ổn định bằng PID.
Xem thêm