Bài 3: cái nhìn chung về Asm6502


Từ bài này ta sẽ đi vào phần lập trình của 6502. Tài liệu về 6502 có rất nhiều trên Google, người đọc có thể tìm hiểu thêm nếu thích.
Có 5 phần chính trong ngôn ngữ Assembly như được giới thiệu lần lượt bên dưới. Trong đó có những phần phải được viết theo một vị trí hàng ngang cố định mới có hiệu lực.

Để tạo ra một game NES (file có đuôi .Nes), đầu tiên ta viết chương trình trong một file text có đuôi .Asm rồi chạy với trình compiller. Compiller là trình biên dịch, nó sẽ chuyển các mệnh lệnh trong file text thành dạng số mà máy có thể hiểu được. Nói nôm na, nhiệm vụ của compiller là dịch từ ngôn ngữ mà con người hiểu được thành ngôn ngữ máy. Chẳng hạn, trong file text ta có mệnh lệnh LDA #03 thì trình biên dịch sẽ chuyển thành 03A9.

Click vào đây để tải NESASM.

Có khá nhiều compiller cho NES, trong số đó có compiller dựa trên nền ngôn ngữ C, nhưng ở đây chỉ giới thiệu NESASM, trình biên dịch Assembler để giúp người đọc hiểu rõ. NESASM là Assembler có thể tạo ra file.NES từ mã nguồn. Sau khi có file.NES, có thể dùng giả lập để chơi. Có nhiều loại giả lập NES, nhưng khuyên dùng Fceux vì nó có chức năng Debugger và Dis-assemble, chức năng phân tích ngược lại từ ngôn ngữ máy ra ngôn ngữ Assmbly. Giả lập này có thể tải miễn phí từ Google.

Dưới đây là lưu trình phát triển một game NES.

[​IMG]

Lệnh dẫn hướng: là những lệnh chỉ cho trình Assembler biết cần phải làm gì, chẳng hạn như tìm code trong bộ nhớ. Các lệnh này đều bắt đầu bằng dấu "." (không ngoặc kép) với một khoảng trắng đứng trước. Có người dùng phím tab để tạo khoảng trắng, hay 4 dấu cách, có người dùng 2 dấu cách. Chẳng hạn, lệnh dẫn hướng dưới đây chỉ cho trình Assembler biết phải đặt code vào trong ROM, bắt đầu từ địa chỉ $8000 tron bộ nhớ"

.org $8000

Label: nhãn. Label phải được viết ngay sát lền trái và có dấu ":" (không ngoặc kép) liền sau. Lable là tên gán cho một đoạn code nào đó để tránh mất thời gian viết khi lặp lại nhiều lần, hay để dễ hiểu, tránh nhầm lẫn. Chẳng hạn có label như sau:

.org $8000
Gokuraku:

Khi trình Assembler gặp label, nó sẽ tự động chuyển label thành địa chỉ. Trong ví dụ trên, ta gán nhãn Gokuraku cho .org $8000 thì 

STA Gokuraku

sẽ được dịch thành STA .org $8000

Opcode: opcode là mệnh lệnh mà bộ xử lý thi hành. Trong ví dụ sau, JMP là opcode chỉ cho bộ xử lý nhảy tới label GameVN:

.org $8000
GameVN:
JMP GameVN

Tất cả các opcode trong các ngôn ngữ console đều là chữ viết tắt gồm 3 ký tự. JMP là opcode có giá trị $4C.

Operand: là thành phần thông tin bổ trợ cho opcode. Opcode có thể đi kèm từ 1~3 oprand. Trong ví dụ dưới đây thì #$FF chính là operand:

.org $8000
Google:
LDA #$FF
JMP Google

Nói cách khác, operand chính là giá trị đi kèm với opcode. Cả opcode và operand cấu thành nên một câu lệnh hoàn chỉnh, gọi là mnemonic. 

Comment: là một kiểu ghi chú bằng "tiếng người" để hiểu được mớ code lằng nhằng đó có ý nghĩa gì. Phần comment sẽ không được trình biên dịch chú ý nên nó không ảnh hưởng gì tới chương trình, chỉ giúp người lập trình dễ dàng theo dõi những gì đang viết mà thôi. Trong ngôn ngữ Assembly, comment được viết sau dấu ; trong khi C hay Java là //, VB là '.

LDA #100001 ; nạp giá trị 00100001 vào Register A (% thể hiện hệ nhị phân)
LDX #13 ; nạp giá trị 13 (thập phân) vào Register X
LDY #$0F ; nạp giá trị 0F (thập lục) vào Register Y

Tổng quát về bộ xử lý 6502

6502 là bộ xử lý 8 bit với Address bus 16 bit, có thể truy cập vào 64KB bộ nhớ mà không cần đổi bank. Không gian bộ nhớ này được chia thành RAM, PPU/APU/Controller và ROM. 

