Mở đầu: Từ hạt cát silicon đến bộ não số

Tại sao máy tính — thiết bị siêu việt có thể mô phỏng vật lý lượng tử, kết xuất đồ họa 3D lộng lẫy và chạy các mô hình AI khổng lồ — về bản chất lại chỉ hiểu hai trạng thái duy nhất: Bật (1) và Tắt (0)? Mọi lập trình viên đều biết điều này, nhưng rất ít người thực sự hình dung được làm cách nào những dòng điện tắt-bật thô sơ đó lại có thể cộng hai số, so sánh chúng, hay đưa ra quyết định rẽ nhánh logic.

Bài học này sẽ đưa bạn khám phá tầng sâu nhất của phần cứng máy tính: từ các phép toán đại số Boolean nền tảng, cách ghép các cổng logic AND/OR/XOR thành mạch cộng bán phần (Half Adder) và toàn phần (Full Adder), cho đến cách xây dựng một ALU 4-bit hoàn chỉnh. Cuối cùng, bạn sẽ tự tay vận hành và quan sát bộ mô phỏng ALU tương tác để hiểu rõ cách CPU tính toán và thiết lập các cờ trạng thái Zero, Sign, và Overflow.


📚 Điều kiện tiên quyết
Nên đọc trước Series 10: Điện Tử & Mô Phỏng Vi Mạch (đặc biệt phần cổng logic — transistor đóng vai trò công tắc điện) để hiểu vì sao dòng điện tắt-bật vật lý lại biểu diễn được đại số Boolean. Bài này không yêu cầu kiến thức lập trình nâng cao, chỉ cần quen thuộc cú pháp C/JavaScript cơ bản.

1. Đại số Boolean & Các cổng logic cơ bản

Vào giữa thế kỷ 19, nhà toán học George Boole đã phát minh ra một hệ thống đại số trong đó các biến chỉ nhận hai giá trị: True (Đúng, đại diện bởi 1) và False (Sai, đại diện bởi 0). Đây chính là nền tảng lý thuyết cho hệ thống máy tính nhị phân ngày nay. Khi dòng điện đi qua các bóng bán dẫn (transistor), chúng hoạt động như các công tắc đóng mở cực nhanh để biểu diễn các phép toán logic cơ bản này.

Có 4 cổng logic cơ bản xây dựng nên toàn bộ hệ thống tính toán:

  • Cổng NOT (Đảo ngược): Output luôn ngược với input. Nếu \(A = 1\), \(Y = \neg A = 0\).
  • Cổng AND (Và): Output chỉ bằng 1 khi tất cả input bằng 1. Công thức: \(Y = A \land B\).
  • Cổng OR (Hoặc): Output bằng 1 khi có ít nhất một input bằng 1. Công thức: \(Y = A \lor B\).
  • Cổng XOR (Hoặc loại trừ): Output bằng 1 khi hai input có giá trị khác nhau. Công thức: \(Y = A \oplus B = (A \land \neg B) \lor (\neg A \land B)\).
⚠️ Cạm bẫy: Nhầm lẫn giữa Bitwise và Logical trong lập trình
Trong các ngôn ngữ lập trình như C, C++ hay JavaScript, có một sự khác biệt rất lớn giữa toán tử bitwise (tác động lên từng bit của biến) và toán tử logical (tác động lên giá trị Boolean tổng thể).
  • &| là các toán tử Bitwise ANDBitwise OR. Chúng duyệt qua từng bit của dữ liệu và thực hiện phép toán logic tương ứng.
  • &&|| là các toán tử Logical ANDLogical OR. Chúng coi toàn bộ biến là true (khác 0) hoặc false (bằng 0) và áp dụng quy tắc short-circuit (dừng sớm nếu đã biết kết quả).
Ví dụ: \(5 \& 3 = 1\) (vì \(0101_2 \land 0011_2 = 0001_2\)), nhưng \(5 \&\& 3 = \text{true}\) (vì cả hai đều khác 0). Trong thiết kế phần cứng, chúng ta luôn làm việc ở cấp độ bitwise.

Đoạn mã C dưới đây minh họa việc sử dụng toán tử bitwise để thực hiện phép toán logic ở mức bit và cách sử dụng mặt nạ bit (bit mask) để đọc hoặc đảo trạng thái của một bit cụ thể:

bitwise_ops.c
#include <stdio.h>

