Quay lại
08/04/2026 32 phút

Chương 21: Đơn vị logic số học

Máy tính hiện đại là một tập hợp phức tạp của vô số linh kiện, nhưng chúng có thể được chia tạm thành ba loại chính:

  • Bộ nhớ (Memory)
  • Bộ xử lý trung tâm (Central processing unit), hay CPU
  • Thiết bị đầu vào và đầu ra (I/O), thường được gọi là thiết bị ngoại vi

Bạn đã tìm hiểu ở Chương 19 cách bộ nhớ truy cập ngẫu nhiên (RAM) được xây dựng và cấu trúc, cũng như cách mỗi byte trong bộ nhớ được truy cập thông qua một địa chỉ. Ở Chương 20, bạn đã thấy cách nội dung của bộ nhớ có thể lưu trữ các con số, và cách các mã lệnh lưu trong bộ nhớ có thể điều khiển các mạch điện để thao tác với những con số này. Trong trường hợp tổng quát hơn, nội dung của bộ nhớ cũng có thể chứa văn bản, hình ảnh, âm nhạc, phim ảnh và bất cứ thứ gì có thể được biểu diễn dưới dạng kỹ thuật số—tức là bằng các số 0 và 1. Các mã lệnh (instruction codes) được lưu trữ trong bộ nhớ thường được gọi chung là (code), và mọi thứ khác là dữ liệu (data). Nói cách khác, bộ nhớ chứa mã và dữ liệu.

Máy tính cũng bao gồm một số thiết bị đầu vào và đầu ra (I/O), thường được gọi là thiết bị ngoại vi. Một máy tính cụ thể bao gồm những thiết bị ngoại vi nào phụ thuộc rất nhiều vào việc máy tính đó nằm trên bàn làm việc, gập lại kẹp nách, nằm gọn trong túi quần, túi xách, hay được giấu kín bên trong lò vi sóng, robot hút bụi hoặc xe hơi.

Các thiết bị I/O dễ thấy nhất trên máy tính để bàn là màn hình, bàn phím và chuột, cùng với một chiếc máy in nằm ở góc phòng. Laptop có thể có bàn di chuột (touchpad) thay vì chuột, trong khi điện thoại thực hiện tất cả các chức năng đó trên một màn hình duy nhất. Tất cả những máy tính này đều có một thiết bị lưu trữ dung lượng lớn, có thể là ổ cứng trên máy tính để bàn, ổ cứng thể rắn (SSD) trên laptop và bộ nhớ flash trên điện thoại, có thể được mở rộng bằng bộ nhớ ngoài như ổ đĩa USB.

Các thiết bị I/O khác thì khó nhận thấy hơn, chẳng hạn như các mạch điện để phát âm thanh và âm nhạc, các mạch kết nối internet qua cổng ethernet hoặc Wi-Fi, các mạch thu tín hiệu Hệ thống Định vị Toàn cầu (GPS) để cho biết bạn đang ở đâu và sẽ đi về đâu, và thậm chí cả các thiết bị phát hiện trọng lực và chuyển động để xác định xem điện thoại của bạn đang quay hướng nào và di chuyển ra sao so với mặt đất.

Nhưng chủ đề của chương này (và ba chương tiếp theo) là CPU, thứ đôi khi được gọi là "trái tim", "linh hồn" hoặc "bộ não" của máy tính, tùy thuộc vào cách ví von mà bạn thích.

Chương 20 đã mô tả một Bộ tích lũy Ba-Byte (Triple-Byte Accumulator) bao gồm một bộ đếm để truy cập bộ nhớ, một bộ cộng và các chốt. Mọi thứ được điều khiển bởi mạch điện sử dụng các mã lệnh lưu trong bộ nhớ để cộng và trừ số, và sau đó ghi tổng cộng dồn vào bộ nhớ.

Một CPU rất giống với Bộ tích lũy Ba-Byte, ngoại trừ việc nó được tổng quát hóa để phản hồi lại nhiều mã lệnh khác. Nhờ vậy, CPU đa năng hơn rất nhiều so với cỗ máy trước đó.

CPU mà tôi bắt đầu xây dựng trong những trang sách này sẽ làm việc với các byte. Có nghĩa là nó được phân loại là CPU 8-bit hoặc bộ vi xử lý 8-bit. Nhưng nó sẽ có khả năng định địa chỉ (addressing) 64K bộ nhớ truy cập ngẫu nhiên, đòi hỏi một địa chỉ bộ nhớ 16-bit, tức 2 byte. Mặc dù CPU 8-bit chủ yếu làm việc với các byte, nó cũng phải có khả năng làm việc với các giá trị 16-bit ở một mức độ hạn chế khi kết nối với địa chỉ bộ nhớ này.

Dù CPU này sẽ không tồn tại trong thế giới thực, nhưng (ít nhất là trên lý thuyết) nó sẽ có khả năng đọc mã và dữ liệu từ bộ nhớ để thực hiện nhiều loại tác vụ số học và logic khác nhau. Xét về khả năng xử lý số học và logic, nó sẽ tương đương với bất kỳ máy tính kỹ thuật số nào dù tinh vi đến đâu.

