Mở đầu: khi đầu ra quay lại làm đầu vào

Mọi công cụ series này xây tới giờ — DFT, FFT, FIR — đều xử lý tín hiệu theo MỘT CHIỀU: đầu vào chảy qua, ra đầu ra, xong. Nhưng có một lớp hệ thống hoàn toàn khác: hệ ĐỆ QUY, nơi đầu ra QUAY LẠI làm một phần đầu vào cho chính bước tính tiếp theo (gọi là feedback). Một bộ tích luỹ đơn giản y[n] = y[n-1] + x[n] đã đủ phá vỡ mọi công cụ DTFT quen thuộc — DTFT giả định hệ thống có đáp ứng xung HỮU HẠN, còn hệ đệ quy có thể "nhớ" mãi mãi.

Bài này giới thiệu công cụ tổng quát hơn — Z-transform — rồi khai thác một hình ảnh hình học đẹp đến bất ngờ: chỉ cần biết vị trí vài điểm trên một mặt phẳng, có thể ĐỌC RA đáp ứng tần số bằng MẮT, biết ngay hệ có ổn định hay không, và thậm chí XÂY một máy tạo dao động số chỉ từ đúng 2 điểm. Đây cũng là bài ra mắt flagship demo của cả series — Pole–Zero Filter Lab.


📚 Điều kiện tiên quyết
Bắt buộc: Bài 5 (phổ, số phức) và Bài 9 (khái niệm filter, đáp ứng xung).

1. Z-transform: ngôn ngữ chung của mọi datasheet DSP

Z-transform của 1 tín hiệu $x[n]$ định nghĩa: $X(z) = \sum_n x[n] z^{-n}$, với $z$ là 1 biến PHỨC bất kỳ. Ký hiệu $z^{-1}$ trong mọi sơ đồ khối DSP (kể cả datasheet chip thật) có nghĩa CỐ ĐỊNH: "trễ đúng 1 mẫu" — $z^{-1} X(z)$ tương ứng với tín hiệu $x[n-1]$. Với công cụ này, một phương trình sai phân ĐỆ QUY như y[n] = x[n] + 0.5·y[n-1] viết lại gọn gàng trong miền $z$: $Y(z) = X(z) + 0.5 z^{-1} Y(z)$ — đại số thuần tuý, không cần "theo dõi thời gian" nữa.

2. Hàm truyền H(z) = B(z)/A(z): tên gọi thành hình ảnh

Từ phương trình sai phân, chuyển sang hàm truyền $H(z) = Y(z)/X(z) = B(z)/A(z)$ chỉ mất 3 bước cơ học: lấy Z-transform 2 vế, gom $Y(z)$ một bên $X(z)$ một bên, rồi chia. $B(z)$ (tử số) và $A(z)$ (mẫu số) đều là đa thức theo $z^{-1}$. Nghiệm của TỬ số gọi là zero (tại đó $H(z)=0$ — filter "câm"), nghiệm của MẪU số gọi là pole (tại đó $H(z) \to \infty$ — filter "hét").

z_transform_polyfromroots.js (trích engine dsp-core.js)
// Nguoc lai: cho TRUOC vi tri pole/zero, ra NGAY he so a/b - "ten goi thanh hinh anh"
function polyFromRoots(roots) {
  let coeffs = [{ re: 1, im: 0 }];
  for (const r of roots) {
    // ... khai trien tich (1 - r*z^-1) ...
  }
  return coeffs; // [c0, c1, c2, ...] - luon c0 = 1
}
// Verified: cap lien hop r=0,7, theta=pi/3 -> he so THUC c1=-2r*cos(theta), c2=r^2

3. Hình học kể chuyện: đọc đáp ứng tần số bằng mắt

Đây là insight đẹp nhất của bài: $|H(e^{j\omega})|$ tính bằng tích khoảng cách từ điểm $e^{j\omega}$ (đi bộ quanh vòng tròn đơn vị khi $\omega$ chạy từ 0 tới $\pi$) tới TỪNG zero, chia cho tích khoảng cách tới TỪNG pole:

$$|H(e^{j\omega})| = \dfrac{\prod_k |e^{j\omega} - \text{zero}_k|}{\prod_k |e^{j\omega} - \text{pole}_k|}$$

