Ebook Robotics · RL–UAV
MODULE 1 — Dynamics & Feedback Control (Lecture 1–5)
Bản ôn nhanh theo module
Chế độ Slide · phím ← → · F toàn màn · Esc về đọc · S mở slide
Bản ôn nhanh theo module
📚 Học sâu: từng bài có hình + demo ở
Lectures/(Lec01–05).
File này = bản gom để ôn nhanh / tra cứu.
Mục tiêu module.
Hiểu robot chuyển động theo luật nào (\(\dot{x} = f(x,u)\)), và cách thiết kế input \(u\) để hệ ổn định, bám mục tiêu — trước khi đưa học máy / RL vào vòng điều khiển.
f(x,u) ──► open-loop chết ──► feedback ──► PD ổn định ──► LQR (cầu RL)
Lec2 Lec2 demo Lec3 Lec4 Lec5
Luồng module:
Lec 1 Robot tự chủ là gì? (bản đồ stack)
Lec 2 Dynamics — viết f(x,u)
Lec 3 Feedback control — vì sao cần vòng kín
Lec 4 Ổn định & PD
Lec 5 LQR — gain tối ưu từ mô hình (+ cầu nối sang RL)
Phát biểu được bài toán robot tự chủ; vẽ stack Sense–Think–Act; nêu ba mức “cắm” policy RL trên UAV.
Máy thế nào thì gọi là robot, và bài toán robot tự chủ gồm những mảnh nào?
Robot = máy tự đóng vòng kín với thế giới vật lý: cảm nhận → quyết định →
hành động → thế giới thay đổi → cảm nhận lại. Khác chương trình offline ở chỗ:
sai lầm có giá vật lý (rơi máy), thông tin không đầy đủ (cảm biến nhiễu), và
thời gian thực (không được “suy nghĩ” 5 giây mỗi bước khi đang rơi).
Ba ứng dụng khóa hay nhắc: UAV, xe tự lái, tay máy. Cả ba chung một khung:
nhiễu ngoài (gió, ma sát, va chạm)
│
▼
u(t) ──►┌────────────────┐──► x(t) thật
│ THẾ GIỚI / │
│ DYNAMICS f │
└────────────────┘
│ cảm biến (IMU, camera, GPS...)
▼
z(t) = h(x) + nhiễu
│
┌────────────────┐
│ ESTIMATOR │──► x̂(t) (ước lượng state)
└────────────────┘
│
┌────────────────┐
│ PLANNER/POLICY │──► quỹ đạo / hành vi mong muốn
└────────────────┘
│
┌────────────────┐
│ CONTROLLER │──► u(t) (đóng vòng)
└────────────────┘
Khi nói “policy điều khiển drone”, phải nói rõ policy thay khối nào:
- thay planner (output waypoint) → an toàn hơn, dễ triển khai, RL chạy tần số thấp;
- thay position controller (output attitude setpoint) → phổ biến nhất trong các bài RL–UAV;
- thay attitude controller (output lực motor) → khó nhất, đòi hỏi mô phỏng rất trung thực.
Việc chọn “chỗ cắm” của policy thường quyết định phần lớn độ khó sim-to-real.
Viết được dạng state-space; giải thích underactuation của quadrotor; tuyến tính hóa quanh hover và nêu khi nào xấp xỉ gãy.
Cho lệnh \(u\), trạng thái robot biến đổi theo luật nào? Viết luật đó thành
\(\dot x = f(x, u)\) như thế nào?
Dynamics là “vật lý của chính robot”. Bạn đẩy xe hàng: xe không dịch tức thì
theo tay bạn — nó có quán tính, có ma sát. Drone cũng vậy: tăng ga không làm nó
đứng ở chỗ cao hơn ngay lập tức, mà tạo gia tốc, gia tốc tích thành vận tốc,
vận tốc tích thành vị trí. Chuỗi “tích phân hai lần” này là lý do điều khiển khó:
tác động của bạn đến vị trí bị trễ hai tầng.
Mọi hệ cơ học viết được về:
\[ \dot x = f(x, u), \qquad x \in \mathbb{R}^n, u \in \mathbb{R}^m \]
Mẹo chuẩn: hệ bậc hai (Newton \(F = ma\)) → đặt \(x = [\text{vị trí}; \text{vận tốc}]\).
Ví dụ 1 — double integrator (hệ đơn giản nhất, nhưng là xương sống):
một khối lượng \(m\) trên trục, đẩy bằng lực \(u\):
\[ \dot p = v, \qquad \dot v = u/m \]
Đây gần đúng là drone chuyển động một trục khi đã giữ được attitude. Rất nhiều
bài toán UAV cấp position quy về double integrator + nhiễu.
Ví dụ 2 — con lắc (pendulum):
\[ \ddot\theta = -\frac{g}{l}\sin\theta + \frac{u}{ml^2} \]
Phi tuyến vì \(\sin\theta\). Là “ruồi giấm” của control: đủ đơn giản để giải,
đủ phi tuyến để thấy mọi vấn đề.
Drone nhìn nghiêng, chuyển động trong mặt phẳng đứng \((y, z)\), góc nghiêng \(\theta\),
hai cánh quạt tạo lực \(F_1, F_2\):
F1 F2
▲ ▲
│ │
┌───┴───────────────┴───┐
│ thân drone │ ← nghiêng góc θ so với ngang
└───────────┬───────────┘
│
▼ mg (trọng lực)
Tổng lực đẩy F = F1 + F2 (dọc trục thân, vuông góc cánh)
Mô-men xoay M = (F2 - F1)·d (d = nửa sải tay)
Phương trình (khối lượng \(m\), quán tính \(I\)):
\[
\ddot y = -\frac{F}{m}\sin\theta, \qquad
\ddot z = \frac{F}{m}\cos\theta - g, \qquad
\ddot\theta = \frac{M}{I}
\]
Đọc chậm ba dòng này, chúng chứa toàn bộ “tính cách” của drone:
- Muốn dịch ngang (\(\ddot y \neq 0\))? Bắt buộc phải nghiêng (\(\sin\theta\)).
Drone không có “lực ngang” trực tiếp — đây là hệ underactuated:
6 bậc tự do (3D) nhưng chỉ 4 input.
- Nghiêng thì thành phần đẩy dọc \( \cos\theta \) giảm → muốn giữ độ cao khi
nghiêng phải tăng tổng lực đẩy. Các khớp nối chéo (coupling) kiểu này là
thứ khiến điều khiển bằng tay khó.
- Chuỗi tác động: \(M \to \theta \to \ddot y \to y\): muốn dịch 1 mét phải đi
qua bốn lần tích phân. Trễ sâu → dễ dao động nếu điều khiển vụng.
Quanh điểm cân bằng hover (\(\theta \approx 0\), \(F \approx mg\)), lấy xấp xỉ
\(\sin\theta \approx \theta\), \(\cos\theta \approx 1\):
\[ \ddot y \approx -g\,\theta, \qquad \ddot z \approx \frac{\Delta F}{m}, \qquad \ddot\theta = \frac{M}{I} \]
Hệ thành tuyến tính: \(\dot x = Ax + Bu\). Toàn bộ Lec 3–5 làm việc trên dạng này.
Tổng quát: \(A = \partial f/\partial x\), \(B = \partial f/\partial u\) tại điểm cân bằng.
import numpy as np
m, I, g, d = 0.03, 1.4e-5, 9.81, 0.046 # số liệu cỡ Crazyflie
dt = 0.001
def f(x, u):
"""x = [y, z, th, vy, vz, om]; u = [F1, F2]"""
y, z, th, vy, vz, om = x
F, M = u[0] + u[1], (u[1] - u[0]) * d
return np.array([vy, vz, om,
-F/m*np.sin(th),
F/m*np.cos(th) - g,
M/I])
def rollout(u_func, T=2.0):
x = np.zeros(6)
for k in range(int(T/dt)):
x = x + dt * f(x, u_func(x)) # Euler integration
return x
print("Tắt máy :", rollout(lambda x: np.array([0, 0]))[:3]) # rơi ~19.6m
print("Hover đúng:", rollout(lambda x: np.array([m*g/2, m*g/2]))[:3])
print("Lệch 1% :", rollout(lambda x: np.array([m*g/2*1.01, m*g/2*0.99]))[:3])
Chạy thử sẽ thấy: hover open-loop đúng lý thuyết thì đứng yên, nhưng chỉ lệch
lực 1% giữa hai cánh là drone xoay và văng đi — minh chứng vì sao cần feedback.
Phân biệt open-loop và feedback; giải thích vì sao P thuần dao động trên hệ có quán tính; nêu steady-state error.
Vì sao điều khiển vòng hở (open-loop) thất bại, và feedback sửa điều đó thế nào?
Open-loop = “tính trước rồi nhắm mắt chạy”. Như rót nước không nhìn cốc: kế
hoạch đúng đến mấy, chỉ cần model lệch nhẹ hoặc ai đó xê cốc là tràn.
Feedback = “vừa rót vừa nhìn”: đo sai số giữa muốn và có, rồi hành động theo
sai số. Cái hay: bạn không cần model hoàn hảo — sai số tự nói cho bạn biết phải làm gì.
Demo Lec 2 đã cho thấy: hover open-loop chết vì lệch 1%. Feedback là thuốc chữa.
Sai số \(e = x_{des} - x\). Luật đơn giản nhất:
\[ u = k_p\, e \]
Lệch nhiều kéo mạnh, lệch ít kéo nhẹ.
Với double integrator \(\ddot p = u\), đặt \(u = k_p(p_{des} - p)\):
\[ \ddot p + k_p\, p = k_p\, p_{des} \]
Đây là phương trình dao động điều hòa không ma sát — nghiệm là sin/cos:
robot lắc quanh đích mãi mãi, không bao giờ dừng. Kéo về mà không phanh thì
quá đà; quá đà thì kéo ngược; lại quá đà…
vị trí
│ ╭─╮ ╭─╮ ╭─╮
p*┤─────╱───╲───╱───╲───╱───╲────── ← P thuần: dao động không tắt
│ ╱ ╲_╱ ╲_╱ ╲
│ ╱
└──┴────────────────────────────► t
x_des ──►(+)──── e ────►┌────────────┐── u ──►┌───────────┐──┬──► x
▲ – │ CONTROLLER │ │ PLANT f │ │
│ └────────────┘ └───────────┘ │
└───────────────────── đo lường (sensor) ◄─────────┘
“Plant” = hệ bị điều khiển (drone). Vòng tròn (+/–) = tính sai số.
import numpy as np
dt, T = 0.01, 8.0
def sim(controller, wind=0.3):
p, v, log = 0.0, 0.0, []
for k in range(int(T/dt)):
u = controller(p, v)
a = u + wind # gió = nhiễu cộng vào gia tốc
v += a*dt; p += v*dt; log.append(p)
return np.array(log)
target = 1.0
openloop = sim(lambda p, v: 0.0) # không phản hồi
pctrl = sim(lambda p, v: 4.0*(target - p)) # P thuần
print("open-loop cuối:", openloop[-1]) # trôi vô hạn theo gió
print("P-control cuối:", pctrl[-1]) # dao động quanh ~1.075 (lệch tĩnh + lắc)
Quan sát: P thuần (i) dao động, (ii) dừng lệch khỏi đích dưới gió hằng
(steady-state error) — hai khuyết tật mà Lec 4 (D) và khâu I (tích phân) xử lý.
Phát biểu ổn định Lyapunov / tiệm cận; dùng eigenvalue để kiểm tra; chỉnh PD qua \(\zeta\); biết vai trò khâu I.
“Ổn định” định nghĩa chính xác là gì, kiểm tra bằng gì, và thêm khâu D giúp gì?
Thả viên bi:
- đáy chén → đẩy nhẹ, bi lăn về đáy: ổn định tiệm cận
- mặt bàn phẳng → đẩy, bi ở chỗ mới: ổn định biên (marginal)
- đỉnh mũ úp ngược → đẩy nhẹ, bi lăn tuột: bất ổn định
Khâu D (derivative) = thêm ma sát nhân tạo: không nhìn “đang lệch bao nhiêu”
mà nhìn “đang lao nhanh cỡ nào” để phanh sớm. Bi trong chén có mật ong thì về
đáy êm không lắc. PD = kéo về (P) + phanh (D).
Điểm cân bằng \(x^*\) (nơi \(f(x^*, u^*) = 0\)):
- Ổn định (Lyapunov): xuất phát đủ gần thì mãi ở gần.
- Ổn định tiệm cận: gần + hội tụ về \(x^*\) khi \(t \to \infty\).
Hệ tuyến tính \(\dot x = Ax\): ổn định tiệm cận khi và chỉ khi mọi eigenvalue
của \(A\) có phần thực âm (nằm bên trái mặt phẳng phức).
- Phần thực = tốc độ tắt (càng âm càng nhanh).
- Phần ảo = tần số dao động (càng lớn càng lắc).
Hệ phi tuyến: linearize rồi xét eigenvalue của \(A\) — kết luận đúng cục bộ.
\[ u = k_p(p_{des} - p) - k_d\, v \quad\Rightarrow\quad \ddot p + k_d\dot p + k_p p = k_p p_{des} \]
Chính là lò xo–giảm chấn: \(k_p\) = độ cứng lò xo, \(k_d\) = độ nhớt giảm chấn.
Đặt \(\omega_n = \sqrt{k_p}\) (tần số tự nhiên), \(\zeta = k_d / (2\sqrt{k_p})\) (hệ số tắt):
ζ < 1 (underdamped) : vọt lố rồi lắc tắt dần ╱╲╱‾‾
ζ = 1 (critical) : về nhanh nhất không vọt lố ╱‾‾‾
ζ > 1 (overdamped) : về từ từ, chậm ╱⎯⎯⎯ (bò lên)
Tuning thực dụng: chọn \(k_p\) theo tốc độ đáp ứng muốn có, rồi
\(k_d \approx 2\sqrt{k_p}\) (critical damping), tinh chỉnh quanh đó.
Khâu I cộng dồn sai số theo thời gian → khử lệch tĩnh (gió hằng, pin yếu,
drone nặng hơn danh định). Giá phải trả: dễ vọt lố, windup. Firmware
(PX4/Betaflight/Crazyflie) dùng PID lồng nhau: vòng attitude trong, position ngoài.
import numpy as np
dt, T, kp = 0.01, 6.0, 9.0 # ω_n = 3 rad/s
def pd(kd):
p, v, log = 0.0, 0.0, []
for _ in range(int(T/dt)):
u = kp*(1.0 - p) - kd*v
v += u*dt; p += v*dt; log.append(p)
return np.array(log)
for kd, name in [(1.5, "under"), (6.0, "critical"), (14.0, "over")]:
tr = pd(kd)
print(f"{name:9s} kd={kd:4.1f} overshoot={tr.max()-1:+.3f} p(T)={tr[-1]:.3f}")
Phát biểu bài toán LQR; đọc được vai trò \(Q,R\); liên hệ \(P\) với value function và \(-Kx\) với policy tối ưu; nêu giới hạn của LQR.
Thay vì mò \(k_p, k_d\) bằng tay cho hệ nhiều chiều, có cách nào tính gain tối ưu
một phát từ mô hình không?
PD tuning tay được với 1–2 biến. Nhưng quadrotor 12 state, 4 input — chọn tay
hàng chục gain đan chéo nhau là ác mộng. LQR đổi cách đặt vấn đề: bạn không chọn
gain, bạn chọn mức độ quan tâm:
- \(Q\): lệch state nào là “đắt”? (lệch độ cao đắt hơn lệch yaw?)
- \(R\): tốn năng lượng/action mạnh “đắt” cỡ nào?
rồi toán học trả về ma trận gain \(K\) tối ưu. Giống thuê chuyên gia tuning:
bạn chỉ nêu khẩu vị, họ tính chi tiết.
Hệ tuyến tính \(\dot x = Ax + Bu\) (từ linearization quanh hover). Tối thiểu:
\[ J = \int_0^\infty \big( x^\top Q x + u^\top R u \big)\, dt, \qquad Q \succeq 0,\ R \succ 0 \]
- \(x^\top Q x\): phạt lệch trạng thái (bình phương → lệch lớn phạt rất nặng)
- \(u^\top R u\): phạt dùng lực (tiết kiệm pin, tránh bão hòa motor)
Giải phương trình Riccati đại số tìm \(P\):
\[ A^\top P + PA - PBR^{-1}B^\top P + Q = 0 \]
rồi:
\[ K = R^{-1}B^\top P, \qquad \boxed{u = -Kx} \]
Ba điều đáng nhớ:
1. Nghiệm là state feedback tuyến tính — vẫn họ hàng với PD, nhưng gain
tối ưu và tự động phối hợp mọi kênh chéo nhau.
2. \(x^\top P x\) chính là cost-to-go tối ưu — người RL đọc ngay ra:
\(P\) là value function! LQR = trường hợp value iteration hội tụ có công thức đóng.
3. Hệ kín \(\dot x = (A - BK)x\) được bảo đảm ổn định (nếu \((A,B)\) controllable).
Q lớn, R nhỏ → gain lớn → bám sát, phản ứng dữ, tốn năng lượng, dễ chạm
giới hạn motor, khuếch đại nhiễu đo
Q nhỏ, R lớn → gain nhỏ → êm, tiết kiệm, nhưng lười và bám kém
Thực dụng: bắt đầu \(Q = \mathrm{diag}(1/x_{i,max}^2)\), \(R = \mathrm{diag}(1/u_{j,max}^2)\)
(quy tắc Bryson) rồi chỉnh.
Lab 1 của khóa: nạp gain LQR vào firmware Crazyflie để hover — sinh viên thấy
ngay reality gap: gain tối ưu trong mô hình vẫn phải chỉnh khi bay thật.
import numpy as np
from scipy.linalg import solve_continuous_are
m, I, g, d = 0.03, 1.4e-5, 9.81, 0.046
# state x = [y, z, th, vy, vz, om], input u = [ΔF, M] quanh hover
A = np.zeros((6, 6)); A[0, 3] = A[1, 4] = A[2, 5] = 1.0; A[3, 2] = -g
B = np.zeros((6, 2)); B[4, 0] = 1/m; B[5, 1] = 1/I
Q = np.diag([10, 10, 1, 1, 1, 0.1])
R = np.diag([1.0, 0.1])
P = solve_continuous_are(A, B, Q, R)
K = np.linalg.solve(R, B.T @ P)
print("Gain K =\n", np.round(K, 3))
print("Eig hệ kín:", np.round(np.linalg.eigvals(A - B @ K), 2)) # đều Re<0
# thử: xuất phát lệch 1m ngang, 0.5m thấp
dt, x = 0.002, np.array([1.0, -0.5, 0, 0, 0, 0.0])
for k in range(int(4/dt)):
u = -K @ x
xdot = A @ x + B @ u
x = x + dt * xdot
print("Sau 4s:", np.round(x, 4)) # → về gần 0 hết
Điểm đáng ngắm trong output: cột gain cho thấy kênh \(M\) (mô-men) tự động
“nhìn” cả \(y\) và \(v_y\) — LQR tự phát hiện “muốn sửa lệch ngang phải nghiêng”,
điều bạn phải tự nghĩ ra nếu tuning PD tay.
Đây là bài quan trọng nhất của module với người làm RL:
- LQR = trường hợp RL giải được bằng giấy bút khi dynamics tuyến tính và cost bậc hai: \(P\) ↔ value function, \(-Kx\) ↔ policy tối ưu, phương trình Riccati ↔ Bellman.
Nếu thuật toán RL không tái tạo gần đúng \(K\) trên bài LQR chuẩn, hãy nghi ngờ implementation trước.
- LQR là baseline bắt buộc trong các bài RL–UAV về hover/tracking — reviewer phía robotics sẽ hỏi.
- \(Q, R\) là ngôn ngữ reward shaping có nguyên tắc: phóng theo giới hạn vật lý (Bryson), tránh hệ số “bốc thuốc”.
- Residual RL: \(u = -Kx + \pi_\theta(x)\) — LQR gánh ổn định, policy học phần phi tuyến còn thiếu. Đáng thử cho hướng NCS.
Dynamics cho bạn \(f\). Feedback cho bạn cách sống sót khi \(f\) không hoàn hảo.
PD tạo trực giác chỉnh tham số. LQR cho nghiệm tối ưu tuyến tính — và là “trường hợp có công thức đóng” gần nhất với value/policy mà bạn dùng trong RL.
Bài tiếp: 02_Motion_Planning.md — robot đã đứng vững, giờ học cách tìm đường.