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

ถ้าจำได้ประโยคเดียวก้าวจาก LC-3 ไป RISC-V ไม่ใช่การเริ่มใหม่ แต่เป็นแนวคิดเดิมที่ขยายให้ใช้งานจริงได้
หัวข้อเนื้อหา
10.1ทำไมต้องย้าย — LC-3 ให้หลักการอะไรไว้ ขาดอะไร และ RV32I เติมเต็มตรงไหน พร้อมตารางเทียบครบทุกมิติ
10.2คำสั่ง LC-3 ที่รู้จัก เขียนเป็น RV32I อย่างไร — ตารางแมปครบทั้งกลุ่มคำนวณ/ลอจิก/หน่วยความจำ และกลุ่ม address/branch/control
10.3RV32I มีอะไรที่ LC-3 ไม่เคยมี — รีจิสเตอร์แบบมีบทบาท (ABI), calling convention ผ่าน sp/ra, คูณ/หาร/เลื่อนบิต/ทศนิยม และแนวคิด modular ISA
10.4ข้อมูลวางในหน่วยความจำแบบไหน — little-endian, การอ้างที่อยู่รายไบต์ และตัวอย่างเขียนโปรแกรม LC-3 เดิมใหม่เป็น RV32I เต็มโปรแกรม
🎮 เครื่องมือ RISC-V ต้นฉบับแนะนำให้ลองเล่นคู่กับเนื้อหานี้ Venus — เขียน RISC-V assembly แล้วรันในเบราว์เซอร์ ดูค่า register/memory ทีละคำสั่ง (ไม่ต้องติดตั้ง) · rvcodec.js — พิมพ์คำสั่ง RV32I แล้วเห็นการเข้ารหัสเป็นบิตแยกทุกฟิลด์ · Compiler Explorer — เขียนภาษา C แล้วดูมันกลายเป็น RISC-V assembly แบบสด · Ripes — โปรแกรมเดสก์ท็อป เห็น datapath/pipeline/cache เคลื่อนไหวจริง (เหมาะกับ Topic 12–13)

ด้านล่างคือตัวจำลอง "พจนานุกรม 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 แบบเครื่องสมัยใหม่ได้ลำบาก

flowchart LR A["LC-3 16-bit เครื่องสอน"] --> B{"พอสำหรับหลักการพื้นฐาน?"} B -->|"ใช่"| C["datapath, control, assembly เบื้องต้น"] B -->|"ไม่พอ"| D["toolchain จริง, career, สเกลขึ้น?"] D --> E["ต้องไป RISC-V RV32I"]
ทำไมต้องรู้ + CPU ใช้ยังไงความกว้างของ address และคำสั่งไม่ใช่แค่ตัวเลขบนกระดาษ แต่เป็นข้อจำกัดทางฟิสิกส์ที่เดินสายไปทั้ง datapath จริง: CPU ต้องมีเส้นลวด (bus) และตัวบวกใน PC/MAR กว้างเท่าจำนวนบิตของ address ถ้ากว้าง 16 บิต วงจรจะแตะหน่วยความจำได้แค่ 2^16 ตำแหน่ง ไม่ว่าจะติดแรมมาเท่าไร การกระโดดไป 32 บิตคือการขยาย "ระยะเอื้อม" ของทั้งชิป
ตัวอย่าง — ขยาย address space กี่เท่า

LC-3 ใช้ address กว้าง 16 บิต ส่วน RV32I ใช้ 32 บิต แต่ละเครื่องระบุที่อยู่ได้กี่ตำแหน่ง และ RV32I มากกว่ากี่เท่า

  1. จำนวนตำแหน่งที่ address n บิตระบุได้ = 2^n
  2. LC-3: n=16 → 2^16 = 65,536 ตำแหน่ง (64 Ki word = ~64 KiB)
  3. RV32I: n=32 → 2^32 = 4,294,967,296 ตำแหน่ง (byte-addressable = 4 GiB)
  4. มากกว่ากี่เท่า = 2^32 / 2^16 = 2^16 = 65,536 เท่า