Qua nhiều năm, khi CPU 8-bit nhường chỗ cho CPU 16-bit rồi CPU 32-bit và 64-bit, những CPU tiên tiến hơn này không hẳn là có thêm khả năng thực hiện các loại tác vụ xử lý mới. Thay vào đó, chúng thực hiện các tác vụ tương tự nhưng nhanh hơn. Trong một số trường hợp, tốc độ thay đổi mọi thứ—ví dụ, khi CPU đang giải mã một luồng dữ liệu mã hoá một bộ phim. Một CPU 8-bit cũng có thể thực hiện cùng quá trình xử lý này, nhưng nó có thể sẽ quá chậm để hiển thị bộ phim ở đúng tốc độ để chiếu.

Mặc dù CPU 8-bit thực hiện các phép toán số học và logic trên các byte, nó cũng sẽ có khả năng làm việc với các con số đòi hỏi nhiều byte. Ví dụ, giả sử bạn muốn cộng hai số 16-bit, có thể là 1388h và 09C4h (giá trị thập lục phân của 5.000 và 2.500). Bạn sẽ nhập các giá trị sau vào bộ nhớ để CPU xử lý:

           +-------+
  0000h:   |  3Eh  |
           +-------+
           |  88h  |
           +-------+
           |  C6h  |
           +-------+
           |  C4h  |
           +-------+
           |  32h  |
           +-------+
           |  10h  |
           +-------+
           |  00h  |
           +-------+
           |  3Eh  |
           +-------+
           |  13h  |
           +-------+
           |  CEh  |
           +-------+
           |  09h  |
           +-------+
           |  32h  |
           +-------+
           |  11h  |
           +-------+
           |  00h  |
           +-------+
           |  76h  |
           +-------+
           |  00h  |
           +-------+
  0010h:   |  00h  |
           +-------+
           |  00h  |
           +-------+

Dĩ nhiên, tất cả các byte đó có thể chẳng mang nhiều ý nghĩa lắm vì chúng là một mớ hỗn độn giữa mã lệnh và dữ liệu, và bạn có lẽ cũng không biết mã lệnh đó là gì. Đây là phiên bản đã được chú thích:

           +-------+
  0000h:   |  3Eh  | Mã để đưa byte kế tiếp vào CPU
           +-------+
           |  88h  | Byte thấp của 1388h (số thập phân 5.000)
           +-------+
           |  C6h  | Mã để cộng byte kế tiếp vào giá trị trong CPU
           +-------+
           |  C4h  | Byte thấp của 09C4h (số thập phân 2.500)
           +-------+
           |  32h  | Mã để lưu kết quả tại địa chỉ bộ nhớ trong 2 byte tiếp theo
           +-------+ ---+
           |  10h  |    |
           +-------+    |---> Địa chỉ bộ nhớ 0010h
           |  00h  |    |
           +-------+ ---+
           |  3Eh  | Mã để đưa byte kế tiếp vào CPU
           +-------+
           |  13h  | Byte cao của 1388h
           +-------+
           |  CEh  | Mã để cộng byte kế tiếp kèm nhớ vào giá trị trong CPU
           +-------+
           |  09h  | Byte cao của 09C4h
           +-------+
           |  32h  | Mã để lưu kết quả tại địa chỉ bộ nhớ trong 2 byte tiếp theo
           +-------+ ---+
           |  11h  |    |
           +-------+    |---> Địa chỉ bộ nhớ 0011h
           |  00h  |    |
           +-------+ ---+
           |  76h  | Mã để dừng CPU
           +-------+
           |  00h  |
           +-------+ ---+
  0010h:   |  00h  |    |
           +-------+    |---> Nơi lưu kết quả
           |  00h  |    |
           +-------+ ---+

Một chuỗi các lệnh như này được gọi là một chương trình máy tính. (Chắc bạn cũng đoán ra rồi!) Đây là một chương trình khá đơn giản để cộng 1388h (tức 5.000 trong hệ thập phân) và 09C4h (2.500 thập phân). Đầu tiên, CPU cộng các byte thấp của hai giá trị 16-bit (88h và C4h), và kết quả được lưu tại địa chỉ bộ nhớ 0010h. Sau đó, hai byte cao (13h và 09h) được cộng kèm theo một cờ nhớ có thể phát sinh từ phép cộng đầu tiên. Tổng đó được lưu ở địa chỉ 0011h. Rồi CPU dừng lại. Tổng 16-bit này sẽ nằm ở địa chỉ 0010h và 0011h, nơi có thể kiểm tra được.

Tôi lấy các mã cụ thể 3Eh, C6h, 32h, CEh và 76h ở đâu ra? Ở thời điểm này, tôi thậm chí còn chưa bắt đầu xây dựng CPU, nên tôi hoàn toàn có thể tự bịa ra chúng. Nhưng tôi đã không làm vậy. Thay vào đó, tôi sử dụng các mã lệnh thực tế được thực thi bởi bộ vi xử lý Intel 8080 nổi tiếng, vốn được dùng trong MITS Altair 8800, chiếc máy tính cá nhân được coi là thành công về mặt thương mại đầu tiên trên thế giới. Chiếc IBM PC đầu tiên không dùng bộ vi xử lý 8080, nhưng nó dùng Intel 8088, (như con số đã ngầm chỉ ra) là thế hệ tiếp theo của dòng bộ vi xử lý này.

