Quay lại
07/04/2026 44 phút

Chương 20: Tự động hóa số học

Loài người chúng ta thường rất sáng tạo và cần mẫn nhưng đồng thời cũng lười biếng vô cùng. Rõ ràng là họ chẳng thích làm việc chút nào. Sự chán ghét này lên tới đỉnh điểm—và trí thông minh cũng nhạy bén đến mức—chúng ta sẵn sàng dành ra vô số giờ đồng hồ để thiết kế và chế tạo những thiết bị thông minh chỉ để cắt giảm vài phút làm việc mỗi ngày. Còn gì thoả mãn bằng cái viễn cảnh được nằm ườn trên võng đu đưa và nhìn cỗ máy mới toanh do chính tay mình chế tạo đang tự động cắt cỏ?

E là tôi sẽ không bày cho bạn bản vẽ thiết kế máy cắt cỏ tự động nào trong sách này đâu. Nhưng trong chương này, tôi sẽ bắt đầu dẫn bạn đi qua quá trình tiến hóa của những cỗ máy ngày càng phức tạp để tự động hóa quá trình cộng và trừ các con số. Nghe chẳng chấn động mấy nhỉ, tôi biết. Nhưng những cỗ máy này sẽ dần trở nên đa năng đến mức chúng có khả năng giải quyết gần như bất kỳ bài toán nào sử dụng phép cộng và trừ, cũng như logic Boolean, và quả thực điều đó bao hàm rất rất nhiều vấn đề đấy.

Tất nhiên, càng tinh vi thì càng phức tạp, nên con đường trước mặt sẽ hơi gồ ghề. Chẳng ai đổ thừa bạn nếu bạn lướt qua những chi tiết khó. Có lúc bạn phản kháng và dặn lòng sẽ không tìm kiếm sự hỗ trợ điện tử nào để giải toán nữa. Nhưng hãy theo tôi thôi, vì cuối con đường ta sẽ phát minh ra một cỗ máy mà có thể được gọi là máy tính hợp lệ.

Bộ cộng gần đây nhất mà ta xem xét nằm ở Chương 17 trên trang 231. Phiên bản đó bao gồm một chốt 8-bit kích cạnh dùng để tích lũy tổng cộng dồn của các số được nhập vào thông qua một cụm tám công tắc:

Tám Công Tắc Tám Bóng Đèn Bộ Cộng 8-Bit A B CI Tổng Chốt 8-Bit D Clk Q Cộng V 8 8 8 8
Chốt 8-bit lưu Tổng của Bộ cộng 8-bit


Như bạn hẳn còn nhớ, một chốt 8-bit sử dụng các flip-flop để lưu trữ một giá trị 8-bit. Ban đầu, nội dung của chốt toàn là số không và đầu ra cũng vậy. Bạn dùng các công tắc để nhập số đầu tiên vào. Bộ cộng chỉ đơn giản là cộng số này với đầu ra số không của chốt, nên kết quả cũng chính là số bạn vừa nhập. Nhấn công tắc Add sẽ lưu số đó vào chốt và bật sáng một vài bóng đèn để hiển thị nó. Vì đây là một chốt kích cạnh, nên chốt sẽ không lưu giá trị mới cho đến khi công tắc Add được nhả ra và nhấn lại lần nữa.

Bây giờ bạn thiết lập số thứ hai trên các công tắc. Bộ cộng sẽ cộng số này với số đang được lưu trong chốt. Nhấn nút Add lần nữa sẽ lưu tổng vào chốt và hiển thị nó bằng bóng đèn. Với cách này, bạn có thể cộng một loạt số và hiển thị tổng cộng dồn. Đương nhiên, hạn chế ở đây là tám bóng đèn không thể hiển thị tổng lớn hơn 255.

Một chốt được dùng để tích lũy tổng cộng dồn của các số thường được gọi là bộ tích lũy (accumulator). Nhưng sau này bạn sẽ thấy rằng một bộ tích lũy không nhất thiết chỉ làm nhiệm vụ tích lũy. Bộ tích lũy thường là một chốt đầu tiên giữ một số, và sau đó con số ấy được kết hợp về mặt số học hoặc logic với một số khác.

Vấn đề lớn với chiếc máy cộng hiển thị ở trên khá hiển nhiên: Giả sử bạn có một danh sách 100 byte cần cộng lại với nhau. Bạn ngồi xuống trước máy cộng và kiên nhẫn còng lưng nhập từng số một và tích lũy tổng. Nhưng khi hoàn thành, bạn lại phát hiện ra một vài số trong danh sách bị sai. Và rồi bạn bắt đầu tự hỏi không biết mình có gõ nhầm thêm lỗi nào trong lúc nhập đống đó không. Giờ thì bạn phải làm lại toàn bộ mọi thứ từ đầu.

Nhưng chắc là phải có cách giải quyết nào đó chứ. Ở chương trước, bạn đã thấy cách sử dụng các flip-flop để xây dựng một mảng RAM chứa 64 KB bộ nhớ. Bạn cũng đã thấy một bảng điều khiển gồm các công tắc và bóng đèn:

Bảng điều khiển RAM 64KB


Việc bật công tắc có nhãn Takeover đúng nghĩa đen là cho phép chúng ta tiếp quản toàn bộ việc ghi và đọc của mảng RAM này, như được trình bày ở đây:

Addr Data Write Enable Bảng Điều Khiển Đèn Addr Data Write Takeover Các đầu vào A Các đầu vào B 26 Bộ chọn 2-sang-1 Select Các đầu ra Addr DI W En RAM 64K x 8 DO Dữ liệu ra 16 8 16 8 16 8 8
Takeover mảng RAM


Nếu bạn đã gõ cả 100 byte vào mảng RAM thay vì nhập trực tiếp vào máy cộng, thì việc kiểm tra các giá trị và sửa lại vài chỗ sai sẽ dễ thở hơn rất nhiều.

Để đơn giản hóa các sơ đồ sau này trong sách, mảng RAM 64K x 8 sẽ đứng một mình thay vì phải kéo theo bảng điều khiển và 26 bộ chọn cần thiết cho việc tiếp quản thao tác đọc và ghi:

Address Dữ liệu vào Write Enable Dữ liệu ra Addr DI W EN DO 65,536 × 8 RAM 16 8 8
Mảng RAM 64K x 8


Sự tồn tại của bảng điều khiển—hay một thứ gì đó tương đương cho phép con người chúng ta ghi các byte vào mảng bộ nhớ và đọc chúng ra—đã được ngầm hiểu trong sơ đồ tối giản này. Đôi khi tôi cũng sẽ lờ luôn cả tín hiệu Enable. Bạn cứ mặc định coi như các bộ đệm ba trạng thái cho Data Out luôn được kích hoạt nếu tín hiệu này không xuất hiện nhé.

Giả sử chúng ta muốn cộng 8 byte—ví dụ, các giá trị thập lục phân 35h, 1Bh, 09h, 31h, 1Eh, 12h, 23h và 0Ch. Nếu bạn lôi ứng dụng máy tính trên Windows hay macOS ra và bật chế độ Programmer lên, bạn sẽ thấy ngay tổng của chúng là E9h, nhưng hãy tự thử thách bản thân bằng cách chế tạo một chút phần cứng có thể tự cộng đống số này.

Bằng cách sử dụng bảng điều khiển, bạn có thể tống 8 byte này vào mảng RAM bắt đầu từ địa chỉ 0000h. Khi xong xuôi, nội dung của mảng RAM có thể được ký hiệu thế này:

           +-------+
  0000h:   |  35h  |
           +-------+
           |  1Bh  |
           +-------+
           |  09h  |
           +-------+
           |  31h  |
           +-------+
           |  1Eh  |
           +-------+
           |  12h  |
           +-------+
           |  23h  |
           +-------+
           |  0Ch  |
           +-------+
  0008h:   |  00h  | <--- Tổng vào đây
           +-------+
           |  ...  |
           +-------+

