Atlas Robotics
Điều khiển động cơTrung bình

Điều khiển tốc độ động cơ: từ open-loop tới closed-loop với PID

Atlasbot · 16/8/2026 · 11 phút đọc

Điều khiển tốc độ động cơ: từ open-loop tới closed-loop với PID
Open-loop vs Closed-loop khi tải thay đổi

Ba bài trước đã tách riêng từng mảnh: DC motor cần driver để chạy, encoder đo được tốc độ thực, PID biết cách tính toán điều chỉnh dựa trên sai số. Bài này ghép cả ba lại thành một vòng điều khiển tốc độ hoàn chỉnh — thứ nằm ở lõi của mọi bánh xe AMR di chuyển ổn định.

Khái niệm chính

Có hai cách điều khiển tốc độ động cơ, khác nhau ở một điểm duy nhất: có dùng phản hồi (feedback) hay không.

Open-loop — chỉ đặt PWM, không kiểm tra kết quả

Cách đơn giản nhất: quyết định một giá trị PWM cố định tương ứng với "tốc độ mong muốn", gửi thẳng cho driver, và tin rằng động cơ sẽ quay đúng tốc độ đó.

set_motor_pwm(600);  // "Hy vọng" động cơ quay khoảng 60% tốc độ tối đa

Vấn đề: tốc độ quay thực tế phụ thuộc cả vào điện áp pin (giảm dần khi pin yếu) và tải (robot leo dốc hay chạy trên sàn phẳng) — cùng một giá trị PWM, tốc độ thực tế sẽ khác nhau tuỳ điều kiện. Open-loop chấp nhận sự thiếu chính xác này để đổi lấy sự đơn giản.

Closed-loop — đo tốc độ thật, tự điều chỉnh liên tục

Cách chính xác hơn: dùng encoder đo tốc độ thực tế, đưa vào PID để tự động tính lại giá trị PWM mỗi chu kỳ, sao cho tốc độ đo được luôn bám sát tốc độ mong muốn — bất kể tải hay điện áp pin thay đổi ra sao.

Tóm lại: Open-loop rẻ và đơn giản, chấp nhận sai số theo tải · Closed-loop (encoder + PID) phức tạp hơn một chút nhưng giữ tốc độ ổn định thực sự — gần như bắt buộc cho bánh xe AMR, nơi tải trọng và độ dốc sàn nhà thay đổi liên tục trong lúc vận hành.

Nguyên lý hoạt động

Vòng lặp điều khiển tốc độ hoàn chỉnh, chạy định kỳ trong ngắt Timer (ví dụ mỗi 10ms = 100Hz):

void speed_control_loop(void) {  // Gọi định kỳ trong ngắt Timer
    int32_t pulses = __HAL_TIM_GET_COUNTER(&htim2);
    __HAL_TIM_SET_COUNTER(&htim2, 0);            // Đọc & reset bộ đếm encoder

    float measured_speed = pulses_to_rpm(pulses); // Quy đổi số xung → vòng/phút

    float error = target_speed - measured_speed;
    integral += error * DT;
    float derivative = (error - prev_error) / DT;

    float pwm = Kp * error + Ki * integral + Kd * derivative;
    pwm = constrain(pwm, -1000, 1000);            // Giới hạn trong dải PWM hợp lệ

    set_motor_pwm(pwm);
    prev_error = error;
}
Sàn phẳng, tải nhẹ:   PWM ổn định ở mức thấp để giữ đúng tốc độ mục tiêu
Leo dốc, tải nặng:    PID tự động TĂNG PWM để bù lại, tốc độ vẫn giữ ổn định
Xuống dốc:            PID tự động GIẢM PWM, tránh vọt tốc độ

Kiến thức kỹ thuật

Công thức

So sánh định lượng sai số tốc độ giữa 2 cách tiếp cận khi tải thay đổi đột ngột (VD: robot bắt đầu leo dốc):

Sai số open-loop (%) = |tốc độ thực tế - tốc độ mong muốn| / tốc độ mong muốn × 100%

VD: PWM cố định cho tốc độ mong muốn 100 RPM trên sàn phẳng
Khi leo dốc, tốc độ thực tế tụt còn 70 RPM (PWM không đổi, tải tăng)
Sai số open-loop = |70 - 100| / 100 × 100% = 30%

Closed-loop (PID chỉnh tốt): sau thời gian ổn định (settling time, thường vài trăm ms
đến vài giây tuỳ hệ thống), sai số thường giữ được dưới 1-5%

Thông số

Bảng so sánh nhanh giúp quyết định khi nào thực sự cần đầu tư thêm encoder + PID:

Tiêu chíOpen-loopClosed-loop (encoder + PID)
Độ chính xác khi tải đổiThấp (sai số có thể 20-30%+)Cao (thường dưới vài % sau khi ổn định)
Chi phí phần cứngThấp (không cần encoder)Cao hơn (cần encoder + xử lý thêm)
Độ phức tạp codeRất đơn giảnCần tuning Kp/Ki/Kd
Phù hợp choQuạt tản nhiệt, tải cố địnhBánh xe AMR, tải thay đổi liên tục