Không cần tính toán gì — chỉ cần NHÌN: zero nằm ĐÚNG TRÊN vòng tròn tại tần số nào → khoảng cách bằng 0 tại đó → $|H|=0$ → một notch khoét sạch đúng tần số đó. Pole nằm GẦN vòng tròn → khoảng cách nhỏ ở mẫu số → $|H|$ vống lên rất cao → một đỉnh cộng hưởng. Verify bằng self-test: hệ 1 pole tại $z=0,5$ (thực) cho $|H(e^{j0})| = 1/(1-0,5) = 2$ và $|H(e^{j\pi})| = 1/(1+0,5) = 0,667$ — khớp CHÍNH XÁC công thức đại số trực tiếp, không sai số.

4. Ổn định: một quy tắc, nhìn một cái biết ngay

Quy tắc ổn định của hệ đệ quy đơn giản đến bất ngờ: MỌI pole phải nằm TRONG vòng tròn đơn vị ($|pole| < 1$). Verify bằng đáp ứng xung THẬT (dùng iirFilterDirect(), chạy trực tiếp phương trình sai phân — chưa phải dạng tối ưu Direct Form II Transposed của Bài 11, nhưng đủ để thấy đúng hành vi):

Vị trí pole Đáp ứng xung sau 100 mẫu Ý nghĩa
$r = 0,9$ (TRONG vòng tròn) $y[99] \approx 0,00003$ Tắt dần về 0 — ổn định
$r = 1,1$ (NGOÀI vòng tròn) $y[99] \approx 12.528$ Tăng trưởng mũ — filter "nổ"
$r = 1,0$, $\theta=0$ (ĐÚNG trên vòng tròn) $y[99] = 1$ (không đổi) Không tắt, không nổ — tích luỹ mãi mãi
Cặp pole $r=1,0$, $\theta=\pi/4$ (trên vòng tròn) Dao động chu kỳ đúng 8 mẫu, biên độ không đổi Oscillator số — dùng chính hiện tượng "mất ổn định biên" để TẠO SÓNG
🔬 Đào sâu: pole trên vòng tròn = máy tạo dao động số
Một cặp pole liên hợp đặt ĐÚNG trên vòng tròn đơn vị tại góc $\theta$ tạo ra đáp ứng xung là một sóng sin THUẦN TUÝ, tần số chuẩn hoá $\theta/(2\pi)$, KHÔNG BAO GIỜ tắt dần — verify bằng self-test: $\theta=\pi/4$ cho dao động chu kỳ chính xác 8 mẫu, biên độ tại mẫu thứ 80 gần bằng hệt biên độ tại mẫu thứ 8. Đây chính xác là cách nhiều chip DSP thật tạo ra sóng sin số (digital oscillator) mà không cần bảng tra giá trị sin — chỉ cần 2 hệ số nhân và vài phép cộng mỗi mẫu.
⚠️ Cạm bẫy: pole gần vòng tròn = ổn định "trên giấy", nguy hiểm trong thực tế
Một pole tại $r=0,999$ về mặt TOÁN HỌC vẫn ổn định ($|pole|<1$) — nhưng đáp ứng xung tắt CỰC KỲ chậm (hàng nghìn mẫu mới về gần 0), tạo cộng hưởng cực nhọn dễ gây tràn số (overflow) khi cài đặt bằng số học dấu phẩy tĩnh hoặc độ chính xác thấp. Bài 11 (biquad) sẽ gặp lại đúng vấn đề này: lượng tử hoá hệ số có thể đẩy 1 pole ổn định trên giấy trượt ra NGOÀI vòng tròn khi chạy thật trên phần cứng.

5. Thực hành: Pole–Zero Filter Lab

Kéo pole (×, đỏ) và zero (○, xanh) trên mặt phẳng z — mỗi điểm tự động thêm liên hợp phức đối xứng để hệ số luôn là số thực. Đáp ứng tần số và đáp ứng xung cập nhật tức thì; bấm "Nghe" để lọc một đoạn nhiễu trắng bằng CHÍNH bộ lọc đang vẽ. Kéo pole ra ngoài vòng tròn để nghe/thấy filter mất ổn định — biên độ phát ra được TỰ ĐỘNG giới hạn để bảo vệ tai:

🎛️ Pole–Zero Filter Lab — DSPJS thật

Đáp ứng tần số |H(f)| (dB):

Đáp ứng xung y[n]:

Đang khởi tạo…

