← Máy tính hoạt động thế nào?

Bài 7 · Nâng cao · 28 phút· Cập nhật 11/06/2026

Lập trình cho CPU

Biên soạn bởi Nguyễn Anh Tuấn

Tự viết chương trình hợp ngữ (assembly) cho CPU 8-bit: nạp/cộng/ghi, nhảy, rẽ nhánh JZ và vòng lặp - dịch ra mã máy rồi chạy ngay.

Ở Bài “Mô hình CPU 8-bit” bạn đã thấy CPU chạy theo chu trình Nạp → Giải mã → Thực thi, lần lượt từng ô. Vậy “lập trình” chỉ là: viết một danh sách lệnh đúng thứ tự để CPU làm việc bạn muốn. Bộ lệnh của CPU này chỉ gồm 9 lệnh (kèm vài chỉ dẫn trình dịch sẽ giới thiệu ngay sau):

LệnhOpcodeÝ nghĩa
LDI n0x5Nạp thẳng số n (0-15) vào thanh ghi A
LDA đc0x1A ← giá trị đọc từ ô nhớ địa chỉ đc
ADD đc0x2A ← A + giá trị ở ô đc
SUB đc0x3A ← A − giá trị ở ô đc
STA đc0x4Ghi giá trị của A vào ô đc
JMP đc0x6Nhảy: cho CPU chạy tiếp từ ô đc
JZ đc0x7Nhảy tới đc NẾU kết quả vừa rồi = 0
OUT0xEHiện A ra “đèn” kết quả
HLT0xFDừng chương trình

Tên lệnh không phải chữ ngẫu nhiên - chúng là viết tắt tiếng Anh (các chữ in đậm ghép lại thành tên lệnh):

  • LDA LoaD Accumulator - nạp giá trị vào thanh ghi A (accumulator = thanh ghi “tích luỹ”)
  • LDI LoaD Immediate - nạp thẳng một số (immediate = số nằm ngay trong lệnh)
  • ADD ADD - cộng
  • SUB SUBtract - trừ
  • STA STore Accumulator - lưu (store) giá trị của A xuống ô nhớ
  • JMP JuMP - nhảy
  • JZ Jump if Zero - nhảy nếu kết quả bằng 0 (zero)
  • OUT OUTput - xuất / hiện A ra ngoài
  • HLT HaLT - dừng (halt)

(Ngoài ra còn NOP = No OPeration - “không làm gì”.)

Trong chương trình bạn sẽ còn gặp vài từ khác - ORG, DAT, nhãn (lap:) và dấu ;. Chúng KHÔNG phải lệnh CPU (không có opcode, CPU không chạy chúng) mà là chỉ dẫn cho trình dịch (assembler directive) - mách trình dịch cách bố trí bộ nhớ:

  • ORG n ORiGin - đặt mục TIẾP THEO vào ô số n (vd ORG 14 = “từ đây xếp vào ô 14”)
  • DAT n DATa - khai báo một ô DỮ LIỆU mang giá trị n (0-255), vd DAT 5
  • nhãn: đặt TÊN cho địa chỉ ô hiện tại (vd lap:); trình dịch tự thay nhãn bằng số khi dịch
  • ; chú thích (comment) - trình dịch bỏ qua phần sau dấu ;

Mấu chốt: lệnh được dịch thành byte mã máy và CPU CHẠY; còn chỉ dẫn chỉ giúp trình dịch SẮP XẾP bộ nhớ, bản thân nó không trở thành lệnh chạy.

Trung thực: đây là CPU dạy học, KHÔNG phải hợp ngữ chuẩn

Tập 9 lệnh trên là tự tạo và đơn giản hoá cho việc học - KHÔNG phải một hợp ngữ tiêu chuẩn (như x86 hay ARM) chạy trong máy thật, và sẽ không chạy được trên CPU thật. Cách đặt tên lệnh (LDA, STA, JMP, JZ…) thì theo đúng quy ước của các CPU thật - vd chip 6502 trong máy NES/Apple II, hay x86 - và mô hình này thuộc dòng CPU dạy học kinh điển: SAP-1máy 8-bit của Ben Eater. Hợp ngữ thật lớn hơn rất nhiều (hàng trăm lệnh, nhiều thanh ghi, ngăn xếp…); ở đây ta cố ý thu nhỏ để thấy rõ từng byte chạy ra sao.

Nhớ lại cấu trúc byte lệnh

Mỗi lệnh là một byte: nửa cao là opcode (làm gì), nửa thấp là toán hạng (với ô nào / số nào) - đúng như Bài Hệ nhị phân.
opcode (4 bit)toán hạng (4 bit)000110100001 = 11010 = 10LDA 10nạp giá trị ở ô nhớ số 10 vào thanh ghi A
Một byte lệnh: nibble cao là opcode (làm gì), nibble thấp là toán hạng. 0001 1010 = LDA 10.