สรุป: เพิ่ม address จาก 16 เป็น 32 บิต ขยายพื้นที่ที่อยู่ขึ้น 65,536 เท่า

เทียบกันชัด ๆ: LC-3 กับ RV32I ครบทุกมิติ

มิติLC-3 (เครื่องสอน)RV32I (ISA จริง)
ความกว้างข้อมูล/รีจิสเตอร์16 บิต32 บิต
ความกว้างคำสั่ง16 บิตคงที่32 บิตคงที่ (ในฐาน I)
จำนวน/ชื่อรีจิสเตอร์8 ตัว: R0–R732 ตัว: x0–x31 (x0 = ศูนย์เสมอ, มีชื่อ ABI เช่น zero, ra, sp, a0, t0)
ขนาด address space2^16 = 65,536 ตำแหน่ง (ทีละ word 16 บิต)2^32 ไบต์ = 4 GiB (รายไบต์)
จำนวนคำสั่งราว 15 opcodeราว 40 คำสั่งในฐาน (ยังไม่นับส่วนขยาย)
ความกว้าง immediate5 บิตใน 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
ตัวอย่าง — ทำไมต้องใช้ 5 บิตเลือกรีจิสเตอร์

LC-3 ใช้ 3 บิตเลือกรีจิสเตอร์ (8 ตัว) ส่วน RV32I ต้องเลือกได้ 32 ตัว ต้องใช้กี่บิต

  1. จำนวนรีจิสเตอร์ที่เลือกได้ = 2^(จำนวนบิต)
  2. 4 บิต → 2^4 = 16 ตัว (น้อยไป)
  3. 5 บิต → 2^5 = 32 ตัว (พอดีกับ x0–x31)
5 บิตคือจำนวนน้อยที่สุดที่พอ — คำสั่ง R-type จึงมี field เลือกรีจิสเตอร์ 3 ชุด (rd, rs1, rs2) ชุดละ 5 บิต = 15 บิต
flowchart LR R["RV32I (ฐาน)"] --> M["+M คูณ/หาร"] R --> F["+F/D ทศนิยม"] R --> C["+C compressed"] R --> S["intro → Ripes → gem5, ISA เดิม สเกลขึ้น"]

บันไดอ่านโค้ด RV32I สามระดับ

วิธีอ่านโค้ด RV32I: (1) อ่านชื่อคำสั่งก่อนว่าอยู่กลุ่มไหน — คำนวณ (add/addi), หน่วยความจำ (lw/sw), หรือควบคุมทิศทาง (beq/jal) (2) หาปลายทางของผลลัพธ์ — กลุ่มคำนวณ/load เขียนลงรีจิสเตอร์ rd แต่ sw เขียนลงหน่วยความจำ (3) ไล่ค่าทีละบรรทัดลงตาราง อย่าข้ามบรรทัด

ระดับ 1 (ง่าย) — คำนวณล้วน
addi x5, x0, 7      # x5 = 0 + 7
add  x6, x5, x5     # x6 = x5 + x5
x5 = 7, x6 = 14 — สังเกต: add rd,rs,rs (บวกตัวเอง) คือสำนวนมาตรฐานของ "คูณ 2" เหมือน LC-3 (ADD R1,R1,R1)
ระดับ 2 (กลาง) — มี lw/sw

หน่วยความจำ: 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
x28 = 30, M[0x1008] = 30 — lw/sw คือญาติของ LDR/STR ใน LC-3 (base+offset เหมือนกัน) ต่างที่ offset นับเป็นไบต์
ระดับ 3 (ยาก) — ลูปสั้นที่มี beq กับ jal
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:
x5 ไล่ค่า 2,4,6,8 — สามรอบแรก beq ไม่กระโดด รอบที่ 4 ได้ x5=8=x6 พอดี beq กระโดดออก รวมรัน 4 รอบ x5 จบที่ 8 — jal x0,loop คือ "กระโดดไม่มีเงื่อนไข" (ทิ้ง return address ลง x0) เทียบได้กับ BRnzp ของ LC-3

