COMPUTER ORGANIZATION · LC-3
สรุปเนื้อหา LC-3 บทที่ 5 ถึง 9
ตั้งแต่ชุดคำสั่งของเครื่อง ไปจนถึงการเขียนโปรแกรม แอสเซมบลี การติดต่ออุปกรณ์ภายนอก และการเรียกใช้ Subroutine
บทที่ 5
LC-3: Instruction Set Architecture
ISA คือส่วนทั้งหมดของคอมพิวเตอร์ที่โปรแกรมเมอร์มองเห็นและควบคุมได้ ประกอบด้วยหน่วยความจำ รีจิสเตอร์ และชุดคำสั่ง
ภาพรวมของ LC-3
หน่วยความจำ
216 ตำแหน่ง ที่อยู่ 16 บิต และแต่ละตำแหน่งเก็บข้อมูล 16 บิต
รีจิสเตอร์ทั่วไป
R0 ถึง R7 กว้าง 16 บิตแต่ละตัว ใช้ 3 บิตในการระบุ
รีจิสเตอร์พิเศษ
PC (Program Counter) และ Condition Code ได้แก่ N, Z, P
ชนิดข้อมูล
จำนวนเต็ม 16 บิตแบบ 2's complement
กลุ่มของ Opcode (15 ตัว)
| กลุ่ม | คำสั่ง | หน้าที่ |
|---|---|---|
| Operate | ADD, AND, NOT | คำนวณระหว่างรีจิสเตอร์ ไม่เข้าถึงหน่วยความจำ |
| Data movement | LD, LDI, LDR, LEAST, STI, STR | อ่านและเขียนข้อมูลระหว่างหน่วยความจำกับรีจิสเตอร์ LEA คำนวณที่อยู่และเก็บลงรีจิสเตอร์ แต่ไม่อ่านหน่วยความจำ |
| Control | BR, JMP, JSR, JSRR, RTI, TRAP | เปลี่ยนลำดับการทำงานของโปรแกรม |
Operate Instructions
มีเพียง ADD, AND และ NOT โดยต้นทางและปลายทางเป็นรีจิสเตอร์ ADD และ AND ใช้ค่าคงที่แบบ immediate ได้ ค่า immediate จะถูก sign-extend
เนื่องจากชุดคำสั่งมีน้อย การทำงานบางอย่างจึงต้องใช้เทคนิค เช่น
- การลบ: ใช้ NOT แล้วบวก 1 (2's complement)
- การล้างรีจิสเตอร์ให้เป็นศูนย์:
AND R2, R2, #0 - การคัดลอกค่า:
ADD R2, R1, #0
โหมดการกำหนดที่อยู่ (Addressing Modes)
| โหมด | ใช้กับคำสั่ง | ลักษณะ |
|---|---|---|
| Immediate | ADD, AND | ค่าอยู่ในคำสั่งเลย |
| Register | ADD, AND, NOT, JMP | ค่าอยู่ในรีจิสเตอร์ |
| PC-relative | LD, ST, LEA, BR | PC + offset 9 บิต (ช่วง −256 ถึง +255) PC ถูกเพิ่มค่าไปแล้วในขั้น FETCH ก่อนคำนวณที่อยู่ LEA ใช้โหมดนี้คำนวณที่อยู่แล้วเก็บลงรีจิสเตอร์ โดยไม่อ่านหน่วยความจำ |
| Indirect | LDI, STI | อ่านที่อยู่จากหน่วยความจำก่อน แล้วค่อยเข้าถึงข้อมูลที่อยู่นั้น |
| Base + Offset | LDR, STR | รีจิสเตอร์ + offset 6 บิต เข้าถึงได้ทั้งหน่วยความจำ |
Condition Codes และ Branch
รีจิสเตอร์ N, Z, P จะถูกตั้งค่าตามผลลัพธ์ของคำสั่งที่เขียนค่าลงรีจิสเตอร์ ได้แก่ ADD, AND, NOT, LD, LDI, LDR และ LEA โดยมีเพียงบิตเดียวที่เป็น 1 เสมอ
BR จะกระโดดไปยังเป้าหมายเมื่อเงื่อนไขที่ระบุเป็นจริง เป้าหมายต้องอยู่ในช่วง 256 คำจากคำสั่ง BR
; ตัวอย่าง: รวมเลข 12 ตัวที่เริ่มที่ x3100 (ตั้ง R1 = x3100 ก่อนเข้าลูป)
AND R3, R3, #0 ; R3 = ผลรวม
AND R2, R2, #0 ; R2 = 0\n ADD R2, R2, #12 ; R2 = จำนวนตัวเลข (12)
LOOP LDR R4, R1, #0 ; R4 = M[R1]
ADD R3, R3, R4 ; R3 += R4
ADD R1, R1, #1 ; ชี้ไปตัวถัดไป
ADD R2, R2, #-1 ; นับถอยหลัง
BRp LOOP ; วนต่อถ้า R2 > 0
ลองทำด้วยตัวเอง
ลบด้วย NOT + 1
เนื่องจาก LC-3 ไม่มีคำสั่งลบ จึงต้องแปลง −B เป็น NOT B แล้วบวก 1 ลองใส่ค่าดูได้
ตรวจสอบว่า BR จะกระโดดหรือไม่
เลือกบิต n z p ที่ตั้งไว้ในคำสั่ง BR และค่า Condition Code ปัจจุบัน
Data Path (ภาพรวมฮาร์ดแวร์)
- Global bus เส้นทางข้อมูล 16 บิตที่ต่อกับทุกส่วน ในแต่ละช่วงมีเพียงหนึ่งตัวที่ส่งสัญญาณลงบัส
- ALU ทำ ADD, AND, NOT และรับค่าจาก register file หรือ immediate
- Register file อ่านได้สองตัวพร้อมกัน (SR1, SR2) และเขียนได้หนึ่งตัว (DR)
- PC และ MAR มี MUX เลือกแหล่งที่มาของค่า
- Control unit เป็น state machine ที่ควบคุมว่าใครขับบัส และรีจิสเตอร์ใดรับค่าในแต่ละรอบ
ตัวอย่างคำถามทบทวน
ถ้า bit [11:9] ของ BR เป็น 0 ทั้งหมด หรือเป็น 1 ทั้งหมด จะเกิดอะไรขึ้น
เลข nzp = 000 หมายถึงไม่มีเงื่อนไขใดถูกระบุ คำสั่งจึงไม่มีวันกระโดด ส่วน nzp = 111 หมายถึงกระโดดเสมอ (unconditional)
บทที่ 6
Programming: การแก้ปัญหาและดีบักโปรแกรม
ขั้นตอนการแก้ปัญหาด้วยคอมพิวเตอร์คือแปลงโจทย์เป็นอัลกอริทึม แล้วแปลงอัลกอริทึมเป็นคำสั่ง LC-3 และเมื่อโปรแกรมไม่ทำงาน ก็ต้องดีบักอย่างเป็นระบบ
Stepwise Refinement
เทคนิคการแบ่งงานใหญ่ออกเป็นงานย่อยเรื่อย ๆ จนถึงระดับคำสั่งเครื่อง เริ่มจากโจทย์ เช่น "นับจำนวนครั้งที่ตัวอักษรปรากฏในไฟล์" แล้วแตกเป็นขั้นย่อย เช่น รับอักขระ นับ และแสดงผล
ตัวอย่างการ refine งาน B (สแกนไฟล์และนับตัวที่ตรงกัน) ได้โครงสร้างแบบ iterative ที่วนจนกว่าจะหมดไฟล์ ภายในลูปแยกเป็นงานย่อย B2 ที่เป็น conditional (ตรวจว่าตรงกันหรือไม่) และ B3 ที่เป็น sequential (ขยับตัวชี้แล้วโหลดตัวถัดไป) จากนั้นจึงแปลแต่ละงานย่อยเป็นคำสั่ง LC-3
โครงสร้างพื้นฐานสามแบบ
Sequential
ทำงาน A ให้เสร็จ แล้วทำงาน B ต่อ คำว่า "ทำ A แล้วทำ B"
Conditional
ถ้าเงื่อนไขเป็นจริงทำ A ไม่เช่นนั้นทำ B คำว่า "ถ้า G ให้ทำ H"
Iterative
ทำซ้ำตราบใดที่เงื่อนไขเป็นจริง คำว่า "สำหรับแต่ละ X ให้ทำ Y" หรือ "ทำ Z จนกว่า W"
การเข้ารหัสเงื่อนไขด้วย LC-3
Sequential ไม่ต้องใช้คำสั่งพิเศษ เพราะคำสั่งเรียงต่อกันอยู่แล้ว ส่วน Conditional และ Iterative ต้องทำให้เงื่อนไขกลายเป็นค่า N, Z หรือ P ก่อน เช่น เทียบ R0 กับ R1 ด้วยการลบ ถ้าเท่ากัน Z จะเป็น 1 แล้วใช้ BR เพื่อส่งการควบคุมไปยังงานย่อยที่ถูกต้อง
ตัวอย่าง: นับตัวอักษรในไฟล์
ตัวอย่างนี้แบ่งงานเป็น 3 ส่วนแบบ sequential คือ เตรียมค่า (A) นับตัวอักษร (B) และแสดงผล (C) ส่วน B เป็น iterative ที่ตรวจทีละตัว และใช้ conditional ตรวจว่าตรงกันหรือไม่
; B2: ตรวจว่าตัวอักษรตรงกับที่ต้องการหรือไม่
NOT R1, R1 ; กลับบิตของตัวอักษรในไฟล์
ADD R1, R1, #1 ; R1 = −(ตัวอักษรในไฟล์)\n ADD R1, R1, R0 ; R1 = R0 − ตัวอักษรในไฟล์ (0 ถ้าตรงกัน)
BRnp B3 ; ไม่ตรง ข้ามการเพิ่มค่านับ
ADD R2, R2, #1 ; ตรงกัน เพิ่ม count
B3 ADD R3, R3, #1 ; ขยับ pointer ไปตัวถัดไป
LDR R1, R3, #0 ; โหลดตัวอักษรถัดไป
เดินตามรอยโปรแกรมคูณ (R2 = R4 × R5)
เริ่มต้น R4 = 10, R5 = 3 เลือกเงื่อนไข branch แล้วกดถัดไปทีละรอบ เปรียบเทียบว่าเงื่อนไขไหนให้ผลถูก
Debugging: ตามรอยโปรแกรม
การดีบักเปรียบได้กับการดูแผนที่เมื่อหลงทาง คือตรวจลำดับคำสั่งที่ทำงานจริง บันทึกผลลัพธ์แต่ละขั้น แล้วเทียบกับผลที่คาดไว้
| เครื่องมือ | หน้าที่ |
|---|---|
| Single-stepping | รันทีละคำสั่ง ช้าแต่ละเอียด |
| Breakpoint | หยุดเมื่อถึงคำสั่งที่กำหนด เพื่อตรวจผลลัพธ์ตรงจุดนั้น |
| Watchpoint | หยุดเมื่อรีจิสเตอร์หรือหน่วยความจำเปลี่ยนค่า หรือเท่ากับค่าที่กำหนด |
ประเภทของข้อผิดพลาด
- Syntax error พิมพ์ผิดจนเป็นคำสั่งที่ไม่ถูกต้อง มักถูกจับได้ตอนแปลโค้ด
- Logic error โค้ดถูกต้องทางไวยากรณ์ แต่ทำงานไม่ตรงกับโจทย์ ต้องตามรอยโปรแกรม
- Data error ข้อมูลนำเข้าไม่เป็นไปตามที่คาดไว้ ต้องทดสอบด้วยข้อมูลหลากหลาย
บทเรียนจากตัวอย่างดีบัก
- โปรแกรมคูณ ผลลัพธ์ได้ 40 แทนที่จะเป็น 30 เพราะคำสั่ง branch ใช้เงื่อนไข zp ทำให้ตอนที่ค่าเป็นศูนย์ยังวนต่ออีกหนึ่งรอบ ควรวนต่อเฉพาะเมื่อยังไม่เป็นศูนย์ (np)
- โปรแกรมรวมอาร์เรย์ ได้ผลผิด เพราะใช้
LDโหลด "เนื้อหา" ของ x3100 แทนที่จะเป็นที่อยู่ x3100 ควรเปลี่ยนเป็นLEA - โปรแกรมหาเลข 5 branch ไม่ทำงานตามที่คาด เพราะค่า condition code มาจากการโหลด R2 ไม่ใช่จากการลด R3
- โปรแกรมหาบิต 1 ตัวแรก ค้างเมื่อข้อมูลเป็นศูนย์ เกิด infinite loop ต้องเพิ่มการตรวจกรณีพิเศษ
ตัวอย่างคำถามทบทวน
ทำไมการคิดให้ชัดก่อนเขียนโค้ดถึงช่วยลดเวลาดีบัก
เพราะถ้าอัลกอริทึมถูกต้องและแบ่งงานชัดเจน ข้อผิดพลาดที่เหลือส่วนใหญ่จะเป็นเรื่องการพิมพ์หรือการใช้คำสั่ง ซึ่งจับได้ง่ายกว่ามาก
บทที่ 7
Assembly Language: ภาษาแอสเซมบลี
แอสเซมเบลอร์คือโปรแกรมที่แปลงสัญลักษณ์ที่มนุษย์อ่านได้ เช่น ADD R6,R2,R6 ให้เป็นเลขฐานสองที่เครื่องรันได้
รูปแบบของคำสั่งแอสเซมบลี
LABEL OPCODE OPERANDS ; COMMENTS
(ไม่บังคับ) (บังคับ)
- Label ชื่อสัญลักษณ์ที่ตำแหน่งนั้น ต้องอยู่ต้นบรรทัด
- Opcode คำสั่งหรือ directive เช่น ADD, LD, .FILL
- Operands รีจิสเตอร์ (R0–R7), ตัวเลข (
#ทศนิยม,xฐานสิบหก) หรือ label คั่นด้วยคอมมา - Comment ทุกอย่างหลัง
;แอสเซมเบลอร์จะไม่สนใจ
ช่องว่างระหว่างสัญลักษณ์และตัวพิมพ์เล็กใหญ่ไม่มีผลต่อการทำงาน
Assembler Directives (Pseudo-ops)
เป็นคำสั่งที่ใช้กับแอสเซมเบลอร์เอง ไม่ได้ทำงานตอนโปรแกรมรัน
| Directive | ความหมาย |
|---|---|
.ORIG address | กำหนดที่อยู่เริ่มต้นของโปรแกรม |
.END | จุดจบของโปรแกรม |
.BLKW n | จองพื้นที่ n คำ |
.FILL value | จองหนึ่งคำและกำหนดค่าเริ่มต้น |
.STRINGZ "string" | จองพื้นที่สำหรับสตริงพร้อม null terminator |
Trap Pseudo-instructions
แอสเซมเบลอร์มีชื่อคำสั่งสำเร็จรูปให้กับ TRAP แต่ละตัว จึงไม่ต้องจำหมายเลข
| ชื่อ | เทียบเท่า | หน้าที่ |
|---|---|---|
GETC | TRAP x20 | อ่านอักขระจากคีย์บอร์ด (ไม่แสดงผล) เก็บใน R0[7:0] |
OUT | TRAP x21 | แสดงอักขระใน R0[7:0] ออกทางจอ |
PUTS | TRAP x22 | แสดงสตริง โดย R0 เก็บที่อยู่ของสตริง |
IN | TRAP x23 | แสดงข้อความ ให้ผู้ใช้พิมพ์ แล้วอ่านและแสดงอักขระ |
HALT | TRAP x25 | หยุดการทำงานและแสดงข้อความ |
แนวทางการเขียนให้อ่านง่าย
- ใส่ header ของโปรแกรม ระบุชื่อผู้เขียน วันที่ และจุดประสงค์
- จัดคอลัมน์ label, opcode, operands และ comment ให้ตรงกันทุกบรรทัด
- คอมเมนต์ว่ารีจิสเตอร์แต่ละตัวทำหน้าที่อะไร
- ตั้งชื่อ label ให้สื่อความหมาย เช่น
ASCIItoBinary,SaveR1 - คอมเมนต์ "เพิ่มข้อมูล" ไม่ใช่ "ทวนสิ่งที่โค้ดทำ" เช่น ไม่ควรเขียนว่า "decrement R1" แต่ควรบอกว่า "R1 เก็บจำนวนรอบ"
- ไม่ให้บรรทัดยาวเกินหน้ากระดาษ
กระบวนการแปลงโค้ด 2 รอบ (Two-pass Assembly)
รอบที่ 1: สร้าง Symbol Table
อ่าน .ORIG เพื่อรู้ที่อยู่เริ่ม แล้วนับ location counter ทีละบรรทัด เก็บ label พร้อมที่อยู่ของมัน ถ้าเป็น .BLKW หรือ .STRINGZ ก็เพิ่มตามจำนวนคำที่จอง
รอบที่ 2: สร้างเลขฐานสอง
แปลงแต่ละคำสั่งเป็นเลขฐานสอง โดยแทนที่ label ด้วยที่อยู่จาก symbol table
จำลองแอสเซมเบลอร์รอบที่ 1 และ 2
กด ขั้นถัดไป เพื่อดูว่า location counter (LC) เพิ่มขึ้นอย่างไร และ label ถูกบันทึกที่ตำแหน่งใด
ข้อผิดพลาดที่พบบ่อย ได้แก่ จำนวนหรือชนิด operand ไม่ถูกต้อง ค่า immediate เกินขนาด และ label ที่อยู่ห่างเกิน 256 คำ จนใช้ PC-relative ไม่ได้
Object File และการโหลดโปรแกรม
ไฟล์ object ที่ใช้โหลดเข้าเครื่องจำลองประกอบด้วยที่อยู่เริ่มต้นที่โปรแกรมต้องถูกโหลด ตามด้วยคำสั่งเป็นเลขฐานสอง
- โค้ดระบบอยู่ต่ำกว่า x3000 ส่วนโค้ดผู้ใช้ควรอยู่ระหว่าง x3000 ถึง xFDFF
- เมื่อโหลดหลายไฟล์ ระวังไม่ให้ที่อยู่ทับกัน
ตัวอย่างไฟล์ object ส่วนต้นของโปรแกรมนับตัวอักษร (ซ้ายคือเลขฐานสอง ขวาคือคำสั่งที่เทียบเท่า)
0011000000000000 .ORIG x3000
0101010010100000 AND R2, R2, #0
0010011000010001 LD R3, PTR
1111000000100011 TRAP x23
Loading และ Linking
- Loading คือการคัดลอกภาพโปรแกรมเข้าหน่วยความจำ ตัวโหลดที่ซับซ้อนสามารถย้ายตำแหน่งได้ และต้องปรับ branch target กับที่อยู่ของ load/store ด้วย
- Linking คือการเชื่อมสัญลักษณ์ระหว่างไฟล์ object ที่แยกกัน เช่น ใช้
.EXTERNALเพื่อบอกว่า label นี้ถูกนิยามอยู่ในโมดูลอื่น
ตัวอย่างคำถามทบทวน
ทำไมต้องใช้แอสเซมเบลอร์แบบสองรอบ
เพราะคำสั่งอาจอ้างถึง label ที่ประกาศอยู่ทีหลัง รอบแรกจึงต้องรวบรวมที่อยู่ของ label ทั้งหมดไว้ก่อน รอบที่สองจึงแทนค่าได้ครบ
บทที่ 8
I/O: การติดต่อกับโลกภายนอก
โปรแกรมต้องรับข้อมูลจากอุปกรณ์ภายนอก เช่น คีย์บอร์ด และส่งข้อมูลออกไป เช่น จอภาพ บทนี้อธิบายว่าเครื่องติดต่อกับอุปกรณ์เหล่านั้นอย่างไร
ประเภทและอัตราการส่งข้อมูลของอุปกรณ์
| ประเภท | ตัวอย่าง | อัตราโดยประมาณ |
|---|---|---|
| Input | คีย์บอร์ด, motion detector | คีย์บอร์ดประมาณ 100 bytes/s |
| Output | จอภาพ, เครื่องพิมพ์ | ขึ้นกับอุปกรณ์ |
| Storage | ดิสก์, CD-ROM | ดิสก์ประมาณ 30 MB/s |
| Network | การ์ดเครือข่าย | 1 Mb/s ถึง 1 Gb/s |
I/O Controller
แต่ละอุปกรณ์มีรีจิสเตอร์ 3 ประเภทที่ CPU ใช้ควบคุม
- Control/Status register CPU สั่งงานโดยเขียนลง control register และตรวจว่างานเสร็จหรือยังโดยอ่าน status register
- Data register ใช้ส่งข้อมูลเข้าหรือออกจากอุปกรณ์
- Device electronics ส่วนที่ทำงานจริง เช่น ส่งพิกเซลขึ้นจอ หรืออ่านอักขระจากคีย์บอร์ด
รูปแบบการเชื่อมต่อ
| คำถาม | ตัวเลือก |
|---|---|
| รีจิสเตอร์ของอุปกรณ์ถูกระบุอย่างไร | Memory-mapped (กำหนดที่อยู่หน่วยความจำให้รีจิสเตอร์) หรือใช้คำสั่งพิเศษ |
| จังหวะการส่งข้อมูลถูกจัดการอย่างไร | Synchronous (รู้จังหวะที่แน่นอน) หรือ Asynchronous (ต้องซิงโครไนซ์ด้วยรีจิสเตอร์สถานะ) |
| ใครเป็นผู้ควบคุมการโอนข้อมูล | CPU ทำ polling หรืออุปกรณ์ส่ง interrupt |
LC-3 Memory-mapped I/O
| ที่อยู่ | รีจิสเตอร์ | หน้าที่ |
|---|---|---|
xFE00 | KBSR (Keyboard Status) | บิต 15 เป็น 1 เมื่อมีอักขระใหม่ มีบิต interrupt enable ด้วย |
xFE02 | KBDR (Keyboard Data) | บิต 7–0 เก็บอักขระล่าสุดที่พิมพ์ |
xFE04 | DSR (Display Status) | บิต 15 เป็น 1 เมื่อจอพร้อมรับอักขระถัดไป |
xFE06 | DDR (Display Data) | อักขระที่เขียนลงบิต 7–0 จะถูกแสดงบนจอ |
การรับข้อมูลจากคีย์บอร์ด
- เมื่อมีการพิมพ์ รหัส ASCII จะถูกวางในบิต 7–0 ของ KBDR และตั้งบิต ready (KBSR[15]) เป็น 1
- คีย์บอร์ดจะถูกปิดชั่วคราวจนกว่า KBDR จะถูกอ่าน ตัวอักษรที่พิมพ์ระหว่างนั้นจะถูกละทิ้ง
- เมื่ออ่าน KBDR บิต ready จะกลับเป็น 0 และคีย์บอร์ดเปิดใหม่
โค้ด Polling สำหรับรับและแสดงผล
; รอรับอักขระจากคีย์บอร์ด
POLL1 LDI R0, KBSRPtr ; ตรวจ KBSR
BRzp POLL1 ; ยังไม่มีอักขระ วนรอ
LDI R0, KBDRPtr ; อ่านอักขระ
; รอให้จอพร้อม แล้วแสดงอักขระ
POLL2 LDI R1, DSRPtr ; ตรวจ DSR
BRzp POLL2 ; จอยังไม่พร้อม วนรอ
STI R0, DDRPtr ; เขียนอักขระลง DDR
KBSRPtr .FILL xFE00
KBDRPtr .FILL xFE02
DSRPtr .FILL xFE04
DDRPtr .FILL xFE06
โค้ดนี้ใช้ BRzp เพราะบิต 15 เป็น 1 ทำให้ค่าเป็นลบ ถ้าบิตนี้เป็น 0 จึงแปลว่า "ยังไม่พร้อม" และให้วนรอต่อ
จำลองคีย์บอร์ดและจอ
ลองกดตามลำดับปกติ แล้วลองกดซ้ำโดยไม่อ่านค่า จะเห็นว่าตัวอักษรหายไปได้อย่างไร
Interrupt-driven I/O
อุปกรณ์สามารถขัดจังหวะการทำงานของโปรแกรม ให้ CPU ทำงานให้มัน แล้วกลับมาทำงานเดิมได้ราวกับไม่มีอะไรเกิดขึ้น
- เหตุผล Polling กินรอบ CPU มาก โดยเฉพาะเมื่อเหตุการณ์เกิดไม่บ่อย รอบที่เหลือจึงนำไปคำนวณอย่างอื่นได้
- การทำงาน ถ้าบิต ready และ interrupt enable (IE) เป็น 1 อุปกรณ์จะส่งสัญญาณ interrupt ไปยัง CPU
- ลำดับความสำคัญ LC-3 มี 8 ระดับ (PL0–PL7) อุปกรณ์ที่มีระดับสูงกว่าสามารถขัดจังหวะโปรแกรมระดับต่ำได้ แต่ไม่ใช่ทางกลับกัน
- การตรวจสอบ CPU จะตรวจสัญญาณระหว่างขั้น STORE และ FETCH ถ้ามีสัญญาณ จะโอนการควบคุมไปยัง interrupt service routine
เนื่องจากมีบิต interrupt enable รีจิสเตอร์สถานะ KBSR และ DSR จึงต้องทั้งอ่านและเขียนได้
ตัวอย่างคำถามทบทวน
ถ้าไม่ตรวจ DSR ก่อนเขียนข้อมูลลงจอ จะเกิดอะไรขึ้น
ถ้าจอยังไม่พร้อม ข้อมูลที่เขียนลง DDR จะถูกละทิ้ง ทำให้อักขระหาย
ถ้าจอเป็น synchronous ซึ่งพร้อมภายใน 1 ไมโครวินาทีหลังเขียนตัวอักษร จะตัด polling ได้อย่างไร
ใช้การรอเวลาคงที่หลังเขียนแต่ละตัว แล้วค่อยเขียนตัวถัดไป ข้อดีคือโค้ดสั้นและไม่ต้องอ่านรีจิสเตอร์สถานะ ข้อเสียคือถ้าอุปกรณ์ช้ากว่าที่คาด ข้อมูลจะหาย และต้องรู้เวลาที่แน่นอนของอุปกรณ์
polling เหมาะกับดิสก์หรือการ์ดเครือข่ายหรือไม่
ไม่ค่อยเหมาะ เพราะอุปกรณ์เหล่านี้มีจังหวะไม่แน่นอนและข้อมูลมาไม่บ่อย การ polling จะเสียรอบ CPU ไปมาก การใช้ interrupt ทำให้ CPU ทำงานอื่นระหว่างรอได้
ทำไมการใช้ LDI/STI กับรีจิสเตอร์อุปกรณ์จึงสะดวก
เพราะ KBSR, KBDR, DSR และ DDR อยู่ไกลเกินกว่าที่ PC-relative จะเข้าถึงได้ การใช้ LDI/STI ทำให้โค้ดอ่านที่อยู่ผ่านตัวชี้ (pointer) ที่เก็บไว้ใกล้ ๆ โปรแกรม
บทที่ 9
TRAP Routines และ Subroutines
การทำงานบางอย่างต้องใช้สิทธิ์พิเศษและความรู้เฉพาะด้าน ระบบปฏิบัติการจึงจัดเตรียม service routine ไว้ให้โปรแกรมเรียกใช้ และโปรแกรมผู้ใช้ก็สามารถแบ่งโค้ดเป็น subroutine ของตัวเองได้
System Call
ทำไมต้องมี system call เพราะอุปกรณ์ I/O ถูกใช้ร่วมกันหลายโปรแกรม ความผิดพลาดเพียงครั้งเดียวอาจกระทบผู้ใช้จำนวนมาก ระบบจึงเก็บการควบคุมไว้ในส่วนของระบบปฏิบัติการ ขั้นตอนคือ โปรแกรมเรียกใช้ → OS ทำงาน → กลับมาที่โปรแกรม ใน LC-3 ทำผ่านกลไก TRAP
กลไก TRAP
- Service routines ชุดโค้ดของระบบ เริ่มที่ตำแหน่งใดก็ได้ (โค้ดระบบมักอยู่ต่ำกว่า x3000) มีได้สูงสุด 256 ตัว
- Trap table ตารางที่อยู่ x0000 ถึง x00FF เก็บที่อยู่เริ่มต้นของแต่ละ routine
- คำสั่ง TRAP trap vector 8 บิต ใช้เป็นดัชนีในตาราง
- การกลับ บันทึกที่อยู่ของคำสั่งถัดไปไว้ใน R7 เพื่อให้กลับมาทำงานต่อได้ทันที
RET (ซึ่งคือ JMP R7)
ข้อควรระวัง service routine ต้องไม่เปลี่ยนค่า R7 มิฉะนั้นจะไม่รู้ว่าต้องกลับไปที่ใด
ติดตามขั้นตอนของ TRAP
เลือก trap vector แล้วกดขั้นถัดไปเพื่อดูว่า PC และ R7 เปลี่ยนไปอย่างไร ที่อยู่ routine ที่มีเครื่องหมาย * เป็นค่าสมมติเพื่อการสาธิต ส่วน OUT ใช้ x0430 ตามตัวอย่างในบทเรียน
TRAP Routines ที่ใช้บ่อย
| Vector | Symbol | หน้าที่ |
|---|---|---|
x20 | GETC | อ่านอักขระหนึ่งตัวจากคีย์บอร์ด (ไม่แสดงผล) |
x21 | OUT | แสดงอักขระออกทางจอ |
x22 | PUTS | แสดงสตริงทางจอ |
x23 | IN | แสดงข้อความแจ้ง แล้วอ่านและแสดงอักขระ |
x25 | HALT | หยุดการทำงานของโปรแกรม |
ตัวอย่าง: แปลงตัวเลข ASCII เป็นตัวพิมพ์เล็ก
.ORIG x3000
LD R2, TERM ; R2 = -'7' (ใช้ตรวจการจบ)
LD R3, ASCII ; R3 = ผลต่างตัวพิมพ์ (x0020)
AGAIN TRAP x23 ; รับอักขระ
ADD R1, R2, R0 ; ตรวจว่าเป็น '7' หรือไม่
BRz EXIT ; ถ้าใช่ จบการทำงาน
ADD R0, R0, R3 ; แปลงเป็นตัวพิมพ์เล็ก
TRAP x21 ; แสดงผล
BRnzp AGAIN
TERM .FILL xFFC9 ; -'7'
ASCII .FILL x0020
EXIT TRAP x25 ; HALT
.END
การบันทึกและกู้คืนรีจิสเตอร์
ต้องบันทึกค่าเมื่อ routine จะทำลายค่านั้น และโปรแกรมยังต้องใช้ค่านั้นหลังจากนั้น มีสองแนวทาง
- Callee-save routine ที่ถูกเรียกบันทึกและคืนค่ารีจิสเตอร์ที่ตนเองจะเปลี่ยน
- Caller-save ผู้เรียกบันทึกค่าที่ตนยังต้องใช้ เช่น ต้องบันทึก R7 ก่อนเรียก TRAP และบันทึก R0 ก่อนใช้ TRAP x23
โดยทั่วไปใช้ callee-save เป็นหลัก ยกเว้นค่าที่ใช้ส่งกลับ ค่าเหล่านี้ไม่ควรถูกคืนค่าเดิม
Subroutine
Subroutine คือโค้ดในพื้นที่ของผู้ใช้ที่ทำงานเฉพาะอย่างและถูกเรียกใช้จากโปรแกรมอื่น ไม่จำเป็นต้องมีสิทธิ์พิเศษ ประโยชน์คือนำโค้ดที่ผ่านการทดสอบแล้วกลับมาใช้ได้ แบ่งงานให้โปรแกรมเมอร์หลายคน และใช้ไลบรารีจากผู้ผลิตได้
JSR และ JSRR
| คำสั่ง | โหมด | เป้าหมาย |
|---|---|---|
JSR | PC-relative | PC + offset 11 บิต (ช่วงประมาณ ±1024 คำสั่ง) |
JSRR | Register | ค่าในรีจิสเตอร์ฐาน เรียกได้ทุกที่ในหน่วยความจำ |
ทั้งสองคำสั่งบันทึกที่อยู่ของคำสั่งถัดไปลงใน R7 เรียกว่า "linking" และกลับด้วย RET เช่นเดียวกับ TRAP ข้อดีของ JSRR คือเรียกได้แม้ปลายทางอยู่ห่างเกินช่วงของ JSR เหมาะกับการเรียกไลบรารีที่โหลดไว้ไกล
ตัวอย่าง: Subroutine 2's Complement
2sComp NOT R0, R0 ; กลับบิตทั้งหมด
ADD R0, R0, #1 ; บวก 1
RET ; กลับไปยังผู้เรียก
; การเรียกใช้งาน: คำนวณ R4 = R1 - R3
ADD R0, R3, #0 ; คัดลอก R3 ไป R0
JSR 2sComp ; ได้ -R3 ใน R0
ADD R4, R1, R0 ; R4 = R1 + (-R3)
หมายเหตุ ถ้าผู้เรียกต้องใช้ R0 ภายหลัง ต้องบันทึกก่อนเรียก
การส่งข้อมูลเข้าและออก
- Argument ค่าที่ส่งเข้าไปให้ subroutine เช่น R0 ของ 2sComp คือค่าที่ต้องการกลับเครื่องหมาย
- Return value ค่าที่ subroutine คำนวณได้และส่งกลับ เช่น ค่าที่กลับเครื่องหมายแล้วอยู่ใน R0
ตัวอย่าง: FirstChar และ CountChar
โจทย์คือเขียน FirstChar เพื่อหาตำแหน่งแรกของอักขระใน R0 ในสตริงที่ R1 ชี้ไป แล้วคืนที่อยู่ใน R2 จากนั้นใช้ FirstChar เขียน CountChar เพื่อนับจำนวนครั้ง ข้อดีคือเขียน CountChar ได้โดยไม่ต้องรู้การทำงานภายในของ FirstChar
; CountChar: นับจำนวนครั้งที่ R0 ปรากฏในสตริงที่ R1 ชี้ไป ผลลัพธ์ใน R2
CountChar
ST R3, CCR3 ; บันทึกรีจิสเตอร์
ST R4, CCR4
ST R7, CCR7 ; ต้องบันทึก R7 เพราะเรียก JSR
ST R1, CCR1
AND R4, R4, #0 ; count = 0
CC1 JSR FirstChar ; หาตำแหน่งถัดไป (ผลใน R2)
LDR R3, R2, #0
BRz CC2 ; ถึง NULL แล้ว จบ
ADD R4, R4, #1 ; เพิ่ม count
ADD R1, R2, #1 ; ชี้ไปตัวถัดไป
BRnzp CC1
CC2 ADD R2, R4, #0 ; คืนค่า count ใน R2
LD R3, CCR3
LD R4, CCR4
LD R1, CCR1
LD R7, CCR7
RET
FirstChar: หาตำแหน่งแรกของอักขระ
ผู้เรียกส่งอักขระใน R0 และที่อยู่ของสตริงใน R1 แล้วรับที่อยู่ของอักขระที่พบ (หรือ NULL ถ้าไม่พบ) กลับมาใน R2 routine นี้บันทึกรีจิสเตอร์ที่ใช้ เก็บค่าลบของอักขระไว้เปรียบเทียบด้วยการบวก วนอ่านทีละตัวจนเจอตัวที่ตรงหรือถึง NULL แล้วกู้คืนรีจิสเตอร์และ RET
Service routine เรียก service routine อื่นได้ไหม
ได้ แต่ต้องบันทึก R7 ก่อนเรียก เพราะ TRAP หรือ JSR จะเขียนทับ R7 ด้วยที่อยู่กลับของตัวเอง ถ้าไม่บันทึกไว้จะกลับไปยังผู้เรียกเดิมไม่ได้
Library Routines
ผู้ผลิตอาจให้ไฟล์ object ที่มี subroutine สำเร็จรูปโดยไม่เปิดเผยซอร์สโค้ด แอสเซมเบลอร์และลิงเกอร์ต้องรองรับ .EXTERNAL หรือผู้ใช้ต้องได้ที่อยู่เริ่มต้นของ routine เอง เมื่อไม่แน่ใจว่าปลายทางอยู่ในช่วง 1024 คำสั่งหรือไม่ ให้ใช้ JSRR เก็บที่อยู่ไว้ในตัวแปร แล้วโหลดมาเรียก
.EXTERNAL SQRT
LD R2, SQAddr ; โหลดที่อยู่ของ SQRT
JSRR R2
...
SQAddr .FILL SQRT
ตัวอย่างคำถามทบทวน
JSRR มีข้อดีอะไรที่ JSR ไม่มี
JSRR เรียกไปยังที่อยู่ใดก็ได้ในหน่วยความจำผ่านรีจิสเตอร์ จึงไม่ถูกจำกัดด้วยช่วง offset 11 บิตของ JSR
ทำไม routine ที่ถูกเรียกต้องรู้ว่าตัวเองจะไม่เปลี่ยน R7 หรือเปลี่ยนแล้วต้องคืนค่า
เพราะ R7 เก็บที่อยู่สำหรับกลับ ถ้าถูกทับ RET จะกระโดดไปผิดที่ จึงต้องบันทึก R7 ก่อนเรียก routine อื่นเสมอ