6502汇编语言程序设计(2版)之程序设计part1
6502汇编语言程序设计(2版)之程序设计part1
FC技术书籍 2008-11-09 22:26:17 阅读620 评论1 字号:大中小 订阅
6502汇编语言程序设计 返回
第一部分 基本概述 第二部分 指令和算法 第三部分 程序实例
___________________________________________________________________________________
(一)
一. CPU 对命令的执行
6502是普通的8位CPU.它需要外接其他器件(如:SRAM、EPROM、定时/计数器、I/O接口器件等)才能工作,它地址线为16,直接寻址能力为64K($0000--$FFFF)之间. 数据线为8,一次只能完成8位数据(0--255)的处理. CPU通过地址线联系到要操作的内存单元,然后读取该单元的操作码,通过预设好了的操作码所代表的操作,CPU对其进行相应命令执行. 例如对命令 ASL 的执行,假设程序计数(PC)=$7000 , ($7000)=$0A .
CPU 首先通过程序计数 PC 联系到单元$7000 ,并读取该单元内的操作码 0A(对应6502汇编助计符为ASL) 然后根据操作码的类型,完成相应的操作,这里0A代表将累加器A的内容左移一位.如果是一个时钟周期完成一项指令操作,那么就是第一个时钟周期CPU读取单元$7000中的0A,第二个时钟周期CPU执行命令0A(将累加器A的内容左移一位,高位被移到C进位标志中,低位被移入0)
二. 6502系统说明
1. $0000--$00FF 6502 CPU 零页公共区 (256字节)
2. $0100--$01FF 6502 CPU 公共堆栈区 (256字节)
没有数据进栈时堆栈指针 S=$FF,每当有一个数据进栈S-1→S. 每当有一个数据出时栈S+1→S .当执行JSR指令时,CPU把下一条指令地址-1(两个字节)自动入栈(也即程序的返回地址-1自动入栈), 然后转入到子程序中执行,当遇到RTS时弹出栈顶2个字节,作为程序的返回地址,并转到下一条指令中执行. 由于堆栈$100--$1FF最多可以存放127个双字节,所以JSR最大连续的转入深度为127层,如果还有其它数据入栈,子程序转入深度还要减少.在设计复杂的程序时,应该考虑堆栈是否会溢出,或作相关返回处理,以防止堆栈溢出而造成死机.
3. $0200--$0700(cheapchange注:综合上下文,应为0200--07FF) 公共内存区 (1.5K字节)
4. $0800--$1FFF $0000 -- $0700的镜像. 连做了3次镜像(6K字节)
换句话说,对它们的操作(读/写)实际就是对$0000--$07FF的操作。比如:读取 $08AB 的内容实际等于读取$00AB的内容。而向$15CC写数据实际等于向$05CC写数据. $0800--$0FFF, $1000--$17FF, $1800--$1FFF 这3块不是物理的 RAM,它们都是镜像.
5. $2000--$3FFF 6502 PPU I/O 地址区,系统使用. (8K字节)
6. $4000--$43FF CPU 内部功能部件 I/O 地址区,系统使用 (1K字)
7. $4400--$5FFF 扩充空间 (5K字节)
可用于各种扩充外设的 I/O 端口,或扩充ROM和RAM地址空间.
8. $6000--$7FFF 公
共RAM空间
公共内存区,可为系统程序及用户程序使用
9. $8000--$BFFF 分页区间 (16K字节)
按16K字节分区,页号00--FF(一个字节),最大支持4M字节容量.用于系统程序及应用程序的主要程序数据区.
10.$C000--$FF00 公共RAM空间(16K字节)
公共内存区,可为系统程序及用户程序使用. (注$C000--$FFFF 区间的RAM与分页区间的最后一页第FF页的RAM是同一段区域)
11.$FF00--$FFFF 系统区
系统中断向量及I/O地址.
三. 汇编指令格式
汇编指令包括四部分,格式如下:
[标号:] 操作码 [操作数][;注释]
[]中的内容为可选项
标号 标号标识了一条指令的位置,可以使用标号作为访问该指令的地址,标号的格式应遵循下面规则:
.必须从第一列开始或者以: 结束
. 不能用操作代码或伪指令的名字命名
. 标号长度为1~28个字符(视各汇编而定)
. 以字母或下划线开头,由字母、下划线、数字组成的字符串
操作码 指令区域,用于写指令
操作数 操作数可以是程序中的地址或数据. 当操作码为单字节时,无操作数. 当进行立即寻址时,操作数为一个字节数据,
使用一个符号来标识其所在位置. 当操作数作为程序地址时,其实就是一个标号.
注释 注释可以提高程序的可读性. 在注释语句前面应该加上一个(;)
例如: LDA #$01 ;将数据01放入累加器A
注意: 操作码和操作数之间必需要加空格.
四. 计算机中的数
计算机内存中只以01011....的形式来存放数据.每8位划分为一个存储单元,每个单元都有一个地址与这个单元对应,用来识别该单元的所在位置.单元中存放着数据,一个位有0和1两种状态,一个单元8位组合起来只能表示256中状态,可用来表示0~255中的一个数,即一个单元里可以存放0~255中的一个数据.
每个字符都有一个数值与其对应,这个数值就是该字符的ASC码值,把一个字符存入内存单元,就是把它的ASC值放入内存单元中,对于字符是以ASC码值存在于内存单元中的,例如: 将字符"A"存于单元$7000,实际上是把65存到$7000中.
对于数,如果它在0~255之间,则可以用一个单元来存储,如果在0~65535这间则要用两个单元来存放,如果更大,则要用多个单元来存放.在计算机中,一般是以数的补码形式存放在单元的,当要表示的数是一个负数时,则是把该负数的补码存于单元中,这时一个单元只能存放 -128~127之间的数,两个单元只能存放 -32768~32767之间的数.数的符号用最高位来表示,最高位为1时表示负数,最高位为0时表示正数.
一个正数的补码是它本身.
负数的补码的求法是将它的原码取反再加一得到.例-7的求补,它的原码10000111,保留符号位,将其它位取反得 11111000,最后加一得补码为11111001 ,如果-7的原码在寄存器A中,对它求补的6502指令如下:
eor #$7f
clc
adc #$01
也可由它的相反数全部取反,再加一得到,若寄存器A中是-7的相反数7,则可由如下指令得到-7的补码
eor #$ff
clc
adc #$01
如果在内存单元中进行的话可以这样实现,设$A0单元存放是-7的相反数7,则可以直接用0-($A0)得到
lda #$00
sec
sbc $A0
五. 简单的程序设计示例
例1.
;把16进制字符成换机内整数(0~FFFF) 6502汇编程序
;这里用了对应变址取对应值方法,避免了求字符ASC的值,目的是简化程序复杂性.
;转换的算法:从高位算起,以次累加到结果单元,每累加一次后把结果单元*16,
;直到末位 (运算前先把16进制标志符$,H或h 处理掉)
HEXCHR:
db "0123456789ABCDEFabcdef"
NUMCODE:
db 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,10,11,12,13,14,15
HEXASC: ;待转换16进制字符串 ,高位在前.(高位为0时也必需写上)
db "0a7F" ;例数
HEXASCBIN: ;对应的数值.
db 0,0,0,0
CODEBIN: ;结果单元.低位在前,高位在后
db 0,0
HEXASC_BIN:
ldy #$00
sty CODEBIN
sty CODEBIN+1 ;清理结果单元
HEXASC_BIN02:
ldx #$00
HEXASC_BIN04:
lda HEXASC,y
cmp HEXCHR,x
beq HEXASC_BIN06
inx
bne HEXASC_BIN04
HEXASC_BIN06:
lda NUMCODE,x
sta HEXASCBIN,y ;存放对字符的对应数
iny
cpy #$04
bne HEXASC_BIN02 ;查找字符对应的数值
ldy #$00
clc
HEXASC_BIN08:
lda HEXASCBIN,y
adc CODEBIN
sta CODEBIN
lda #$00
adc CODEBIN+1
sta CODEBIN+1
iny
cpy #$04 ;检查是否累加结束
beq HEXASC_BIN12
asl CODEBIN
rol CODEBIN+1
rol CODEBIN
rol CODEBIN+1
rol CODEBIN
rol CODEBIN+1
rol CODEBIN
rol CODEBIN+1 ;将(CODEBIN+1|CODEBIN)*16 再累加次位数据
jmp HEXASC_BIN08
HEXASC_BIN12:
rts
例2.
;无符号字节型数据乘法
;($A1)*($A2)=(A|$A2) 其中在寄存器A中是积高位,低位在原来乘数单元中
;算法: 逐次判断$A2的低位,若是0则直接,将A右位移,若是1则累加后再移,由于每次可以得到一位不参加运算的低位,则可将它从左到右地移到乘数单元中,最后原乘数单元的数位全部被移出,取而代之的是积所产生的低位.
MULCH:
lda #$00
ldy #$08
clc
MULCH02:
ror $A2
dey
bmi MULCH08
bcc MULCH04
clc
adc $A1
MULCH04:
ror
jmp MULCH02
MULCH08:
rts
例3.
;无符号整型数除法
;($A2|$A1)/($A4|$A3)=$A2|$A1).........($A6|$A5)
;结果 , 商:($A2|$A1) 余数:($A6|$A5)
;说明: C=0有效, C=1 无效(除数为0)
DIV:
lda $A4
bne DIV02
lda $A3
bne DIV02
sec
rts
DIV02:
ldy #17
lda #0
sta $A5
sta $A6
clc
DIV04:
rol $A1
rol $A2
dey
beq DIV08
rol $A5
rol $A6
lda $A5
sec
sbc $A3
tax
lda $A6
sbc $A4
bcc DIV04
sta $A6
stx $A5
bcs DIV04
DIV08:
rts
例4.
; 中缀表达试到后缀表达式的转换,子程序设计
;程序:
top_lev: equ $cf ;栈顶算符优先级单元
ch_reg: equ $d0 ;字符暂存单元
p_inf: equ $d1 ;中缀表达式指针
p_suf: equ $d3 ;后缀表达式指针
p_stack: equ $d5 ;算符栈指针
stackstr: equ $0200 ;算符栈
sufstr: equ $0400 ;后缀表达式生成区( 逆波兰式)
numdef: ;数字定义
db "0123456789ABCDEFabcdef"
db "Hh$",0
operatdef: ;算符定义
db "+-*/%",0
levdef: ;优先级定义
db "+-",0 ;优先级1
db "*/%",0 ;优先级2
infstr: ;调试串
db "585+415*(582%(256-36548)-95)",0
;初始化各指针
initial_p:
ldx #>infstr ;(X)=高位
ldy #
stx p_inf+1
sty p_inf
ldx #>sufstr
ldy #
stx p_suf+1
sty p_suf
ldx #>stackstr-1
ldy #
stx p_stack+1
sty p_stack
rts
isdigit: ;判断是否为数字
ldx #$ff
isdigit02:
inx
lda numdef,x
beq isdigit06
cmp ch_reg
bne isdigit02
isdigit04:
lda ch_reg
ldx #$00 ;yes num N=0
rts
isdigit06:
lda ch_reg
ldx #$ff ;no num N=1
rts
isoperat: ;判断ch是否为算符
ldx #$ff
isoperat02:
inx
lda operatdef,x
beq isoperat06
cmp ch_reg
bne isoperat02
isoperat04:
lda ch_reg
ldx #$00 ;N=0 yes
rts
isoperat06:
lda ch_reg
ldx #$ff ;N=1 no
rts
getch_inf: ;取出一个字符到(A)
ldy #$0
lda (p_inf),y
inc p_inf
bne getch_inf02
inc p_inf+1
getch_inf02:
rts
putch_suf: ;输出一个字符(A)
ldy #$00
sta (p_suf),y
inc p_suf
bne putch_suf02
inc p_suf+1
putch_suf02:
rts
ch_join: ;算符(A)进栈
inc p_stack
bne ch_join02
inc p_stack+1
ch_join02:
ldy #$00
sta (p_stack),y
rts
get_stack: ;算符出栈到(A)
ldy #$00
lda (p_stack),y
ldy p_stack
dey
sty p_stack
cpy #$ff
bne get_stack02
dec p_stack+1
get_stack02:
rts
stack_suf: ;把算符退栈到指定地,直遇到“(“为止
jsr get_stack
cmp #40; "("
beq stack_suf02
jsr putch_suf
jmp stack_suf
stack_suf02:
rts
stack_lev: ;得出栈顶算符优先级别, 在(top_lev)中
ldy #$00
sty top_lev
lda (p_stack),y
cmp #40;"("
beq stack_lev06 ;把栈中的“(”设为0级
cmp #$00
beq stack_lev06
ldx #$ff
stack_lev02:
inc top_lev
stack_lev04:
inx
lda levdef,x
beq stack_lev02
cmp (p_stack),y
bne stack_lev04
stack_lev06:
rts
regA_lev: ;得出(A)中算符优先级,在(X)中
ldx #$00
ldy #$ff
regA_lev02:
inx
regA_lev04:
iny
lda levdef,y
beq regA_lev02
cmp ch_reg
bne regA_lev04
rts
lev_cmp: ;比较当前ch与栈顶算符优先级,并处理
sta ch_reg
lev_cmp02:
jsr stack_lev
jsr regA_lev
cpx top_lev
bcc lev_cmp04 ;若小于或等时把栈中算符输出到
beq lev_cmp04 ;指定位置
lda ch_reg
jsr ch_join
rts
lev_cmp04:
jsr get_stack
jsr putch_suf
jmp lev_cmp02 ;直到ch可以入栈为止
;*****把中缀表达式转换成后缀表达式子程序*****
inftosuf:
jsr initial_p
lda #$00
jsr ch_join ;设置栈初为0
inftosuf02:; main
jsr getch_inf
sta ch_reg
jsr isdigit
bne inftosuf08 ;no num
inftosuf06:
jsr putch_suf
jsr getch_inf
sta ch_reg
jsr isdigit ;
beq inftosuf06 ;num
lda #$20 ; 每个数字段输一空格
jsr putch_suf
lda ch_reg
inftosuf08:
cmp #40; "("
bne inftosuf10
jsr ch_join ;遇“(”时直接入栈
jmp inftosuf02
inftosuf10:
jsr isoperat
bne inftosuf12
jsr lev_cmp
jmp inftosuf02
inftosuf12:
cmp #41 ;")"
bne inftosuf14
jsr stack_suf ;遇“)”退出栈中算符直到遇“(”
jmp inftosuf02
inftosuf14:
cmp #$00
bne inftosuf18
inftosuf16: ;(A)=0时,输出栈中所有算符,到0为止
jsr get_stack
jsr putch_suf
cmp #$00
bne inftosuf16
inftosuf18:
rts
---------------------------------------------------------------------------------------------------------
附:
为了后面更好说明清楚,在这里附带介绍几条步步学生电脑系统的几个调用.其它系统请自行设置所用功能.步步学生电脑系统分为BIOS调用 , DOS调用功能, 文件调用 ,外设调用.
1. BIOS 调用说明
★ BIOS调用功能号放在寄存器A中,BIOS调用功能入口地址为$5800. 即:
入口
: (A)=功能号 入口地址=$5800
其它参数视各功能号而定.
出口: 状态寄存器(C、V、I)内容不变
其它参数视各功能号而定
★ 使用BIOS调用的功能号及调用入口都要用符号来表示,不要直接使用数值.
ROMBiosEntry equ $5800
BiosSetPage equ $01 ;设置当前使用的页,
BiosGetPage equ $02 ;取当前使用的页号
BIOS页功能子程序:
; ** getPage() A page **
getpage:
lda #BiosGetPage
jsr ROMBiosEntry
rts
; ** setPage() X page **
setpage:
pha
lda #BiosSetPage
jsr ROMBiosEntry
pla
rts
★ BIOS调用中出现的X|Y,表示由8位寄存器X和Y合成的一个16位字,其中X为字的高8位,Y为低8位.
★ BIOS调用中,用到的缓冲区位置通常由页号连同指针共同给出,其中仅当这个缓冲区指针处于分页区($8000--$BFFF)时才使用
页号,其它地址时页号不被采用.
★ 除了特别说明外,调用后所有寄存器和输入变量都可能被破坏,所有未用的内存单元的内容都不被破坏.
2. DOS调用功能号说明
DOS调用功能号放在寄存器A中,DOS调用功能入口地址为$580C. 即:
入口: (A)=功能号 入口地址=$580C
其它参数视各功能号而定.
出口: Cy=0 操作正确.
Cy=1 操作错误. (A)=出错返回码.
其它参数视各功能号而定
(1). DosProgramReturn equ 0
程序结束返回.(等效于 MS-DOS AH=0)
入口: (X)=返回码
(Y) 位0=1 :要求初始化显示屏幕(执行DosInitialScreen)
位1=1 :要求初始化系统资源(执行BiosRunInitial)
(2). DosReadKeyEcho equ 1
读键盘屏幕回显.(等效于MS-DOS AH=1)
出口: (A)=字符码
当字符码等于0为功能键输入,下次调用取出功能键码.
(有关字符码和功能键码与MS-DOS完全相同)
(3). DosDisplayChar equ 2
显示字符. (等效于MS-DOS AH=2)
入口: (X)=字符码
当启动中文系统后,处理汉字国标码(两个国标码拼成一个汉字)
(4). DosGetKBSting equ 6
输入字符串并回显.(等效于MS-DOS AH=0AH)
入口: X|Y=字符串缓存指针(当前页)
缓存字节0: 可输入的最大字符个数(包括回车字符).
出口: 缓存内容 缓存字节0: 不改变
缓存字节1: 输入字符串的字符个数(不包括回车字符)
缓存字节2开始: 输入的字符串(最后一个为回车字符).
注: 字符串不接收功能键,字符串以回车[Enter]结束.DosGetKBString支持命令行编辑功能键(F1-F4)
输入及显示字符DOS功能子程序:
; A: char *
putc: ;在屏幕显示一个字符
tax
lda #DosDisplayChar
jmp DosIOEntry
; A : exit mode . (0 or
ff) *
exit: ; 退出程序
tay
lda #DosProgramReturn
jmp DosIOEntry
;printf() A:X|Y the string, end by 0 *
printf: ;在屏幕上显示一串字符,字符串以0为结束标志
sta printf02+1
stx printf04+2
sty printf04+1
jsr getpage
sta printfE+1
printf02:
ldx #$00
jsr setpage
printf04:
lda $7000
beq printfE
jsr putc
printf08:
inc printf04+1
bne printf04
inc printf04+2
lda printf04+2
cmp #$c0
bcc printf04
asl printf04+2
inc print02+1
bne printf02
printfE:
ldx #$00
jsr setpage
rts