Welcome to maybe to most interesting (and definately most wanted) lesson in the Ash school! Now you'll
learn what the stack is, and maybe more imporant, graphics! Quick graphics!
CONTENTS
STACK
The stack is one of the most important things you must know when programming. Think of the stack
as a deck of cards. When you put a card on the deck, it will be the top card. Then you put another card,
then another. When you remove the cards, you remove them backwards, the last card first and so on.
The stack works the same way, you put (push) words (addresses or register pairs) on the stack and
then remove (pop) them backwards. That's called LIFO, Last In First Out.
The Z80 uses a 16 bit register to know where the stack top is, and that register is called SP. You
should NOT change that register with Inc, Dec or something else if you don't know what you're doing!
PUSH
AND
POP
As you may have guessed, push and pop is used to push things on the stack and then
take them off. When you push something, the stack counter will decrease with 2 (the stack "grows" down,
from higher addresses to lower, but you usually don't have to bother about that) and then the register pair
is loaded into the stack. When you pop, the register pair is first lifted of the stack, and then SP increases with
2.
You can push (and pop) all register pairs: BC, DE, HL and AF. When you pop AF, remember that all
flags may be changed. You can't push an immediate value. If you want, you'll have to load a register pair
with the value and then push it. Perhaps it's worth noting that when you push something, the contents
of the registers will still be the same; they won't be erased or something. Also, if you push de, you can
pop it back as hl (you don't have to pop it back to the same register where you got it from).
Actually, the stack is also updated when you call and return from subroutines. The PC (program counter
which points at the current instruction being executed) is pushed to the stack and the calling address
is loaded into PC. When returning, PC is popped from the stack.
So, when is this useful? It's almost always used when you call subroutines. For example, you have an
often used value stored in HL. You have to call a subroutine that you know will destroy HL (with destroy
I mean that HL will be changed to another value, which you perhaps don't know). Instead of first saving
HL in a memory location and then loading it back after the subroutine you can push HL before calling
and directly after the calling pop it back. Of course, it's often better to use the pushes and pops inside
the subroutine. All registers you know will be changed are often pushed in the beginning of a subroutine
and then popped at the end, in reverse order! Don't forget - last in first out. Of course you shouldn't pop
back a register whos contents you're intersted in. If you call a routine to get a value that will be stored
in B, you should not push and pop bc since then B would have the same value as you had before
you called the routine.
If you want to only push one 8 bit register, you still have to push it's "friend". Therefore, be aware
that if you want to store away D with pushing and popping, remember that E will also be changed back
to what it was before. In those cases (if you don't want that to happen you should try first to change
register (try to store the information in E in another register if you can) or else you have to store it in
a temporary variable.
Before executing a program, you should keep track of your pushes and pops, since they are responsible
for 99% of all calculator crashes! For example, if you push HL and then forget to pop it back, the next
RET instruction will cause a jump to HL, which can be anywhere in the ROM/RAM and the calculator
will crash. That's also a way to jump to the location stored in HL, but then you should use JP (HL), which
does the same thing.
Push and pop doesn't change any flags, so you can use them between a compare and a relative
jump depending on a condition, which is often very useful. I should also add that you can almost always use
SP when you can use BC or DE.
Now I think you know everything about the stack. I don't know exactly how much you can push (after
a while SP will point to someplace in the RAM where variables are stored, which will corrupt the memory).
If anyone figures that out (can't be so hard I guess), I would be interested in knowing.
THE
DISPLAY
On the TI82 the only way to communicate with the display is by using the ports. You do this by sending commands to
one port and data to another port. The commands and the data is then handled by the display controller which takes care of
displaying the data on the display. If you want to make really fast graphics this is the way to do it, but there is a simpler
way. The ROM includes a rutine which display the data you place the graph mem, so we can use the this instead. Using the graph
mem is slower than using the display controller, so maybe using the display controller should be part of another lesson ?
The graph mem is a part of the RAM which the system uses to save the last graph it displayed in, since we are not going to do
any graphing in our assembly program we can use this for displaying graphics. Each byte of the buffer holds 8 pixels (since the
pixels are either white or black (0;1) each pixel takes one bit). The top left corner is located at GRAPH_MEM (an alias defined in
ti82.h). A simpel formula would tell us where in graph mem we would change one pixel: GRAPH_MEM+y*12+x DIV 8. But we also have to
know where in the byte, which bit, we should change! The bits are numbered 0-7, and bit 0 is the right most pixel.
We could very easily write a routine that determinates which bit and which address we should change (try
it, it's good practice), but there exist already such a routine in the ROM, so why reinvent the wheel? It's fast
(you can make a faster one, but it's not worth it), but it has one stupid "feature": the origin is at the bottom left
corner, not the top left. Now that's easy to change, just take y=63-y and that's done with. Before using
the routine (called FIND_PIXEL) you also have to swap ROM page to ROM page 4. You only have to do
that once in the program (in the beginning) and it looks like this:
LD A,$8C
OUT (2),A
Put these two rows in the beginning of the program if you use FIND_PIXEL. Now you should ask what
input values FIND_PIXEL uses and what registers it will change. You store the x location in B and the
y register in C before calling FIND_PIXEL and it'll return the address in HL (but you have to add
GRAPH_MEM to it) and 2^bit in A. It's good that it returns 2 is raised to bit, because if you want to set bit A,
you can't SET A,(HL) because the first value must be known! Instead, you use logical operators.
PLOTTING
PIXELS
Here are four routines for putting a pixel, removing a pixel, changing a pixel, and checking to see
if a pixel is lit (all in the graph mem). I have made a small library which includes most of the functions here,
butin a version which uses the display controller to write directly to the display. you can find it at the
Ash homepage or on www.ticalc.org.
PutPixel: ; Puts a pixel at B,C
CALL FIND_PIXEL
ld de,GRAPH_MEM
add hl,de
or (hl)
ld (hl),a
ret
RemovePixel: ; Removes a pixel at B,C
CALL FIND_PIXEL
ld de,GRAPH_MEM
add hl,de
cpl
and (hl)
ld (hl),a
ret
ChangePixel: ; Changes the pixel at B,C
CALL FIND_PIXEL
ld de,GRAPH_MEM
add hl,de
xor (hl)
ld (hl),a
ret
TestPixel: ; Tests the pixel at B,C. If the Z flag is set, no pixel, else a pixel is lit
CALL FIND_PIXEL
ld de,GRAPH_MEM
add hl,de
and (hl)
ret
All these routines will destroy A, DE and HL, so you should push and pop these if you don't want
too loose those values. These routines also shows you that all logical operators are useful.
All routines starts with with three identical rows, which make HL point to the right address and
A will be the byte you should "do something with". In PutPixel, we OR A with (HL) and then we
put the result (which will be stored in A) back in (HL), since it's in the graph mem location we want
the change made. When removing, we changes all bits in A with CPL, and then we mask out
the bit to remove. Since A in the beginning only have one bit set (I really hope you get that), A will
know have 7 bits set, and the bit that isn't set is the one that will disappear, since AND requires
both bits to be set.
The third example is simple, XOR changes the pixel. Since seven bits are unset, those seven
location won't change, and the one bit that is set will make a change. The fourth example
masks out the tested bit, and if A is zero, there were no pixel and the zero flag will be set (AND
sets the zero flag if the result is 0).
It's very important that you understand this section to 100%, else you'll get in big
trouble later on. Try rereading it if you don't understand, and experiment by yourself and try
to change some instructions and understand what the result should be.
PUT
IMAGES
Now when you know how to put pixels, you could also put images. But putting them pixel
by pixel is a stupid way to do it. First, if you use FIND_PIXEL for every pixel, it will be slow and
it's really simple to do it another way.
Now I'll show you a simple way to put images that you'll just use as background. As you've
seen of how the graphic memory works, it's a lot easier to make aligned pictures, that is, you
change all bits in one byte. That's the reason why many games have the same GUI with sprites
that are 8 pixels wide. That's because you don't have to bother about bits, and it's much faster
too.
The image putting routing below always starts on first bit (bit 7, since bit 7 is the leftmost bit
in a byte) since that makes it much easier. You just copy 8 pixels at the same time from the
data at the end of the program straight into the graph mem. Simple! Then you go to the next
byte and do the same until the whole picture is created. Remember this function
draw the image in the graph mem not on the display, to show it you have to call DISP_GRAPH.
This is how I do it. Comments below.
PutImage: ; Puts the image stored at (HL). { 28 bytes }
ld e,(hl)
inc hl
ld d,(hl)
inc hl
ld b,(hl)
inc hl
ld c,(hl)
inc hl
PI_NewRow:
push bc
push de
PI_NewCol:
ld a,(hl)
inc hl
ld (de),a
inc de
djnz PI_NewCol
pop de
ld bc,12
ex de,hl
add hl,bc
ex de,hl
pop bc
dec c
jr nz,PI_NewRow
ret
And here is how the image is stored in the end of the program:
Image:
.dw GRAPH_MEM+12
.db 2,16
.db %00000111,%11100000
.db %00011000,%00011000
.db %00100000,%00000100
.db %01000000,%00000010
.db %01000000,%00000010
.db %10000000,%00000001
.db %10000001,%10000001
.db %10000010,%01000001
.db %10000010,%01000001
.db %10000001,%10000001
.db %10000000,%00000001
.db %01000000,%00000010
.db %01000000,%00000010
.db %00100000,%00000100
.db %00011000,%00011000
.db %00000111,%11100000
Let us first look at how the image is stored. First we have defined a word (.dw) with
the address. Since these kind of pictures won't move we can calculate the address in
the head so we don't have to waste unnecessary bytes in the program to do that. If you
want it to move, you could change the routine in the beginning by using a FIND_PIXEL.
On the second row, I've defined first the x width in bytes, that is, how many bytes
wide it is, not how many bits!. The second byte is the y width, which is the number
of rows. After that the image is stored. I've used % (that means that the value is in
binary) so you can see how the sprite will look like easier.
Now let us look at the PutImage routine. The first 8 rows loads DE with the address
(note that E is first read, then D. The reason for that is that the LSB is stored first when
you define a word, or store a word for that matter), and then loads B with the x size
in bytes and C with the y size. Then we push BC because we will return here
again, and then we want B to be the same and we push DE so we can pop it back
at the end of routine and add 12 (12 = exactly one row down) to it later.
After the label NewCol we just load A with the first byte of the image (HL is
after four increases in the beginning now pointing at the image itself) and then we
store it at (DE), which is the address in the graph memory. See, we changes 8 pixels
at once, without caring about the bits! They'll just fall into place! We also increase
HL so it points to the next byte in the image and DE so it points to the next byte
in the graph memory. Then we repeat NewCol B times (B was the x size, which
is the number of times we want it to be repeated).
When one row has been put on the screen, we pop DE to get the address
where it was on the beginning of the line, because then we can easily get to the
next row by adding 12 to DE. If we don't push and pop DE in that way, we have
to add DE with 12-xsize, and to calculate that takes a few more bytes. But
how do we add to DE? Only HL is allowed in 16 bit addition. It's time for a new,
very useful instruction, when you deal with 16 bit registers, EX DE,HL. It's
really simple, it swaps the contents of DE and HL. This instruction is special,
because it doesn't work on any other common registers (it works on some
other register, more about that in a later lesson).
Anyway, we just change DE with HL, then add 12 to HL, and then change
back. A easy way to do it. Note that we have to load BC with 12, because
you can't add with an immediate value. Luckily, the contents of BC doesn't
matter for the moment, so we don't have to push it. As you see, we pop it
back the next instruction, and then BC will contain the x size and y size once again.
Now we decrease the y size, and if it's not zero, we'll jump back and start with
a new row. This time when we push BC, the y size will be one less, which is
exaclty what we wanted.
Puh... that was a long explanation of how to put an image... Now we're getting
to how to put non aligned sprites (that is, we put sprites, images, between two
graph memory bytes).
SPRITES
I did not have the time to finish this section, but i decided to release this lesson anyway so
you could get started with graphics programming. When i get the time to write this part you should
be able to find two routines which display non aligned sprites using the graph mem. A section which includes
information on other graphics routines might also be added. Any news on this will be announced on the
main page.