Ứng dụng thực tế

Ngoài việc giữ tốc độ ổn định, có sẵn vòng closed-loop còn mở ra khả năng phát hiện trượt bánh (wheel slip) — nếu PID phải liên tục tăng PWM lên gần mức tối đa mà tốc độ đo được từ encoder vẫn không tăng tương ứng, đây là dấu hiệu bánh xe đang trượt trên sàn trơn hoặc treo khỏi mặt đất (VD: một bánh đang ở mép bậc thềm) — thông tin này có thể dùng làm tín hiệu cảnh báo an toàn, điều mà hệ thống open-loop hoàn toàn không có khả năng phát hiện vì nó không bao giờ biết tốc độ thực tế là bao nhiêu.

Ví dụ thực tế

#include <stdio.h>

int main() {
    float target = 100.0;   // RPM mong muốn
    float pwm_openloop = 600;   // giá trị PWM cố định tương ứng 100 RPM trên sàn phẳng

    // Mô phỏng đơn giản: mỗi đơn vị PWM tạo ra tốc độ khác nhau tuỳ tải
    float speed_flat = pwm_openloop * 100.0 / 600.0;      // sàn phẳng: đúng như thiết kế
    float speed_incline_openloop = speed_flat * 0.7;       // leo dốc: tụt 30% (open-loop bất lực)

    // Closed-loop: PID tự tăng PWM bù lại, giả định đạt lại ~98% target sau khi ổn định
    float speed_incline_closedloop = target * 0.98;

    printf("Sàn phẳng (open-loop):        %.1f RPM\n", speed_flat);
    printf("Leo dốc (open-loop):          %.1f RPM (sai so %.0f%%)\n",
           speed_incline_openloop, (target - speed_incline_openloop)/target*100);
    printf("Leo dốc (closed-loop PID):    %.1f RPM (sai so %.0f%%)\n",
           speed_incline_closedloop, (target - speed_incline_closedloop)/target*100);
    return 0;
}
Output:
San phang (open-loop):        100.0 RPM
Leo doc (open-loop):          70.0 RPM (sai so 30%)
Leo doc (closed-loop PID):    98.0 RPM (sai so 2%)

Kết quả minh hoạ đúng bảng so sánh ở mục Kiến thức kỹ thuật — cùng gặp tải thay đổi (leo dốc), open-loop chấp nhận sai số 30%, trong khi closed-loop PID tự điều chỉnh để giữ sai số chỉ còn khoảng 2%.

Lỗi thường gặp

  • Dùng open-loop cho ứng dụng tải thay đổi liên tục — chọn sai công cụ cho bài toán, như minh hoạ ở trên; open-loop chỉ phù hợp khi tải gần như cố định (VD: quạt tản nhiệt), không phù hợp cho bánh xe robot di chuyển trên địa hình thay đổi.
  • Không xử lý tràn số bộ đếm encoder — thanh ghi đếm encoder trên nhiều MCU chỉ 16-bit, nếu không đọc và reset đều đặn (như dòng __HAL_TIM_SET_COUNTER(&htim2, 0) ở ví dụ trên), số đếm có thể tràn giữa 2 lần đọc, gây tính sai tốc độ ở đúng thời điểm đó.
  • Tần số vòng lặp điều khiển không ổn định — nếu speed_control_loop() không chạy đúng chu kỳ cố định (VD: bị chèn bởi ngắt khác ưu tiên cao hơn, hoặc đặt trong loop() thay vì ngắt Timer), giá trị DT giả định trong code sẽ không khớp thời gian thực tế trôi qua, làm sai lệch cả phần I lẫn D của PID.
  • Bỏ qua bước tuning khi đổi động cơ/tải trọng robot — bộ Kp/Ki/Kd đã chỉnh tốt cho 1 động cơ/tải cụ thể không tự động phù hợp khi đổi sang động cơ khác hoặc tải trọng robot thay đổi đáng kể (VD: thêm hàng hoá lên robot chở hàng) — cần kiểm tra và chỉnh lại nếu thay đổi phần cứng đáng kể.

Kết luận

Kết hợp DC motor, encoder, và PID thành vòng điều khiển closed-loop hoàn chỉnh là nền tảng cho bất kỳ bánh xe AMR nào cần di chuyển ổn định qua địa hình và tải trọng thay đổi — đánh đổi bằng chi phí phần cứng và công sức tuning cao hơn open-loop, nhưng gần như bắt buộc cho ứng dụng thực tế. Đây cũng là bài cuối cùng của chuyên mục Điều khiển động cơ, khép lại toàn bộ category Lập trình nhúng (Embedded) — từ Kiến thức nền tảng, Linh kiện điện tử, Vi điều khiển, Lập trình C/C++, Giao tiếp phần cứng, tới Điều khiển động cơ vừa học, đã đủ nền tảng để bắt tay vào xây dựng tầng điều khiển thời gian thực hoàn chỉnh cho một robot AMR thực tế.

Điều khiển tốc độ động cơ: từ open-loop tới closed-loop với PID — Atlasbot