Từ giờ trở đi, đây là cách tôi sẽ biểu diễn một phần của bộ nhớ. Các ô vuông đại diện cho nội dung của bộ nhớ. Mỗi byte bộ nhớ nằm gọn trong một ô. Địa chỉ của ô đó nằm ở bên trái. Không phải mọi địa chỉ đều cần được ghi rõ, vì các địa chỉ mang tính tuần tự và bạn luôn có thể tự suy ra địa chỉ nào gắn với ô nào. Ở bên phải là vài lời bình luận về phần bộ nhớ này. Cụ thể thì lời bình luận này gợi ý rằng chúng ta muốn chế ra một thứ gì đó cộng 8 byte đầu tiên lại rồi ghi tổng đó vào vị trí bộ nhớ đầu tiên chứa một byte 00h, trong trường hợp này chính là địa chỉ 0008h.

Tất nhiên, bạn đâu có bị trói buộc vào việc chỉ lưu 8 con số. Nếu có 100 số, bạn sẽ lưu chúng ở các địa chỉ từ 0000h đến 0063h. Bây giờ chúng ta phải đối mặt với một thách thức là kết nối mảng RAM với bộ cộng tích lũy từ Chương 17, để tôi nhắc lại cho bạn nhớ nó trông như thế này:

Tám Công Tắc Tám Bóng Đèn Bộ Cộng 8-Bit A B CI Tổng Chốt 8-Bit D Clk Q Cộng V 8 8 8 8
Chốt 8-bit lưu Tổng của Bộ cộng 8-bit


Đám công tắc và bóng đèn đó giờ chẳng còn cần thiết nữa vì chúng ta đã có sẵn công tắc và bóng đèn trên bảng điều khiển được nối với mảng bộ nhớ rồi. Chúng ta có thể thay thế các công tắc đi vào bộ cộng bằng các tín hiệu Data Out từ mảng RAM. Và thay vì để đầu ra từ chốt thắp sáng bóng đèn, ta có thể lái đường ra đó đến các đầu vào Data In của RAM:

64K × 8 RAM Addr DO DI Write Bộ Cộng 8-Bit A B Sum Chốt 8-Bit D Clk Q 8 8 8 8
Chốt vào Ram


Dĩ nhiên là bức tranh này vẫn còn thiếu vài mảnh ghép. Nó không cho bạn biết cái gì được kết nối với tín hiệu Clock trên chốt, thứ vốn đóng vai trò tối quan trọng để lưu tổng tích lũy. Nó cũng chẳng thèm chỉ ra cái gì nối với tín hiệu Write trên RAM, thứ rất cần thiết để lưu kết quả cuối cùng. RAM cũng đang thiếu một địa chỉ 16-bit bắt buộc phải có để truy cập nội dung bên trong.

Đầu vào Address của RAM phải tăng dần một cách tuần tự, khởi đầu ở 0000h, rồi nhích lên 0001h, 0002h, 0003h, v.v. Đây đúng là công việc sinh ra dành cho một bộ đếm được lắp từ một chuỗi các flip-flop nối tiếp, như cái bạn đã thấy ở trang 237 của Chương 17:

Bộ Đếm 16-Bit Đầu ra Clk 16
Bộ đếm 16-bit


Hãy để ý rằng đường dẫn dữ liệu từ đầu ra của bộ đếm được vẽ bè ra một chút để ám chỉ nó chứa 16 bit chứ không chỉ 8.

Bộ đếm này cung cấp đầu vào Address cho RAM:

RAM 64K × 8 Addr DO DI Write Bộ Đếm 16-Bit Đầu ra Clk Bộ Cộng 8-Bit A B Tổng Chốt 8-Bit D Clk Q 16 8 8 8 8
Tích hợp Bộ đếm 16-bit


Tôi gọi cỗ máy này là Bộ Cộng Tích Lũy Tự Động (Automated Accumulating Adder).

Tất nhiên, bằng cách nhét thêm một bộ đếm vào để cấp địa chỉ cho RAM, chúng ta lại vừa "đẻ" ra thêm một sự thiếu hụt nữa: tín hiệu Clock cần thiết để tăng giá trị của bộ đếm. Nhưng chúng ta đang đi đúng hướng rồi. Toàn bộ các đường dữ liệu 8-bit và 16-bit nòng cốt đã được định hình. Giờ đây tất cả những gì ta cần là ba tín hiệu sau:

  • Đầu vào Clock cho bộ đếm
  • Đầu vào Clock cho chốt
  • Đầu vào Write cho bộ nhớ truy cập ngẫu nhiên

Những tín hiệu kiểu này đôi khi được gọi chung là các tín hiệu điều khiển (control signals), và chúng thường hóa ra lại là phần nhức não nhất của một mạch điện kiểu này. Ba tín hiệu phải được phối hợp và đồng bộ hóa nhịp nhàng với nhau.

Đầu vào Clock cho bộ đếm khiến bộ đếm nhích lên địa chỉ tiếp theo, từ 0000h đến 0001h, và rồi từ 0001h đến 0002h, cứ thế. Địa chỉ đó dùng để truy cập vào một byte bộ nhớ nhất định, byte này sau đó sẽ chui vào bộ cộng cùng với đầu ra của chốt. Kế tiếp, đầu vào Clock trên chốt phải lưu lại tổng số mới đó. Ở ngoài đời thực, việc truy cập bộ nhớ và thực hiện phép cộng mất một xíu xiu thời gian, điều này có nghĩa là tín hiệu Clock trên chốt phải phát ra một khoảng thời gian ngắn sau khi tín hiệu Clock trên bộ đếm diễn ra, và tương tự, tín hiệu Clock tiếp theo trên bộ đếm cũng phải chờ một lúc sau tín hiệu Clock trên chốt.

Để đạt được điều đó, hãy đi dây cho hai flip-flop như thế này nhé:

Bộ dao động Flip-Flop 1 Flip-Flop 2 Clk D Q Q Clk D Q Q Đầu vào Clk cho bộ đếm Pulse
Đi dây cho 2 Flip-flop


Bộ dao động ở ngoài cùng bên trái chỉ là một thứ đảo đi đảo lại giữa 0 và 1. Nó có thể cực nhanh, chẳng hạn như bộ dao động thạch anh dùng trong đồng hồ và máy tính, hoặc nó có thể đơn giản như một cái công tắc hay nút bấm mà bạn dùng ngón tay tự nhấp.

Flip-flop đầu tiên được nối mạch để chia đôi tần số đó, giống như bạn đã thấy ở gần cuối Chương 17. Đầu ra Q của flip-flop sẽ thành đầu vào Clock cho bộ đếm, giúp tăng giá trị của bộ đếm lên mỗi khi có sự chuyển đổi từ 0 sang 1. Đây là biểu đồ định thời cho flip-flop đầu tiên:

Flip-Flop 1 Clk Flip-Flop 1 D Flip-Flop 1 Q Đầu ra bộ đếm 0000h 0001h 0002h
Biểu đồ định thời cho Flip-flop đầu tiên


Dòng dưới cùng của biểu đồ định thời tượng trưng cho cách đầu ra của bộ đếm thay đổi.

Đầu vào Clock cho flip-flop thứ hai ngược với flip-flop đầu tiên, và đầu vào D chính là đầu ra Q từ flip-flop thứ nhất, nghĩa là đầu ra Q của flip-flop thứ hai sẽ bị lệch pha đúng một chu kỳ so với đầu ra Q của cái thứ nhất. Để tiện so sánh, biểu đồ dưới đây ôm trọn cả phần đầu ra bộ đếm từ biểu đồ trước đó:

Đầu ra bộ đếm Flip-Flop 2 Clk Flip-Flop 2 D Flip-Flop 2 Q Pulse 0000h 0001h 0002h
Biểu đồ cho Flip-flop 2


Cổng AND kết hợp đầu ra Q̄ từ flip-flop thứ nhất và đầu ra Q từ flip-flop thứ hai. Tôi sẽ gọi đầu ra từ cổng AND đó là Pulse (Xung nhịp).

Tín hiệu Pulse này sẽ trở thành đầu vào Clock của chốt:

Chốt 8-Bit D Q Clk Tín hiệu Pulse từ Flip-flop
Pulse vào Chốt


Chúng ta muốn đảm bảo rằng có dư dả thời gian để giá trị từ bộ đếm xác định địa chỉ bộ nhớ, và để dữ liệu từ bộ nhớ được cộng vào tổng số trước đó trước khi nó được lưu trong chốt. Mục tiêu là để đảm bảo rằng mọi thứ đã vào độ ổn định trước khi chốt lưu tổng mới. Nói cách khác, ta muốn né mấy lỗi chập chờn. Điều này đã được hiện thực hóa: Đầu ra của bộ đếm đứng im bất động khi tín hiệu Pulse là 1.

Tín hiệu khác cần có trong Bộ Cộng Tích Lũy Tự Động là tín hiệu Write cho bộ nhớ. Tôi đã đề cập lúc nãy rằng ta muốn ghi lại tổng tích lũy vào vị trí bộ nhớ đầu tiên có giá trị 00h. Vị trí bộ nhớ đó có thể được "đánh hơi" bằng cách móc nối vào các tín hiệu Data Out từ RAM và lùa chúng vào một cổng NOR 8-bit. Đầu ra của cổng NOR này là 1 nếu tất cả các giá trị Data Out riêng lẻ đều là 0. Đầu ra đó sau đó có thể được kết hợp với đầu ra Pulse từ cụm flip-flop:

Dữ liệu ra RAM Tín hiệu Pulse từ Flip-flop Ghi RAM
Đầu ra cổng NOR + Pulse


Một phiên bản tương tác của Bộ Cộng Tích Lũy Tự Động hoàn chỉnh đã lên sóng trên trang web CodeHiddenLanguage.com.

Vẫn chưa có biện pháp nào được chuẩn bị để ngăn Bộ Cộng Tích Lũy Tự Động chạy mãi không ngừng. Chừng nào bộ dao động còn tiếp tục phát tín hiệu luân phiên giữa 0 và 1, bộ đếm vẫn còn truy cập bộ nhớ. Nếu các byte khác trong bộ nhớ bằng 00h, mạch sẽ ghi tổng hoàn chỉnh vào những vị trí đó.

Cuối cùng, nếu bộ dao động vẫn cứ chạy, bộ đếm sẽ đạt tới FFFFh, và rồi nó sẽ quay vòng lại giá trị 0000h và bắt đầu lại bài ca cộng tích lũy từ đầu. Có điều lần này, nó sẽ cộng thêm tất cả các giá trị trong bộ nhớ vào tổng đã được tính trước đó.

Để nắm quyền kiểm soát đối với quá trình này, có lẽ bạn sẽ muốn cắm thêm một nút bấm hay công tắc dán nhãn Clear (Xóa). Bộ đếm cung cấp địa chỉ bộ nhớ được lắp ráp từ các flip-flop kích cạnh, nên rất có thể nó đã được thủ sẵn một đầu vào Clear. Chốt cũng được làm từ các flip-flop kích cạnh, và các flip-flop kích cạnh cũng đã được trưng dụng để tạo ra các tín hiệu điều khiển. Nút Clear này có thể xoá sạch bộ đếm cùng với chốt và bắt cặp flip-flop kia ngưng hoạt động. Khi đó, bạn có thể thong thả nhập các giá trị mới vào bộ nhớ và khởi động lại quá trình cộng.

Ấy nhưng vấn đề lớn nhất của Bộ Cộng Tích Lũy Tự Động là nó bị giới hạn ở việc chỉ có thể cộng byte, mà giá trị của byte thì chỉ lanh quanh từ 00h đến FFh, hay 255 trong hệ thập phân.

Ví dụ công việc mà tôi vạch ra cho Bộ Cộng Tích Lũy Tự Động này là cộng 8 byte 35h, 1Bh, 09h, 31h, 1Eh, 12h, 23h, và 0Ch, thành E9h, hay 233 thập phân. Nhưng giả sử có thêm byte thứ chín là 20h thì sao? Tổng số khi đó sẽ lên tới 109h. Cơ mà con số đó đâu còn nằm gọn trong giá trị 1-byte nữa. Đầu ra của bộ cộng 8-bit sẽ chỉ chưng ra được 09h, và đó cũng là những gì sẽ được nạp vào bộ nhớ. Tín hiệu Carry Out của bộ cộng sẽ chỉ báo rằng tổng này đã vượt FFh, ngặt nỗi Bộ Cộng Tích Lũy Tự Động lại chẳng hề làm gì với tín hiệu ấy.

Giả sử bạn muốn dùng Bộ Cộng Tích Lũy Tự Động để dò lại số tiền gửi trong tài khoản vãng lai của mình. Ở Hoa Kỳ, tiền bạc được đếm bằng đô la và xu—ví dụ như $1.25—và nhiều quốc gia khác cũng xài các hệ thống tương tự. Để nhét giá trị đó vào một byte, bạn sẽ cần phải phù phép biến nó thành một số nguyên bằng cách nhân với 100 để có 125 xu, tương đương 7Dh trong hệ thập lục phân.

Điều đó nghĩa là nếu bạn muốn lấy byte để lưu các khoản tiền, bạn sẽ bị kẹt ở mức tối đa là FFh, hay 255 thập phân, hay một con số bèo bọt: $2.55.

Bạn sẽ phải dùng thêm nhiều byte hơn cho các số tiền lớn hơn. Thế dùng hai byte thì sao? Các giá trị hai byte có thể bắt đầu từ 0000h đến FFFFh, hay 65.535 trong hệ thập phân, hay là $655.35.

Khá hơn nhiều rồi đấy, nhưng rất có thể bạn cũng sẽ muốn biểu diễn cả số tiền âm cũng như tiền dương—ví dụ như khi tài khoản vãng lai của bạn bị thấu chi (xài lố tiền trong thẻ). Điều đó đồng nghĩa với việc bạn phải cầu viện đến số bù hai, thứ mà tôi đã bàn ở Chương 16. Với số bù hai, giá trị dương 16-bit to nhất là 7FFFh, hay 32.767 thập phân, và giá trị âm nhỏ nhất là 8000h, tức là -32.768. Lựa chọn này sẽ chứa được khoảng tiền từ -$327.68 đến $327.67.

Hãy thử 3 byte xem. Nếu xài số bù hai, các giá trị 3 byte có thể trải dài từ 800000h đến 7FFFFFh, hay từ -8.388.608 đến 8.388.607 thập phân, đổi ra tiền mặt thì sẽ từ -$83,886.08 đến $83,886.07. Tôi trộm nghĩ đây là một giới hạn an toàn hơn nhiều đối với tài khoản vãng lai của đa số mọi người, vậy nên chốt phương án này nhé.

Làm thế nào để "độ" bộ cộng tự động này lên để nó giữ được các giá trị 3-byte thay vì chỉ 1-byte đây?

Câu trả lời đơn giản là cứ phình to bộ nhớ ra để chứa các giá trị 24-bit, rồi đi lắp ráp các bộ cộng và chốt 24-bit.