Trong chương này và vài chương tiếp theo, tôi sẽ sử dụng Intel 8080 làm mô hình để thiết kế CPU của riêng mình. Nhưng chỉ là mô hình thôi nhé. CPU của tôi sẽ chỉ thực thi một tập con của 8080. Intel 8080 thực thi 244 mã lệnh, nhưng khi CPU của tôi hoàn thành, nó sẽ chỉ thực thi hơn một nửa số đó. Dù sao đi nữa, bạn vẫn sẽ có một cái nhìn rất rõ ràng về những gì đang diễn ra ở chính trung tâm (hay linh hồn, bộ não) của một chiếc máy tính.

Tôi vẫn gọi các mã này là mã lệnh, mã thao tác (operation codes) hay opcode. Chúng cũng được gọi là mã máy (machine codes) vì chúng được sử dụng trực tiếp bởi máy—chính là các mạch điện cấu thành nên bộ xử lý trung tâm. Chương trình máy tính nhỏ xíu ở trên là một ví dụ của một chương trình mã máy.

Tất cả các mã lệnh của 8080 chỉ dài đúng 1 byte. Tuy nhiên, một số lệnh cần thêm 1 hoặc 2 byte nối tiếp sau byte lệnh đó. Trong ví dụ trên, các mã lệnh 3Eh, C6h và CEh luôn được theo sau bởi một byte khác. Chúng được gọi là lệnh 2-byte vì byte theo sau mã thao tác thực chất là một phần của cùng lệnh đó. Mã lệnh 32h được theo sau bởi 2 byte để xác định một địa chỉ bộ nhớ. Đây là một trong số vài lệnh 3-byte. Nhiều lệnh không yêu cầu thêm bất kỳ byte nào, chẳng hạn như mã 76h, dùng để dừng CPU. Sự đa dạng về độ dài của các lệnh này chắc chắn sẽ làm phức tạp hóa quá trình thiết kế CPU.

Chuỗi mã và dữ liệu cụ thể trong ví dụ trước không phải là cách tốt nhất để cộng hai số 16-bit với nhau. Các mã lệnh và dữ liệu bị trộn lẫn vào nhau. Thường thì tốt hơn là nên giữ mã và dữ liệu ở các khu vực tách biệt trong bộ nhớ. Bạn sẽ hiểu rõ hơn về cách thức hoạt động của nó trong chương tiếp theo.

Bản thân bộ xử lý trung tâm (CPU) bao gồm nhiều thành phần khác nhau. Phần còn lại của chương này sẽ tập trung vào phần cơ bản nhất của CPU, được gọi là đơn vị logic số học, hay ALU (Arithmetic Logic Unit). Đây là bộ phận của CPU đảm nhiệm việc cộng và trừ, cũng như thực hiện một vài tác vụ hữu ích khác.

Trong một CPU 8-bit, ALU chỉ có khả năng cộng và trừ 8-bit. Nhưng rất thường xuyên, chúng ta cần làm việc với các con số rộng 16 bit, 24 bit, hoặc 32 bit, và thậm chí lớn hơn. Như bạn đã thấy, những con số lớn này phải được cộng và trừ theo từng byte một, bắt đầu từ byte ít quan trọng nhất. Mỗi phép cộng hoặc trừ 1-byte tiếp theo phải tính đến cờ nhớ từ phép toán trước đó.

Điều này ngụ ý rằng ALU của chúng ta phải có khả năng thực hiện các phép toán cơ bản sau:

  • Cộng một số 8-bit với một số khác.
  • Cộng một số 8-bit với một số khác kèm thêm cờ nhớ (nếu có) từ phép cộng trước. Đây được gọi là cộng có cờ nhớ (addition with carry).
  • Trừ một số 8-bit cho một số khác.
  • Trừ một số 8-bit cho một số khác trừ đi cờ nhớ (nếu có) từ phép trừ trước. Đây được gọi là trừ có cờ nhớ (subtraction with carry) hoặc phổ biến hơn là trừ có mượn (subtraction with borrow), về cơ bản chỉ là cách dùng từ khác đi cho cùng một việc.

Để thuận tiện, hãy rút gọn mô tả của bốn phép toán này:

  • Cộng (Add)
  • Cộng có cờ nhớ (Add with Carry)
  • Trừ (Subtract)
  • Trừ có mượn (Subtract with Borrow)

Dần dần tôi sẽ còn viết tắt các mô tả này hơn nữa. Hãy nhớ rằng các phép toán Cộng có cờ nhớ và Trừ có mượn sử dụng bit nhớ từ phép cộng hoặc phép trừ trước đó. Bit đó có thể là 0 hoặc 1, tùy thuộc vào việc phép toán có sinh ra cờ nhớ hay không. Điều này có nghĩa là ALU phải lưu lại bit nhớ từ một phép toán để sử dụng cho phép toán tiếp theo.

Như thường lệ, việc xử lý bit nhớ làm cho các phép tính số học cơ bản trở nên phức tạp hơn đáng kể so với khi không có.

Ví dụ, giả sử bạn cần cộng một cặp số 32-bit (mỗi số gồm 4 byte). Đầu tiên, bạn sẽ cộng hai byte ít quan trọng nhất. Phép cộng đó có thể sinh ra một cờ nhớ, hoặc không. Hãy gọi nhớ đó là Cờ nhớ (Carry flag), vì nó biểu thị rằng có nhớ phát sinh từ phép cộng. Cờ nhớ đó có thể là 0 hoặc 1. Sau đó bạn sẽ cộng hai byte quan trọng tiếp theo cùng với Cờ nhớ từ phép cộng trước đó, rồi tiếp tục với các byte còn lại.

