Mở đầu: 12 bài trước dạy từng viên gạch — bài này bắt đầu xây cả căn nhà
Từ Bài 2 tới Bài 12, bạn đã học từng mảnh ghép riêng lẻ: cổng logic tổ hợp, thanh ghi, máy trạng thái, bộ cộng, FIFO, timing, kiến trúc FPGA, flow ASIC. Nhưng tất cả những mảnh ghép đó tồn tại để phục vụ đúng 1 mục tiêu cuối: xây một bộ xử lý (CPU) chạy được chương trình thật. Bài 13-14 là capstone của series — ghép mọi kiến thức đã học thành 1 CPU RISC-V RV32I hoàn chỉnh, chạy đúng chương trình assembly bạn tự viết.
Phần 1 (bài này) tập trung vào datapath — con đường vật lý dữ liệu di chuyển qua CPU: tại sao chọn RISC-V, 6 định dạng lệnh, tập lệnh con ~14 lệnh của dự án, từng khối datapath (nhiều khối bạn đã tự tay xây ở các bài trước — register file chính là FIFO/bộ nhớ ở Bài 7, ALU là bản nâng cấp của Bài 2), và bảng tín hiệu điều khiển sơ khởi. Cuối bài, Datapath Visualizer cho bạn tự viết vài dòng assembly và xem chính xác đường dữ liệu nào "sáng lên" cho từng loại lệnh. Phần 2 (Bài 14) sẽ hoàn thiện control unit và chạy chương trình thật.
Tài nguyên ngoài: HDLBits, ChipVerify, EDA Playground.
1. Vì sao chọn RISC-V để học thiết kế CPU
RISC-V là tập lệnh (ISA) mở hoàn toàn miễn phí bản quyền — khác x86/ARM (phải trả phí license để thiết kế chip tuân theo), bất kỳ ai cũng tự do tổng hợp CPU RISC-V thật lên FPGA hoặc ASIC (đúng flow đã học ở Bài 10/Bài 12) mà không cần xin phép. Quan trọng hơn cho việc HỌC: định dạng lệnh RISC-V cực kỳ đều đặn — mọi lệnh dài đúng 32-bit, chia thành 6 định dạng cố định, các trường bit (opcode, thanh ghi, immediate) luôn nằm ở vị trí gần giống nhau giữa các định dạng. Đây chính là lý do RISC-V trở thành ISA phổ biến nhất để dạy kiến trúc máy tính.
| Định dạng | Dùng cho | Trường bit đặc trưng |
|---|---|---|
| R | Số học/logic 2 thanh ghi (add, sub...) |
opcode, rd, funct3, rs1, rs2, funct7 |
| I | Số học tức thời + load (addi, lw...) |
opcode, rd, funct3, rs1, imm[11:0] |
| S | Store (sw...) |
opcode, imm[4:0], funct3, rs1, rs2, imm[11:5] |
| B | Rẽ nhánh (beq, bne...) |
opcode, imm, funct3, rs1, rs2, imm (lệch bit, luôn chẵn) |
| U | Hằng số 20-bit cao (lui...) |
opcode, rd, imm[31:12] |
| J | Nhảy không điều kiện (jal...) |
opcode, rd, imm (lệch bit, luôn chẵn) |
Dự án này chỉ dùng 4 trong 6 định dạng (R, I, S, B, J — bỏ U vì hằng số 20-bit cao ngoài phạm vi tập lệnh con), đủ để dựng 1 CPU hoạt động thật với các lệnh số học, truy cập bộ nhớ, và rẽ nhánh.
Bit: 31-25 24-20 19-15 14-12 11-7 6-0
Truong: funct7 rs2 rs1 funct3 rd opcode
Gia tri: 0000000 00010 00001 000 00011 0110011
(x2) (x1) (add) (x3) (R-type)
Doc tu phai sang trai: opcode (0110011) noi day la R-type -> control
unit biet ngay can doc CA rs1 LAN rs2 (dung vi tri co dinh, khong can
biet truoc la lenh gi) -> Register File xuat 2 gia tri -> ALU cong
theo funct3+funct7 (000+0000000 = add) -> ghi ket qua vao rd (x3).
2. Tập lệnh RV32I của dự án (~14 lệnh)
| Nhóm | Lệnh | Ý nghĩa |
|---|---|---|
| Số học/logic (R-type) | add rd, rs1, rs2 |
rd = rs1 + rs2 |
sub rd, rs1, rs2 |
rd = rs1 − rs2 | |
and rd, rs1, rs2 |
rd = rs1 & rs2 | |
or rd, rs1, rs2 |
rd = rs1 | rs2 | |
slt rd, rs1, rs2 |
rd = (rs1 < rs2) ? 1 : 0 | |
| Số học tức thời (I-type) | addi rd, rs1, imm |
rd = rs1 + imm |
andi rd, rs1, imm |
rd = rs1 & imm | |
ori rd, rs1, imm |
rd = rs1 | imm | |
slti rd, rs1, imm |
rd = (rs1 < imm) ? 1 : 0 | |
| Nạp/cất bộ nhớ | lw rd, imm(rs1) |
rd = mem[rs1 + imm] |
sw rs2, imm(rs1) |
mem[rs1 + imm] = rs2 | |
| Rẽ nhánh (B-type) | beq rs1, rs2, label |
nếu rs1 == rs2: PC = label |
bne rs1, rs2, label |
nếu rs1 != rs2: PC = label | |
| Nhảy (J-type) | jal rd, label |
rd = PC + 4; PC = label |
14 lệnh này đủ để viết chương trình có ý nghĩa thật: vòng lặp (kết hợp bne/beq
với jal), tính toán số học, đọc/ghi mảng qua lw/sw — chính xác
những gì Bài 14 sẽ dùng để chạy chương trình Fibonacci thật.
addi x1, x0, 0 # sum = 0
addi x2, x0, 5 # counter = 5
loop:
add x1, x1, x2 # sum += counter
addi x2, x2, -1 # counter -= 1
bne x2, x0, loop # neu counter != 0, quay lai loop
# Ket qua: x1 = 5+4+3+2+1 = 15, x2 = 0
Chú ý bne ở đây "quay lại" (nhảy tới địa chỉ nhỏ hơn PC hiện tại) — immediate của lệnh nhảy
lùi luôn là số âm, và Immediate Generator (Mục 3) phải sign-extend đúng để giữ nguyên dấu
âm đó, không phải chỉ hoạt động đúng với nhảy tiến.
3. Các khối datapath: nơi mỗi bài học trước "gặp lại" nhau
| Khối | Vai trò | Liên hệ bài đã học |
|---|---|---|
| PC (Program Counter) | Thanh ghi giữ địa chỉ lệnh hiện hành, tự tăng 4 (hoặc nhảy) mỗi chu kỳ | Đúng 1 thanh ghi always_ff đơn giản — Bài 3 |
| Instruction Memory | Bộ nhớ chỉ đọc, địa chỉ vào là PC, ra là 32-bit lệnh | Suy luận ROM — Bài 7 Mục 1 |
| Register File | 32 thanh ghi 32-bit, đọc 2 cổng (rs1, rs2) đồng thời + ghi 1 cổng (rd) | Chính xác thiết kế 2-read/1-write — Bài 7 Mục 2 |
| Immediate Generator | Trích + sign-extend hằng số từ lệnh, vị trí bit khác nhau theo từng định dạng | Sign-extension số bù 2 — Series C Bài 2 |
| ALU | Tính toán số học/logic, so sánh cho rẽ nhánh, tính địa chỉ bộ nhớ | Bản nâng cấp ALU 4-bit — Bài 2 |
| Data Memory | Bộ nhớ đọc/ghi cho lw/sw |
RAM inference — Bài 7 Mục 1 |
$$imm_I = \text{sext}(inst[31{:}20]) \qquad imm_B = \text{sext}(inst[31] \Vert inst[7] \Vert inst[30{:}25] \Vert inst[11{:}8] \Vert 0)$$
Công thức trên minh hoạ vì sao cần 1 khối Immediate Generator riêng: mỗi định dạng lệnh "giấu" các bit của
hằng số ở vị trí khác nhau (để giữ vị trí rs1/rs2 cố định giữa mọi định dạng,
giúp phần cứng đọc thanh ghi được ngay cả khi CHƯA biết chắc đây là lệnh loại gì) — Immediate Generator
ghép lại đúng thứ tự rồi sign-extend (nối bit dấu) thành số 32-bit đầy đủ.
// Ban nang cap cua ALU 4-bit o Bai 2 — gio nhan 32-bit + chon phep toan
// bang funct3 (dung dung tin hieu tu bang control unit Muc 4).
module alu_rv32i (
input logic [31:0] a, b,
input logic [2:0] alu_op, // suy tu funct3 (+ funct7 cho add/sub)
output logic [31:0] result
);
always_comb begin
case (alu_op)
3'b000: result = a + b; // add / addi
3'b001: result = a - b; // sub
3'b010: result = a & b; // and / andi
3'b011: result = a | b; // or / ori
3'b100: result = (a < b) ? 1 : 0; // slt / slti
default: result = 32'b0;
endcase
end
endmodule
Cùng đúng cấu trúc case/always_comb đã học kỹ ở
Bài 2 — chỉ đổi độ rộng bit và số phép toán. Đây chính là module
trong file tải về cuối bài, đã tự kiểm chứng chạy đúng trên VeriLite trước khi đưa vào bài viết.
add) hay phức tạp
(lw phải đi qua ALU RỒI Data Memory), đều hoàn tất trong đúng 1 chu kỳ clock. Hệ quả: chu
kỳ clock phải đủ dài để lệnh CHẬM NHẤT (thường là lw, đường dài nhất qua datapath) kịp xong
— mọi lệnh khác đều "lãng phí" phần lớn chu kỳ chờ đợi dù bản thân chúng xong sớm hơn nhiều. CPU thật
gần như luôn dùng pipelining (chia datapath thành nhiều tầng, xử lý nhiều lệnh gối lên
nhau) để tránh lãng phí này — đúng hướng mở rộng tự nhiên sau khi nắm vững single-cycle, xem thêm ở mục
tài liệu tham khảo cuối bài.
4. Control Unit sơ khởi: bảng tín hiệu điều khiển theo opcode
Cùng 1 phần cứng datapath phải "biến hình" hành vi khác nhau tuỳ loại lệnh — đó là việc của
control unit: đọc opcode (7 bit thấp của lệnh), xuất ra các tín hiệu điều khiển bật/tắt
đường dữ liệu tương ứng. Bảng dưới đây là control unit sơ khởi (Phần 2 sẽ ghép nốt phần
funct3 để phân biệt add/sub cùng opcode):
| Opcode (loại lệnh) | RegWrite | ALUSrc | MemRead | MemWrite | MemToReg | Branch |
|---|---|---|---|---|---|---|
R-type (0110011) |
1 | 0 (dùng rs2) | 0 | 0 | 0 (dùng kết quả ALU) | 0 |
I-type số học (0010011) |
1 | 1 (dùng imm) | 0 | 0 | 0 | 0 |
lw (0000011) |
1 | 1 | 1 | 0 | 1 (dùng dữ liệu từ Data Memory) | 0 |
sw (0100011) |
0 | 1 | 0 | 1 | — | 0 |
B-type (1100011) |
0 | 0 | 0 | 0 | — | 1 |
J-type (1101111) |
1 | — | 0 | 0 | 0 (dùng PC+4) | 0 |
Đây chính xác cùng khái niệm "bảng tra opcode → hành vi" đã gặp ở mọi case statement xuyên
suốt series — chỉ khác quy mô: thay vì 1 FSM nhỏ (Bài 4), control unit CPU là 1
bảng tra opcode → 6 tín hiệu điều khiển đồng thời.
5. Demo tương tác: Datapath Visualizer
Viết vài dòng assembly RV32I (hỗ trợ nhãn cho beq/bne/jal), bấm
Step để chạy từng lệnh — datapath bên dưới tô sáng đúng khối đang hoạt động cho loại lệnh
đó, bảng thanh ghi và bộ nhớ cập nhật theo thời gian thực.
Thanh ghi (x0-x7):
Data Memory (địa chỉ 0-12):
Chương trình mặc định: tính x1+x2, cất vào bộ nhớ, đọc lại, so sánh rồi rẽ nhánh bỏ qua 1 lệnh — tự sửa assembly rồi bấm lại "Nạp chương trình" để thử lệnh khác.
RISC-V đảm bảo x0 luôn đọc ra 0, kể cả khi có lệnh "ghi" vào nó — nhiều chương trình biên
dịch thật cố tình lợi dụng điều này: lệnh nop (không làm gì) thực chất
chỉ là addi x0, x0, 0, và rất nhiều lệnh dùng x0 làm "thùng rác" khi kết quả
không cần dùng tới. Nếu bộ mô phỏng/RTL của bạn ghi thẳng vào thanh ghi 0 mà không có nhánh đặc biệt,
x0 sẽ ngừng là 0 sau lệnh đầu tiên ghi vào nó —
mọi lệnh sau đó giả định x0=0 đều sai theo, một lỗi cực khó phát hiện vì chương trình
vẫn "chạy", chỉ cho kết quả sai dần.
// ❌ SAI: ghi thẳng, không có trường hợp đặc biệt cho thanh ghi 0
function writeRegBad(regs, i, v) {
regs[i] = v; // neu i===0, x0 se ngung la 0 vinh vien tu day
}
// ✅ ĐÚNG: x0 luôn bị "chặn" ghi — đúng ngữ nghĩa RISC-V thật
function writeRegGood(regs, i, v) {
if (i !== 0) regs[i] = v; // ghi vao x0 la no-op hop le, khong loi
}
Tóm lược
Bài này dựng xong "bộ khung" datapath — Phần 2 sẽ thổi hồn bằng chương trình chạy thật:
- ✅ RISC-V: ISA mở miễn phí bản quyền, 6 định dạng lệnh cố định 32-bit dễ decode.
- ✅ ~14 lệnh RV32I đủ viết chương trình có ý nghĩa: số học, load/store, rẽ nhánh, nhảy.
- ✅ Datapath: PC, Instruction Memory, Register File, Immediate Generator, ALU, Data Memory — mỗi khối đã tự tay xây ở 1 bài trước đó trong series.
- ✅ Control unit: bảng tra opcode → 6 tín hiệu điều khiển, cùng khái niệm case-based FSM đã học, chỉ khác quy mô.
- ✅ x0 luôn = 0: cạm bẫy kinh điển khi tự viết register file RISC-V, chương trình biên dịch thật dựa hẳn vào đảm bảo này.
Trắc nghiệm ôn tập
Câu 1
Vì sao RISC-V chia lệnh thành nhiều định dạng (R/I/S/B/U/J) thay vì 1 định dạng chung duy nhất?
Câu 2
Register File trong datapath CPU RISC-V cần bao nhiêu cổng đọc/ghi tối thiểu, và vì sao?
Câu 3
Vì sao cần 1 khối Immediate Generator riêng thay vì đọc thẳng immediate từ 1 vị trí bit cố định?
Câu 4
Nếu register file KHÔNG chặn ghi vào x0 (bug trong callout cạm bẫy), hậu quả thực tế là gì?
Tải file code thực hành minh họa bài học
File SystemVerilog gồm ALU RV32I (nâng cấp từ Bài 2, hỗ trợ đủ 5 phép R-type/I-type của tập lệnh), cùng comment mô tả đầy đủ 6 định dạng lệnh và bảng control signal tham khảo:
Tải về riscv_alu_example.sv
Bình luận