Kéo × (pole) hoặc ○ (zero) trên mặt phẳng z. Pole ra NGOÀI vòng tròn (viền đỏ) = mất ổn định — vẫn nghe được tiếng "nổ" nhưng biên độ đã bị giới hạn tự động, an toàn cho tai.

polezero_lab_demo.js (đúng logic đang chạy ở tab Xem trước)
import { polyFromRoots, freqRespFromPZ, polesStable, iirFilterDirect, whiteNoise } from './dsp-core.js';

function updateFilter(pole, zero) {
  const poles = [pole, { re: pole.re, im: -pole.im }];   // tu dong lien hop
  const zeros = [zero, { re: zero.re, im: -zero.im }];
  const a = polyFromRoots(poles).map((c) => c.re);        // he so mau so THUC
  const b = polyFromRoots(zeros).map((c) => c.re);        // he so tu so THUC
  const stable = polesStable(poles);

  const freqResp = freqRespFromPZ(zeros, poles, 1, 200);   // ve |H(f)|
  const noise = Array.from({ length: 4000 }, (_, n) => whiteNoise(n, 0.3, 3));
  const filtered = iirFilterDirect(noise, b, a);           // LOC THAT tren nhieu trang

  // an toan: chi giam am luong khi nang luong THAT SU tang truong (dang no),
  // dao dong bien do khong doi (oscillator tren vong tron) van phat binh thuong
  const half = filtered.length / 2;
  const isExploding = maxAbs(filtered.slice(half)) / maxAbs(filtered.slice(0, half)) > 3;
  const peak = maxAbs(filtered);
  const safeAudio = filtered.map((v) => (v / peak) * (isExploding ? 0.12 : 0.5));
  return { a, b, stable, freqResp, safeAudio };
}

Tóm lược

  • ✅ Z-transform $X(z) = \sum x[n] z^{-n}$ tổng quát hoá DTFT — xử lý được cả hệ ĐỆ QUY (feedback) mà FIR/DTFT không đủ sức.
  • ✅ $H(z) = B(z)/A(z)$: zero = nghiệm tử số (filter "câm"), pole = nghiệm mẫu số (filter "hét").
  • ✅ Đọc đáp ứng tần số bằng HÌNH HỌC: tích khoảng cách tới zero ÷ tích khoảng cách tới pole — verified khớp chính xác công thức đại số trực tiếp.
  • ✅ Ổn định ⟺ MỌI pole trong vòng tròn đơn vị — verified: $r=0,9$ tắt dần, $r=1,1$ nổ tới hơn 12.000 lần chỉ sau 100 mẫu, $r=1,0$ dao động period đúng 8 mẫu không tắt không nổ (oscillator số).
  • ✅ Pitfall: pole gần vòng tròn ổn định "trên giấy" nhưng dễ tràn số khi cài đặt thật.
  • ✅ Ra mắt flagship Pole–Zero Filter Lab — nền tảng tái sử dụng ở Bài 11 (Biquad Cookbook).

Trắc nghiệm ôn tập

Câu 1

Vì sao cần Z-transform thay vì chỉ dùng DTFT/DFT như các bài trước?

Câu 2

Trong H(z) = B(z)/A(z), một ZERO nằm ĐÚNG TRÊN vòng tròn đơn vị tại một tần số cụ thể có ý nghĩa gì?

Câu 3

Verified: pole tại r=1,0 (đúng trên vòng tròn), θ=π/4 cho đáp ứng xung dao động period 8 mẫu, KHÔNG tắt dần theo thời gian. Điều này ứng dụng vào đâu?

Câu 4

Verified: pole tại r=0,999 vẫn thoả điều kiện ổn định toán học (|pole|<1). Vì sao đây vẫn là một cạm bẫy thực tế?

Tải file code thực hành minh họa bài học

File JavaScript DSPJS (dsp-core.js) — thư viện DSP tự viết dùng xuyên suốt cả 15 bài, Bài 10 vừa thêm số phức đầy đủ (cộng/trừ/nhân/chia), polyFromRoots(), freqRespFromPZ(), polesStable(), iirFilterDirect(), kèm self-test đối chiếu đúng mọi hành vi trong bài (chạy node dsp-core.js, không cần cài thêm gì):

Tải về dsp-core.js

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 9: Filter FIR: từ trung bình trượt đến windowed-sinc Bài 11: Filter IIR & Biquad Quay lại Lộ trình Series Xử Lý Tín Hiệu Số

Bình luận