Phép cộng một cặp số 32-bit đòi hỏi bốn phép toán cho bốn cặp byte:

  • Cộng
  • Cộng có nhớ
  • Cộng có nhớ
  • Cộng có nhớ

Quá trình cũng diễn ra tương tự đối với phép trừ, ngoại trừ việc số bị trừ được chuyển đổi thành số bù hai, như đã thảo luận ở Chương 16: Tất cả các bit 0 trở thành 1, và tất cả các bit 1 trở thành 0. Đối với byte đầu tiên của một số đa byte, số 1 sẽ được cộng vào bằng cách thiết lập đầu vào nhớ của bộ cộng. Do đó, một phép trừ số 32-bit này cho số 32-bit khác cũng yêu cầu bốn phép toán:

  • Trừ
  • Trừ có mượn
  • Trừ có mượn
  • Trừ có mượn

Tôi muốn đóng gói phần mạch điện thực hiện các phép cộng và trừ này vào trong một chiếc hộp như sau:

F1 F0 A B CY Vào CY Ra Bộ Cộng/Trừ Đầu ra 8 8 8
Bộ Cộng/Trừ (với nhớ, mượn)


Hộp này trông không quá lạ lẫm. Hai đầu vào 8-bit được cộng hoặc trừ để tạo ra một đầu ra 8-bit. Nhưng chiếc hộp này có vài điểm khác biệt so với các hộp tương tự mà bạn đã thấy.

Thông thường khi dán nhãn một bộ cộng 8-bit, tôi hay dùng CI cho Carry In và CO cho Carry Out. Nhưng hộp này được dán nhãn hơi khác một chút. Tôi đang dùng chữ viết tắt CY để đại diện cho Cờ nhớ. Như bạn sẽ thấy, CY Out giống hệt với Carry Out từ bộ cộng, nhưng CY In là Cờ nhớ từ phép cộng hoặc trừ trước đó, và nó có thể không giống với Carry In của bộ cộng.

Một điểm mới nữa trong sơ đồ này là hai đầu vào có nhãn F0 và F1. Chữ F là viết tắt của "chức năng" (function), và hai đầu vào này chi phối những gì diễn ra bên trong hộp:

+-------+-------+-------------+
|  F1   |  F0   |  Phép tính  |
+-------+-------+-------------+
|   0   |   0   | Cộng        |
+-------+-------+-------------+
|   0   |   1   | Cộng có nhớ |
+-------+-------+-------------+
|   1   |   0   | Trừ         |
+-------+-------+-------------+
|   1   |   1   | Trừ có mượn |
+-------+-------+-------------+

Hãy nhớ rằng chúng ta đang xây dựng một hệ thống hoạt động cùng với các mã lệnh được lưu trữ trong bộ nhớ. Nếu thiết kế các mã lệnh này thông minh, thì hai bit của các mã đó có thể được dùng để cung cấp các đầu vào chức năng cho khối Add/Subtract này, giống hệt như các bit cho mã lệnh Cộng và Trừ ở chương trước. Phần lớn khối Add/Subtract này trông khá quen thuộc:

Bù 1 Vào Ra Đảo Bộ Cộng 8-Bit A B CI CO Ra CY Ra 8 8 8 8
Bên trong khối Cộng/Trừ


Khối có nhãn Ones' Complement (Bù một) sẽ đảo ngược đầu vào khi tín hiệu Inv ("đảo") là 1. Đây là bước đầu tiên cần thiết để chuyển đổi thành số bù hai khi thực hiện phép trừ.

Carry Out từ bộ cộng sẽ trở thành CY Out của khối Add/Subtract. Nhưng sơ đồ này còn thiếu tín hiệu Inv cho khối Ones' Complement và tín hiệu CI cho bộ cộng. Tín hiệu Inv phải là 1 cho trừ, nhưng CI thì phức tạp hơn một chút. Hãy xem liệu nó có thể được làm rõ bằng một sơ đồ logic hay không:

+-------+-------+-------------+-------+-------+
|  F1   |  F0   |  Chức năng  |  Inv  |  CI   |
+-------+-------+-------------+-------+-------+
|   0   |   0   | Cộng        |   0   |   0   |
+-------+-------+-------------+-------+-------+
|   0   |   1   | Cộng có nhớ |   0   |   CY  |
+-------+-------+-------------+-------+-------+
|   1   |   0   | Trừ         |   1   |   1   |
+-------+-------+-------------+-------+-------+
|   1   |   1   | Trừ có mượn |   1   |   CY  |
+-------+-------+-------------+-------+-------+

Bảng này cho thấy tín hiệu Inv tới bộ đảo Ones' Complement giống hệt với F1. Khá dễ hiểu! Nhưng đầu vào CI của bộ cộng thì lằng nhằng hơn chút. Nó là 1 cho phép toán Trừ. Đó là byte đầu tiên của một phép trừ đa byte, khi chúng ta cần cộng thêm 1 vào số bù một để lấy số bù hai. Nếu F0 là 1, thì CI chính là cờ CY từ phép cộng hoặc trừ trước đó. Tất cả điều này có thể đạt được bằng mạch điện sau:

F1 F0 CY Vào Inv CI
Mạch 4 chức năng


Một phiên bản tương tác của khối Add/Subtract hoàn chỉnh hiện đã có mặt trên trang web CodeHiddenLanguage.com.

