อาจารย์ทิ้งจุดไข่ปลาไว้ (“จงเขียนโค้ด Z80 Assembly ……….”) แปลว่าโจทย์จะผูกกับฮาร์ดแวร์ในข้อ 1 — และข้อ 4 กับข้อ 5 จะเอาโค้ดนี้ไปใช้ต่อ เพราะฉะนั้น เขียนโค้ดที่แตะทั้งหน่วยความจำและ I/O จะได้เดินต่อได้ทั้งสองข้อ
ปีที่แล้วข้อ Assembly กับข้อ Delay เป็นคนละข้อกัน รวม 50 จาก 80 คะแนน และโจทย์ delay มาจาก Lab 3 ตรง ๆ — จุดไข่ปลาใน “จงเขียนโค้ด Z80 Assembly ……….” ของปีนี้จึงมีโอกาสสูงมากที่จะเป็นโจทย์นั้น
LD A,(nn) หรือ LD A,(HL))LD (nn),A)IN A,(n) และ OUT (n),A)ORG ให้ตรงกับ ROM ที่ออกแบบไว้ และ address ปลายทางให้อยู่ในช่วง RAMcpu กับ hof คืออะไร มาจากไหนทุกโปรแกรมในใบงาน Lab เริ่มด้วยสองบรรทัดนี้เสมอ แต่ไม่มีใครอธิบายว่ามันคืออะไร
cpu "z80.tbl"
hof "INT8"
org 2000h
... โปรแกรมของเรา ...
end
มาจากตัวโปรแกรมแปลภาษาที่ใช้ในแลป — คำสั่ง C16 ที่รันใน DOSBox
คือ Cross-16 Meta-Assembler เวอร์ชัน 1.21 ของบริษัท Universal Cross-Assemblers (ปี 1987)
ตรวจสอบจากไฟล์ C16.EXE ในโฟลเดอร์ z80 โดยตรง —
ข้อความในไฟล์เขียนว่า “Cross-16 Meta-Assembler PC/MS-DOS Version 1.21 · Copyright (C) 1987 Universal Cross-Assemblers”
cpu "z80.tbl" — บอกว่าจะแปลเป็นภาษาซีพียูตัวไหนคำว่า “Meta-Assembler” แปลว่าตัวมันเองไม่รู้จักคำสั่งของ CPU ตัวไหนเลย
มันเป็นแค่โครงเปล่า ๆ ที่ต้องโหลดตารางชุดคำสั่งเข้ามาก่อนถึงจะแปลได้
— เขียน cpu "z80.tbl" ก็คือสั่งให้ไปอ่านไฟล์ Z80.TBL
(อยู่ในโฟลเดอร์ z80 ที่ unzip มา) ข้างในหน้าตาแบบนี้
ADCA,(HL)^8E:
ADCA,A^8F:
ADCA,B^88:
ADDA,(HL)^86:
ADDA,A^87:
ADDA,B^80:
↑ ↑
mnemonic opcode
ถ้าเปลี่ยนบรรทัดนี้เป็น cpu "6502.tbl"
ตัวเดียวกันนี้ก็แปลภาษา 6502 ได้ทันที — นี่คือเหตุผลที่มันชื่อ Cross-16
hof "INT8" — บอกว่าจะออกไฟล์ผลลัพธ์เป็นรูปแบบไหนHOF ย่อมาจาก Hex Output Format
· INT8 คือ Intel HEX แบบ 8 บิต ซึ่งเป็นรูปแบบไฟล์ .h
ที่โปรแกรม Z80Sim โหลดได้ด้วยคำสั่ง L
รูปแบบอื่นที่ Cross-16 รองรับ:
BIN8 BIN16 INT8 INT16 MOT8 MOT16
ไฟล์ LAB1.H ที่ได้ออกมาหน้าตาแบบนี้
:0C2000003E0106020E0816038081827665
:00000001FF
| ส่วน | ค่า | ความหมาย |
|---|---|---|
| : | : | ตัวเริ่มเรคอร์ด |
| ByteCount | 0C | ข้อมูล 12 ไบต์ |
| Address | 2000 | เริ่มที่ 2000H (มาจาก org) |
| Type | 00 | เรคอร์ดข้อมูล |
| Data | 3E 01 06 02 … | machine code ของโปรแกรม |
| Checksum | 65 | ไว้ตรวจว่าไฟล์ไม่เพี้ยน |
เขียนไปด้วยจะดีกว่า — ใช้เวลาแค่ 2 บรรทัด และแสดงว่าเข้าใจว่าโปรแกรมจริง ๆ ต้องมีอะไรบ้าง ไม่ได้ท่องมาแค่ตัวคำสั่ง
แต่ต้องรู้ว่ามันไม่ใช่คำสั่ง —
cpu, hof, org, end เรียกว่า
assembler directive (คำสั่งสำหรับตัวแปลภาษา ไม่ใช่สำหรับ CPU)
จึงไม่ถูกแปลเป็น machine code และไม่กินตำแหน่ง address
⇒ ในตาราง opcode ของข้อ 4 จะไม่มีบรรทัดพวกนี้ (ยกเว้น org ที่เขียนกำกับไว้ว่าตั้ง address ที่เท่าไร)
| Directive | ทำอะไร | กิน address ไหม |
|---|---|---|
| cpu "z80.tbl" | โหลดตารางชุดคำสั่งของ Z80 | ไม่ |
| hof "INT8" | ตั้งรูปแบบไฟล์ผลลัพธ์เป็น Intel HEX 8 บิต | ไม่ |
| org 2000h | ตั้งตำแหน่งเริ่มต้นของโค้ดถัดจากนี้ | ไม่ แต่กำหนดว่า address เริ่มที่ไหน |
| end | บอกว่าจบไฟล์ต้นฉบับแล้ว | ไม่ |
| equ | ตั้งชื่อให้ค่าคงที่ เช่น PORT: equ 40h | ไม่ |
| dfb / defb | ใส่ข้อมูลลงหน่วยความจำโดยตรง (ไม่ใช่คำสั่ง) | กิน |
ก่อนดูตาราง ให้ดูผังจริงก่อน — นี่คือหน้าตาที่ตำราและดาต้าชีตของ Zilog วาดไว้ ข้อสอบมักให้เขียนผังนี้จากความจำ หรือถามว่ารีจิสเตอร์ตัวไหนอยู่ตรงไหน
PUSH AF / POP AF เป็นหลักLD A′,B ไม่ได้ ไม่มีคำสั่งแบบนั้นในชุดคำสั่ง
ต้องสลับเข้ามาเป็นชุดหลักก่อนเท่านั้นปุ่มด้านล่างคือคำสั่งจริงสองคำสั่งที่มีในชุดคำสั่ง Z80 · สังเกตว่าค่าไม่ได้ถูกคัดลอก แต่สลับที่กัน กดซ้ำอีกครั้งค่าจะกลับมาเหมือนเดิม
| ชุดหลัก | ชุดสำรอง | ใช้ทำอะไร |
|---|---|---|
| A | A′ | Accumulator — ผลลัพธ์ของ ALU และเป็นตัวเดียวที่ใช้กับ IN/OUT แบบ (n) |
| F | F′ | Flag register — ผลสถานะจาก ALU |
| B C | B′ C′ | ทั่วไป · B เป็นตัวนับของ DJNZ · C เป็นเลขพอร์ตของ IN/OUT แบบ (C) |
| D E | D′ E′ | ทั่วไป · คู่ DE ใช้เป็น pointer ปลายทางของ LDIR |
| H L | H′ L′ | ทั่วไป · คู่ HL เป็น pointer หลัก ใช้กับ (HL) |
สลับชุดด้วย EX AF,AF′ และ EXX — ใช้ตอนเข้า interrupt จะเร็วกว่า push/pop
| ชื่อ | บิต | ใช้ทำอะไร |
|---|---|---|
| BC DE HL | 16 | จับคู่จาก register 8 บิต |
| IX IY | 16 | Index register — ใช้กับ (IX+d) โดย d เป็น signed −128…+127 |
| SP | 16 | Stack Pointer — ทำงานแบบ LIFO ใช้กับ PUSH/POP/CALL/RET/interrupt |
| PC | 16 | Program Counter — ชี้คำสั่งถัดไป · RESET แล้วเป็น 0000H |
| I | 8 | Interrupt vector register — ใช้เป็นไบต์สูงของ vector ใน interrupt mode 2 |
| R | 8 | Memory Refresh — เพิ่มเฉพาะบิต 0–6 ทุก M1 บิต 7 คงเดิม |
R เป็น register 8 บิต ไม่ใช่ 7 บิต — แค่การนับใช้แค่ 7 บิตล่าง (ดูหน้า สไลด์ผิด)
F เป็นรีจิสเตอร์ 8 บิตเหมือนตัวอื่น แต่แต่ละบิตมีความหมายเฉพาะตัว ไม่ได้เก็บตัวเลข — ALU จะเขียนค่าลงบิตเหล่านี้ให้อัตโนมัติทุกครั้งที่คำนวณเสร็จ แล้วคำสั่ง jump แบบมีเงื่อนไขจะมาอ่านต่อ
เลือกคำสั่ง ใส่ค่า A และ n (เลขฐานสิบหก) แล้วดูค่าธงพร้อมเหตุผลของแต่ละตัว — ลองกดตัวอย่างสำเร็จรูปด้านขวาดูก่อนก็ได้
INC / DEC ไม่แตะ Carry — เป็นกับดักคลาสสิก
ถ้าอยากให้กระทบ C ต้องใช้ ADD A,1 / SUB 1LD ไม่แตะ flag เลยสักตัว (ยกเว้น LD A,I และ LD A,R)DAA ใช้ภายในเท่านั้น| บิต | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
|---|---|---|---|---|---|---|---|---|
| ชื่อ | S | Z | F5 | H | F3 | P/V | N | C |
| ความหมาย | Sign (= บิต 7 ของผล) | Zero (ผล = 0) | ไม่ใช้ | Half carry (บิต 3→4) | ไม่ใช้ | Parity / Overflow | Add / Subtract | Carry |
| ทดสอบเงื่อนไขได้ไหม | ได้ (P/M) | ได้ (Z/NZ) | — | ไม่ได้ | — | ได้ (PE/PO) | ไม่ได้ | ได้ (C/NC) |
H และ N ใช้ภายในสำหรับคำสั่ง DAA เท่านั้น — เขียนเงื่อนไข jump จากสองตัวนี้ไม่ได้
| โหมด | ตัวอย่าง | ความหมาย |
|---|---|---|
| Implied | HALT, CPL | ไม่มี operand หรือ operand ฝังอยู่ใน opcode |
| Register | LD B,A | ทำงานระหว่าง register ตรง ๆ เร็วที่สุด (1 byte, 4 T) |
| Immediate | LD A,9FH | ค่าคงที่ 8 บิตตามหลัง opcode |
| Immediate extended | LD HL,2100H | ค่าคงที่ 16 บิต เก็บ ไบต์ต่ำก่อน |
| Extended (direct) | LD A,(1000H) | ระบุ address 16 บิตตรง ๆ |
| Register indirect | LD A,(HL) | ใช้ค่าใน register pair เป็น address |
| Indexed | LD A,(IX+5) | address = IX + displacement (signed 8 บิต) · ต้องมี prefix DD/FD |
| Relative | JR LOOP | PC = PC + 2 + d โดย d เป็น signed −128…+127 |
| Bit | SET 3,A | ระบุหมายเลขบิตใน opcode · ต้องมี prefix CB |
Z80 เก็บค่า 16 บิตแบบ ไบต์ต่ำก่อน ไบต์สูงทีหลัง
LD HL,2100H → machine code 21 00 21 ไม่ใช่ 21 21 00
LD (1000H),A → 32 00 10
LD A, B ; register → register
LD A, 9FH ; ค่าคงที่ → register
LD HL, 2100H ; ค่าคงที่ 16 บิต
LD A, (HL) ; อ่านจาก address ใน HL
LD (HL), A ; เขียนไปที่ address ใน HL
LD A, (1000H) ; อ่านจาก address ตรง ๆ
LD (1000H), A ; เขียนไปที่ address ตรง ๆ
EX DE, HL ; สลับสองคู่
PUSH BC / POP BC ; ผ่าน stack
ADD A, B ; A ← A + B
ADC A, B ; บวกพร้อม carry
SUB B ; A ← A − B (ไม่ต้องเขียน A,)
SBC A, B ; ลบพร้อม borrow
INC A / DEC A ; +1 / −1 (ไม่กระทบ carry)
INC DE ; +1 กับคู่ 16 บิต
AND B / OR B / XOR B
CP 10H ; เปรียบเทียบ (ลบทิ้ง เก็บแค่ flag)
CPL ; กลับทุกบิตของ A
ADD HL, DE ; บวก 16 บิต
JP 2000H ; กระโดดแบบระบุ address
JP NZ, 2000H ; มีเงื่อนไข
JR LOOP ; กระโดดสั้น ±127 (2 byte)
DJNZ LOOP ; B←B−1 แล้วกระโดดถ้า B≠0
CALL 3000H ; เรียก subroutine
RET / RET Z ; กลับ
HALT ; หยุด (ยังวน refresh อยู่)
IN A, (84H) ; อ่านพอร์ต 84H เข้า A
OUT (40H), A ; ส่ง A ออกพอร์ต 40H
IN B, (C) ; เลขพอร์ตอยู่ใน C (2 byte, prefix ED)
OUT (C), B ; เลขพอร์ตอยู่ใน C
แบบ (n) ใช้ได้กับ A เท่านั้น
ถ้าจะใช้ register อื่นต้องใช้แบบ (C) ซึ่งกิน 2 ไบต์และ 12 T
IN A, (84H) ; อ่าน DIP switch
LD (1000H), A ; เก็บไว้ใน RAM
CPL ; กลับบิตทุกตัว
OUT (40H), A ; แสดงที่ LED
LD B, 0FFH ; ตัวนับ 255 รอบ
DELAY: DJNZ DELAY ; วนจนกว่า B = 0
; เวลา = 255 รอบ × 13 T − 5 T (รอบสุดท้าย 8 T) = 3,310 T
; ที่ 4 MHz ≈ 827.5 µs
LD HL, 1000H ; ต้นทาง
LD DE, 1100H ; ปลายทาง
LD BC, 0010H ; จำนวน 16 ไบต์
LDIR ; ทำซ้ำจนกว่า BC = 0
LD HL, 2100H
LD DE, 2101H
LD A, (HL) ; ตัวตั้ง
LD B, A
LD A, (DE) ; ตัวบวก
ADD A, B
INC DE ; ชี้ไป 2102H
LD (DE), A ; เก็บผลลัพธ์
HALT
พิมพ์โค้ดในกล่องซ้าย ระบบจะแปลให้ทันที พร้อมบอก address, machine code, จำนวนไบต์, machine cycle และ T-state — ใช้ตรวจคำตอบข้อ 4 ได้เลย
จงเขียนโค้ด Z80 Assembly สำหรับระบบที่ออกแบบในข้อ 1 (ROM 4 KB ที่ 0000H, RWM 4 KB ที่ 1000H, พอร์ตอินพุต 84H, พอร์ตเอาต์พุต 40H) โดยให้โปรแกรมอ่านค่าจากสวิตช์ เก็บลงหน่วยความจำ แล้วนำค่ากลับมาแสดงที่ LED พร้อมทั้งอธิบายการทำงานของแต่ละคำสั่ง
CPU "z80.tbl" ; โหลดตารางคำสั่ง Z80 ให้ตัวแปลภาษา
HOF "INT8" ; ให้ออกไฟล์เป็น Intel HEX 8 บิต
ORG 0000H ; RESET แล้ว PC = 0000H จึงต้องเริ่มที่นี่
START: LD SP, 1FFFH ; ตั้ง stack ที่ท้าย RWM (โตลงล่าง)
LD HL, 1000H ; HL ชี้ที่เก็บข้อมูลใน RWM
MAIN: IN A, (84H) ; อ่าน 8 บิตจาก DIP switch เข้า A
LD (HL), A ; เก็บลง RWM ที่ 1000H
LD A, (1000H) ; อ่านกลับมาจาก RWM (ยืนยันว่าเขียนติด)
CPL ; กลับทุกบิต (ตัวอย่างการประมวลผลก่อนแสดงผล)
OUT (40H), A ; ส่งออกไปที่ latch ของ LED
LD B, 0FFH ; หน่วงเวลาให้ตาเห็น
DELAY: DJNZ DELAY
JR MAIN ; วนอ่านสวิตช์ตลอดไป
HALT
END
| คำสั่ง | ทำอะไร | machine cycle |
|---|---|---|
LD SP,1FFFH | ตั้ง stack pointer ไว้ปลายบนของ RWM เพราะ stack โตจากสูงไปต่ำ ถ้าไม่ตั้งค่า SP จะเป็น FFFFH ซึ่งไม่มี RAM อยู่จริง | M1 + MR + MR |
LD HL,1000H | ให้ HL เป็น pointer ชี้ไปที่ RWM | M1 + MR + MR |
IN A,(84H) | สร้าง I/O Read cycle · A0–A7 = 84H → SEL84 ทำงาน → 74LS244 ปล่อยค่าสวิตช์ขึ้น data bus → เข้า A | M1 + MR + IOR |
LD (HL),A | สร้าง Memory Write cycle ไปที่ 1000H → CERWM ทำงาน → MEMWR เขียนข้อมูลลงเซลล์ | M1 + MW |
LD A,(1000H) | สร้าง Memory Read cycle · อ่าน operand 2 ไบต์ก่อน (little-endian) แล้วอ่านข้อมูลจริง | M1 + MR + MR + MR |
CPL | กลับทุกบิตของ A (one's complement) — ใส่ไว้เป็นตัวอย่างการประมวลผลก่อนแสดงผล · 1 ไบต์ 4 T ไม่แตะบัสเพิ่ม | M1 |
OUT (40H),A | สร้าง I/O Write cycle · A0–A7 = 40H, A8–A15 = ค่าใน A → IOWR clock ให้ 74LS273 เก็บค่า → LED ติด | M1 + MR + IOW |
DJNZ DELAY | B ← B−1 แล้วกระโดดกลับถ้ายังไม่ 0 · M1 ของ DJNZ ยาว 5 T ไม่ใช่ 4 T | M1(5) + MR (+5 T ถ้ากระโดด) |
JR MAIN | กระโดดแบบ relative — เก็บ displacement เป็น signed 8 บิต ประหยัดกว่า JP 1 ไบต์ | M1 + MR (+5 T) |
ORG 0000H และอธิบายว่าทำไมต้องเป็น 0000H (RESET แล้ว PC = 0000H)จงเขียนโปรแกรมที่อ่านค่า 2 ไบต์จาก 1000H และ 1001H นำมาบวกกัน แล้วเก็บผลที่ 1002H ถ้ามี carry ให้เก็บ 01H ที่ 1003H มิฉะนั้นเก็บ 00H
CPU "z80.tbl"
HOF "INT8"
ORG 0000H
LD HL, 1000H
LD A, (HL) ; A ← ตัวตั้ง
INC HL
ADD A, (HL) ; A ← A + ตัวบวก (ตั้ง carry flag ให้ด้วย)
INC HL
LD (HL), A ; เก็บผลที่ 1002H
INC HL
LD A, 00H ; เตรียมค่า 0 (ห้ามใช้ XOR A เพราะจะล้าง carry)
ADC A, 00H ; A ← 0 + 0 + carry ⇒ ได้ 0 หรือ 1 พอดี
LD (HL), A ; เก็บที่ 1003H
HALT
XOR A เป็นวิธียอดนิยมในการเคลียร์ A (1 ไบต์ 4 T)
แต่มันล้าง carry flag ทิ้ง ถ้าจะเก็บ carry ไว้ใช้ต่อ ต้องใช้ LD A,00H แทน
จงเขียนโปรแกรมนับไฟวิ่ง (LED เพิ่มค่าทีละ 1) ออกที่พอร์ต 40H โดยมีการหน่วงเวลาระหว่างรอบ และวนไม่รู้จบ
CPU "z80.tbl"
HOF "INT8"
ORG 0000H
XOR A ; A ← 0 (สั้นกว่า LD A,0 อยู่ 1 ไบต์)
LOOP: OUT (40H), A
INC A
PUSH AF ; เก็บ A ไว้ก่อน เพราะ DJNZ จะใช้ B
LD B, 0FFH
DLY: DJNZ DLY
POP AF
JR LOOP
END
เทียบกับใบงาน Lab 2 ที่ใช้ XOR A / OUT (40H),A / INC A / JR loop —
โปรแกรมนี้เพิ่มการหน่วงเวลาเพื่อให้เห็นด้วยตาเปล่า และ push/pop เพื่อไม่ให้ B ไปทับค่าที่ต้องนับ
จงเขียนโปรแกรมที่รอจนกว่าจะมีการกดสวิตช์บิต 0 ของพอร์ต 84H (อ่านได้ 0 เมื่อกด) แล้วจึงส่งค่า 55H ออกพอร์ต 40H
CPU "z80.tbl"
HOF "INT8"
ORG 0000H
WAITSW: IN A, (84H) ; อ่านสวิตช์
BIT 0, A ; ทดสอบบิต 0 → ผลไปที่ Z flag
JR NZ, WAITSW ; ยังไม่กด (บิต = 1) → วนรอต่อ
LD A, 55H
OUT (40H), A
HALT
BIT b,r ใช้ prefix CB — BIT 0,A = CB 47 (2 ไบต์, 8 T)
ทางเลือกที่ไม่ต้องใช้ CB: AND 01H แล้ว JR NZ
— สั้นกว่า 0 ไบต์แต่ทำลายค่าใน A
จงเขียนโปรแกรมย้ายข้อมูล 16 ไบต์จาก ROM ตำแหน่ง 0100H ไปยัง RAM ตำแหน่ง 1000H สองแบบ: แบบใช้ลูปเอง และแบบใช้คำสั่งบล็อก แล้วเปรียบเทียบขนาดโค้ด
แบบลูปเอง
LD HL, 0100H
LD DE, 1000H
LD B, 16
CP1: LD A, (HL)
LD (DE), A
INC HL
INC DE
DJNZ CP1
HALT
แบบใช้คำสั่งบล็อก
LD HL, 0100H ; ต้นทาง
LD DE, 1000H ; ปลายทาง
LD BC, 0010H ; จำนวน
LDIR ; ED B0 — ทำซ้ำเองจนกว่า BC = 0
HALT
| แบบ | ขนาด | T-state รวม (16 ไบต์) |
|---|---|---|
| ลูปเอง | 15 ไบต์ | 10+10+7 + 16×(7+7+6+6+13) − 5 = 646 T |
| LDIR | 12 ไบต์ | 10+10+10 + 15×21 + 16 = 361 T |
(นับรวม HALT ทั้งสองแบบ) LDIR สั้นกว่า 3 ไบต์และเร็วกว่าเกือบเท่าตัว — แต่ต้องจำว่า LDIR ใช้ 21 T ต่อรอบ
และรอบสุดท้าย (ตอน BC = 0) ใช้แค่ 16 T
จงเขียน subroutine ชื่อ OUTBYTE ที่รับค่าใน A แล้วส่งออกพอร์ต 40H
พร้อมหน่วงเวลา และเขียนโปรแกรมหลักที่เรียกใช้ subroutine นี้ 3 ครั้งด้วยค่า 01H, 02H, 04H
CPU "z80.tbl"
HOF "INT8"
ORG 0000H
LD SP, 1FFFH ; ต้องตั้ง SP ก่อนใช้ CALL เสมอ!
LD A, 01H
CALL OUTBYTE
LD A, 02H
CALL OUTBYTE
LD A, 04H
CALL OUTBYTE
HALT
OUTBYTE: OUT (40H), A
PUSH BC ; รักษาค่า B ของผู้เรียก
LD B, 0FFH
OB1: DJNZ OB1
POP BC
RET
END
ลืมตั้ง SP ก่อน CALL — CALL จะ push return address 2 ไบต์ลง stack ถ้า SP ยังเป็น FFFFH (ค่าหลัง reset จริง ๆ คือค่าไม่แน่นอน) จะเขียนลงที่ที่ไม่มี RAM พอ RET ก็จะกระโดดไปมั่ว โปรแกรมพัง