สังเกต: ลูปนี้คือโครงเดียวกับลูป 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 เสมอ

ทำไมต้องรู้ + CPU ใช้ยังไงตัว CPU จริงไม่ได้ "เข้าใจ" คำสั่งเชิงแนวคิด มันถอดรหัส opcode จริงในฮาร์ดแวร์แล้วสั่ง ALU/หน่วยความจำทำงาน การเทียบ LC-3 กับ RV32I ทีละคำสั่งทำให้เห็นว่า opcode ที่ RV32I "มี" กับ "ไม่มี" (เช่น ไม่มี NOT โดยตรง) บังคับให้ compiler ต้องเลือกลำดับคำสั่งจริงที่ CPU รันได้ — จำเป็นสำหรับตอนสร้าง datapath ในโปรเจกต์ปลายภาค

กลุ่มที่ 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เคลียร์เป็น 0andi 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 ให้อัตโนมัติ

ตัวอย่าง — xori เป็น NOT จริงหรือ

x2 (8 บิตล่าง) = 1010 0101 หาผลของ xori x1,x2,-1

  1. -1 = 0xFFFFFFFF → ทุกบิตเป็น 1 (8 บิตล่างของ mask = 1111 1111)
  2. XOR ทีละบิต (a XOR 1 = NOT a): 1010 0101 XOR 1111 1111 = 0101 1010
  3. ตรวจ: กลับทุกบิตของ 1010 0101 = 0101 1010 ✓ ตรงกัน
x1 (8 บิตล่าง) = 0101 1010 = 0x5A — NOT กระทำทั้ง 32 บิต บิตบนที่เดิมเป็น 0 กลับเป็น 1 ทั้งหมดด้วย

กลุ่มที่ 2: เข้าถึงหน่วยความจำ (Load / Store)

จุดต่างเชิงปรัชญาที่สำคัญที่สุด: LC-3 มีโหมดที่อยู่หลายแบบ (PC-relative และ base+offset) ส่วน RV32I เป็น load-store architecture อย่างเคร่งครัด — คำสั่งคำนวณทำงานกับรีจิสเตอร์เท่านั้น การแตะหน่วยความจำต้องผ่าน load/store เสมอ

LC-3โหมดRV32I ที่เทียบเท่า
LDR R1,R2,#offbase + offsetlw x1, off(x2)
STR R1,R2,#offbase + offsetsw x1, off(x2)
LD R1,LABELPC-relativeต้องคำนวณที่อยู่ก่อน แล้ว lw
ST R1,LABELPC-relativeคำนวณที่อยู่ก่อน แล้ว sw

LD/ST ของ LC-3 (PC-relative) ไม่มีคู่ตรงใน RV32I ต้องสร้างที่อยู่ของ LABEL ไว้ในรีจิสเตอร์ก่อน (กลุ่มที่ 3) แล้วจึง lw/sw ผ่าน base register นั้น