Bạn muốn bộ logic số học này làm gì khác ngoài cộng và trừ? Nếu câu trả lời là "nhân và chia", thì e là bạn sẽ phải thất vọng rồi. Nếu bạn thấy việc xây dựng mạch điện để cộng và trừ đã khó rồi thì cứ thử tưởng tượng độ phức tạp logic của phép nhân và chia xem! Dù việc chế tạo một mạch như vậy là hoàn toàn khả thi, nhưng nó lại vượt quá tham vọng khiêm tốn của cuốn sách này. Và vì bản thân Intel 8080 không hề tích hợp phép nhân hay chia, nên CPU của tôi cũng vậy. Tuy nhiên, nếu bạn đủ kiên nhẫn, bạn sẽ thấy vào cuối Chương 24, chiếc CPU mà chúng ta đang chế tạo sẽ có đủ các công cụ cơ bản để thực hiện phép nhân.

Thay vì bận tâm đến phép nhân, hãy nghĩ về từ thứ hai trong cụm bộ logic số học. Trong sách này, từ logic thường nhắc đến các phép toán Boolean. Chúng có thể hữu ích như thế nào?

Giả sử bạn có các mã ASCII sau được lưu trong bộ nhớ bắt đầu tại một địa chỉ tùy ý:

           +-------+
  1000h:   |  54h  |  T
           +-------+
           |  6Fh  |  o
           +-------+
           |  6Dh  |  m
           +-------+
           |  53h  |  S
           +-------+
           |  61h  |  a
           +-------+
           |  77h  |  w
           +-------+
           |  79h  |  y
           +-------+
           |  65h  |  e
           +-------+
           |  72h  |  r
           +-------+


Có thể bạn muốn chuyển tất cả đoạn văn bản trên thành chữ thường (lowercase).

Nếu bạn lướt qua trang 154 ở Chương 13, bạn sẽ thấy rằng các mã ASCII cho chữ hoa nằm trong khoảng từ 41h đến 5Ah, và mã ASCII cho chữ thường nằm trong khoảng từ 61h đến 7Ah. Mã ASCII cho các chữ hoa và chữ thường tương ứng chênh lệch nhau đúng 20h. Nếu biết một chữ cái đang là chữ hoa, bạn có thể chuyển nó thành chữ thường bằng cách cộng thêm 20h vào mã ASCII đó. Ví dụ, bạn có thể cộng 20h với 54h (mã ASCII của chữ T hoa) để có được 74h (mã ASCII của chữ t thường). Đây là phép cộng đó dưới dạng nhị phân:

  0 1 0 1 0 1 0 0
+ 0 0 1 0 0 0 0 0
-----------------
  0 1 1 1 0 1 0 0

Nhưng bạn không thể làm như vậy cho mọi chữ cái. Nếu bạn cộng 20h với 6Fh (mã ASCII của chữ o thường), bạn sẽ được 8Fh, vốn chẳng phải là một mã ASCII hợp lệ nào cả:

  0 1 1 0 1 1 1 1
+ 0 0 1 0 0 0 0 0
-----------------
  1 0 0 0 1 1 1 1

Nhưng hãy nhìn kỹ vào các mẫu bit. Đây là chữ A hoa và chữ a thường, mang mã ASCII lần lượt là 41h và 61h:

A: 0 1 0 0 0 0 0 1
a: 0 1 1 0 0 0 0 1

Và đây là chữ Z hoa và chữ z thường, có mã ASCII là 5Ah và 7Ah:

Z: 0 1 0 1 1 0 1 0
z: 0 1 1 1 1 0 1 0

Đối với mọi chữ cái, điểm khác biệt duy nhất giữa chữ hoa và chữ thường là một bit duy nhất, đó là bit thứ ba từ trái sang. Bạn có thể chuyển chữ hoa thành chữ thường bằng cách gán bit đó thành 1. Sẽ không có vấn đề gì nếu chữ cái đó vốn dĩ đã là chữ thường, vì bit đó khi ấy vốn đã được gán sẵn là 1 rồi.

Do vậy, thay vì cộng thêm 20h, sẽ hợp lý hơn nếu ta dùng một phép toán Boolean OR trên từng cặp bit. Bạn còn nhớ bảng này từ Chương 6 không?

+-------+-------+-------+
|  OR   |   0   |   1   |
+-------+-------+-------+
|   0   |   0   |   1   |
+-------+-------+-------+
|   1   |   1   |   1   |
+-------+-------+-------+

Kết quả của phép toán OR là 1 nếu một trong hai toán hạng là 1.

Lại là chữ T hoa đây, nhưng thay vì cộng thêm 20h, hãy áp dụng phép toán OR giữa các bit tương ứng của 54h (chữ T) và 20h:

   0 1 0 1 0 1 0 0
OR 0 0 1 0 0 0 0 0
------------------
   0 1 1 0 0 1 0 0

Kết quả sẽ là bit 1 nếu một trong hai bit tương ứng là 1. Lợi thế của phương pháp này là các chữ thường sẽ không bị thay đổi gì cả. Kết quả của phép toán OR với chữ o thường và 20h:

   0 1 1 0 1 1 1 1
OR 0 0 1 0 0 0 0 0
------------------
   0 1 1 0 1 1 1 1