Cộng hai số rồi hiện kết quả. Vì 5 và 3 đã có sẵn trong hai ô nhớ, ta nạp số thứ nhất vào A, cộng số thứ hai, rồi hiện ra và dừng. Đọc kỹ phần chú thích, sửa thử, và bấm các ví dụ khác:

Mã máy (6 byte / 16 ô):

[0]0001 1110LDA 14
[1]0010 1111ADD 15
[2]1110 0000OUT
[3]1111 0000HLT
[14]0000 0101DAT 5
[15]0000 0011DAT 3

Kết quả chạy (đèn OUT):

8

A cuối = 8 · 12 bước

Sửa chương trình bên trái - mã máy và kết quả tự cập nhật. Bấm một ví dụ ở trên để nạp nhanh. (Muốn xem từng bước chạy trên bảng mạch? Mở Bài “Mô hình CPU 8-bit”.)

  • LDA 14 / ADD 15: làm việc với SỐ đang nằm trong ô nhớ 14, 15.
  • OUT hiện A ra “đèn”; HLT dừng máy.
  • ORG 14 + DAT đặt dữ liệu vào ô 14, 15 (tách khỏi vùng lệnh).

CPU này chỉ có 16 ô (Bài Bộ nhớ & RAM). Cả lệnh lẫn dữ liệu đều phải nằm trong đó. CPU bắt đầu chạy từ ô 0 nên ta xếp lệnh ở các ô thấp, rồi dùng ORG để đặt dữ liệu ra các ô cao - kẻo CPU “chạy nhầm” dữ liệu như thể nó là lệnh.

  • Ô 0,1,2,…: các byte lệnh, CPU chạy lần lượt.
  • ORG 14: “từ đây là dữ liệu”, đặt ở ô 14 trở đi.
  • DAT 5: một ô dữ liệu mang giá trị 5.
  • Hết 16 ô là hết - đó là vì sao chương trình phức tạp không nhét vừa.

Bình thường CPU chạy ô kế tiếp. Lệnh JMP đc đổi điều đó: nó cho CPU nhảy tới ô khác. Nhảy ngược về phía trên tạo thành vòng lặp. Ví dụ “Đếm lên” cộng 1 mãi mãi:

; Đếm lên vô hạn - minh hoạ vòng lặp
; Lưu ý: đếm quá 255 sẽ TRÀN SỐ và CPU dừng
LDI 1       ; A ← 1
lap:        ; nhãn cho vòng lặp
OUT         ; hiện A
ADD 15      ; A ← A + 1
JMP lap     ; quay lại nhãn "lap"

ORG 15
DAT 1       ; ô 15 = 1

Vô hạn… cho tới khi tràn

Vòng lặp này không có điểm dừng - nhưng cộng tới 256 thì A vượt 8 bit → TRÀN SỐ và CPU dừng cứng. Nạp ví dụ “Đếm lên” ở công cụ Bước 2 để xem nó in 1, 2, 3, … rồi dừng ở 255.

Muốn máy quyết định, ta cần rẽ nhánh. JZ đc chỉ nhảy khi kết quả vừa tính ra bằng 0 (cờ ZERO bật). Mẹo so sánh hai số: lấy hiệu của chúng - nếu hiệu = 0 thì chúng bằng nhau.

; Hai số có bằng nhau không?
; Hiện 1 nếu bằng, 0 nếu khác.
; Cách làm: lấy hiệu, nếu = 0 thì bằng nhau
LDA 13    ; A ← số X (ô 13)
SUB 14    ; A ← X − Y
JZ bang   ; nếu hiệu = 0 → nhảy "bang"
LDI 0     ; khác nhau → A = 0
OUT
HLT
bang:
LDI 1     ; bằng nhau → A = 1
OUT
HLT

ORG 13
DAT 6     ; ← số X (đổi để thử)
DAT 6     ; ← số Y (đổi để thử)
  • JZ giống một “lối rẽ có điều kiện”: kết quả vừa rồi bằng 0 thì rẽ sang ô khác, không thì đi thẳng.
  • Ghép SUB + JZ là cách kiểm tra “hai số có bằng nhau không”.
  • Chỉ có JZ (không có “nhảy nếu âm/nhớ”) nên chia chỉ phát hiện được chia hết.

Đến lượt bạn. Dùng sân chơi dưới đây: gõ chương trình, mã máy và kết quả tự cập nhật. Nếu sai, trình dịch sẽ chỉ rõ dòng lỗi. Thử ví dụ “Tổng 1+…+N” - nó gói gọn cả vòng lặp lẫn rẽ nhánh:

Mã máy (6 byte / 16 ô):

[0]0001 1110LDA 14
[1]0010 1111ADD 15
[2]1110 0000OUT
[3]1111 0000HLT
[14]0000 0101DAT 5
[15]0000 0011DAT 3