กับดักอันดับหนึ่ง: offset นับหน่วยต่างกันLC-3 เป็น word-addressable — offset ใน LDR/STR นับเป็น "จำนวน word" ส่วน RV32I เป็น byte-addressable — offset ใน lw/sw นับเป็น "จำนวน byte" และหนึ่ง word ของ RV32I กว้าง 4 byte ขั้นตอนแปล offset: อ่าน offset ของ LC-3 → คูณด้วย 4 → ตรวจว่าอยู่ในช่วง imm12 (−2048..+2047) แล้วจึงเขียน lw/sw
LC-3 offset (word)คูณ 4RV32I offset (byte)ตัวอย่าง
#33×412LDR R1,R2,#3 → lw x1,12(x2)
#1010×440LDR R1,R2,#10 → lw x1,40(x2)
#-2(-2)×4-8STR R5,R2,#-2 → sw x5,-8(x2)
ตัวอย่างยาก — แปลบล็อกโหลด-AND-เก็บ
; 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-relativeauipc 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น้อยกว่า 0blt rs,x0,LABELbltz rs,LABEL
BRz LABELเท่ากับ 0beq rs,x0,LABELbeqz rs,LABEL
BRp LABELมากกว่า 0blt x0,rs,LABELbgtz rs,LABEL
BRnz LABELน้อยกว่าหรือเท่ากับ 0bge x0,rs,LABELblez rs,LABEL
BRnp LABELไม่เท่ากับ 0bne rs,x0,LABELbnez rs,LABEL
BRzp LABELมากกว่าหรือเท่ากับ 0bge rs,x0,LABELbgez rs,LABEL
BRnzp LABELกระโดดเสมอjal x0,LABELj LABEL

จุดสังเกต: (1) BRp/BRnz ต้องสลับ rs ไปฝั่งขวา เพราะ RV32I มีแต่ blt/bge ไม่มี bgt/ble ตรง ๆ (2) BRnzp คือเงื่อนไขที่จริงเสมอ จึงกลายเป็น jump ธรรมดา jal x0,LABEL (ทิ้ง return address ลง x0)

สูตรแปล branch แบบ 1-2-3 (idiom "เทียบกับศูนย์")
  1. หารีจิสเตอร์ที่ตั้ง CC ล่าสุด — มองย้อนจาก BR ขึ้นไป คำสั่งที่เขียนค่าล่าสุดคือตัวตั้ง N/Z/P
  2. แปลงเงื่อนไข flag เป็นการเทียบกับศูนย์: z="เท่ากับ 0", n="น้อยกว่า 0", p="มากกว่า 0"
  3. เลือกคำสั่ง 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 เป็นจริงเสมอ

ตัวอย่างยาก — แปลลูปนับถอยหลัง (ไม่พึ่ง flag ค้าง)
LOOP  ADD R2, R2, #-1   ; ตั้ง CC จากผลลัพธ์
      BRp LOOP          ; วนถ้าผลลัพธ์มากกว่า 0
  1. BRp วน "ถ้าผลล่าสุด (R2) มากกว่า 0" (signed)
  2. "R2 มากกว่า 0" = "0 น้อยกว่า R2" (สลับตัวถูกเทียบเพราะไม่มี bgt ตรง ๆ)
  3. เลือก 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)

ตัวอย่าง — ไล่ค่า ra ด้วยที่อยู่จริง

jal x1, my_func อยู่ที่ 0x1000, my_func อยู่ที่ 0x1010 (+16 byte)

ra = PC ของ jal + 4 = 0x1004 (ที่อยู่คำสั่งถัดจาก jal เสมอ ไม่ใช่ที่อยู่ my_func) — jalr x0,0(x1) ที่ 0x1014 พากลับไป PC=0x1004 (นี่คือ ret) — LC-3 ทำแบบเดียวกันเป๊ะ ต่างแค่ RV32I บวก 4 (หนึ่งคำสั่ง=4 byte) ส่วน LC-3 บวก 1 (นับที่อยู่เป็น word)

กลุ่มที่ 6: เรียกบริการระบบ (Trap)

