ส่วนที่ 4: QFTASM และ Cogol
ภาพรวมสถาปัตยกรรม
ในระยะสั้นคอมพิวเตอร์ของเรามีสถาปัตยกรรม RISC Harvard แบบอะซิงโครนัส 16 บิต เมื่อสร้างตัวประมวลผลด้วยมือสถาปัตยกรรม RISC ( ลดชุดคำสั่งคอมพิวเตอร์ ) นั้นเป็นสิ่งจำเป็นอย่างยิ่ง ในกรณีของเรานี้หมายความว่าจำนวนของรหัสที่มีขนาดเล็กและที่สำคัญกว่านั้นคือคำสั่งทั้งหมดจะถูกประมวลผลในลักษณะที่คล้ายกันมาก
สำหรับการอ้างอิงคอมพิวเตอร์ Wireworld ใช้สถาปัตยกรรม transport-triggeredซึ่งเป็นคำสั่งเดียวMOV
และการคำนวณถูกดำเนินการโดยการเขียน / อ่านรีจิสเตอร์พิเศษ แม้ว่ากระบวนทัศน์นี้จะนำไปสู่สถาปัตยกรรมที่ง่ายต่อการนำไปปฏิบัติ แต่ผลลัพธ์ก็ไม่สามารถใช้ได้ในแนวเขตแดน: การดำเนินการทางคณิตศาสตร์ / ตรรกะ / เงื่อนไขทั้งหมดต้องการสามคำสั่ง เห็นได้ชัดว่าเราต้องการสร้างสถาปัตยกรรมที่ลึกลับน้อยลง
เพื่อให้โปรเซสเซอร์ของเราง่ายขึ้นในขณะที่เพิ่มความสะดวกในการใช้งานเราได้ทำการตัดสินใจการออกแบบที่สำคัญหลายประการ:
- ไม่มีการลงทะเบียน ที่อยู่ใน RAM ทุกตัวจะได้รับการปฏิบัติอย่างเท่าเทียมกันและสามารถใช้เป็นอาร์กิวเมนต์สำหรับการดำเนินการใด ๆ ก็ได้ ในความหมายนี้หมายความว่า RAM ทั้งหมดสามารถปฏิบัติเช่นเดียวกับการลงทะเบียน ซึ่งหมายความว่าไม่มีคำแนะนำในการโหลด / ร้านค้าพิเศษ
- ในหลอดเลือดดำที่คล้ายกันหน่วยความจำแมป ทุกสิ่งที่สามารถเขียนหรืออ่านจากการแบ่งปันรูปแบบการกำหนดที่อยู่แบบครบวงจร ซึ่งหมายความว่าตัวนับโปรแกรม (PC) คือที่อยู่ 0 และความแตกต่างเพียงอย่างเดียวระหว่างคำสั่งปกติและคำแนะนำการควบคุมโฟลว์คือคำแนะนำการไหลของการควบคุมใช้ที่อยู่ 0
- ข้อมูลเป็นแบบอนุกรมในการส่งข้อมูลขนานในการจัดเก็บ เนื่องจากลักษณะของ "อิเล็กตรอน" เป็นพื้นฐานของคอมพิวเตอร์ของเราการเพิ่มและการลบจึงง่ายต่อการนำไปใช้อย่างมากเมื่อข้อมูลถูกส่งในรูปแบบอนุกรม little-endian (บิตที่มีนัยสำคัญน้อยที่สุด) นอกจากนี้ข้อมูลแบบอนุกรมยังช่วยลดความจำเป็นในการใช้บัสข้อมูลที่ยุ่งยากซึ่งมีทั้งความกว้างและความยุ่งยากในการใช้เวลาอย่างเหมาะสม (เพื่อให้ข้อมูลอยู่ด้วยกัน "เลน" ทั้งหมดของรถบัสจะต้องเดินทางล่าช้าเหมือนกัน)
- สถาปัตยกรรมฮาร์วาร์ดหมายถึงการแบ่งระหว่างโปรแกรมหน่วยความจำ (ROM) และหน่วยความจำข้อมูล (RAM) แม้ว่าสิ่งนี้จะลดความยืดหยุ่นของตัวประมวลผล แต่สิ่งนี้จะช่วยในเรื่องการปรับขนาดให้ดีที่สุด: ความยาวของโปรแกรมมีขนาดใหญ่กว่าจำนวน RAM ที่เราต้องการดังนั้นเราจึงสามารถแยกโปรแกรมออกเป็น ROM แล้วมุ่งเน้นไปที่การบีบอัด ROM ซึ่งง่ายกว่ามากเมื่ออ่านอย่างเดียว
- ความกว้างของข้อมูล 16 บิต นี่คือพลังที่เล็กที่สุดของสองที่กว้างกว่าบอร์ด Tetris มาตรฐาน (10 บล็อก) สิ่งนี้ทำให้เรามีช่วงข้อมูลที่ -32768 ถึง +32767 และความยาวโปรแกรมสูงสุดคือคำแนะนำ 65536 (2 ^ 8 = 256 คำสั่งนั้นเพียงพอสำหรับสิ่งที่ง่ายที่สุดที่เราอาจต้องการให้ตัวประมวลผลของเล่นทำ แต่ไม่ใช่ Tetris)
- การออกแบบแบบอะซิงโครนัส แทนที่จะมีนาฬิกากลาง (หรือหลายเท่าเท่ากัน) กำหนดเวลาของคอมพิวเตอร์ข้อมูลทั้งหมดจะมาพร้อมกับ "สัญญาณนาฬิกา" ซึ่งเดินทางไปพร้อมกับข้อมูลที่ไหลไปรอบ ๆ คอมพิวเตอร์ เส้นทางบางเส้นทางอาจสั้นกว่าเส้นทางอื่นและในขณะนี้อาจทำให้เกิดความยากลำบากสำหรับการออกแบบแบบรวมศูนย์การออกแบบแบบอะซิงโครนัสสามารถจัดการกับการดำเนินการตามตัวแปรเวลาได้อย่างง่ายดาย
- คำแนะนำทั้งหมดมีขนาดเท่ากัน เรารู้สึกว่าสถาปัตยกรรมที่คำสั่งแต่ละคำสั่งมี 1 ตัวเลือกที่มีตัวถูกดำเนินการ 3 ตัว (ปลายทางของค่าของมูลค่า) เป็นตัวเลือกที่ยืดหยุ่นที่สุด สิ่งนี้ครอบคลุมการดำเนินงานข้อมูลไบนารีรวมถึงการย้ายตามเงื่อนไข
- ระบบที่อยู่โหมดที่เรียบง่าย การมีโหมดการกำหนดแอดเดรสที่หลากหลายนั้นมีประโยชน์อย่างมากสำหรับการรองรับสิ่งต่าง ๆ เช่นอาร์เรย์หรือการเรียกซ้ำ เราจัดการเพื่อใช้โหมดการระบุที่สำคัญหลายประการด้วยระบบที่ค่อนข้างง่าย
ภาพประกอบของสถาปัตยกรรมของเราอยู่ในโพสต์ภาพรวม
ฟังก์ชั่นและการดำเนินงาน ALU
จากที่นี่มันเป็นเรื่องของการกำหนดฟังก์ชั่นที่โปรเซสเซอร์ของเราควรมี ความใส่ใจเป็นพิเศษคือความสะดวกในการใช้งานรวมถึงความสามารถรอบตัวของแต่ละคำสั่ง
การย้ายตามเงื่อนไข
การเคลื่อนไหวตามเงื่อนไขมีความสำคัญมากและทำหน้าที่เป็นทั้งการควบคุมการไหลขนาดเล็กและขนาดใหญ่ "Small-scale" หมายถึงความสามารถในการควบคุมการดำเนินการย้ายข้อมูลเฉพาะในขณะที่ "large-scale" หมายถึงการใช้เป็นการดำเนินการกระโดดแบบมีเงื่อนไขเพื่อถ่ายโอนโฟลว์ควบคุมไปยังโค้ดใด ๆ ไม่มีการดำเนินการกระโดดโดยเฉพาะเพราะเนื่องจากการแมปหน่วยความจำการย้ายแบบมีเงื่อนไขสามารถคัดลอกข้อมูลไปยัง RAM ปกติและคัดลอกที่อยู่ปลายทางไปยังพีซี นอกจากนี้เรายังเลือกที่จะละทิ้งการเคลื่อนไหวแบบไม่มีเงื่อนไขและการกระโดดแบบไม่มีเงื่อนไขด้วยเหตุผลที่คล้ายกัน: ทั้งคู่สามารถนำไปใช้เป็นการย้ายแบบมีเงื่อนไขโดยมีเงื่อนไขซึ่งเป็นรหัสที่ยากสำหรับ TRUE
เราเลือกที่จะมีการเคลื่อนไหวตามเงื่อนไขสองประเภท: "ย้ายหากไม่ใช่ศูนย์" ( MNZ
) และ "ย้ายถ้าน้อยกว่าศูนย์" ( MLZ
) ในทางปฏิบัติMNZ
จำนวนเงินที่จะตรวจสอบว่าบิตใด ๆ ของข้อมูลเป็น 1 ในขณะที่MLZ
จำนวนเงินที่จะตรวจสอบว่าบิตสัญญาณเป็น 1 พวกเขามีประโยชน์สำหรับความเท่าเทียมกันและการเปรียบเทียบตามลำดับ เหตุผลที่เราเลือกสองตัวนี้เหนือคนอื่นเช่น "ย้ายถ้าเป็นศูนย์" ( MEZ
) หรือ "ย้ายถ้ามากกว่าศูนย์" ( MGZ
) คือMEZ
ต้องสร้างสัญญาณ TRUE จากสัญญาณว่างขณะที่MGZ
เป็นการตรวจสอบที่ซับซ้อนมากขึ้น เครื่องหมายบิตเป็น 0 ในขณะที่อีกอย่างน้อยหนึ่งบิตเป็น 1
คณิตศาสตร์
คำแนะนำที่สำคัญที่สุดถัดไปในแง่ของแนวทางการออกแบบตัวประมวลผลคือการดำเนินการทางคณิตศาสตร์ขั้นพื้นฐาน ดังที่ฉันได้กล่าวไปแล้วก่อนหน้านี้เรากำลังใช้ข้อมูลอนุกรมแบบเอนด์เอนด์น้อยโดยมีตัวเลือกของความ endianness ที่กำหนดโดยความง่ายในการเพิ่ม / ลบ โดยการให้บิตที่มีนัยสำคัญน้อยมาถึงอันดับแรกหน่วยเลขคณิตสามารถติดตามบิตนำได้อย่างง่ายดาย
เราเลือกที่จะใช้การแทนส่วนประกอบ 2 ของตัวเลขลบเนื่องจากทำให้การบวกและการลบสอดคล้องกัน เป็นที่น่าสังเกตว่าคอมพิวเตอร์ Wireworld ใช้ส่วนประกอบ 1 อย่าง
การเพิ่มและการลบคือขอบเขตของการสนับสนุนทางคณิตศาสตร์พื้นเมืองของคอมพิวเตอร์ของเรา (นอกเหนือจากการเลื่อนบิตซึ่งจะกล่าวถึงในภายหลัง) การดำเนินการอื่น ๆ เช่นการคูณนั้นซับซ้อนเกินกว่าที่จะจัดการโดยสถาปัตยกรรมของเราและต้องนำไปใช้ในซอฟต์แวร์
การใช้งานระดับบิต
หน่วยประมวลผลของเรามีAND
, OR
และXOR
คำแนะนำที่ทำในสิ่งที่คุณคาดหวัง แทนที่จะมีNOT
คำสั่งเราเลือกที่จะมีคำสั่ง "และ - ไม่" ( ANT
) ความยากลำบากในการNOT
เรียนการสอนเป็นอีกครั้งที่จะต้องสร้างสัญญาณจากการขาดสัญญาณซึ่งเป็นเรื่องยากที่มีเซลลูลาร์ออโตมาตา ANT
คำแนะนำและผลตอบแทนที่ 1 เท่านั้นถ้าบิตอาร์กิวเมนต์แรกคือ 1 และบิตอาร์กิวเมนต์ที่สองเป็น 0 ดังนั้นNOT x
จะเทียบเท่ากับANT -1 x
(เช่นเดียวกับXOR -1 x
) นอกจากนี้ANT
มีความหลากหลายและมีข้อได้เปรียบหลักในการปิดบัง: ในกรณีของโปรแกรม Tetris ที่เราใช้เพื่อลบ tetrominoes
การขยับบิต
การดำเนินการเปลี่ยนบิตเป็นการดำเนินการที่ซับซ้อนที่สุดที่จัดการโดย ALU พวกเขาใช้สองอินพุตข้อมูล: ค่าที่จะเปลี่ยนและจำนวนเงินที่จะเปลี่ยนมัน แม้จะมีความซับซ้อน (เนื่องจากจำนวนตัวแปรที่เปลี่ยนแปลง) การดำเนินการเหล่านี้มีความสำคัญสำหรับงานที่สำคัญมากมายรวมถึงการดำเนินงาน "กราฟิก" จำนวนมากที่เกี่ยวข้องกับ Tetris การเลื่อนบิตจะทำหน้าที่เป็นพื้นฐานสำหรับอัลกอริทึมการคูณ / การหารที่มีประสิทธิภาพ
หน่วยประมวลผลของเรามีการดำเนินการกะสามบิต "shift left" ( SL
), "shift right logical" ( SRL
) และ "shift right arithmetic" ( SRA
) สองบิตแรกการเลื่อน ( SL
และSRL
) เติมบิตใหม่ด้วยค่าศูนย์ทั้งหมด (หมายความว่าจำนวนลบที่ถูกเลื่อนไปทางขวาจะไม่เป็นลบอีกต่อไป) หากอาร์กิวเมนต์ที่สองของการเปลี่ยนแปลงอยู่นอกช่วง 0 ถึง 15 ผลลัพธ์จะเป็นศูนย์ทั้งหมดตามที่คุณคาดหวัง สำหรับการเลื่อนบิตสุดท้ายSRA
บิตจะรักษาสัญญาณของอินพุตและดังนั้นจึงทำหน้าที่เป็นการหารที่แท้จริงโดยสอง
ท่อส่งคำสั่ง
ตอนนี้เป็นเวลาที่จะพูดคุยเกี่ยวกับรายละเอียดบางอย่างของสถาปัตยกรรม แต่ละรอบของ CPU ประกอบด้วยห้าขั้นตอนต่อไปนี้:
1. ดึงคำสั่งปัจจุบันจาก ROM
ค่าปัจจุบันของพีซีใช้สำหรับดึงคำสั่งที่สอดคล้องจาก ROM แต่ละคำสั่งมีหนึ่งรหัสและสามตัวถูกดำเนินการ แต่ละตัวถูกดำเนินการประกอบด้วยหนึ่งคำข้อมูลและโหมดที่อยู่หนึ่ง ส่วนเหล่านี้จะแยกออกจากกันตามที่อ่านจาก ROM
opcode คือ 4 บิตเพื่อรองรับ opcodes ที่ไม่ซ้ำกัน 16 อันโดยกำหนดให้ 11:
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. เขียนผลลัพธ์ (ถ้าจำเป็น) ของคำสั่งก่อนหน้านี้ไปยัง RAM
ขึ้นอยู่กับเงื่อนไขของคำสั่งก่อนหน้า (เช่นค่าของอาร์กิวเมนต์แรกสำหรับการย้ายตามเงื่อนไข) การเขียนจะถูกดำเนินการ ที่อยู่ของการเขียนจะถูกกำหนดโดยตัวถูกดำเนินการที่สามของคำสั่งก่อนหน้า
เป็นสิ่งสำคัญที่จะต้องทราบว่าการเขียนเกิดขึ้นหลังจากดึงคำแนะนำ สิ่งนี้นำไปสู่การสร้างช่วงการหน่วงเวลาของสาขาซึ่งคำสั่งทันทีหลังจากคำสั่ง Branch (การดำเนินการใด ๆ ที่เขียนไปยัง PC) จะถูกดำเนินการแทนคำสั่งแรกที่เป้าหมายสาขา
ในบางกรณี (เช่นการข้ามที่ไม่มีเงื่อนไข) สามารถหน่วงเวลาการหน่วงเวลาของสาขาได้ ในกรณีอื่น ๆ ไม่สามารถทำได้และคำแนะนำหลังจากสาขาต้องปล่อยว่างไว้ นอกจากนี้สล็อตความล่าช้าประเภทนี้หมายความว่าสาขาจะต้องใช้เป้าหมายสาขาที่เป็น 1 ที่อยู่น้อยกว่าคำสั่งเป้าหมายจริงเพื่อบัญชีสำหรับการเพิ่มขึ้นของพีซีที่เกิดขึ้น
กล่าวโดยย่อเนื่องจากผลลัพธ์ของคำสั่งก่อนหน้านี้ถูกเขียนไปที่ RAM หลังจากเรียกคำสั่งถัดไปการกระโดดตามเงื่อนไขจำเป็นต้องมีคำสั่งเปล่าหลังจากนั้นมิฉะนั้นพีซีจะไม่ได้รับการอัพเดตอย่างเหมาะสมสำหรับการกระโดด
3. อ่านข้อมูลสำหรับอาร์กิวเมนต์ของคำสั่งปัจจุบันจาก RAM
ดังที่ได้กล่าวไว้ก่อนหน้านี้ตัวถูกดำเนินการทั้งสามประกอบด้วยคำข้อมูลและโหมดการกำหนดแอดเดรส คำข้อมูลคือ 16 บิตความกว้างเท่ากับ RAM โหมดการกำหนดแอดเดรสคือ 2 บิต
โหมดการกำหนดแอดเดรสสามารถเป็นแหล่งของความซับซ้อนที่สำคัญสำหรับตัวประมวลผลเช่นนี้เนื่องจากโหมดการกำหนดแอดเดรสในโลกแห่งความจริงจำนวนมากเกี่ยวข้องกับการคำนวณหลายขั้นตอน (เช่นการเพิ่มออฟเซ็ต) ในเวลาเดียวกันโหมดการกำหนดแอดเดรสที่หลากหลายมีบทบาทสำคัญในการใช้งานโปรเซสเซอร์
เราพยายามที่จะรวมแนวคิดของการใช้หมายเลขตายตัวเป็นตัวถูกดำเนินการและใช้ที่อยู่ข้อมูลเป็นตัวถูกดำเนินการ สิ่งนี้นำไปสู่การสร้างโหมดการกำหนดแอดเดรสแบบตอบโต้: โหมดการกำหนดแอดเดรสของตัวถูกดำเนินการเป็นเพียงตัวเลขที่แสดงถึงจำนวนครั้งที่ข้อมูลควรถูกส่งไปรอบ ๆ ลูปการอ่าน RAM สิ่งนี้ครอบคลุมที่อยู่ทันทีทางตรงและทางอ้อมสองทาง
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)
หลังจากดำเนินการยกเลิกการลงทะเบียนแล้วตัวดำเนินการทั้งสามของคำสั่งจะมีบทบาทที่แตกต่างกัน ตัวถูกดำเนินการแรกมักจะเป็นอาร์กิวเมนต์แรกสำหรับตัวดำเนินการแบบไบนารี แต่ยังทำหน้าที่เป็นเงื่อนไขเมื่อคำสั่งปัจจุบันเป็นการย้ายแบบมีเงื่อนไข ตัวถูกดำเนินการที่สองทำหน้าที่เป็นอาร์กิวเมนต์ที่สองสำหรับผู้ประกอบการแบบไบนารี ตัวถูกดำเนินการตัวที่สามทำหน้าที่เป็นที่อยู่ปลายทางสำหรับผลลัพธ์ของคำสั่ง
เนื่องจากคำสั่งสองคำแรกทำหน้าที่เป็นข้อมูลในขณะที่คำสั่งที่สามทำหน้าที่เป็นที่อยู่โหมดการกำหนดแอดเดรสจึงมีการตีความที่แตกต่างกันเล็กน้อยขึ้นอยู่กับตำแหน่งที่ใช้ในตัวอย่างเช่นโหมดโดยตรงจะใช้ในการอ่านข้อมูลจากที่อยู่ RAM คงที่ จำเป็นต้องอ่าน RAM หนึ่งอัน) แต่ใช้โหมดทันทีเพื่อเขียนข้อมูลไปยังที่อยู่ RAM คงที่ (เนื่องจากไม่จำเป็นต้องอ่าน RAM)
4. คำนวณผลลัพธ์
opcode และตัวถูกดำเนินการสองตัวแรกจะถูกส่งไปยัง ALU เพื่อดำเนินการแบบไบนารี สำหรับการดำเนินการทางเลขคณิต bitwise และ shift หมายถึงการดำเนินการที่เกี่ยวข้อง สำหรับการย้ายแบบมีเงื่อนไขนี่หมายถึงการคืนค่าตัวถูกดำเนินการที่สอง
opcode และ operand แรกถูกใช้เพื่อคำนวณเงื่อนไขซึ่งกำหนดว่าจะเขียนผลลัพธ์ไปยังหน่วยความจำหรือไม่ ในกรณีของการย้ายแบบมีเงื่อนไขนี่หมายถึงการพิจารณาว่าบิตใด ๆ ในตัวถูกดำเนินการเป็น 1 (สำหรับMNZ
) หรือพิจารณาว่าบิตสัญญาณเป็น 1 (สำหรับMLZ
) หาก opcode ไม่ใช่การย้ายเงื่อนไขการเขียนจะดำเนินการเสมอ (เงื่อนไขเป็นจริงเสมอ)
5. เพิ่มตัวนับโปรแกรม
ในที่สุดตัวนับโปรแกรมจะถูกอ่านเพิ่มและเขียน
เนื่องจากตำแหน่งของการเพิ่ม PC ระหว่างคำสั่งการอ่านและการเขียนคำสั่งนั่นหมายความว่าคำสั่งที่เพิ่มพีซีโดย 1 คือ no-op คำสั่งที่คัดลอกพีซีไปยังตัวเองทำให้คำสั่งต่อไปที่จะดำเนินการสองครั้งในแถว แต่ได้รับคำเตือนคำแนะนำพีซีหลาย ๆ อันในแถวอาจทำให้เกิดเอฟเฟกต์ที่ซับซ้อนรวมถึงการวนลูปไม่สิ้นสุดหากคุณไม่ใส่ใจกับขั้นตอนการสอน
Quest for Tetris Assembly
เราสร้างภาษาแอสเซมบลีใหม่ที่ชื่อว่า QFTASM สำหรับโปรเซสเซอร์ของเรา ภาษาแอสเซมบลีนี้ตรงกับ 1-to-1 พร้อมกับรหัสเครื่องใน ROM ของคอมพิวเตอร์
โปรแกรม QFTASM ใด ๆ จะถูกเขียนเป็นชุดคำสั่งหนึ่งรายการต่อบรรทัด แต่ละบรรทัดมีรูปแบบดังนี้:
[line numbering] [opcode] [arg1] [arg2] [arg3]; [optional comment]
รายการ Opcode
ตามที่กล่าวไว้ก่อนหน้านี้คอมพิวเตอร์มีตัวรองรับสิบเอ็ดตัวซึ่งแต่ละตัวมีตัวถูกดำเนินการสามตัว:
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.
ที่อยู่โหมด
ตัวถูกดำเนินการแต่ละตัวมีทั้งค่าข้อมูลและการย้ายที่อยู่ ค่าข้อมูลถูกอธิบายด้วยตัวเลขทศนิยมในช่วง -32768 ถึง 32767 โหมดการกำหนดแอดเดรสถูกอธิบายโดยคำนำหน้าตัวอักษรหนึ่งตัวสำหรับค่าข้อมูล
mode name prefix
0 immediate (none)
1 direct A
2 indirect B
3 double-indirect C
รหัสตัวอย่าง
ลำดับ Fibonacci ในห้าบรรทัด:
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
รหัสนี้คำนวณลำดับ Fibonacci โดยที่อยู่ RAM 1 มีคำศัพท์ปัจจุบัน มันล้นอย่างรวดเร็วหลังจาก 28657
รหัสสีเทา:
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
โปรแกรมนี้คำนวณรหัสสีเทาและเก็บรหัสในที่อยู่ที่สำเร็จเริ่มต้นจากที่อยู่ 5 โปรแกรมนี้ใช้คุณสมบัติที่สำคัญหลายประการเช่นการกำหนดที่อยู่ทางอ้อมและการกระโดดตามเงื่อนไข มันจะหยุดเมื่อรหัสสีเทาผลลัพธ์คือ101010
ซึ่งเกิดขึ้นสำหรับอินพุต 51 ที่ที่อยู่ 56
ล่ามออนไลน์
El'endia Starman ได้สร้างล่ามออนไลน์ที่มีประโยชน์มากที่นี่ที่นี่คุณสามารถผ่านรหัสตั้งค่าเบรกพอยต์ดำเนินการเขียนด้วยตนเองไปที่ RAM และแสดงภาพ RAM เป็นจอแสดงผล
Cogol
เมื่อกำหนดสถาปัตยกรรมและภาษาแอสเซมบลีแล้วขั้นตอนต่อไปในด้าน "ซอฟต์แวร์" ของโครงการคือการสร้างภาษาระดับสูงขึ้นซึ่งเป็นสิ่งที่เหมาะสำหรับ Tetris ดังนั้นฉันจึงสร้างโคกอล ชื่อเป็นทั้งปุนบน "COBOL" และตัวย่อสำหรับ "C of Game of Life" แม้ว่ามันจะคุ้มค่าที่จะต้องทราบว่า Cogol คือ C สำหรับคอมพิวเตอร์ของเราคืออะไรกับคอมพิวเตอร์จริง
โคกอลอยู่ในระดับเหนือภาษาแอสเซมบลี โดยทั่วไปบรรทัดส่วนใหญ่ในโปรแกรม Cogol แต่ละตัวจะสอดคล้องกับชุดประกอบบรรทัดเดียว แต่มีคุณสมบัติที่สำคัญบางอย่างของภาษา:
- คุณสมบัติพื้นฐานประกอบด้วยตัวแปรที่มีชื่อพร้อมการมอบหมายและโอเปอเรเตอร์ที่มีไวยากรณ์ที่อ่านง่ายขึ้น ตัวอย่างเช่น
ADD A1 A2 3
จะกลายเป็นz = x + y;
กับตัวแปรการทำแผนที่คอมไพเลอร์ไปยังที่อยู่
- วนรอบโครงสร้างเช่น
if(){}
, while(){}
และdo{}while();
เพื่อให้คอมไพเลอร์ที่จับแตกแขนง
- อาร์เรย์หนึ่งมิติ (พร้อมตัวชี้เลขคณิต) ซึ่งใช้สำหรับบอร์ด Tetris
- รูทีนย่อยและ call stack สิ่งเหล่านี้มีประโยชน์ในการป้องกันการทำซ้ำของโค้ดขนาดใหญ่และเพื่อสนับสนุนการเรียกซ้ำ
คอมไพเลอร์ (ที่ฉันเขียนตั้งแต่เริ่มต้น) นั้นเรียบง่าย / ไร้เดียงสา แต่ฉันพยายามปรับแต่งโครงสร้างภาษาหลาย ๆ ภาษาเพื่อให้ได้ความยาวโปรแกรมที่คอมไพล์สั้น ๆ
ต่อไปนี้เป็นภาพรวมสั้น ๆ เกี่ยวกับคุณลักษณะของภาษาต่างๆ:
tokenization
ซอร์สโค้ดถูกโทเค็นเชิงเส้นตรง (single-pass) โดยใช้กฎง่าย ๆ เกี่ยวกับอักขระที่อนุญาตให้อยู่ติดกันภายในโทเค็น เมื่อพบอักขระที่ไม่สามารถติดกับอักขระตัวสุดท้ายของโทเค็นปัจจุบันโทเค็นปัจจุบันจะถือว่าเสร็จสมบูรณ์และอักขระใหม่จะเริ่มโทเค็นใหม่ อักขระบางตัว (เช่น{
หรือ,
) ไม่สามารถอยู่ติดกับตัวละครอื่น ๆ ได้ดังนั้นจึงเป็นโทเค็นของตัวเอง อื่น ๆ (เช่น>
หรือ=
) ที่ได้รับอนุญาตเท่านั้นที่จะอยู่ใกล้เคียงกับตัวละครอื่น ๆ ที่อยู่ในชั้นเรียนของตนและทำให้สามารถสร้างราชสกุลเช่น>>>
, ==
หรือแต่ไม่ชอบ>=
=2
อักขระช่องว่างบังคับให้มีขอบเขตระหว่างโทเค็น แต่จะไม่รวมอยู่ในผลลัพธ์ อักขระที่ยากที่สุดในการทำโทเค็นคือ-
เพราะมันสามารถเป็นตัวแทนของการลบและการปฏิเสธเอกภาพและดังนั้นจึงต้องมีปลอกพิเศษ
วจีวิภาค
การแยกวิเคราะห์ทำในแบบ single-pass คอมไพเลอร์มีวิธีการจัดการการสร้างภาษาที่แตกต่างกันและโทเค็นจะแตกออกจากรายการโทเค็นส่วนกลางตามที่ใช้โดยวิธีการคอมไพเลอร์ต่างๆ หากคอมไพเลอร์เคยเห็นโทเค็นที่ไม่ได้คาดไว้จะทำให้เกิดข้อผิดพลาดทางไวยากรณ์
การจัดสรรหน่วยความจำร่วม
คอมไพเลอร์กำหนดตัวแปรโกลบอลแต่ละตัว (คำหรืออาร์เรย์) RAM แอดเดรสที่กำหนดของตนเอง มีความจำเป็นต้องประกาศตัวแปรทั้งหมดโดยใช้คีย์เวิร์ดmy
เพื่อให้คอมไพเลอร์รู้ที่จะจัดสรรพื้นที่ให้ เย็นกว่าตัวแปรทั่วโลกที่มีชื่อคือการจัดการที่อยู่หน่วยความจำเริ่มต้น คำแนะนำจำนวนมาก (เงื่อนไขที่สะดุดตาและการเข้าถึงอาร์เรย์จำนวนมาก) ต้องใช้ที่อยู่ "เริ่มต้น" ชั่วคราวเพื่อจัดเก็บการคำนวณระดับกลาง ในระหว่างกระบวนการรวบรวมคอมไพเลอร์จะจัดสรรและยกเลิกการจัดสรรที่อยู่ในสครับตามความจำเป็น หากคอมไพเลอร์ต้องการที่อยู่ที่มีรอยขีดข่วนมากขึ้นก็จะอุทิศ RAM มากขึ้นเป็นที่อยู่ของรอยขีดข่วน ฉันเชื่อว่าเป็นเรื่องปกติสำหรับโปรแกรมที่จะต้องใช้เพียงไม่กี่ที่อยู่ในการลบถึงแม้ว่าที่อยู่ของแต่ละคนจะถูกนำมาใช้หลายครั้ง
IF-ELSE
งบ
ไวยากรณ์สำหรับif-else
คำสั่งเป็นรูปแบบ C มาตรฐาน:
other code
if (cond) {
first body
} else {
second body
}
other code
เมื่อแปลงเป็น QFTASM โค้ดจะถูกจัดเรียงดังนี้:
other code
condition test
conditional jump
first body
unconditional jump
second body (conditional jump target)
other code (unconditional jump target)
หากร่างแรกถูกดำเนินการส่วนที่สองจะถูกข้ามไป หากเนื้อหาแรกถูกข้ามไปเนื้อหาที่สองจะถูกดำเนินการ
ในแอสเซมบลีการทดสอบเงื่อนไขมักจะเป็นเพียงการลบและเครื่องหมายของผลลัพธ์จะเป็นตัวกำหนดว่าจะทำการกระโดดหรือดำเนินการกับร่างกาย MLZ
การเรียนการสอนจะใช้ในการจัดการกับความไม่เท่าเทียมกันเช่นหรือ>
การเรียนการสอนจะใช้ในการจัดการเพราะมันกระโดดทั่วร่างกายเมื่อความแตกต่างไม่ได้เป็นศูนย์ (และดังนั้นเมื่อการขัดแย้งจะไม่เท่ากัน) ไม่รองรับเงื่อนไขแบบหลายนิพจน์<=
MNZ
==
หากelse
ละเว้นคำสั่งการข้ามแบบไม่มีเงื่อนไขจะถูกข้ามด้วยเช่นกันและรหัส QFTASM มีลักษณะดังนี้:
other code
condition test
conditional jump
body
other code (conditional jump target)
WHILE
งบ
ไวยากรณ์สำหรับwhile
งบเป็นรูปแบบ C มาตรฐานด้วย:
other code
while (cond) {
body
}
other code
เมื่อแปลงเป็น QFTASM โค้ดจะถูกจัดเรียงดังนี้:
other code
unconditional jump
body (conditional jump target)
condition test (unconditional jump target)
conditional jump
other code
การทดสอบเงื่อนไขและการกระโดดตามเงื่อนไขอยู่ที่จุดสิ้นสุดของบล็อกซึ่งหมายความว่าจะถูกดำเนินการอีกครั้งหลังจากการดำเนินการของบล็อกแต่ละครั้ง เมื่อเงื่อนไขส่งคืนค่าเท็จร่างกายจะไม่ทำซ้ำและลูปสิ้นสุด ในระหว่างการเริ่มการทำงานลูปการไหลของการควบคุมจะกระโดดข้ามลูปบอดี้ไปยังรหัสเงื่อนไขดังนั้นร่างกายจะไม่ถูกดำเนินการหากเงื่อนไขเป็นเท็จในครั้งแรก
MLZ
การเรียนการสอนจะใช้ในการจัดการกับความไม่เท่าเทียมกันเช่นหรือ>
<=
ต่างจากif
คำแถลงMNZ
คำสั่งที่ใช้ในการจัดการ!=
เนื่องจากมันกระโดดไปที่ร่างกายเมื่อความแตกต่างไม่เป็นศูนย์ (ดังนั้นเมื่อข้อโต้แย้งไม่เท่ากัน)
DO-WHILE
งบ
ความแตกต่างเพียงอย่างเดียวระหว่างwhile
และdo-while
คือตัวdo-while
ลูปไม่ได้ข้ามไปในตอนแรกดังนั้นจึงถูกเรียกใช้งานอย่างน้อยหนึ่งครั้งเสมอ ฉันมักจะใช้do-while
คำสั่งเพื่อบันทึกรหัสแอสเซมบลีสองบรรทัดเมื่อฉันรู้ว่าลูปไม่จำเป็นต้องข้ามทั้งหมด
อาร์เรย์
อาร์เรย์หนึ่งมิติถูกนำไปใช้เป็นบล็อกหน่วยความจำต่อเนื่อง อาร์เรย์ทั้งหมดมีความยาวคงที่ตามการประกาศ มีการประกาศอาร์เรย์ดังนี้:
my alpha[3]; # empty array
my beta[11] = {3,2,7,8}; # first four elements are pre-loaded with those values
สำหรับอาร์เรย์นี่เป็นการจับคู่ RAM ที่เป็นไปได้ซึ่งแสดงว่าที่อยู่ 15-18 สำรองไว้สำหรับอาร์เรย์อย่างไร:
15: alpha
16: alpha[0]
17: alpha[1]
18: alpha[2]
ที่อยู่ที่มีป้ายกำกับalpha
จะเต็มไปด้วยตัวชี้ไปยังตำแหน่งของalpha[0]
ดังนั้นในกรณีที่อยู่ 15 นี้มีค่า 16 alpha
ตัวแปรสามารถใช้ภายในรหัส Cogol อาจเป็นตัวชี้สแต็กถ้าคุณต้องการใช้อาร์เรย์นี้เป็นกองซ้อน .
การเข้าถึงองค์ประกอบของอาร์เรย์ทำได้ด้วยarray[index]
สัญกรณ์มาตรฐาน หากค่าของindex
เป็นค่าคงที่การอ้างอิงนี้จะถูกเติมด้วยที่อยู่สัมบูรณ์ขององค์ประกอบนั้นโดยอัตโนมัติ มิฉะนั้นจะดำเนินการทางคณิตศาสตร์พอยน์เตอร์บางส่วน (เพียงแค่เพิ่ม) เพื่อค้นหาที่อยู่ที่แน่นอนที่ต้องการ alpha[beta[1]]
นอกจากนี้ยังเป็นไปได้ที่จะจัดทำดัชนีรังเช่น
รูทีนย่อยและการโทร
รูทีนย่อยเป็นกลุ่มของรหัสที่สามารถเรียกใช้จากบริบทหลาย ๆ อันเพื่อป้องกันการทำซ้ำของรหัสและอนุญาตให้สร้างโปรแกรมซ้ำ นี่คือโปรแกรมที่มีรูทีนย่อยแบบเรียกซ้ำเพื่อสร้างหมายเลขฟีโบนักชี (โดยทั่วไปเป็นอัลกอริธึมที่ช้าที่สุด):
# 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;
}
มีการประกาศรูทีนย่อยด้วยคีย์เวิร์ดsub
และสามารถวางรูทีนย่อยไว้ที่ใดก็ได้ภายในโปรแกรม แต่ละรูทีนย่อยสามารถมีตัวแปรโลคัลหลายตัวซึ่งถูกประกาศเป็นส่วนหนึ่งของรายการอาร์กิวเมนต์ ข้อโต้แย้งเหล่านี้ยังสามารถได้รับค่าเริ่มต้น
เพื่อจัดการกับการเรียกซ้ำ recursive ตัวแปรโลคัลของรูทีนย่อยจะถูกเก็บไว้ในสแต็ก ตัวแปรสแตติกสุดท้ายใน RAM คือตัวชี้สแต็กการโทรและหน่วยความจำทั้งหมดหลังจากนั้นทำหน้าที่เป็นสแต็กการโทร เมื่อเรียกรูทีนย่อยมันจะสร้างเฟรมใหม่บน call stack ซึ่งรวมถึงตัวแปรโลคัลทั้งหมดรวมถึงแอดเดรส return (ROM) แต่ละรูทีนย่อยในโปรแกรมจะได้รับที่อยู่ RAM แบบคงที่เดียวเพื่อใช้เป็นตัวชี้ ตัวชี้นี้ให้ตำแหน่งของการเรียก "ปัจจุบัน" ของรูทีนย่อยใน call stack การอ้างอิงตัวแปรโลคัลถูกดำเนินการโดยใช้ค่าของตัวชี้แบบสแตติกนี้บวกกับออฟเซ็ตเพื่อให้ที่อยู่ของตัวแปรโลคอลนั้น ๆ ยังมีอยู่ใน call stack เป็นค่าก่อนหน้าของตัวชี้คงที่ นี่'
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
สิ่งหนึ่งที่น่าสนใจเกี่ยวกับรูทีนย่อยคือพวกมันไม่คืนค่าใด ๆ แต่จะสามารถอ่านตัวแปรโลคัลของรูทีนย่อยทั้งหมดได้หลังจากดำเนินการรูทีนย่อยเพื่อดึงข้อมูลที่หลากหลายจากการเรียกรูทีนย่อย สิ่งนี้สามารถทำได้โดยการเก็บตัวชี้สำหรับการเรียกรูทีนย่อยเฉพาะนั้นซึ่งสามารถใช้เพื่อกู้คืนตัวแปรโลคอลใด ๆ จากภายในกรอบสแต็ก (เพิ่งถูกจัดสรรคืน)
มีหลายวิธีในการเรียกรูทีนย่อยทั้งหมดโดยใช้call
คำหลัก:
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
สามารถกำหนดค่าจำนวนเท่าใดก็ได้เป็นอาร์กิวเมนต์สำหรับการเรียกรูทีนย่อย อาร์กิวเมนต์ใด ๆ ที่ไม่ได้ระบุจะถูกกรอกด้วยค่าเริ่มต้นหากมี อาร์กิวเมนต์ที่ไม่ได้จัดเตรียมไว้และไม่มีค่าเริ่มต้นจะไม่ถูกล้างออก (เพื่อบันทึกคำแนะนำ / เวลา) ดังนั้นอาจเกิดขึ้นกับค่าใด ๆ ที่จุดเริ่มต้นของรูทีนย่อย
พอยน์เตอร์เป็นวิธีการเข้าถึงตัวแปรย่อยหลายตัวของรูทีนย่อยแม้ว่าจะเป็นสิ่งสำคัญที่จะต้องทราบว่าตัวชี้เป็นเพียงชั่วคราว: ข้อมูลที่ตัวชี้ชี้จะถูกทำลายเมื่อมีการเรียกรูทีนย่อยอื่น
การดีบักป้ายกำกับ
{...}
โค้ดบล็อกใด ๆในโปรแกรม Cogol สามารถนำหน้าด้วยเลเบลอธิบายหลายคำ ป้ายกำกับนี้ถูกแนบเป็นความคิดเห็นในรหัสแอสเซมบลีที่รวบรวมและอาจมีประโยชน์มากสำหรับการตรวจแก้จุดบกพร่องเนื่องจากทำให้ง่ายต่อการค้นหาตำแหน่งของรหัสเฉพาะ
การเพิ่มประสิทธิภาพสล็อตสาขาล่าช้า
เพื่อที่จะปรับปรุงความเร็วของโค้ดที่คอมไพล์แล้วคอมไพเลอร์ Cogol จะทำการเพิ่มประสิทธิภาพการหน่วงเวลาสล็อตขั้นพื้นฐานบางอย่างเป็นการส่งผ่านรหัส QFTASM ขั้นสุดท้าย สำหรับการกระโดดที่ไม่มีเงื่อนไขใด ๆ กับสล็อตการหน่วงเวลาสาขาที่ว่างเปล่าสล็อตการหน่วงเวลาสามารถถูกเติมด้วยคำสั่งแรกที่ปลายทางการกระโดดและปลายทางการกระโดดจะเพิ่มขึ้นหนึ่งเพื่อชี้ไปที่คำสั่งถัดไป โดยทั่วไปจะบันทึกหนึ่งรอบในแต่ละครั้งที่ดำเนินการกระโดดแบบไม่มีเงื่อนไข
การเขียนโค้ด Tetris ใน Cogol
โปรแกรม Tetris สุดท้ายที่ถูกเขียนใน Cogol และรหัสที่มาสามารถใช้ได้ที่นี่ รหัส QFTASM รวบรวมใช้ได้ที่นี่ เพื่อความสะดวกคุณสามารถดู Permalink ได้ที่นี่: Tetris ใน QFTASM QFTASM เนื่องจากเป้าหมายคือการตีกอล์ฟรหัสการประกอบ (ไม่ใช่รหัสโคกอล) รหัสโคกอลที่เป็นผลลัพธ์จึงไม่น่าเชื่อถือ หลาย ๆ ส่วนของโปรแกรมโดยปกติจะอยู่ในรูทีนย่อย แต่รูทีนย่อยเหล่านั้นสั้นมากพอที่จะทำซ้ำคำสั่งที่บันทึกรหัสไว้บนcall
งบ รหัสสุดท้ายมีเพียงหนึ่งรูทีนย่อยนอกเหนือจากรหัสหลัก นอกจากนี้อาร์เรย์จำนวนมากจะถูกลบและแทนที่ด้วยรายการของตัวแปรแต่ละตัวที่มีความยาวเท่ากันหรือโดยตัวเลขจำนวนมากในโปรแกรม รหัส QFTASM ที่คอมไพล์ครั้งสุดท้ายอยู่ภายใต้ 300 คำสั่งถึงแม้ว่ามันจะมีความยาวมากกว่าแหล่ง Cogol เพียงเล็กน้อยเท่านั้น