Mô phỏng, kiểm thử với scipy.signal và lượng tử hóa fixed-point
Vị trí bài học và mục tiêu
Module 1 đã cho bạn hệ số bộ lọc: b từ firwin/remez cho FIR, hoặc hệ số SOS từ biến đổi song tuyến tính (bilinear transform) cho IIR. Nhưng đó là số thực dấu phẩy động (floating-point) 64 bit, còn FPGA chỉ có số nguyên (integer) với độ rộng bit hữu hạn. Bài này dựng cầu nối giữa hai thế giới đó: mô phỏng kiểm chứng bằng scipy.signal, rồi lượng tử hóa (quantization) sang dấu phẩy tĩnh (fixed-point) và đo xem thiết kế còn đạt đặc tả hay không, trước khi viết dòng Verilog đầu tiên.
Sau bài này, bạn cần:
- Dựng được mô hình tham chiếu (reference model) bằng
scipy.signalvà trích ra các chỉ tiêu đo được. - Viết bộ kiểm thử với tín hiệu chuẩn và ngưỡng chấp nhận rõ ràng.
- Chuyển hệ số, dữ liệu sang Q-format và tính ngân sách bit cho thanh ghi tích lũy (accumulator).
- So sánh đáp ứng lý tưởng với bản fixed-point, chỉ ra nguyên nhân sai lệch.
- Phát hiện sớm lỗi do lượng tử hóa, thay vì phát hiện trên bo mạch.
Mô hình tham chiếu dựng bằng scipy.signal
Nguyên tắc phương pháp luận: mọi so sánh chỉ có nghĩa khi có chuẩn. Mô hình float64 chính là chuẩn đó, và nó phải được lưu lại (hệ số, đáp ứng tần số, đầu ra mẫu) để mọi phiên bản sau này đối chiếu.
import numpy as np
from scipy import signal
fs = 48_000.0
b = signal.firwin(63, 4000, fs=fs, window="hamming") # FIR tham chiếu
w, H = signal.freqz(b, worN=8192, fs=fs)
mag_db = 20 * np.log10(np.abs(H) + 1e-12)
f_pass, f_stop = 3500, 5000
print("gợn dải thông (dB):", 20*np.log10(np.abs(H[w <= f_pass]).max()))
print("suy giảm dải chắn (dB):", -20*np.log10(np.abs(H[w >= f_stop]).max()))
Với IIR, hãy giữ dạng SOS (signal.sosfreqz, signal.sosfilt) làm chuẩn, vì đó cũng là dạng bạn nên hiện thực trên phần cứng.
Bài tập 1 (theo mạch capstone). Bộ lọc thông thấp 63 tap, fs = 48 kHz, cắt tại 4 kHz. Viết ba dòng lệnh in ra suy giảm tại 6 kHz và độ gợn lớn nhất trong [0, 3.5 kHz].
Xem gợi ý
Dùng signal.freqz(b, worN=8192, fs=fs) rồi lấy w, H. Suy giảm tại 6 kHz: nội suy np.abs(H) tại w == 6000 hoặc lấy mẫu gần nhất; độ gợn: 20*np.log10(np.abs(H[w <= 3500]).max()). Điều quan trọng là chốt ngưỡng số (ví dụ ≥ 50 dB, gợn ≤ 0.1 dB) trước khi lượng tử hóa, để sau này có cái mà so.
Bộ kiểm thử: tín hiệu chuẩn và chỉ tiêu đo
Mỗi tín hiệu chuẩn trả lời một câu hỏi khác nhau:
| Tín hiệu | Câu hỏi kiểm chứng |
|---|---|
| Xung đơn vị $\delta[n]$ | Đáp ứng xung (impulse response) có khớp hệ số thiết kế? |
| Sin tại tần số dải thông | Biên độ có bị suy hao quá mức? |
| Sin tại tần số dải chắn | Có bị rò rỉ (leakage) quá ngưỡng? |
| Nhiễu trắng | Mật độ phổ công suất (PSD) đầu ra có đúng hình dạng? |
Cách làm chặt chẽ: chạy cùng một tín hiệu qua mô hình float và mô hình fixed-point, rồi báo cáo sai số theo RMS và theo dB. Đừng chỉ nhìn đồ thị bằng mắt.
Bài tập 2. Capstone yêu cầu SNR ≥ 60 dB sau lượng tử hóa, dữ liệu 12 bit. Có đạt không?
Xem gợi ý
Theo lý thuyết, SQNR ≈ 6.02 × 12 + 1.76 ≈ 74 dB, nên về nguyên tắc đạt. Nhưng đó là trần lý thuyết cho bộ lượng tử đơn thuần; hệ số lượng tử, làm tròn trong tích lũy và nhiễu đầu vào thực tế sẽ ăn mất biên dự trữ. Kết luận phải dựa trên mô phỏng so sánh, không dựa trên công thức.
Lượng tử hóa fixed-point: Q-format và ngân sách bit
Định dạng Qm.n dành m bit cho phần nguyên (gồm bit dấu) và n bit cho phần phân số. Bước lượng tử (LSB) là _____. Với số nguyên có dấu 16 bit và đỉnh chuẩn hóa về ±1, bạn có Q1.15. Với 18 bit, Q1.17. Hệ số FIR thường được chuẩn hóa đỉnh về _____ trước khi lượng tử để dùng hết dải động.
Sai số lượng tử cứ mỗi bit thêm vào cải thiện chất lượng khoảng 6 dB:
$$ SQNR \approx 6.02B + 1.76 \text{ dB} $$
Với bộ lọc FIR N tap, dữ liệu B_x bit và hệ số B_h bit, thanh ghi tích lũy cần tối thiểu _____ bit để không tràn. Ví dụ 128 tap: thêm _____ bit bảo vệ (guard bits). Khi buộc phải tràn, hãy chọn _____ thay vì tràn vòng (wrap-around), vì tràn vòng biến một mẫu lớn thành một mẫu lớn ngược dấu, tạo tiếng "tách" rất khó chẩn đoán.
Bài tập 3. FIR 128 tap, dữ liệu 12 bit, hệ số 16 bit. Cần bao nhiêu bit tích lũy?
Xem gợi ý
12 + 16 + ceil(log2 128) = 12 + 16 + 7 = 35 bit. Trên FPGA, đó là lý do các bộ lọc thường dùng tích lũy 36 hoặc 48 bit thay vì 32 bit.
Mô phỏng fixed-point và so sánh với mô hình lý tưởng
Bước cuối là mô phỏng lại toàn bộ đường tín hiệu bằng số nguyên, mô phỏng đúng những gì phần cứng sẽ làm: làm tròn (round) hay cắt (truncate), dịch bit, bão hòa.
F = 14 # Q1.14 cho hệ số
h_q = np.round(b / np.abs(b).max() * (2**F - 1)).astype(np.int64)
w2, H_q = signal.freqz(h_q / 2**F, worN=8192, fs=fs)
suy_giam_ly_tuong = -20*np.log10(np.abs(H[w >= f_stop]).max())
suy_giam_fixed = -20*np.log10(np.abs(H_q[w2 >= f_stop]).max())
print(suy_giam_ly_tuong, suy_giam_fixed)
Ba lỗi lượng tử hóa điển hình cần nhận diện sớm:
- Lệch đáp ứng do lượng tử hệ số. Với FIR, sai lệch thường nhỏ. Với IIR bậc cao, nghiệm (pole) gần vòng tròn đơn vị dịch rất mạnh khi hệ số đổi một LSB, có thể làm mất ổn định. Cách chữa: dùng dạng SOS hoặc cascade các khối bậc hai.
- Nhiễu làm tròn tích lũy. Xuất hiện dưới dạng nền nhiễu tăng trong dải chắn; đo bằng PSD đầu ra khi đầu vào là nhiễu trắng.
- Dao động giới hạn (limit cycle). Đầu ra tự dao động nhỏ khi đầu vào bằng 0, do làm tròn trong vòng hồi tiếp. Kiểm tra bằng cách cho đầu vào 0 sau một đoạn tín hiệu.
Bài tập 4. Sau khi lượng tử hệ số sang Q1.14, suy giảm dải chắn tụt từ 52 dB xuống 41 dB. Bạn xử lý thế nào?
Xem gợi ý
Theo thứ tự chi phí tăng dần: (1) kiểm tra lại chuẩn hóa hệ số, đừng để hệ số lớn nhất chạm trần; (2) tăng số bit phân số cho hệ số (Q1.17 với DSP slice 18 bit); (3) thiết kế lại với cửa sổ cho suy giảm dư biên (ví dụ đặt mục tiêu 60 dB để sau lượng tử còn 52 dB); (4) chuyển sang cấu trúc cascade. Ghi lại từng thử nghiệm vào nhật ký mô phỏng, vì đây chính là phần "phương pháp" trong báo cáo capstone.
Thẻ ôn tập các khái niệm chốt
Front: Vì sao phải dựng mô hình tham chiếu float trước khi lượng tử hóa? Back: Để có chuẩn so sánh; mọi sai lệch của bản fixed-point đều đo được so với nó.
Front: Hàm scipy.signal thiết kế FIR theo phương pháp cửa sổ? Back: signal.firwin.
Front: Hàm scipy.signal thiết kế FIR theo Parks-McClellan? Back: signal.remez.
Front: Hàm đo đáp ứng tần số của bộ lọc số? Back: signal.freqz, hoặc signal.sosfreqz cho dạng SOS.
Front: Bước lượng tử của định dạng Qm.n? Back: 2^(-n), với n là số bit phần phân số.
Front: SQNR xấp xỉ của bộ lượng tử B bit? Back: 6.02B + 1.76 dB.
Front: Số bit tích lũy tối thiểu cho FIR N tap, dữ liệu B_x bit, hệ số B_h bit? Back: B_x + B_h + ceil(log2 N).
Front: Hai cách xử lý khi tích lũy tràn? Back: Tràn vòng (wrap-around) và bão hòa (saturation); nên dùng bão hòa.
Front: Làm tròn (round) khác cắt (truncate) ở điểm nào? Back: Round giảm sai số trung bình và không tạo lệch DC; truncate gây lệch DC.
Front: Vì sao IIR bậc cao nhạy với lượng tử hệ số? Back: Nghiệm gần vòng tròn đơn vị dịch mạnh khi hệ số đổi một LSB; nên dùng SOS hoặc cascade.
Front: Dao động giới hạn (limit cycle) là gì? Back: Dao động tự duy trì ở đầu ra do làm tròn trong vòng hồi tiếp khi đầu vào bằng 0.
Front: Tín hiệu chuẩn để đo đáp ứng xung? Back: Xung đơn vị delta[n].
Front: Chỉ tiêu nào cho biết mức rò rỉ dải chắn? Back: Suy giảm dải chắn, tính bằng -20log10|H| tại tần số dải chắn.
Cạm bẫy thường gặp khi lượng tử hóa
- Đừng lượng tử hóa hệ số trước khi kiểm tra mô hình float. Nếu bản float đã trượt đặc tả, lượng tử hóa chỉ làm mọi thứ khó chẩn đoán hơn.
- Luôn giữ biên dự trữ (margin): thiết kế nhắm 60 dB để sau lượng tử còn 50 dB, chứ đừng thiết kế đúng 50 dB rồi hi vọng.
- Đừng quên lượng tử cả dữ liệu đầu vào, không chỉ hệ số. Sai số đầu vào thường là nguồn nhiễu lớn hơn bạn tưởng.
- Round tốt hơn truncate trong hầu hết trường hợp, nhưng round tốn tài nguyên hơn; hãy để mô phỏng quyết định, không phải cảm tính.
- Ghi lại mọi cấu hình bit (B_x, B_h, F, độ rộng tích lũy) trong một bảng duy nhất. Bảng này sẽ đi thẳng vào báo cáo capstone và vào phần khai báo tham số Verilog ở bài 3.
Chốt bài và bước sang kiến trúc phần cứng
Ba điểm mang theo: (1) mô hình tham chiếu float là chuẩn, fixed-point chỉ là bản xấp xỉ có đo đạc; (2) ngân sách bit phải tính từ đặc tả và từ công thức tích lũy, không chọn tùy ý; (3) mọi lỗi lượng tử hóa phải lộ ra trong mô phỏng, không phải trên bo mạch.
Bài tiếp theo sẽ lấy đúng bảng tham số bit bạn vừa chốt và biến nó thành kiến trúc phần cứng: đường ống (pipeline) và song song tài nguyên (resource parallelism) để đạt thông lượng mẫu yêu cầu.