Phần bộ nhớ của Famicom được chia làm RAM và ROM, gọi là Memory Map. Đại khái như dưới đây.

$0000-$07FF: RAM nội bộ, dung lượng 2KB bên trong NES, user tự do sử dụng
$0800-$1FFF: đối xứng gương của RAM
$2000-$2007: các cổng truy cập vào PPU (I/O Register)
$2008-$3FFF: đối xứng gương của I/O Register
$4000-$401F: các cổng truy cập vào APU và Controller
$4020-$5FFF: ROM mở rộng
$6000-$7FFF: WRAM có thể có hay không bên trong Cartridge (backup RAM)
$8000-$BFFF: ROM bên trong Cartridge
$C000-$FFFF: ROM bên trong Cartridge

Phần bộ nhớ người dùng có thể tùy nghi sử dụng là từ $0000 đến $07FF, dung lượng 2KB.

Tổng quan về Asm 6502

Ngôn ngữ Assembly cho 6502 bắt đầu với code 3 chữ cái như mô tả ở phần trên. Có tất cả 56 lệnh, trong số đó có chừng 10 lệnh hay được dùng nhất. Nhiều lệnh trong đó có giá trị (operand) đi kèm opcode. Phần giá trị (operand) này có thể viết ở 3 dạng: thập lục, thập phân và nhị phân. Dấu $ đứng trước giá trị cho biết nó ở hệ thập lục. Dấu % đứng trước giá trị cho biết nó ở hệ nhị phân, trong khi số thập phân thì đứng độc lập, không đi kèm ký hiệu gì.
Nếu giá trị không có dấu # đi kèm thì nghĩa là giá trị tại địa chỉ đó. Chẳng hạn:

LDA #$000A ; tải giá trị 0A vào Register A
LDA $000A ; tải giá trị tại địa chỉ $0A trong bộ nhớ vào Register A

Giải thích về Register

Register là một vùng bên trong bộ xử lý để giữ một giá trị. Từ "register" có nghĩa là lưu giữ. 6502 chỉ có 3 Register cho phép tự do sử dụng, ngoài ra còn có Stack Register và vài Register nữa nhưng tôi không đề cập đến trong bài này. Các Register đó là:


A: Accumulator, dùng để tính toán.
X, Y: Index, chỉ mục. Dùng để truy nhập vào bộ nhớ.

Các Register của 6502 chỉ có 8bit nên chúng chỉ chứa được giá trị từ 00 đến FF (0~255).

Accumulator: Register này được ký hiệu là A, là Register chủ yếu để chứa, tải, so sánh và tính toán trên dữ liệu. Một vài lệnh thường dùng liên quan đến Accumulator:

LDA #$FF ; tải giá trị hex FF (255) vào trong A
STA $0000 ; chứa (ghi) Accumulator vào địa chỉ $0000 trong bộ nhớ, đây là phần RAM nội bộ

Index Register X: thường được dùng để đếm hay truy nhập bộ nhớ. Trong các vòng lặp thì Register này được dùng để theo dõi vòng lặp đã lặp được bao nhiêu lần, trong khi dùng A để xử lý dữ liệu. Vài ví dụ:

LDX $0000 ; tải giá trị bộ nhớ tại địa chỉ $0000 vào X
INX ; tăng X thêm 1 đơn vị, X=X+1

Index Register Y: hoạt động giống X. Tuy nhiên có vài lệnh chỉ hoạt động với X mà không hoạt động với Y. Vài ví dụ:

STY $00BA ; chứa Y vào địa chỉ $00BA trong bộ nhớ
TYA ; chuyển Y vào Accumulator

Status Register: Register trạng thái này giữ flag với thông tin về mệnh lệnh trước. Chẳng hạn như khi đang thực hiện phép trừ, nó cho phép kiểm tra xem kết quả có phải zero không.


Các lệnh 6502 căn bản

Dưới đây là các lệnh căn bản. Một số lệnh phức tạp hơn sẽ được đề cập ở những phần sau.

Opcode liên quan đến nạp/ghi

LDA #$0A ; nạp giá trị 0A vào Accumulator
; phần giá trị có thể là con số hoặc địa chỉ
; nếu giá trị là zero thì zero flag được thiết lập

LDX $0000 ; nạp giá trị tại địa chỉ $0000 vào trong bộ nhớ vào Index Register X
; nếu giá trị là zero thì zero flag được thiết lập

LDY #$FF ; nạp giá trị FF vào trong bộ nhớ vào Index Register Y
; nếu giá trị là zero thì zero flag được thiết lập

STA $2000 ; chứa giá trị của A vào địa chỉ $2000
; phần số phải là địa chỉ, không chấp nhận giá trị

