Có một câu ngạn ngữ quen thuộc trong giới kỹ sư rằng 10% cuối cùng của dự án lại ngốn tới 90% công sức. Nghe hơi nản nhưng lại là điều khắc cốt ghi tâm. Ta đã tiến một chặng dài trong việc lắp ráp máy tính nhưng vẫn chưa xong đâu. Những gì còn lại không hẳn chiếm tới 90% khối lượng công việc, nhưng chặng nước rút có lẽ còn xa hơn bạn tưởng một chút.
Bộ xử lý trung tâm (CPU) mà tôi đang thiết kế được xây dựng dựa trên bộ vi xử lý Intel 8080. Bộ logic số học (ALU) và mảng thanh ghi mà bạn đã thấy ở hai chương trước tạo thành phần nòng cốt của CPU này. ALU thực hiện các phép toán số học và logic trên các byte. Mảng thanh ghi chứa các chốt cho bảy thanh ghi được nhận diện bằng các chữ cái A, B, C, D, E, H, và L. Bạn cũng đã thấy rằng cần thêm ba chốt nữa để lưu byte lệnh và một hoặc hai byte theo sau một số lệnh nhất định.
Các linh kiện này được kết nối với nhau và với bộ nhớ truy cập ngẫu nhiên (RAM) thông qua hai bus dữ liệu: một bus dữ liệu 8-bit chuyên chở các byte qua lại giữa các linh kiện, và một bus địa chỉ 16-bit dùng cho địa chỉ bộ nhớ. Ở chương trước, bạn cũng đã diện kiến một bộ đếm chương trình chuyên lưu giữ địa chỉ cho RAM này, cùng với một bộ tăng/giảm làm nhiệm vụ cộng thêm hoặc trừ đi 1 cho địa chỉ bộ nhớ 16-bit.
Hai bus này cung cấp nguồn kết nối chính giữa các linh kiện, nhưng chúng cũng được kết nối với nhau bằng một tập hợp các tín hiệu điều khiển phức tạp hơn. Cái tên nói lên tất cả, chúng dùng để điều khiển các linh kiện này phối hợp nhịp nhàng nhằm thực thi các lệnh được lưu trong bộ nhớ.
Hầu hết các tín hiệu điều khiển này thuộc hai loại chính:
Các tín hiệu đưa một giá trị lên một trong hai bus.
Các tín hiệu lưu một giá trị từ một trong hai bus.
Trọng tâm của chương này chỉ xoay quanh chuyện các giá trị "nhảy lên" và "nhảy xuống" xe bus.
Tín hiệu quăng giá trị lên bus được nối với các đầu vào Enable của hàng loạt bộ đệm ba trạng thái—những bộ đệm kết nối đầu ra của linh kiện với bus. Tín hiệu lưu giá trị từ bus thường điều khiển các đầu vào Clock của những cái chốt kết nối bus với các linh kiện. Ngoại lệ duy nhất là khi một giá trị trên bus dữ liệu được lưu vào bộ nhớ bằng tín hiệu RAM Write.
Sự đồng bộ hóa của những tín hiệu này chính là chìa khóa cho phép CPU thực thi các lệnh lưu trong bộ nhớ. Đây là cách các giá trị 8-bit và 16-bit di chuyển qua lại giữa các linh kiện CPU và bộ nhớ. Đây là cách thức nền tảng mà qua đó, các mã lệnh lưu trong bộ nhớ điều khiển phần cứng của máy tính. Đây chính xác là lúc phần cứng và phần mềm hòa làm một, như đã được ẩn ý ngay từ tựa đề cuốn sách này. Bạn có thể mường tượng quá trình này như một nghệ sĩ múa rối đang điều khiển một đoàn rối gỗ trong một vũ điệu số học và logic được biên đạo tinh vi. Các tín hiệu điều khiển CPU chính là những sợi dây rối.
Dưới đây là sáu linh kiện chính, cho thấy cách chúng kết nối với bus dữ liệu và bus địa chỉ, cùng với các tín hiệu điều khiển mà chúng cần.
Đầu vào Address của bộ nhớ được nối với bus địa chỉ 16-bit. Bộ nhớ cũng kết nối với bus dữ liệu 8-bit qua các ngõ Data In và Data Out:
RAM 64x8
Hai tín hiệu điều khiển ở đây là Write, dùng để ghi giá trị trên bus dữ liệu vào bộ nhớ, và Enable, dùng để kích hoạt bộ đệm ba trạng thái trên RAM Data Out nhằm đẩy nội dung của nó lên bus dữ liệu. Một bảng điều khiển có thể được gắn vào mảng bộ nhớ này để cho phép con người ghi các byte vào bộ nhớ và xem xét chúng.
Linh kiện rắc rối nhất chắc chắn là mảng thanh ghi từ Chương 22, thứ mà đôi khi bạn sẽ thấy tôi viết tắt là RA:
Mảng thanh ghi (RA)
Mảng thanh ghi có hai nhóm đầu vào Select nằm ở phía trên. Các tín hiệu SI sẽ quyết định thanh ghi nào lưu giá trị đang có trên bus dữ liệu. Tín hiệu RA Clock ở bên trái xác định khi nào giá trị đó được lưu. Các tín hiệu SO cùng với tín hiệu RA Enable ở bên trái sẽ đẩy giá trị của một trong các thanh ghi lên bus dữ liệu.
Như bạn đã thấy ở chương trước, mảng thanh ghi này phức tạp ở hai điểm. Thứ nhất, nó phải triển khai thêm hai tín hiệu điều khiển cho bộ tích lũy, đôi khi được viết tắt là Acc: Tín hiệu Accumulator Clock lưu giá trị từ bus dữ liệu vào bộ tích lũy, và tín hiệu Accumulator Enable kích hoạt một bộ đệm ba trạng thái để đẩy giá trị của bộ tích lũy lên bus dữ liệu.
Thứ hai, các thanh ghi H và L của mảng thanh ghi cũng được kết nối với bus địa chỉ tuân theo ba tín hiệu điều khiển nằm bên phải: HL Select chọn bus địa chỉ làm đầu vào cho các thanh ghi H và L, HL Clock lưu nội dung của bus địa chỉ vào các thanh ghi H và L, và HL Enable kích hoạt một bộ đệm ba trạng thái để đẩy nội dung của các thanh ghi H và L lên bus địa chỉ.
Bộ ALU từ Chương 21 có các đầu vào F0, F1 và F2 để quyết định xem ALU sẽ thực hiện phép cộng, trừ, so sánh hay các hàm logic:
Bộ Logic Số học
Lưu ý rằng đầu vào B và đầu ra Out của ALU đều được kết nối với bus dữ liệu, nhưng đầu vào A thì lại được kết nối trực tiếp với đầu ra Acc của mảng thanh ghi. ALU bị làm rối thêm một chút vì nó cần phải lưu lại Cờ nhớ (CY), Cờ Zero (Z) và Cờ Dấu (S) được thiết lập dựa trên phép toán số học hay logic vừa thực hiện.
ALU cũng triển khai một tín hiệu Clock dùng để lưu kết quả của phép toán số học hoặc logic vào một chốt (và lưu các cờ vào một chốt khác), cùng một tín hiệu Enable để kích hoạt bộ đệm ba trạng thái nhằm đẩy kết quả của ALU lên bus dữ liệu.
Một chốt 16-bit khác sẽ giữ giá trị hiện tại của bộ đếm chương trình, thứ được dùng để định vị các byte trong bộ nhớ:
Bộ đếm chương trình
Bộ đếm chương trình đôi khi được viết tắt là PC (Program Counter). Nó có ba tín hiệu điều khiển: Tín hiệu Clock lưu giá trị 16-bit từ bus địa chỉ vào chốt. Tín hiệu Enable kích hoạt một bộ đệm ba trạng thái để đẩy nội dung của chốt lên bus địa chỉ. Tín hiệu Reset ở bên trái biến toàn bộ nội dung của chốt thành số không (0) để bắt đầu truy cập các byte từ bộ nhớ tại địa chỉ 0000h.
Ba chốt 8-bit bổ sung sẽ lưu trữ tới 3 byte của một lệnh. Chúng được đóng gói trong chiếc hộp sau:
Chốt lệnh
Một số lệnh đơn giản chỉ bao gồm một mã thao tác. Số khác lại kéo theo 1 hoặc 2 byte nối gót theo sau. Ba tín hiệu Clock ở bên trái lưu trữ tối đa 3 byte cấu thành nên một lệnh.
Byte đầu tiên cũng chính là mã thao tác (opcode). Nếu lệnh có một byte thứ hai, byte đó có thể được đưa lên bus dữ liệu nhờ tín hiệu Latch 2 Enable ở bên phải. Nếu mã thao tác được theo sau bởi 2 byte, chúng sẽ tạo thành một địa chỉ bộ nhớ 16-bit, và nó có thể được đẩy lên bus địa chỉ thông qua tín hiệu Latches 2 & 3 Enable.
Linh kiện cuối cùng là một mạch có thể tăng hoặc giảm một giá trị 16-bit. Mạch này thỉnh thoảng sẽ được viết tắt là Inc-Dec:
Bộ tăng/giảm
Tín hiệu Clock lưu giá trị từ bus địa chỉ vào chốt Tăng/Giảm. Hai tín hiệu Enable ở bên phải kích hoạt đưa giá trị đã được tăng hoặc giảm lên bus địa chỉ.
Để cho bạn cảm nhận chút xíu về việc các tín hiệu điều khiển này phải được phối hợp với nhau như thế nào, hãy cùng xem xét một chương trình 8080 nhỏ với vỏn vẹn sáu lệnh. Một số lệnh chỉ dài 1 byte, trong khi những lệnh khác cần thêm 1 hoặc 2 byte theo sau mã thao tác:
Chương trình này không làm gì nhiều. Lệnh đầu tiên di chuyển giá trị 27h vào thanh ghi A, hay còn gọi là bộ tích lũy. Lệnh MOV sao chép giá trị đó sang thanh ghi B. Giá trị 61h được cộng vào bộ tích lũy, biến nó thành 88h. Giá trị trong thanh ghi B sau đó được cộng dồn vào, nâng tổng giá trị lên thành AFh. Lệnh STA lưu giá trị vào bộ nhớ tại địa chỉ 000Ah. Lệnh HLT dừng CPU lại vì không còn gì để làm nữa.
Hãy cùng ngẫm xem CPU cần làm gì để thực thi mấy lệnh này. CPU dùng một giá trị gọi là bộ đếm chương trình để định địa chỉ bộ nhớ và dời các lệnh vào trong các chốt lệnh. Bộ đếm chương trình được khởi tạo ở giá trị 0000h truy cập lệnh đầu tiên từ bộ nhớ. Lệnh đó là MVI (Di chuyển tức thời), có mục đích đưa giá trị 27h vào bộ tích lũy.
Cần một chuỗi gồm năm bước để xử lý lệnh đầu tiên. Mỗi bước liên quan đến việc đặt một thứ gì đó lên bus địa chỉ và lưu nó vào nơi khác, hoặc đặt một thứ gì đó lên bus dữ liệu và lưu nó vào nơi khác, hoặc cả hai.
Bước đầu tiên là định địa chỉ RAM bằng giá trị của bộ đếm chương trình là 0000h và lưu giá trị 3Eh từ bộ nhớ vào Chốt Lệnh 1. Việc này đòi hỏi bốn tín hiệu điều khiển liên quan đến cả bus địa chỉ lẫn bus dữ liệu:
Program Counter Enable: Đưa bộ đếm chương trình lên bus địa chỉ. Giá trị đó là 0000h.
RAM Data Out Enable: Đưa giá trị của RAM tại địa chỉ đó lên bus dữ liệu. Giá trị đó là 3Eh.
Incrementer-Decrementer Clock: Lưu giá trị trên bus địa chỉ vào bộ tăng/giảm.
Instruction Latch 1 Clock: Lưu giá trị trên bus dữ liệu vào Chốt Lệnh 1.
Bước thứ hai tăng bộ đếm chương trình lên. Bước này chỉ dính líu tới bus địa chỉ:
Increment Enable: Đưa giá trị đã được tăng lên từ bộ tăng/giảm lên bus địa chỉ. Giá trị đó giờ là 0001h.
Program Counter Clock: Lưu giá trị đã được tăng đó vào bộ đếm chương trình.
Bây giờ khi byte lệnh đầu tiên đã được lưu vào Chốt Lệnh 1, nó có thể được dùng để điều khiển các bước tiếp theo. Trong trường hợp này, bước ba và bốn giống hệt bước một và hai, ngoại trừ việc chúng truy cập byte tại địa chỉ bộ nhớ 0001h và lưu nó vào Chốt Lệnh 2.
Những bước đọc các byte lệnh từ bộ nhớ này được gọi là quá trình lấy lệnh (instruction fetch). Mục đích của chúng là lấy các byte lệnh từ bộ nhớ và lưu trữ chúng vào trong các chốt lệnh. Với lệnh MVI 27h, giá trị 27h lúc này đã nằm gọn trong Chốt Lệnh 2. Giá trị đó phải được chuyển vào bộ tích lũy. Đây là bước thứ năm, được gọi là quá trình thực thi (execution) lệnh:
Instruction Latch 2 Enable: Đưa giá trị của chốt đó lên bus dữ liệu.
Accumulator Clock: Lưu giá trị trên bus dữ liệu vào bộ tích lũy.
Hãy để ý rằng tất cả năm bước này đều dính đến nhiều nhất là chỉ một giá trị trên bus địa chỉ và một giá trị trên bus dữ liệu. Bất kỳ giá trị nào được đặt lên một trong hai bus sau đó sẽ được lưu ở một nơi khác.
Giờ thì sang lệnh thứ hai, tức là MOV B,A. Vì lệnh này chỉ dài 1 byte, nên chỉ cần hai bước cho quá trình lấy lệnh. Bước thực thi của lệnh là:
Register Array Enable: Đưa giá trị trong mảng thanh ghi lên bus dữ liệu.
Register Array Clock: Lưu giá trị trên bus dữ liệu vào mảng thanh ghi.
Khoan đã! Phần mô tả bước thực thi này chỉ đề cập đến mảng thanh ghi, chứ không hề nhắc tới thanh ghi A và B, là những thứ mà bước này đang hướng tới! Tại sao lại như vậy?
Đơn giản thôi: Các bit cấu thành nên lệnh MOV của 8080 có định dạng là:
0 1 D D D S S S
trong đó DDD là thanh ghi đích và SSS là thanh ghi nguồn. Mã thao tác đã được lưu trong Chốt Lệnh 1. Mảng thanh ghi có hai cụm tín hiệu Select 3-bit để xác định thanh ghi nào là nguồn và thanh ghi nào là đích. Như bạn sẽ thấy, các tín hiệu này xuất phát từ mã opcode được lưu trong Chốt Lệnh 1, nên ta chỉ cần kích hoạt mảng thanh ghi và chốt mảng thanh ghi lại để hoàn tất việc thực thi.
Một lệnh Cộng Tức Thời xuất hiện tiếp theo:
ADI 61h
Đây là một trong tám lệnh tương tự nhau có định dạng:
1 1 F F F 1 1 0
trong đó FFF ám chỉ chức năng mà lệnh đó thực hiện: Cộng, Cộng có nhớ, Trừ, Trừ có mượn, AND, XOR, OR, hoặc So sánh. Bạn sẽ nhớ lại rằng ALU có một đầu vào Chức Năng 3-bit tương ứng với các giá trị này. Điều này có nghĩa là ba bit chức năng từ opcode nằm trong Chốt Lệnh 1 có thể được chạy thẳng vào ALU.
Sau khi 2 byte của lệnh ADI được lấy ra, việc thực thi lệnh sẽ cần thêm hai bước. Đây là bước đầu tiên:
Instruction Latch 2 Enable: Đưa giá trị 61h lên bus dữ liệu.
ALU Clock: Lưu kết quả của ALU và các cờ vào các chốt.
Cần thêm một bước thực thi thứ hai để chuyển kết quả đó vào thanh ghi tích lũy:
ALU Enable: Đưa kết quả của ALU lên bus dữ liệu.
Accumulator Clock: Lưu giá trị đó vào bộ tích lũy.
Lệnh ADD xuất hiện tiếp theo cũng yêu cầu hai bước thực thi. Bước đầu tiên là:
Register Array Enable: Đưa thanh ghi B lên bus dữ liệu.
ALU Clock: Lưu kết quả của phép cộng và các cờ.
Bước thực thi thứ hai giống y hệt như bước thực thi của lệnh ADI.
Lệnh STA đòi hỏi sáu bước cho quá trình lấy lệnh. 2 byte nối gót lệnh STA được lưu vào Chốt Lệnh 2 và 3. Bước thực thi yêu cầu các tín hiệu điều khiển sau:
Instruction Latches 2 & 3 Enable: Đưa byte lệnh thứ hai và thứ ba lên bus địa chỉ để định địa chỉ RAM.
Accumulator Enable: Đưa giá trị của thanh ghi tích lũy lên bus dữ liệu.
RAM Write: Ghi giá trị trên bus dữ liệu vào bộ nhớ.
Lệnh HLT làm một việc độc nhất vô nhị, đó là ra lệnh cho CPU ngưng thực thi các lệnh tiếp theo. Tôi sẽ để dành phần trình bày về cách triển khai lệnh đó ở phần sau của chương này.
Những bước mà tôi vừa kể còn được gọi là các chu kỳ (cycles), chả khác nào các chu kỳ giặt, xả, và vắt của máy giặt. Ở ngôn ngữ chuyên ngành, chúng được gọi là chu kỳ máy(machine cycles). Trong CPU mà tôi đang lắp ráp, các byte lệnh được truy xuất từ bộ nhớ trong một chu kỳ, và luôn được theo sau bởi một chu kỳ khác dùng để tăng bộ đếm chương trình. Do đó, tùy thuộc vào việc lệnh có 1, 2, hay 3 byte, CPU sẽ phải thực thi hai, bốn, hoặc sáu chu kỳ.
Việc thực thi lệnh yêu cầu một hoặc hai chu kỳ máy, tùy thuộc vào lệnh đang được thực thi. Đây là bảng thống kê những gì phải xảy ra trong chu kỳ thực thi đầu tiên của tất cả các lệnh mà tôi đã giới thiệu từ nãy giờ:
Chu kỳ thực thi đầu tiên
-------------+--------------------------------+---------------------------
Lệnh | Bus Địa Chỉ 16-Bit | Bus Dữ Liệu 8-Bit
-------------+--------------------------------+---------------------------
MOV r,r | | Register Array Enable
| | Register Array Clock
-------------+--------------------------------+---------------------------
MOV r,M | HL Enable | RAM Data Out Enable
| | Register Array Clock
-------------+--------------------------------+---------------------------
MOV M,r | HL Enable | Register Array Enable
| | RAM Write
-------------+--------------------------------+---------------------------
MVI r,data | | Instruction Latch 2 Enable
| | Register Array Clock
-------------+--------------------------------+---------------------------
MVI M,data | HL Enable | Instruction Latch 2 Enable
| | RAM Write
-------------+--------------------------------+---------------------------
LDA | Instruction Latch 2 & 3 Enable | RAM Data Out Enable
| | Accumulator Clock
-------------+--------------------------------+---------------------------
STA | Instruction Latch 2 & 3 Enable | Accumulator Enable
| | RAM Write
-------------+--------------------------------+---------------------------
ADD r ... | | Register Array Enable
| | ALU Clock
-------------+--------------------------------+---------------------------
ADD M ... | HL Enable | RAM Data Out Enable
| | ALU Clock
-------------+--------------------------------+---------------------------
ADI data ... | | Instruction Latch 2 Enable
| | ALU Clock
-------------+--------------------------------+---------------------------
INX/DCX HL | HL Enable |
| Incrementer-Decrementer Clock |
Chú ý dấu ba chấm (...) ở cột đầu tiên của ba hàng. Các hàng có chứa lệnh ADD cũng bao hàm cả ADC, SUB, SBB, ANA, XRA, ORA, và CMP; hàng chứa ADI cũng gom luôn ACI, SUI, SBI, ANI, XRI, ORI, và CPI.
Bốn dòng cuối của bảng là những lệnh đòi hỏi một chu kỳ thực thi thứ hai. Bảng sau chỉ ra những gì phải diễn ra trong các chu kỳ thực thi thứ hai này:
Chu kỳ thực thi thứ hai
-------------+--------------------+------------------
Lệnh | Bus Địa Chỉ 16-Bit | Bus Dữ Liệu 8-Bit
-------------+--------------------+------------------
ADD r ... | | ALU Enable
| | Accumulator Clock
-------------+--------------------+------------------
ADD M ... | | ALU Enable
| | Accumulator Clock
-------------+--------------------+------------------
ADI data ... | | ALU Enable
| | Accumulator Clock
-------------+--------------------+------------------
INX HL | Increment Enable |
| HL Select |
| HL Clock |
-------------+--------------------+------------------
DCX HL | Decrement Enable |
| HL Select |
| HL Clock |
Để làm được ngần ấy việc, mã thao tác phải được giải mã và biến đổi thành các tín hiệu điều khiển nhằm thao túng tất cả các linh kiện này và cả RAM. Các tín hiệu điều khiển này sẽ kích hoạt các bộ đệm ba trạng thái, các đầu vào Clock của các chốt khác nhau, đầu vào Write của RAM, và một vài đầu vào khác.
Phần còn lại của chương sẽ chỉ cho bạn thấy điều đó được thực hiện như thế nào. Đó là một quá trình đòi hỏi nhiều bước và vài chiến thuật khác nhau.
Đây là bảng mã thao tác cho tất cả các lệnh này:
Lệnh | Operation Code
--------------------------------------------+----------------
MOV r, r | 0 1 D D D S S S
--------------------------------------------+----------------
MOV r, M | 0 1 D D D 1 1 0
--------------------------------------------+----------------
MOV M, r | 0 1 1 1 0 S S S
--------------------------------------------+----------------
HLT | 0 1 1 1 0 1 1 0
--------------------------------------------+----------------
MVI r, data | 0 0 D D D 1 1 0
--------------------------------------------+----------------
MVI M, data | 0 0 1 1 0 1 1 0
--------------------------------------------+----------------
ADD, ADC, SUB, SBB, ANA, XRA, ORA, CMP r. | 1 0 F F F S S S
--------------------------------------------+----------------
ADD, ADC, SUB, SBB, ANA, XRA, ORA, CMP M | 1 0 F F F 1 1 0
--------------------------------------------+----------------
ADI, ACI, SUI, SBI, ANI, XRI, ORI, CPI data | 1 1 F F F 1 1 0
--------------------------------------------+----------------
INX HL | 0 0 1 0 0 0 1 1
--------------------------------------------+----------------
DCX HL | 0 0 1 0 1 0 1 1
--------------------------------------------+----------------
LDA addr | 0 0 1 1 1 0 1 0
--------------------------------------------+----------------
STA addr | 0 0 1 1 0 0 1 0
Bạn sẽ nhớ lại rằng các chuỗi SSS và DDD trong những mã này ám chỉ một thanh ghi nguồn hoặc đích cụ thể, như được hiển thị trong bảng sau:
SSS or DDD | Register
-----------+---------
0 0 0 | B
-----------+---------
0 0 1 | C
-----------+---------
0 1 0 | D
-----------+---------
0 1 1 | E
-----------+---------
1 0 0 | H
-----------+---------
1 0 1 | L
-----------+---------
1 1 1 | A
Chuỗi bit 110 bị mất tích khỏi danh sách này vì chuỗi đó ám chỉ bộ nhớ được định địa chỉ bởi các thanh ghi HL.
Đối với các lệnh số học và logic, các bit FFF đại diện cho chức năng(function) và trỏ tới một trong tám phép toán số học hoặc logic.
Một phần "dễ thở" của mạch điều khiển CPU là việc kết nối trực tiếp Chốt Lệnh 1 với Input Select và Output Select của mảng thanh ghi, cũng như Function Select của ALU:
Chốt lệnh 1
Chữ C ở đây đại diện cho code. Các bit đầu ra C0, C1 và C2 của chốt này đi thẳng đến Input Select của mảng thanh ghi, trong khi các bit C3, C4 và C5 thì ghé qua Output Select của mảng thanh ghi và Function Select của ALU. Đây là một cách tận dụng các quy luật trong mã thao tác.
Có thể bạn sẽ lờ mờ nhận ra một vài quy luật khác trong các opcode: Mọi mã thao tác bắt đầu bằng 01 đều là các lệnh MOV ngoại trừ 76h, vốn là lệnh HLT. Tất cả các lệnh số học và logic (ngoại trừ các biến thể dùng số tức thời như ADI, ACI, v.v.) đều mở đầu bằng các bit 10.
Bước đầu tiên để giải mã opcode là kết nối các bit đầu ra của Chốt Lệnh 1 ở hình trên với ba bộ giải mã: một bộ giải mã 2-sang-4 và hai bộ giải mã 3-sang-8. Những bộ giải mã này được dùng để sinh ra các tín hiệu bổ sung, một số tương ứng trực tiếp với các lệnh, số khác lại đại diện cho các nhóm lệnh:
3 bộ giải mã
Tín hiệu Move Group (Nhóm lệnh di chuyển) ở phía trên bên phải tương ứng với các lệnh bắt đầu bằng bit 01, trong khi tín hiệu Arithmetic/Logic Group (Nhóm lệnh số học/logic) tương ứng với các lệnh bắt đầu bằng bit 10.
Cần phải phân biệt rạch ròi các lệnh MOV di chuyển byte giữa các thanh ghi với những lệnh di chuyển giá trị giữa thanh ghi và bộ nhớ. Các lệnh liên quan tới bộ nhớ được nhận diện qua các giá trị nguồn và đích là 110. Các tín hiệu Memory Source và Memory Destination trong mạch điện trước đó sẽ báo hiệu khi các bit nguồn và đích là 110. Cuối cùng, Move Immediates (Lệnh di chuyển tức thời) là những lệnh mở màn bằng 00 và kết thúc bằng 110.
Năm tín hiệu ở phía trên bên phải của sơ đồ mạch được giải mã sâu hơn ở đây:
Giải mã 5 tín hiệu
Giờ đây, mỗi lệnh hoặc nhóm các lệnh tương tự nhau đều được đại diện bằng một tín hiệu. Những tín hiệu này sẽ sẵn sàng xuất trận ngay khi mã thao tác được lưu vào Chốt Lệnh 1, và chúng có thể được trưng dụng theo nhiều cách khác nhau để chi phối quá trình xử lý của lệnh đó.
Tiếp đến, opcode phải được dùng để xác định xem cần phải lấy bao nhiêu byte lệnh bổ sung từ bộ nhớ và cần bao nhiêu chu kỳ máy để thực thi lệnh đó:
Lấy byte và chu kỳ
Chuyện gì sẽ xảy ra nếu một mã opcode không thuộc về bất kỳ lệnh nào trong số này? Ví dụ, tôi vẫn chưa nhắc đến một lệnh 8080 khá dị mang tên NOP, đọc là "no op" (viết tắt của "no operation" - không làm gì cả), và nó có mã opcode là 00h.
Bạn sẽ nhận thấy rằng nếu không có tín hiệu đầu vào nào ở bên trái là 1, thì tất cả các đầu ra của các cổng OR đều là 0, và các tín hiệu ở bên phải sẽ chỉ ra một chu kỳ lấy lệnh (1-byte fetch) và một chu kỳ thực thi.
Nhịp thời gian (timing) cơ bản của CPU được thiết lập bởi một phiên bản nâng cấp của một mạch điện nhỏ mà bạn đã diện kiến ở trang 296 trong Chương 20:
Timeing của CPU
Bộ dao động ở bên trái là một thiết bị "nhả" ra tín hiệu luân phiên giữa 0 và 1, thường là ở tốc độ bàn thờ. Đây chính là thứ khiến CPU chạy được. Nó như nhịp tim của CPU vậy.
Tín hiệu Reset ở trên cùng cũng giống như tín hiệu Reset trong mạch điện trước; nó được kích hoạt bởi người dùng máy tính để khởi động lại CPU từ vạch xuất phát. Thông thường, tín hiệu Reset là 0, nhưng khi nó nhảy lên 1 (ví dụ như khi người ta bấm vào nút có chữ Reset), CPU sẽ phanh gấp và mọi thứ quay về điểm bắt đầu.
Trong mạch điện này, tín hiệu Reset sẽ đưa ba flip-flop về 0 sao cho tất cả các đầu ra Q đều là 0 và tất cả các đầu ra Q̄ đều là 1. Khi tín hiệu Reset quay về 0, các flip-flop được phép hoạt động bình thường, và CPU bắt đầu guồng máy.
Sau khi CPU được reset, đầu ra Q̄ của flip-flop ở trên cùng là 1. Nó là một trong hai đầu vào cho một cổng AND cho phép bộ dao động điều khiển các đầu vào Clock của hai flip-flop nằm ở phần dưới của sơ đồ.
Tín hiệu Halt ở trên cùng cho biết một lệnh HLT đã được thi hành. Điều này khiến đầu ra Q̄ của flip-flop ở trên cùng rơi về 0, hệ quả là ngăn cản hoàn toàn bộ dao động điều khiển CPU. CPU có thể được "đánh thức" (unhalted) bằng một tín hiệu Reset.
Nếu CPU chưa bị ngừng, hai flip-flop ở dưới cùng của sơ đồ sẽ tạo ra hai tín hiệu được dán nhãn Cycle Clock và Pulse, được hiển thị trong biểu đồ định thời này:
Biểu đồ định thời của 2 flip-flop dưới cùng
Mỗi chu kỳ của Cycle Clock tương ứng với một chu kỳ máy. Bất cứ khi nào Cycle Clock đi từ thấp lên cao (từ 0 lên 1), một chu kỳ máy mới sẽ bắt đầu.
Ví dụ, trong chương trình mẫu ở phần trước, lệnh đầu tiên là Di chuyển tức thời, hay MVI. Lệnh này cần tới năm chu kỳ máy:
Lấy mã thao tác (Fetch the opcode).
Tăng bộ đếm chương trình.
Lấy byte theo sau mã thao tác.
Tăng bộ đếm chương trình.
Thực thi lệnh.
Đây là năm chu kỳ đó, được dán nhãn hơi ngắn gọn một tí:
5 chu kì cho lệnh MVI
Tất cả chu kỳ này đều liên quan đến việc các bộ đệm ba trạng thái khác nhau được kích hoạt, dẫn đến các giá trị khác nhau sẽ lên bus địa chỉ và bus dữ liệu. Chẳng hạn, trong chu kỳ Fetch 1, bộ đếm chương trình được đưa lên bus địa chỉ, và RAM Data Out được đưa lên bus dữ liệu. Tín hiệu Pulse được dùng để điều khiển đầu vào Clock trên chốt Instruction Byte 1, và đầu vào Clock trên bộ tăng/giảm.
Trong suốt chu kỳ tăng PC, đầu ra của bộ tăng/giảm sẽ ở trên bus địa chỉ, và tín hiệu Pulse được dùng để lưu giá trị đã được tăng đó vào bộ đếm chương trình.
Trước đó bạn đã thấy một mạch chỉ ra một lệnh bao gồm 1, 2 hay 3 byte, và liệu lệnh đó cần chỉ một chu kỳ để thực thi hay tận hai chu kỳ.
Bước tiếp theo trong việc giải mã opcode là tạo ra các tín hiệu cho biết loại chu kỳ nào hiện đang có hiệu lực—dù cho đó là chu kỳ lấy lệnh (fetch) thứ nhất, thứ hai, hay thứ ba, một chu kỳ tăng PC, hay chu kỳ thực thi thứ nhất hoặc thứ hai.
Việc này được phó mặc cho một mạch điện khá phức tạp sau:
Mạch cho biết chu kỳ nào đang có hiệu lực
Các đầu vào nằm bên trái, và các đầu ra nằm bên phải. Mấy đầu vào và đầu ra này thỉnh thoảng có tên na ná nhau, nên thoạt nhìn sơ đồ có thể hơi rối não! Lấy ví dụ, đầu vào "2-Byte Fetch" ám chỉ rằng lệnh có độ dài 2 byte. Trong khi đó đầu ra "Fetch Cycle 2" lại mang thông điệp rằng byte thứ hai của lệnh hiện đang trong quá trình được lấy.
Tín hiệu Reset ở trên cùng cũng chính là tín hiệu Reset trong mạch điện trước đó; nó được kích hoạt bởi một người thao tác với CPU để bắt nó chạy lại từ đầu. Thêm vào đó, bộ đếm 4-bit cũng có thể được reset từ một tín hiệu nằm ở dưới cùng của mạch.
Cycle Clock có nhiệm vụ đẩy bộ đếm tiến lên. Vì đây là bộ đếm 4-bit, nó có thể đếm từ giá trị nhị phân 0000 đến 1111, tức là từ 0 đến 15 trong hệ thập phân. Các đầu ra này đi thẳng tới một bộ giải mã 4-sang-16 nằm bên dưới bộ đếm. Số nhị phân từ bộ đếm được giải mã thành 16 đầu ra tuần tự, nhưng mạch này chỉ xài 9 cái đầu tiên thôi. Mỗi đầu ra báo hiệu một chu kỳ máy mới, bất kể đó là chu kỳ lấy lệnh, chu kỳ tăng bộ đếm chương trình (được viết tắt là "PC Increment" trong sơ đồ), hay một chu kỳ thực thi.
Khi các đầu ra của bộ giải mã bước qua 0, 1, 2, 3, và cứ thế, các tín hiệu sau đây được sinh ra:
0. Fetch Cycle 1
Program Counter Increment
Fetch Cycle 2 nhưng chỉ khi lệnh đó không phải là lệnh lấy 1-byte
Program Counter Increment cho lệnh lấy 2-byte hoặc 3-byte
Fetch Cycle 3 nhưng chỉ khi tín hiệu 3-Byte Fetch là 1
Program Counter Increment cho lệnh lấy 3-byte
Tín hiệu Fetch Cycle 1 và tín hiệu Program Counter Increment đầu tiên lúc nào cũng được sinh ra. Sau đó, mã opcode đã được lấy ra, đưa vào Chốt Lệnh 1, và được giải mã một phần, nên tất cả các tín hiệu đầu vào ở bên trái của sơ đồ đều đã sẵn sàng.
Cùng lắm thì một lệnh sẽ đòi hỏi ba chu kỳ lấy lệnh, mỗi chu kỳ đó lại được nối gót bởi một chu kỳ tăng PC và hai chu kỳ thực thi, tổng cộng là 8, tương ứng với các đầu ra của bộ giải mã từ 0 đến 7.
Mớ logic này rắc rối phết vì nó phải gánh đủ các tình huống lấy lệnh nhiều byte và chu kỳ thực thi nhiều bước. Tỉ dụ như, tín hiệu Execution Cycle 1 ở bên phải có thể là chu kỳ thứ ba đối với các lệnh yêu cầu lấy 1 byte, hoặc là chu kỳ thứ năm đối với các lệnh lấy 2 byte, hoặc chu kỳ thứ bảy với các lệnh lấy 3 byte.
Và cái mớ logic reset ở dưới cùng mới loạn nhất. Nó có thể diễn ra sớm nhất ở chu kỳ thứ tư đối với một lệnh cần một chu kỳ lấy lệnh và một chu kỳ thực thi, hoặc kéo dài tới tận chu kỳ thứ chín cho một lệnh yêu cầu ba chu kỳ lấy lệnh và hai chu kỳ thực thi.
Trong ba chu kỳ lấy lệnh, bộ đếm chương trình sẽ được cho phép lên bus 16-bit, và RAM Data Out sẽ độc chiếm bus 8-bit. Tín hiệu Pulse sẽ lưu giá trị trên bus địa chỉ vào bộ tăng/giảm và giá trị trên bus dữ liệu vào một trong ba chốt lệnh. Đấy chính là mục đích tồn tại của mạch điện sau, chuyên sinh ra mọi tín hiệu cho các chu kỳ lấy lệnh (nhưng chừa lại các chu kỳ tăng PC nhé):
Mạch sinh tín hiệu cho chu kỳ lấy lệnh
Dù cho đó là đợt lấy lệnh thứ nhất, thứ hai hay thứ ba, bộ đếm chương trình vẫn luôn được bật lên bus địa chỉ và RAM Data Out thì lên bus dữ liệu. Trong cả ba trường hợp, tín hiệu Pulse lúc nào cũng điều khiển đầu vào Clock trên chốt Incrementer-Decrementer. Tùy vào chu kỳ lấy lệnh đó là 1, 2, hay 3, tín hiệu Pulse cũng sẽ điều khiển clock trên chốt lệnh tương ứng.
Hãy để ý mấy bộ đệm ba trạng thái được gắn trên hai tín hiệu này nhé. Đó là vì các mạch khác (sắp tới đây thôi) cũng có thể sẽ chen chân vào điều khiển tín hiệu Enable trên bộ đệm ba trạng thái RAM Data Out, cũng như tín hiệu Clock trên chốt Incrementer-Decrementer. Tín hiệu nằm bên trái bộ đệm ba trạng thái vừa làm đầu vào vừa làm tín hiệu Enable.
Các tín hiệu cần thiết cho chu kỳ tăng PC sẽ do mạch này gánh vác toàn bộ:
Mạch tín hiệu cho chu kỳ tăng PC
Xong! Tất tần tật tín hiệu cho các chu kỳ lấy lệnh và chu kỳ tăng PC đã được khai sinh. Bây giờ chỉ còn lại những tín hiệu dành cho chu kỳ thực thi. Mấy món này mới khoai, vì nó còn tùy thuộc vào lệnh cụ thể nào đang được thi hành.
Mạch điện bự chảng ở trang 370 có hai tín hiệu đầu ra mang tên Exec. Cycle 1 và Exec. Cycle 2. Hai chu kỳ thực thi này có thể được gọi thân mật là EC1 và EC2, như trong mạch điện dưới đây:
EC1 và EC2
Hai tín hiệu này lại đan xéo với tín hiệu Pulse để nhào nặn ra thêm hai tín hiệu Execute Pulse (Xung Thực Thi), xin được gọi vắn tắt là EP1 và EP2.
Có một lệnh xử lý khá nhẹ nhàng. Đó chính là lệnh HLT, chuyên trị việc bắt CPU dừng:
Halt
Tín hiệu HLT ở mé trái xuất phát từ bộ giải mã lệnh ở trang 367; tín hiệu Halt ở bên phải chạy sang mạch chứa bộ dao động ở trang 368.
Mối quan hệ mờ ám giữa mấy lệnh còn lại và các tín hiệu cần phải rặn ra thì rối rắm thôi rồi, nên cách khôn ngoan nhất là né việc dùng cả đống cổng logic nhức đầu và thay vào đó xử lý chúng bằng một vài ma trận diode ROM, y hệt như những gì bạn đã thấy ở Chương 18.
Ma trận diode ROM mào đầu này sẽ thầu toàn bộ các tín hiệu Enable và Clock liên đới tới bus địa chỉ 16-bit, cho cả chu kỳ thực thi thứ nhất và thứ hai:
Ma trận diode ROM xử lý tín hiệu Enable và Clock
Nằm lòng nhé, các tín hiệu ở dưới cùng chỉ dành riêng cho bus địa chỉ. Mấy tín hiệu cho bus dữ liệu 8-bit thì bạn sẽ được chiêm ngưỡng ngay đây thôi. Sơ đồ này chính là hiện thân của cột "Bus Địa Chỉ 16-Bit" trong các bảng ở trang 363 và 364.
Bộ đệm ba trạng thái nằm dưới cùng bên trái của sơ đồ được kích hoạt bởi tín hiệu Execution Cycle 1. Nó xem xem giá trị nào sẽ được thả rông trên bus địa chỉ trong chu kỳ thực thi đầu tiên. Rút cục, đối với lệnh MOV, MVI, và các lệnh số học dính líu đến bộ nhớ được định địa chỉ bằng thanh ghi HL, đó chính là thanh ghi HL.
Thêm vào đó, thanh ghi HL cũng được kích hoạt cho các lệnh INX và DCX. Đây là hai tay sai lo việc cộng thêm hoặc trừ đi thanh ghi HL. Tuy nhiên, đối với đám lệnh LDA và STA, thì địa chỉ bộ nhớ dùng để lấy hoặc cất một byte sẽ được lôi ra từ Chốt Lệnh 2 và 3.
Trường hợp của lệnh INX và DCX, tín hiệu Execution Pulse 1 (Xung Thực Thi 1) sẽ ép giá trị của các thanh ghi HL vào nằm trong chốt tăng/giảm.
Hai gã INX và DCX này là những kẻ lạc loài duy nhất dính dáng đến bus địa chỉ trong chu kỳ thực thi thứ hai. Chúng khiến cho giá trị đã được gia giảm của thanh ghi HL xuất hiện trên bus địa chỉ. Tín hiệu Execution Pulse 2 (Xung Thực Thi 2) lúc đó sẽ đưa giá trị mới toanh của HL về lại các thanh ghi H và L.
Ma trận diode ROM cho bus dữ liệu 8-bit thì rắc rối hơn tẹo. Tôi đã chẻ nó ra làm hai sơ đồ ứng với hai chu kỳ lệnh. Đây là chu kỳ lệnh đầu tiên:
Ma trận diode ROM cho bus dữ liệu 8-bit
Mạch này chính là hiện thân của cột "Bus Dữ Liệu 8-Bit" trong bảng ở trang 363. Hai bộ đệm ba trạng thái ở dưới cùng bị kích hoạt bởi các tín hiệu Execution Cycle 1 và Execution Pulse 1. Bộ đệm đầu tiên điều phối xem cái gì sẽ lởn vởn trên bus dữ liệu; còn gã thứ hai thì quản lý xem giá trị đó sẽ bị đưa đi đâu.
Ba thể loại lệnh MOV ở trên cùng đều theo sau bởi một đích đích và một nguồn. Đích đến và nguồn này có thể là bất kỳ thanh ghi nào, hoặc là bộ nhớ được định địa chỉ bằng thanh ghi HL. Khi nguồn là một thanh ghi, mảng thanh ghi (viết tắt là RA trong sơ đồ) sẽ được kích hoạt lên bus dữ liệu; khi nguồn là bộ nhớ, Data Out của RAM sẽ được gọi tên. (Nằm lòng là RAM lúc này đang được định địa chỉ bởi bus 16-bit, và ma trận diode ROM cho bus địa chỉ đã gán giá trị đó thành các thanh ghi HL rồi nhé.) Khi đích đến là một thanh ghi, bộ đệm ba trạng thái thứ hai sẽ điều khiển đầu vào Clock cho mảng thanh ghi. Còn nếu đích đến là bộ nhớ, tín hiệu RAM Write sẽ cất giá trị đó vào bộ nhớ.
Đối với hai thể loại lệnh MVI ("di chuyển tức thời"), nội dung của Chốt Lệnh 2 sẽ được đưa lên bus dữ liệu; giá trị đó hoặc sẽ chui vào mảng thanh ghi hoặc bị ném vào bộ nhớ.
Tất tần tật các lệnh số học và logic đều hội tụ trong sơ đồ này dưới cái bóng của các lệnh ADD và ADI ("cộng tức thời"). Tùy thuộc vào từng lệnh, giá trị được đưa lên bus dữ liệu sẽ là từ mảng thanh ghi, RAM Data Out, hay Chốt Lệnh 2. Kiểu gì thì giá trị đó cũng sẽ bị chốt trong bộ logic số học. Những lệnh này sẽ cần thêm chút sức lực trong chu kỳ thực thi thứ hai, thứ mà bạn sẽ được diện kiến ngay sau đây.
Đối với các lệnh LDA ("tải lên bộ tích lũy") và STA ("lưu từ bộ tích lũy"), ma trận diode ROM cho bus địa chỉ sẽ bảo đảm rằng RAM được định địa chỉ bằng nội dung của Chốt Lệnh 2 và 3. Với LDA, RAM Data Out sẽ được đẩy lên bus dữ liệu, và giá trị đó sẽ lưu vào bộ tích lũy. Với lệnh STA, bộ tích lũy lại được bật lên bus dữ liệu, và giá trị đó sẽ lưu vào bộ nhớ.
Đám lệnh số học và logic đòi hỏi một chu kỳ thực thi thứ hai dính líu tới bus dữ liệu. Ma trận diode ROM cho các ca này lại đơn giản hơn đám còn lại:
Ma trận diode ROM cho lệnh số học và logic
Với các lệnh này, giá trị từ ALU sẽ được đưa lên bus dữ liệu, và giá trị đó ắt phải được cất vào bộ tích lũy, y xì như những gì cột "Bus Dữ Liệu 8-Bit" trong bảng ở trang 364 đã viết.
Vậy là tập con của bộ vi xử lý 8080 mà tôi lụi cụi lắp ráp suốt ba chương qua cuối cùng cũng đã hoàn thiện, và bạn có thể nghịch thử một bản mô phỏng đang chạy ngon lành trên trang web CodeHiddenLanguage.com.
Các kỹ sư thiết kế máy tính thường dành một đống thời gian để vắt óc nghĩ cách làm cho mấy cỗ máy đó chạy càng nhanh càng tốt. Nhiều thiết kế mạch logic kỹ thuật số khác nhau có thể cho ra tốc độ nhanh chậm khác nhau. Thường thì để làm một mạch kỹ thuật số chạy nhanh hơn, ta phải nhét thêm cả rổ cổng logic.
Giả dụ tôi muốn đẩy nhanh tốc độ CPU mà tôi vừa thao thao bất tuyệt, tôi sẽ ngắm nghía mấy chu kỳ lấy lệnh đầu tiên. Mỗi lần lấy lệnh đều phải ngốn một chu kỳ máy thứ hai chỉ để phục vụ mục đích tăng bộ đếm chương trình. Tôi sẽ cố nhồi nhét mớ logic đó vào chung một chu kỳ lấy lệnh để làm hai việc cùng lúc. Việc này chắc kèo sẽ dính líu tới một bộ tăng chuyên biệt (dedicated incrementer). Nâng cấp này sẽ rút nửa thời gian cần thiết để tải các lệnh từ bộ nhớ!
Thậm chí mấy thay đổi lặt vặt cũng mang lại lợi ích siêu to. Giả sử bạn đang thiết kế một CPU có nguy cơ chui vào hàng triệu máy tính, mà mỗi máy tính lại có khả năng thực thi hàng triệu lệnh mỗi giây, thì việc diệt gọn các chu kỳ máy thừa thãi sẽ là một ân huệ to lớn cho người dùng.
Cùng liếc qua một chương trình đơn giản mà CPU này có thể quất được nhé. Tưởng tượng bạn có 5 byte bộ nhớ bắt đầu từ địa chỉ 1000h và bạn muốn viết một chương trình để cộng tổng lại. Nó đây:
Hai lệnh đầu tiên sẽ cài đặt giá trị cho các thanh ghi H và L. Sau đó chương trình dùng HL để truy xuất các byte và tích lũy tổng, nhớ tăng HL sau mỗi bận vào bộ nhớ.
Như bạn thấy đấy, có sự lặp đi lặp lại ở đây. Một lệnh INX được theo sau bởi một lệnh ADD lặp lại tới bốn lần. Điều này chẳng hề hấn gì với một chương trình ngắn thế này, nhưng nếu bạn muốn cộng 20 giá trị thì sao? Hay một trăm? Và lỡ đâu những thứ cần cộng không phải là byte mà là các giá trị 16-bit hay 32-bit đòi hỏi một lô lốc lệnh mới tính ra được tổng thì sao?
Liệu ta có thể né được cảnh phải lặp đi lặp lại code như vậy không? Liệu có thể tạo ra một lệnh khiến các chuỗi lệnh khác phải lặp lại không? Nó sẽ trông như thế nào? Và nó hoạt động ra sao?
Chủ đề này quan trọng đến mức tôi sẽ dành trọn một chương để mổ xẻ nó!