Nhưng cách đó nghe chừng không khả thi cho lắm. Có lẽ bạn đã rót vốn để làm ra một mảng RAM 64K x 8 rồi, lại còn đang cầm trong tay một bộ cộng 8-bit, và mấy thứ này đâu phải muốn thay là thay dễ dàng được.

Nếu ta quyết thủy chung với bộ nhớ 8-bit, các giá trị 24-bit có thể được lưu bằng cách chia chúng ra và nhét vào ba vị trí bộ nhớ liền kề nhau. Nhưng câu hỏi chí mạng ở đây là: Theo chiều nào?

Tôi nói thế là có ý gì?

Giả sử bạn muốn cất giá trị $10,000.00. Đổi ra là 1.000.000 xu hay 0F4240h ở hệ thập lục phân, tách ra là 3 byte 0Fh, 42h, và 40h. Ta có thể gọi 3 byte này là byte "cao", "giữa", và "thấp". Thế nhưng chúng có thể được cất vào bộ nhớ theo một trong hai cách. Ta sẽ lưu 3 byte này theo thứ tự như này?

           +-------+
  0000h:   |  0Fh  | <-- Byte Cao
           +-------+
           |  42h  | <-- Byte Giữa
           +-------+
           |  40h  | <-- Byte Thấp
           +-------+
           |  ...  |
           +-------+


Hay như này?

           +-------+
  0000h:   |  40h  | <-- Byte Thấp
           +-------+
           |  42h  | <-- Byte Giữa
           +-------+
           |  0Fh  | <-- Byte Cao
           +-------+
           |  ...  |
           +-------+


Bạn có thể sẽ thắc mắc: Kiểu nào mới đúng đây? Hay: Chuẩn mực ngành ngàn đời nay người ta làm theo cách nào? Buồn thay, câu trả lời cho những thắc mắc đó lại là: Cả hai. Vài hệ thống máy tính ưng làm cách này; số khác lại làm cách kia.

Hai phương thức để lưu trữ các con số đa byte (multibyte) này được biết đến với cái tên big-endianlittle-endian. Tôi đã từng đá động tới sự phân định này ở Chương 13 khi bàn về Unicode. Mấy thuật ngữ này mượn từ cuốn tiểu thuyết trào phúng Gulliver Du Ký của Jonathan Swift (Phần I, Chương 4 và các phần sau), trong đó mô tả cảnh dân tình xứ Lilliput đã bị cuốn vào một cuộc cãi vả trường kỳ chỉ vì không thống nhất được nên đập vỏ trứng luộc ở đầu nhỏ hay đầu to. Trong ngành công nghiệp máy tính, đây chẳng phải là một cuộc tranh cãi nảy lửa gì cho cam mà giống một sự khác biệt cơ bản mà mọi người đều đã học cách sống chung.

Thoạt nhìn, phe big-endian có vẻ hợp lý hơn vì nó thuận theo thứ tự mà chúng ta thường dùng để viết các byte. Còn phái little-endian lại trông như đi lùi vì nó bắt đầu bằng byte ít quan trọng nhất.

Nhưng khoan đã, nếu bạn đang dò đọc các giá trị đa byte từ bộ nhớ chỉ để làm phép cộng, bạn sẽ muốn việc bắt đầu từ byte ít quan trọng nhất. Phép cộng ở byte ít quan trọng nhất có thể sẽ sinh một cờ nhớ để dùng tiếp trong phép cộng ở byte quan trọng tiếp theo.

Vì cớ đó, tôi sẽ lưu các byte theo định dạng little-endian—đưa byte ít quan trọng nhất lên đầu. Nhưng cái này chỉ áp dụng cho thứ tự lúc chúng yên vị trong bộ nhớ thôi nhé. Khi trình diễn các giá trị thập lục phân ở những chỗ khác, tôi sẽ vẫn giữ thói quen ưu tiên xếp byte quan trọng nhất lên đầu.

Trong lúc họa lại bức tranh về những gì cỗ máy mới này có thể mần được, tôi sẽ dùng mấy từ ngữ như "tiền gửi" (deposits) và "rút tiền" (withdrawals) y như thể nó đang hạch toán số dư cho một tài khoản ngân hàng vậy. Nói vậy chứ mang nó đi tính toán chi phí và doanh thu khi vận hành doanh nghiệp, hay quản lý tài sản và nợ nần cũng mượt không kém đâu.

Hãy mở hàng với hai khoản gửi là $450.00 và $350.00. Ở hệ thập lục phân, chúng sẽ được cộng lại như sau:

  00 AF C8
+ 00 88 B8
----------
  01 38 80

Khi mỗi cặp byte được cộng lại từ phải sang trái, một cờ nhớ sẽ được sinh ra làm ảnh hưởng tới cặp byte kế tiếp.

Bây giờ hãy thử rút $500.00 ra khỏi tổng đó xem, giá trị là 00C350h ở hệ thập lục phân:

  01 38 80
– 00 C3 50
----------

Ở Chương 16 tôi đã miêu tả cách các số nhị phân bị trừ đi thế nào. Đầu tiên bạn phải làm phép chuyển đổi số bị trừ sang số bù hai và sau đó đem cộng. Để tìm ra số bù hai của 00C350h, hãy đảo lộn toàn bộ các bit (bit 0 thành bit 1 và bit 1 thành bit 0) để có được FF3CAFh, rồi cộng thêm 1 để được FF3CB0h. Giờ lấy nó ra cộng thôi:

  01 38 80
+ FF 3C B0
----------
  00 75 30

Trong hệ thập phân, tổng này là 30.000, tức $300. Giờ hãy rút thêm $500 nữa:

  00 75 30
+ FF 3C B0
----------
  FF B1 E0

Đời không như mơ, kết quả là một số âm. Số dư của chúng ta đã tụt xuống dưới 0! Để xem mặt mũi cái giá trị âm đó ra sao, hãy lặp lại việc đảo ngược tất cả các bit để lấy 004E1Fh và cộng thêm 1 ta được 004E20h, hay 20.000 trong hệ thập phân. Số dư đang là –$200.00.

May thay, lộc tới rồi. Lần này món tiền gửi siêu to khổng lồ $2000.00, hay 030D40h. Cộng vào kết quả âm khi nãy:

  FF B1 E0
+ 03 0D 40
----------
  02 BF 20

Và con số đó, tôi rất hoan hỉ thông báo, là 180.000 thập phân, tương đương $1800.00.

Đấy chính xác là thứ công việc mà tôi muốn cái cỗ máy mới này sẽ làm. Tôi muốn nó cộng và trừ các giá trị 3-byte nằm trong bộ nhớ, và tôi muốn nó nhả kết quả ngược trở lại bộ nhớ.

Và dĩ nhiên là tôi thực sự muốn nó làm được phép trừ. Tôi muốn lưu một khoản rút $500 dưới dạng 3 byte là 00, C3, và 50, chứ không thèm phải số bù hai. Tôi muốn cái máy này phải tự tính lấy số bù hai.

Nhưng nếu tất cả các số đều được lưu trong bộ nhớ dưới dạng giá trị dương, thì tiền gửi với tiền rút trông như hai giọt nước. Làm sao để phân biệt chúng đây?