Nếu bạn áp dụng phép toán OR với 20h cho từng chữ cái trong bộ nhớ, bạn có thể chuyển toàn bộ đoạn văn bản thành chữ thường:

           +-------+
  1000h:   |  74h  |  t
           +-------+
           |  6Fh  |  o
           +-------+
           |  6Dh  |  m
           +-------+
           |  73h  |  s
           +-------+
           |  61h  |  a
           +-------+
           |  77h  |  w
           +-------+
           |  79h  |  y
           +-------+
           |  65h  |  e
           +-------+
           |  72h  |  r
           +-------+


Hành động ta vừa làm có tên gọi đàng hoàng. Nó được gọi là phép toán OR theo bit (bitwise OR), bởi vì nó thực hiện phép toán OR giữa từng cặp bit tương ứng với nhau. Hóa ra nó còn hữu dụng cho nhiều tác vụ khác ngoài việc chuyển đổi văn bản sang chữ thường. Vì lý do đó, tôi muốn thêm mạch điện sau vào bộ logic số học:

A7 B7 A6 B6 A5 B5 A4 B4 A3 B3 A2 B2 A1 B1 A0 B0
8 Cổng OR


Đối với 8 bit tương ứng của 2 byte được dán nhãn A và B, mạch điện thực hiện một phép toán OR. Hãy đóng gói cái mạch đó vào một chiếc hộp với mác đơn giản:

A B OR Đầu ra 8 8 8
Hộp bitwise OR


Bây giờ hãy cân nhắc xem làm cách nào để chuyển một đoạn văn bản thành chữ hoa. Quá trình này hơi khác một chút thay vì gán một bit thành 1, bạn lại muốn gán bit đó thành 0. Thay vì phép toán OR, bạn sẽ cần một phép toán AND. Đây là bảng AND từ Chương 6:

+-------+-------+-------+
|  AND  |   0   |   1   |
+-------+-------+-------+
|   0   |   0   |   0   |
+-------+-------+-------+
|   1   |   0   |   1   |
+-------+-------+-------+

Thay vì dùng phép toán OR với 20h, bạn dùng phép toán AND với DFh (11011111 ở hệ nhị phân), vốn chính là giá trị đảo ngược của 20h. Đây là cách chữ o thường được chuyển thành chữ hoa:

    0 1 1 0 1 1 1 1
AND 1 1 0 1 1 1 1 1
-------------------
    0 1 0 0 1 1 1 1

Mã ASCII 6Fh biến thành mã 4Fh, chính là mã ASCII của chữ O hoa.

Nếu chữ cái vốn dĩ đã là chữ hoa, thì phép toán AND với DFh sẽ chẳng có tác dụng gì. Ví dụ với chữ T hoa:

    0 1 0 1 0 1 0 0
AND 1 1 0 1 1 1 1 1
-------------------
    0 1 0 1 0 1 0 0

Nó vẫn giữ nguyên là chữ hoa sau phép toán AND với DFh. Trên toàn bộ đoạn văn bản, một phép toán AND sẽ chuyển mọi chữ cái thành chữ hoa:

           +-------+
  1000h:   |  54h  |  T
           +-------+
           |  4Fh  |  O
           +-------+
           |  4Dh  |  M
           +-------+
           |  53h  |  S
           +-------+
           |  41h  |  A
           +-------+
           |  57h  |  W
           +-------+
           |  59h  |  Y
           +-------+
           |  45h  |  E
           +-------+
           |  52h  |  R
           +-------+


Sẽ rất tiện lợi nếu ALU chứa một bộ gồm tám cổng AND để thực hiện phép toán AND theo bit (bitwise AND) giữa 2 byte:

A7 B7 A6 B6 A5 B5 A4 B4 A3 B3 A2 B2 A1 B1 A0 B0
8 cổng AND


Hãy gói gọn nó vào một chiếc hộp nhỏ để dễ bề tham khảo nhé:

A B AND Đầu ra 8 8 8
Hộp bitwise AND


Một phép toán bitwise AND cũng hữu dụng để xác định xem các bit cụ thể của một byte đang là 0 hay 1. Ví dụ, giả sử bạn có một byte chứa mã ASCII của một chữ cái, và bạn muốn biết nó là chữ thường hay chữ hoa. Hãy thực hiện phép toán bitwise AND với 20h. Nếu kết quả là 20h, thì chữ cái đó là chữ thường. Nếu kết quả là 00h, thì nó là chữ hoa.

Một phép toán bitwise hữu ích khác là exclusive OR, hay XOR. Bảng sau đã từng xuất hiện ở Chương 14 khi rõ ràng rằng phép toán này rất đắc lực để cộng:

+-------+-------+-------+
|  XOR  |   0   |   1   |
+-------+-------+-------+
|   0   |   0   |   1   |
+-------+-------+-------+
|   1   |   1   |   0   |
+-------+-------+-------+

Đây là một hàng tám cổng XOR được nối mạch để thực hiện phép toán XOR theo bit giữa 2 byte:

A7 B7 A6 B6 A5 B5 A4 B4 A3 B3 A2 B2 A1 B1 A0 B0
8 cổng XOR


Một lần nữa, hãy nhét mạch điện đó vào một chiếc hộp cho gọn gàng:

A B XOR Đầu ra 8 8 8
Hộp bitwise XOR