int main() {
    unsigned char a = 5;  // Hệ nhị phân: 0000 0101
    unsigned char b = 3;  // Hệ nhị phân: 0000 0011
    
    // 1. Phép toán bitwise AND
    printf("a & b (bitwise AND)  = %d\n", a & b);   // Kết quả: 1 (0000 0001)
    
    // 2. Phép toán logical AND
    printf("a && b (logical AND) = %d\n", a && b);  // Kết quả: 1 (True && True)
    
    // 3. Sử dụng mặt nạ bit (Bit Mask) để kiểm tra bit 2 (đếm từ 0)
    unsigned char mask = 0x04; // Hệ nhị phân: 0000 0100
    if ((a & mask) != 0) {
        printf("Bit index 2 cua A dang duoc bat (1)\n");
    }
    
    // 4. Phép toán XOR dùng để đảo bit (toggling)
    a = a ^ mask; // Đảo bit index 2 của a (thành 0000 0001)
    printf("A sau khi toggle bit 2: %d\n", a); // Kết quả: 1
    
    return 0;
}

2. Mạch cộng bán phần & toàn phần (Half / Full Adder)

Khi đã có các cổng logic cơ bản, làm cách nào chúng ta có thể thực hiện phép toán số học như cộng hai số nhị phân? Câu trả lời nằm ở sự kết hợp khéo léo của các cổng logic để tạo ra các mạch cộng phần cứng.

Mạch cộng bán phần (Half Adder)

Mạch cộng bán phần nhận vào 2 bit \(A\) và \(B\) và tạo ra 2 ngõ ra: bit tổng \(Sum\) (\(S\)) và bit nhớ \(Carry-out\) (\(C\)). Nhìn vào bảng chân trị nhị phân, ta thấy: - \(Sum\) chỉ bằng 1 khi một trong hai bit bằng 1 (chính là phép toán XOR): \(S = A \oplus B\). - \(Carry-out\) chỉ bằng 1 khi cả hai bit cùng bằng 1 (chính là phép toán AND): \(C = A \land B\).

Mạch này gọi là "bán phần" vì nó không có ngõ vào cho bit nhớ từ hàng trước chuyển sang.

half_adder.v
module half_adder (
    input a,
    input b,
    output sum,
    output carry
);
    assign sum = a ^ b;      // Cổng XOR
    assign carry = a & b;    // Cổng AND
endmodule

Mạch cộng toàn phần (Full Adder)

Để thực hiện phép cộng các số nhiều bit, mạch cộng bắt buộc phải xử lý được bit nhớ từ hàng trước truyền sang. Đó là lý do mạch cộng toàn phần (Full Adder) ra đời. Nó nhận vào 3 đầu vào: \(A\), \(B\), và bit nhớ ngõ vào \(C_{in}\). Các công thức logic được tính toán như sau: \[S = A \oplus B \oplus C_{in}\] \[C_{out} = (A \land B) \lor (C_{in} \land (A \oplus B))\]

full_adder.v
module full_adder (
    input a,
    input b,
    input cin,
    output sum,
    output cout
);
    wire s1, c1, c2;
    
    // Sử dụng hai bộ Half Adder để tạo nên một bộ Full Adder
    assign s1 = a ^ b;
    assign c1 = a & b;
    
    assign sum = s1 ^ cin;
    assign c2 = s1 & cin;
    
    // Gộp hai nguồn sinh bit nhớ Carry-out
    assign cout = c1 | c2;
endmodule
⚡ Phân tích sâu: Trễ lan truyền bit nhớ (Carry Propagation Delay)
Khi chúng ta ghép nối \(N\) bộ Full Adder nối tiếp nhau để tạo thành một bộ cộng \(N\)-bit (gọi là Ripple Carry Adder - RCA), bit nhớ của cột trước phải truyền sang cột sau. Điều này tạo ra một nút thắt cổ chai về hiệu năng phần cứng gọi là trễ lan truyền. Giả sử trễ lan truyền của mỗi cổng logic là \(\tau\). Trễ của ngõ ra Carry-out qua một bộ Full Adder là \(2\tau\). Công thức tính thời gian trễ cực đại (worst-case delay) của một bộ cộng Ripple Carry Adder \(N\)-bit là: \[T_{\text{delay}} \approx (N-1) \times T_{\text{carry}} + T_{\text{sum}}\] Với bộ cộng 32-bit Ripple Carry, tín hiệu bit nhớ cuối cùng chỉ ổn định sau khoảng hơn 60 cổng logic. Trong các CPU hiện đại, người ta phải thiết kế các mạch phức tạp hơn như bộ cộng đoán trước bit nhớ (Carry Lookahead Adder - CLA) để tính toán nhanh bit nhớ trong thời gian \(O(\log N)\) thay vì \(O(N)\).

So sánh nhanh hai loại mạch cộng vừa xây dựng:

