Mở đầu: Con chip đúc xong từ 5 năm trước "biến hình" thành mạch bạn vừa nghĩ ra tối nay
Toàn bộ series đến giờ có một nghịch lý chưa được trả lời. Bài 6 và
Bài 8 đều nói về những cổng logic, chuỗi carry, đường timing — như thể chúng
là dây đồng và transistor được khắc cố định vào silicon. Nhưng bạn viết SystemVerilog, nhấn nút, và vài
phút sau mạch của bạn chạy thật trên một con FPGA — một con chip được sản xuất hàng loạt từ nhiều
năm trước, khi thiết kế của bạn còn chưa tồn tại. Không ai khắc lại silicon. Vậy bộ cộng
rca4 của bạn "nằm" ở đâu trên con chip đó?
Câu trả lời là một trong những ý tưởng đẹp nhất của kỹ thuật số: đừng chế tạo cổng logic — hãy chế tạo bộ nhớ chứa bảng chân lý của cổng logic. Bài này mổ xẻ kiến trúc FPGA từ đơn vị nhỏ nhất: LUT (bảng tra cứu trong SRAM), khối CLB với carry chain chuyên dụng (lý do adder FPGA nhanh bất ngờ — nối thẳng vào Bài 6 và Bài 8), các khối "cứng hoá" BRAM/DSP cùng cạm bẫy inference khiến tool âm thầm phung phí tài nguyên, và mạng routing chiếm phần lớn diện tích lẫn độ trễ của cả con chip. Cuối bài, LUT Explorer cho bạn tự tay nạp bảng chân lý vào một LUT-4 và map bộ cộng 4-bit lên lưới CLB có carry chain.
rca4 được map lên lưới CLB trong demo cuối bài;
Series Điện Tử Bài 12 (Cổng logic & bảng chân lý)
— bài này giả định bạn đọc thạo bảng chân lý.
Tài nguyên ngoài: HDLBits, ChipVerify, EDA Playground.
1. LUT: mọi hàm logic k đầu vào là một bảng chân lý — và bảng chân lý là bộ nhớ
Nhìn lại bất kỳ hàm logic nào của Bài 2: dù viết bằng
assign, always_comb hay lồng bao nhiêu toán tử, một hàm $k$ đầu vào
1 đầu ra rốt cuộc chỉ là một bảng chân lý với $2^k$ hàng. Ý tưởng gốc của
FPGA: thay vì nối cổng AND/OR/XOR vật lý theo từng thiết kế, hãy đặt sẵn một khối SRAM tí hon
$2^k$ bit — gọi là LUT-k (Look-Up Table) — và dùng $k$ đầu vào
làm địa chỉ đọc:
- Nạp cột "output" của bảng chân lý vào $2^k$ ô SRAM (lúc cấu hình FPGA).
- Khi mạch chạy, tổ hợp đầu vào chọn đúng 1 ô — giá trị đọc ra chính là kết quả của hàm. Không có cổng nào "tính toán" cả: chỉ là một lần tra bảng.
Một LUT-4 chứa $2^4 = 16$ bit, biểu diễn được $2^{16} = 65\,536$ hàm khác nhau — từ AND-4, XOR-4 cho tới bất kỳ hàm "quái dị" nào bạn nghĩ ra. Đổi hàm = ghi lại 16 bit SRAM, không đổi một sợi dây vật lý nào. Đó là nghĩa đen của "phần cứng lập trình lại được". Hai hàm quen thuộc nhất của series nhìn dưới dạng LUT:
// Full adder của Bài 6, tách làm 2 hàm 3 đầu vào — mỗi hàm vừa khít 1 LUT.
// Chạy được trên VeriLite. Sau khi synthesis cho FPGA, MỖI module dưới đây
// biến thành đúng 1 LUT với 8 bit nội dung (nạp lên LUT-4 thì nửa trên
// lặp lại nửa dưới vì đầu vào thứ 4 không dùng — xem demo Mục 6):
module fa_sum (
input logic a, b, cin,
output logic s
);
assign s = a ^ b ^ cin; // nội dung LUT: 0x96 (10010110)
endmodule
module fa_carry (
input logic a, b, cin,
output logic co
);
assign co = (a & b) | (cin & (a ^ b)); // nội dung LUT: 0xE8 (11101000)
endmodule
Chú ý điều quan trọng: cả 2 module trên khi tổng hợp cho FPGA có cùng độ trễ — 1 lần tra
LUT — dù fa_carry "trông" phức tạp hơn (4 toán tử so với 2). Trên FPGA, độ phức tạp của biểu
thức bên trong giới hạn k đầu vào là miễn phí; thứ tốn kém là khi hàm cần
nhiều hơn k đầu vào, buộc tool phải ghép nhiều LUT nối tiếp — mỗi tầng LUT cộng thêm độ
trễ tra bảng và tệ hơn nữa, độ trễ dây nối giữa chúng (Mục 4).
2. CLB: LUT + Flip-flop + Carry chain — viên gạch của cả con chip
LUT một mình chỉ làm được logic tổ hợp. Ghép nó với 1 flip-flop (để chốt kết quả theo clock — chính là
always_ff của Bài 3) và một ít logic phụ trợ, ta được đơn
vị lặp lại phủ kín con chip: CLB (Configurable Logic Block — Xilinx/AMD gọi phần con là
slice, Intel/Altera gọi là ALM; bài dùng thuật ngữ chung CLB):
Sơ đồ khối 1 CLB đơn giản hoá: LUT-4 tính logic tổ hợp, MUX chọn kết quả đi thẳng ra (mạch tổ hợp) hoặc qua D-FF (mạch tuần tự). Khối CARRY màu cam có dây nối thẳng đứng riêng lên/xuống CLB kề — không đi qua mạng routing chung.
Khối CARRY chính là câu trả lời cho một khúc mắc từ Bài 6: chuỗi carry của ripple-carry adder tưởng chừng rất chậm ($O(n)$ tầng), nhưng adder trên FPGA thực tế lại nhanh đáng kinh ngạc. Lý do: các hãng FPGA biết trước rằng phép cộng xuất hiện trong hầu hết mọi thiết kế, nên họ "cứng hoá" đường carry — mỗi CLB có sẵn logic carry và dây nối trực tiếp tới CLB ngay trên nó, bỏ qua hoàn toàn mạng routing chung vốn chậm (Mục 4). Theo mô hình arrival time của Bài 8: một nấc carry chain chỉ tốn cỡ 0,1 ns, trong khi đi vòng qua routing chung tốn cỡ 0,8 ns cho mỗi bước nhảy — demo Mục 6 cho bạn bật tắt giữa 2 chế độ này và xem chênh lệch 6 lần trên cùng một bộ cộng.
Muốn "kích hoạt" carry chain, bạn không cần viết gì đặc biệt — chỉ cần dùng toán tử +:
// Chạy được trên VeriLite. Với tool FPGA (Yosys, Vivado...), đúng 1 dòng
// cộng này được map thẳng vào carry chain chuyên dụng — KHÔNG phải chuỗi
// LUT nối qua routing. So sánh với rca4 viết tay từng cổng ở Bài 6:
// chức năng giống hệt, nhưng dạng "+" cho tool toàn quyền chọn tài nguyên
// tốt nhất — thêm 1 lý do để viết RTL mô tả Ý ĐỊNH thay vì cấu trúc.
module adder4_plus (
input logic [3:0] a, b,
input logic cin,
output logic [4:0] sum5 // bit 4 là carry-out
);
assign sum5 = a + b + cin;
endmodule
3. BRAM & DSP slice: tài nguyên "cứng hoá" sẵn — và cạm bẫy khiến tool từ chối dùng chúng
Cùng logic đó — "thứ gì ai cũng cần thì cứng hoá sẵn" — FPGA hiện đại rải khắp chip hai loại khối chuyên dụng:
-
BRAM: các khối SRAM lớn (18–36 Kbit mỗi khối) — chính là nơi module
ram_infercủa Bài 7 được map vào. Xây RAM 256×8 bằng LUT + FF rời rạc sẽ nuốt hàng trăm CLB; một góc BRAM làm xong việc đó "miễn phí". -
DSP slice: khối nhân–cộng cứng (multiply-accumulate). Bộ nhân
mult4_arraycủa Bài 6 (assign product = a * b;) trên FPGA thật không sinh ra lưới cổng AND + cây cộng nào cả — tool ném thẳng phép nhân vào 1 DSP slice, nhanh hơn và tiết kiệm hơn bất kỳ cách ghép LUT nào.
Điểm mấu chốt của cả hai: bạn không gọi tên BRAM hay DSP trong code. Tool suy luận (inference) từ khuôn mẫu RTL — đúng như Bài 7 đã dạy với RAM. Và vì là suy luận theo khuôn mẫu, chỉ cần code lệch khỏi khuôn một chút, tool sẽ lặng lẽ quay về ghép LUT + FF rời rạc, phung phí tài nguyên gấp trăm lần mà không hề báo lỗi:
BRAM vật lý không có chân reset cho từng ô nhớ — nó là khối SRAM đúc sẵn. Nếu RTL của bạn mô tả một mảng bộ nhớ mà toàn bộ nội dung bị reset đồng loạt, tool không thể dùng BRAM (vì phần cứng đó không tồn tại) và buộc phải xây mảng bằng flip-flop rời — thứ DUY NHẤT reset được đồng loạt:
// ⚠️ Ngoài subset VeriLite (mảng bộ nhớ) — chạy thật trên Verilator/Yosys.
// ❌ SAI: reset quét sạch cả mảng → tool KHÔNG map được vào BRAM,
// sinh ra 256×8 = 2048 flip-flop + rừng LUT giải mã địa chỉ.
module ram_bad (
input logic clk, rst, we,
input logic [7:0] addr, din,
output logic [7:0] dout
);
logic [7:0] mem [0:255];
integer i;
always_ff @(posedge clk) begin
if (rst)
for (i = 0; i < 256; i = i + 1) mem[i] <= 0; // ← thủ phạm
else if (we)
mem[addr] <= din;
dout <= mem[addr];
end
endmodule
// ✅ ĐÚNG: không reset nội dung mảng — khớp khuôn mẫu BRAM (Bài 7).
// Cần giá trị khởi tạo? Dùng initial/$readmemh — BRAM hỗ trợ nạp nội
// dung lúc cấu hình FPGA, chỉ không hỗ trợ reset lúc chạy.
module ram_good (
input logic clk, we,
input logic [7:0] addr, din,
output logic [7:0] dout
);
logic [7:0] mem [0:255];
always_ff @(posedge clk) begin
if (we) mem[addr] <= din;
dout <= mem[addr];
end
endmodule
Cách tự kiểm tra: sau synthesis, đọc báo cáo sử dụng tài nguyên (utilization report). RAM 256×8 mà thấy 0 BRAM + hàng nghìn FF là dính bẫy này. Cùng họ với bài học Bài 7 (đọc đồng bộ để map BRAM) — inference là hợp đồng khuôn mẫu giữa bạn và tool, không phải phép màu.
// Chạy được trên VeriLite (chức năng). Trên FPGA thật: 1 phép nhân dạng
// "*" được map vào DSP slice cứng — kiểm tra utilization report sẽ thấy
// "DSP: 1" thay vì vài chục CLB. Phép nhân rộng hơn DSP (vd 64×64 trên
// DSP 18×25) được tool tự ghép từ NHIỀU DSP + carry chain.
module dsp_mult (
input logic [3:0] a, b,
output logic [7:0] product
);
assign product = a * b;
endmodule
4. Routing fabric: phần vô hình chiếm phần lớn con chip
Nhìn ảnh die của một FPGA, trực giác bảo rằng LUT và BRAM chiếm chỗ nhiều nhất. Thực tế ngược lại: mạng dây nối khả trình (routing fabric) chiếm phần lớn diện tích silicon — thường được trích dẫn ở mức 50–80% tuỳ kiến trúc. Mỗi CLB được bao quanh bởi các kênh dây ngang/dọc, giao nhau tại switch matrix — ma trận công tắc bằng transistor cho phép "hàn mềm" bất kỳ ngõ ra CLB nào tới bất kỳ ngõ vào CLB nào khác. Chính các công tắc này (cùng SRAM cấu hình điều khiển chúng) là cái giá phải trả cho tính khả trình.
Hệ quả quan trọng cho timing: tín hiệu đi qua routing chậm hơn nhiều so với đi trong CLB — mỗi lần băng qua vài switch matrix có thể tốn nhiều hơn cả một lần tra LUT. Nối lại với Bài 8: trong $t_{comb}$ của một đường timing thật trên FPGA, độ trễ dây thường lớn hơn độ trễ logic — đó là lý do khung "Đào sâu" của Bài 8 nhấn mạnh STA thật phải cộng thêm độ trễ routing sau place & route, và là lý do carry chain (né hoàn toàn routing) tạo khác biệt 6 lần trong demo Mục 6. Riêng tín hiệu clock — thứ phải đến hàng chục nghìn flip-flop cùng lúc với độ lệch (skew) tối thiểu — được cấp hẳn một mạng dây riêng: clock tree với các global buffer, tách biệt hoàn toàn khỏi routing dữ liệu. Bao quanh rìa chip là các IO block khả trình (chuẩn điện áp, tốc độ, pull-up... đều cấu hình được) — cây cầu giữa fabric và thế giới bên ngoài.
5. FPGA vs ASIC vs MCU: khi nào chọn gì?
Đặt FPGA cạnh 2 lựa chọn còn lại khi cần "một con chip làm việc X" — thuê đúc ASIC riêng, hay dùng vi điều khiển (Series Điện Tử Bài 15) chạy phần mềm:
| Tiêu chí | MCU (phần mềm) | FPGA | ASIC |
|---|---|---|---|
| Chi phí khởi đầu (NRE) | ~0 (mua chip, viết code) | Thấp (mua board, tool miễn phí có) | Rất cao (mask set: hàng trăm nghìn → hàng triệu USD) |
| Giá mỗi đơn vị khi sản xuất lớn | Rẻ | Đắt nhất trong 3 (trả tiền cho tính khả trình) | Rẻ nhất khi số lượng đủ lớn |
| Hiệu năng / mức song song | Tuần tự, giới hạn bởi CPU | Song song thật, $f_{max}$ hàng trăm MHz | Cao nhất (GHz), tối ưu từng transistor |
| Công suất cho cùng tác vụ | Trung bình | Cao hơn ASIC (overhead routing + SRAM cấu hình) | Thấp nhất |
| Sửa thiết kế sau khi "xuất xưởng" | Nạp firmware mới | Nạp bitstream mới — sửa cả "phần cứng" | Không thể — respin tốn NRE lần nữa |
| Hợp nhất | Sản phẩm logic đơn giản, tốc độ thấp, cần rẻ + nhanh ra thị trường | Số lượng nhỏ–vừa, cần song song/độ trễ thấp, giao thức tuỳ biến, prototype ASIC | Số lượng rất lớn hoặc cần hiệu năng/công suất tuyệt đối |
Quy tắc ngón tay cái: MCU trước, FPGA khi phần mềm không kịp thở, ASIC khi bài toán nhân với số lượng đủ lớn để NRE tự trả nợ. FPGA còn giữ một vai đặc biệt trong chính flow ASIC: gần như mọi chip lớn đều được prototype trên FPGA trước khi tape-out — cùng RTL bạn viết ở series này, chỉ đổi target. Bài 10 sẽ đi tiếp con đường FPGA: từ RTL qua synthesis, place & route đến bitstream.
6. Thực hành: LUT Explorer & lưới CLB với carry chain
Demo đầu: một LUT-4 "sống". Bấm từng ô để lật bit SRAM (tự chế hàm của bạn), hoặc nạp preset; bật tắt 4 đầu vào để thấy chúng chọn địa chỉ — ô được chọn viền đậm, giá trị của nó chính là ngõ ra. Thử preset "FA sum (3 vào)" rồi nhìn kỹ: nửa trên bảng lặp y hệt nửa dưới, vì đầu vào i3 không được dùng — 8 bit "thật" nhân đôi để lấp đầy LUT-4.
Mỗi ô = 1 bit SRAM; nhãn nhỏ là địa chỉ (i3 i2 i1 i0). Đầu vào là ĐỊA CHỈ ĐỌC — không có cổng logic nào tính toán cả. Đổi hàm = bấm lật vài bit, không đổi dây.
// 1 LUT-k = 1 mảng 2^k bit + 1 phép đọc theo địa chỉ. Chỉ vậy thôi.
const sram = new Array(16).fill(0); // nội dung LUT-4 (nạp lúc cấu hình)
function lutRead(i3, i2, i1, i0) {
const addr = (i3 << 3) | (i2 << 2) | (i1 << 1) | i0; // đầu vào = địa chỉ
return sram[addr]; // đầu ra = bit được trỏ tới
}
// "Tổng hợp" 1 hàm bất kỳ vào LUT = điền bảng chân lý vào sram:
for (let addr = 0; addr < 16; addr++) {
const [i0, i1, i2, i3] = [addr & 1, (addr >> 1) & 1, (addr >> 2) & 1, (addr >> 3) & 1];
sram[addr] = i0 ^ i1 ^ i2 ^ i3; // ví dụ: XOR-4 → 0x6996
}
Demo thứ hai map đúng bộ cộng rca4 của Bài 6 lên một cột CLB (mỗi full adder 1 CLB) trong
lưới 4×4. Nút chuyển chế độ cho carry đi qua carry chain chuyên dụng (dây cam thẳng đứng)
hoặc đi vòng qua routing chung (dây xám gấp khúc qua switch matrix) — cùng mạch, cùng kết
quả cộng, nhưng độ trễ ước lượng chênh nhau 6 lần theo mô hình arrival time của Bài 8.
Cột trái: 4 CLB đảm nhận FA0→FA3 (dưới lên trên, đúng chiều carry chain vật lý). 12 CLB còn lại để trống — sẵn sàng cho phần thiết kế khác của bạn. Độ trễ dùng mô hình minh hoạ: LUT 0,4 ns · nấc carry chain 0,1 ns · bước nhảy routing 0,8 ns.
// Xem module rca4 đầy đủ ở Bài 6, hoặc dạng adder4_plus ở Mục 2 —
// demo này tính toán bằng đúng logic full adder đó, chỉ khác cách
// "đặt" nó lên tài nguyên vật lý của FPGA.
Tóm lược
Con chip "biến hình" hoá ra là một hệ tài nguyên xếp lớp, mỗi lớp một vai:
- ✅ LUT-k = bảng chân lý $2^k$ bit trong SRAM, đầu vào là địa chỉ đọc — mọi hàm logic cùng 1 độ trễ tra bảng, đổi hàm không đổi dây.
- ✅ CLB = LUT + D-FF + carry chain; carry chain là dây chuyên dụng né routing, biến adder $O(n)$ "trên giấy" thành nhanh nhất fabric.
- ✅ BRAM/DSP = tài nguyên cứng hoá, kích hoạt bằng inference theo khuôn mẫu — lệch khuôn (RAM có reset toàn mảng) là tool lặng lẽ phung phí gấp trăm lần.
- ✅ Routing fabric chiếm phần lớn diện tích và thường cả độ trễ; clock có mạng cây riêng để giữ skew thấp.
- ✅ MCU / FPGA / ASIC: chọn theo NRE ↔ giá đơn vị ↔ hiệu năng ↔ tính linh hoạt; FPGA còn là bệ prototype cho chính ASIC.
Trắc nghiệm ôn tập
Câu 1
Vì sao trên FPGA, hàm (a & b) | (cin & (a ^ b)) (4 toán tử) và hàm
a ^ b ^ cin (2 toán tử) lại có cùng độ trễ?
Câu 2
Vì sao carry chain làm bộ cộng trên FPGA nhanh hơn hẳn đi qua logic thường?
Câu 3
Bạn thêm if (rst) for (...) mem[i] <= 0; vào module RAM 256×8. Sau synthesis,
utilization report cho thấy 0 BRAM và ~2048 flip-flop. Chuyện gì đã xảy ra?
Câu 4
Một startup cần 500 thiết bị xử lý video độ trễ thấp cho năm đầu, chưa chắc chắn về thuật toán (sẽ còn chỉnh). Lựa chọn hợp lý nhất theo bảng so sánh của bài?
Tải file code thực hành minh họa bài học
File SystemVerilog gồm full adder tách dạng LUT (fa_sum/fa_carry), bộ cộng
dạng + map vào carry chain, phép nhân map vào DSP, cặp RAM ❌/✅ minh hoạ cạm bẫy BRAM
inference, kèm testbench đối chiếu:
Bình luận