Xây dựng một trò chơi Tetris hoạt động trong Trò chơi cuộc sống của Conway


994

Đây là một câu hỏi lý thuyết - một câu hỏi không đủ khả năng trả lời dễ dàng trong mọi trường hợp, thậm chí không phải là câu hỏi tầm thường.

Trong Trò chơi Cuộc sống của Conway, tồn tại các cấu trúc như siêu hình cho phép Trò chơi Cuộc sống mô phỏng bất kỳ hệ thống quy tắc Trò chơi Cuộc sống nào khác. Ngoài ra, người ta biết rằng Trò chơi cuộc sống đã hoàn tất.

Nhiệm vụ của bạn là xây dựng một thiết bị tự động di động bằng cách sử dụng các quy tắc của trò chơi cuộc sống của Conway sẽ cho phép chơi một trò chơi Tetris.

Chương trình của bạn sẽ nhận đầu vào bằng cách thay đổi thủ công trạng thái của máy tự động ở một thế hệ cụ thể để biểu diễn một ngắt (ví dụ: di chuyển một mảnh sang trái hoặc phải, thả nó, xoay nó hoặc tạo ngẫu nhiên một mảnh mới để đặt vào lưới), đếm một số thế hệ cụ thể như thời gian chờ đợi và hiển thị kết quả ở đâu đó trên máy tự động. Kết quả được hiển thị phải rõ ràng giống với lưới Tetris thực tế.

Chương trình của bạn sẽ được ghi vào những điều sau đây, theo thứ tự (với tiêu chí thấp hơn đóng vai trò là người phá vỡ cho tiêu chí cao hơn):

  • Kích thước hộp giới hạn - hộp hình chữ nhật có diện tích nhỏ nhất chứa hoàn toàn giải pháp đã cho sẽ thắng.

  • Những thay đổi nhỏ hơn đối với đầu vào - ít ô nhất (đối với trường hợp xấu nhất trong máy tự động của bạn) cần được điều chỉnh thủ công để giành chiến thắng ngắt.

  • Thực hiện nhanh nhất - ít thế hệ nhất tiến lên một tích tắc trong chiến thắng mô phỏng.

  • Số lượng tế bào sống ban đầu - số lượng nhỏ hơn chiến thắng.

  • Đầu tiên để đăng bài - trước đó bài thắng.


95
"Ví dụ hoạt động mạnh mẽ" có nghĩa là một cái gì đó chạy trong vài giờ, hoặc một cái gì đó có thể được chứng minh là chính xác mặc dù nó sẽ mất cho đến khi cái chết nóng của vũ trụ để chơi?
Peter Taylor

34
Tôi khá chắc chắn một cái gì đó như thế này là có thể và có thể chơi được. Chỉ là rất ít người có chuyên môn để có thể lập trình những gì có lẽ là một trong những "ngôn ngữ lắp ráp" bí truyền hơn trên thế giới.
Justin L.

58
Thử thách này đang được thực hiện! Phòng chat | Tiến độ | Blog
mbomb007

49
Tính đến 5:10 sáng nay (9:10 UTC), câu hỏi này là câu hỏi đầu tiên trong lịch sử PPCG đạt 100 phiếu mà không nhận được câu trả lời! Làm tốt lắm mọi người.
Joe Z.

76
Tôi đang cố gắng giải quyết điều này ... Bây giờ, khi tôi đi ngủ, tôi thấy tàu lượn ở khắp mọi nơi, va chạm vào một mớ hỗn độn khổng lồ. Giấc ngủ của tôi đầy những cơn ác mộng, nơi những khối ngũ giác xung quanh chặn đường tôi và Herschels đang tiến hóa để hấp thụ tôi. Làm ơn, John Conway, cầu nguyện cho tôi ...
mờ

Câu trả lời:


938

Điều này bắt đầu như một nhiệm vụ nhưng kết thúc như một cuộc phiêu lưu.

Nhiệm vụ cho Bộ xử lý Tetris, 2.940.928 x 10.295.296

Các tập tin mẫu, trong tất cả vinh quang của nó, có thể được tìm thấy ở đây , có thể xem trong trình duyệt ở đây .

Dự án này là đỉnh cao của những nỗ lực của nhiều người dùng trong suốt 1 & 1/2 năm qua. Mặc dù thành phần của nhóm đã thay đổi theo thời gian, những người tham gia khi viết như sau:

Chúng tôi cũng muốn gửi lời cảm ơn tới 7H3_H4CK3R, Conor O'Brien và nhiều người dùng khác đã nỗ lực giải quyết thử thách này.

Do phạm vi chưa từng có của sự hợp tác này, câu trả lời này được chia thành nhiều phần trong nhiều câu trả lời được viết bởi các thành viên của nhóm này. Mỗi thành viên sẽ viết về các chủ đề phụ cụ thể, gần tương ứng với các lĩnh vực của dự án mà họ tham gia nhiều nhất.

Vui lòng phân phối bất kỳ upvote hoặc tiền thưởng trên tất cả các thành viên của nhóm.

Mục lục

  1. Tổng quan
  2. Metapixels và VarLife
  3. Phần cứng
  4. QFTASM và Cogol
  5. Hội, Dịch, và Tương lai
  6. Trình biên dịch và ngôn ngữ mới

Ngoài ra, hãy xem xét việc kiểm tra tổ chức GitHub của chúng tôi , nơi chúng tôi đã đặt tất cả các mã chúng tôi đã viết như một phần của giải pháp. Câu hỏi có thể được chuyển đến phòng chat phát triển của chúng tôi .


Phần 1: Tổng quan

Ý tưởng cơ bản của dự án này là trừu tượng . Thay vì trực tiếp phát triển một trò chơi Tetris trong Cuộc sống, chúng tôi dần dần bắt kịp sự trừu tượng trong một loạt các bước. Ở mỗi lớp, chúng ta càng rời xa những khó khăn của Cuộc sống và tiến gần hơn đến việc xây dựng một chiếc máy tính dễ lập trình như bất kỳ loại nào khác.

Đầu tiên, chúng tôi sử dụng các siêu dữ liệu OTCA làm nền tảng cho máy tính của chúng tôi. Những siêu hình này có khả năng mô phỏng bất kỳ quy tắc "giống như cuộc sống" nào. Wireworldmáy tính Wireworld đóng vai trò là nguồn cảm hứng quan trọng cho dự án này, vì vậy chúng tôi đã tìm cách tạo ra một cấu trúc tương tự với metapixels. Mặc dù không thể mô phỏng Wireworld bằng các siêu dữ liệu OTCA, nhưng có thể gán các siêu dữ liệu khác nhau cho các quy tắc khác nhau và xây dựng các sắp xếp metapixel có chức năng tương tự như dây.

Bước tiếp theo là xây dựng một loạt các cổng logic cơ bản để làm cơ sở cho máy tính. Ở giai đoạn này, chúng tôi đang xử lý các khái niệm tương tự như thiết kế bộ xử lý trong thế giới thực. Dưới đây là một ví dụ về cổng OR, mỗi ô trong hình ảnh này thực sự là toàn bộ siêu dữ liệu OTCA. Bạn có thể thấy "electron" (mỗi electron đại diện cho một bit dữ liệu) nhập và rời khỏi cổng. Bạn cũng có thể thấy tất cả các loại metapixel khác nhau mà chúng tôi đã sử dụng trong máy tính của mình: B / S làm nền đen, B1 / S màu xanh lam, B2 / S màu xanh lá cây và B12 / S1 màu đỏ.

Hình ảnh, tưởng tượng

Từ đây, chúng tôi đã phát triển một kiến ​​trúc cho bộ xử lý của chúng tôi. Chúng tôi đã dành nỗ lực đáng kể để thiết kế một kiến ​​trúc vừa phi bí truyền vừa dễ thực hiện nhất có thể. Trong khi máy tính Wireworld sử dụng kiến ​​trúc kích hoạt giao thông thô sơ, dự án này sử dụng kiến ​​trúc RISC linh hoạt hơn nhiều với nhiều opcode và chế độ địa chỉ. Chúng tôi đã tạo ra một ngôn ngữ lắp ráp, được gọi là QFTASM (Quest for Tetris hội), hướng dẫn việc xây dựng bộ xử lý của chúng tôi.

Máy tính của chúng tôi cũng không đồng bộ, có nghĩa là không có đồng hồ toàn cầu điều khiển máy tính. Thay vào đó, dữ liệu được kèm theo tín hiệu đồng hồ khi nó chảy xung quanh máy tính, điều đó có nghĩa là chúng ta chỉ cần tập trung vào thời gian cục bộ chứ không phải định thời toàn cầu của máy tính.

Dưới đây là một minh họa về kiến ​​trúc bộ xử lý của chúng tôi:

Hình ảnh, tưởng tượng

Từ đây chỉ là vấn đề thực hiện Tetris trên máy tính. Để giúp thực hiện điều này, chúng tôi đã nghiên cứu nhiều phương pháp biên dịch ngôn ngữ cấp cao hơn thành QFTASM. Chúng tôi có một ngôn ngữ cơ bản gọi là Cogol, ngôn ngữ thứ hai, tiên tiến hơn đang được phát triển và cuối cùng chúng tôi có một phụ trợ GCC đang được xây dựng. Chương trình Tetris hiện tại được viết / biên soạn từ Cogol.

Khi mã TetFT QFTASM cuối cùng được tạo, các bước cuối cùng là lắp ráp từ mã này sang ROM tương ứng, sau đó từ siêu dữ liệu đến Trò chơi cuộc sống cơ bản, hoàn thành việc xây dựng của chúng tôi.

Chạy Tetris

Đối với những người muốn chơi Tetris mà không làm phiền máy tính, bạn có thể chạy mã nguồn Tetris trên trình thông dịch QFTASM . Đặt địa chỉ hiển thị RAM thành 3-32 để xem toàn bộ trò chơi. Đây là một permalink cho thuận tiện: Tetris trong QFTASM .

Tính năng trò chơi:

  • Tất cả 7 tetrominoes
  • Chuyển động, xoay, giọt mềm
  • Xóa dòng và ghi bàn
  • Xem trước mảnh
  • Đầu vào người chơi tiêm ngẫu nhiên

Trưng bày

Máy tính của chúng tôi đại diện cho bảng Tetris như một lưới trong bộ nhớ của nó. Địa chỉ 10-31 hiển thị bảng, địa chỉ 5-8 hiển thị phần xem trước và địa chỉ 3 chứa điểm.

Đầu vào

Việc nhập vào trò chơi được thực hiện bằng cách chỉnh sửa thủ công nội dung của địa chỉ RAM 1. Sử dụng trình thông dịch QFTASM, điều này có nghĩa là thực hiện ghi trực tiếp vào địa chỉ 1. Tìm "Ghi trực tiếp vào RAM" trên trang của trình thông dịch. Mỗi lần di chuyển chỉ yêu cầu chỉnh sửa một bit RAM và thanh ghi đầu vào này sẽ tự động bị xóa sau khi sự kiện đầu vào được đọc.

value     motion
   1      counterclockwise rotation
   2      left
   4      down (soft drop)
   8      right
  16      clockwise rotation

Hệ thống chấm điểm

Bạn nhận được một phần thưởng cho việc xóa nhiều dòng trong một lượt.

1 row    =  1 point
2 rows   =  2 points
3 rows   =  4 points
4 rows   =  8 points

14
@ Christopher2EZ4RTZ Bài viết tổng quan này chi tiết công việc được thực hiện bởi nhiều thành viên dự án (bao gồm cả bài viết thực tế của bài tổng quan). Như vậy, nó thích hợp cho nó là CW. Chúng tôi cũng đã cố gắng tránh một người có hai bài đăng, vì điều đó sẽ khiến họ nhận được một lượng đại diện không công bằng, vì chúng tôi đang cố gắng giữ lại đại diện.
Mego

28
Trước hết là +1, vì đây là một thành tựu cực kỳ tuyệt vời (đặc biệt là khi bạn xây dựng một máy tính trong trò chơi cuộc sống, thay vì chỉ là tetris). Thứ hai, máy tính nhanh như thế nào và trò chơi tetris nhanh như thế nào? Nó thậm chí có thể chơi từ xa? (một lần nữa: điều này thật tuyệt vời)
Socratic Phoenix

