Mở đầu: Cùng 1 dòng code, bấm nút 2 lần, ra 2 con chip khác nhau
Lập trình viên phần mềm quen với một niềm tin ngầm định: build lại cùng 1 source code phải luôn ra cùng 1 kết quả (bỏ qua các yếu tố ngoại lai như phiên bản compiler). Nhưng chạy toàn bộ flow FPGA — từ SystemVerilog tới bitstream nạp vào chip — 2 lần liên tiếp, không đổi 1 ký tự nào — hoàn toàn có thể cho ra 2 file bitstream khác nhau, với $f_{max}$ đo được khác nhau vài phần trăm. Đây không phải bug của tool. Đây là hệ quả tất yếu của một sự thật: bài toán đặt khối logic vào đúng vị trí trên chip (placement) và nối dây giữa chúng (routing) thuộc lớp bài toán NP-hard — không có thuật toán nào tìm ra lời giải tối ưu tuyệt đối trong thời gian hợp lý, nên mọi tool thực tế đều dùng heuristic có yếu tố ngẫu nhiên.
Bài này đi hết chặng đường từ RTL bạn viết tới bitstream nạp vào chip thật: synthesis (RTL → lưới LUT), ràng buộc timing SDC — và cạm bẫy kinh điển khi thiếu nó, placement/routing và lý do kết quả không tất định, cách đọc timing report thật (WNS/TNS) để biết sửa RTL hay sửa ràng buộc, và toolchain mã nguồn mở Yosys → nextpnr → bitstream đủ để nạp vào board FPGA giá rẻ thật. Cuối bài, bạn tự tay đặt khối lên Sa bàn Place & Route và so với thuật toán tự động — xem chính mắt vì sao "chạy lại vẫn ra số khác".
Tài nguyên ngoài: HDLBits, ChipVerify, EDA Playground.
1. Synthesis: Từ RTL tới lưới LUT
Synthesis (tổng hợp logic) là bước dịch SystemVerilog thành một lưới (netlist) các LUT — đúng khái niệm LUT đã học ở Bài 9. Ba việc tool tự động làm:
| Bước | Việc tool làm | Tương đương thủ công bạn đã học |
|---|---|---|
| Elaboration | Đọc SystemVerilog, dựng cây cú pháp/netlist "generic" (chưa gắn công nghệ cụ thể) | Giống bước VeriLiteParser.parse() của engine trang này, nhưng đầy đủ chuẩn IEEE |
| Tối giản logic | Constant propagation, loại bỏ logic thừa, gộp biểu thức Boolean tương đương | Chính là bìa Karnaugh bạn làm tay ở Series Điện Tử — chỉ khác quy mô hàng nghìn biến |
| Technology mapping | Gói từng cụm logic tối giản vào đúng LUT-k của FPGA đích (vd LUT-4, LUT-6) | Đúng bước "nạp bảng chân lý vào LUT Explorer" ở Bài 9, làm tự động cho toàn bộ thiết kế |
Kết quả sau synthesis là 1 file netlist (thường dạng JSON hoặc EDIF) liệt kê hàng trăm-hàng nghìn LUT và dây nối giữa chúng — nhưng chưa biết LUT nào nằm ở đâu trên chip. Đó là việc của placement (Mục 3).
$ yosys -p "read_verilog -sv rca4.sv; synth_ice40 -top rca4 -json rca4.json"
2.1. Executing Verilog-2005 frontend: rca4.sv
2.25. Executing SYNTH_ICE40 pass.
Executing OPT_EXPR pass (perform const folding).
Executing OPT_CLEAN pass (remove unused cells and wires).
Executing ABC pass (technology mapping using ABC).
ABC RESULTS: $lut cells: 8
Number of cells: 8 $lut
Định dạng log trên đúng thật với Yosys — số lượng LUT cụ thể tuỳ phiên bản tool và cách ABC tối giản,
nhưng dễ nhẩm bằng tay: mỗi full adder trong rca4 (Bài 6) cần 2 hàm tổ hợp độc lập (sum
3 đầu vào và carry 3 đầu vào) — mỗi hàm gói vừa 1 LUT-4, nên 4 full adder tối thiểu cần
8 LUT, không phải 1 LUT cho mỗi cổng AND/XOR bạn gõ tay trong code.
2. Ràng buộc timing SDC: Vì sao "không lỗi" chưa chắc là "đúng"
SDC là file ràng buộc timing bạn viết tay, khai báo cho tool biết mục tiêu tốc độ của thiết kế — không tool nào tự đoán được bạn muốn chạy ở 50MHz hay 500MHz. Không có SDC, câu hỏi "mạch có đủ nhanh không" hoàn toàn vô nghĩa vì không có mốc nào để so sánh.
Nếu quên khai báo create_clock, STA không có chu kỳ mục tiêu nào để tính slack — nhiều
tool trong tình huống này báo cáo "không có vi phạm" chỉ vì
không có gì được kiểm tra cả, không phải vì thiết kế đủ nhanh. Đây là cạm bẫy nguy
hiểm nhất của người mới: đọc báo cáo "PASS" rồi yên tâm nạp bitstream lên board thật, để rồi mạch chạy
sai vì thực chất chưa từng được xác minh timing.
// ❌ SAI: không có create_clock nào — STA không biết "đủ nhanh" nghĩa là
// gì, báo cáo "no timing violations" ở đây là VÔ NGHĨA, không phải bằng
// chứng mạch chạy đúng tần số bạn định dùng.
// (file .sdc trống, hoặc chỉ có set_input_delay/set_output_delay lẻ tẻ)
// ✅ ĐÚNG: khai báo rõ mục tiêu 100 MHz + trễ tín hiệu vào/ra thực tế
create_clock -name clk -period 10.0 [get_ports clk] ;# 10ns = 100 MHz
set_input_delay 2.0 -clock clk [get_ports {a0 a1 a2 a3 b0 b1 b2 b3 cin}]
set_output_delay 2.0 -clock clk [get_ports {s0 s1 s2 s3 cout}]
Với bản ✅, nếu rca4 (chưa pipeline, xem Bài 8) không kịp
chạy ở 100MHz, STA sẽ báo slack âm — đúng lúc bạn cần biết, trước khi tốn thời gian
nạp lên board thật.
3. Placement & Routing: bài toán NP-hard, và vì sao chạy lại ra số khác
Placement quyết định mỗi LUT/CLB trong netlist nằm ở toạ độ nào trên lưới vật lý của chip; routing tìm đường dây thực tế nối các CLB đó qua mạng routing (đã học ở Bài 9 Mục 4). Số cách sắp xếp N khối lên M vị trí tăng theo giai thừa — với vài nghìn CLB, không gian tìm kiếm lớn hơn số nguyên tử trong vũ trụ quan sát được. Không tool nào duyệt hết; tất cả dùng heuristic (annealing giả lập, thuật toán tham lam, phân vùng đệ quy...) khởi tạo từ 1 điểm ngẫu nhiên rồi cải thiện dần.
Đây chính là lý do nêu ở đầu bài: seed ngẫu nhiên khác nhau → kết quả placement khác nhau → tổng chiều dài dây khác nhau → độ trễ routing khác nhau → $f_{max}$ đo được khác nhau — dù netlist đầu vào giống hệt. Placement tốt (các khối có liên kết chặt được đặt gần nhau) giảm chiều dài dây, giảm độ trễ routing, tăng $f_{max}$; placement tệ (khối liên kết chặt bị đặt xa nhau) làm ngược lại — congestion (tắc nghẽn routing) xảy ra khi quá nhiều dây phải chen qua cùng 1 vùng kênh routing hẹp, buộc router phải đi đường vòng dài hơn.
4. Đọc timing report thật: WNS, TNS và cách lần theo failing path
Sau P&R, tool xuất báo cáo timing liệt kê slack của mọi đường tín hiệu (path) trong thiết kế. Hai con số tóm tắt quan trọng nhất:
$$WNS = \min_i(\text{slack}_i) \qquad TNS = \sum_{i:\ \text{slack}_i < 0} \text{slack}_i$$
WNS (Worst Negative Slack) — slack xấu nhất trong toàn thiết kế, chỉ đúng 1 con số cho biết "chỗ tệ nhất tệ tới đâu". TNS (Total Negative Slack) — tổng tất cả slack âm cộng lại, cho biết "có bao nhiêu đường đang vi phạm, tổng thiệt hại lớn cỡ nào" (1 đường vi phạm nặng và 100 đường vi phạm nhẹ có thể cho cùng WNS nhưng TNS rất khác nhau).
| Path | Slack (ns) | Ghi chú |
|---|---|---|
| reg_a → reg_sum | +0.8 | Đạt (positive slack) |
| reg_b → reg_cout | −1.2 | Vi phạm — đây là WNS (âm nhất) |
| reg_c → reg_s2 | −0.3 | Vi phạm nhẹ hơn |
| reg_d → reg_s3 | +0.1 | Đạt, nhưng sát ngưỡng |
Với bảng trên: $WNS = -1.2\text{ns}$, $TNS = -1.2 + (-0.3) = -1.5\text{ns}$. Quy trình debug chuẩn: mở path có WNS trước tiên — nếu độ trễ dồn phần lớn vào 1 khối logic tổ hợp quá sâu (nhiều tầng cổng nối tiếp), sửa RTL (thêm tầng pipeline, giống Bài 8); nếu độ trễ dồn phần lớn vào routing (dây dài bất thường dù logic đơn giản), khả năng cao là placement tệ — thử chạy lại P&R với seed khác, hoặc ràng buộc vị trí (floorplan) thủ công cho đúng cụm đó.
Startpoint: reg_b (posedge clk)
Endpoint: reg_cout (posedge clk)
Path Group: clk
Path Type: max
Delay type Incr Path
---------- ------ ------
clock clk 0.00 0.00
reg_b/CLK 0.00 0.00 r
reg_b/Q 0.45 0.45 f <- t_clk->q
logic 10.25 10.70 <- 4 tang LUT noi tiep (routing + comb)
reg_cout/D 0.00 10.70
----------------------------
data arrival time 10.70
clock clk (rise) 10.00 10.00
clock uncertainty -0.20 9.80
reg_cout/setup -0.30 9.50
----------------------------
data required time 9.50
slack (VIOLATED) -1.20 <- required 9.50 - arrival 10.70, day chinh la WNS o bang tren
Báo cáo timing thật (định dạng trên phổ biến ở cả tool thương mại lẫn nextpnr) luôn tách 2 nhánh:
data arrival time (tín hiệu thực sự mất bao lâu để tới đích) và
data required time (deadline suy ra từ chu kỳ clock trừ setup/uncertainty) — slack chính
là hiệu số giữa 2 nhánh, và dòng logic ở giữa cho biết ngay độ trễ đó đến từ đâu (bao nhiêu
tầng LUT, có kèm routing hay không) để quyết định sửa RTL hay sửa constraint như trên.
5. Toolchain mã nguồn mở: Yosys → nextpnr → bitstream thật
Không cần license thương mại đắt đỏ để đi hết flow này — hệ sinh thái mã nguồn mở đã đủ trưởng thành cho nhiều dòng FPGA giá rẻ (Lattice iCE40, ECP5):
# 1. Synthesis: SystemVerilog -> netlist JSON (LUT cho iCE40)
yosys -p "read_verilog -sv rca4.sv; synth_ice40 -top rca4 -json rca4.json"
# 2. Place & Route: gán vị trí thật + đọc ràng buộc SDC + chân board (PCF)
nextpnr-ice40 --hx8k --json rca4.json --pcf rca4.pcf --sdc rca4.sdc --asc rca4.asc
# 3. Sinh bitstream nhị phân nạp được vào chip
icepack rca4.asc rca4.bin
# 4. Nạp vào board FPGA thật qua cổng USB
openFPGALoader -b ice40_generic rca4.bin
4 dòng lệnh này chính là bản mã nguồn mở của quy trình thương mại (Vivado/Quartus) — cùng khái niệm
synthesis → P&R → bitstream, chỉ khác công cụ. nextpnr in ra đúng loại báo cáo WNS/TNS ở Mục
4 sau bước 2; nếu không có board FPGA thật, bạn vẫn chạy được bước 1-3 để tự đọc báo cáo timing.
6. Demo tương tác: Sa bàn Place & Route
6 khối logic (A-F) cần đặt lên lưới 5×4, với các liên kết cố định giữa chúng (mô phỏng 1 cụm netlist nhỏ). Kéo-thả (bấm chọn khối rồi bấm ô trống) để tự đặt thủ công, hoặc bấm "Auto-place" nhiều lần để thấy thuật toán tự động — có yếu tố ngẫu nhiên trong thứ tự xử lý — cho ra tổng chiều dài dây (và $f_{max}$ ước lượng) khác nhau mỗi lần chạy, đúng hiện tượng mô tả ở Mục 3.
Bấm 1 khối ở hàng trên rồi bấm 1 ô lưới để đặt (hoặc đổi chỗ khối đã đặt). Bấm Auto-place nhiều lần liên tiếp — chú ý con số tổng chiều dài dây/f_max đổi mỗi lần dù cùng 1 thuật toán, đúng lý do STA mini ở Mục 3.
Tóm lược
Bài này khép lại chặng đường từ RTL tới silicon thật:
- ✅ Synthesis: RTL → lưới LUT, gồm elaboration, tối giản Boolean (Karnaugh tự động), technology mapping.
-
✅ SDC: bắt buộc khai báo
create_clock— thiếu nó, "timing MET" là báo cáo vô nghĩa, không phải bằng chứng mạch đủ nhanh. - ✅ Placement & Routing: bài toán NP-hard, giải bằng heuristic có seed ngẫu nhiên — cùng netlist, 2 lần chạy có thể cho 2 kết quả $f_{max}$ khác nhau.
- ✅ WNS/TNS: WNS = slack xấu nhất (1 con số), TNS = tổng mọi slack âm (mức độ nghiêm trọng tổng thể) — lần theo path WNS trước để quyết định sửa RTL hay sửa constraint.
- ✅ Toolchain mã nguồn mở: Yosys (synthesis) → nextpnr (P&R) → icepack (bitstream) → openFPGALoader (nạp board thật) — không cần license thương mại để đi hết flow.
Trắc nghiệm ôn tập
Câu 1
Vì sao chạy lại đúng 1 netlist qua P&R 2 lần có thể cho 2 kết quả f_max khác nhau?
Câu 2
Nếu quên khai báo create_clock trong file SDC, hậu quả gì xảy ra?
Câu 3
WNS (Worst Negative Slack) và TNS (Total Negative Slack) khác nhau ở điểm nào?
Câu 4
Trong toolchain mã nguồn mở Yosys → nextpnr → icepack, bước nào chịu trách nhiệm placement & routing?
Tải file code thực hành minh họa bài học
File SystemVerilog gồm rca4 (bộ cộng dùng xuyên suốt Bài 6/8/9) kèm testbench, cộng ví dụ
file SDC ❌ thiếu constraint vs ✅ đủ constraint dạng comment tham khảo, và toàn bộ 4 lệnh flow mã nguồn
mở để tự chạy thật:
Bình luận