BÀI 50 – ĐIỀU KHIỂN 2 MOTOR ĐỘC LẬP BẰNG ENCODER + PID TRÊN STM32F103
Đây là một cột mốc rất quan trọng trong chuỗi bài học của chúng ta.
Từ Bài 49, ta đã có:
Target Position
↓
Position Controller
↓
Target Speed
↓
Speed PID
↓
PWM
↓
Motor
↓
Encoder
Nhưng đó mới là 1 motor.
Trong Bài 50, chúng ta xây dựng hệ thống cho 2 motor độc lập:
STM32F103
/ \
/ \
LEFT MOTOR RIGHT MOTOR
│ │
Encoder L Encoder R
│ │
▼ ▼
Speed L Speed R
│ │
▼ ▼
PID L PID R
│ │
▼ ▼
PWM L PWM R
Đây chính là nền móng của robot 2 bánh vi sai – differential drive robot.
1. Tại sao phải điều khiển 2 motor độc lập?
Giả sử bạn ra lệnh:
PWM Left = 60%
PWM Right = 60%
Bạn có thể nghĩ:
Hai bánh sẽ chạy cùng tốc độ.
Nhưng thực tế không chắc.
Ví dụ:
Left = 105 RPM
Right = 92 RPM
Robot sẽ:
↗
/
/
/
START
Nó bị lệch sang một phía.
2. Tại sao cùng PWM nhưng tốc độ khác nhau?
Có rất nhiều nguyên nhân:
- Hai motor không hoàn toàn giống nhau.
- Ma sát hai bánh khác nhau.
- Điện trở cuộn dây khác nhau.
- Tải hai bên khác nhau.
- Bánh xe có đường kính thực tế hơi khác.
- Hộp số có sai số.
- Điện áp motor thay đổi.
- Mặt sàn không đồng đều.
Vì vậy:
PWM trái = PWM phải
không đồng nghĩa với:
RPM trái = RPM phải
3. Encoder giải quyết vấn đề này
Mỗi motor có một encoder:
LEFT MOTOR
│
▼
ENCODER L
│
▼
RPM_L
và:
RIGHT MOTOR
│
▼
ENCODER R
│
▼
RPM_R
Sau đó mỗi motor có một PID riêng.
4. Hai PID độc lập
Target RPM L ──► PID L ──► PWM L ──► Motor L
▲ │
│ ▼
RPM L ◄────────── Encoder L
và:
Target RPM R ──► PID R ──► PWM R ──► Motor R
▲ │
│ ▼
RPM R ◄────────── Encoder R
Hai vòng này hoạt động độc lập.
5. Đây là ý tưởng quan trọng nhất của Bài 50
Nếu muốn robot đi thẳng:
Target RPM L = 100
Target RPM R = 100
PID sẽ tự điều chỉnh:
PWM L
PWM R
để cuối cùng:
Actual RPM L ≈ 100
Actual RPM R ≈ 100
Cho dù:
PWM L ≠ PWM R
Điều này hoàn toàn bình thường.
6. Ví dụ thực tế
Giả sử:
Target L = 100 RPM
Target R = 100 RPM
Ban đầu:
Actual L = 80 RPM
Actual R = 70 RPM
PID tính:
Error L = 20
Error R = 30
Do đó:
PWM L = 55%
PWM R = 62%
Motor phải có thể được cấp PWM lớn hơn để bắt kịp.
Sau một thời gian:
Actual L = 100 RPM
Actual R = 100 RPM
PID sẽ giảm PWM về mức cần thiết.
7. Kiến trúc tổng thể
STM32F103
│
┌──────────────┴──────────────┐
│ │
▼ ▼
LEFT CONTROL RIGHT CONTROL
│ │
Target RPM L Target RPM R
│ │
▼ ▼
PID LEFT PID RIGHT
│ │
▼ ▼
PWM LEFT PWM RIGHT
│ │
▼ ▼
MOTOR LEFT MOTOR RIGHT
│ │
▼ ▼
ENCODER LEFT ENCODER RIGHT
│ │
▼ ▼
RPM LEFT RPM RIGHT
│ │
└──────────────┬──────────────┘
▼
STM32F103
8. Phần cứng cần gì?
Một hệ thống cơ bản:
STM32F103C8T6
│
├── Motor Driver
│ ├── Left Motor
│ └── Right Motor
│
├── Encoder Left
│
└── Encoder Right
Ví dụ driver:
TB6612FNG
hoặc các driver H-bridge phù hợp với motor.
9. Mỗi motor cần 2 loại tín hiệu
Motor driver
PWM
DIR
Encoder
A
B
Vậy hai motor có:
Motor L:
PWM_L
DIR_L
ENC_L_A
ENC_L_B
Motor R:
PWM_R
DIR_R
ENC_R_A
ENC_R_B
10. STM32F103 có đủ Timer không?
Đây là câu hỏi rất quan trọng.
STM32F103 có nhiều timer:
TIM1
TIM2
TIM3
TIM4
và một số timer khác tùy package.
Ta có thể bố trí:
TIM2 → Encoder Left
TIM3 → Encoder Right
Còn PWM:
TIM1 / TIM4
hoặc sử dụng các channel PWM phù hợp với chân phần cứng thực tế.
Không nên chọn chân trước rồi mới kiểm tra Timer mapping.
Đối với STM32, cần kiểm tra:
Timer
↓
Channel
↓
GPIO Alternate Function
11. Hai Encoder Timer Mode
Ví dụ:
TIM2 → Encoder Left
TIM3 → Encoder Right
Cả hai đều ở:
Encoder Mode
Sau đó:
left_count = TIM2->CNT;
right_count = TIM3->CNT;
12. Không nên dùng trực tiếp CNT làm vị trí dài hạn
Giống Bài 48, chúng ta tạo:
volatile int32_t left_position;
volatile int32_t right_position;
Sau mỗi chu kỳ:
int16_t left_now;
int16_t right_now;
int16_t left_delta;
int16_t right_delta;
rồi:
left_position += left_delta;
right_position += right_delta;
13. Cấu trúc Encoder
Ta có thể dùng:
typedef struct
{
int16_t last_count;
int32_t position;
float rpm;
} Encoder_t;
Khai báo:
Encoder_t left_encoder;
Encoder_t right_encoder;
14. Cập nhật encoder
void Encoder_Update(
Encoder_t *enc,
volatile uint16_t *cnt)
{
int16_t current;
int16_t delta;
current = (int16_t)(*cnt);
delta =
current - enc->last_count;
enc->position += delta;
enc->last_count =
current;
}
Sau đó:
Encoder_Update(
&left_encoder,
&TIM2->CNT);
Encoder_Update(
&right_encoder,
&TIM3->CNT);
15. Tính RPM
Giả sử:
CPR = 400
và chu kỳ đo:
10 ms
Tức:
dt = 0.01 s
Nếu trong 10 ms encoder tăng:
20 counts
thì:
[
RPM =
\frac{20}{400}
\times
\frac{60}{0.01}
]
=
300 RPM
16. Hàm tính RPM
float Encoder_CalculateRPM(
int16_t delta,
float cpr,
float dt)
{
return
((float)delta / cpr)
* (60.0f / dt);
}
Ví dụ:
left_encoder.rpm =
Encoder_CalculateRPM(
left_delta,
400.0f,
0.01f);
17. Bây giờ có hai PID
Ở Bài 49 chúng ta có:
PID_t speed_pid;
Bây giờ phải có:
PID_t left_pid;
PID_t right_pid;
Đây là điểm rất quan trọng.
18. Khởi tạo
Ví dụ:
PID_Init(
&left_pid,
2.0f,
0.5f,
0.0f,
-1000.0f,
1000.0f);
và:
PID_Init(
&right_pid,
2.0f,
0.5f,
0.0f,
-1000.0f,
1000.0f);
Ban đầu có thể dùng cùng thông số.
Sau này hai motor có thể cần tuning khác nhau.
19. Target RPM
Ta tạo:
float target_left_rpm;
float target_right_rpm;
Ví dụ robot đi thẳng:
target_left_rpm = 100.0f;
target_right_rpm = 100.0f;
20. Speed PID Left
float pwm_left;
pwm_left =
PID_Update(
&left_pid,
target_left_rpm,
left_encoder.rpm,
0.01f);
21. Speed PID Right
float pwm_right;
pwm_right =
PID_Update(
&right_pid,
target_right_rpm,
right_encoder.rpm,
0.01f);
Sau đó:
Motor_Left_SetPWM(
(int16_t)pwm_left);
Motor_Right_SetPWM(
(int16_t)pwm_right);
22. Đây chính là hai vòng kín
LEFT:
Target RPM
↓
PID L
↓
PWM L
↓
Motor L
↓
Encoder L
↓
RPM L
└──────► PID L
và:
RIGHT:
Target RPM
↓
PID R
↓
PWM R
↓
Motor R
↓
Encoder R
↓
RPM R
└──────► PID R
23. Hàm điều khiển hai motor
Ta có thể viết:
void Motors_ControlUpdate(void)
{
float pwm_left;
float pwm_right;
pwm_left =
PID_Update(
&left_pid,
target_left_rpm,
left_encoder.rpm,
0.01f);
pwm_right =
PID_Update(
&right_pid,
target_right_rpm,
right_encoder.rpm,
0.01f);
Motor_Left_SetPWM(
(int16_t)pwm_left);
Motor_Right_SetPWM(
(int16_t)pwm_right);
}
24. Vấn đề cực kỳ quan trọng: chiều motor
Giả sử:
PWM Left = +
PWM Right = +
nhưng do hai motor được lắp đối xứng:
Left motor → tiến
Right motor → lùi
thì robot sẽ quay tại chỗ.
Vì vậy cần chuẩn hóa chiều.
Ví dụ:
#define LEFT_MOTOR_INVERTED 0
#define RIGHT_MOTOR_INVERTED 1
25. Hàm xử lý chiều
void Motor_Right_SetPWM(
int16_t pwm)
{
if (pwm >= 0)
{
Motor_Right_Forward();
}
else
{
Motor_Right_Reverse();
pwm = -pwm;
}
if (pwm > PWM_MAX)
pwm = PWM_MAX;
Motor_Right_PWM(pwm);
}
Nếu motor phải bị đảo chiều vật lý, ta đảo trong driver.
26. Encoder cũng có thể bị đảo chiều
Đây là lỗi rất thường gặp.
Bạn ra lệnh:
Motor Forward
nhưng encoder báo:
RPM = -100
Có nghĩa là:
Motor direction
và:
Encoder direction
không cùng quy ước.
Ta có thể thêm:
#define LEFT_ENCODER_INVERTED 0
#define RIGHT_ENCODER_INVERTED 1
27. Quy tắc rất quan trọng
Sau khi cấu hình:
Motor Forward
phải đảm bảo:
Encoder RPM > 0
và:
Motor Reverse
phải:
Encoder RPM < 0
Nếu không, PID sẽ hoạt động sai dấu.
28. Một lỗi nguy hiểm
Giả sử:
Target = +100 RPM
Actual = -100 RPM
PID thấy:
Error = 200 RPM
và tăng PWM.
Nhưng motor càng chạy nhanh theo chiều ngược.
→ hệ thống có thể mất ổn định.
Vì vậy phải kiểm tra dấu encoder trước khi chạy PID.
29. Test từng motor trước
Đừng lập tức chạy:
Left PID + Right PID
Hãy test:
Motor trái
PWM = +30%
Kiểm tra:
RPM > 0
Motor phải
PWM = +30%
Kiểm tra:
RPM > 0
Sau đó mới bật PID.
30. Test PID từng bên
Trước tiên:
Left:
Target = 50 RPM
Right:
Target = 0
Kiểm tra motor trái.
Sau đó:
Left:
Target = 0
Right:
Target = 50 RPM
Kiểm tra motor phải.
Chỉ khi hai PID đều hoạt động ổn định, mới chạy đồng thời.
31. Đi thẳng bằng hai PID
Bây giờ:
Target Left = 100 RPM
Target Right = 100 RPM
Giả sử motor trái khỏe:
PWM L = 45%
motor phải yếu:
PWM R = 55%
Nhưng:
RPM L ≈ 100
RPM R ≈ 100
Robot sẽ đi thẳng tốt hơn rất nhiều so với PWM cố định.
32. Nhưng vẫn có thể lệch
Encoder PID chỉ đảm bảo:
RPM L ≈ RPM R
Không đảm bảo tuyệt đối:
Distance L = Distance R
vì:
- encoder sai số;
- bánh trái/phải khác đường kính;
- encoder CPR khác nhau;
- bánh trượt;
- mặt sàn không đều.
Do đó robot thực tế có thể cần thêm:
Differential correction
33. Hiệu chỉnh chênh lệch hai bánh
Giả sử:
RPM L = 100
RPM R = 95
ta tính:
[
Error_{sync}=RPM_L-RPM_R
]
=
5
Sau đó:
Correction = Ksync × ErrorSync
Ví dụ:
Ksync = 0.2
thì:
Correction = 1
34. Tạo tốc độ mục tiêu hiệu chỉnh
Nếu tốc độ cơ bản:
100 RPM
thì:
Left target = 100 - Correction
Right target = 100 + Correction
hoặc ngược lại tùy quy ước.
Ví dụ:
Left = 99
Right = 101
để hai bánh đồng bộ hơn.
35. Nhưng chưa nên thêm correction ngay
Ở Bài 50, ta nên làm theo thứ tự:
1. Encoder Left
2. Encoder Right
3. RPM Left
4. RPM Right
5. PID Left
6. PID Right
7. Hai motor chạy cùng RPM
8. Sau đó mới thêm synchronization
Đây là cách debug dễ nhất.
36. Kiến trúc phần mềm
Một project sạch có thể:
Core/
│
├── motor.c
├── motor.h
│
├── encoder.c
├── encoder.h
│
├── pid.c
├── pid.h
│
└── robot.c
robot.h
37. motor.h
Ví dụ:
#ifndef MOTOR_H
#define MOTOR_H
void Motor_Left_SetPWM(int16_t pwm);
void Motor_Right_SetPWM(int16_t pwm);
void Motor_Left_Stop(void);
void Motor_Right_Stop(void);
#endif
38. encoder.h
#ifndef ENCODER_H
#define ENCODER_H
typedef struct
{
int16_t last_count;
int32_t position;
float rpm;
} Encoder_t;
void Encoder_Init(void);
void Encoder_Update(void);
int32_t Encoder_LeftPosition(void);
int32_t Encoder_RightPosition(void);
float Encoder_LeftRPM(void);
float Encoder_RightRPM(void);
#endif
39. pid.h
#ifndef PID_H
#define PID_H
typedef struct
{
float kp;
float ki;
float kd;
float integral;
float previous_error;
float output_min;
float output_max;
} PID_t;
void PID_Init(
PID_t *pid,
float kp,
float ki,
float kd,
float min,
float max);
float PID_Update(
PID_t *pid,
float target,
float actual,
float dt);
void PID_Reset(PID_t *pid);
#endif
40. robot.h
#ifndef ROBOT_H
#define ROBOT_H
void Robot_Init(void);
void Robot_SetSpeed(
float left_rpm,
float right_rpm);
void Robot_Stop(void);
void Robot_ControlUpdate(void);
#endif
41. robot.c
static PID_t left_pid;
static PID_t right_pid;
static float target_left_rpm;
static float target_right_rpm;
Khởi tạo:
void Robot_Init(void)
{
PID_Init(
&left_pid,
2.0f,
0.5f,
0.0f,
-1000.0f,
1000.0f);
PID_Init(
&right_pid,
2.0f,
0.5f,
0.0f,
-1000.0f,
1000.0f);
}
42. Đặt tốc độ
void Robot_SetSpeed(
float left_rpm,
float right_rpm)
{
target_left_rpm =
left_rpm;
target_right_rpm =
right_rpm;
}
Ví dụ:
Robot_SetSpeed(
100.0f,
100.0f);
→ đi thẳng.
43. Quay tại chỗ
Nếu:
Robot_SetSpeed(
80.0f,
-80.0f);
thì:
Left → tiến
Right → lùi
Robot quay tại chỗ.
44. Quay trái
Ví dụ:
Robot_SetSpeed(
40.0f,
100.0f);
Kết quả:
Left chậm
Right nhanh
Robot quay trái.
45. Quay phải
Robot_SetSpeed(
100.0f,
40.0f);
→ robot quay phải.
46. Lùi
Robot_SetSpeed(
-80.0f,
-80.0f);
→ cả hai bánh lùi.
47. Robot Control Update
void Robot_ControlUpdate(void)
{
float pwm_left;
float pwm_right;
pwm_left =
PID_Update(
&left_pid,
target_left_rpm,
left_encoder.rpm,
0.01f);
pwm_right =
PID_Update(
&right_pid,
target_right_rpm,
right_encoder.rpm,
0.01f);
Motor_Left_SetPWM(
(int16_t)pwm_left);
Motor_Right_SetPWM(
(int16_t)pwm_right);
}
48. Tần số điều khiển
Một lựa chọn tốt ban đầu:
Timer = 1 ms
Sau đó:
10 ms → Speed PID
Tức:
100 Hz
Đây là tần số khá phù hợp cho bài học motor DC nhỏ.
49. main.c
Về logic:
int main(void)
{
System_Init();
Encoder_Init();
Motor_Init();
Robot_Init();
Robot_SetSpeed(
100.0f,
100.0f);
while (1)
{
if (control_flag)
{
control_flag = 0;
Encoder_Update();
Robot_ControlUpdate();
}
}
}
Trong project thực tế, control_flag có thể được set bởi Timer interrupt.
50. Không nên tính RPM bằng delay
Tránh kiểu:
while(1)
{
delay_ms(100);
read_encoder();
calculate_rpm();
}
Vì sau này robot còn phải xử lý:
Encoder
PID
Ultrasonic
UART
Bluetooth
OLED/LCD
State Machine
Dùng Timer tick sẽ tốt hơn.
51. Luồng điều khiển chuẩn
Timer 1 ms
│
├── Encoder sampling
│
└── 10 ms
│
▼
Calculate RPM
│
▼
PID Left
│
PID Right
│
▼
Update PWM
52. Một bài test rất quan trọng
Đặt:
Target L = 100 RPM
Target R = 100 RPM
Ghi lại:
Time | RPM_L | RPM_R | PWM_L | PWM_R
Ví dụ:
0 ms 0 0 0 0
100 70 65 60 65
200 92 88 52 57
300 99 98 48 51
400 100 100 47 50
Đây là kết quả tốt.
53. Nếu thấy
RPM_L = 100
RPM_R = 85
mặc dù:
Target L = Target R = 100
thì:
PID R
cần được tuning lại.
Không nên sửa bằng:
Target R = 110
ngay lập tức.
Bởi vì mục tiêu của PID là làm:
Actual R → Target R
chứ không phải bù sai số bằng tay.
54. Tuning hai PID
Một quy trình tốt:
Motor trái
Target = 50 RPM
tune:
Kp
Ki
Kd
Motor phải
Target = 50 RPM
tune riêng.
Sau đó:
Target L = 100
Target R = 100
test đồng thời.
55. Một lưu ý về Kd
Ở motor DC nhỏ, encoder thường tạo RPM dạng:
80
100
80
100
thay vì:
90
91
92
93
Nếu Kd quá lớn:
Derivative
sẽ khuếch đại nhiễu.
Vì vậy ban đầu:
kd = 0;
thường dễ tune hơn.
Sau đó mới thử:
Kd > 0
nếu thực sự cần.
56. Bài tập thực hành
Bài tập 1
Cho:
Target L = 100 RPM
Target R = 100 RPM
Hãy đo:
RPM L
RPM R
PWM L
PWM R
Bài tập 2
Cho:
Left = 100 RPM
Right = 50 RPM
Robot sẽ quay về phía nào?
→ Về phía bánh chậm hơn, tức phía trái nếu Left = 50 và Right = 100; với Left = 100, Right = 50 thì robot quay phải.
Bài tập 3
Cho:
Left = 80
Right = -80
Robot làm gì?
→ Quay tại chỗ.
57. Bài tập quan trọng nhất
Hãy làm robot chạy:
100 RPM
trong:
5 giây
Sau đó:
STOP
Đọc:
Left Position
Right Position
Nếu:
Left ≈ Right
thì hệ thống rất tốt.
58. Từ đây chúng ta đã có nền móng của robot thật
Ta đã có:
STM32F103
│
┌──────────┴──────────┐
│ │
▼ ▼
PID LEFT PID RIGHT
│ │
▼ ▼
PWM LEFT PWM RIGHT
│ │
▼ ▼
MOTOR LEFT MOTOR RIGHT
│ │
▼ ▼
ENCODER LEFT ENCODER RIGHT
Đây chính là hệ thống điều khiển 2 bánh độc lập.
59. Nhưng vẫn còn một tầng rất thú vị
Hiện tại ta mới nói:
Left RPM
Right RPM
Nếu muốn robot:
Đi thẳng 1 mét
thì không thể đơn giản nói:
100 RPM trong 5 giây
bởi vì tốc độ không đảm bảo chính xác tuyệt đối.
Ta cần:
Distance Target
↓
Encoder Position
↓
Position Control
↓
Left/Right Target Speed
↓
2 Speed PID
↓
2 Motor
Và còn phải đảm bảo:
Left distance ≈ Right distance
60. Đây là bước sang Bài 51
BÀI 51 – ROBOT 2 BÁNH ĐI THẲNG CHÍNH XÁC BẰNG 2 ENCODER + PID
Ở Bài 51 chúng ta sẽ xây dựng:
TARGET DISTANCE
│
▼
POSITION CONTROL
│
┌──────┴──────┐
▼ ▼
LEFT TARGET RIGHT TARGET
SPEED SPEED
│ │
▼ ▼
PID LEFT PID RIGHT
│ │
▼ ▼
PWM LEFT PWM RIGHT
│ │
MOTOR L MOTOR R
│ │
ENC L ENC R
└──────┬────────┘
▼
DISTANCE
và quan trọng hơn, chúng ta sẽ học cách dùng chênh lệch Encoder trái/phải để tự sửa robot đang lệch khỏi đường thẳng.
Đây chính là bước nối trực tiếp từ kiến thức STM32 Timer/Encoder/PID sang robot 2 bánh thực tế.