LC-3ความหมายRV32I ที่เทียบเท่า
TRAP x25 (HALT)เรียกบริการ OS/firmwareecall
GETC/OUT/PUTSบริการ I/O ผ่าน trap vectorecall (ผ่าน 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 เทียบเคียง
พิมพ์จำนวนเต็ม1a0 = ค่าที่จะพิมพ์(ไม่มีตรง ๆ)
พิมพ์สตริง4a0 = ที่อยู่สตริงTRAP x22 (PUTS)
อ่านจำนวนเต็ม5ผลกลับใน a0(ไม่มีตรง ๆ)
จบโปรแกรม10-TRAP x25 (HALT)
พิมพ์อักขระ11a0 = รหัสอักขระTRAP x21 (OUT)
อ่านอักขระ12ผลกลับใน a0TRAP x20 (GETC)
ecall ไม่ใช่คำสั่ง "จบโปรแกรม"ecall แปลว่า "ขอบริการจากระบบ" เฉย ๆ พอระบบทำเสร็จก็คืนการควบคุมกลับมาคำสั่งถัดไปแล้วเดินต่อ เหมือนเรียกฟังก์ชัน สิ่งที่ตัดสินว่าเกิดอะไรขึ้นคือเลขใน a7 ไม่ใช่ตัวคำสั่ง ecall เอง (ทุกครั้งมีรหัสเครื่องเดียวกันคือ 0x00000073) มีเพียงบริการหมายเลข 10 (จบโปรแกรม) เท่านั้นที่ไม่คืนกลับมา

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
ตัวอย่าง — srai กับ srli ให้ผลไม่เหมือนกัน

x6 = -16 (0xFFFFFFF0) หาผลของ srai x5,x6,2 เทียบกับ srli x5,x6,2

  1. srai (arithmetic — เติมซ้ำ sign bit=1 ทางซ้าย): ผล = 0xFFFFFFFC = -4 (ตรวจ: -16÷4=-4 ✓)
  2. srli (logical — เติม 0 ทางซ้าย): ผล = 0x3FFFFFFC = 1,073,741,820 (กลายเป็นเลขบวกมหึมา เพราะ 0 ทำลายบิตเครื่องหมาย)
สรุป: เลขมีเครื่องหมาย เลื่อนขวาด้วย srai · เลขไม่มีเครื่องหมาย เลื่อนขวาด้วย srli — LC-3 ไม่มีคำสั่งเลื่อนขวาเลยสักแบบ
ตัวอย่าง — คูณด้วยซอฟต์แวร์ (LC-3) เทียบ mul คำสั่งเดียว
; 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)41
คำสั่งที่ execute จริง (คูณ 12)371
ไซเคิลโดยประมาณ~185 (5 ไซเคิล/คำสั่ง)1–3
ถ้าตัวคูณเป็น 10003001 คำสั่ง — โตตามค่าตัวคูณ1 คำสั่งเท่าเดิม
เวลาทำงานของลูปโตตาม "ค่า" ของตัวคูณ ส่วน mul ใช้วงจรคูณจริงจึงใช้เวลาคงที่ — เหตุผลที่ extension M คุ้มทรานซิสเตอร์สำหรับงานที่คูณบ่อย
ตัวอย่างยาก — คูณแบบ shift-add ด้วย RV32I ล้วน (ไม่มี M ก็คูณฉลาดได้)

คูณ 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
x10=6(ตัวตั้ง), x11=5=101(ตัวคูณ): รอบ1 บิต=1→x12=6, x10=12,x11=2 · รอบ2 บิต=0→x12=6เท่าเดิม, x10=24,x11=1 · รอบ3 บิต=1→x12=6+24=30, x10=48,x11=0 หมดบิต → x12=30=6×5 ✓ (วนแค่ 3 รอบ = จำนวนบิตของตัวคูณ) — เทียบตัวคูณ=1000: บวกซ้ำ 1000 รอบ, shift-add 10 รอบ (โตตามจำนวนบิต), mul 1 คำสั่งจบ

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 ตามงาน

flowchart LR I["RV32I base (~40 คำสั่ง บังคับมี)"] --> M["+ M (mul/div)"] I --> F["+ F (float 32-bit)"] I --> D["+ D (float 64-bit)"] I --> A["+ A (atomic)"] I --> C["+ C (compressed 16-bit)"]

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

