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à MOV
và 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, MNZ
số tiền để kiểm tra xem có bất kỳ bit nào trong dữ liệu là 1 hay không, trong khi MLZ
số 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à MEZ
yêu cầu tạo tín hiệu TRUE từ tín hiệu trống, trong khi đó MGZ
là 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
, OR
và XOR
hướng dẫn làm những gì bạn mong đợi. Thay vì có một NOT
hướ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 NOT
hướ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 ANT
chỉ trả về 1 nếu bit đối số thứ nhất là 1 và bit đối số thứ hai là 0. Do đó, NOT x
tương đương với ANT -1 x
(cũng như XOR -1 x
). Hơn nữa, ANT
là 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 ( SL
và SRL
) đ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 3
trở 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(){}
và 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-else
câ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 MLZ
hướng dẫn được sử dụng để xử lý các bất đẳng thức như >
hoặc <=
. Một MNZ
lệ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 else
câ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 while
câ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 MLZ
hướ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 if
câu lệnh, một MNZ
lệ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 while
và do-while
là phần do-while
thâ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-while
câ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 alpha
chứ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. alpha
Biế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 index
là 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 sub
và 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 call
từ 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 quacall
cá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.