STX $5A12 ; chứa giá trị của X vào địa chỉ $5A12
; phần số phải là địa chỉ, không chấp nhận giá trị

STY $010F ; chứa giá trị của Y vào địa chỉ $010F
; phần số phải là địa chỉ, không chấp nhận giá trị

Đồ hình dưới đây giải thích rõ thêm về chức năng của LDA và STA.

[​IMG]

TAX ; (transfer A to X) chuyển giá trị từ A vào X
; nếu giá trị là zero thì zero flag được thiết lập

TAY ; (transfer A to Y) chuyển giá trị từ A vào Y
; nếu giá trị là zero thì zero flag được thiết lập

TXA ; chuyển X vào A
; nếu giá trị là zero thì zero flag được thiết lập

TYA ; chuyển Y vào A
; nếu giá trị là zero thì zero flag được thiết lập


Opcode tính toán phổ thông

ADC #$01 ; Add with Carry, cộng với carry
; A= A + $01 +carry
; nếu kết quả là zero thì zero flag được thiết lập

SBC #$80 ; Subtract with Carry, trừ với carry
; A= A - $80 - (1 - carry)
; nếu kết quả là zero thì zero flag được thiết lập

CLC ; Clear Carry, xóa carry flag trong Status Register
; thường được thực hiện trước khi thi hành ADC

SEC ; Set carry, thiết lập carry flag trong Status Register
; thường được thực hiện trước khi thi hành SBC

INC $0100 ; Increment, tăng giá trị tại địa chỉ $0100
; nếu kết quả là zero thì zero flag được thiết lập

DEC $0001 ; Decrement, giảm giá trị tại địa chỉ $0001
; nếu kết quả là zero thì zero flag được thiết lập

INY ; Incrememt Y Register, tăng Y
; nếu kết quả là zero thì zero flag được thiết lập

INX ; Increment X Register, tăng X
; nếu kết quả là zero thì zero flag được thiết lập

DEY ; Decrement Y, giảm Y
; nếu kết quả là zero thì zero flag được thiết lập

DEX ; Decrement X, giảm X
; nếu kết quả là zero thì zero flag được thiết lập

ASL A ; Arithmetic Shift Left, dịch chuyển số học về trái
; dời tất cả bit 1 vị trí về bên trái
; luôn được nhân 2
; nếu kết quả là zero thì zero flag được thiết lập

LSR $6000 ; Logical Shift Right, dịch chuyển số học về phải
; dời tất cả bit 1 vị trí về bên phải

1 byte là tổ hợp 8 bit, thứ tự từ bit 0 đến bit 7. Hình ảnh dưới đây minh họa cho 2 phép dời bit

[​IMG]


Opcode liên quan đến lưu trình điều khiển

JMP $8000 ; Jump, nhảy đến địa chỉ $8000 và tiếp tục thực hiện code ở đây

BEQ $FF00 ; Branch if Equal, phân nhánh nếu bằng, tiếp tục chạy code ở đây
; đầu tiên là so sánh (CMP), kết quả sẽ xóa hay thiết lập zero flag
; BEQ sẽ kiểm tra zero flag
; nếu zero flag được lập (kết quả = zero), code nhảy đến $FF00 và chạy từ đây
; nếu zero flag bị xóa (kết quả không = zero) thì không nhảy, tiếp tục chạy code từ vị trí cũ

BNE $FF00 ; Branch if not equal, phân nhánh nếu không bằng
; chức năng ngược lại với BEQ, nhảy đến $FF00 nếu kết quả khác zero.

Cấu trúc NES code

iNES Header: phần header này gồm 16 byte cho trình giả lập biết mọi thông tin về game, bao gồm Mapper, đối xứng đồ họa và kích thước PRG/CHR. Ta nên đưa những thông tin này vào đầu file asm.

.inesprg 1 ; 1x 16KB bank cho code PRG (dùng 1 bank trong số mấy bank của chương trình)
.ineschr 1 ; 1x 8KB bank cho dữ liệu CHR (dùng 1 bank trong số mấy bank của dữ liệu đồ họa)
.inesmap 0 ; mapper 0= NROM, không chuyển đổi bank
.inesmir 1 ; chọn đối xứng gương ngang (0) hay dọc (1) của VRAM đối với ảnh nền. Đang chọn dọc.

Bank: dữ liệu chương trình và đồ họa được phân chia thành các đơn vị gọi là bank. Ở đây đề cập đến 3 bank như bên dưới.

Bank 0 bắt đầu từ $8000, là khu vực chứa chương trình trong ROM.
Bank 1 bắt đầu từ $FFFA, là table ngắt.
Bank 2 bắt đầu từ $0000 trong VRAM, là nơi chứa ảnh sprite và ảnh nền (BG).