Phép toán XOR rất hữu dụng trong việc đảo ngược các bit. Ví dụ, nếu bạn áp dụng phép toán XOR cho các mã ASCII của chuỗi "TomSawyer" với 20h, tất cả các chữ hoa sẽ biến thành chữ thường, và toàn bộ chữ thường sẽ lột xác thành chữ hoa! Thực hiện một phép toán XOR với FFh sẽ đảo ngược tất cả các bit trong một giá trị.

Trước đó tôi đã định nghĩa hai bit chức năng gắn nhãn F1 và F0 cho khối Add/Subtract. Đối với toàn bộ ALU, chúng ta sẽ cần ba bit chức năng:

+-------+-------+-------+-------------+
|  F2   |  F1   |  F0   |  Phép tính  |
+-------+-------+-------+-------------+
|   0   |   0   |   0   | Cộng        |
+-------+-------+-------+-------------+
|   0   |   0   |   1   | Cộng có nhớ |
+-------+-------+-------+-------------+
|   0   |   1   |   0   | Trừ         |
+-------+-------+-------+-------------+
|   0   |   1   |   1   | Trừ có mượn |
+-------+-------+-------+-------------+
|   1   |   0   |   0   | Bitwise AND |
+-------+-------+-------+-------------+
|   1   |   0   |   1   | Bitwise XOR |
+-------+-------+-------+-------------+
|   1   |   1   |   0   | Bitwise OR  |
+-------+-------+-------+-------------+
|   1   |   1   |   1   | So sánh     |
+-------+-------+-------+-------------+

Tôi không hề gán mấy mã chức năng này một cách ngẫu hứng đâu nhé. Như bạn sẽ thấy, các mã này được ngầm chỉ định bởi các mã lệnh thực tế mà bộ vi xử lý Intel 8080 thực thi. Bên cạnh bitwise AND, XOR và OR, bạn sẽ thấy rằng một phép toán khác, mang tên So sánh (Compare), đã được thêm vào bảng. Tôi sẽ thảo luận về nó ngay sau đây.

Ở phần đầu của chương này, tôi đã cho bạn xem một chương trình nhỏ với các mã thao tác C6h và CEh, chúng thực hiện phép cộng với byte tiếp theo trong bộ nhớ. Mã C6h là một phép cộng thông thường, trong khi CEh là phép cộng có nhớ. Chúng được gọi là các lệnh tức thời (immediate) bởi vì chúng sử dụng ngay byte tiếp theo nối đuôi mã thao tác. Trong Intel 8080, hai mã đó là một phần của một họ gồm tám mã thao tác, được hiển thị ở đây:

+-------------------+---------+
|       Lệnh        |  Opcode |
+-------------------+---------+
| Cộng ngay         |   C6h   |
+-------------------+---------+
| Cộng ngay với nhớ |   CEh   |
+-------------------+---------+
| Trừ ngay          |   D6h   |
+-------------------+---------+
| Trừ ngay với mượn |   DEh   |
+-------------------+---------+
| AND ngay          |   E6h   |
+-------------------+---------+
| XOR ngay          |   EEh   |
+-------------------+---------+
| OR ngay           |   F6h   |
+-------------------+---------+
| So sánh ngay      |   FEh   |
+-------------------+---------+

Các opcode này có dạng tổng quát như sau:

1 1 F2 F1 F0 1 1 0

trong đó F2, F1 và F0 là các bit được hiển thị trong bảng trước. Ba bit đó được sử dụng trong mạch điện tiếp theo, nơi kết hợp các hộp bitwise AND, XOR và OR:

AND A B Out XOR A B Out OR A B Out TRI In Out En TRI In Out En TRI In Out En F2 F1 F0 A B
Kết hợp 3 tín hiệu F với hộp bitwise


Các đầu vào A và B được dẫn tới cả ba hộp AND, XOR và OR. Cả ba đều đồng loạt thực hiện nhiệm vụ được giao. Nhưng chỉ có một kết quả được chọn làm đầu ra. Đó chính là mục đích của ba hộp mang nhãn TRI, thực chất là các bộ đệm ba trạng thái 8-bit. Các bộ đệm ba trạng thái này cho phép chọn ra một trong số chúng (hoặc không chọn cái nào) dựa trên ba tín hiệu chức năng F0, F1 và F2. Nếu F2 là 0, hoặc nếu cả F2, F1 và F0 đều là 1, thì không có đầu ra nào được chọn.

Hãy gói ghém sơ đồ đó vào một chiếc hộp khác:

F2 F1 F0 A B Logic Đầu ra 8 8 8
Hộp Logic


Đó chính là thành phần logic của đơn vị logic số học (ALU).

Bảng trên cho thấy nếu F2, F1 và F0 đều là 1, thì phép toán So sánh (Compare) sẽ được thực thi. Điều này có nghĩa là gì?

Đôi khi sẽ có ích để xác định xem một số có nhỏ hơn, lớn hơn hay bằng một số khác hay không. Bạn làm điều đó như thế nào? Về cơ bản, nó là một phép trừ. Trừ byte B cho byte A. Nếu kết quả là 0, bạn biết rằng hai số đó bằng nhau. Nếu không, nếu Cờ nhớ được bật, thì byte B lớn hơn byte A, và nếu Cờ nhớ không được bật, thì byte A lớn hơn.

Phép toán Compare hoạt động hệt như phép toán Subtract với một điểm khác biệt quan trọng là kết quả sẽ không được lưu vào đâu cả. Thay vào đó, Cờ nhớ mới là thứ được lưu lại.

