Welcome to lesson #6 of the Ash School! This lesson tells you about some more instructions, some
new registers and a few other tihngs. The last sections contains some information about greyscale graphics.
CONTENTS
OPCODES
It's about time that you should know more than just program. You should also
know why things happens, and how it happens. For example, a computer (and a
calculator) understands machine code, 0 and 1. That's what the assembler does,
it translates the assembly language into machine code. The difference between
assembly languages and high level languages is that each assembly instruction
has it's own opcode, one or more bytes in a row that does exactly what you've
told it. In Pascal or C++ for example, every instruction is usually changed
into many opcodes. Many of those opcodes are usually unnecessary or could be
done in a smarter way. For example if you do a short for loop in a Ash
program, you can use the B register and the djnz instruction, which is made
for that purpose. If you did a for-loop in Pascal or C++, it would use a
variable, a memory location, to store the counter. Each time the loop was
repeated it would load that value into a register, increase it with one, store
the variable, check if the limit is reached and then repeat if not end of loop
Guess why assembly language is faster...
On a Z80, the processor used by the TI-82 if you have forgotten, each
instruction (opcode) takes between 1 and 4 bytes. Small instructions are, of
course, generally executed faster, but there are 2 byte instructions that are
executed faster than 1 byte instruction (it depends on what the instruction
does). It's quite easy to guess how long each instructions is, which is useful
when optimizing your code. Each time you load a byte into a 8-bit register,
the size is two bytes; one byte to tell the processor that you should load an
immediate value into a 8 bit register, and one byte where that value is stored.
16-bit registers takes three bytes. Generally, all instructions that has one
byte takes two bytes, and the instructions using words (both as immediate
values and addresses) takes three bytes. Other instructions, such as loading
register A with E, takes one byte. All bit-manipulating instructions except the
logical instructions (and, or, xor, not) takes two bytes though. One prefix
($CB) and then the byte that tells what to do.
Now you know some more about how instructions really look like, which will help
in the future...
ALIAS
AND
MACROS
Aliases and Macros are used by the compiler and has nothing to do with
assembly languages actually, but they're used quite often since it makes the
source much easier to read. In TI-85.H there are lots of aliases and macros
(actually, there are only aliases and macros in it, plus comments).
You could probably guess what an alias is. If you type
Three = 3
the compiler will substitute Three with 3 in the whole source code (when
compiling, it won't change the file itself). All "variables" in a program you
define in the beginning are just aliases. You could typ those addresses in the
source code if you like that, but it's much easier to read if you make
aliases... Also, if you want to change an address, and have that address
located in many different places in the program, it would have been smarter to
make an alias of it. Then you would only have to change the aliases, not every
time in the source where the address occured.
Macros are similiar to aliases, and also similiar to subroutines sort of. To
make a macro in Table Assembler (if you use another assembler, CAZ for example,
you probably do it another way; read the documentation) you can type like this:
#DEFINE PushAll push af \ push bc \ push de \ push hl
If you now type PushAll in your code, the compiler will replace it with
those four instructions (must be separated with a backslash). You can also
give parameters which is useful: (the parameters must be numbers, or aliases,
not registers or addresses)
#DEFINE GotoXY(xy) push hl \ ld hl,xy \ ld ($800C,hl) \ pop hl
An example on a macro you use in almost every program is ROM_CALL(addr).
Just check ti82.h and you'll see. The parameter in
the ROM_CALL macro is an aliases by the way. ROM_CALL(D_ZT_STR) could be replaced
with ROM_CALL($3914-$1A), but it just makes the source harder to understand.
Creating your own macros is often useful, but remember that if you make a big
macro and use if often, it may make the source big... in the GotoXY example
above, the push hl \ pop hl part may be unnecessary if there were no important
things in the HL register. Macros should generally be small!
MORE
REGISTERS!
If you've programmed for a while you've probably quite often thought: "Damn,
I'm out of registers again!" It's a fact that the Z80 has few registers,
especially compared to the 68k processor which the TI-92 use... There are
actually, some more register you could use, though they have limitations. Here
they are:
The index registers IX and IY
These registers are handy. The IY register is used by interrupts and rom
calls, and should not be changed unless you've disabled the interrupts (see
below). The IX register though is free to use anytime! It works almost exactly
like the HL register. When you can use the HL register, you can use the IX
register! There is one drawback though, the instructions become one or two bytes
longer, depending on the use. If you use IX as a dataregister in an instruction,
the instruction becomse one byte longer. That byte is a prefix ($DD) which tells
the processor that the next instruction will have IX in it, not HL! This is
necessary, because the opcode for an instruction which uses IX is the same as
for using HL. It's only the prefix that is the difference.
When you use the IX register as an addressregister, a pointer, it becomse two
bytes longer, the prefix and a shift byte. That shift byte is useful sometimes,
though most often you don't use it (but the instruction gets two bytes longer
anyway). The shift value is a signed byte and therefore has the limitation
-128..127.
Here are some examples:
ld ix,$1234 ; The instructions is one byte longer than ld hl,$1234
ld (ix+5),62 ; Loads the byte 62 in the memory location IX+5. If
; this instruction was executed right after the previos
; one, it would mean at $1239
If you don't use the shift byte, just (ix), you may still have to put it there,
(ix+0). I think you must do this on the CAZ assembler, but you don't have to
add that if you use TASM.
If you want to use the IY register, it works the same way. The only difference
is that the prefix is $FD instead of $DD. Remember to disable the interrupts
(see below)!
When you use the IX register (or IY) you can't use the HL register in the
same instructions (neither just H or L). For example, add ix,hl wouldn't work,
but add ix,ix works fine.
Maybe you've thought why the index registers aren't divided into two 8 bit
register, as HL. The answer is they are, there are just no instructions to
access those, or at least those instructions are not documented and not
supported by assemblers directly. Of course, if you type the opcodes for the
instructions it works just fine! First you should know what these 8 bit registers
are called. I don't know if this is the "real" name, but IXh, IXl and IYh, IYl
is used (h=higher byte, l=lower byte). The opcodes are quite simple... if the
assembler doesn't handle those instructions, type this:
.db $DD \ ld a,h ; If you want to ld a,IXh
.db $FD \ ld l,e ; If you want to ld IYl,e
.db $DD \ ld l,(hl) ; This should be equal to ld IXl,(IX)
The last one I'm not sure about, but it should work that way... try it
yourself.
The "not-so-often-used" R and I registers
These registers are very seldom use, because they are very limited. The only
way to "communicate" with them is through the accumulator, so the only allowed
instructions are:
ld a,r
ld r,a
ld a,i
ld i,a
What do they do? The R register is called the "Memory Refresh" registers
and is updated after every instructions (added with two I think), thus making
this register more or less a random register. The only real use for it is in
random generators, though it's not a good random generator... First you'll have
to shift it one step to the right (since it will only contain even numbers if
you don't change it with ld a,r) and then you'll have to mask away the bits
you don't want. If you want a random number between 0 and 15 (the range must
be a power of two for good generation, see below), you do like this:
ld a,r ; Loads the accumulator with the "random" register
srl a ; Shift it logically (put a 0 at the highest bit) one step
; to the right
and 15 ; Masks off the unnecessary bits
This works fine if you don't use if often. If you want 100 random numbers and
use this routine the randomize will be bad (if you randomize all numbers at once)!
It will be even worse if the range isn't a power of two. Let us say we want a
number between 0-6. How would you do? You can type and 6 (figure this one out
if you're unsure about binary and masking) because then you would get a strange
result (only 0, 2, 4 and 6 would occur). The best way is to do like this:
Random:
ld a,r
srl a
and 7 ; between 0-7
jr z,Random ; If it's 0, rerandom
This works, and we'll get a random number between 1-7 (decrease with one to
get between 0-6). The problem is the frequency... one of the numbers will appear
2/8 of the time, the others 1/8. Why? Because every time the routine have to
rerandomize it means that the R register ends with 4 zeros (the last bit that
was shifted away plus the three masked bits). The newly randomized numbers will
then always be the same (should be 3 or 4 I think) because the number of
instructions are always the same.
How to fix this!?!? Well, you could make a random lookup table of 56 bytes
and use the R registers in another way. If you really want to know, you could
message me. It still won't be good, but much better. Another way which may work
is to use the HALT instruction. More about that instruction another time.
Back to the I register... this one is unimportant most of the time. It's only
useful when you program interrupt handler and stuff like that. It holds the
higher byte of the address which should be called when an interrups occurs and
you're in interupt mode 2. If you don't understand what I'm talking about, don't
bother... you don't need to know right now. It'll be better explained in another
lesson.
You can use it though as a temporary register if the interrupt mode isn't set
to 2. The interrup mode is usually set to 1, but when using grayscale graphics
it's set to 2, so then the calc would crash if you changed the I register (same
thing happens if Game Wizard or any other memory resident program is run)...
Since you can only change it through the A register, it's often not worth that
use though. One last thing also, you don't have to reset the register if you
change it. It's not used in the ROM memory. The TI-82 was never meant to use
the I register or interrupt mode 2. More about that in the lesson about
interrupts.
The shadow registers
Now something cool... I think many will be surprised to know that there are
7 more 8 bit registers available to use! These registers are called the shadow
registers and are a copy of the common registers, A, BC, DE and HL. Their names
are similiar also, you add an ' after the register name: A', BC', DE' and HL'.
The drawback is quite big though, you can never access those register through
any instruction at all actually... There are instructions, however, that will
exchange the ordinary registers with the shadow register. Those instructions
are:
ex af,af' ; Exchange AF with AF' (F' = shadow flag register)
exx ; Exchange BC, DE and HL with BC', DE' and HL'
When using these instructions you must have disabled the interrupts, because
they're used by the ROM. You don't have to restore them to what they were before
(no push/pop needed). To disable interrupts (and enable them again) you use two
short instructions:
di ; Disable interrupts
ei ; Enable interrups
Another useful exchange instruction
I think I've missed a useful instruction before. Here it is:
ex de,hl ; Exchanges the contents in HL and DE
It will only work on those two registers, and you must type ex de,hl not ex
hl,de (even though they would do the same thing).
There are three more exchange instructions, but they all have to do with
the stack pointer, and are of less use. More about those in an upcoming appendix.
STRING
INSTRUCTIONS
Now some very useful instructions which I've probably waited too long with
to explain. Have you never wanted to copy many bytes from one place to another?
In a simple way? Then you should use LDIR (or LDDR)! First LDI and LDD...
LDI is an instructions that does many things at once, you could say. It does
this:
ld (de),(hl) ; This instruction isn't even allowed...
inc de
inc hl
dec bc
It copes one byte from HL to DE (where they point) and increase both
pointers. LDD does the same except that both pointers are decreased. Both
instructions also decrease BC with one.
The LDIR (LDDR) instructions repeats the LDI (LDD) instruction until BC
becomes 0! If you want to copy 100 bytes from one place to another, just set
HL to the source and DE to the pointer and BC to 100 and run LDIR, and you're
done! Here's how to copy the graph memory to the apd buffer:
ld hl,GRAPH_MEM ; graph mem
ld de,$8228 ; apd buffer
ld bc,756 ; 756 bytes to copy. It's $400 and equal to 128 x 64 / 8
ldir
Most often you use LDIR but sometimes LDDR is necessary.
There are also some other string instructions (that's what they're called)
which will be explained in another lesson.
GRAYSCALE
GRAPHICS
Both on the TI-85 and the TI-92 many games uses greyscale graphics. Because of the
way the displays are handled on the two calcs it is quite easy to make good looking
greyscales. On the TI-82 however there are no easy way to do greyscale, this means that
greyscale does not look very good, and therefore it has not been used in any games. For
this reason i wont go into details, since you probably wont need it.
Greyscale graphics is not something to do with hardware if anyone
though that. What you do is that you have two layers, two pictures. To make it grayscale,
those pictures are flipped very fast, in 200 Hz. 2/3 of the time picture 1 is shown and
1/3 of the time the other picture is shown. If a pixel is set on both layers, it is shown the whole
time (3/3) and becomes black. If it's only set on the first picture it's
visible 2/3 of the time and looks like darkgray. Only on the second picture, 1/3
of the time (lighgray) and if it's never visible, well, then it's white.
That's how it works. If you are interested in greyscale graphics I have written a library which
makes it a lot easier to do. The library also includes a test program which should show you have it is
used. As always this program can be found on the Ash Homepage or at www.ticalc.org.
To use greyscale you will probably need rutine such as sprite routines and line
routines, but since i do find greyscale very useful on the TI-82 i wont go into that.
That was all for now I think.... Hope you've learned something at least...
Happy programming!!