ทำไมต้องรู้ + CPU ใช้ยังไงmodular ISA กำหนดว่าจะมีทรานซิสเตอร์ชุดไหนอยู่บนซิลิคอนจริง: barrel shifter เป็นวงจรใน ALU ที่ขยับทุกบิตพร้อมกันในหนึ่งรอบสัญญาณนาฬิกา ส่วนตัวคูณ/หาร (M) และหน่วยทศนิยม (F/D) เป็นบล็อกฮาร์ดแวร์ใหญ่ที่กินพื้นที่/ไฟ — ไมโครคอนโทรลเลอร์ในเครื่องซักผ้าอาจเอาแค่ RV32I เปล่า ส่วน CPU เซิร์ฟเวอร์ใส่ครบ M/F/D/A

จาก 8 register สู่ 32 register

ทำไมต้องรู้ + CPU ใช้ยังไงcalling convention คือกติกาที่ทำให้โค้ดจากคนละไฟล์/ภาษา/compiler เรียกฟังก์ชันกันได้จริงบน CPU เดียวกัน CPU เห็นแค่คำสั่ง jal/jalr ที่กระโดดไป-กลับ และ register file 32 ตัวที่ทุกส่วนของโปรแกรมใช้ร่วมกัน ถ้าไม่มีข้อตกลงว่าใครรักษาค่า register ตัวไหน ค่าจะถูกทับพังทันทีที่ call ข้ามฟังก์ชัน

LC-3 มี register แค่ 8 ตัว (3 บิตเลือก) RV32I มี 32 register (5 บิต เพราะ 2^5=32) กว้าง 32 บิตเท่ากัน การมี register มากขึ้นช่วยลดการ spill ค่าลง memory: compiler เก็บตัวแปรที่ใช้บ่อยไว้บน register ได้มากกว่า

ตัวอย่าง — total=(a+b)+(c+d)+(e+f) บน 8 กับ 32 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)
จำนวนคำสั่ง155
แตะ memory ฝั่งข้อมูล10 ครั้ง0 ครั้ง
ค่าที่ต้อง "มีชีวิตพร้อมกัน" มีสูงสุด 9 ค่า มากกว่า register ว่างจริงของ LC-3 จึงถูกบังคับ spill เสมอ ส่วน RV32I มี 32 ตัว ค่าทั้งหมดอยู่บน register ได้ถาวรตลอดฟังก์ชัน — ความต่างไม่ได้อยู่ที่จำนวนคำสั่งอย่างเดียว แต่อยู่ที่การแตะ memory ซึ่งช้ากว่า register หลายเท่า

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หน้าที่ใครรักษาค่า
x0zeroค่าศูนย์เสมอ— (คงที่)
x1rareturn addresscaller-saved
x2spstack pointercallee-saved
x3gpglobal pointerreserved (ตั้งค่าตอน startup)
x4tpthread pointerreserved (ตั้งค่าตอน startup)
x5–x7t0–t2temporarycaller-saved
x8s0 / fpsaved / frame pointercallee-saved
x9s1savedcallee-saved
x10–x17a0–a7argument / return valuecaller-saved
x18–x27s2–s11savedcallee-saved
x28–x31t3–t6temporarycaller-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 ลดงานเซฟโดยรวม

ขั้นตอนตัดสินใจเลือกกลุ่ม register (ใช้ได้ทุกครั้ง)
  1. เป็นอาร์กิวเมนต์/ค่าคืนหรือไม่ → ใช่: ใช้ a0–a7 เท่านั้น
  2. ไม่ใช่ → ต้องรอดข้าม call หรือไม่ → ไม่ข้าม: ใช้ t0–t6 (ถูกที่สุด ไม่มีภาระเซฟ)
  3. ต้องรอดข้าม call → ใช้ s0–s11 และทำหน้าที่ callee ให้ครบ (เซฟก่อนใช้ restore ก่อน return)
ตัวอย่าง: ค่า address ชั่วคราวไม่มี call คั่น → t0 · sum สะสมในลูปที่เรียก print ทุกรอบ → s1 (ต้องรอดข้าม call) · อาร์กิวเมนต์ w,h ของ area(w,h) → a0,a1 ผลลัพธ์กลับใน a0
# โครงร่าง 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)
ตัวอย่างยาก — push/pop ผ่าน sp พร้อมผัง stack