Chúng ta cần gài thêm một thứ gì đó với các con số trong bộ nhớ để làm chỉ điểm, cho ta biết mình muốn làm gì với chúng. Sau một hồi trăn trở—cần thì thức đêm luôn—bạn có thể sẽ lóe lên ý tưởng thiên tài là đặt trước mỗi con số trong bộ nhớ một mã nào đó. Một mã có thể ngụ ý "hãy cộng giá trị 3-byte tiếp theo vào đi," và một mã khác có thể mang thông điệp "hãy trừ giá trị 3-byte tiếp theo đi." Nó có thể sẽ trông như thế này trong bộ nhớ cho bài toán ví dụ tôi vừa kể ở trên:

           +-----+
  0000h:   | 02h | <--- Mã để cộng giá trị kế tiếp
           +-----+ --
           | C8h |  |
           +-----+  |
           | AFh |  |-> 00AFC8h hay $450.00
           +-----+  |
           | 00h |  |
           +-----+ --
  0004h:   | 02h | <--- Mã để cộng giá trị kế tiếp
           +-----+ --
           | B8h |  |
           +-----+  |
           | 88h |  |-> 0088B8h hay $350.00
           +-----+  |
           | 00h |  |
           +-----+ --
  0008h:   | 03h | <--- Mã để trừ giá trị kế tiếp
           +-----+ --
           | 50h |  |
           +-----+  |
           | C3h |  |-> 00C350 hay $500.00
           +-----+  |
           | 00h |  |
           +-----+ --
  000Ch:   | 03h | <--- Mã để trừ giá trị kế tiếp
           +-----+ --
           | 50h |  |
           +-----+  |
           | C3h |  |-> 00C350 or $500.00
           +-----+  |
           | 00h |  |
           +-----+ --
  0010h:   | 02h | <--- Mã để cộng giá trị kế tiếp
           +-----+ --
           | 40h |  |
           +-----+  |
           | 0Dh |  |->  030D40 or $2000.00
           +-----+  |
           | 03h |  |
           +-----+ --

Tôi đã chọn mã 02h để biểu thị cho việc cộng giá trị 3-byte tiếp theo vào tổng cộng dồn, và một mã 03h để ám chỉ lệnh trừ. Mấy mã này trông có vẻ ngẫu nhiên, nhưng không hẳn đâu. (Bạn sẽ mau chóng thấu hiểu ẩn ý của tôi thôi.)

Mã như vầy đôi khi được gọi với cái tên là mã lệnh (instruction codes) hoặc mã thao tác (operation codes) hay opcodes. Chúng dặn dò một cỗ máy đang đọc bộ nhớ phải làm gì, và cỗ máy sẽ ngoan ngoãn chiều theo bằng cách thực hiện một vài thao tác cụ thể như cộng hoặc trừ.

Nội dung của bộ nhớ giờ đây có thể được phân chia rạch ròi thành (code) và dữ liệu (data). Lấy ví dụ này, mỗi byte mã sẽ đi trước 3 byte dữ liệu.

Giờ khi đã nắm trong tay bảo bối là các mã để cộng và trừ các giá trị, hãy cùng nhau chế ra một mã khác để lưu lại tổng cộng dồn vào bộ nhớ nằm ngay sau đoạn mã, và thêm một mã khác để bắt cỗ máy dừng lại, tránh việc nó cứ chạy nhong nhong mà chẳng có việc gì làm:

           +-----+
  0014h:   | 04h | <--- Mã để lưu tổng đang tính
           +-----+ --
           | 00h |  |
           +-----+  |
           | 00h |  |-> Nơi lưu giá trị
           +-----+  |
           | 00h |  |
           +-----+ --
  0018h:   | 08h | <--- Mã để dừng
           +-----+

Thống nhất gọi cỗ máy làm được trò kỳ diệu này là Bộ Tích Lũy Ba-Byte (Triple-Byte Accumulator). Giống như Bộ Cộng Tích Lũy Tự Động, nó sẽ tiếp tục truy cập mảng bộ nhớ 64K x 8, và tích lũy tổng cộng dồn bằng cách sử dụng một bộ cộng 8-bit. Có điều số lượng chốt giờ đây phải phình to lên thành bốn—một cái để giữ mã lệnh và ba cái còn lại thì nhốt tổng cộng dồn. Đây là toàn bộ các thành phần chính và những đường dẫn dữ liệu:

RAM Bộ Đếm Chốt Lệnh Bù 1 Bộ Cộng Chốt Cao Chốt Giữa Chốt Thấp 3-Trạng-Thái 3-Trạng-Thái 3-Trạng-Thái Addr DO DI D Q A B Sum D Q D Q D Q
Bộ Tích Luỹ 3-Byte


Nhằm giúp sơ đồ bớt phức tạp, không một bóng dáng tín hiệu điều khiển nào trong mớ hổ lốn đó được vẽ ra. Tôi sẽ dành phần lớn thời gian còn lại của chương này để cho bạn thấy mấy tín hiệu điều khiển ấy. Chẳng thèm góp mặt trong sơ đồ đã quá rối rắm này còn có đầu vào của vô vàn những chiếc hộp gắn liền với các tín hiệu điều khiển—ví dụ như, các đầu vào Clock tới bộ đếm và các chốt, các tín hiệu Enable cho các bộ đệm ba trạng thái, và tín hiệu Write dành cho RAM.

Giống với Bộ Cộng Tích Lũy Tự Động, Bộ Tích Lũy Ba-Byte sở hữu một bộ đếm 16-bit chuyên cung cấp địa chỉ cho bộ nhớ truy cập ngẫu nhiên. Đầu vào Clock của bộ đếm này đến từ cùng một cấu hình gồm hai flip-flop như đã trình bày trước đó.

Bộ Tích Lũy Ba-Byte có bốn chốt: Chốt đầu tiên tên "Inst. Latch," là viết tắt của "instruction latch" (chốt lệnh). Anh bạn này có nhiệm vụ giam giữ mã lệnh tới từ các địa chỉ bộ nhớ 0000h, 0004h, 0008h, và cứ thế.

Ba chốt còn lại được dán nhãn "High", "Mid", và "Low". Chúng được dùng để chứa 3 byte của tổng cộng dồn. Đầu vào của ba chốt này là đầu ra Sum của bộ cộng. Các đầu ra lại đi vào ba hộp dán nhãn “Tri-State,” không đâu khác chính là các bộ đệm ba trạng thái mà tôi đã mô tả ở chương trước. Mấy cái hộp này giấu các tín hiệu Enable trong hình minh họa. Như bản tính của mọi bộ đệm ba trạng thái khác, nếu tín hiệu Enable là 1, thì Outputs y hệt như Inputs—0 nếu input là 0, và 1 nếu input là 1. Khổ nỗi, nếu tín hiệu Enable là 0, thì Outputs không phải 0 cũng chả phải 1—chẳng phải là một điện áp cũng chả phải một cực nối đất, mà lại là không gì cả. Đó chính là trạng thái thứ ba.

Mỗi một trong số ba bộ đệm ba trạng thái xuất hiện trong Bộ Tích Lũy Ba-Byte đều sở hữu riêng một tín hiệu Enable. Ở bất kỳ khoảnh khắc nào, chỉ có một trong ba tín hiệu Enable này là 1. Nó cho phép các đầu ra của ba bộ đệm ba trạng thái được phép nối với nhau mà không hề hấn gì trước sự va chạm giữa điện áp và cực nối đất. Tín hiệu Enable điều khiển xem đầu ra nào trong ba chốt được phái đến Data In của RAM và chạy sang đầu vào B của bộ cộng.

Ba tín hiệu Enable này phải nhờ vào 2 bit ít quan trọng nhất trong địa chỉ bộ nhớ do bộ đếm sinh ra. Các byte trong bộ nhớ đã được bài trí theo một cung cách lớp lang: Khởi xướng là một byte lệnh rồi theo sau là các byte thấp, giữa, và cao của một số 3-byte. Vai trò của 4 byte này sẽ ăn khớp với 2 bit ít quan trọng nhất của địa chỉ bộ nhớ. Bộ đôi 2 bit này sẽ đi từ 00 lên 01 rồi 10 tới 11 theo một vòng lặp bất tận để đảm bảo rằng mỗi giá trị sẽ tương ứng với một thể loại byte riêng biệt trong bộ nhớ:

  • Nếu 2 bit thấp hơn của địa chỉ là 00, thì byte tại địa chỉ đó sẽ là mã lệnh.
  • Nếu 2 bit này là 01, thì byte tại đó sẽ là byte ít quan trọng nhất (byte thấp) của số đem cộng hoặc trừ.
  • Tương tự, nếu 2 bit thấp hơn là 10, thì nó là byte giữa.
  • Còn nếu là 11, nó ắt hẳn là byte cao.

