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 ตัว)

กลุ่มคำสั่งหน้าที่
OperateADD, AND, NOTคำนวณระหว่างรีจิสเตอร์ ไม่เข้าถึงหน่วยความจำ
Data movementLD, LDI, LDR, LEA
ST, STI, STR
อ่านและเขียนข้อมูลระหว่างหน่วยความจำกับรีจิสเตอร์ LEA คำนวณที่อยู่และเก็บลงรีจิสเตอร์ แต่ไม่อ่านหน่วยความจำ
ControlBR, 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)

โหมดใช้กับคำสั่งลักษณะ
ImmediateADD, ANDค่าอยู่ในคำสั่งเลย
RegisterADD, AND, NOT, JMPค่าอยู่ในรีจิสเตอร์
PC-relativeLD, ST, LEA, BRPC + offset 9 บิต (ช่วง −256 ถึง +255) PC ถูกเพิ่มค่าไปแล้วในขั้น FETCH ก่อนคำนวณที่อยู่ LEA ใช้โหมดนี้คำนวณที่อยู่แล้วเก็บลงรีจิสเตอร์ โดยไม่อ่านหน่วยความจำ
IndirectLDI, STIอ่านที่อยู่จากหน่วยความจำก่อน แล้วค่อยเข้าถึงข้อมูลที่อยู่นั้น
Base + OffsetLDR, STRรีจิสเตอร์ + offset 6 บิต เข้าถึงได้ทั้งหน่วยความจำ
ทำไมต้องมี Indirect และ Base + Offset PC-relative เข้าถึงได้เฉพาะ 256 คำรอบตัวคำสั่ง ส่วน Indirect และ Base + Offset แก้ปัญหานี้ได้ โดย Base + Offset ใช้รีจิสเตอร์ฐานเพื่อสร้างที่อยู่เต็ม 16 บิต

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 ปัจจุบัน

CC ปัจจุบัน:

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 ต้องเพิ่มการตรวจกรณีพิเศษ
หลักสำคัญ ทดสอบด้วยกรณีสุดขั้วเสมอ เช่น ข้อมูลทั้งหมดเป็น 1, ไม่มี 1 เลย, ข้อมูลว่าง

ตัวอย่างคำถามทบทวน

ทำไมการคิดให้ชัดก่อนเขียนโค้ดถึงช่วยลดเวลาดีบัก

เพราะถ้าอัลกอริทึมถูกต้องและแบ่งงานชัดเจน ข้อผิดพลาดที่เหลือส่วนใหญ่จะเป็นเรื่องการพิมพ์หรือการใช้คำสั่ง ซึ่งจับได้ง่ายกว่ามาก

บทที่ 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 แต่ละตัว จึงไม่ต้องจำหมายเลข

ชื่อเทียบเท่าหน้าที่
GETCTRAP x20อ่านอักขระจากคีย์บอร์ด (ไม่แสดงผล) เก็บใน R0[7:0]
OUTTRAP x21แสดงอักขระใน R0[7:0] ออกทางจอ
PUTSTRAP x22แสดงสตริง โดย R0 เก็บที่อยู่ของสตริง
INTRAP x23แสดงข้อความ ให้ผู้ใช้พิมพ์ แล้วอ่านและแสดงอักขระ
HALTTRAP 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 ถูกบันทึกที่ตำแหน่งใด

LC ปัจจุบัน:—

ข้อผิดพลาดที่พบบ่อย ได้แก่ จำนวนหรือชนิด 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