ฟังก์ชัน 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
sp ใหม่ = 0x9000-16 = 0x8FF0 · address 0x8FFC=0x40C(ra), 0x8FF8=0x11(s0), 0x8FF4=0x22(s1) · หลัง pop ทั้งหมดกลับมาค่าเดิมทุกตัว sp กลับเป็น 0x9000 — pop ต้องย้อนลำดับ push เสมอ และ offset ตอน lw ต้องตรงกับตอน sw ทุกตัว ถ้าสลับช่องเดียว ค่าที่ restore จะสลับกันทั้งเฟรมโดยไม่มี error เตือน

ทบทวนความเข้าใจ

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

ทำไมต้องรู้ + CPU ใช้ยังไงทุกครั้งที่รัน lw/sw วงจร address generation ใน datapath จะบวก base register กับ offset แล้วส่งค่าออก address bus การที่ RV32I เป็น byte-addressable ทำให้ต้องบวกทีละ 4 เพื่อเดินอาเรย์ word และต้องมีวงจรเลือกขนาด (byte/halfword/word) พร้อมตรรกะ sign/zero extend ก่อนเขียนลงรีจิสเตอร์

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

flowchart LR subgraph LC3["LC-3 (word-addressable)"] A0["addr 0"] --> A1["addr 1"] --> A2["addr 2"] A0 -.- W0["word 16 บิต"] A1 -.- W1["word 16 บิต"] end subgraph RV["RV32I (byte-addressable)"] B0["addr 0"] --> B1["addr 1"] --> B2["addr 2"] --> B3["addr 3"] --> B4["addr 4"] B0 -.- WB["word = 4 byte (addr 0..3)"] B4 -.- WB2["word ถัดไปที่ addr 4"] end

โปรแกรมจริงต้องจัดการข้อมูลหลายขนาด (ตัวอักษร 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]

  1. แต่ละ word=4 byte → ระยะห่างระหว่างสมาชิก=4 byte
  2. addr(A[i]) = base + 4×i
  3. 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 เรียงอย่างไรในหน่วยความจำ

ทำไมต้องรู้ + CPU ใช้ยังไงendianness กำหนดว่าฮาร์ดแวร์วางบิตของ word ลงในแต่ละ byte ของหน่วยความจำอย่างไร ถ้าเข้าใจผิดจะอ่านค่าสลับหัวท้ายทันที — สำคัญเฉพาะเมื่อเข้าถึงข้อมูลก้อนเดียวกันด้วยขนาดต่างกัน หรือส่งข้อมูลข้ามเครื่อง ไม่กระทบเลขคณิตปกติที่อ่าน/เขียนเป็น word เต็มทั้งคู่ LC-3 ไม่มีประเด็นนี้ เพราะ word 16 บิตเป็นหน่วยที่อยู่เดียวที่เล็กที่สุด

RV32I มาตรฐานเป็น little-endian: กติกา 3 ขั้น — (1) หั่นค่าใน register เป็น 4 byte จากต่ำสุดไปสูงสุด (2) วาง byte ต่ำสุดที่ address ต่ำสุด ไล่ byte ถัดไปทีละ 1 (3) อ่านกลับทำย้อนทาง

ตัวอย่าง — sw หนึ่งครั้งแล้วส่องรายไบต์

x5=0x11223344, x10=0x1000, รัน sw x5,0(x10)

addressbyte ที่เก็บหมายเหตุ
0x10000x44least-significant byte
0x10010x33
0x10020x22
0x10030x11most-significant byte
ผังหน่วยความจำ little-endian ดู "กลับหัว" เมื่อเทียบกับลำดับที่เขียนบนกระดาษ (0x11 ที่เห็นก่อนตามสายตา กลับไปอยู่ address สูงสุด)
ตัวอย่าง — lb กับ lbu อ่าน byte เดียวกัน ได้คนละค่า