Ba tín hiệu Enable dành cho các bộ đệm ba trạng thái hoàn toàn có thể được tạo ra bởi một bộ giải mã 2-sang-4, dùng 2 bit ít quan trọng nhất của địa chỉ bộ nhớ, tạm gọi là A0 và A1 ở đây:

A1 A0 Tín hiệu Pulse từ Flip-flop Bộ giải mã 2-sang-4 Bit 1 Bit 0 11 10 01 00 Enable Byte Cao Enable Byte Giữa Enable Byte Thấp Chốt Lệnh cho Clock
3 tín hiệu Enable


Xuất hiện chung luôn là đầu vào Clock cho chốt lệnh. Chuyện này xảy đến khi 2 bit thấp nhất của địa chỉ là 00 và tín hiệu Pulse từ bộ cấu hình hai flip-flop là 1. Lệnh vẫn sẽ bám trụ trong chốt đó trong thời gian 3 byte tiếp theo bị truy cập. Mấy tín hiệu Clock cho ba chốt còn lại thì nhức não hơn tẹo, nhưng tôi sẽ sớm giải thích.

Đây là cách Bộ Tích Lũy Ba-Byte hoạt động. Giả sử là tất cả các chốt ban đầu đã bị dọn dẹp sạch sẽ và trống trơn: Bộ đếm sẽ phát ra một địa chỉ RAM là 0000h. Byte đậu ở địa chỉ đó (trong ví dụ này là 02h) sẽ bị "nhốt" vào chốt lệnh.

Bộ đếm tiếp tục phát ra địa chỉ RAM là 0001h. Đấy chính là nơi trong bộ nhớ chứa byte thấp của số đầu tiên. Byte đó tiến vào bộ cộng. (Tạm thời cứ lơ cái hộp đeo mác "1s' Comp." đi nhé; cứ coi như nó ngồi chơi xơi nước, và điều này hoàn toàn đúng khi có một phép cộng đang rục rịch diễn ra.) 2 bit thấp hơn của địa chỉ lúc này là 01, nên bộ đệm ba trạng thái cho byte thấp sẽ được chọn. Cơ mà vì chốt byte thấp đã bị xoá, đầu vào B của bộ cộng chỉ là 00h. Đầu ra Sum của bộ cộng nghiễm nhiên trở thành byte thấp của con số đầu tiên. Giá trị đó sẽ bị chốt trong chốt byte thấp.

Kế đến, bộ đếm phát ra địa chỉ RAM 0002h. Ấy chính là byte giữa của số đầu tiên. Nó phi thẳng vào bộ cộng cùng giá trị trong chốt giữa, lúc này là 00h. Đầu ra Sum y chang byte giữa từ bộ nhớ, và rồi bị chốt byte giữa.

Lại một lần nữa, bộ đếm phát ra địa chỉ RAM 0003h. Đó là byte cao, và nó sẽ xuyên qua bộ cộng rồi ngoan ngoãn nằm trong chốt byte cao.

Bộ đếm phát ra địa chỉ RAM 0004h. Đây là một mã lệnh mang giá trị 02h, ám chỉ lệnh cộng.

Bộ đếm phát ra địa chỉ RAM 0005h. Đó là byte thấp của con số thứ hai. Nó vào đầu vào A của bộ cộng. Bộ đệm ba trạng thái byte thấp được bật. Bởi lẽ chốt lúc này đang giữ byte thấp của số đầu tiên, nó trở thành đầu vào B của bộ cộng. Hai byte đó được cộng lại và rồi được chốt trong chốt byte thấp.

Quá trình này cứ thế tiếp diễn với các byte giữa và cao, rồi chuyển sang số tiếp theo.

Khi thiết kế Bộ Tích Lũy Ba-Byte, tôi đã định nghĩa bốn mã lệnh sau:

  • 02h để cộng số 3-byte tiếp theo
  • 03h để trừ số 3-byte tiếp theo
  • 04h để lưu tổng cộng dồn 3-byte vào bộ nhớ
  • 08h để dừng cỗ máy lại

Chỉ 4 bit là cần để lưu vào chốt lệnh. Dưới đây là cách mà các bit đó tương ứng với bốn lệnh:

Dữ liệu ra từ RAM D Chốt Lệnh Q3 Q2 Q1 Q0 0 0 1 0 = Cộng số tiếp theo 0 0 1 1 = Trừ số tiếp theo 0 1 0 0 = Ghi tổng tích lũy vào bộ nhớ 1 0 0 0 = Dừng máy
4 lệnh


Mã lệnh vẫn còn trong chốt trong khi 3 byte tiếp theo đang nạp vào máy. Các bit trong mã lệnh được dùng để điều khiển các bộ phận khác của Bộ Tích Lũy Ba-Byte.

Đơn cử như, bit Q1 sẽ ở mức 1 nếu 3 byte tới bị lôi ra cộng hoặc trừ với tổng cộng dồn. Điều này đồng nghĩa với việc ta có thể tận dụng bit này để xác định các đầu vào Clock cho đám chốt chứa 3 byte dữ liệu. Dưới đây xin được trình ra lại bộ giải mã 2-sang-4 để biểu diễn cách mà các đầu ra đó cặp với bit lệnh Q1 và tín hiệu Pulse từ đám flip-flop:

A1 A0 Tín hiệu Pulse từ Flip-flop Đầu ra Q1 từ chốt lệnh Bộ giải mã 2-sang-4 Bit 1 Bit 0 11 10 01 00 Chốt Byte Thấp Cho Clock Chốt Byte Giữa Cho Clock Chốt Byte Cao Cho Clock
Bộ giải mã 2-sang-4


Như bạn thấy trước đó, khi 2 bit thấp hơn của địa chỉ là 00, thì byte lệnh sẽ bị chốt. Byte lệnh đó sẽ có thể được xài tới khi 2 bit địa chỉ này nhích lên 01, 10, và 11, tức là lúc 3 byte dữ liệu đang được truy cập. Nếu lệnh đó là Cộng hoặc Trừ, thì đầu ra Q1 của chốt lệnh sẽ ở mức 1, và ba cổng AND này sẽ nhả ra các tín hiệu Clock để chốt nối đuôi nhau 3 byte đó.

Khéo lại thấy hơi dị khi ngay từ đầu tôi lại định nghĩa lệnh Cộng là 02h và lệnh Trừ là 03h nhỉ. Mắc gì không phải là 01h và 02h? Hoặc 23h và 7Ch? Tôi tính cả rồi, để cho hai mã lệnh Cộng và Trừ có thể "dùng chung" một bit hòng thao túng các tín hiệu Clock cho đám chốt đấy.

Bit Q0 của lệnh này chỉ lên 1 nếu con số nằm trong 3 byte tiếp theo đang chờ bị trừ. Nếu đúng là rơi vào trường hợp đó, thì ta phải đi tìm số bù hai của số đó. Số bù hai được tính bằng cách trước tiên tìm ra số bù một rồi cộng thêm 1. Số bù một đơn giản chỉ cần lật toàn bộ các bit từ 0 thành 1 và 1 thành 0. Bạn đã từng thấy mạch chuyên làm chuyện này ở Chương 16 rồi đó:

Đảo Các đầu vào Các đầu ra
Bù 1