Kết quả chạy (đèn OUT):

8

A cuối = 8 · 12 bước

Sửa chương trình bên trái - mã máy và kết quả tự cập nhật. Bấm một ví dụ ở trên để nạp nhanh. (Muốn xem từng bước chạy trên bảng mạch? Mở Bài “Mô hình CPU 8-bit”.)

  • Lỗi hay gặp: số quá phạm vi (toán hạng > 15, DAT > 255).
  • Lỗi hay gặp: chương trình dài quá 16 ô → “ô bị ghi đè”.
  • Mẹo: viết từng phần nhỏ, chạy thử liên tục, đọc kỹ chú thích.

Hết chương phần cứng 🎉

Bạn vừa đi hết chặng đường: xung nhịp → cổng logic → nhị phân → phép toán bit → bộ nhớ → CPU → và giờ là tự viết chương trình cho nó chạy. Chẳng có gì thần bí - vẫn là các công tắc ấy, giờ bạn điều khiển chúng bằng một danh sách lệnh.

Câu hỏi thường gặp

Vì toán hạng chỉ có 4 bit (nửa thấp của byte lệnh), mà 4 bit đếm được 0-15. Muốn có số lớn hơn, ta đặt nó vào một ô nhớ (DAT) rồi dùng LDA để nạp.

LDA thay thế A bằng giá trị ở ô nhớ (A ← ô). ADD thì cộng thêm vào A (A ← A + ô). Muốn bắt đầu một phép tính, thường LDA trước rồi ADD/SUB sau.

CPU KHÔNG hiểu nhãn; nó chỉ hiểu địa chỉ số. Nhãn là tiện ích cho người viết: trình dịch (assembler) tự thay mỗi nhãn bằng địa chỉ ô tương ứng khi dịch ra mã máy.

CPU chạy lệnh ở ô mà con trỏ lệnh (PC) đang trỏ tới, lần lượt từ ô 0. Ta dùng ORG để đặt dữ liệu ra các ô cao (vd 13-15) để CPU không “chạy nhầm” dữ liệu. Sắp xếp sai là chương trình hỏng.

Vì chỉ có 16 ô. Nhân bằng vòng lặp hay chia tổng quát cần ~13 lệnh + vài ô dữ liệu - quá 16 ô, trình dịch sẽ báo “ô bị ghi đè”. Đây là bài học thật về giới hạn phần cứng, không phải lỗi.

Trên CPU thật thì nó chạy mãi. Ở mô hình này, ví dụ “Đếm lên” cộng mãi sẽ vượt 255 → TRÀN SỐ và CPU dừng cứng. Đó là cách giới hạn 8-bit tự bộc lộ.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Khác nhau cốt lõi giữa lệnh CPU (như ADD) và chỉ dẫn trình dịch (như ORG, DAT) là gì?

  1. 1

    Đổi số hạng

    Mở công cụ ở Bước 2, sửa chương trình “Cộng 5 + 3” thành 7 + 2 (đổi hai dòng DAT). Kết quả ra gì?

    Hoàn thành khi: DAT 7DAT 2 → đèn hiện 9.

  2. 2

    Viết phép trừ

    Tự viết chương trình tính 10 − 6 rồi hiện kết quả (gợi ý: LDA, SUB, OUT, HLT, ORG, DAT).

    Hoàn thành khi: LDA 14 / SUB 15 / OUT / HLT / ORG 14 / DAT 10 / DAT 6 → ra 4.

  3. 3

    Nhân bằng cộng lặp

    Viết chương trình tính 5 × 3 bằng cách cộng (không có lệnh nhân).

    Hoàn thành khi: LDI 0 rồi ADD ô-chứa-5 ba lần (hoặc ADD ô-chứa-3 năm lần) → OUT ra 15.

  4. 4

    Đọc vòng lặp

    Nạp ví dụ “Đếm lên”. Vì sao nó dừng, và số cuối cùng in ra là bao nhiêu?

    Hoàn thành khi: Cộng tới 256 thì TRÀN SỐ (vượt 8 bit) nên CPU dừng; số cuối cùng in được là 255.

  5. 5

    Rẽ nhánh với JZ

    Nạp ví dụ “So sánh bằng nhau”. Đổi hai số để chúng KHÁC nhau rồi chạy. Kết quả đổi thế nào, vì sao?

    Hoàn thành khi: Hai số khác nhau → hiệu ≠ 0 → JZ không nhảy → in ra 0 (thay vì 1).

  6. 6

    Tự viết có vòng lặp

    Dùng ví dụ “Tổng 1+…+N” (hoặc tự viết), tính tổng 1+2+3. Đặt N = 3 và chạy.

    Hoàn thành khi: Đặt DAT N = 3 → chương trình lặp cộng dồn → OUT ra 6.