Topic 10: สะพาน LC-3 → RISC-V (RV32I)
310-2202 โครงสร้างระบบคอมพิวเตอร์ — เอกสารสรุปเนื้อหา
ภาพรวม Topic 10 — สะพาน LC-3 → RISC-V (RV32I)
หลังเข้าใจหลักการครบจาก LC-3 (เครื่อง 16-bit เพื่อการสอน) Topic นี้ข้ามไปสู่ ISA (instruction set architecture, ชุดคำสั่ง) จริงระดับอุตสาหกรรม คือ RISC-V RV32I (32-bit, มาตรฐานเปิด, มี toolchain ครบ) โดยทำ "พจนานุกรม" แมปทุกแนวคิดของ LC-3 เป็น RV32I ดูสิ่งที่ LC-3 ไม่มีแล้ว RISC-V เติมเต็ม และฝึกเขียนโปรแกรม LC-3 เดิมใหม่เป็น RV32I
เมื่อจบ Topic นี้ ผู้เรียนจะสามารถ: อธิบายเหตุผลที่ก้าวจาก LC-3 ไป RV32I และข้อดีของ ISA แบบ modular/เปิด · แมปคำสั่ง LC-3 ↔ RV32I (ADD→add, BR→beq/bne, JSR→jal, TRAP→ecall) · อธิบายสิ่งที่ LC-3 ขาด (x0, ABI, mul/shift/float, little-endian) และเขียนโปรแกรม LC-3 ใหม่เป็น RV32I
| หัวข้อ | เนื้อหา |
|---|---|
| 10.1 | ทำไมต้องย้าย — LC-3 ให้หลักการอะไรไว้ ขาดอะไร และ RV32I เติมเต็มตรงไหน พร้อมตารางเทียบครบทุกมิติ |
| 10.2 | คำสั่ง LC-3 ที่รู้จัก เขียนเป็น RV32I อย่างไร — ตารางแมปครบทั้งกลุ่มคำนวณ/ลอจิก/หน่วยความจำ และกลุ่ม address/branch/control |
| 10.3 | RV32I มีอะไรที่ LC-3 ไม่เคยมี — รีจิสเตอร์แบบมีบทบาท (ABI), calling convention ผ่าน sp/ra, คูณ/หาร/เลื่อนบิต/ทศนิยม และแนวคิด modular ISA |
| 10.4 | ข้อมูลวางในหน่วยความจำแบบไหน — little-endian, การอ้างที่อยู่รายไบต์ และตัวอย่างเขียนโปรแกรม LC-3 เดิมใหม่เป็น RV32I เต็มโปรแกรม |
ด้านล่างคือตัวจำลอง "พจนานุกรม LC-3 → RV32I" จากเอกสารต้นฉบับ — คลิกคำสั่ง LC-3 ทางซ้ายเพื่อดูคำแปล RV32I จริง:
10.1 ทำไมต้องย้ายจาก LC-3 ไป RISC-V RV32I
LC-3 คืออะไร และทำไมมันพอแค่ระดับ "เครื่องสอน"
LC-3 เป็นเครื่องสอน (educational machine) ขนาด 16-bit: คำสั่ง รีจิสเตอร์ และที่อยู่หน่วยความจำกว้าง 16 บิตทั้งหมด มีรีจิสเตอร์ทั่วไป 8 ตัว (R0–R7) ชุดคำสั่งเล็กมากราว 15 opcode (ADD, AND, NOT, LD/ST, LDR/STR, LDI/STI, LEA, BR, JMP/JSR, TRAP) ความกว้างคำสั่งคงที่ 16 บิต มี opcode 4 บิตด้านบนทำให้ถอดรหัสด้วยมือได้ง่าย เหมาะกับการเรียน datapath และ FSM ของหน่วยควบคุม
จุดแข็ง: เล็ก โปร่งใส ไม่มีรายละเอียดกวนใจ เหมาะกับการ "เห็นทั้งเครื่อง" ตั้งแต่ logic gate, datapath, control ไปจนถึง assembly
ข้อจำกัดที่ทำให้ไปต่อไม่ได้: (1) ไม่มี toolchain ระดับใช้งานจริง — มีแค่ assembler/simulator เพื่อการเรียน ไม่มีคอมไพเลอร์ C จริง ไม่มี OS ไม่มี ecosystem (2) ไม่มี career transfer — ไม่มีชิป LC-3 ขายจริง ทักษะไม่ถ่ายโอนสู่อุตสาหกรรม (3) สเกลไม่ขึ้น — 16 บิต + รีจิสเตอร์น้อย + คำสั่งน้อย ทำให้ต่อยอดไปเรื่อง pipeline, cache, virtual memory แบบเครื่องสมัยใหม่ได้ลำบาก
LC-3 ใช้ address กว้าง 16 บิต ส่วน RV32I ใช้ 32 บิต แต่ละเครื่องระบุที่อยู่ได้กี่ตำแหน่ง และ RV32I มากกว่ากี่เท่า
- จำนวนตำแหน่งที่ address n บิตระบุได้ = 2^n
- LC-3: n=16 → 2^16 = 65,536 ตำแหน่ง (64 Ki word = ~64 KiB)
- RV32I: n=32 → 2^32 = 4,294,967,296 ตำแหน่ง (byte-addressable = 4 GiB)
- มากกว่ากี่เท่า = 2^32 / 2^16 = 2^16 = 65,536 เท่า
เทียบกันชัด ๆ: LC-3 กับ RV32I ครบทุกมิติ
| มิติ | LC-3 (เครื่องสอน) | RV32I (ISA จริง) |
|---|---|---|
| ความกว้างข้อมูล/รีจิสเตอร์ | 16 บิต | 32 บิต |
| ความกว้างคำสั่ง | 16 บิตคงที่ | 32 บิตคงที่ (ในฐาน I) |
| จำนวน/ชื่อรีจิสเตอร์ | 8 ตัว: R0–R7 | 32 ตัว: x0–x31 (x0 = ศูนย์เสมอ, มีชื่อ ABI เช่น zero, ra, sp, a0, t0) |
| ขนาด address space | 2^16 = 65,536 ตำแหน่ง (ทีละ word 16 บิต) | 2^32 ไบต์ = 4 GiB (รายไบต์) |
| จำนวนคำสั่ง | ราว 15 opcode | ราว 40 คำสั่งในฐาน (ยังไม่นับส่วนขยาย) |
| ความกว้าง immediate | 5 บิตใน ADD/AND (−16..+15), offset 6/9/11 บิต | 12 บิตในกลุ่มคำนวณ/load/store (−2048..+2047), 20 บิตใน lui/jal |
| คูณ/หาร | ไม่มี — ต้องเขียนลูปบวกซ้ำ | ฐาน I ไม่มี แต่เติมส่วนขยาย M ได้ (mul, div, rem) |
| เลื่อนบิต | ไม่มีคำสั่งเลื่อนบิตเลย | มีครบ: sll, srl, sra และแบบ immediate slli/srli/srai |
| วิธีเรียกระบบ | TRAP พร้อมเวกเตอร์ เช่น x20 (GETC), x21 (OUT), x25 (HALT) | ecall คำสั่งเดียว โดยใส่หมายเลขบริการไว้ในรีจิสเตอร์ |
วิธีอ่านตารางนี้: (1) จำคู่ตัวเลขเชิงขนาดก่อน — 16→32 บิต และ 8→32 register คือหัวใจของการย้าย (2) แถวคูณ/หาร เลื่อนบิต immediate คือความต่างเชิงความสามารถ — 10.3 เจาะต่อ (3) แถว TRAP↔ecall คือความต่างเชิงวิธีคุยกับระบบ — 10.2 เจาะต่อ
ตัวอย่าง: โปรแกรมเดียวกัน เขียนสองภาษา
โจทย์: บวกเลข 1 ถึง 5 แล้วเก็บผลรวมลงตัวแปรในหน่วยความจำ
; LC-3: รวม 1+2+3+4+5 เก็บลง RESULT
.ORIG x3000
AND R1, R1, #0 ; R1 = sum = 0
AND R2, R2, #0 ; ล้าง R2 ก่อน
ADD R2, R2, #1 ; R2 = i = 1
LOOP ADD R1, R1, R2 ; sum = sum + i
ADD R2, R2, #1 ; i = i + 1
ADD R3, R2, #-6 ; R3 = i - 6 (ตั้ง condition code)
BRn LOOP ; ถ้า R3 ติดลบ วนต่อ
ST R1, RESULT ; เก็บ sum
HALT
RESULT .FILL x0000
.END
# RV32I: รวม 1+2+3+4+5 เก็บลง result
.data
result: .word 0
.text
addi t0, zero, 0 # t0 = sum = 0
addi t1, zero, 1 # t1 = i = 1
addi t2, zero, 5 # t2 = ค่าสุดท้าย
loop: add t0, t0, t1 # sum = sum + i
addi t1, t1, 1 # i = i + 1
bge t2, t1, loop # ถ้า i ยังไม่เกิน 5 วนต่อ
la t3, result
sw t0, 0(t3) # เก็บ sum
addi a0, zero, 10 # หมายเลขบริการ exit
ecall
สามจุดต่างสำคัญ: (1) รีจิสเตอร์ค่าศูนย์ (x0/zero) — LC-3 ไม่มี ต้องล้างด้วย AND #0 ก่อนเสมอ RV32I ตั้งค่าคงที่ได้ในคำสั่งเดียวด้วย addi rd, zero, ค่า (2) branch เทียบรีจิสเตอร์ตรง ๆ — LC-3 ตัดสินใจจาก condition code (N,Z,P) ที่คำสั่งก่อนหน้าตั้งไว้ RV32I ใส่การเปรียบเทียบไว้ในตัว branch เอง (3) เข้าถึงหน่วยความจำผ่านรีจิสเตอร์ฐาน — LC-3 อ้าง label ตรง ๆ ได้ (PC-relative) RV32I ต้องมีที่อยู่ในรีจิสเตอร์ก่อนแล้วบวก offset แลกกับระยะเอื้อมทั้ง 4 GiB
Q: เมื่อโปรแกรมทั้งสองฉบับรันจบ ตัวแปรผลลัพธ์เก็บค่าเท่าไร
A: 15 — ลูปบวกสะสม 1+2+3+4+5 = 15 (ตรวจจากตารางไล่ค่า t0/R1: 1, 3, 6, 10, 15)
RV32I ให้อะไรที่ LC-3 ไม่ให้
- 32-bit จริง — ข้อมูล/รีจิสเตอร์/ที่อยู่กว้าง 32 บิต มีรีจิสเตอร์ทั่วไป 32 ตัว (x0-x31, x0 ผูกค่าเป็นศูนย์เสมอ) ความกว้างคำสั่งฐานคงที่ 32 บิต
- ISA แบบ modular และเปิด (open standard) — แยกเป็น "ฐาน + ส่วนขยาย" RV32I คือฐานจำนวนเต็ม แล้วเติม M (คูณ/หาร), A (atomic), F/D (floating-point), C (compressed) ต่อเมื่อจำเป็น เป็นมาตรฐานเปิดที่ใครก็นำไปออกแบบได้โดยไม่ติดค่าลิขสิทธิ์
- toolchain ครบจริง — ใช้ได้กับ GCC และ LLVM/Clang, รัน Linux ได้, มี simulator หลายระดับ เช่น Spike (golden model), QEMU (emulation ทั้งระบบ), Ripes (visualizer เชิงการศึกษา)
- career transfer จริง — ทักษะ RISC-V ถ่ายโอนตรงไปสู่ชิปและงานจริงในตลาด
- สเกลได้โดยไม่เปลี่ยน ISA — เริ่มจาก RV32I ระดับ intro แล้วไต่ไปจนถึง microarchitecture ละเอียดบน gem5 ได้ โดยยังเป็น ISA เดิม
ถึงจะเป็น ISA จริง แต่ฐาน RV32I มีจำนวนคำสั่งไม่มาก จัดกลุ่มเป็นไม่กี่ format ตามหลักการ — R (register-register), I (immediate/load), S (store), B (branch), U (upper immediate), J (jump) — ความสม่ำเสมอนี้ทำให้ถอดรหัสและสอนเป็นระบบได้:
# LC-3: บวก R1 = R2 + R3
ADD R1, R2, R3
# RISC-V RV32I: บวก x1 = x2 + x3 (รูปแบบ R-type)
add x1, x2, x3
LC-3 ใช้ 3 บิตเลือกรีจิสเตอร์ (8 ตัว) ส่วน RV32I ต้องเลือกได้ 32 ตัว ต้องใช้กี่บิต
- จำนวนรีจิสเตอร์ที่เลือกได้ = 2^(จำนวนบิต)
- 4 บิต → 2^4 = 16 ตัว (น้อยไป)
- 5 บิต → 2^5 = 32 ตัว (พอดีกับ x0–x31)
บันไดอ่านโค้ด RV32I สามระดับ
วิธีอ่านโค้ด RV32I: (1) อ่านชื่อคำสั่งก่อนว่าอยู่กลุ่มไหน — คำนวณ (add/addi), หน่วยความจำ (lw/sw), หรือควบคุมทิศทาง (beq/jal) (2) หาปลายทางของผลลัพธ์ — กลุ่มคำนวณ/load เขียนลงรีจิสเตอร์ rd แต่ sw เขียนลงหน่วยความจำ (3) ไล่ค่าทีละบรรทัดลงตาราง อย่าข้ามบรรทัด
addi x5, x0, 7 # x5 = 0 + 7
add x6, x5, x5 # x6 = x5 + x5
หน่วยความจำ: 0x1000=10, 0x1004=20 (RV32I อ้างที่อยู่รายไบต์ word ถัดไปกระโดดทีละ 4)
lui x5, 1 # x5 = 0x1000 (สร้างที่อยู่ฐาน)
lw x6, 0(x5) # x6 = M[x5+0] = 10
lw x7, 4(x5) # x7 = M[x5+4] = 20
add x28, x6, x7 # x28 = 30
sw x28, 8(x5) # เขียน 30 ลง M[x5+8] = 0x1008
addi x5, x0, 0 # x5 = ตัวนับ เริ่มที่ 0
addi x6, x0, 8 # x6 = ค่าเป้าหมาย
loop: addi x5, x5, 2 # เพิ่มตัวนับทีละ 2
beq x5, x6, done # ถ้า x5 == x6 ออกจากลูป
jal x0, loop # กระโดดกลับไป loop เสมอ
done:
สังเกต: ลูปนี้คือโครงเดียวกับลูป LC-3 ทุกประการ (นับ → เทียบ → วน) — พร้อมเข้าตารางแมปคำสั่งของ 10.2 แล้ว
อ้างอิง
- D. Patterson และ J. Hennessy, Computer Organization and Design (RISC-V Edition) — บทที่ 2
- Y. Patt และ S. Patel, Introduction to Computing Systems — ส่วน LC-3 (บทที่ 1–9)
10.2 ตารางแมปคำสั่ง LC-3 ↔ RV32I
หัวข้อนี้สร้าง "พจนานุกรม" ระหว่างคำสั่งของ LC-3 กับคำสั่งจริงของ RISC-V RV32I ทุกอย่างที่ LC-3 ทำได้ RV32I ก็ทำได้ แต่ RV32I ออกแบบมาให้ "ตรงไปตรงมา" และเป็นมาตรฐานอุตสาหกรรมมากกว่า LC-3 มี 16 บิต รีจิสเตอร์ 8 ตัว ส่วน RV32I มี 32 บิต รีจิสเตอร์ 32 ตัว โดย x0 ถูกฮาร์ดไวร์เป็นค่า 0 เสมอ
กลุ่มที่ 1: เลขคณิตและตรรกะ (Arithmetic / Logic)
LC-3 มีคำสั่งคำนวณเพียง 3 ตัว: ADD, AND, NOT — แมปตรงตัวได้เกือบทั้งหมด:
| LC-3 | ความหมาย | RV32I ที่เทียบเท่า |
|---|---|---|
| ADD R1,R2,R3 | บวกรีจิสเตอร์ | add x1,x2,x3 |
| ADD R1,R2,#5 | บวกค่าคงที่ (imm5) | addi x1,x2,5 |
| AND R1,R2,R3 | และระดับบิต | and x1,x2,x3 |
| AND R1,R2,#0 | เคลียร์เป็น 0 | andi x1,x2,0 (หรือ mv x1,x0) |
| NOT R1,R2 | กลับบิตทุกบิต | xori x1,x2,-1 |
จุดที่ต้องเน้น: RV32I ไม่มี opcode สำหรับ NOT โดยตรง เพราะค่า -1 ใน two's complement คือ 0xFFFFFFFF (ทุกบิตเป็น 1) เมื่อ XOR แต่ละบิตกับ 1 บิตนั้นจะกลับค่าเสมอ ตรงตามนิยาม NOT พอดี — assembler มี pseudo-instruction not x1,x2 ที่ขยายเป็น xori x1,x2,-1 ให้อัตโนมัติ
x2 (8 บิตล่าง) = 1010 0101 หาผลของ xori x1,x2,-1
- -1 = 0xFFFFFFFF → ทุกบิตเป็น 1 (8 บิตล่างของ mask = 1111 1111)
- XOR ทีละบิต (a XOR 1 = NOT a): 1010 0101 XOR 1111 1111 = 0101 1010
- ตรวจ: กลับทุกบิตของ 1010 0101 = 0101 1010 ✓ ตรงกัน
กลุ่มที่ 2: เข้าถึงหน่วยความจำ (Load / Store)
จุดต่างเชิงปรัชญาที่สำคัญที่สุด: LC-3 มีโหมดที่อยู่หลายแบบ (PC-relative และ base+offset) ส่วน RV32I เป็น load-store architecture อย่างเคร่งครัด — คำสั่งคำนวณทำงานกับรีจิสเตอร์เท่านั้น การแตะหน่วยความจำต้องผ่าน load/store เสมอ
| LC-3 | โหมด | RV32I ที่เทียบเท่า |
|---|---|---|
| LDR R1,R2,#off | base + offset | lw x1, off(x2) |
| STR R1,R2,#off | base + offset | sw x1, off(x2) |
| LD R1,LABEL | PC-relative | ต้องคำนวณที่อยู่ก่อน แล้ว lw |
| ST R1,LABEL | PC-relative | คำนวณที่อยู่ก่อน แล้ว sw |
LD/ST ของ LC-3 (PC-relative) ไม่มีคู่ตรงใน RV32I ต้องสร้างที่อยู่ของ LABEL ไว้ในรีจิสเตอร์ก่อน (กลุ่มที่ 3) แล้วจึง lw/sw ผ่าน base register นั้น
| LC-3 offset (word) | คูณ 4 | RV32I offset (byte) | ตัวอย่าง |
|---|---|---|---|
| #3 | 3×4 | 12 | LDR R1,R2,#3 → lw x1,12(x2) |
| #10 | 10×4 | 40 | LDR R1,R2,#10 → lw x1,40(x2) |
| #-2 | (-2)×4 | -8 | STR R5,R2,#-2 → sw x5,-8(x2) |
; LC-3 (R2 = base ของบล็อกข้อมูล)
LDR R1, R2, #0 ; R1 ← MEM[R2+0]
LDR R3, R2, #1 ; R3 ← MEM[R2+1] (mask)
AND R4, R1, R3 ; R4 = R1 AND R3
STR R4, R2, #2 ; MEM[R2+2] ← R4
lw x1, 0(x2) # offset 0×4=0 lw x3, 4(x2) # offset 1×4=4 ← จุดพลาดบ่อยที่สุด and x4, x1, x3 sw x4, 8(x2) # offset 2×4=8
ทำไมต้อง load-store discipline: การบังคับให้คำสั่งคำนวณแตะเฉพาะรีจิสเตอร์ ทำให้แต่ละคำสั่งมีงานชัดเจน คงที่ สั้น เหมาะกับ pipeline (Topic 11+) เพราะขั้นตอน execute ไม่ต้องรอ memory access พร้อมกัน — เป็นหัวใจของปรัชญา RISC
กลุ่มที่ 3: สร้างที่อยู่ / ค่าคงที่ใหญ่ (LEA, immediate ใหญ่)
LC-3 ใช้ LEA R1,LABEL โหลด "ที่อยู่" แบบ PC-relative ในคำสั่งเดียว ใน RV32I การได้ที่อยู่/ค่าคงที่เต็ม 32 บิตต้องแบ่งสองขั้น เพราะคำสั่งยาว 32 บิต ใส่ immediate เต็ม 32 บิตในคำสั่งเดียวไม่ได้
| LC-3 | ความหมาย | RV32I ที่เทียบเท่า |
|---|---|---|
| LEA R1,LABEL | ที่อยู่แบบ PC-relative | auipc x1,hi; addi x1,x1,lo |
| (ค่าคงที่ใหญ่) | โหลดค่าคงที่ 32 บิต | lui x1,hi; addi x1,x1,lo |
lui x1,imm20 (load upper immediate): วางค่า 20 บิตไว้ที่บิตบนของรีจิสเตอร์ (บิต 31-12) แล้วเติม 0 บิตล่าง auipc x1,imm20 (add upper immediate to PC): เหมือน lui แต่บวกผลเข้ากับ PC ปัจจุบัน ได้ที่อยู่ PC-relative ตรงกับวิญญาณของ LEA จากนั้นใช้ addi เติมส่วนล่าง 12 บิตให้ครบ — assembler ปรับส่วนบน 20 บิตชดเชยการ sign-extend ของส่วนล่างให้อัตโนมัติ ผู้เรียนไม่ต้องคำนวณเอง
กลุ่มที่ 4: กระโดดแบบมีเงื่อนไข (Branch)
ความต่างเชิงแนวคิดที่ใหญ่ที่สุด: LC-3 ใช้ condition codes (N,Z,P) ที่ถูกตั้งอัตโนมัติทุกครั้งที่เขียนค่าลงรีจิสเตอร์ แล้ว BRn/BRz/BRp ตรวจ flag เหล่านี้ ส่วน RV32I ใช้ compare-and-branch: ไม่มี flag เก็บไว้ คำสั่ง branch เปรียบเทียบรีจิสเตอร์สองตัวในตัวเดียวแล้วตัดสินใจกระโดดทันที
| LC-3 | เงื่อนไข (เทียบ rs กับ 0) | RV32I ที่เทียบเท่า | pseudo |
|---|---|---|---|
| BRn LABEL | น้อยกว่า 0 | blt rs,x0,LABEL | bltz rs,LABEL |
| BRz LABEL | เท่ากับ 0 | beq rs,x0,LABEL | beqz rs,LABEL |
| BRp LABEL | มากกว่า 0 | blt x0,rs,LABEL | bgtz rs,LABEL |
| BRnz LABEL | น้อยกว่าหรือเท่ากับ 0 | bge x0,rs,LABEL | blez rs,LABEL |
| BRnp LABEL | ไม่เท่ากับ 0 | bne rs,x0,LABEL | bnez rs,LABEL |
| BRzp LABEL | มากกว่าหรือเท่ากับ 0 | bge rs,x0,LABEL | bgez rs,LABEL |
| BRnzp LABEL | กระโดดเสมอ | jal x0,LABEL | j LABEL |
จุดสังเกต: (1) BRp/BRnz ต้องสลับ rs ไปฝั่งขวา เพราะ RV32I มีแต่ blt/bge ไม่มี bgt/ble ตรง ๆ (2) BRnzp คือเงื่อนไขที่จริงเสมอ จึงกลายเป็น jump ธรรมดา jal x0,LABEL (ทิ้ง return address ลง x0)
- หารีจิสเตอร์ที่ตั้ง CC ล่าสุด — มองย้อนจาก BR ขึ้นไป คำสั่งที่เขียนค่าล่าสุดคือตัวตั้ง N/Z/P
- แปลงเงื่อนไข flag เป็นการเทียบกับศูนย์: z="เท่ากับ 0", n="น้อยกว่า 0", p="มากกว่า 0"
- เลือกคำสั่ง branch จากตาราง โดยวาง x0 ในตำแหน่งของ "ศูนย์"
ADD R1,R1,#0 แล้ว BRz ZERO_CASE → rs=x1, เงื่อนไข "เท่ากับ 0" → beq x1,x0,ZERO_CASE (บรรทัด ADD ไม่ต้องแปลเลย เพราะหน้าที่เดียวคือตั้ง flag ซึ่ง RV32I ไม่ใช้)signed กับ unsigned: blt/bge เทียบแบบมีเครื่องหมาย ส่วน bltu/bgeu เทียบแบบไม่มีเครื่องหมาย ขณะที่ N flag ของ LC-3 ตีความผลเป็น signed เสมอ ข้อควรระวัง: bltu rs,x0 เป็นเท็จเสมอ (ไม่มีค่า unsigned ใดน้อยกว่า 0) ส่วน bgeu rs,x0 เป็นจริงเสมอ
LOOP ADD R2, R2, #-1 ; ตั้ง CC จากผลลัพธ์
BRp LOOP ; วนถ้าผลลัพธ์มากกว่า 0
- BRp วน "ถ้าผลล่าสุด (R2) มากกว่า 0" (signed)
- "R2 มากกว่า 0" = "0 น้อยกว่า R2" (สลับตัวถูกเทียบเพราะไม่มี bgt ตรง ๆ)
- เลือก blt โดยวาง 0 เป็น rs1:
blt x0, x2, LOOP
LOOP: addi x2, x2, -1 # x2 = x2 - 1
blt x0, x2, LOOP # วนถ้า 0 < x2กลุ่มที่ 5: เรียกซับรูทีนและกลับ (Call / Return)
| LC-3 | ความหมาย | RV32I ที่เทียบเท่า |
|---|---|---|
| JSR LABEL | เรียกซับรูทีน (PC-relative) | jal x1, LABEL |
| JSRR R2 | เรียกซับรูทีน (ผ่านรีจิสเตอร์) | jalr x1, 0(x2) |
| RET | กลับจากซับรูทีน | jalr x0, 0(x1) |
ทั้งสอง ISA เก็บ "ที่อยู่กลับ" ไว้ในรีจิสเตอร์ — LC-3 ใช้ R7 ส่วน RV32I ใช้ x1 (ชื่อ ra = return address) jal x1,LABEL: กระโดดไป LABEL พร้อมเก็บที่อยู่คำสั่งถัดไปลง x1 jalr x0,0(x1): กระโดดกลับไปยัง ra โดยทิ้ง return address ใหม่ลง x0 (assembler มี pseudo ret)
jal x1, my_func อยู่ที่ 0x1000, my_func อยู่ที่ 0x1010 (+16 byte)
กลุ่มที่ 6: เรียกบริการระบบ (Trap)
| LC-3 | ความหมาย | RV32I ที่เทียบเท่า |
|---|---|---|
| TRAP x25 (HALT) | เรียกบริการ OS/firmware | ecall |
| GETC/OUT/PUTS | บริการ I/O ผ่าน trap vector | ecall (ผ่าน OS convention) |
LC-3 ใช้ TRAP พร้อม trap vector (x20=GETC, x21=OUT, x25=HALT) เพื่อข้ามไปยังรูทีนบริการ RV32I ใช้คำสั่งเดียวคือ ecall โดยหมายเลขบริการและพารามิเตอร์ถูกส่งผ่านรีจิสเตอร์ตามข้อตกลง (a7 = หมายเลข syscall, a0-a6 = อาร์กิวเมนต์) ขั้นตอน: ใส่อาร์กิวเมนต์ลง a0 → ใส่หมายเลขบริการลง a7 → เรียก ecall
| บริการ | a7 | อาร์กิวเมนต์/ผลลัพธ์ | trap vector LC-3 เทียบเคียง |
|---|---|---|---|
| พิมพ์จำนวนเต็ม | 1 | a0 = ค่าที่จะพิมพ์ | (ไม่มีตรง ๆ) |
| พิมพ์สตริง | 4 | a0 = ที่อยู่สตริง | TRAP x22 (PUTS) |
| อ่านจำนวนเต็ม | 5 | ผลกลับใน a0 | (ไม่มีตรง ๆ) |
| จบโปรแกรม | 10 | - | TRAP x25 (HALT) |
| พิมพ์อักขระ | 11 | a0 = รหัสอักขระ | TRAP x21 (OUT) |
| อ่านอักขระ | 12 | ผลกลับใน a0 | TRAP x20 (GETC) |
Q: โปรแกรมเรียก ecall 4 ครั้งติดกัน (a7=1,11,1,10) ตามด้วย 3 คำสั่งอีก มีคำสั่งกี่คำสั่งที่ถูกรันจริงจากทั้งหมด 14 คำสั่ง
A: 11 คำสั่ง — ecall ที่ a7=10 (คำสั่งที่ 11) จบโปรแกรมทันที สามคำสั่งสุดท้ายไม่ถูกแตะเลย
ทบทวนความเข้าใจ
Q: ความต่างหลักของการ branch ระหว่าง LC-3 กับ RV32I คืออะไร
A: LC-3 ตรวจ flag N/Z/P ที่ตั้งไว้ก่อน (condition-code) ส่วน RV32I เทียบรีจิสเตอร์สองตัวในคำสั่ง branch เดียว (compare-and-branch) ไม่มี flag เก็บค้างเลย
Q: คำสั่งใดเทียบเท่า JSR (เก็บ return address ที่ x1 แล้วกระโดดไปซับรูทีน)
A: jal x1, LABEL — ส่วน jalr x0,0(x1) คือ ret (เทียบเท่า RET) ไม่ใช่การเรียก
อ้างอิง
- D. Patterson และ J. Hennessy, Computer Organization and Design (RISC-V Edition) — บทที่ 2
- Y. Patt และ S. Patel, Introduction to Computing Systems — ส่วน LC-3 (บทที่ 1–9)
10.3 สิ่งที่ RV32I มีแต่ LC-3 ไม่มี
คูณ/หาร และ barrel shift
LC-3 base ไม่มีคำสั่งคูณ/หารเป็น primitive ต้องเขียนลูปบวกซ้ำ และไม่มีคำสั่งเลื่อนบิตหลายตำแหน่งในจังหวะเดียว RISC-V เติมเต็มสองเรื่องนี้:
- Barrel shift อยู่ใน base RV32I เลย — sll (shift left logical), srl (shift right logical, เติม 0), sra (shift right arithmetic, เติม sign bit รักษาเครื่องหมาย) เลื่อนได้หลายตำแหน่งในคำสั่งเดียว มีเวอร์ชัน immediate (slli/srli/srai) ช่วง 0–31 (5 บิต)
- คูณ/หารอยู่ใน extension M (ไม่ใช่ base) — mul, mulh/mulhu/mulhsu (32 บิตบนของผลคูณ), div/divu, rem/remu ถ้าชิปไม่มี M ต้องคูณ/หารด้วยซอฟต์แวร์เหมือน LC-3
x6 = -16 (0xFFFFFFF0) หาผลของ srai x5,x6,2 เทียบกับ srli x5,x6,2
- srai (arithmetic — เติมซ้ำ sign bit=1 ทางซ้าย): ผล = 0xFFFFFFFC = -4 (ตรวจ: -16÷4=-4 ✓)
- srli (logical — เติม 0 ทางซ้าย): ผล = 0x3FFFFFFC = 1,073,741,820 (กลายเป็นเลขบวกมหึมา เพราะ 0 ทำลายบิตเครื่องหมาย)
; LC-3: R2 = R0 × 12 (R1 เริ่มต้น = 12 ใช้เป็นตัวนับ)
AND R2, R2, #0
LOOP ADD R2, R2, R0
ADD R1, R1, #-1
BRp LOOP
| LC-3 ลูปบวกซ้ำ | RV32IM mul | |
|---|---|---|
| คำสั่งในโปรแกรม (static) | 4 | 1 |
| คำสั่งที่ execute จริง (คูณ 12) | 37 | 1 |
| ไซเคิลโดยประมาณ | ~185 (5 ไซเคิล/คำสั่ง) | 1–3 |
| ถ้าตัวคูณเป็น 1000 | 3001 คำสั่ง — โตตามค่าตัวคูณ | 1 คำสั่งเท่าเดิม |
คูณ 6×5 มองตัวคูณ 5=101 ฐานสอง แล้ว 6×5 = 6×4 + 6×1 (บวกเฉพาะตำแหน่งบิตที่เป็น 1)
addi x12, x0, 0 # ผลสะสม = 0
LOOP: andi x5, x11, 1 # ดูบิตต่ำสุดของตัวคูณ
beq x5, x0, SKIP # บิตเป็น 0 → ไม่ต้องบวก
add x12, x12, x10 # บิตเป็น 1 → บวกตัวตั้ง
SKIP: slli x10, x10, 1 # ตัวตั้ง × 2
srli x11, x11, 1 # ตัวคูณเลื่อนขวา
bne x11, x0, LOOP
Stack discipline ผ่าน sp
LC-3 ไม่มี stack pointer มาตรฐาน (Topic 8 นิยม R6 กันเอง) RISC-V ยกระดับเป็น ABI: sp (x2) คือ stack pointer อย่างเป็นทางการ fp (x8=s0) ชี้ frame ปัจจุบัน stack โตลงทาง address ต่ำ push คือ addi sp,sp,-k แล้ว sw · pop คือ lw แล้ว addi sp,sp,+k ฮาร์ดแวร์ไม่มีคำสั่ง push/pop แยก — สอดคล้องปรัชญา RISC ที่ให้คำสั่งน้อย เรียบง่าย แล้วประกอบเอา
Floating point: extension F (และ D)
LC-3 ไม่มีเลขทศนิยมเลย RISC-V แยกเป็น extension: F = single-precision (32 บิต) เพิ่ม register f0–f31 และคำสั่งอย่าง fadd.s, fmul.s, flw/fsw ตามมาตรฐาน IEEE 754 · D = double-precision (64 บิต) ต่อยอดจาก F การแยกออกจาก base ทำให้ไมโครคอนโทรลเลอร์เล็ก ๆ ที่ไม่ต้องใช้ทศนิยมประหยัดทรานซิสเตอร์ได้
Modular ISA: base + extensions
หัวใจที่ทำให้ RISC-V ต่างจาก LC-3 และ ISA เก่า ๆ: ออกแบบเป็นโมดูล มี base เล็ก ๆ บังคับ แล้วเลือกเสริม extension ตามงาน
RV32I = base integer ISA ~40 คำสั่ง ทุกชิปต้องมี · M=คูณ/หาร · F=float single · D=float double · A=atomic (multi-core/concurrency) · C=compressed (คำสั่ง 16 บิตที่ใช้บ่อย ลดขนาดโค้ด) ทิศทางรันเป็น subset⊂superset: โปรแกรม base ล้วนรันบนชิปที่มี extension เพิ่มได้สบาย แต่ถ้าใช้ mul/float ไปรันบนชิป base เปล่า จะเจอ illegal instruction
จาก 8 register สู่ 32 register
LC-3 มี register แค่ 8 ตัว (3 บิตเลือก) RV32I มี 32 register (5 บิต เพราะ 2^5=32) กว้าง 32 บิตเท่ากัน การมี register มากขึ้นช่วยลดการ spill ค่าลง memory: compiler เก็บตัวแปรที่ใช้บ่อยไว้บน register ได้มากกว่า
ฝั่ง RV32I: a..f มาใน a0–a5 ตาม ABI อยู่แล้ว งานจบใน register ไม่แตะ memory เลย (5 คำสั่ง) ฝั่ง LC-3: register เหลือ scratch แค่ R0,R1 ตัวแปรทั้ง 6 ต้องอยู่ใน memory และผลรวมย่อยต้อง spill ลง memory ชั่วคราว
| LC-3 (8 register) | RV32I (32 register) | |
|---|---|---|
| จำนวนคำสั่ง | 15 | 5 |
| แตะ memory ฝั่งข้อมูล | 10 ครั้ง | 0 ครั้ง |
x0: register ที่ถูก hardwired เป็นศูนย์ อ่านได้ค่า 0 เสมอ เขียนทับไม่ได้ ประโยชน์: ทำ NOP ด้วย addi x0,x0,0, ก็อปปี้ค่าด้วย add x6,x7,x0, เทียบกับศูนย์ด้วย beq x5,x0,LABEL โดยไม่เปลือง register จริง
ABI: register แต่ละตัวมีหน้าที่
ตัวจำลอง "RISC-V Register File" จากเอกสารต้นฉบับ — คลิก register แต่ละตัวเพื่อดู ABI/หน้าที่/ใครรักษาค่า หรือกดปุ่มกรองเฉพาะกลุ่ม:
ตารางอ้างอิงฉบับเต็ม (สาระเดียวกับตัวจำลองด้านบน):
| register | ชื่อ ABI | หน้าที่ | ใครรักษาค่า |
|---|---|---|---|
| x0 | zero | ค่าศูนย์เสมอ | — (คงที่) |
| x1 | ra | return address | caller-saved |
| x2 | sp | stack pointer | callee-saved |
| x3 | gp | global pointer | reserved (ตั้งค่าตอน startup) |
| x4 | tp | thread pointer | reserved (ตั้งค่าตอน startup) |
| x5–x7 | t0–t2 | temporary | caller-saved |
| x8 | s0 / fp | saved / frame pointer | callee-saved |
| x9 | s1 | saved | callee-saved |
| x10–x17 | a0–a7 | argument / return value | caller-saved |
| x18–x27 | s2–s11 | saved | callee-saved |
| x28–x31 | t3–t6 | temporary | caller-saved |
สรุปจำง่าย: argument/return value → a0–a7, temporary → t0–t6, saved → s0–s11, return address → ra, stack pointer → sp, fp คือชื่อแฝงของ s0
Caller-saved vs Callee-saved
หัวใจของ calling convention: ใครรับผิดชอบเก็บค่า register ข้าม subroutine call — Caller-saved (t0–t6, a0–a7, ra): callee มีสิทธิ์ทับได้ตามใจ ถ้า caller ยังต้องใช้ค่าต้องเซฟเองก่อนเรียก · Callee-saved (s0–s11, sp): callee ต้องเซฟค่าเดิมลง stack ตอนเข้า แล้ว restore ก่อน return การแบ่งสองกลุ่มเป็นการประนีประนอม (trade-off) ให้ compiler วางตัวแปร "อายุยาว" ไว้ใน s-register และตัวแปร "อายุสั้น" ไว้ใน t-register ลดงานเซฟโดยรวม
- เป็นอาร์กิวเมนต์/ค่าคืนหรือไม่ → ใช่: ใช้ a0–a7 เท่านั้น
- ไม่ใช่ → ต้องรอดข้าม call หรือไม่ → ไม่ข้าม: ใช้ t0–t6 (ถูกที่สุด ไม่มีภาระเซฟ)
- ต้องรอดข้าม call → ใช้ s0–s11 และทำหน้าที่ callee ให้ครบ (เซฟก่อนใช้ restore ก่อน return)
# โครงร่าง subroutine RISC-V ตามมาตรฐาน ABI
func:
addi sp, sp, -16 # จองเฟรมบน stack
sw ra, 12(sp) # เซฟ return address
sw s0, 8(sp) # เซฟ s0 (callee-saved)
# ... ตัวงาน ...
lw s0, 8(sp) # restore s0
lw ra, 12(sp) # restore ra
addi sp, sp, 16 # คืนพื้นที่ stack
ret # = jalr x0, 0(ra)
ฟังก์ชัน calc ใช้ s0,s1 และเรียกฟังก์ชันลูก (ต้องเซฟ ra ด้วย) เข้าฟังก์ชันตอน sp=0x9000, ra=0x40C, s0เดิม=0x11, s1เดิม=0x22
calc:
addi sp, sp, -16 # (1) จองเฟรม 16 byte
sw ra, 12(sp) # (2)
sw s0, 8(sp) # (3)
sw s1, 4(sp) # (4)
# ... ตัวงาน ...
lw s1, 4(sp) # (5) pop ย้อนลำดับ
lw s0, 8(sp) # (6)
lw ra, 12(sp) # (7)
addi sp, sp, 16 # (8)
ret
ทบทวนความเข้าใจ
Q: ความสามารถใดอยู่ใน base RV32I ตั้งแต่แรก (ไม่ต้องเพิ่ม extension)
A: การเลื่อนบิตหลายตำแหน่งด้วย sll/srl/sra — mul/div/rem อยู่ใน extension M ส่วน float อยู่ใน extension F/D
Q: register กลุ่มใดที่ "callee ต้องเซฟ/รีสโตร์เองถ้าจะใช้"
A: s0–s11 (saved) — t0–t6 และ a0–a7 เป็น caller-saved ซึ่ง callee มีสิทธิ์ทับได้
อ้างอิง
- D. Patterson และ J. Hennessy, Computer Organization and Design (RISC-V Edition) — บทที่ 2
- Y. Patt และ S. Patel, Introduction to Computing Systems — ส่วน LC-3 (บทที่ 1–9)
10.4 Little-Endian, Addressing และตัวอย่างเขียนใหม่
โมเดลหน่วยความจำ: LC-3 vs RV32I
Word-addressable (LC-3): หนึ่งที่อยู่ชี้ไปยังหนึ่ง word ขนาด 16 บิต เพิ่มที่อยู่ทีละ 1 คือเลื่อนไปอีกหนึ่ง word เต็ม ๆ ไม่มีแนวคิด "ทีละ byte" ในระดับ ISA พื้นฐาน
Byte-addressable (RV32I): หนึ่งที่อยู่ชี้ไปยังหนึ่ง byte หนึ่ง word 32 บิต = 4 byte ดังนั้น word ที่อยู่ติดกันห่างกัน 4 (เช่น 0x1000, 0x1004, 0x1008...) เดินอาเรย์ของ word จึงบวกทีละ 4 ไม่ใช่ทีละ 1
โปรแกรมจริงต้องจัดการข้อมูลหลายขนาด (ตัวอักษร 1 byte, จำนวนเต็มสั้น 2 byte, word 4 byte) การกำหนดที่อยู่ทีละ byte ทำให้เข้าถึงข้อมูลแต่ละขนาดได้ตรง ๆ RV32I จึงมีคำสั่ง load/store หลายขนาด: lw/sw — word (4 byte) · lh/lhu/sh — halfword (2 byte; lh sign-extend, lhu zero-extend) · lb/lbu/sb — byte (1 byte; lb sign-extend, lbu zero-extend) คำสั่งที่ดึงข้อมูลเล็กกว่า 32 บิตต้องเลือกว่าจะขยายเครื่องหมายหรือขยายด้วยศูนย์ — จุดนี้ไม่มีใน LC-3 เพราะทุกอย่าง 16 บิตเท่ากันหมด
Addressing mode: base + offset
# RV32I: โหลด word จาก (x10+8) ไปที่ x5
lw x5, 8(x10)
sw x6, 0(x10)
immediate offset เป็นค่า 12 บิตแบบมีเครื่องหมาย ครอบคลุมช่วง -2048 ถึง +2047 byte เทียบกับ LDR/STR ของ LC-3 (base+offset6) ที่แนวคิดเดียวกันแต่ offset แคบกว่าและนับเป็น word
อาเรย์ A (int 32 บิต) เริ่ม base=0x2000 (x10) หาที่อยู่ A[5]
- แต่ละ word=4 byte → ระยะห่างระหว่างสมาชิก=4 byte
- addr(A[i]) = base + 4×i
- i=5: offset=4×5=20=0x14 → addr=0x2000+0x14=0x2014
lw x5, 20(x10) — ถ้าคิดแบบ word-addressable (LC-3) จะได้ 0x2005 ผิด! (ตกกลาง byte ของ A[1])Little-Endian: byte เรียงอย่างไรในหน่วยความจำ
RV32I มาตรฐานเป็น little-endian: กติกา 3 ขั้น — (1) หั่นค่าใน register เป็น 4 byte จากต่ำสุดไปสูงสุด (2) วาง byte ต่ำสุดที่ address ต่ำสุด ไล่ byte ถัดไปทีละ 1 (3) อ่านกลับทำย้อนทาง
x5=0x11223344, x10=0x1000, รัน sw x5,0(x10)
| address | byte ที่เก็บ | หมายเหตุ |
|---|---|---|
| 0x1000 | 0x44 | least-significant byte |
| 0x1001 | 0x33 | |
| 0x1002 | 0x22 | |
| 0x1003 | 0x11 | most-significant byte |
หน่วยความจำที่ 0x1000 เก็บ 0xFF รัน lb x5,0(x10) และ lbu x6,0(x10)
debugger แสดง 0x2000: 78 56 34 12 — รัน lw x5,0(x10) (x10=0x2000) บนเครื่อง little-endian vs big-endian ได้ค่าใด
| กติกาเครื่อง | วิธีประกอบ | ค่า word |
|---|---|---|
| little-endian | byte ที่ address ต่ำ = byte ต่ำของค่า | 0x12345678 |
| big-endian | byte ที่ address ต่ำ = byte สูงของค่า | 0x78563412 |
PC-relative addressing
ทั้งสอง ISA ใช้ที่อยู่อ้างอิงกับ PC สำหรับกระโดด/กระโดดมีเงื่อนไข เพื่อให้โค้ดย้ายที่อยู่ได้ (position-independent) และ encode ระยะกระโดดด้วยบิตน้อย LC-3: BR, JSR, LD, LDI, LEA ใช้ PCoffset9/11 บวกกับ PC ที่ชี้คำสั่งถัดไป RV32I: branch และ jal ใช้ offset ที่บวกกับที่อยู่ของคำสั่ง branch เอง (ไม่ใช่ PC+4) และ offset เป็นหน่วย byte เป็นพหุคูณของ 2 เสมอ
ขั้นตอนแปลโปรแกรม LC-3 → RV32I อย่างเป็นระบบ (5 ขั้น)
- แมปตัวแปร → รีจิสเตอร์ — ทำตารางก่อนว่ารีจิสเตอร์ LC-3 แต่ละตัวถือค่าอะไร จับคู่กับรีจิสเตอร์ RV32I ให้ครบก่อนแปลแม้แต่บรรทัดเดียว
- แปลงคำสั่งเข้าถึงหน่วยความจำ (offset×4) — LDR/STR → lw/sw
- แปลง branch — หาว่า condition code มาจากรีจิสเตอร์ตัวไหน แล้วเขียนการเปรียบเทียบให้ชัด (บ่อยครั้งคือเทียบกับ x0)
- แปลง call/return — JSR label → jal x1,label · RET → jalr x0,0(x1)
- ตรวจด้วย trace — เดินโปรแกรมทั้งสองเวอร์ชันด้วยข้อมูลทดสอบเดียวกัน เทียบค่าทุกตัวแปรทุกรอบ
; LC-3: R1=base, R2=N, R0=ผลรวม
AND R0, R0, #0
ADD R2, R2, #0
LOOP BRz DONE
LDR R3, R1, #0
ADD R0, R0, R3
ADD R1, R1, #1 ; เดินตัวชี้ +1 word
ADD R2, R2, #-1
BRnzp LOOP
DONE HALT
# RV32I: x10=base, x11=N, x12=ผลรวม
addi x12, x0, 0
loop: beq x11, x0, done
lw x13, 0(x10)
add x12, x12, x13
addi x10, x10, 4 # เดินตัวชี้ +4 byte
addi x11, x11, -1
jal x0, loop
done:
| จุดตรวจ | x10 | x11 | x12 |
|---|---|---|---|
| ก่อนเข้าลูป | 0x2000 | 3 | 0 |
| จบรอบ 1 (อ่าน 10) | 0x2004 | 2 | 10 |
| จบรอบ 2 (อ่าน 20) | 0x2008 | 1 | 30 |
| จบรอบ 3 (อ่าน 30) | 0x200C | 0 | 60 |
จุดต่างชัดเจน: (1) LC-3 เดินตัวชี้ +1 แต่ RV32I เดิน +4 byte เพราะ word ห่างกัน 4 byte (2) เคลียร์ศูนย์: LC-3 ใช้ AND #0, RV32I ใช้ addi ร่วมกับ x0 (3) วนลูปไม่มีเงื่อนไขใน RV32I ใช้ jal x0,loop
ทบทวนความเข้าใจ
Q: เก็บ 0x11223344 ด้วย sw ที่ address A บน RV32I (little-endian) แล้วอ่าน lbu จาก A+0 ได้ค่าใด
A: 0x44 — least-significant byte อยู่ address ต่ำสุดเสมอ
Q: ลูปบวกอาเรย์เวอร์ชัน RV32I ต้องเพิ่มตัวชี้ครั้งละกี่ byte เพื่อเลื่อนไป word ถัดไป
A: 4 byte (word=32 บิต=4 byte) เทียบกับ LC-3 ที่เลื่อนทีละ 1 เพราะ word-addressable
อ้างอิง
- D. Patterson และ J. Hennessy, Computer Organization and Design (RISC-V Edition) — บทที่ 2
- Y. Patt และ S. Patel, Introduction to Computing Systems — ส่วน LC-3 (บทที่ 1–9)