18
Đây ... điều này là hoàn toàn điên rồ. +1 cho tất cả các câu trả lời ngay lập tức.
scottinet

28
Một cảnh báo cho bất kỳ ai muốn phân phối tiền thưởng nhỏ qua các câu trả lời: bạn phải nhân đôi số tiền thưởng của mình mỗi lần (cho đến khi bạn đạt 500), vì vậy một người không thể đưa ra số tiền tương tự cho mỗi câu trả lời trừ khi số tiền đó là 500 rep.
Martin Ender

23
Đây là điều tuyệt vời nhất tôi từng cuộn qua trong khi hiểu rất ít.
Kỹ sư Toast

678

Phần 2: Metapixel OTCA và VarLife

Metapixel OTCA

Siêu dữ liệu OTCA
( Nguồn )

Các OTCA Metapixel là một cấu trúc trong Game of Life của Conway có thể được sử dụng để mô phỏng bất kỳ cellular automata Cuộc sống giống như. Như LifeWiki (được liên kết ở trên) nói,

Siêu dữ liệu OTCA là một tế bào đơn vị 35328 thời kỳ 2048 × 2048 được xây dựng bởi Brice Do ... Nó có nhiều lợi thế ... bao gồm khả năng mô phỏng bất kỳ thiết bị tự động di động giống như Cuộc sống và thực tế là, khi thu nhỏ, BẬT và các ô TẮT rất dễ phân biệt ...

Những gì automata giống như tế bào có nghĩa là ở đây về cơ bản là các tế bào được sinh ra và các tế bào tồn tại theo số lượng tám tế bào lân cận của chúng còn sống. Cú pháp của các quy tắc này như sau: a B theo sau là số hàng xóm sống sẽ sinh ra, sau đó là dấu gạch chéo, sau đó là S theo sau là số hàng xóm sống sẽ giữ cho tế bào sống. Một chút dài dòng, vì vậy tôi nghĩ rằng một ví dụ sẽ giúp. Trò chơi Cuộc sống kinh điển có thể được đại diện bởi quy tắc B3 / S23, nói rằng bất kỳ tế bào chết nào có ba người hàng xóm còn sống sẽ trở nên sống động và bất kỳ tế bào sống nào có hai hoặc ba người hàng xóm còn sống sẽ vẫn còn sống. Nếu không, tế bào chết.

Mặc dù là một ô 2048 x 2048, nhưng siêu dữ liệu OTCA thực sự có một hộp giới hạn 2058 x 2058, lý do là nó chồng lên nhau bởi năm ô theo mọi hướng với các hàng xóm chéo của nó . Các ô chồng lấp dùng để chặn các tàu lượn - được phát ra để báo hiệu cho các hàng xóm siêu dữ liệu đang bật - để chúng không can thiệp vào các siêu dữ liệu khác hoặc bay đi vô thời hạn. Các quy tắc sinh và tồn tại được mã hóa trong một phần đặc biệt của các tế bào ở phía bên trái của metapixel, bởi sự hiện diện hoặc vắng mặt của người ăn ở các vị trí cụ thể dọc theo hai cột (một cho sinh, một cho sinh tồn). Đối với việc phát hiện trạng thái của các ô lân cận, đây là cách điều đó xảy ra:

Luồng 9-LWSS sau đó đi theo chiều kim đồng hồ xung quanh ô, làm mất một LWSS cho mỗi ô 'trên' liền kề gây ra phản ứng honeybit. Số lượng các LWSS bị thiếu được tính bằng cách phát hiện vị trí của các LWSS phía trước bằng cách đâm một LWSS khác vào nó từ hướng ngược lại. Sự va chạm này giải phóng tàu lượn, gây ra một hoặc hai phản ứng honeybit khác nếu những người ăn chỉ ra rằng tình trạng sinh / sống không có.

Một sơ đồ chi tiết hơn về từng khía cạnh của siêu dữ liệu OTCA có thể được tìm thấy tại trang web ban đầu của nó: Nó hoạt động như thế nào? .

VarLife