NESASM sắp đặt mọi thứ trong các bank 8KB code và 8KB đồ họa. Cần có 2 bank để đầy 16KB PRG. Đối với mỗi bank, cần khai báo thông tin để trình Assembler biết nó phải bắt đầu ở đâu trong bộ nhớ.

.bank 0 ; chọn bank 0
.org $C000 ; bắt đầu từ $C000
; code chương trình bắt đầu từ đây

Có 3 lúc bộ xử lý phải dừng code và nhảy đến vị trí mới. Những vector này nằm trong PRG ROM và cho bộ xử lý biết cần phải nhảy đến đâu khi những tình huống này xảy ra.

NMI Vector: xảy ra một lần cho mỗi khung hình khi được cho phép. PPU cho bộ xử lý biết nó đang bắt đầu thời gian VBlank và có thể cập nhật hình ảnh.
RESET Vector: xảy ra khi NES khởi động hay nút Reset được nhấn.
IRQ Vector: được kích hoạt từ vài con chip mapper hay ngắt audio, không được đề cập đến ở đây.

3 vector này phải được xuất hiện trong file .asm đúng thứ tự. Lệnh .dw được dùng để định nghĩa một Data Word (1 word = 2 byte).

.bank 1 ; đổi sang bank 1
.org $FFFA ; vector đầu tiên bắt đầu từ $FFFA
.dw NMI ; khi NMI xảy ra (1 lần mỗi khung hình), bộ xử lý sẽ nhảy đến label NMI:
.dw RESET ; khi bộ xử lý được bật hay reset, nó sẽ nhảy đến label RESET:
.dw 0 ; ngắt VBlank
.dw Start ; ngắt Reset. Nhảy đến label Start khi khởi động và reset
.dw 0 ; phát sinh do ngắt phần cứng và ngắt phần mềm

Reset code: vector reset được gán cho label RESET, nên khi bộ xử lý khỏi động thì nó sẽ bắt đầu từ RESET bằng cách dùng lệnh .org mà code được viết trong ROM. 

.bank 0
.org $C000
RESET:
SEI ; bỏ IRQ
CLD ; bỏ chế độ số thập phân

Ở đây bank 1 viết về table vector bộ ngắt. Ngắt là một chức năng xử lý quan trọng. Ở đây khi nhấn nút Reset thì mọi thứ trở về ban đầu. Nếu dùng chức năng ngắt thì phải ghi địa chỉ routine ngắt vào .dw. Routine ngắt là một chương trình nằm chờ trên bộ nhớ để xử lý, khống chế chức năng ngắt. Khi kết quả tính toán dẫn đến lỗi hay khi có yêu cầu xử lý từ thiết bị ngoại vi gửi đến thì CPU sẽ dừng chương trình đang chạy và gọi chương trình đã đăng ký sẵn ra. Đây gọi là chức năng ngắt. Chẳng hạn, khi đang chạy game, ta nhấn nút Reset trên máy Famicom thì toàn bộ game sẽ dừng và quay trở lại màn hình đầu tiên.

Thêm file binary: chức năng thêm file thường được dùng cho dữ liệu đồ họa và dữ liệu các màn chơi. Dữ liệu này được đưa vào file .nes như sau:

.bank 2 ; đổi sang bank 2
.org $0000 ; bắt đầu từ $0000 (trong VRAM)
.incbin "kage.bkg" ; include file kage.bkg, file ảnh nền (BG)
.incbin "kage.spr" ; include file kage.spr, file ảnh sprite

Theo thứ tự này thì ảnh nền là Pattern table 0, ảnh sprite là Pattern table 1. incbin có chức năng giống #include trong ngôn ngữ C, bao gồm file đối tượng được chỉ định.

Trong VRAM cũng có khái niệm Memory map. Phần dưới là nhắc lại kiến thức ở phần trước đây. Chức năng của từng cái sẽ được giới thiệu ở những bài sau.

$0000-$0FFF: Pattern table 0
$1000-$1FFF: Pattern table 1
$2000-$23BF: Name table
$23C0-$23FF: table thuộc tính
$2400-$27BF: Name table
$27C0-$27FF: table thuộc tính
$2800-$2BBF: Name table
$2BC0-$2BFF: table thuộc tính
$2C00-$2FBF: Name table
$2FC0-$2FFF: table thuộc tính
$3000-$3EFF: đối xứng gương của $2000-$2EFF
$3F00-$3F0F: palette dùng cho BG
$3F10-$3F10: palette dùng cho sprite
$3F20-$3FFF: đối xứng gương của palette
$4000-$FFFF: đối xứng gương của $0000-$3FFF

1 bình luận :

  1. Cảm ơn bạn đã chia sẻ bài viết rất hay và chi tiết
    ..........................
    Huyền Sport
    Võ Thuật
    bong88 l bong88

    ReplyDelete

 
Top