Mở đầu: từ tập lệnh mini tự bịa sang tập lệnh THẬT
Bài 2 xây một Toy CPU chạy tập lệnh MINI tự bịa — mỗi lệnh là một object
JavaScript tiện lợi như { op: 'ADD', rd: 2, rs1: 0, rs2: 1 }. Dễ hiểu, nhưng KHÔNG CPU thật
nào chạy object JavaScript — mọi CPU thật chỉ hiểu SỐ NHỊ PHÂN. Bài này thay tập lệnh mini bằng
RV32I — tập lệnh RISC-V 32-bit thật, đang chạy trên hàng tỷ chip nhúng/laptop ngoài đời —
và xây một datapath đơn chu kỳ (single-cycle) để thực thi nó.
1. Tập lệnh RISC-V (RV32I)
Mọi lệnh RV32I đều dài đúng 32-bit cố định (đặc trưng RISC — Bài 2 đã so sánh với độ dài biến đổi của CISC), nhưng chia thành 6 bit trường khác nhau tuỳ dạng lệnh. Bài này dùng 3 dạng phổ biến nhất:
| Dạng | Cấu trúc bit (31→0) | Dùng cho |
|---|---|---|
| R-type | funct7[7] rs2[5] rs1[5] funct3[3] rd[5] opcode[7] | ADD, SUB, AND, OR, XOR — cả 2 toán hạng là thanh ghi |
| I-type | imm[12] rs1[5] funct3[3] rd[5] opcode[7] | ADDI, ANDI, ORI, XORI, LW — toán hạng thứ 2 là hằng số (immediate) |
| S-type | imm[7] rs2[5] rs1[5] funct3[3] imm[5] opcode[7] | SW — cần ĐỊA CHỈ (rs1+imm) và giá trị cần lưu (rs2), không có ô cho rd |
Vì sao ADD và SUB CÙNG opcode nhưng KHÔNG lẫn lộn? Chúng dùng đúng
opcode R-type (0110011) nhưng khác funct7 — "mã
phụ" 7-bit đóng vai trò như một switch phân biệt các lệnh cùng họ. Bit cao nhất của
funct7 chính là "cờ trừ": 0000000 cho ADD, 0100000 cho SUB.
R-type: funct7[31:25] rs2[24:20] rs1[19:15] funct3[14:12] rd[11:7] opcode[6:0]
I-type: imm[31:20] rs1[19:15] funct3[14:12] rd[11:7] opcode[6:0]
S-type: imm[31:25] rs2[24:20] rs1[19:15] funct3[14:12] imm[11:7] opcode[6:0]
# Vi du that: ADD x3, x1, x2 -> 0000000 00010 00001 000 00011 0110011
# funct7 rs2 rs1 f3 rd opcode
# (=0) (=2) (=1) (0) (=3) (R-type)
2. Dịch Assembly sang Mã máy
Dịch (assemble) là ánh xạ CƠ HỌC: tra bảng mnemonic → opcode/funct3/funct7, rồi xếp các trường vào đúng vị
trí bit. Verified bằng chính engine (không suy diễn tay): ADD x3, x1, x2 dịch thành đúng
0x2081b3; đổi CHỈ funct7 (giữ nguyên toán hạng) cho SUB x3, x1, x2 ra
0x402081b3 — đúng khác biệt DUY NHẤT nằm ở 7 bit cao nhất.
function assembleRV32I(mnemonic, args) {
const info = RV32I_MNEMONIC[mnemonic]; // tra bang: {type, funct3, funct7}
if (info.type === 'R') {
return ((info.funct7 << 25) | (args.rs2 << 20) | (args.rs1 << 15)
| (info.funct3 << 12) | (args.rd << 7) | RV32I_OPCODE.R) >>> 0;
}
// ... I-type, S-type tuong tu, xep dung vi tri bit ...
}
// Verified: assembleRV32I('ADD', {rd:3,rs1:1,rs2:2}) === 0x2081b3
// Verified: assembleRV32I('SUB', {rd:3,rs1:1,rs2:2}) === 0x402081b3 (chi funct7 doi)
Giải mã (decode) là chiều NGƯỢC LẠI: tách bit theo đúng vị trí, tra ngược (funct3, funct7) → mnemonic. Verified bằng phép khứ hồi: dịch rồi giải mã LẠI cả 8 mnemonic (ADD/SUB/AND/OR/XOR/ADDI/LW/SW) đều cho ĐÚNG lại mnemonic ban đầu — không có lệnh nào "mất tích" qua vòng dịch-giải mã.
function decodeRV32I(word) {
const opcode = word & 0x7f; // 7 bit thap nhat luon la opcode
const rd = (word >>> 7) & 0x1f;
const funct3 = (word >>> 12) & 0x7;
const rs1 = (word >>> 15) & 0x1f;
const rs2 = (word >>> 20) & 0x1f;
const funct7 = (word >>> 25) & 0x7f;
if (opcode === RV32I_OPCODE.R) {
// tra NGUOC (funct3, funct7) -> mnemonic trong bang RV32I_MNEMONIC
const mnemonic = /* tim mnemonic co type R khop dung funct3+funct7 */;
return { mnemonic, type: 'R', rd, rs1, rs2 };
}
// ... I-type, ILOAD, S-type tuong tu ...
}
// Verified: 8/8 mnemonic khu hoi dung (assemble roi decode ra DUNG lai mnemonic).
JMP addr với
addr là địa chỉ TUYỆT ĐỐI (vd "nhảy tới ô nhớ số 4"). RISC-V THẬT mã hoá lệnh nhảy/rẽ nhánh
(B-type/J-type, ngoài phạm vi bộ mã hoá R/I/S của bài này) bằng độ lệch
TƯƠNG ĐỐI so với PC (vd "nhảy lùi 8 byte so với vị trí hiện tại") — lý do: chương trình
có thể được nạp vào BẤT KỲ vùng nhớ nào (định vị lại — relocatable code) mà không cần sửa lại địa chỉ
nhảy bên trong. Nhầm 2 kiểu này là lỗi kinh điển khi đọc mã máy thật: cùng một con số offset có thể mang
ý nghĩa hoàn toàn khác nhau tuỳ dạng lệnh.
3. Thiết kế Datapath đơn chu kỳ (Single-Cycle)
Datapath đơn chu kỳ là "đường đi vật lý" của dữ liệu qua CPU: lệnh được fetch từ RAM → giải mã để biết cần
đọc thanh ghi nào → chảy qua ALU (Bài 1, tái dùng thẳng không
viết lại) để tính toán/tính địa chỉ → (nếu là LW/SW) chạm vào bộ nhớ dữ liệu → ghi kết quả trở lại
Register File. TOÀN BỘ hành trình này diễn ra trong ĐÚNG MỘT chu kỳ xung nhịp — vì vậy
chu kỳ xung nhịp CPU phải đủ DÀI để chứa lệnh CHẬM NHẤT (thường là LW, phải đi qua cả ALU
tính địa chỉ LẪN bộ nhớ).
ADD (chỉ cần fetch + decode + ALU) cũng bị "giam" trong cùng khoảng thời
gian dài đó, dù bản thân nó có thể xong sớm hơn nhiều. Đây chính là động lực thiết kế
pipeline ở Bài 4 (sắp ra mắt) — chia nhỏ công việc để mỗi giai đoạn có xung nhịp ngắn
hơn, thay vì ép mọi lệnh theo nhịp của lệnh chậm nhất.
Verified bằng một chương trình RV32I THẬT (không phải tập lệnh mini của Bài 2): tính $(5+3)-2$ bằng đúng 5
lệnh ADDI/ADD/ADDI/SUB, lưu kết quả vào mem[100] bằng SW, rồi đọc lại bằng LW — chạy qua
executeRV32I() cho đúng $x_3=8$, $x_5=6$, mem[100]=6, và đọc lại đúng $x_6=6$.
Phép ADD/SUB bên trong gọi THẲNG aluExecute() của Bài 1 — không viết lại phép cộng/trừ lần
thứ hai.
const program = [
assembleRV32I('ADDI', { rd: 1, rs1: 0, imm: 5 }), // x1 = 5
assembleRV32I('ADDI', { rd: 2, rs1: 0, imm: 3 }), // x2 = 3
assembleRV32I('ADD', { rd: 3, rs1: 1, rs2: 2 }), // x3 = x1+x2 = 8
assembleRV32I('ADDI', { rd: 4, rs1: 0, imm: 2 }), // x4 = 2
assembleRV32I('SUB', { rd: 5, rs1: 3, rs2: 4 }), // x5 = x3-x4 = 6
assembleRV32I('SW', { rs1: 0, rs2: 5, imm: 100 }), // mem[100] = x5
assembleRV32I('LW', { rd: 6, rs1: 0, imm: 100 }), // x6 = mem[100]
];
const { regs, mem } = runRV32IProgram(program);
// Verified: regs[3]=8, regs[5]=6, mem[100]=6, regs[6]=6
4. Thực hành: Trực quan hóa Datapath đơn chu kỳ RISC-V
Gõ một lệnh Assembly (vd ADD x3, x1, x2 hoặc LW x5, 8(x2)), bấm "Dịch
(Assemble)" để xem breakdown bit 32-bit THẬT, rồi bấm "Thực thi" để chạy lệnh đó qua datapath — khối đang
hoạt động sẽ phát sáng cam:
Mã máy 32-bit (breakdown bit):
Thanh ghi (khác 0):
Tóm lược
- ✅ RV32I: mọi lệnh dài cố định 32-bit, chia trường theo 3 dạng R/I/S-type (bài này dùng).
-
✅ Verified:
ADD x3,x1,x2=0x2081b3,SUB x3,x1,x2=0x402081b3— chỉ khác 7 bit funct7. - ✅ Verified: khứ hồi assemble→decode khớp đúng mnemonic trên cả 8 lệnh test.
- ✅ Pitfall: địa chỉ tuyệt đối (Toy CPU Bài 2) khác hẳn địa chỉ tương đối PC của lệnh nhảy RISC-V thật.
- ✅ Datapath đơn chu kỳ: TOÀN BỘ lệnh chạy trong 1 chu kỳ, xung nhịp bị ép dài theo lệnh CHẬM NHẤT (LW) — động lực trực tiếp cho pipeline ở Bài 4.
- ✅ Verified: chương trình RV32I thật (5+3)-2 chạy đúng qua datapath, tái dùng thẳng ALU của Bài 1.
Trắc nghiệm ôn tập
Câu 1
Verified: ADD x3,x1,x2 = 0x2081b3, SUB x3,x1,x2 = 0x402081b3 (cùng toán hạng). Vì sao 2 lệnh khác nhau chỉ ở giá trị này?
Câu 2
Vì sao lệnh nhảy/rẽ nhánh RISC-V thật mã hoá địa chỉ đích TƯƠNG ĐỐI so với PC, thay vì tuyệt đối như Toy CPU của Bài 2?
Câu 3
Trong thiết kế datapath đơn chu kỳ, vì sao xung nhịp CPU phải dài bằng đúng lệnh CHẬM NHẤT (thường là LW)?
Tải file code thực hành minh họa bài học
File JavaScript CPUJS — thư viện kiến trúc máy tính mini dùng xuyên suốt cả 12 bài, Bài 3
vừa thêm assembleRV32I(), decodeRV32I(), executeRV32I(),
runRV32IProgram() — bộ dịch/giải mã/thực thi RV32I R/I/S-type thật, kèm self-test đối chiếu
đúng mọi con số trong bài (chạy node cpu-core.js, không cần cài thêm gì):
📖 Tài liệu tham khảo
- Đặc tả chính thức: RISC-V International — Technical Specifications — tài liệu gốc định nghĩa mã hoá RV32I R/I/S/B/U/J-type.
- Sách giáo khoa cốt lõi: Computer Organization and Design, RISC-V Edition (Patterson & Hennessy) — Chương 2-4 trình bày chi tiết mã hoá lệnh và datapath đơn chu kỳ.
- Tham khảo nhanh: WikiChip — RISC-V Instruction Set — bảng tra opcode/funct3/funct7 đầy đủ.