Tôi đã xây dựng một trình mô phỏng trực tuyến các quy tắc giống như Cuộc sống nơi bạn có thể khiến bất kỳ tế bào nào hoạt động theo bất kỳ quy tắc nào giống như cuộc sống và gọi đó là "Biến thể của cuộc sống". Tên này đã được rút ngắn thành "VarLife" để ngắn gọn hơn. Đây là một ảnh chụp màn hình của nó (liên kết đến đây: http://play.starmaninnovations.com/varlife/BeeHkfCpNR ):

Ảnh chụp màn hình VarLife

Các tính năng đáng chú ý:

  • Chuyển đổi các ô giữa sống / chết và vẽ bảng với các quy tắc khác nhau.
  • Khả năng bắt đầu và dừng mô phỏng, và thực hiện từng bước một. Bạn cũng có thể thực hiện một số bước nhất định nhanh nhất có thể hoặc chậm hơn, với tốc độ được đặt trong các hộp tick-per-second và mili giây-per-tick.
  • Xóa tất cả các ô sống hoặc để hoàn toàn đặt lại bảng về trạng thái trống.
  • Có thể thay đổi kích thước ô và bảng, và cũng để cho phép gói hình xuyến theo chiều ngang và / hoặc theo chiều dọc.
  • Permalinks (mã hóa tất cả thông tin trong url) và các url ngắn (vì đôi khi chỉ có quá nhiều thông tin, nhưng dù sao chúng cũng rất hay).
  • Bộ quy tắc, với đặc điểm kỹ thuật B / S, màu sắc và tính ngẫu nhiên tùy chọn.
  • Và cuối cùng nhưng không kém phần quan trọng, kết xuất gifs!

Tính năng kết xuất gif là yêu thích của cả hai vì tôi phải mất rất nhiều công việc để thực hiện, vì vậy tôi thực sự rất hài lòng khi cuối cùng tôi đã bẻ khóa nó vào lúc 7 giờ sáng và vì nó rất dễ chia sẻ các cấu trúc VarLife với người khác .

Mạch VarLife cơ bản

Nói chung, máy tính VarLife chỉ cần bốn loại tế bào! Tám tiểu bang trong tất cả các trạng thái chết / sống. Họ đang:

  • B / S (đen / trắng), đóng vai trò là bộ đệm giữa tất cả các thành phần vì các tế bào B / S không bao giờ có thể tồn tại.
  • B1 / S (xanh dương / lục lam), là loại tế bào chính được sử dụng để truyền tín hiệu.
  • B2 / S (xanh / vàng), chủ yếu được sử dụng để điều khiển tín hiệu, đảm bảo nó không bị backpropagate.
  • B12 / S1 (đỏ / cam), được sử dụng trong một vài tình huống chuyên biệt, chẳng hạn như tín hiệu chéo và lưu trữ một chút dữ liệu.

Sử dụng url ngắn này để mở VarLife với các quy tắc đã được mã hóa: http://play.starmaninnovations.com/varlife/BeeHkfCpNR .

Dây điện

Có một vài thiết kế dây khác nhau với các đặc điểm khác nhau.

Đây là dây dễ nhất và cơ bản nhất trong VarLife, một dải màu xanh được viền bởi các dải màu xanh lá cây.

dây cơ bản
Url ngắn: http://play.starmaninnovations.com/varlife/WcsGmjLiBF

Dây này là đơn hướng. Đó là, nó sẽ giết bất kỳ tín hiệu nào cố gắng đi theo hướng ngược lại. Nó cũng hẹp hơn một ô so với dây cơ bản.

dây đơn hướng
Url ngắn: http://play.starmaninnovations.com/varlife/ARWgUgPTEJ

Dây chéo cũng tồn tại nhưng chúng không được sử dụng nhiều.

dây chéo
Url ngắn: http://play.starmaninnovations.com/varlife/kJotsdSXIj

Cổng

Thực tế có rất nhiều cách để xây dựng mỗi cổng riêng lẻ, vì vậy tôi sẽ chỉ hiển thị một ví dụ về mỗi loại. Gif đầu tiên này thể hiện các cổng AND, XOR và OR tương ứng. Ý tưởng cơ bản ở đây là một ô màu xanh lá cây hoạt động như một AND, một ô màu xanh hoạt động như một XOR và một ô màu đỏ hoạt động như một OR và tất cả các ô khác xung quanh chúng đều ở đó để điều khiển dòng chảy chính xác.

Cổng logic AND, XOR, OR
Url ngắn: http://play.starmaninnovations.com/varlife/EGTlKktmeI

Cổng AND-NOT, viết tắt là "Cổng ANT", hóa ra là một thành phần quan trọng. Đó là một cổng truyền tín hiệu từ A khi và chỉ khi không có tín hiệu từ B. Do đó, "A VÀ KHÔNG B".

Cổng VÀ KHÔNG
Url ngắn: http://play.starmaninnovations.com/varlife/RsZBiNqIUy

Mặc dù không chính xác là một cổng , một gạch chéo dây vẫn rất quan trọng và hữu ích để có.

qua dây
Url ngắn: http://play.starmaninnovations.com/varlife/OXMsPyaNTC

Ngẫu nhiên, không có cổng KHÔNG ở đây. Đó là bởi vì không có tín hiệu đến, phải tạo ra đầu ra không đổi, hoạt động không tốt với sự đa dạng về thời gian mà phần cứng máy tính hiện tại yêu cầu. Chúng tôi vẫn hợp nhau mà không có nó.

Ngoài ra, nhiều thành phần được thiết kế có chủ ý để nằm gọn trong hộp giới hạn 11 x 11 (một ô ) trong đó nó nhận tín hiệu 11 tích tắc khi đi vào ô để rời khỏi ô. Điều này làm cho các thành phần trở nên mô đun hơn và dễ dàng hơn để vỗ vào nhau khi cần mà không phải lo lắng về việc điều chỉnh dây cho khoảng cách hoặc thời gian.

Để xem thêm các cổng được phát hiện / xây dựng trong quá trình khám phá các thành phần mạch điện, hãy xem bài đăng trên blog này của PhiNotPi: Building Blocks: Logic Gates .

Thành phần trễ

Trong quá trình thiết kế phần cứng của máy tính, KZhang đã nghĩ ra nhiều thành phần trễ, được hiển thị bên dưới.

Độ trễ 4 đánh dấu: url ngắn: http://play.starmaninnovations.com/varlife/oltOMIXxdh
Độ trễ 4 đánh dấu

Độ trễ 5 đánh dấu: url ngắn: http://play.starmaninnovations.com/varlife/JItNjJvnUB
Trì hoãn 5 đánh dấu

Độ trễ 8 đánh dấu (ba điểm nhập khác nhau): Url ngắn: http://play.starmaninnovations.com/varlife/nSTRaVEDvA
Độ trễ 8 đánh dấu

Độ trễ 11-tick: url ngắn: http://play.starmaninnovations.com/varlife/kfoADussXA
Độ trễ 11 đánh dấu

Độ trễ 12 đánh dấu: Url ngắn: http://play.starmaninnovations.com/varlife/bkamAfUfud
Độ trễ 12 đánh dấu

Độ trễ 14 đánh dấu: Url ngắn: http://play.starmaninnovations.com/varlife/TkwzYIBWln
Độ trễ 14 đánh dấu

Độ trễ 15 đánh dấu (được xác minh bằng cách so sánh với điều này ): Url ngắn: http://play.starmaninnovations.com/varlife/jmgpehYlpT
Độ trễ 15 đánh dấu

Chà, đó là cho các thành phần mạch cơ bản trong VarLife! Xem bài phần cứng của KZhang cho các mạch chính của máy tính!


4
VarLife là một trong những phần ấn tượng nhất của dự án này; đó là tính linh hoạt và đơn giản so với, ví dụ, Wireworld là hiện tượng. OTCA Metapixel dường như lớn hơn rất nhiều so với mức cần thiết, liệu đã có bất kỳ nỗ lực nào để đánh bại nó chưa?
primo

@primo: Có vẻ như Dave Greene đang làm việc đó. chat.stackexchange.com/transcript/message/40106098#40106098
El'endia Starman

6
Vâng, đã đạt được một số tiến bộ đáng kể vào cuối tuần này trên trung tâm của metacell 512x512 thân thiện với HashLife ( conwaylife.com/forums/viewtopic.php?f=&p=51287#p51287 ). Metacell có thể được làm nhỏ hơn một chút, tùy thuộc vào diện tích "pixel" muốn báo hiệu trạng thái của ô khi bạn phóng to ra như thế nào. Tuy nhiên, nó chắc chắn có giá trị dừng lại ở một lát chính xác có kích thước 2 ^ N, bởi vì thuật toán HashLife của Golly sẽ có thể chạy máy tính nhanh hơn rất nhiều.
Dave Greene

2
Không thể thực hiện dây và cổng theo cách ít "lãng phí" hơn? Một electron sẽ được đại diện bởi tàu lượn hoặc tàu vũ trụ (tùy theo hướng). Tôi đã thấy các sắp xếp chuyển hướng chúng (và thay đổi từ cái này sang cái khác nếu cần thiết) và một số cổng làm việc với tàu lượn. Vâng, họ mất nhiều không gian hơn, thiết kế phức tạp hơn và thời gian cần phải chính xác. Nhưng một khi bạn có các khối xây dựng cơ bản đó, chúng sẽ đủ dễ dàng để kết hợp với nhau và chúng sẽ tốn ít không gian hơn so với VarLife triển khai bằng OTCA. Nó cũng sẽ chạy nhanh hơn.
Heimdall

@Heimdall Mặc dù điều đó sẽ hoạt động tốt, nhưng nó sẽ không hiển thị tốt khi chơi Tetris.
MilkyWay90

649

Phần 3: Phần cứng

Với kiến ​​thức về cổng logic và cấu trúc chung của bộ xử lý, chúng ta có thể bắt đầu thiết kế tất cả các thành phần của máy tính.

Máy khử trùng

Một demultiplexer, hoặc demux, là một thành phần quan trọng đối với ROM, RAM và ALU. Nó định tuyến tín hiệu đầu vào đến một trong nhiều tín hiệu đầu ra dựa trên một số dữ liệu chọn đã cho. Nó bao gồm 3 phần chính: bộ chuyển đổi nối tiếp sang song song, bộ kiểm tra tín hiệu và bộ chia tín hiệu đồng hồ.

Chúng tôi bắt đầu bằng cách chuyển đổi dữ liệu bộ chọn nối tiếp thành "song song". Điều này được thực hiện bằng cách phân chia chiến lược và trì hoãn dữ liệu sao cho bit dữ liệu ngoài cùng bên trái giao với tín hiệu đồng hồ ở ô vuông 11x11 ngoài cùng bên trái, bit dữ liệu tiếp theo giao với tín hiệu đồng hồ ở ô vuông 11x11 tiếp theo, v.v. Mặc dù mỗi bit dữ liệu sẽ được xuất ra trong mỗi ô vuông 11x11, nhưng mỗi bit dữ liệu sẽ giao nhau với tín hiệu đồng hồ chỉ một lần.

Bộ chuyển đổi nối tiếp sang song song

Tiếp theo, chúng tôi sẽ kiểm tra xem dữ liệu song song có khớp với địa chỉ đặt trước không. Chúng tôi làm điều này bằng cách sử dụng cổng AND và ANT trên đồng hồ và dữ liệu song song. Tuy nhiên, chúng ta cần đảm bảo rằng dữ liệu song song cũng được xuất ra để có thể so sánh lại. Đây là những cánh cổng mà tôi nghĩ ra:

Cổng kiểm tra tín hiệu

Cuối cùng, chúng tôi chỉ cần tách tín hiệu đồng hồ, xếp chồng một loạt các bộ kiểm tra tín hiệu (một cho mỗi địa chỉ / đầu ra) và chúng tôi có một bộ ghép kênh!

Bộ ghép kênh

ROM

ROM được coi là lấy một địa chỉ làm đầu vào và gửi hướng dẫn tại địa chỉ đó làm đầu ra. Chúng tôi bắt đầu bằng cách sử dụng bộ ghép kênh để hướng tín hiệu đồng hồ theo một trong các hướng dẫn. Tiếp theo, chúng ta cần tạo tín hiệu bằng cách sử dụng một số giao cắt dây và cổng OR. Các giao thoa dây cho phép tín hiệu đồng hồ truyền xuống tất cả 58 bit của lệnh và cũng cho phép tín hiệu được tạo (hiện đang song song) di chuyển xuống qua ROM để được xuất ra.

Bit ROM

Tiếp theo chúng ta chỉ cần chuyển đổi tín hiệu song song thành dữ liệu nối tiếp và ROM đã hoàn tất.

Song song với bộ chuyển đổi nối tiếp

ROM

ROM hiện được tạo bằng cách chạy tập lệnh trong Golly sẽ dịch mã lắp ráp từ khay nhớ tạm của bạn sang ROM.

SRL, SL, SRA

Ba cổng logic này được sử dụng cho dịch chuyển bit và chúng phức tạp hơn so với AND, OR, XOR thông thường của bạn, v.v. Để làm cho các cổng này hoạt động, trước tiên chúng ta sẽ trì hoãn tín hiệu đồng hồ một lượng thời gian thích hợp để gây ra "dịch chuyển" trong dữ liệu. Đối số thứ hai được đưa ra cho các cổng này cho biết có bao nhiêu bit để dịch chuyển.

Đối với SL và SRL, chúng ta cần phải

  1. Đảm bảo rằng 12 bit quan trọng nhất không được bật (nếu không thì đầu ra chỉ đơn giản là 0) và
  2. Trì hoãn dữ liệu đúng số lượng dựa trên 4 bit có trọng số thấp nhất.

Điều này có thể thực hiện được với một loạt các cổng AND / ANT và bộ ghép kênh.

SRL

SRA hơi khác một chút, vì chúng ta cần sao chép bit dấu trong ca làm việc. Chúng tôi thực hiện điều này bằng cách ANDing tín hiệu đồng hồ với bit dấu, và sau đó sao chép đầu ra đó một loạt các lần với bộ chia dây và cổng OR.

SRA

Chốt cài đặt lại (SR)

Nhiều phần chức năng của bộ xử lý phụ thuộc vào khả năng lưu trữ dữ liệu. Sử dụng 2 ô B12 / S1 màu đỏ, chúng ta có thể làm điều đó. Hai tế bào có thể giữ nhau và cũng có thể ở cùng nhau. Sử dụng một số bộ bổ sung, thiết lập lại và đọc mạch, chúng ta có thể tạo ra một chốt SR đơn giản.

Chốt SR

Đồng bộ hóa

Bằng cách chuyển đổi dữ liệu nối tiếp thành dữ liệu song song, sau đó thiết lập một loạt các chốt SR, chúng ta có thể lưu trữ toàn bộ từ dữ liệu. Sau đó, để lấy lại dữ liệu, chúng ta chỉ cần đọc và đặt lại tất cả các chốt và trì hoãn dữ liệu tương ứng. Điều này cho phép chúng tôi lưu trữ một (hoặc nhiều) từ dữ liệu trong khi chờ người khác, cho phép hai từ dữ liệu đến vào các thời điểm khác nhau được đồng bộ hóa.

Đồng bộ hóa

Đọc quầy

Thiết bị này theo dõi số lần cần thiết để giải quyết từ RAM. Nó thực hiện điều này bằng cách sử dụng một thiết bị tương tự như chốt SR: một lật lật T. Mỗi lần lật T nhận được một đầu vào, nó sẽ thay đổi trạng thái: nếu bật, nó sẽ tắt và ngược lại. Khi lật T lật được lật từ trên xuống, nó sẽ gửi một xung đầu ra, có thể được đưa vào một lật lật T khác để tạo thành bộ đếm 2 bit.

Bộ đếm hai bit

Để tạo Bộ đếm Đọc, chúng ta cần đặt bộ đếm thành chế độ địa chỉ phù hợp với hai cổng ANT và sử dụng tín hiệu đầu ra của bộ đếm để quyết định nơi gửi tín hiệu đồng hồ: đến ALU hoặc RAM.

Đọc quầy

Đọc hàng đợi

Hàng đợi đọc cần theo dõi bộ đếm đọc nào đã gửi đầu vào tới RAM, để nó có thể gửi đầu ra của RAM đến đúng vị trí. Để làm điều đó, chúng tôi sử dụng một số chốt SR: một chốt cho mỗi đầu vào. Khi tín hiệu được gửi đến RAM từ bộ đếm đọc, tín hiệu đồng hồ sẽ được phân tách và đặt chốt SR của bộ đếm. Đầu ra của RAM sau đó được ANDed với chốt SR và tín hiệu đồng hồ từ RAM đặt lại chốt SR.

Đọc hàng đợi

ALU

Các chức năng ALU tương tự như hàng đợi đọc, trong đó nó sử dụng chốt SR để theo dõi nơi gửi tín hiệu. Đầu tiên, chốt SR của mạch logic tương ứng với opcode của lệnh được đặt bằng bộ ghép kênh. Tiếp theo, các giá trị của đối số thứ nhất và thứ hai được ANDed với chốt SR và sau đó được chuyển đến các mạch logic. Tín hiệu đồng hồ đặt lại chốt khi nó đi qua để ALU có thể được sử dụng lại. (Hầu hết các mạch được đánh xuống và một tấn quản lý độ trễ được đưa vào, vì vậy có vẻ như một chút lộn xộn)

ALU

RAM

RAM là phần phức tạp nhất của dự án này. Nó đòi hỏi phải kiểm soát rất cụ thể đối với từng chốt SR lưu trữ dữ liệu. Để đọc, địa chỉ được gửi vào bộ ghép kênh và gửi đến các đơn vị RAM. Các đơn vị RAM xuất dữ liệu mà chúng lưu trữ song song, được chuyển đổi thành nối tiếp và xuất ra. Để ghi, địa chỉ được gửi vào một bộ ghép kênh khác, dữ liệu được ghi được chuyển đổi từ nối tiếp sang song song và các đơn vị RAM truyền tín hiệu trong toàn bộ RAM.

Mỗi đơn vị RAM metapixel 22x22 có cấu trúc cơ bản này:

Đơn vị RAM

Đặt toàn bộ RAM lại với nhau, chúng ta sẽ có được thứ gì đó trông như thế này:

RAM

Đặt mọi thứ lại với nhau

Sử dụng tất cả các thành phần này và kiến ​​trúc máy tính nói chung được mô tả trong Tổng quan , chúng tôi có thể xây dựng một máy tính hoạt động!

Tải xuống: - Máy tính Tetris đã hoàn thành - Tập lệnh tạo ROM, máy tính trống và máy tính tìm kiếm chính

Máy tính


49
Tôi chỉ muốn nói rằng những hình ảnh trong bài viết này, vì lý do gì, rất đẹp theo quan điểm của tôi. : P +1
HyperNeutrino

7
Đây là điều tuyệt vời nhất tôi từng thấy .... Tôi sẽ +20 nếu có thể
FantaC

3
@tfbninja Bạn có thể, đó được gọi là tiền thưởng và bạn có thể cho 200 danh tiếng.
Fabian Röling

10
Bộ xử lý này có dễ bị tấn công bởi Spectre và Meltdown không? :)
Ferrybig

5
@Ferrybig không có dự đoán chi nhánh, vì vậy tôi nghi ngờ nó.
JAD

621

Phần 4: QFTASM và Cogol

Tổng quan kiến ​​trúc

Nói tóm lại, máy tính của chúng tôi có kiến ​​trúc RISC Harvard không đồng bộ 16 bit. Khi xây dựng bộ xử lý bằng tay, kiến trúc RISC ( máy tính tập lệnh giảm ) thực tế là một yêu cầu. Trong trường hợp của chúng tôi, điều này có nghĩa là số lượng opcodes là nhỏ và quan trọng hơn hết là tất cả các hướng dẫn đều được xử lý theo cách rất giống nhau.

Để tham khảo, máy tính Wireworld sử dụng kiến trúc kích hoạt vận chuyển , trong đó chỉ dẫn duy nhất là MOVvà tính toán được thực hiện bằng cách viết / đọc các thanh ghi đặc biệt. Mặc dù mô hình này dẫn đến một kiến ​​trúc rất dễ thực hiện, kết quả cũng là đường biên không thể sử dụng: tất cả các phép toán số học / logic / điều kiện cần có ba hướng dẫn. Rõ ràng với chúng tôi rằng chúng tôi muốn tạo ra một kiến ​​trúc bí truyền ít hơn nhiều.

Để giữ cho bộ xử lý của chúng tôi đơn giản trong khi tăng khả năng sử dụng, chúng tôi đã đưa ra một số quyết định thiết kế quan trọng:

  • Không có đăng ký. Mọi địa chỉ trong RAM đều được xử lý như nhau và có thể được sử dụng làm đối số cho mọi hoạt động. Theo một nghĩa nào đó, điều này có nghĩa là tất cả RAM có thể được coi như các thanh ghi. Điều này có nghĩa là không có hướng dẫn tải / lưu trữ đặc biệt.
  • Trong một tĩnh mạch tương tự, ánh xạ bộ nhớ. Tất cả mọi thứ có thể được viết hoặc đọc từ chia sẻ một sơ đồ địa chỉ thống nhất. Điều này có nghĩa là bộ đếm chương trình (PC) là địa chỉ 0 và điểm khác biệt duy nhất giữa hướng dẫn thông thường và hướng dẫn luồng điều khiển là hướng dẫn luồng điều khiển sử dụng địa chỉ 0.
  • Dữ liệu là nối tiếp trong truyền, song song trong lưu trữ. Do tính chất dựa trên "điện tử" của máy tính của chúng tôi, phép cộng và phép trừ dễ dàng thực hiện hơn khi dữ liệu được truyền dưới dạng nối tiếp nhỏ (đầu tiên ít quan trọng nhất). Hơn nữa, dữ liệu nối tiếp loại bỏ nhu cầu về xe buýt dữ liệu cồng kềnh, vừa thực sự rộng vừa cồng kềnh theo thời gian (để dữ liệu ở cùng nhau, tất cả các "làn" của xe buýt đều phải trải qua cùng một độ trễ di chuyển).
  • Kiến trúc Harvard, nghĩa là sự phân chia giữa bộ nhớ chương trình (ROM) và bộ nhớ dữ liệu (RAM). Mặc dù điều này làm giảm tính linh hoạt của bộ xử lý, nhưng điều này giúp tối ưu hóa kích thước: độ dài của chương trình lớn hơn nhiều so với dung lượng RAM chúng ta cần, vì vậy chúng ta có thể tách chương trình thành ROM và sau đó tập trung vào việc nén ROM , dễ dàng hơn nhiều khi nó chỉ đọc.
  • Độ rộng dữ liệu 16 bit. Đây là sức mạnh nhỏ nhất của hai cái rộng hơn một bảng Tetris tiêu chuẩn (10 khối). Điều này cung cấp cho chúng tôi phạm vi dữ liệu từ -32768 đến +32767 và độ dài chương trình tối đa là 65536 hướng dẫn. (2 ^ 8 = 256 hướng dẫn là đủ cho hầu hết những điều đơn giản mà chúng ta có thể muốn bộ xử lý đồ chơi thực hiện, nhưng không phải là Tetris.)
  • Thiết kế không đồng bộ. Thay vì có đồng hồ trung tâm (hoặc, tương đương, một số đồng hồ) chỉ định thời gian của máy tính, tất cả dữ liệu được kèm theo "tín hiệu đồng hồ" truyền song song với dữ liệu khi nó chạy xung quanh máy tính. Một số đường dẫn nhất định có thể ngắn hơn các đường khác và trong khi điều này sẽ gây khó khăn cho thiết kế đồng hồ tập trung, một thiết kế không đồng bộ có thể dễ dàng xử lý các hoạt động theo thời gian thay đổi.
  • Tất cả các hướng dẫn có kích thước bằng nhau. Chúng tôi cảm thấy rằng một kiến ​​trúc trong đó mỗi lệnh có 1 opcode với 3 toán hạng (đích giá trị giá trị) là tùy chọn linh hoạt nhất. Điều này bao gồm các hoạt động dữ liệu nhị phân cũng như di chuyển có điều kiện.
  • Hệ thống chế độ địa chỉ đơn giản. Có nhiều chế độ địa chỉ rất hữu ích để hỗ trợ những thứ như mảng hoặc đệ quy. Chúng tôi quản lý để thực hiện một số chế độ địa chỉ quan trọng với một hệ thống tương đối đơn giản.

Một minh họa về kiến ​​trúc của chúng tôi có trong bài viết tổng quan.

Chức năng và hoạt động ALU

Từ đây, vấn đề là xác định chức năng nào bộ xử lý của chúng ta nên có. Đặc biệt chú ý đến sự dễ thực hiện cũng như tính linh hoạt của từng lệnh.

Di chuyển có điều kiện

Di chuyển có điều kiện là rất quan trọng và phục vụ như cả dòng điều khiển quy mô nhỏ và quy mô lớn. "Quy mô nhỏ" đề cập đến khả năng kiểm soát việc thực hiện di chuyển dữ liệu cụ thể của nó, trong khi "quy mô lớn" đề cập đến việc sử dụng nó như một hoạt động nhảy có điều kiện để chuyển luồng điều khiển sang bất kỳ đoạn mã tùy ý nào. Không có thao tác nhảy chuyên dụng vì, do ánh xạ bộ nhớ, di chuyển có điều kiện có thể sao chép dữ liệu vào RAM thông thường và sao chép địa chỉ đích vào PC. Chúng tôi cũng chọn từ bỏ cả những bước đi vô điều kiện và những bước nhảy vô điều kiện vì một lý do tương tự: cả hai đều có thể được thực hiện như một động thái có điều kiện với một điều kiện được mã hóa thành TRUE.

Chúng tôi đã chọn có hai loại di chuyển có điều kiện khác nhau: "di chuyển nếu không bằng không" ( MNZ) và "di chuyển nếu nhỏ hơn 0" ( MLZ). Về mặt chức năng, MNZsố tiền để kiểm tra xem có bất kỳ bit nào trong dữ liệu là 1 hay không, trong khi MLZsố tiền để kiểm tra xem bit dấu có phải là 1. Chúng rất hữu ích cho các đẳng thức và so sánh tương ứng. Lý do chúng tôi chọn hai cái này hơn các lý do khác như "di chuyển nếu không" ( MEZ) hoặc "di chuyển nếu lớn hơn 0" ( MGZ) là MEZyêu cầu tạo tín hiệu TRUE từ tín hiệu trống, trong khi đó MGZlà kiểm tra phức tạp hơn, yêu cầu ký hiệu bit là 0 trong khi ít nhất một bit khác là 1.

Môn số học

Các hướng dẫn quan trọng nhất tiếp theo, về mặt hướng dẫn thiết kế bộ xử lý, là các hoạt động số học cơ bản. Như tôi đã đề cập trước đó, chúng tôi đang sử dụng dữ liệu nối tiếp nhỏ, với sự lựa chọn về tuổi thọ được xác định bởi sự dễ dàng của các hoạt động cộng / trừ. Bằng cách có bit có ý nghĩa nhỏ nhất đến trước, các đơn vị số học có thể dễ dàng theo dõi bit mang.

Chúng tôi đã chọn sử dụng biểu diễn bổ sung của 2 cho các số âm, vì điều này làm cho phép cộng và phép trừ phù hợp hơn. Điều đáng chú ý là máy tính Wireworld đã sử dụng phần bù 1.

Phép cộng và phép trừ là phạm vi hỗ trợ số học riêng của máy tính của chúng tôi (bên cạnh các thay đổi bit sẽ được thảo luận sau). Các hoạt động khác, như phép nhân, quá phức tạp để được xử lý bởi kiến ​​trúc của chúng tôi và phải được thực hiện trong phần mềm.

Hoạt động bitwise

Bộ xử lý của chúng tôi có AND, ORXORhướng dẫn làm những gì bạn mong đợi. Thay vì có một NOThướng dẫn, chúng tôi đã chọn để có một hướng dẫn "và không" ( ANT). Khó khăn với NOThướng dẫn một lần nữa là nó phải tạo tín hiệu từ việc thiếu tín hiệu, điều này rất khó với một máy tự động di động. Lệnh ANTchỉ trả về 1 nếu bit đối số thứ nhất là 1 và bit đối số thứ hai là 0. Do đó, NOT xtương đương với ANT -1 x(cũng như XOR -1 x). Hơn nữa, ANTlà linh hoạt và có lợi thế chính của nó trong mặt nạ: trong trường hợp chương trình Tetris, chúng tôi sử dụng nó để xóa tetrominoes.

Dịch chuyển bit

Các hoạt động dịch chuyển bit là các hoạt động phức tạp nhất được xử lý bởi ALU. Họ lấy hai dữ liệu đầu vào: một giá trị để thay đổi và một lượng để thay đổi nó theo. Mặc dù sự phức tạp của chúng (do lượng dịch chuyển thay đổi), các hoạt động này rất quan trọng đối với nhiều nhiệm vụ quan trọng, bao gồm nhiều hoạt động "đồ họa" liên quan đến Tetris. Sự dịch chuyển bit cũng sẽ đóng vai trò là nền tảng cho các thuật toán nhân / chia hiệu quả.

Bộ xử lý của chúng tôi có ba thao tác dịch chuyển bit, "shift trái" ( SL), "shift phải logic" ( SRL) và "dịch chuyển số học phải" ( SRA). Hai thay đổi bit đầu tiên ( SLSRL) điền vào các bit mới với tất cả các số không (có nghĩa là một số âm được dịch chuyển sang phải sẽ không còn âm). Nếu đối số thứ hai của sự thay đổi nằm ngoài phạm vi từ 0 đến 15, kết quả là tất cả các số không, như bạn có thể mong đợi. Đối với sự dịch chuyển bit cuối cùng SRA, sự dịch chuyển bit sẽ giữ nguyên dấu hiệu của đầu vào và do đó đóng vai trò là một phép chia thực sự cho hai.

Hướng dẫn đường ống

Bây giờ là lúc để nói về một số chi tiết nghiệt ngã của kiến ​​trúc. Mỗi chu kỳ CPU bao gồm năm bước sau:

1. Lấy hướng dẫn hiện tại từ ROM

Giá trị hiện tại của PC được sử dụng để tìm nạp lệnh tương ứng từ ROM. Mỗi lệnh có một opcode và ba toán hạng. Mỗi toán hạng bao gồm một từ dữ liệu và một chế độ địa chỉ. Các phần này được tách ra khỏi nhau khi chúng được đọc từ ROM.

Opcode là 4 bit để hỗ trợ 16 opcode duy nhất, trong đó 11 bit được gán:

0000  MNZ    Move if Not Zero
0001  MLZ    Move if Less than Zero
0010  ADD    ADDition
0011  SUB    SUBtraction
0100  AND    bitwise AND
0101  OR     bitwise OR
0110  XOR    bitwise eXclusive OR
0111  ANT    bitwise And-NoT
1000  SL     Shift Left
1001  SRL    Shift Right Logical
1010  SRA    Shift Right Arithmetic
1011  unassigned
1100  unassigned
1101  unassigned
1110  unassigned
1111  unassigned

2. Viết kết quả (nếu cần) của hướng dẫn trước vào RAM

Tùy thuộc vào điều kiện của lệnh trước đó (chẳng hạn như giá trị của đối số đầu tiên cho di chuyển có điều kiện), việc ghi được thực hiện. Địa chỉ của ghi được xác định bởi toán hạng thứ ba của lệnh trước đó.

Điều quan trọng cần lưu ý là viết xảy ra sau khi tìm nạp lệnh. Điều này dẫn đến việc tạo ra một khe trễ nhánh trong đó lệnh ngay sau lệnh rẽ nhánh (bất kỳ thao tác nào ghi vào PC) được thực hiện thay cho lệnh đầu tiên tại mục tiêu nhánh.

Trong một số trường hợp nhất định (như nhảy vô điều kiện), khe trễ nhánh có thể được tối ưu hóa đi. Trong các trường hợp khác, nó không thể, và lệnh sau một nhánh phải để trống. Hơn nữa, loại khe trễ này có nghĩa là các nhánh phải sử dụng mục tiêu nhánh ít hơn 1 địa chỉ so với hướng dẫn đích thực tế, để tính đến sự gia tăng của PC xảy ra.

Nói tóm lại, vì đầu ra của lệnh trước được ghi vào RAM sau khi lệnh tiếp theo được tìm nạp, các bước nhảy có điều kiện cần phải có lệnh trống sau chúng, nếu không thì PC sẽ không được cập nhật đúng cho bước nhảy.

3. Đọc dữ liệu cho các đối số của hướng dẫn hiện tại từ RAM

Như đã đề cập trước đó, mỗi trong ba toán hạng bao gồm cả từ dữ liệu và chế độ địa chỉ. Từ dữ liệu là 16 bit, cùng chiều rộng với RAM. Chế độ địa chỉ là 2 bit.

Các chế độ địa chỉ có thể là một nguồn phức tạp đáng kể cho bộ xử lý như thế này, vì nhiều chế độ địa chỉ trong thế giới thực liên quan đến việc tính toán nhiều bước (như thêm phần bù). Đồng thời, các chế độ địa chỉ đa năng đóng một vai trò quan trọng trong khả năng sử dụng của bộ xử lý.

Chúng tôi đã tìm cách thống nhất các khái niệm sử dụng các số được mã hóa cứng làm toán hạng và sử dụng địa chỉ dữ liệu làm toán hạng. Điều này dẫn đến việc tạo ra các chế độ địa chỉ dựa trên bộ đếm: chế độ địa chỉ của toán hạng chỉ đơn giản là một số biểu thị số lần dữ liệu sẽ được gửi xung quanh vòng lặp đọc RAM. Điều này bao gồm địa chỉ trực tiếp, trực tiếp, gián tiếp và gián tiếp kép.

00  Immediate:  A hard-coded value. (no RAM reads)
01  Direct:  Read data from this RAM address. (one RAM read)
10  Indirect:  Read data from the address given at this address. (two RAM reads)
11  Double-indirect: Read data from the address given at the address given by this address. (three RAM reads)

Sau khi hội thảo này được thực hiện, ba toán hạng của lệnh có các vai trò khác nhau. Toán hạng đầu tiên thường là đối số đầu tiên cho toán tử nhị phân, nhưng cũng đóng vai trò là điều kiện khi lệnh hiện tại là một động thái có điều kiện. Toán hạng thứ hai đóng vai trò là đối số thứ hai cho toán tử nhị phân. Toán hạng thứ ba đóng vai trò là địa chỉ đích cho kết quả của lệnh.

Do hai hướng dẫn đầu tiên đóng vai trò là dữ liệu trong khi hướng dẫn thứ ba đóng vai trò là địa chỉ, nên các chế độ địa chỉ có cách hiểu hơi khác nhau tùy thuộc vào vị trí chúng được sử dụng. Ví dụ: chế độ trực tiếp được sử dụng để đọc dữ liệu từ một địa chỉ RAM cố định (vì một lần đọc RAM là cần thiết), nhưng chế độ tức thời được sử dụng để ghi dữ liệu vào một địa chỉ RAM cố định (vì không cần đọc RAM).

4. Tính kết quả

Opcode và hai toán hạng đầu tiên được gửi đến ALU để thực hiện thao tác nhị phân. Đối với các phép toán số học, bitwise và shift, điều này có nghĩa là thực hiện các hoạt động liên quan. Đối với các bước di chuyển có điều kiện, điều này có nghĩa đơn giản là trả về toán hạng thứ hai.

Opcode và toán hạng đầu tiên được sử dụng để tính toán điều kiện, xác định xem có ghi kết quả vào bộ nhớ hay không. Trong trường hợp di chuyển có điều kiện, điều này có nghĩa là xác định xem có bất kỳ bit nào trong toán hạng là 1 (for MNZ) hay không, hoặc xác định xem bit dấu có phải là 1 (for MLZ) hay không. Nếu opcode không phải là một động thái có điều kiện, thì việc ghi luôn được thực hiện (điều kiện luôn luôn đúng).

5. Tăng bộ đếm chương trình

Cuối cùng, bộ đếm chương trình được đọc, tăng và viết.

Do vị trí của mức tăng PC giữa lệnh đọc và lệnh ghi, điều này có nghĩa là một lệnh tăng PC lên 1 là không có lệnh. Một lệnh sao chép PC vào chính nó làm cho lệnh tiếp theo được thực thi hai lần liên tiếp. Nhưng, được cảnh báo, nhiều lệnh PC liên tiếp có thể gây ra các hiệu ứng phức tạp, bao gồm cả vòng lặp vô hạn, nếu bạn không chú ý đến đường dẫn lệnh.

Nhiệm vụ cho hội Tetris

Chúng tôi đã tạo một ngôn ngữ lắp ráp mới có tên QFTASM cho bộ xử lý của chúng tôi. Ngôn ngữ lắp ráp này tương ứng 1-1 với mã máy trong ROM của máy tính.

Bất kỳ chương trình QFTASM nào cũng được viết dưới dạng một loạt các hướng dẫn, mỗi hướng dẫn. Mỗi dòng được định dạng như thế này:

[line numbering] [opcode] [arg1] [arg2] [arg3]; [optional comment]

Danh sách mã hóa

Như đã thảo luận trước đó, có mười một mã được máy tính hỗ trợ, mỗi mã có ba toán hạng:

MNZ [test] [value] [dest]  – Move if Not Zero; sets [dest] to [value] if [test] is not zero.
MLZ [test] [value] [dest]  – Move if Less than Zero; sets [dest] to [value] if [test] is less than zero.
ADD [val1] [val2] [dest]   – ADDition; store [val1] + [val2] in [dest].
SUB [val1] [val2] [dest]   – SUBtraction; store [val1] - [val2] in [dest].
AND [val1] [val2] [dest]   – bitwise AND; store [val1] & [val2] in [dest].
OR [val1] [val2] [dest]    – bitwise OR; store [val1] | [val2] in [dest].
XOR [val1] [val2] [dest]   – bitwise XOR; store [val1] ^ [val2] in [dest].
ANT [val1] [val2] [dest]   – bitwise And-NoT; store [val1] & (![val2]) in [dest].
SL [val1] [val2] [dest]    – Shift Left; store [val1] << [val2] in [dest].
SRL [val1] [val2] [dest]   – Shift Right Logical; store [val1] >>> [val2] in [dest]. Doesn't preserve sign.
SRA [val1] [val2] [dest]   – Shift Right Arithmetic; store [val1] >> [val2] in [dest], while preserving sign.

Chế độ địa chỉ

Mỗi toán hạng chứa cả giá trị dữ liệu và di chuyển địa chỉ. Giá trị dữ liệu được mô tả bằng một số thập phân trong phạm vi -32768 đến 32767. Chế độ địa chỉ được mô tả bằng tiền tố một chữ cái cho giá trị dữ liệu.

mode    name               prefix
0       immediate          (none)
1       direct             A
2       indirect           B
3       double-indirect    C 

Mã ví dụ

Chuỗi Fibonacci trong năm dòng:

0. MLZ -1 1 1;    initial value
1. MLZ -1 A2 3;   start loop, shift data
2. MLZ -1 A1 2;   shift data
3. MLZ -1 0 0;    end loop
4. ADD A2 A3 1;   branch delay slot, compute next term

Mã này tính toán chuỗi Fibonacci, với địa chỉ RAM 1 chứa thuật ngữ hiện tại. Nó nhanh chóng tràn ra sau 28657.

Mã màu xám:

0. MLZ -1 5 1;      initial value for RAM address to write to
1. SUB A1 5 2;      start loop, determine what binary number to covert to Gray code
2. SRL A2 1 3;      shift right by 1
3. XOR A2 A3 A1;    XOR and store Gray code in destination address
4. SUB B1 42 4;     take the Gray code and subtract 42 (101010)
5. MNZ A4 0 0;      if the result is not zero (Gray code != 101010) repeat loop
6. ADD A1 1 1;      branch delay slot, increment destination address

Chương trình này tính toán mã Gray và lưu trữ mã trong các địa chỉ thành công bắt đầu từ địa chỉ 5. Chương trình này sử dụng một số tính năng quan trọng như địa chỉ gián tiếp và bước nhảy có điều kiện. Nó dừng lại một khi mã Gray kết quả là 101010, xảy ra cho đầu vào 51 tại địa chỉ 56.

Phiên dịch trực tuyến

El'endia Starman đã tạo ra một thông dịch viên trực tuyến rất hữu ích ở đây . Bạn có thể bước qua mã, đặt điểm dừng, thực hiện ghi thủ công vào RAM và hiển thị RAM dưới dạng màn hình.

Cogol

Khi kiến ​​trúc và ngôn ngữ lắp ráp được xác định, bước tiếp theo về phía "phần mềm" của dự án là tạo ra một ngôn ngữ cấp cao hơn, một cái gì đó phù hợp với Tetris. Do đó tôi đã tạo ra Cogol . Cái tên này vừa là một cách chơi chữ của "COBOL" vừa là từ viết tắt của "C of Game of Life", mặc dù điều đáng chú ý là Cogol dành cho C máy tính của chúng ta là máy tính thực tế.

Cogol tồn tại ở một cấp độ ngay trên ngôn ngữ lắp ráp. Nói chung, hầu hết các dòng trong chương trình Cogol đều tương ứng với một dòng lắp ráp, nhưng có một số tính năng quan trọng của ngôn ngữ:

  • Các tính năng cơ bản bao gồm các biến được đặt tên với các bài tập và toán tử có cú pháp dễ đọc hơn. Ví dụ, ADD A1 A2 3trở thành z = x + y;, với các biến ánh xạ trình biên dịch vào địa chỉ.
  • Các cấu trúc vòng lặp như if(){}, while(){}do{}while();do đó trình biên dịch xử lý phân nhánh.
  • Mảng một chiều (với số học con trỏ), được sử dụng cho bảng Tetris.
  • Chương trình con và ngăn xếp cuộc gọi. Chúng rất hữu ích trong việc ngăn chặn sự trùng lặp của các đoạn mã lớn và để hỗ trợ đệ quy.

Trình biên dịch (mà tôi đã viết từ đầu) rất cơ bản / ngây thơ, nhưng tôi đã cố gắng tối ưu hóa một số cấu trúc ngôn ngữ để đạt được độ dài chương trình biên dịch ngắn.

Dưới đây là một số tổng quan ngắn về cách các tính năng ngôn ngữ khác nhau hoạt động:

Mã thông báo

Mã nguồn được mã hóa tuyến tính (một lượt), sử dụng các quy tắc đơn giản về các ký tự được phép liền kề trong mã thông báo. Khi gặp phải một ký tự không thể liền kề với ký tự cuối cùng của mã thông báo hiện tại, mã thông báo hiện tại được coi là hoàn thành và ký tự mới bắt đầu mã thông báo mới. Một số ký tự (chẳng hạn như {hoặc ,) không thể liền kề với bất kỳ ký tự nào khác và do đó là mã thông báo của riêng họ. Những người khác (như >hay =) chỉ được phép được tiếp giáp với các nhân vật khác trong lớp học của họ, và do đó có thể hình thành tokens như >>>, ==hoặc >=, nhưng không thích =2. Các ký tự khoảng trắng buộc một ranh giới giữa các mã thông báo nhưng không bao gồm chính chúng trong kết quả. Ký tự khó mã hóa nhất là- bởi vì nó có thể đại diện cho phép trừ và phủ định đơn phương, và do đó đòi hỏi một số vỏ đặc biệt.

Phân tích cú pháp

Phân tích cú pháp cũng được thực hiện theo kiểu một lượt. Trình biên dịch có các phương thức để xử lý từng cấu trúc ngôn ngữ khác nhau và các mã thông báo được bật ra khỏi danh sách mã thông báo toàn cầu khi chúng được sử dụng bởi các phương thức biên dịch khác nhau. Nếu trình biên dịch từng thấy mã thông báo mà nó không mong đợi, nó sẽ phát sinh lỗi cú pháp.

Phân bổ bộ nhớ toàn cầu

Trình biên dịch gán cho mỗi biến toàn cục (từ hoặc mảng) địa chỉ RAM được chỉ định của riêng nó. Cần phải khai báo tất cả các biến bằng cách sử dụng từ khóa myđể trình biên dịch biết phân bổ không gian cho nó. Mát hơn nhiều so với các biến toàn cục được đặt tên là quản lý bộ nhớ địa chỉ đầu. Nhiều hướng dẫn (đáng chú ý là có điều kiện và nhiều truy cập mảng) yêu cầu địa chỉ "cào" tạm thời để lưu trữ các tính toán trung gian. Trong quá trình biên dịch, trình biên dịch phân bổ và phân bổ lại các địa chỉ đầu khi cần thiết. Nếu trình biên dịch cần nhiều địa chỉ đầu, nó sẽ dành nhiều RAM hơn làm địa chỉ đầu. Tôi tin rằng một chương trình điển hình chỉ yêu cầu một vài địa chỉ đầu, mặc dù mỗi địa chỉ đầu sẽ được sử dụng nhiều lần.

IF-ELSE Các câu lệnh

Cú pháp cho các if-elsecâu lệnh là dạng C tiêu chuẩn:

other code
if (cond) {
  first body
} else {
  second body
}
other code

Khi được chuyển đổi thành QFTASM, mã được sắp xếp như sau:

other code
condition test
conditional jump
first body
unconditional jump
second body (conditional jump target)
other code (unconditional jump target)

Nếu cơ thể thứ nhất được thực thi, cơ thể thứ hai được bỏ qua. Nếu cơ thể thứ nhất bị bỏ qua, cơ thể thứ hai được thực thi.

Trong phần lắp ráp, một bài kiểm tra điều kiện thường chỉ là phép trừ và dấu hiệu của kết quả sẽ quyết định việc thực hiện bước nhảy hay thực hiện phần thân. Một MLZhướng dẫn được sử dụng để xử lý các bất đẳng thức như >hoặc <=. Một MNZlệnh được sử dụng để xử lý ==, vì nó nhảy qua cơ thể khi chênh lệch không bằng 0 (và do đó khi các đối số không bằng nhau). Điều kiện đa biểu thức hiện không được hỗ trợ.

Nếu elsecâu lệnh bị bỏ qua, bước nhảy vô điều kiện cũng bị bỏ qua và mã QFTASM trông như thế này:

other code
condition test
conditional jump
body
other code (conditional jump target)

WHILE Các câu lệnh

Cú pháp cho các whilecâu lệnh cũng là dạng C tiêu chuẩn:

other code
while (cond) {
  body
}
other code

Khi được chuyển đổi thành QFTASM, mã được sắp xếp như sau:

other code
unconditional jump
body (conditional jump target)
condition test (unconditional jump target)
conditional jump
other code

Kiểm tra điều kiện và nhảy có điều kiện nằm ở cuối khối, có nghĩa là chúng được thực hiện lại sau mỗi lần thực hiện khối. Khi điều kiện được trả về false, phần thân không được lặp lại và vòng lặp kết thúc. Trong khi bắt đầu thực hiện vòng lặp, luồng điều khiển nhảy qua thân vòng lặp đến mã điều kiện, vì vậy thân máy không bao giờ được thực thi nếu điều kiện sai lần đầu tiên.

Một MLZhướng dẫn được sử dụng để xử lý các bất đẳng thức như >hoặc <=. Không giống như trong các ifcâu lệnh, một MNZlệnh được sử dụng để xử lý !=, vì nó nhảy vào phần thân khi chênh lệch không bằng 0 (và do đó khi các đối số không bằng nhau).

DO-WHILE Các câu lệnh

Sự khác biệt duy nhất giữa whiledo-whilelà phần do-whilethân vòng lặp ban đầu không được bỏ qua để nó luôn được thực hiện ít nhất một lần. Tôi thường sử dụng các do-whilecâu lệnh để lưu một vài dòng mã lắp ráp khi tôi biết vòng lặp sẽ không bao giờ cần phải bỏ qua hoàn toàn.

Mảng

Mảng một chiều được thực hiện như các khối bộ nhớ liền kề. Tất cả các mảng có độ dài cố định dựa trên khai báo của chúng. Mảng được tuyên bố như vậy:

my alpha[3];               # empty array
my beta[11] = {3,2,7,8};   # first four elements are pre-loaded with those values

Đối với mảng, đây là ánh xạ RAM có thể, hiển thị cách các địa chỉ 15-18 được dành riêng cho mảng:

15: alpha
16: alpha[0]
17: alpha[1]
18: alpha[2]

Địa chỉ được gắn nhãn alphachứa đầy một con trỏ đến vị trí của alpha[0], vì vậy trong trường hợp này, địa chỉ 15 chứa giá trị 16. alphaBiến có thể được sử dụng bên trong mã Cogol, có thể là con trỏ ngăn xếp nếu bạn muốn sử dụng mảng này làm ngăn xếp .

Truy cập các phần tử của một mảng được thực hiện với array[index]ký hiệu chuẩn . Nếu giá trị của indexlà một hằng số, tham chiếu này sẽ tự động được điền vào địa chỉ tuyệt đối của phần tử đó. Mặt khác, nó thực hiện một số số học con trỏ (chỉ cần thêm) để tìm địa chỉ tuyệt đối mong muốn. Cũng có thể lập chỉ mục lồng, chẳng hạn như alpha[beta[1]].

Chương trình con và gọi

Chương trình con là các khối mã có thể được gọi từ nhiều ngữ cảnh, ngăn ngừa trùng lặp mã và cho phép tạo các chương trình đệ quy. Đây là một chương trình với một chương trình con đệ quy để tạo ra các số Fibonacci (về cơ bản là thuật toán chậm nhất):

# recursively calculate the 10th Fibonacci number
call display = fib(10).sum;
sub fib(cur,sum) {
  if (cur <= 2) {
    sum = 1;
    return;
  }
  cur--;
  call sum = fib(cur).sum;
  cur--;
  call sum += fib(cur).sum;
}

Một chương trình con được khai báo với từ khóa subvà một chương trình con có thể được đặt ở bất cứ đâu trong chương trình. Mỗi chương trình con có thể có nhiều biến cục bộ, được khai báo như là một phần của danh sách các đối số của nó. Các đối số này cũng có thể được đưa ra các giá trị mặc định.

Để xử lý các cuộc gọi đệ quy, các biến cục bộ của chương trình con được lưu trữ trên ngăn xếp. Biến tĩnh cuối cùng trong RAM là con trỏ ngăn xếp cuộc gọi và tất cả bộ nhớ sau đó đóng vai trò là ngăn xếp cuộc gọi. Khi một chương trình con được gọi, nó đã tạo một khung mới trên ngăn xếp cuộc gọi, bao gồm tất cả các biến cục bộ cũng như địa chỉ trả về (ROM). Mỗi chương trình con trong chương trình được cung cấp một địa chỉ RAM tĩnh duy nhất để phục vụ như một con trỏ. Con trỏ này đưa ra vị trí của lệnh gọi "hiện tại" của chương trình con trong ngăn xếp cuộc gọi. Tham chiếu một biến cục bộ được thực hiện bằng cách sử dụng giá trị của con trỏ tĩnh này cộng với một phần bù để cung cấp địa chỉ của biến cục bộ cụ thể đó. Cũng chứa trong ngăn xếp cuộc gọi là giá trị trước đó của con trỏ tĩnh. Đây'

RAM map:
0: pc
1: display
2: scratch0
3: fib
4: scratch1
5: scratch2
6: scratch3
7: call

fib map:
0: return
1: previous_call
2: cur
3: sum

Một điều thú vị về chương trình con là chúng không trả về bất kỳ giá trị cụ thể nào. Thay vào đó, tất cả các biến cục bộ của chương trình con có thể được đọc sau khi chương trình con được thực hiện, do đó, nhiều loại dữ liệu có thể được trích xuất từ ​​một lệnh gọi chương trình con. Điều này được thực hiện bằng cách lưu trữ con trỏ cho lệnh gọi cụ thể của chương trình con, sau đó có thể được sử dụng để khôi phục bất kỳ biến cục bộ nào từ trong khung ngăn xếp (được giải quyết gần đây).

Có nhiều cách để gọi một chương trình con, tất cả đều sử dụng calltừ khóa:

call fib(10);   # subroutine is executed, no return vaue is stored

call pointer = fib(10);   # execute subroutine and return a pointer
display = pointer.sum;    # access a local variable and assign it to a global variable

call display = fib(10).sum;   # immediately store a return value

call display += fib(10).sum;   # other types of assignment operators can also be used with a return value

Bất kỳ số lượng giá trị nào cũng có thể được cung cấp làm đối số cho lệnh gọi chương trình con. Bất kỳ đối số nào không được cung cấp sẽ được điền vào với giá trị mặc định của nó, nếu có. Một đối số không được cung cấp và không có giá trị mặc định sẽ không bị xóa (để lưu hướng dẫn / thời gian) vì vậy có thể có khả năng đảm nhận bất kỳ giá trị nào khi bắt đầu chương trình con.

Con trỏ là một cách truy cập nhiều biến cục bộ của chương trình con, mặc dù điều quan trọng cần lưu ý là con trỏ chỉ là tạm thời: dữ liệu mà con trỏ trỏ tới sẽ bị hủy khi thực hiện lệnh gọi chương trình con khác.

Nhãn gỡ lỗi

Bất kỳ {...}khối mã nào trong chương trình Cogol đều có thể được đặt trước nhãn mô tả nhiều từ. Nhãn này được đính kèm dưới dạng một nhận xét trong mã lắp ráp được biên dịch và có thể rất hữu ích để gỡ lỗi vì nó giúp dễ dàng xác định vị trí các đoạn mã cụ thể.

Tối ưu hóa độ trễ chi nhánh

Để cải thiện tốc độ của mã được biên dịch, trình biên dịch Cogol thực hiện một số tối ưu hóa khe trễ thực sự cơ bản như là một bước cuối cùng đối với mã QFTASM. Đối với bất kỳ bước nhảy vô điều kiện nào có khe trễ nhánh trống, khe trễ có thể được lấp đầy bởi lệnh đầu tiên tại đích nhảy và đích nhảy được tăng thêm một để trỏ đến lệnh tiếp theo. Điều này thường tiết kiệm một chu kỳ mỗi lần thực hiện bước nhảy vô điều kiện.

Viết mã Tetris bằng Cogol

Chương trình Tetris cuối cùng được viết bằng Cogol và mã nguồn có sẵn ở đây . Mã QFTASM được biên dịch có sẵn ở đây . Để thuận tiện, một permalink được cung cấp ở đây: Tetris trong QFTASM . Vì mục tiêu là đánh golf mã lắp ráp (không phải mã Cogol), nên mã Cogol kết quả là khó sử dụng. Nhiều phần của chương trình thường được đặt trong các chương trình con, nhưng các chương trình con đó thực sự đủ ngắn để sao chép mã đã lưu các hướng dẫn quacallcác câu lệnh. Mã cuối cùng chỉ có một chương trình con ngoài mã chính. Ngoài ra, nhiều mảng đã được loại bỏ và thay thế bằng một danh sách các biến riêng lẻ dài tương đương hoặc bằng nhiều số được mã hóa cứng trong chương trình. Mã QFTASM được biên dịch cuối cùng có dưới 300 hướng dẫn, mặc dù nó chỉ dài hơn một chút so với chính nguồn Cogol.


22
Tôi thích việc lựa chọn hướng dẫn ngôn ngữ lắp ráp được xác định bởi phần cứng cơ chất của bạn (không có MEZ vì lắp ráp đúng từ hai lỗi là khó). Đọc tuyệt vời.
AlexC

1
Bạn nói rằng =chỉ có thể đứng bên cạnh chính nó, nhưng có một !=.
Fabian Röling

@Fabian a+=
Oliphistic

@Oliphaunt Vâng, mô tả của tôi không chính xác lắm, nó giống với một lớp nhân vật hơn, trong đó một lớp nhân vật nhất định có thể nằm liền kề nhau.
PhiNotPi

606

Phần 5: Hội, Dịch, và Tương lai

Với chương trình lắp ráp của chúng tôi từ trình biên dịch, đã đến lúc lắp ráp ROM cho máy tính Varlife và dịch mọi thứ thành một mẫu GoL lớn!

hội,, tổ hợp

Việc lắp ráp chương trình lắp ráp vào ROM được thực hiện theo cách tương tự như trong lập trình truyền thống: mỗi lệnh được dịch thành tương đương nhị phân và sau đó chúng được nối thành một blob nhị phân lớn mà chúng ta gọi là thực thi. Đối với chúng tôi, sự khác biệt duy nhất là, blob nhị phân cần được dịch sang các mạch Varlife và được gắn vào máy tính.

K Zhang đã viết CreatROM.py , một tập lệnh Python cho Golly thực hiện việc lắp ráp và dịch thuật. Điều này khá đơn giản: nó lấy một chương trình lắp ráp từ bảng tạm, lắp ráp nó thành một nhị phân và chuyển nhị phân đó thành mạch. Dưới đây là một ví dụ với trình kiểm tra tính nguyên thủy đơn giản có trong tập lệnh:

#0. MLZ -1 3 3;
#1. MLZ -1 7 6; preloadCallStack
#2. MLZ -1 2 1; beginDoWhile0_infinite_loop
#3. MLZ -1 1 4; beginDoWhile1_trials
#4. ADD A4 2 4;
#5. MLZ -1 A3 5; beginDoWhile2_repeated_subtraction
#6. SUB A5 A4 5;
#7. SUB 0 A5 2;
#8. MLZ A2 5 0;
#9. MLZ 0 0 0; endDoWhile2_repeated_subtraction
#10. MLZ A5 3 0;
#11. MNZ 0 0 0; endDoWhile1_trials
#12. SUB A4 A3 2;
#13. MNZ A2 15 0; beginIf3_prime_found
#14. MNZ 0 0 0;
#15. MLZ -1 A3 1; endIf3_prime_found
#16. ADD A3 2 3;
#17. MLZ -1 3 0;
#18. MLZ -1 1 4; endDoWhile0_infinite_loop

Điều này tạo ra nhị phân sau:

0000000000000001000000000000000000010011111111111111110001
0000000000000000000000000000000000110011111111111111110001
0000000000000000110000000000000000100100000000000000110010
0000000000000000010100000000000000110011111111111111110001
0000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000011110100000000000000100000
0000000000000000100100000000000000110100000000000001000011
0000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000110100000000000001010001
0000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000001010100000000000000100001
0000000000000000100100000000000001010000000000000000000011
0000000000000001010100000000000001000100000000000001010011
0000000000000001010100000000000000110011111111111111110001
0000000000000001000000000000000000100100000000000001000010
0000000000000001000000000000000000010011111111111111110001
0000000000000000010000000000000000100011111111111111110001
0000000000000001100000000000000001110011111111111111110001
0000000000000000110000000000000000110011111111111111110001

Khi được dịch sang các mạch Varlife, nó trông như thế này:

ROM

ROM chụp gần

ROM sau đó được liên kết với máy tính, tạo thành một chương trình hoạt động đầy đủ trong Varlife. Nhưng chúng ta vẫn chưa xong ...

Dịch sang Trò chơi cuộc sống

Toàn bộ thời gian này, chúng tôi đã làm việc trong nhiều lớp trừu tượng khác nhau trên cơ sở Trò chơi cuộc sống. Nhưng bây giờ, đã đến lúc kéo lại bức màn trừu tượng và chuyển tác phẩm của chúng tôi thành mô hình Trò chơi cuộc sống. Như đã đề cập trước đây, chúng tôi đang sử dụng OTCA Metapixel làm cơ sở cho Varlife. Vì vậy, bước cuối cùng là chuyển đổi từng ô trong Varlife thành siêu dữ liệu trong Trò chơi cuộc sống.

Rất may, Golly đi kèm với một tập lệnh ( metafier.py ) có thể chuyển đổi các mẫu trong các quy tắc khác nhau thành các mẫu Trò chơi cuộc sống thông qua OTCA Metapixel. Thật không may, nó chỉ được thiết kế để chuyển đổi các mẫu với một quy tắc toàn cầu duy nhất, vì vậy nó không hoạt động trên Varlife. Tôi đã viết một phiên bản sửa đổi giải quyết vấn đề đó, để quy tắc cho mỗi metapixel được tạo trên cơ sở từng tế bào cho Varlife.

Vì vậy, máy tính của chúng tôi (với ROM Tetris) có hộp giới hạn 1.436 x 5.082. Trong số 7.297.752 ô trong ô đó, 6.075.811 là không gian trống, để lại số lượng dân số thực tế là 1.21.941. Mỗi ô trong số đó cần được dịch thành siêu dữ liệu OTCA, có hộp giới hạn 2048x2048 và dân số là 64.691 (đối với siêu dữ liệu BẬT) hoặc 23.920 (đối với siêu dữ liệu TẮT). Điều đó có nghĩa là sản phẩm cuối cùng sẽ có một hộp giới hạn 2.940.928 x 10.407.936 (cộng thêm vài nghìn cho đường viền của metapixel), với dân số từ 29.228.828.720 đến 79.048.585.231. Với 1 bit cho mỗi ô sống, cần từ 27 đến 74 GiB để đại diện cho toàn bộ máy tính và ROM.

Tôi đã bao gồm những tính toán đó ở đây vì tôi đã bỏ qua việc chạy chúng trước khi bắt đầu tập lệnh và rất nhanh hết bộ nhớ trên máy tính của tôi. Sau một killlệnh hoảng loạn , tôi đã thực hiện một sửa đổi cho tập lệnh metafier. Cứ sau 10 dòng metapixel, mẫu được lưu vào đĩa (dưới dạng tệp RLE được nén) và lưới được xóa. Điều này bổ sung thêm thời gian chạy cho bản dịch và sử dụng nhiều dung lượng đĩa hơn, nhưng giữ cho việc sử dụng bộ nhớ trong giới hạn cho phép. Vì Golly sử dụng định dạng RLE mở rộng bao gồm vị trí của mẫu, điều này không làm tăng thêm độ phức tạp cho việc tải mẫu - chỉ cần mở tất cả các tệp mẫu trên cùng một lớp.

K Zhang đã xây dựng nên công việc này và tạo ra một tập lệnh siêu dữ liệu hiệu quả hơn sử dụng định dạng tệp MacroCell, tải hiệu quả hơn RLE cho các mẫu lớn. Tập lệnh này chạy nhanh hơn đáng kể (một vài giây, so với nhiều giờ đối với tập lệnh siêu dữ liệu gốc), tạo ra sản lượng nhỏ hơn rất nhiều (121 KB so với 1,7 GB) và có thể biến đổi toàn bộ máy tính và ROM trong một lần trượt mà không cần sử dụng một lượng lớn của bộ nhớ. Nó lợi dụng thực tế là các tệp MacroCell mã hóa các cây mô tả các mẫu. Bằng cách sử dụng tệp mẫu tùy chỉnh, các siêu dữ liệu được tải sẵn vào cây và sau khi một số tính toán và sửa đổi để phát hiện hàng xóm, mẫu Varlife có thể được thêm vào.

Tệp mẫu của toàn bộ máy tính và ROM trong Game of Life có thể được tìm thấy ở đây .


Tương lai của dự án

Bây giờ chúng ta đã tạo ra Tetris, chúng ta đã hoàn thành, phải không? Thậm chí không gần gũi. Chúng tôi có thêm một số mục tiêu cho dự án này mà chúng tôi đang hướng tới:

  • bùn và Kritixi Lithos đang tiếp tục làm việc với ngôn ngữ cấp cao hơn được biên dịch thành QFTASM.
  • El'endia Starman đang nghiên cứu nâng cấp lên trình thông dịch QFTASM trực tuyến.
  • quartata đang làm việc trên một phụ trợ GCC, cho phép biên dịch mã C và C ++ tự do (và các ngôn ngữ khác, như Fortran, D hoặc Objective-C) vào QFTASM thông qua GCC. Điều này sẽ cho phép các chương trình phức tạp hơn được tạo ra bằng một ngôn ngữ quen thuộc hơn, mặc dù không có thư viện chuẩn.
  • Một trong những rào cản lớn nhất mà chúng tôi phải vượt qua trước khi có thể đạt được nhiều tiến bộ hơn là thực tế là các công cụ của chúng tôi không thể phát ra mã độc lập với vị trí (ví dụ như các bước nhảy tương đối). Không có PIC, chúng tôi không thể thực hiện bất kỳ liên kết nào và vì vậy chúng tôi bỏ lỡ những lợi thế đến từ việc có thể liên kết với các thư viện hiện có. Chúng tôi đang cố gắng tìm cách làm PIC chính xác.
  • Chúng tôi đang thảo luận về chương trình tiếp theo mà chúng tôi muốn viết cho máy tính QFT. Ngay bây giờ, Pong trông giống như một mục tiêu tốt đẹp.

2
Chỉ cần nhìn vào tiểu mục trong tương lai, không phải là một bước nhảy tương đối chỉ là một ADD PC offset PC? Xin lỗi vì sự ngây ngô của tôi nếu điều này không chính xác, lập trình lắp ráp không bao giờ là sở trường của tôi.
MBraedley

3
@Timmmm Có, nhưng rất chậm. (Bạn cũng phải sử dụng HashLife).
một spaghetto

75
Chương trình tiếp theo bạn viết cho nó phải là Trò chơi cuộc sống của Conway.
ACK_stoverflow

13
@ACK_stoverflow Điều đó sẽ được thực hiện tại một số điểm.
Mego

13
Bạn có video của nó đang chạy không?
PyRulez

583

Phần 6: Trình biên dịch mới hơn cho QFTASM

Mặc dù Cogol là đủ để thực hiện Tetris thô sơ, nhưng nó quá đơn giản và quá thấp để lập trình cho mục đích chung ở mức độ dễ đọc. Chúng tôi bắt đầu làm việc với một ngôn ngữ mới vào tháng 9 năm 2016. Tiến trình về ngôn ngữ này chậm do các lỗi khó hiểu cũng như đời thực.

Chúng tôi đã xây dựng một ngôn ngữ cấp thấp với cú pháp tương tự như Python, bao gồm một hệ thống loại đơn giản, các chương trình con hỗ trợ toán tử đệ quy và toán tử nội tuyến. Trình biên dịch từ văn bản sang QFTASM đã được tạo với 4 bước: mã thông báo, cây ngữ pháp, trình biên dịch cấp cao và trình biên dịch cấp thấp.

Mã thông báo

Quá trình phát triển đã được bắt đầu bằng Python bằng thư viện tokeniser tích hợp, có nghĩa là bước này khá đơn giản. Chỉ có một vài thay đổi đối với đầu ra mặc định là bắt buộc, bao gồm cả nhận xét tước (nhưng không phải #includes).

Cây ngữ pháp

Cây ngữ pháp được tạo ra để có thể dễ dàng mở rộng mà không phải sửa đổi bất kỳ mã nguồn nào.

Cấu trúc cây được lưu trữ trong tệp XML bao gồm cấu trúc của các nút có thể tạo nên cây và cách chúng được tạo thành với các nút và mã thông báo khác.

Ngữ pháp cần thiết để hỗ trợ các nút lặp lại cũng như các nút tùy chọn. Điều này đã đạt được bằng cách giới thiệu các thẻ meta để mô tả cách đọc mã thông báo.

Các mã thông báo được tạo sau đó được phân tích cú pháp thông qua các quy tắc ngữ pháp sao cho đầu ra tạo thành một cây các yếu tố ngữ pháp như subs và generic_variables, từ đó chứa các yếu tố ngữ pháp và mã thông báo khác.

Biên dịch thành mã cấp cao

Mỗi tính năng của ngôn ngữ cần có thể được biên dịch thành các cấu trúc cấp cao. Chúng bao gồm assign(a, 12)call_subroutine(is_prime, call_variable=12, return_variable=temp_var). Các tính năng như nội tuyến của các phần tử được thực thi trong phân đoạn này. Chúng được định nghĩa là operators và đặc biệt ở chỗ chúng được nội tuyến mỗi khi một toán tử như +hoặc %được sử dụng. Do đó, chúng bị hạn chế nhiều hơn mã thông thường - chúng không thể sử dụng toán tử riêng của chúng cũng như bất kỳ toán tử nào phụ thuộc vào mã được xác định.

Trong quá trình nội tuyến, các biến nội bộ được thay thế bằng các biến được gọi. Điều này có hiệu lực lần lượt

operator(int a + int b) -> int c
    return __ADD__(a, b)
int i = 3+3

vào

int i = __ADD__(3, 3)

Tuy nhiên, hành vi này có thể gây bất lợi và dễ bị lỗi nếu biến đầu vào và biến đầu ra trỏ đến cùng một vị trí trong bộ nhớ. Để sử dụng hành vi 'an toàn hơn', unsafetừ khóa điều chỉnh quá trình biên dịch sao cho các biến bổ sung được tạo và sao chép vào và từ nội tuyến khi cần.

Biến cào và các thao tác phức tạp

Các phép toán như a += (b + c) * 4không thể được tính mà không sử dụng các ô nhớ thêm. Trình biên dịch cấp cao xử lý vấn đề này bằng cách tách các hoạt động thành các phần khác nhau:

scratch_1 = b + c
scratch_1 = scratch_1 * 4
a = a + scratch_1

Điều này giới thiệu khái niệm về các biến số đầu được sử dụng để lưu trữ thông tin trung gian của các phép tính. Chúng được phân bổ theo yêu cầu và được phân bổ vào nhóm chung sau khi hoàn thành. Điều này làm giảm số lượng vị trí bộ nhớ đầu cần thiết để sử dụng. Biến cào được coi là toàn cầu.

Mỗi chương trình con có Var biếnStore riêng để giữ tham chiếu đến tất cả các biến mà chương trình con sử dụng cũng như loại của chúng. Vào cuối quá trình biên dịch, chúng được dịch thành các phần bù tương đối từ đầu cửa hàng và sau đó được cung cấp địa chỉ thực tế trong RAM.

Cấu trúc RAM

Program counter
Subroutine locals
Operator locals (reused throughout)
Scratch variables
Result variable
Stack pointer
Stack
...

Biên dịch cấp thấp

Những điều chỉ trình biên dịch ở mức thấp có để đối phó với những sub, call_sub, return, assign, ifwhile. Đây là một danh sách giảm các nhiệm vụ có thể được dịch sang các hướng dẫn QFTASM dễ dàng hơn.

sub

Điều này xác định vị trí bắt đầu và kết thúc của một chương trình con được đặt tên. Trình biên dịch cấp thấp thêm nhãn và trong trường hợp của mainchương trình con, thêm một lệnh thoát (nhảy đến cuối ROM).

ifwhile

Cả phiên dịch viên cấp thấp whileifcấp thấp đều khá đơn giản: họ nhận được con trỏ đến điều kiện của họ và nhảy tùy thuộc vào họ. whilecác vòng lặp hơi khác nhau ở chỗ chúng được biên dịch thành

...
condition
jump to check
code
condition
if condtion: jump to code
...

call_subreturn

Không giống như hầu hết các kiến ​​trúc, máy tính chúng tôi đang biên dịch không có hỗ trợ phần cứng để đẩy và bật từ ngăn xếp. Điều này có nghĩa là cả đẩy và bật từ ngăn xếp đều có hai hướng dẫn. Trong trường hợp xuất hiện, chúng tôi giảm con trỏ ngăn xếp và sao chép giá trị vào một địa chỉ. Trong trường hợp đẩy, chúng tôi sao chép một giá trị từ một địa chỉ sang địa chỉ tại con trỏ ngăn xếp hiện tại và sau đó tăng dần.

Tất cả các địa phương cho một chương trình con được lưu trữ tại một vị trí cố định trong RAM được xác định tại thời điểm biên dịch. Để làm cho đệ quy hoạt động, tất cả các địa phương cho một chức năng được đặt vào ngăn xếp khi bắt đầu cuộc gọi. Sau đó, các đối số cho chương trình con được sao chép vào vị trí của chúng trong cửa hàng địa phương. Giá trị của địa chỉ trả về được đặt vào ngăn xếp và chương trình con thực thi.

Khi gặp một returncâu lệnh, đỉnh của ngăn xếp được bật ra và bộ đếm chương trình được đặt thành giá trị đó. Các giá trị cho các địa phương của chương trình con gọi là bật ra khỏi ngăn xếp và vào vị trí trước đó của chúng.

assign

Các phép gán biến là những thứ dễ biên dịch nhất: chúng lấy một biến và một giá trị và biên dịch thành một dòng duy nhất: MLZ -1 VALUE VARIABLE

Chỉ định mục tiêu nhảy

Cuối cùng, trình biên dịch sẽ thực hiện các mục tiêu nhảy cho các nhãn được đính kèm theo hướng dẫn. Vị trí tuyệt đối của các nhãn được xác định và sau đó các tham chiếu đến các nhãn đó được thay thế bằng các giá trị đó. Các nhãn tự được xóa khỏi mã và cuối cùng số hướng dẫn được thêm vào mã được biên dịch.

Ví dụ từng bước biên dịch

Bây giờ chúng ta đã trải qua tất cả các giai đoạn, chúng ta hãy trải qua một quá trình biên dịch thực tế cho một chương trình thực tế, từng bước một.

#include stdint

sub main
    int a = 8
    int b = 12
    int c = a * b

Ok, đủ đơn giản. Nó nên được rõ ràng rằng ở phần cuối của chương trình, a = 8, b = 12, c = 96. Đầu tiên, hãy bao gồm các phần có liên quan của stdint.txt:

operator (int a + int b) -> int
    return __ADD__(a, b)

operator (int a - int b) -> int
    return __SUB__(a, b)

operator (int a < int b) -> bool
    bool rtn = 0
    rtn = __MLZ__(a-b, 1)
    return rtn

unsafe operator (int a * int b) -> int
    int rtn = 0
    for (int i = 0; i < b; i+=1)
        rtn += a
    return rtn

sub main
    int a = 8
    int b = 12
    int c = a * b

Ok, phức tạp hơn một chút. Hãy di chuyển lên mã thông báo và xem những gì phát ra. Ở giai đoạn này, chúng ta sẽ chỉ có một dòng mã thông báo tuyến tính mà không có bất kỳ hình thức cấu trúc nào

NAME NAME operator
LPAR OP (
NAME NAME int
NAME NAME a
PLUS OP +
NAME NAME int
NAME NAME b
RPAR OP )
OP OP ->
NAME NAME int
NEWLINE NEWLINE
INDENT INDENT     
NAME NAME return
NAME NAME __ADD__
LPAR OP (
NAME NAME a
COMMA OP ,
NAME NAME b
RPAR OP )
...

Bây giờ tất cả các mã thông báo được đưa qua trình phân tích cú pháp ngữ pháp và xuất ra một cây với tên của từng phần. Điều này cho thấy cấu trúc cấp cao như được đọc bởi mã.

GrammarTree file
 'stmts': [GrammarTree stmts_0
  '_block_name': 'inline'
  'inline': GrammarTree inline
   '_block_name': 'two_op'
   'type_var': GrammarTree type_var
    '_block_name': 'type'
    'type': 'int'
    'name': 'a'
    '_global': False

   'operator': GrammarTree operator
    '_block_name': '+'

   'type_var_2': GrammarTree type_var
    '_block_name': 'type'
    'type': 'int'
    'name': 'b'
    '_global': False
   'rtn_type': 'int'
   'stmts': GrammarTree stmts
    ...

Cây ngữ pháp này thiết lập thông tin sẽ được phân tích cú pháp bởi trình biên dịch cấp cao. Nó bao gồm các thông tin như kiểu cấu trúc và thuộc tính của một biến. Cây ngữ pháp sau đó lấy thông tin này và gán các biến cần thiết cho chương trình con. Cây cũng chèn tất cả các dòng.

('sub', 'start', 'main')
('assign', int main_a, 8)
('assign', int main_b, 12)
('assign', int op(*:rtn), 0)
('assign', int op(*:i), 0)
('assign', global bool scratch_2, 0)
('call_sub', '__SUB__', [int op(*:i), int main_b], global int scratch_3)
('call_sub', '__MLZ__', [global int scratch_3, 1], global bool scratch_2)
('while', 'start', 1, 'for')
('call_sub', '__ADD__', [int op(*:rtn), int main_a], int op(*:rtn))
('call_sub', '__ADD__', [int op(*:i), 1], int op(*:i))
('assign', global bool scratch_2, 0)
('call_sub', '__SUB__', [int op(*:i), int main_b], global int scratch_3)
('call_sub', '__MLZ__', [global int scratch_3, 1], global bool scratch_2)
('while', 'end', 1, global bool scratch_2)
('assign', int main_c, int op(*:rtn))
('sub', 'end', 'main')

Tiếp theo, trình biên dịch cấp thấp phải chuyển đổi biểu diễn mức cao này thành mã QFTASM. Các biến được chỉ định vị trí trong RAM như vậy:

int program_counter
int op(*:i)
int main_a
int op(*:rtn)
int main_c
int main_b
global int scratch_1
global bool scratch_2
global int scratch_3
global int scratch_4
global int <result>
global int <stack>

Các hướng dẫn đơn giản sau đó được biên soạn. Cuối cùng, số lệnh được thêm vào, dẫn đến mã QFTASM có thể thực thi được.

0. MLZ 0 0 0;
1. MLZ -1 12 11;
2. MLZ -1 8 2;
3. MLZ -1 12 5;
4. MLZ -1 0 3;
5. MLZ -1 0 1;
6. MLZ -1 0 7;
7. SUB A1 A5 8;
8. MLZ A8 1 7;
9. MLZ -1 15 0;
10. MLZ 0 0 0;
11. ADD A3 A2 3;
12. ADD A1 1 1;
13. MLZ -1 0 7;
14. SUB A1 A5 8;
15. MLZ A8 1 7;
16. MNZ A7 10 0;
17. MLZ 0 0 0;
18. MLZ -1 A3 4;
19. MLZ -1 -2 0;
20. MLZ 0 0 0;

Cú pháp

Bây giờ chúng ta đã có ngôn ngữ trần trụi, chúng ta thực sự phải viết một chương trình nhỏ trong đó. Chúng tôi đang sử dụng thụt lề như Python, tách các khối logic và điều khiển luồng. Điều này có nghĩa là khoảng trắng rất quan trọng đối với các chương trình của chúng tôi. Mỗi chương trình đầy đủ có một mainchương trình con hoạt động giống như main()hàm trong các ngôn ngữ giống như C. Các chức năng được chạy khi bắt đầu chương trình.

Biến và loại

Khi các biến được xác định lần đầu tiên, chúng cần phải có một loại liên kết với chúng. Các loại hiện được xác định là intboolvới cú pháp cho các mảng được xác định nhưng không phải là trình biên dịch.

Thư viện và nhà điều hành

Một thư viện được gọi stdint.txtlà có sẵn trong đó xác định các toán tử cơ bản. Nếu điều này không được bao gồm, ngay cả các toán tử đơn giản sẽ không được xác định. Chúng tôi có thể sử dụng thư viện này với #include stdint. stdintđịnh nghĩa các toán tử như +, >>và thậm chí *%, cả hai đều không phải là mã op QFTASM trực tiếp.

Ngôn ngữ cũng cho phép các mã op QFTASM được gọi trực tiếp với __OPCODENAME__.

Ngoài ra stdintđược định nghĩa là

operator (int a + int b) -> int
    return __ADD__(a, b)

Mà định nghĩa +toán tử làm gì khi cho hai ints.


1
Tôi có thể hỏi, tại sao nó lại quyết định tạo ra một CA giống như thế giới dây trong trò chơi cuộc sống của Conway và tạo ra một bộ xử lý mới bằng cách sử dụng mạch này thay vì sử dụng lại / trang bị thêm một máy tính phổ biến cgol như thế này không?
eaglgenes101

4
@ eaglgenes101 Đối với người mới bắt đầu, tôi không nghĩ rằng hầu hết chúng ta đều biết về sự tồn tại của các máy tính phổ dụng có thể sử dụng khác. Ý tưởng về một CA giống như thế giới dây với nhiều quy tắc hỗn hợp xuất hiện do kết quả của việc chơi xung quanh với siêu dữ liệu (tôi tin rằng - Phi là người đã đưa ra ý tưởng). Từ đó, đó là một sự tiến bộ hợp lý cho những gì chúng tôi tạo ra.
Mego
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.