Tiêu chí Half Adder Full Adder
Số ngõ vào 2 (A, B) 3 (A, B, Carry-in)
Công thức Sum \(S = A \oplus B\) \(S = A \oplus B \oplus C_{in}\)
Công thức Carry-out \(C = A \land B\) \(C_{out} = (A \land B) \lor (C_{in} \land (A \oplus B))\)
Ghép chuỗi nhiều bit Không xử lý được carry từ hàng trước — không ghép chuỗi được Ghép nối tiếp N bộ thành bộ cộng N-bit (Ripple Carry Adder)

3. Thiết kế khối ALU 4-bit và các cờ trạng thái

Đơn Vị Số Học Luận Lý (ALU) là trái tim tính toán của CPU. Một bộ ALU đơn giản có thể được tạo ra bằng cách đặt song song các khối chức năng (Bộ cộng, khối AND, khối OR, khối XOR) và sử dụng bộ chọn kênh (Multiplexer - MUX) để quyết định kết quả của phép toán nào sẽ được xuất ra ngoài dựa trên mã lệnh (Opcode).

Để hỗ trợ CPU đưa ra quyết định rẽ nhánh logic (ví dụ lệnh if (a < b)), bộ ALU sẽ sinh ra các cờ trạng thái (Flags) dựa trên kết quả tính toán hiện tại:

  • Cờ Zero (Z): Bằng 1 nếu toàn bộ các bit kết quả bằng 0. Công thức toán học cho kết quả 4-bit \(R\): \(Z = \neg(R_3 \lor R_2 \lor R_1 \lor R_0)\).
  • Cờ Sign (S): Biểu thị dấu của kết quả. Trong hệ bù 2, bit cao nhất (MSB) quyết định dấu. Với số 4-bit: \(S = R_3\). Nếu \(S = 1\), kết quả là số âm.
  • Cờ Carry-out (C): Bằng 1 nếu phép cộng số không dấu bị tràn (vượt quá \(15\)), hoặc phép trừ số không dấu cần mượn.
  • Cờ Overflow (V): Bằng 1 nếu xảy ra hiện tượng tràn số đối với phép tính có dấu (signed).
⚠️ Phân biệt cờ Carry-out (C) và cờ Overflow (V)
Đây là cạm bẫy cực kỳ phổ biến khiến nhiều kỹ sư hiểu sai:
  • Cờ Carry (C) chỉ có ý nghĩa đối với toán học không dấu (unsigned). Nó báo hiệu kết quả vượt quá phạm vi biểu diễn của \(N\) bit (ví dụ với 4-bit, kết quả lớn hơn 15).
  • Cờ Overflow (V) chỉ có ý nghĩa đối với toán học có dấu (signed). Nó báo hiệu phép tính tạo ra kết quả sai dấu do vượt quá giới hạn biểu diễn của số bù 2 (phạm vi từ \(-8\) đến \(+7\) với 4-bit).
Ví dụ: Cộng hai số 4-bit có dấu: \(0101_2\) (\(+5\)) và \(0100_2\) (\(+4\)). - Kết quả nhị phân: \(1001_2\) (đại diện cho \(-7\) trong hệ bù 2). - Rõ ràng \(5 + 4 = 9\) (dương), nhưng kết quả nhị phân lại mang dấu âm. Trạng thái này kích hoạt cờ Overflow V = 1 báo hiệu kết quả bị sai dấu. Cờ Carry lúc này bằng 0 vì phép cộng nhị phân không sinh ra bit nhớ vượt quá bit thứ 4.

Công thức logic phần cứng để xác định cờ Overflow (V) cho kết quả \(R\) của phép cộng \(A + B\) hoặc trừ \(A - B\) trên biến \(N\)-bit (với bit dấu thứ \(N-1\)):

  • Đối với phép cộng: \[V = (A_{N-1} \land B_{N-1} \land \neg R_{N-1}) \lor (\neg A_{N-1} \land \neg B_{N-1} \land R_{N-1})\] (Tràn xảy ra khi cộng hai số cùng dấu nhưng kết quả ra ngược dấu).
  • Đối với phép trừ: \[V = (A_{N-1} \land \neg B_{N-1} \land \neg R_{N-1}) \lor (\neg A_{N-1} \land B_{N-1} \land R_{N-1})\] (Tràn xảy ra khi trừ hai số khác dấu nhưng kết quả ra ngược dấu với số bị trừ).

Dưới đây là mã C mô phỏng trọn vẹn thuật toán hoạt động bên trong một bộ ALU 4-bit phần cứng:

alu_4bit.c
#include <stdio.h>
#include <stdbool.h>