Nhưng đối với phép toán Compare, chúng ta cũng cần phải biết liệu kết quả của phép toán có bằng 0 hay không, điều này báo hiệu rằng hai byte có giá trị bằng nhau. Điều này đòi hỏi phải có một cờ khác, gọi là Cờ Zero (Zero flag), vốn cũng phải được lưu lại cùng với Cờ nhớ.

Nhân tiện đây, hãy bổ sung thêm một cờ nữa, gọi là Cờ Dấu (Sign flag). Cờ này được bật nếu bit quan trọng nhất của kết quả phép toán là 1. Nếu con số đó ở dạng số bù hai, Cờ Dấu sẽ cho biết số đó là âm hay dương. Cờ sẽ là 1 nếu số đó âm và 0 nếu số đó dương.

(Intel 8080 trên thực tế định nghĩa tới năm cờ. Tôi sẽ không triển khai cờ Nhớ Phụ (Auxiliary Carry flag), vốn cho biết liệu có nhớ phát sinh từ 4 bit ít quan trọng nhất của bộ cộng sang 4 bit quan trọng nhất hay không. Điều này cần thiết cho việc thực thi một lệnh của Intel 8080 gọi là Decimal Adjust Accumulator, lệnh này chuyển đổi giá trị trong thanh ghi tích lũy từ dạng nhị phân sang dạng thập phân mã hóa nhị phân (BCD), mà tôi đã thảo luận khi chế tạo đồng hồ ở Chương 18. CPU của tôi sẽ không tích hợp lệnh đó. Cờ còn lại mà tôi sẽ bỏ qua là cờ Chẵn Lẻ (Parity flag), cờ này bằng 1 nếu kết quả của phép toán số học hoặc logic có số lượng bit 1 là số chẵn. Cờ này khá dễ để triển khai với bảy cổng XOR, nhưng nó ít hữu dụng hơn nhiều so với các cờ khác.)

Hóa ra đối với một số tác vụ lập trình, phép toán So sánh lại quan trọng hơn cả phép cộng và trừ. Ví dụ, giả sử bạn đang viết một chương trình tìm kiếm văn bản trên một trang web. Việc này đòi hỏi phải so sánh các ký tự của đoạn văn bản trên trang web với các ký tự của đoạn văn bản mà bạn đang muốn tìm.

Toàn bộ đơn vị logic số học sẽ kết hợp khối Add/Subtract và khối Logic cùng với một mớ mạch điện hỗ trợ khá lộn xộn:

Mạch ALU


Hai hộp được dán nhãn TRI là các bộ đệm ba trạng thái. Khối Logic chỉ kích hoạt một đầu ra cho ba tổ hợp của F0, F1 và F2 chọn các phép toán AND, OR và XOR. Bộ đệm ba trạng thái ở đầu ra của khối Add/Subtract chỉ được kích hoạt nếu F2 là 0, biểu thị phép cộng hoặc trừ.

Ở phía dưới cùng, hai chốt có các đầu vào Clk được nối với một đầu vào Clock ở góc dưới bên trái, áp dụng cho toàn bộ ALU. Một bộ đệm ba trạng thái khác được điều khiển bởi một tín hiệu Enable ở góc dưới bên trái, đây cũng là một đầu vào của ALU. Bộ đệm ba trạng thái ở góc dưới bên phải là đầu ra tổng hợp từ khối Add/Subtract và khối Logic.

Phần lớn các cổng logic trong sơ đồ đều dành riêng cho Cờ nhớ (Carry flag - viết tắt là CY). Cờ nhớ sẽ được bật nếu tín hiệu F2 là 0 (chỉ định phép toán cộng hoặc trừ) hoặc F1 và F0 đều là 1, biểu thị phép toán So sánh.

Ba cờ này là đầu vào cho chốt nằm ở giữa phía dưới cùng. Một cổng NOR tám-đầu-vào sẽ xác định xem kết quả của phép toán có phải toàn là các số không hay không. Đó chính là Cờ Zero (Zero flag - viết tắt là Z). Bit cao của đầu ra dữ liệu là Cờ Dấu (Sign flag - viết tắt là S). Mặc dù chỉ có ba cờ, nhưng chúng được xử lý như 3 bit của một byte khi xuất ra từ ALU. Cờ nhớ sau đó vòng ngược lên trên cùng để cung cấp đầu vào CY In cho khối Add/Sub.

Bước tiếp theo là giấu nhẹm tất cả cái mớ logic lộn xộn đó vào một chiếc hộp đơn giản:

F2 F1 F0 A B Clock Enable Đơn vị Logic Số học (ALU) Các Cờ Đầu ra
Đơn vị logic số học (ALU)


Đơn bị logic số học đã hoàn thiện!

Mặc dù ALU là một thành phần cực kỳ quan trọng của bộ xử lý trung tâm, nhưng CPU cần nhiều thứ hơn là chỉ một công cụ để thực hiện các phép toán số học và logic trên các con số. Nó cần một cách để đưa các con số vào ALU, và một cách để lưu trữ các kết quả rồi di chuyển chúng qua lại. Đó chính là bước tiếp theo.
Cảm ơn bạn đã đọc bài.
0

Thảo luận trên Bluesky

Đi

Đang tải bình luận...

Powered by Bluesky AT Protocol