Các đầu vào đến từ Data Out của RAM. Còn các đầu ra thì vào đầu vào A của bộ cộng 8-bit. Tín hiệu Invert thì có thể đến từ đầu ra Q0 của chốt lệnh. Bit đó là kẻ chỉ ra số đang chuẩn bị trừ thay vì được cộng.

Số bù hai chính là số bù một thêm 1. Việc thêm con 1 đó có thể được thực hiện bằng cách đặt đầu vào Carry In của bộ cộng lên 1, nhưng chỉ dành riêng cho byte đi đầu trong bộ 3 byte thôi nhé:

A1 A0 Đầu ra Q0 từ Chốt Lệnh Dữ liệu ra từ RAM Bộ giải mã 2-sang-4 Bit 1 Bit 0 11 10 01 00 Bù 1 Đầu vào Đầu ra Đảo Bộ Cộng 8-Bit A B CI Tổng Tín hiệu Nhớ từ Bộ đệm 3-Trạng-Thái
Bù 2


Cứ hễ bit thấp nhất từ chốt lệnh lên 1, thì ắt hẳn một phép trừ khác đang được thi hành. Tất tần tật những byte dữ liệu kéo đến từ RAM đều phải bị đảo lộn. Đó chính là lý do tồn tại của hộp đeo biển "Ones' Comp." (Bù Một). Song song đó, đầu vào Carry In của bộ cộng cũng phải được kích lên 1, nhưng chỉ áp dụng cho byte đầu thôi. Đấy là mục đích của cổng AND. Đầu ra 01 của bộ giải mã sẽ chỉ lên 1 duy nhất cho byte dữ liệu đầu kia.

Sự góp mặt của cổng OR là bởi lẽ Carry In vào bộ cộng cũng có thể sẽ cần được đặt khi cộng hoặc trừ các byte dữ liệu thứ hai và thứ ba. Để giữ cho mấy sơ đồ đơn giản, nãy giờ tôi đã làm ngơ hoàn toàn cờ nhớ, nhưng giờ đã tới lúc chúng ta phải xốc lại tinh thần và đối mặt với nó.

Bộ Tích Lũy Ba-Byte chứa ba chốt và ba bộ đệm ba trạng thái để lưu và truy cập vào 3 byte của tổng cộng dồn:

Bộ Cộng A B CI CO Tổng Chốt Cao D Q Chốt Giữa D Q Chốt Thấp D Q 3-Trạng-Thái 3-Trạng-Thái 3-Trạng-Thái RAM DI
Bên trong Bộ tích luỹ 3-byte


Nhưng mà chưa chắc. Để dọn đường cho bit nhớ, hai trong số chốt này cần giữ tới 9 bit lận: 8 bit của tổng và Carry Out lòi ra từ bộ cộng. Hai trong số các bộ đệm ba trạng thái cũng phải xoay xở với 9 bit để cờ nhớ từ phép cộng byte-thấp có thể được xài tới trong phép cộng byte-giữa, và cờ nhớ từ phép cộng byte-giữa có thể được tái sử dụng khi cộng byte-cao:

Hình hơi rối chưa vẽ SVG được :))


Hãy nhìn kỹ tín hiệu Carry Out phóng ra từ bộ cộng bị nhốt gọn trong chốt byte-thấp và byte-giữa, thế nhưng những giá trị này thoắt cái lại lẻn vào các bộ đệm ba trạng thái byte-giữa và byte-cao, tôi biết nghe là rất kì cục. Lý giải đây:

Khi các byte thấp được cộng, thì đầu vào Carry In của bộ cộng sẽ là 0 cho phép cộng và 1 cho phép trừ. (Đó là cái đầu vào còn lại vào cổng OR đấy.) Phép cộng đó có thể sinh ra một cờ nhớ. Đầu ra Carry Out của bộ cộng sẽ được lưu vào chốt byte-thấp. Khốn nỗi, cái bit nhớ đó phải được đem ra xài khi cộng byte giữa. Đấy là nguyên do tại sao giá trị của bit nhớ trong chốt thấp lại thành một đầu vào khác vào bộ đệm ba trạng thái của byte giữa. Tương tự, một cờ nhớ từ phép cộng các byte giữa được lôi ra dùng khi cộng các byte cao.

Chúng ta tới chặng nước rút rồi đây. Toàn bộ mớ cộng và trừ giờ đã được xử lý ổn thỏa. Vẫn còn sót lại là mã lệnh 04h. Cái này dùng để viết 3 byte vào bộ nhớ. Mạch này dùng bit Q2 được lưu trong chốt lệnh:

A1 A0 Tín hiệu Pulse từ Flip-flop Đầu ra Q2 từ chốt lệnh Bộ giải mã 2-sang-4 Bit 1 Bit 0 11 10 01 00 Kích hoạt Byte Cao Kích hoạt Byte Giữa Kích hoạt Byte Thấp Ghi RAM
Mã lệnh 04h


Tín hiệu Write của RAM chỉ được sinh ra khi 2 bit thấp của địa chỉ RAM là 01, 10, hoặc 11, khớp với 3 byte dữ liệu. Đấy là sứ mệnh của inverter. Khi những bit địa chỉ đó là 01, 10, hoặc 11, ba bộ đệm ba trạng thái nối đuôi nhau sẽ được bật. Nếu bit Q2 là 1 (báo hiệu một lệnh Write) và tín hiệu Pulse từ cấu hình flip-flop là 1, thì 3 byte đó sẽ lần lượt được viết vào bộ nhớ.

Lệnh cuối là 08h, mang hàm ý Halt (Dừng lại). Này thì dễ. Khi bit Q3 từ chốt lệnh là 1, về cơ bản ta chỉ muốn bộ dao động nãy giờ chạy phải ngừng hoạt động:

Bộ dao động Bit Q3 từ chốt lệnh Tới cấu hình flip-flop
Lệnh 08h, Dừng


Sẽ tiện hơn nếu thêm một nút Clear, nút này sẽ dọn dẹp nội dung của các flip-flop, bộ đếm, và tất cả các chốt để chuẩn bị tích lũy một tổng cộng dồn khác.

Một phiên bản tương tác của Bộ Tích Lũy Ba-Byte hoàn chỉnh đang đợi bạn trên trang web CodeHiddenLanguage.com.

Đến giờ thì ắt hẳn đã sáng tỏ tại sao tôi lại "chế" ra bốn mã lệnh theo kiểu đó. Tôi muốn xài các bit trực tiếp bên trong mạch. Tôi muốn một bit biểu thị việc cộng hay trừ, một bit khác cho trừ, một bit khác nữa dùng để viết kết quả vào bộ nhớ, và bit chót để ra lệnh ngừng. Thử tưởng tượng nếu tôi cứ dùng các con số 01h, 02h, 03h, và 04h thì sao, ắt sẽ cần thêm nhiều mạch phụ để giải mã mấy giá trị đó thành những tín hiệu tách biệt.

Cũng có thể thấy rõ mười mươi tại sao tôi lại dùng 3 byte để cất giữ mỗi con số thay vì 4. Nước cờ này cho phép dùng các bit A0 và A1 của địa chỉ bộ nhớ để điều khiển đủ thể loại chốt theo một cách trực diện. Nếu tôi cố nhồi 4 byte cho mỗi con số, thì các mã lệnh sẽ bị phân tán ở địa chỉ bộ nhớ 0000h, 0005h, 000Ah, 000Fh, 0012h, điều đó chỉ tổ làm cho việc lưu vào đúng chốt của nó trở nên khó khăn hơn thôi.