typedef struct {
    unsigned char result; // 4-bit result (0-15)
    bool carry;           // Carry-out flag
    bool overflow;        // Overflow flag
    bool zero;            // Zero flag
    bool sign;            // Sign flag
} ALU_Output;

ALU_Output alu_4bit(unsigned char a, unsigned char b, unsigned char opcode) {
    ALU_Output out = {0};
    a &= 0x0F; // Rút gọn đầu vào về đúng 4-bit
    b &= 0x0F;
    
    unsigned short temp_res = 0;
    
    switch (opcode) {
        case 0: // PHÉP CỘNG (ADD)
            temp_res = (unsigned short)a + b;
            out.result = temp_res & 0x0F;
            out.carry = (temp_res > 0x0F);
            // Kiểm tra Overflow có dấu: hai toán hạng cùng dấu, kết quả khác dấu
            out.overflow = (((a >> 3) == (b >> 3)) && (((a >> 3) ^ (out.result >> 3)) & 1));
            break;
            
        case 1: // PHÉP TRỪ (SUB)
            // Trừ A - B bằng cách cộng A với số bù 2 của B: A + (~B + 1)
            temp_res = (unsigned short)a + ((~b + 1) & 0x0F);
            out.result = temp_res & 0x0F;
            out.carry = (a < b); // Carry-out đối với phép trừ đóng vai trò là cờ mượn (borrow)
            // Kiểm tra Overflow có dấu cho phép trừ: số bị trừ và số trừ khác dấu, kết quả khác dấu số bị trừ
            out.overflow = (((a >> 3) != (b >> 3)) && (((a >> 3) ^ (out.result >> 3)) & 1));
            break;
            
        case 2: // PHÉP AND BITWISE
            out.result = a & b;
            break;
            
        case 3: // PHÉP OR BITWISE
            out.result = a | b;
            break;
            
        case 4: // PHÉP XOR BITWISE
            out.result = a ^ b;
            break;
    }
    
    out.zero = (out.result == 0);
    out.sign = (out.result >> 3) & 1; // Lấy bit cao nhất làm bit dấu
    
    return out;
}

4. Thực hành tương tác: Bộ mô phỏng ALU 4-bit

Dưới đây là bộ mô phỏng trực quan hoạt động của ALU 4-bit. Hãy tùy chỉnh giá trị đầu vào của toán hạng \(A\), \(B\) bằng các nút gạt nhị phân, lựa chọn phép toán số học hoặc luận lý và quan sát kết quả nhị phân, thập phân cùng trạng thái kích hoạt của các cờ. Sơ đồ mạch điện phía dưới sẽ tô sáng đường đi hoạt động tương ứng với phép toán đang chọn.

📟 Trực quan hóa và Giả lập ALU 4-bit
Toán hạng A (4-bit nhị phân) Thập phân: 5 (Có dấu: 5)
Toán hạng B (4-bit nhị phân) Thập phân: 3 (Có dấu: 3)
Đầu vào A: 0101
Đầu vào B: 0011
Kết quả R: 0000 (0)
Kết quả có dấu: 0
Z
S
C
V
A (4-bit) B (4-bit) Khối Số Học (Adder) Khối Luận Lý MUX Opcode Kết quả R (4-bit) Cờ Flags

5. Câu hỏi trắc nghiệm ôn tập

Câu 1

Câu 1: Cho hai số 4-bit có dấu bù 2: \(A = 0101_2\) (thập phân: \(+5\)) và \(B = 0100_2\) (thập phân: \(+4\)). Khi thực hiện phép tính cộng \(A + B\) trong ALU, kết quả nhị phân thu được và trạng thái của các cờ Zero (Z), Sign (S), Overflow (V) là gì?

Câu 2

Câu 2: Điểm khác biệt mấu chốt giữa cờ Carry-out (C) và cờ Overflow (V) trong bộ ALU là gì?

Câu 3

Câu 3: Một bộ cộng song song Ripple Carry Adder 32-bit được ghép từ 32 bộ Full Adder (FA). Biết trễ lan truyền từ ngõ vào đến ngõ ra carry của mỗi bộ FA là \(2\text{ ns}\), và trễ từ ngõ vào đến ngõ ra Sum là \(3\text{ ns}\). Trễ lan truyền cực đại (worst-case delay) của bộ cộng 32-bit này là bao nhiêu?

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 1 vừa thêm half/full adder, bộ cộng ripple-carry và ALU 4-bit (ADD/SUB/AND/OR/XOR) cùng 4 cờ Zero/Sign/Carry/Overflow, 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 2: Kiến Trúc Von Neumann & Tập Lệnh ISA Quay lại Lộ trình Kiến Trúc Máy Tính