Mở đầu: từ mạch đếm bit đến một "bộ não" biết đọc chương trình
Bài 1 xây một khối ALU biết cộng, trừ, và so sánh — nhưng ALU đó chỉ tính MỘT phép toán khi có ai đó bơm đúng 2 toán hạng vào tay nó. Câu hỏi lớn hơn: làm sao một chuỗi phép toán — một CHƯƠNG TRÌNH — tự động chảy qua ALU đó theo đúng thứ tự, không cần con người bấm nút từng bước? Câu trả lời là một cỗ máy nhỏ gọi CPU, và một vòng lặp vô hạn ba bước đã điều khiển mọi chiếc máy tính từ 1945 tới nay: Fetch — Decode — Execute.
Bài này xây Toy CPU đầu tiên của series — một cỗ máy tối giản chạy đúng vòng lặp đó trên một tập lệnh mini tự thiết kế, tái dùng thẳng ALU của Bài 1 cho các lệnh số học. Đi kèm là 2 câu hỏi kiến trúc nền tảng: bộ nhớ nên CHUNG hay TÁCH cho lệnh/dữ liệu (Von Neumann vs Harvard), và tập lệnh nên ĐƠN GIẢN hay PHỨC TẠP (RISC vs CISC).
aluExecute() cho lệnh ADD/SUB).
1. Mô hình Von Neumann vs Harvard
Trong kiến trúc Von Neumann (John von Neumann, 1945), lệnh chương trình và dữ liệu nằm CHUNG một bộ nhớ, truy cập qua CHUNG một bus — CPU không phân biệt được nó đang đọc "một lệnh" hay "một con số" cho tới khi thực sự dùng giá trị đó. Kiến trúc Harvard đối lập: 2 bộ nhớ vật lý tách biệt, 2 bus riêng cho lệnh và dữ liệu — CPU có thể fetch lệnh KẾ TIẾP đồng thời đọc/ghi dữ liệu của lệnh HIỆN TẠI, tránh nút thắt cổ chai Von Neumann bottleneck (một bus dùng chung giới hạn băng thông tổng).
Toy CPU của bài này dùng đúng mô hình Von Neumann: một mảng ram DUY NHẤT chứa cả object lệnh
lẫn số liệu dữ liệu — không có ranh giới cứng nào phân biệt "vùng lệnh" với "vùng dữ liệu", đúng bản chất
"bus chung" của mô hình gốc.
// VON NEUMANN: mot mang bo nho DUY NHAT, dung chung cho lenh va du lieu.
// CPU khong biet mot o nho la "lenh" hay "so lieu" cho toi khi dung no.
uint32_t unified_memory[MEM_SIZE]; // ca chuong trinh lan bien deu o day
// HARVARD: hai mang bo nho VAT LY tach biet, hai bus rieng.
// Fetch lenh KE TIEP va doc/ghi du lieu HIEN TAI co the dien ra DONG THOI.
uint32_t instruction_memory[PROG_SIZE]; // chi CPU-fetch duoc doc, khong ghi
uint32_t data_memory[DATA_SIZE]; // doc/ghi tu do, khong bao gio duoc "fetch"
STORE ghi dữ liệu ĐÈ LÊN đúng
địa chỉ đang chứa một lệnh khác trong CHÍNH chương trình đó. Verified thật (không suy diễn): cho chạy
một chương trình có STORE R0, [2] trong khi địa chỉ 2 đang là lệnh
LOADI R1, 7 — CPU fetch tới địa chỉ 2 gặp phải một con số (99) thay vì object lệnh hợp lệ,
và crash thật với lỗi "Lệnh không hợp lệ: undefined". Đây chính là lớp lỗi
bảo mật/hành vi-không-xác-định nghiêm trọng nhất của self-modifying code — và là lý do các hệ điều hành
hiện đại đánh dấu vùng nhớ chứa code là read-only + non-writable (bit NX/XD) ngay ở
tầng phần cứng MMU.
2. Chu kỳ Fetch-Decode-Execute
Đây là vòng lặp vô hạn điều khiển toàn bộ CPU kể từ lúc khởi động chip tới lúc tắt máy:
- Fetch: đọc lệnh tại địa chỉ mà thanh ghi PC (Program Counter) đang trỏ tới, nạp vào thanh ghi IR (Instruction Register).
-
Decode: giải mã
IRđể biết đây là lệnh gì, cần thanh ghi/địa chỉ nào. - Execute: thực thi hiệu ứng thật (cập nhật thanh ghi, bộ nhớ, hoặc chính PC).
function cpuStep(state) {
const instr = state.ram[state.pc]; // FETCH
state.ir = instr;
state.pc = state.pc + 1; // PC tang NGAY - TRUOC KHI decode/execute!
switch (instr.op) { // DECODE
case 'JMP': state.pc = instr.addr; break; // EXECUTE: GHI DE PC vua tang
case 'ADD': /* ... goi thang aluExecute() cua Bai 1 ... */ break;
// ...
}
}
// Verified: chuong trinh tuyen tinh 5+3 chay DUNG 5 chu ky (5 lenh, khong nhay),
// PC dung lai o 5 sau HALT - dung so lenh da fetch.
JMP/BEQ), giá trị PC vừa tăng đó bị GHI ĐÈ ở bước execute
bằng địa chỉ đích tuyệt đối. Nếu nhầm thứ tự (tưởng PC tăng SAU khi nhảy), lập trình vi kiến trúc sẽ
tính sai địa chỉ đích — verified bằng vòng lặp tính tổng $1+2+3+4+5$: lệnh JMP 4 ở địa chỉ
7 phải nhảy về ĐÚNG địa chỉ 4 (không phải 4+1=5) để lặp lại đúng phép kiểm tra điều kiện dừng.
Verified bằng vòng lặp thật: chương trình tính $1+2+3+4+5$ bằng bộ đếm giảm dần và lệnh
BEQ/JMP cho ra đúng $R_{sum} = 15$, bộ đếm về đúng $0$, sau đúng
26 chu kỳ fetch-decode-execute — con số này do chính engine chạy ra, không bịa tay.
3. Kiến trúc tập lệnh ISA: RISC vs CISC
ISA (Instruction Set Architecture) là "hợp đồng" giữa phần mềm và phần cứng: tập hợp MỌI lệnh mà CPU hiểu được. RISC (Reduced Instruction Set Computer — ví dụ ARM, RISC-V) chọn một tập lệnh nhỏ, đơn giản, độ dài cố định, dễ giải mã bằng mạch cứng tối giản. CISC (Complex Instruction Set Computer — ví dụ x86) cho phép lệnh phức tạp, độ dài biến đổi, một lệnh có thể làm việc của nhiều lệnh RISC gộp lại.
| Tiêu chí | RISC (ARM, RISC-V) | CISC (x86) |
|---|---|---|
| Độ dài lệnh | Cố định (RISC-V: luôn 32-bit) | Biến đổi (x86: 1-15 byte) |
| Độ phức tạp giải mã phần cứng | Đơn giản, ít transistor hơn cho decoder | Phức tạp — cần decoder biến đổi lệnh thành micro-ops |
| Số lệnh cho 1 tác vụ | Nhiều lệnh đơn giản hơn | Ít lệnh, mỗi lệnh làm nhiều việc hơn |
| Ưu thế thực tế | Hiệu quả điện năng, thiết kế chip đơn giản (di động) | Tương thích ngược cực cao (hàng thập kỷ phần mềm cũ) |
; CISC (x86) - MOT lenh lam het: doc, cong, ghi lai
add DWORD PTR [rax], 5 ; 1 lenh, do dai bien doi, phan cung tu lo doc-cong-ghi
; RISC (RISC-V) - PHAI tach thanh nhieu lenh don gian, moi lenh 1 viec
lw t0, 0(a0) ; doc gia tri tu bo nho vao thanh ghi
addi t0, t0, 5 ; cong hang so NGAY TREN THANH GHI (khong dung bo nho)
sw t0, 0(a0) ; ghi ket qua tro lai bo nho
4. Thực hành: Bộ giả lập Toy CPU đơn giản
Chạy đúng chương trình vòng lặp tính $1+2+3+4+5$ đã verify ở Mục 2 — bấm "Từng bước" để xem PC, IR, thanh ghi cập nhật theo ĐÚNG một chu kỳ fetch-decode-execute mỗi lần bấm, hoặc "Chạy hết" để xem toàn bộ 26 chu kỳ diễn ra tức thì:
const PROGRAM = [
{ op: 'LOADI', rd: 0, imm: 5 }, // 0: R0 = counter = 5
{ op: 'LOADI', rd: 1, imm: 0 }, // 1: R1 = sum = 0
{ op: 'LOADI', rd: 2, imm: 0 }, // 2: R2 = 0 (hang so dieu kien dung)
{ op: 'LOADI', rd: 3, imm: 1 }, // 3: R3 = 1 (hang so tru)
{ op: 'BEQ', rs1: 0, rs2: 2, addr: 8 }, // 4: counter==0 -> thoat vong lap
{ op: 'ADD', rd: 1, rs1: 1, rs2: 0 }, // 5: sum += counter
{ op: 'SUB', rd: 0, rs1: 0, rs2: 3 }, // 6: counter -= 1
{ op: 'JMP', addr: 4 }, // 7: quay lai kiem tra dieu kien
{ op: 'HALT' }, // 8
];
// Verified: runProgram(PROGRAM) -> R1 (tong) = 15, R0 (counter) = 0, dung 26 chu ky.
RAM (Von Neumann — lệnh & dữ liệu chung)
Tóm lược
- ✅ Von Neumann dùng CHUNG bộ nhớ/bus cho lệnh và dữ liệu; Harvard tách riêng để tránh nút thắt cổ chai.
- ✅ Verified: self-modifying code (STORE ghi đè vùng lệnh) khiến fetch kế tiếp đọc phải rác và crash thật — đúng lý do MMU hiện đại đánh dấu vùng code read-only.
- ✅ Fetch-Decode-Execute là vòng lặp vô hạn; PC tăng NGAY sau fetch, TRƯỚC KHI lệnh nhảy có cơ hội ghi đè nó.
- ✅ Verified: Toy CPU chạy chương trình 5+3 đúng 5 chu kỳ; vòng lặp BEQ/JMP tính tổng 1..5=15 sau đúng 26 chu kỳ.
- ✅ RISC (đơn giản, cố định) vs CISC (phức tạp, biến đổi) — CISC hiện đại dịch ngầm về micro-ops kiểu RISC.
Trắc nghiệm ôn tập
Câu 1
Vì sao mô hình Von Neumann tạo ra "nút thắt cổ chai" (Von Neumann bottleneck)?
Câu 2
Verified: một lệnh STORE ghi đè đúng địa chỉ đang chứa một lệnh khác khiến CPU crash khi
fetch tới đó. Đây là hiện tượng gì, và tại sao nó CÓ THỂ xảy ra trong Von Neumann?
Câu 3
Trong chu kỳ Fetch-Decode-Execute, PC (Program Counter) tăng lên tại thời điểm nào?
Câu 4
Nhận định nào về RISC và CISC là ĐÚNG?
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 2
vừa thêm createCpuState(), cpuStep(), runProgram() — Toy CPU
fetch-decode-execute trên tập lệnh mini (LOADI/ADD/SUB/STORE/LOAD/JMP/BEQ/HALT), 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
- Sách giáo khoa cốt lõi: Computer Organization and Design, MIPS Edition (Patterson & Hennessy) — Chương 4 trình bày chi tiết chu kỳ fetch-decode-execute và datapath.
- Von Neumann vs Harvard: Wikipedia — Von Neumann architecture — nguồn gốc lịch sử và so sánh với Harvard architecture.
- RISC vs CISC: Wikipedia — Reduced instruction set computer — lịch sử tranh luận RISC/CISC và thực tế vi kiến trúc hiện đại.