Giờ tôi đã sẵn sàng để định nghĩa hai từ xuất hiện trong tựa đề cuốn sách này: phần cứngphần mềm. Bộ Tích Lũy Ba-Byte là minh hoạ rõ ràng sự khác biệt của chúng: Phần cứng là toàn bộ hệ thống mạch điện, trong khi phần mềm là tập hợp của mã lệnh và dữ liệu được lưu trong bộ nhớ. Chúng bị gán cho cái mác "mềm" vì chúng dễ dàng thay đổi. Lỡ như bạn có trượt tay gõ nhầm một con số, hay nhầm lẫn mã lệnh cộng với trừ, bạn hoàn toàn có thể dễ dàng sửa lại các giá trị đó. Trong khi, mạch điện lại khó thay đổi hơn nhiều. Nó vẫn đúng ngay cả khi mạch điện đó chỉ là mô phỏng, như trên trang CodeHiddenLanguage.com, chứ chưa hề chạm tới đống dây nhợ với transistor thật ngoài đời.

Tuy nhiên, Bộ Tích Lũy Ba-Byte cũng mô phỏng một mối quan hệ rất thân thiết giữa phần cứng và phần mềm. Các mã lệnh và con số lưu trong các flip-flop bên trong bộ nhớ, và chính những bit tạo ra giá trị này sẽ biến thành các tín hiệu tích hợp với phần còn lại của phần cứng. Xét ở cấp độ đơn giản nhất, phần cứng lẫn phần mềm cũng chỉ là các tín hiệu điện tương tác qua lại với cổng logic.

Nếu nhỡ đâu bạn muốn chế tạo Bộ Tích Lũy Ba-Byte của riêng mình và dùng nó để hạch toán tài chính cho doanh nghiệp nhỏ của bạn, bạn hoàn toàn có quyền lo âu nếu chuyện làm ăn bỗng dưng phát đạt vượt ngoài sức tưởng tượng. Rất có thể bạn sẽ nhận về một khoản thu hoặc chi nào đó phình to vượt quá sức chứa 3-byte của cỗ máy. Cái máy này không phải là loại dễ mở rộng. Rào cản 3-byte đã bị "đóng cọc" vào trong phần cứng mất rồi.

E là chúng ta đành phải coi Bộ Tích Lũy Ba-Byte như một ngõ cụt. May là, mọi công sức mà ta đã đúc kết trong quá trình lắp ráp nó sẽ chẳng hề phung phí. Ta thật sự đã khám phá ra một thứ rất quan trọng.

Ta đã giác ngộ ra rằng có thể lắp được một cỗ máy nghe lời các mã được lưu trong bộ nhớ. Bộ Tích Lũy Ba-Byte chỉ mới xài đúng bốn mã, nhưng nếu các mã lệnh được lưu dưới lốt byte, thì ta dư sức định nghĩa ra hẳn 256 mã khác nhau để làm đủ mọi thể loại công việc. Mớ 256 tác vụ khác biệt này thoạt nghe có vẻ đơn giản, nhưng lại có khả năng đa dụng đủ để kết hợp thành những nhiệm vụ phức tạp hơn.

Mấu chốt ở đây là những trò đơn giản hơn sẽ được phần cứng thầu, trong khi những việc phức tạp hơn lại được đẩy sang cho phần mềm giải quyết thông qua việc kết hợp nhiều mã lệnh.

Nếu lỡ đâu bạn đã quyết định chế tạo cỗ máy đa năng này vào thời điểm năm 1970, ắt hẳn bạn sẽ bị cả một núi việc đè bẹp dí. Nhưng chỉ ngót một thập kỷ sau, vào năm 1980, bạn sẽ chẳng cần phải lắp ráp nó nữa! Cứ móc hầu bao ra là mua được ngay một con chip gọi là bộ vi xử lý (microprocessor) có khả năng truy cập 64 KB bộ nhớ và dịch gần 256 mã lệnh khác nhau.

Cái "máy tính trên một con chip" đầu tiên lên kệ vào tháng 11 năm 1971. Intel đã tạo ra nó và đặt tên là 4004. Đây là một bộ xử lý 4-bit chứa tới 2.250 transistor và có thể truy cập 4KB bộ nhớ. Đến giữa năm 1972, Intel đã cho xuất xưởng bộ vi xử lý 8-bit đầu tay của họ, 8008, có thể truy xuất 16 KB bộ nhớ.

Mấy con chip này vẫn chưa đủ độ đa năng và dung lượng bộ nhớ để có thể trở thành những máy tính cá nhân đi kèm bàn phím và màn hình. Chúng chủ yếu được thiết kế cho các hệ thống nhúng, nơi chúng sẽ làm chung với các mạch logic kỹ thuật số khác, có lẽ là để điều khiển máy móc hay gánh vác các nhiệm vụ chuyên biệt.

Rồi vào tháng 4 năm 1974, Intel 8080 đã xuất hiện. Một bộ xử lý 8-bit với khoảng 4.500 transistor có thể truy xuất 64 KB bộ nhớ. Intel 8080 này được đóng gói trong một vỏ chip 40 chân:

ARCH: 8-BIT MICROPROCESSOR YEAR: 1974 SPEED: 2 MHz intel C8080A P8080A S5152 © 1974
Intel 8080

Thời của chip Intel 8080 cũng tới. Đây chính là bộ vi xử lý được dùng trong chiếc máy tính gia đình đầu tiên, Altair 8800, được lên trang bìa tạp chí Popular Electronics ở cuối Chương 19, và nó chính là cụ tổ của những bộ vi xử lý 16-bit nhà Intel được dùng trong chiếc máy tính cá nhân (PC) đầu tiên của IBM, ra đời vào tháng 8 năm 1981.

Trong khi đó, Motorola cũng sản xuất vi xử lý. Motorola 6800, cũng lên kệ vào năm 1974, là một bộ vi xử lý 8-bit khác với khả năng truy xuất 64 KB bộ nhớ. Một phiên bản nhỏ gọn của 6800 đã được xưởng MOS Technology tung ra vào năm 1975 và được gọi là MOS 6502. Đây chính là con chip mà Steve Wozniak (sinh năm 1950) dùng trong chiếc máy tính Apple II có sức ảnh hưởng lớn, chào sân vào tháng 6 năm 1977.

Mặc dù Intel 8080 và Motorola 6800 tương đồng nhau ở vài khía cạnh - chúng đều là vi xử lý 8-bit gói trong chip 40 chân và có thể truy cập 64 KB bộ nhớ—những mã lệnh mà chúng triển khai hoàn toàn khác nhau. Chúng còn khác nhau ở một điểm cốt lõi nữa: Lúc nãy tôi có nói về hai phái lưu trữ các số đa byte là big-endian và little-endian. Motorola 6800 theo phái big-endian, lưu các giá trị đa byte với byte quan trọng nhất lên đầu. Còn Intel 8080 lại là little-endian, ưu tiên byte ít quan trọng nhất trước.

Kể từ chương tiếp theo, tôi sẽ đưa bạn vào hẳn bên trong Intel 8080 bằng cách ráp thử một con. Tôi sẽ không dùng hàng nóng nào ngoại trừ mớ linh kiện mộc mạc mà bạn đã nhẵn mặt từ lâu, tỷ như cổng logic, flip-flop, bộ cộng, chốt, và cả bộ đệm ba trạng thái nữa.

Nói trước là tôi sẽ không tới bến với dự án tham vọng này đâu. Bản sao Intel 8080 của tôi chắc chắn sẽ không đủ trình bằng hàng "real". Nhưng tôi sẽ lết đủ xa để đến khi xong xuôi, bạn sẽ cắm rễ hiểu biết của mình vào sâu bên trong một chiếc máy tính.
Cảm ơn bạn đã đọc bài.
0

Open to Work

Looking for a Fullstack Ruby on Rails developer?

I am open to new remote/hybrid opportunities and contract work.

Thảo luận trên Bluesky

Đi

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

Powered by Bluesky AT Protocol