ที่อยู่รีจิสเตอร์หน้าที่
xFE00KBSR (Keyboard Status)บิต 15 เป็น 1 เมื่อมีอักขระใหม่ มีบิต interrupt enable ด้วย
xFE02KBDR (Keyboard Data)บิต 7–0 เก็บอักขระล่าสุดที่พิมพ์
xFE04DSR (Display Status)บิต 15 เป็น 1 เมื่อจอพร้อมรับอักขระถัดไป
xFE06DDR (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

  1. Service routines ชุดโค้ดของระบบ เริ่มที่ตำแหน่งใดก็ได้ (โค้ดระบบมักอยู่ต่ำกว่า x3000) มีได้สูงสุด 256 ตัว
  2. Trap table ตารางที่อยู่ x0000 ถึง x00FF เก็บที่อยู่เริ่มต้นของแต่ละ routine
  3. คำสั่ง TRAP trap vector 8 บิต ใช้เป็นดัชนีในตาราง
  4. การกลับ บันทึกที่อยู่ของคำสั่งถัดไปไว้ใน R7 เพื่อให้กลับมาทำงานต่อได้ทันที
การทำงานของ TRAP 1) ดึงที่อยู่ของ routine จากตารางแล้วใส่ใน PC 2) PC ถูกเพิ่มค่าไปแล้วตั้งแต่ขั้น FETCH ดังนั้นค่าที่เก็บใน R7 คือที่อยู่ของคำสั่งถัดไป 3) กลับด้วย RET (ซึ่งคือ JMP R7)

ข้อควรระวัง service routine ต้องไม่เปลี่ยนค่า R7 มิฉะนั้นจะไม่รู้ว่าต้องกลับไปที่ใด

ติดตามขั้นตอนของ TRAP

เลือก trap vector แล้วกดขั้นถัดไปเพื่อดูว่า PC และ R7 เปลี่ยนไปอย่างไร ที่อยู่ routine ที่มีเครื่องหมาย * เป็นค่าสมมติเพื่อการสาธิต ส่วน OUT ใช้ x0430 ตามตัวอย่างในบทเรียน

TRAP Routines ที่ใช้บ่อย

VectorSymbolหน้าที่
x20GETCอ่านอักขระหนึ่งตัวจากคีย์บอร์ด (ไม่แสดงผล)
x21OUTแสดงอักขระออกทางจอ
x22PUTSแสดงสตริงทางจอ
x23INแสดงข้อความแจ้ง แล้วอ่านและแสดงอักขระ
x25HALTหยุดการทำงานของโปรแกรม

ตัวอย่าง: แปลงตัวเลข 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 เป็นหลัก ยกเว้นค่าที่ใช้ส่งกลับ ค่าเหล่านี้ไม่ควรถูกคืนค่าเดิม

ข้อผิดพลาดที่พบบ่อย ใน routine ที่แปลงตัวเลข ถ้าโค้ดเรียก TRAP หรือ subroutine อื่นโดยไม่บันทึก R7 ก่อน จะไม่สามารถกลับไปยังผู้เรียกได้ และถ้า R7 ถูกใช้เป็น counter ก็จะถูกทับค่าได้เช่นกัน

Subroutine

Subroutine คือโค้ดในพื้นที่ของผู้ใช้ที่ทำงานเฉพาะอย่างและถูกเรียกใช้จากโปรแกรมอื่น ไม่จำเป็นต้องมีสิทธิ์พิเศษ ประโยชน์คือนำโค้ดที่ผ่านการทดสอบแล้วกลับมาใช้ได้ แบ่งงานให้โปรแกรมเมอร์หลายคน และใช้ไลบรารีจากผู้ผลิตได้

JSR และ JSRR

คำสั่งโหมดเป้าหมาย
JSRPC-relativePC + offset 11 บิต (ช่วงประมาณ ±1024 คำสั่ง)
JSRRRegisterค่าในรีจิสเตอร์ฐาน เรียกได้ทุกที่ในหน่วยความจำ

ทั้งสองคำสั่งบันทึกที่อยู่ของคำสั่งถัดไปลงใน 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 ด้วยที่อยู่กลับของตัวเอง ถ้าไม่บันทึกไว้จะกลับไปยังผู้เรียกเดิมไม่ได้

ตัวอย่างข้อผิดพลาดจากหนังสือ routine ที่นับตัวเลขโหลดค่า 10 ลงใน R7 เพื่อใช้เป็นตัวนับ แต่ TRAP x23 จะเขียน R7 ทับด้วยที่อยู่กลับ ตัวนับจึงเพี้ยน ต้องใช้รีจิสเตอร์อื่นหรือบันทึก 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 อื่นเสมอ