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).


📚 Điều kiện tiên quyết
Bắt buộc đọc Bài 1 (ALU 4-bit + cờ trạng thái — Toy CPU bài này tái dùng thẳng 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_vs_harvard.c (mô tả cấu trúc bộ nhớ)
// 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"
⚠️ Cạm bẫy: Self-modifying code — khi dữ liệu vô tình đè lên lệnh
Vì Von Neumann dùng CHUNG bộ nhớ, không có gì ngăn một lệnh 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:

  1. 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).
  2. Decode: giải mã IR để biết đây là lệnh gì, cần thanh ghi/địa chỉ nào.
  3. Execute: thực thi hiệu ứng thật (cập nhật thanh ghi, bộ nhớ, hoặc chính PC).
cpu_step.js (trích engine dùng chung cpu-core.js)
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.
⚠️ Cạm bẫy: thứ tự tăng PC trước lệnh nhảy
PC tăng lên 1 NGAY SAU bước fetch — TRƯỚC KHI lệnh được decode/execute. Với lệnh nhảy (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ũ)
risc_vs_cisc_memcpy.asm (cùng tác vụ: cộng 1 hằng số vào ô nhớ)
; 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
⚠️ "RISC luôn nhanh hơn CISC" — không hoàn toàn đúng
Đây là hiểu lầm phổ biến. CPU x86 (CISC) hiện đại KHÔNG thực thi trực tiếp các lệnh phức tạp — chúng có một tầng giải mã ẩn dịch mỗi lệnh CISC thành một hoặc nhiều micro-ops đơn giản, rồi chạy các micro-ops đó trên một lõi thực thi mang tinh thần RISC ở bên trong. Nói cách khác, hầu hết CPU x86 hiện đại là "CISC ở vỏ ngoài, RISC ở lõi trong" — sự khác biệt hiệu năng thực tế phụ thuộc vi kiến trúc cụ thể, không phải nhãn RISC/CISC đơn thuần.

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ì:

loop_sum_program.js (chương trình chạy trong demo bên dưới)
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.
📟 Bộ giả lập Toy CPU — Fetch-Decode-Execute

RAM (Von Neumann — lệnh & dữ liệu chung)

    PC:0
    IR:
    Thanh ghi:R0=0 R1=0 R2=0 R3=0
    Đang khởi tạo…

    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 về cpu-core.js

    📖 Tài liệu tham khảo

    Bài viết liên quan trong series

    Bài 1: Cổng Logic đến Đơn Vị ALU Bài 3: Hợp Ngữ RISC-V & Đường Đi Của Dữ Liệu (Datapath) Quay lại Lộ trình Kiến Trúc Máy Tính