Đ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

Hình minh họa encoder Hall hai kênh A và B lệch pha 90 độ: quay thuận A đi trước B và mỗi cạnh được cộng 1, quay nghịch B đi trước A và mỗi cạnh bị trừ 1, kèm công thức số đếm mỗi vòng
Hình minh họa: hai kênh lệch pha cho biết cả tốc độ và chiều quay.
  • 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ị

4. Sơ đồ nối dây

Sơ đồ khối: ESP32 nối L298N qua ENA D27, IN1 D26, IN2 D25; L298N cấp điện cho động cơ qua OUT1 OUT2; encoder A vào D32, B vào D33, VCC vào 3V3, GND chung; pin 2 x 18650 cấp nguồn cho L298N
Hình minh họa: sơ đồ khối và bảng 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);
  }
}
Hình minh họa Serial Monitor sau lệnh q: bảng duty từ 0 đến 100 phần trăm và tốc độ vòng mỗi phút tương ứng, phần duty thấp động cơ chưa quay
Hình minh họa: định dạng bảng do lệnh q in ra; số tốc độ là số minh họa.

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_count cù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_action và set_level_action tạ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.

  1. Đồ 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.
  2. Ả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.)
  3. 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.
  4. Đ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

Lỗi thường gặp
Hiện tượngNguyên nhân thường gặpCách xử lý
Số đếm không đổi khi động cơ quayEncoder chưa có nguồn; sai chân A, BKiể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ảmHai 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ênNhiễu từ động cơ chạy dọc dâyDâ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 đôiSai 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ừngChân không hỗ trợ hoặc đang dùng cho việc khácDù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

Bài tiếp theo trong loạt dùng số đo từ encoder để giữ tốc độ động cơ ổn định bằng PID.


Tin tức liên quan

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 điều khiển servo SG90 bằng PWM, không cần thư viện: quét góc, biến trở, hiệu chỉnh
ESP32 điều khiển servo SG90 bằng PWM, không cần thư viện: quét góc, biến trở, hiệu chỉnh

Hướng dẫn điều khiển servo SG90 bằng ESP32 với bộ PWM LEDC 50 Hz có sẵn: xung 0,5–2,4 ms, sơ đồ nối, quét góc, vặn biến trở, hiệu chỉnh hai đầu hành trình và thí nghiệm STEM.

Cánh tay robot 4 servo với ESP32: điều khiển bằng biến trở, ghi nhớ và phát lại động tác
Cánh tay robot 4 servo với ESP32: điều khiển bằng biến trở, ghi nhớ và phát lại động tác

Làm cánh tay robot 4 servo với ESP32: điều khiển từng khớp bằng biến trở, nhấn nút ghi tư thế, phát lại chuỗi động tác mượt, lưu vào flash khi mất điện và gợi ý STEM.

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch không cần router Wi-Fi
Hướng dẫn ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch không cần router Wi-Fi

Hướng dẫn dùng ESP-NOW để hai mạch ESP32 truyền dữ liệu trực tiếp không cần router: xem địa chỉ MAC, chương trình gửi, chương trình nhận, đọc RSSI và xử lý lỗi thường gặp.

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Hướng dẫn bắt đầu với Arduino Nano CH340: sơ đồ chân, driver và nạp code
Hướng dẫn bắt đầu với Arduino Nano CH340: sơ đồ chân, driver và nạp code

Hướng dẫn bắt đầu với Arduino Nano CH340: sơ đồ chân, cài driver, chọn Board và Processor, nạp Blink, in ra Serial Monitor, quét I2C trên A4/A5, cấp nguồn và xử lý lỗi thường gặp.

Hướng dẫn bắt đầu với Wemos D1 R2 (ESP8266) trên Arduino IDE
Hướng dẫn bắt đầu với Wemos D1 R2 (ESP8266) trên Arduino IDE

Hướng dẫn dùng Wemos D1 R2 (ESP8266 khung Arduino UNO) với Arduino IDE 2.x: sơ đồ chân, cài driver CH340 và gói board ESP8266, nạp code nháy LED, kết nối WiFi và xử lý lỗi thường gặp.


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