原文标题:《Tutorial 89 v.5》
原文地址:http://tifreakware.net/tutorials/89/a/calc/89asm.htm
中文翻译如下:
非常初级的TI-89教程。这篇文章全面地介绍了在TI-9下使用68K汇编进行编程的基础。
这也是目前唯一的在89上的汇编教程!:)
注意:一些人给了我一个很好的标题页!:)
- 0.0 介绍
- 1.0 基础知识
- 2.0 寻址
- 3.0 第一条指令
- 4.0 变量
- 5.0 分支/跳转
- 6.0 库调用
- 7.0 编译
- 8.0 堆栈
- 9.0 ROM调用
- 10.0 图形
- 11.0 排查错误
- 12.0 更多帮助/工作人员
TI-89图形计算器是一个非常强悍的工具。可以执行用68K汇编语言编写程序的潜在能力更使其如此。这部教程假定你拥有一部TI-89,对其基本功能了解得相当好,但是对于如何使用汇编语言一无所知。希望有人因为读了这篇文档而准备学习68K,并且写出很好的游戏来,这样我就不会在数学和物理课堂上感到无聊了。:)
你应该同时下载 68kguide.txt
,以及 92guide.zip
。尽管这些是为TI92写的,它们对于TI89的编程也是很有用的。这些文档可以通过 http://www.ticalc.org/pub/text
来下载
TI-89使用了摩托罗拉68000(68K)处理器,因此,我们将要学习68K汇编(ASM)。我推荐使用PlusShell
,因为这正是我所用的,但是 DoorsOS
也写得极其出色。
处理器只能理解二进制数据格式。我将会在本文中使用一个 %
符号来表示一个二进制数字。例如:
%00110101
。我将会使用一个 $
符号来表示一个十六进制数字。例如:$A3
。除非有特别的说明否则数字一般表示十进制。你储存的所有数据,不论是数字34还是一个字符串,都以二进制的格式保存。这些对于显示图像都是很有用的。
在TI-89的心脏部位是一颗10MHZ的68K处理器。处理器中有一些重要的区域,首先是寄存器(register)。寄存器是位于处理器的一些小的存储单元。有时必需要把寄存器当做数据存储单元来使用,而且通常要比使用RAM中的存储单元速度更快。(location当做存储单元翻译)。所有这些寄存器都是32bit的长度空间,意味着他们能够存储32个二进制数字。共有8个数据寄存器(D0,D1,D2,...D7),你可以用它们来做任何你想做的事情。它们通常在调用库函数时被用来提供参数。这些寄存器可以互换使用,这里并没有像Z80汇编中的A寄存器一样的“主”寄存器。另外还有8个地址寄存器(A1,A2,A3...A7)。它们主要被用来保存指向RAM里数据的指针,或者保存那些指向我们准备跳转过去的RAM里的子程序的指针。这里同样也有一个状态寄存器,虽然我们不需要使用。仅需要知道当你使用 bxx
指令时,你会用到状态寄存器。如果你想要知道更多关于状态寄存器的信息,去读 Jimmy Mardell 写的 68kguide.txt
吧。它是为TI-92写的,但是既然它们都使用68k处理器,状态寄存器是相同的。
在TI-89里有3种数据长度。1个BYTE(b)为8bits,它能保存一个8位的二进制数。1个 word(w)是2个bytes长,1个longword(l)是2个words长。
68K使用大端方式来保存数字,因此 $1234
被保存为 $12 $34
,不像Z80会保存为 $34 $12
。
分号(;
)用来标识注释语句。注释会被编译器忽略掉,因此增加它们来帮助其他人阅读你的代码。1个分号表示在它后面的这一行里剩余部分是一条注释。
在汇编中我们需要随时了解那些保存在RAM中的数据或函数的的地址。有两种保存地址信息的方式,相对于我们当前位置的地址位置,或者绝对地址位置。如果我们使用相对地址保存地址信息,我们说:“这片数据位于我们现在位置的95个 bytes
之前的位置”。这是相当限制的,当我们执行程序时就会移动当前位置,这样那个地址就不可用了。然而这种方式比较小,因此当我们修改程序的字节时,可以尝试着使用这种方式。第二种方式是绝对地址。RAM中的每件事物都有一个数字来标识它所在的存储单元。这些数字通常写成16进制的形式。例如,TI-92的LCD存储被保存于 $4440
处。绝对地址允许你从程序之外获取数据,查看VAT(不必担心你不明白这是什么)等等。这两种地址方式都使用地址寄存器。
很明显你需要学一些简单的指令,这样你就能编些什么程序出来。 :)如果你需要把数据从一点移动到另一点,你将使用 move.x
指令。move.x
指令有两个参数,一个源,一个目的。x
用1个表示长度的字母来代替,可以使用 b
,w
或 l
替换,取决于你想要移动1个byte,word 或者 longword。
代码示例:
move.b d0,d1 ;moves a byte from d0 to d1
move.w #10,d2 ;Puts the decimal number 10 in d2
移动数据到一个寄存器时一定要小心操作,如果你移动一个 byte
,这个 byte
将会填满这个寄存器的最低 byte
位,并且不管
这个寄存器剩余的部分。
#
表示一个立即数(也就是一个单纯的数值,不代表内存单元地址,也不代表指针)。通过执行 move.b #$F,d0
来移动一个16进制数。如果你执行 move.b $F,d0
这条语句,将会把一个存储在内存单元 $F
处的 byte
移动到 d0
里去。
例如:
如果 d0
包含 %10100100101001001010010010100100
,并且我移动 byte
%00000000
到它里面,这样 d0
就会变为下面的数据:
%10100100101001001010010000000000
下一条指令是 clr.x
。它带有1个参数并且它会把那个参数里的所有 bits
都设置为0,x
再次指示长度。
例如:
clr.l d0 ;把d0里的所有 bits 都设置为0
现在就谈数学指令。68k有相当强大的数学指令。这里是基本的一些:
add.x
, sub.x
, mulu.x
, muls.x
, divu.x
, divs.x
add.x
和 sub.x
比较简单。他们是加法和减法。
如果我想把 d0
和 d1
相加,然后把结果储存到 d1
,我会执行这条指令:
add.l d0,d1
务必要指定长度。如果和超过了长度限制,答案将不会正确,因此要细心。
如果我想用 d0
减去 d1
,然后把结果储存在 d0
中,我会执行这条指令:
sub.l d1,d0
再次记得指定长度。
现在做乘法,这里有2个不同的指令,muls
和 mulu
。
mulu
执行无符号数,意思是非负的。
muls
则可以处理负数和正数。你可以通过阅读 68kguide.txt
或 newbies.tx
t 来了解 68k 是如何储存负数的。
想要把 d0
乘以 d1
并且把答案储存在 d1
中,执行如下语句:
mulu.x d0,d1
要注意你应该确认你所选的数字不会超过数据长度的限制。要确保指定数据长度。
接下来是除法。几乎和乘法是一样的。
你有 divu.x
和 divs.x
可用。
执行 d0
除以 d1
(使用无符号数除法),然后把答案储存在 d0
里,执行如下语句:
divu.x d1,d0
你可以在 68k 中使用变量,并且把它们保存在你的程序里。这些变量甚至在你退出程并且后重新运行程序之后仍然保持它们最后的值。这对于游戏中的高分榜是很有用的。下面就是如何声明一个变量,代码形式如下:
var_name: dc.x 0
用你实际的变量名替换 var_name
,用数据长度替换 x
。你也可以放一个起始值来替换 0 。
使用字符串的方式如下:
string_name: dc.b "This is a string",0
你必须使用 dc.b
因为字符串中的每个字符都占据1个 byte 的位置。其中 0
是加到每个字符串末尾的一个空字符。加 0
是必须的这样计算器能知道什么时候字符串结束。
你可以通过 move.x
来把一些数据放到变量里,如下:
move.x #10,var_name
只要确保 move
的数据长度小于或等于变量的数据长度就可以了。 :)
首先,我们需要一些 labels
用于跳转。你可以像下面一样定义一个 label
:
label_name:
这就够了,然而千万不要忘记 label
后面的那个冒号 :
跳转到另外一个内存区域的第一种方法是执行 goto
语句的等价语句 jmp
。你需要有一个绝对地址来使用它,但是我不推荐用这个因为你无法使用一条 rts
语句从跳转过去的地址返回。你应该使用 bsr
来做你程序内的无条件转移。就像这样:
bsr label_name
计算器会跳转到标号处并且继续执行代码。 bsr
保存了生成跳转语句所在位置的地址。如果你使用 rts
指令,计算器就会返回到 bsr
语句,并且继续执行下一条指令。
例如:
bsr label_name;---|
move.w d0,d1 ;---+-|
... ; | |
... ; | |
label_name: ;---| |
move.w #10,d0 ; |
rts ;-----|
想要做一个条件跳转,只要执行到如果某个条件存在的 label
即可,我们使用 cmp
和 bxx
指令。你用某些字母替换掉 xx
来指定跳转依赖于哪些条件即可,举例如下:
cmp B,A
bxx name_of_label_to_branch_to
然后 bxx
将会执行一条分支到 label if
: 处
处理无符号数的 bxx
指令如下所列:
A > B
对应 bxx
为: bhi
( 如果 A 大于 B,则跳转 )
A ≥ B
对应 bxx
为: bcc
( 如果 A 大于等于 B,则跳转 )
A ≤ B
对应 bxx
为: bls
( 如果 A 小于等于 B,则跳转 )
A < B
对应 bxx
为: bcs
( 如果 A 小于等于 B,则跳转 )
A = B
对应 bxx
为: beq
( 如果 A 等于 B,则跳转 )
A ≠ B
对应 bxx
为: bne
( 如果 A 不等于 B,则跳转 )
处理有符号数如下所列:
A > B
对应 bxx
为: bgt
( 如果 A 大于 B,则跳转 )
A ≥ B
对应 bxx
为: bge
( 如果 A 大于等于 B,则跳转 )
A ≤ B
对应 bxx
为: ble
( 如果 A 小于等于 B,则跳转 )
A < B
对应 bxx
为: blt
( 如果 A 小于等于 B,则跳转 )
A = B
对应 bxx
为: beq
( 如果 A 等于 B,则跳转 )
A ≠ B
对应 bxx
为: bne
( 如果 A 不等于 B,则跳转 )
这实际上是用一个操作数减去另一个操作数,把部分结果保存在状态寄存器中,同时 bxx
指令会去检查状态寄存器的标志位,不过你现在需要了解得只是如何去使用它。
例如:
move.w #10,d0
move.w #14,d1
cmp d0,d1
beq never_branches_here
现在说一下外部跳转。使用指令 jsr
进行外部跳转。它的工作原理同 bsr
一样。通常被用来调用库函数。
库函数调用允许程序员把一些常用的函数放到计算器中的某处,并且让所有的程序都可以使用它们。
TI-89最重要的两个库是 util
和 tios
。tios
实际上不是一个库,它包含 ROM
调用。util
包含 Fargo2
的 flib
中的所有函数。我建议你读读 util.htm
来学习它的函数,它被包括在
PlusShell.zip
中。在 util
库中有很多有用的函数。一个例子就是 clr_scr
。
clr_scr
清除整个屏幕,并且重画屏幕底部的分界线。
如下的例子展示了如何调用这个函数:
jsr util::clr_scr
这就是清除屏幕所有你需要做的!
一个更一般的例子如下:
jsr lib_name::function_name
注意:你必须把函数库头文件 include
到你源代码的开始处,否则编译器不知道你的意思。
例如,在你源文件的顶部输入如下内容:
include "util.h"
一个一般的例子:
include "library.h"
对你准备在程序中使用的每个库都执行如上的操作,同时确保 library.89z
文件也在你运行程序的计算器里。:) 多读读 util.htm
文件,和大多数规格TI(Dimension-TI 不知怎么翻译)上的函数库里的
.h 文件一样,因为通常有人已经把你想要的程序写好了,而且它们很可能会工作得很好。
很好,目前为止一切都很不错,但我们需要知道如何去编译。使用 PlusShell
,我创建了下面的名为
setup.bat
的批处理文件:
@echo off
call e:\ti89\shells\plus\setps e:\ti89\shells\plus
拿你自己计算机上的路径替换掉上述文件中的路径 e:\ti89...\plus
如果运行这个文件,你接着就能使用 asm89
来把你的源文件编译成一个能够在计算器上运行的 .89z
的文件。为你的源文件命名为 name.ASM
,可以取任何你想取的名字。
然后,从DOS命令行,输入:
asm89 name
这样你的程序就会被编译了!将会显示所有的错误信息,如果没有错误,它将会新建
name.89z
使用 asm89
需要记住的重要的是:既然用 a68k来汇编,全部指令必须不能从第一列开始。或者在指令前加一个 tab
或一个空格,或者编译器会把指令当做标号来处理。你能用任何文本编辑器来创建 .asm
文件,但是它必须是 .txt
文件格式,既不能是 MS WORD6.0
的文件格式,也不能是其他任何文件格式。我使用 MS-DOS 编辑器 (edit.com)
基本的程序结构如下:
include "tios.h" ;Include tios.h so we can use the functions in tios
include "util.h" ;Include util.h so we can use the functions in util
xdef _main ;You must have this line, it tells the calc where to begin execution
xdef _comment ;Tells the calc where the comment for the program is
xdef _ti89 ;Tells the compiler that this is a TI-89 program
_main: ;execution begins at the _main label
... ;your code
rts ;exit the program
_comment: dc.b "Hello World!",0 ;This is the comment for the program
end ;Tells the compiler to stop here. NECCESARY!!!
需要注意的是标号 _main:
和 _command:
可以从第一列开始。这是因为它们都是标号,而不是指令。其他任何都不能从第一列开始。注意注释行都放在分号 ;
之后。
堆栈是内存中的一个区域通常通过寄存器 a7
来被指向。它能够被用来为 ROM
函数提供参数,它也被用于 bsr's
, jsr's
和 rts's
。你必须要记住更新堆栈指针。不论什么数据放入堆栈都要遵循后进先出的原则。移动一些 word
到堆栈可以使用 move
指令,我们不需要一个单独的 push
指令。
例如:
move.w d0,-(a7)
这条语句通过寄存器 a7
和1个 word 在把指向堆栈的地址指针减少之后,把储存在 d0
中的 word 移动到堆栈。我们必须让地址减少,否则这个 word 将会覆盖掉其他很可能很重要的的信息。 :)
把这个 word 取出的操作语句如下:
move.w (a7),d0
lea 2(a7),a7
围绕 a7
的括号意味着在寄存器 a7
中所保持的内存地址单元处的数据信息,并不是 a7
中的数据。 语句 lea 2(a7),a7
更新了堆栈指针。 lea
是一条意思为 “加载有效地址” 的指令。这条语句让 a7
增加2,然后再把它保存回 a7
。我们之所以为 a7
加 2 是因为之前我们减少了 1 个 word = 2 bytes。在使用堆栈之后一定要记得更新堆栈指针 a7
,否则你的计算器很可能会崩溃。
ROM
调用对于TI-89来说非常有用。TI已经写了很多有用的程序,而且我们可以使用它们。诸如显示文本,更改字体以及使用 linkport
的程序都可以被程序员使用。在这个 zip 文件中你能找到 tios.fct
,这个文件包括了 tios
库里的全部函数和它们的功能和参数设置的介绍。你调用 tios
函数就跟调用其他普通的库函数一样。参数通常在堆栈中,并且最后一个参数是最先被压栈的。ROM
函数也很可能使用了寄存器 d0-d1
和其他别的地址寄存器,因此在你调用 ROM
函数时不要指望这些寄存器会保持原值不变。
一个 ROM
函数的例子是 DrawStrXY
tios.fct 包含如下:
;-------------------------------------------------------
; void DrawStrXY(WORD x, WORD y, BYTE *string, WORD color)
;
; Function: prints {string} at {x,y} with current font
;-------------------------------------------------------
这里有一些关于颜色的选项,来自 Fargo2 的 types.txt
:
color
-----
0 = white on black
1 = black on black (!!!)
2 = white on black (??)
3 = gray on white
4 = black on white
函数中参数 string
前的星号 *
表示是一个指向字符串的指针,因为我们需要找到我们想要显示的字符串的地址。
下面是一个使用了这个函数的例子程序:
include "tios.h"
include "flib.h"
include "macros.h"
xdef _main
xdef _comment
xdef _ti89
_main:
jsr flib::idle_loop
jsr flib::clr_scr
move.w #4,-(a7) ;color=black on white
pea _comment ;nice instruction, pushes the pointer we need for us
;and takes care of the stack.
move.w #10,-(a7) ;y coordinate of text=10
move.w #10,-(a7) ;x coordinate of text=10
jsr tios::DrawStrXY ;Display it
lea 10(a7),a7 ;we pushed a total of ten bytes
rts
_comment:
dc.b "Hello World!",0
end
阅读 tios.fct
文件来了解其他 ROM
调用,我也推荐从 Dimension-TI 的 fargo2.zip 文件中获得 types.txt
,handles.txt
,以及 traps.txt
。 它们都是非常有用的文件。
如果你想要成为一名游戏程序员,你需要懂得如何处理图像。好的,首先是使用库。 :) 去读 linelib.g
和 graphlib.h
。它们有很多有用的函数。如果你想做你自己的图像,不过这些讲的都是TI-89如何处理图像的。
包含在变量 LCD_MEM
中的地址的起点,是一个 3840 bytes
长的内存区域。这个内存区域是计算器 LCD
屏幕的一个镜像。如果这个内存区域中的 1 个 bit
被设置,屏幕上的一个像素点会变黑。在这个内存区域,由像素点组成的每条横行都有 20 bytes 长。接着,有一个 10 bytes 长的 buffer,被用于 92+ ,接着的下一行又是 20 bytes 长,以此类推。你可以移动二进制数字到这块内存中来把一些内容显示到屏幕上。
你也可以使用位操作指令。这些指令对一个 byte 中的单个 bit 起作用。所有的指令在影响 bit 之前会先检测这个 bit:
BTST 检测一个bit的值
BSET 检测一个bit的值,然后把它设置为 1
BCLR 检测一个bit的值,然后把它重置为 0
BCHG 检测一个bit的值,然后把它翻转 (原来是0,就置为1,原来是1就置为0,0 → 1, 1 → 0)
执行完这些检测指令后,如果对应的像素点被开启,你可以使用 bxx
指令来跳转到任何位置 。所有这些指令都有2个参数,第一个是要检测的 bit
,第二个是这个 bit
所处的内存单元的地址。
在 util.h
中有一个非常有用的函数 find_pixel
。想要使用 find_pixel
,我们先把想要找的像素点的 y
坐标值用一个 word 压到堆栈里,然后是标识 x
坐标值的1个 word 。然后,记得更新堆栈指针。接着 d0 包含着我们想要找的像素点的 bit 数,a0
包含我们想要找得像素点的内存单元地址。然后我们可以执行:
bchg.b d0,(a0)
来改变这个像素点!
因此,翻转屏幕上(左上)的一个像素点,并且接着如果它在我们改变之前已经被设置,则分支跳转到
pixel_was_black
,可以用如下语句来实现:
move.w #0,-(a7) ;y=0
move.w #0,-(a7) ;x=0
jsr flib::find_pixel
lea 4(a7),a7 ;update stack pointer, 2 words=4 bytes
bchg.b d0,(a0)
bne pixel_was_black
dbxx
指令在图像处理中非常有用,它们是 68k 处理循环的方式。
dbxx
是一个和 loops
对等的指令。这条指令和 bxx
非常相似(相同的条件被使用,除了很少一些不同之外),除了第一个操作数是数据寄存器,这个操作数每次将被减少 1
直到减为 -1
,然后循环停止。如果标志位(判断条件所指定的)被正确设置,循环也会退出。你经常使用 dbra
(从不退出),当数据寄存器变为 -1
时将会退出循环。如果你想让循环进行 10
次,你应该把数据寄存器设置为 9
(因为它会在 -1
时终止,而不是 0
时终止)。可以在 68kguide.txt
中查看其它条件。
这里是一个滚动程序我使用在 tunnl
,它可能对于很多程序有用。你可以修改它或者使用它,只要你把我的名字列在你随程序发布的文本文件上的 CREDIT
列表中就可以了。
scroll: ;scrolls the entire screen down one row. Leaves the top row
;however it was before the scrolling, NOT CLEARED.
lea LCD_MEM+3810,a0 ; 3840 bytes long-30 bytes to get the last row
move.w #1904,d0
RepScrl:
move.w -(a0),30(a0)
dbra d0,RepScrl
rts
关于灰度的教程可能在本文将来的版本中出现。给我发EMAIL或者同我在IRC上交流,如果你希望我把它加进去的话(看本文底端的信息)
用汇编编程你的程序常常会崩溃。多读代码,试着指出问题在哪里。这样是很有益处的:如果你每次写一小段,然后测试每一小段,因此你能把问题隔离出来。
如果你在一个大程序中发现一个 bug
,试着把一些代码注释出来,通过这样来看你是否可以指出 bug
所在。如果你找不出 bug
,有时你需要从编码中离开一小会儿。不要放弃!如果你需要别人帮助分析你的代码,请看第 12.0 节
如果你需要更多帮助,不要犹豫联系我
Skip Jordan - [email protected]
Cassius on EFNet, DalNet
如果你需要更多帮助请接着往下看
Assembly 89 的邮件列表可以在 http://www.ticalc.org 找到,给我发邮件,进入 IRC 并且在 EFNet 加入 #dim-ti 和 #ti。这里有一个Windows系统下的 IRC 客户端共享软件,可以在这里下载 http://www.mirc.co.uk/
这个文档并不意味着把 68k 所有的内容都教会你,它仅仅给了你一个起点。去阅读 68kguide.txt,以及其他人的程序的源代码。去练习然后你将学到比这篇文档所教会你的更多的内容。
如果任何有经验的程序员,或者新手,在这篇文档中发现了错误,请一定告诉我!!!我将会修改它!只要告诉我出错教程的版本号,以及错误是什么/错误在哪里就可以可以了。谢谢。如果你希望我增加诸如灰度图像、中断、写自己的库或者VAT相关的内容,请联系我。
另外,请让我知道是否需要对一些内容解释得更好一些,这是我写的第一份教程,我会喜欢你的建议的。
致谢:
David Ellsworth: for Fargo
Rusty Wagmer: for PlusShell
The Doors Team: for DoorsOS
Jimmy Mardell: for tremendous games and 68kguide.txt and sprmaker
Dimension-TI: for a great website. (http://dim-ti.akromix.net/)
The people in #dim-ti and #ti on EFNet for chatting
Programmers for TI89 assembly: For their great work.
Skip Jordan
Last revised 12/23/98
This is version .5 of Tutorial 89.
翻译者:FreeBlues
如果有哪些地方翻译得有问题,欢迎大家指出,确认无误后我会把更确切的翻译合入新版本,谢谢!