หน่วยความจำที่ 0x1000 เก็บ 0xFF รัน lb x5,0(x10) และ lbu x6,0(x10)

lb sign-extend: บิตสูงสุด(บิต7)=1 → ติดลบ → x5=0xFFFFFFFF=-1 · lbu zero-extend: x6=0x000000FF=255 — byte ในหน่วยความจำเป็นก้อนบิตเฉย ๆ ไม่มีเครื่องหมายติดตัว คำสั่งโหลดต่างหากที่เลือกการตีความ
ตัวอย่างยาก — โจทย์ย้อนกลับจาก byte dump

debugger แสดง 0x2000: 78 56 34 12 — รัน lw x5,0(x10) (x10=0x2000) บนเครื่อง little-endian vs big-endian ได้ค่าใด

กติกาเครื่องวิธีประกอบค่า word
little-endianbyte ที่ address ต่ำ = byte ต่ำของค่า0x12345678
big-endianbyte ที่ address ต่ำ = byte สูงของค่า0x78563412
byte ชุดเดียวกันแท้ ๆ ให้ word คนละค่า — การส่งข้อมูล binary ข้ามเครื่องต่างตระกูลต้องตกลง endianness กันก่อนเสมอ

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 ขั้น)

  1. แมปตัวแปร → รีจิสเตอร์ — ทำตารางก่อนว่ารีจิสเตอร์ LC-3 แต่ละตัวถือค่าอะไร จับคู่กับรีจิสเตอร์ RV32I ให้ครบก่อนแปลแม้แต่บรรทัดเดียว
  2. แปลงคำสั่งเข้าถึงหน่วยความจำ (offset×4) — LDR/STR → lw/sw
  3. แปลง branch — หาว่า condition code มาจากรีจิสเตอร์ตัวไหน แล้วเขียนการเปรียบเทียบให้ชัด (บ่อยครั้งคือเทียบกับ x0)
  4. แปลง call/return — JSR label → jal x1,label · RET → jalr x0,0(x1)
  5. ตรวจด้วย 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:
จุดตรวจx10x11x12
ก่อนเข้าลูป0x200030
จบรอบ 1 (อ่าน 10)0x2004210
จบรอบ 2 (อ่าน 20)0x2008130
จบรอบ 3 (อ่าน 30)0x200C060
รอบ 4: beq x11,x0 เห็น x11=0 → กระโดดออกไป done, x12=60=10+20+30 ✓
flowchart TD S["sum ← 0"] --> C{"N == 0 ?"} C -- "ใช่" --> D["done: ผลรวมพร้อมใช้"] C -- "ไม่" --> L["โหลดสมาชิก, sum += สมาชิก"] L --> P["ตัวชี้ += 4 byte, N -= 1"] P --> C

จุดต่างชัดเจน: (1) LC-3 เดินตัวชี้ +1 แต่ RV32I เดิน +4 byte เพราะ word ห่างกัน 4 byte (2) เคลียร์ศูนย์: LC-3 ใช้ AND #0, RV32I ใช้ addi ร่วมกับ x0 (3) วนลูปไม่มีเงื่อนไขใน RV32I ใช้ jal x0,loop

เกริ่นโปรเจกต์ปลายภาค: สร้าง CPU RV32I subsetความเข้าใจเรื่องโมเดลหน่วยความจำ endianness และ addressing เป็นพื้นฐานของโปรเจกต์ปลายภาค — ออกแบบและจำลอง CPU ที่รัน RV32I subset ขอบเขตแนะนำ: รองรับกลุ่มคำสั่งหลัก (add, sub, and, or, xor, slt, addi, lw, sw, beq, bne, blt, jal, jalr), datapath แบบ single-cycle ก่อนแล้วค่อยขยายเป็น pipeline, หน่วยความจำต้องเป็น byte-addressable และ little-endian ให้ตรงมาตรฐาน

ทบทวนความเข้าใจ

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)