As you can see, the program is a simple sketch program, almost like an
"Etch-a-Sketch", except we can't go diagonal using both controls at the
same time. To move the pen, use the arrow keys. To clear the screen, hit
the Clear key, ESC to exit the program.
Make sure to compile the right version for the right calculator. If you
run the TI-89 version on the TI-92+/V200, the arrow keys will be wrong,
and you won't be able to go to the edges of the screen. If you use the
TI-92+/V200 version on the TI-89, you will be able to go off screen, and
the arrow keys will be wrong.
A lot of the code should be very familiar, so I'm going to skip a lot of
it. I want just to highlight some important details so we know what's
going on. Towards the top of asm_main, we see this:
moveq #WIDTH/2,%d3 | (x,y) = (xCenter,yCenter)
moveq #HEIGHT/2,%d4
movea.l AMS_jumptable,%a5 | load AMS jumptable into a5
movea.l #LCD_MEM,%a3 | load LCD address into a3
The opcodes should be familiar to you, but I want to touch upon a couple
details. First, we are going to use the d3 and d4 registers as the
current (x,y) position. We initialize these to be the center of the
screen. The constants WIDTH and HEIGHT are defined in the .equ
directives at the top of the file.
Ignoring the jump table move, we move LCD_MEM into a3. LCD_MEM is a
constant defined in os.h, our header file. The important thing to note
here is that we are moving the immediate value #LCD_MEM, and not
LCD_MEM. This difference is important, because LCD_MEM is the address at
which the screen memory is stored. We want to use the a3 register as a
pointer to this address. Compare this to the AMS jump table. We do not
want the value 0xC8 in our register. We want the value (address) 0xC8 is
pointing at, i.e. the true location of the jump table. If we had moved
LCD_MEM into a3, the calc would crash because we would be writing to
some unknown place.
Let's skip down to the sketch_loop label.
sketch_loop:
movea.l 4*ngetchx(%a5),%a0 | %d0 = ngetchx();
jsr (%a0)
cmpi.w #KEY_ESC,%d0 | if ESC, goto end_loop
beq end_loop
cmpi.w #KEY_CLEAR,%d0 | if not CLEAR, goto check_left
bne check_left
movea.l 4*ClrScr(%a5),%a0 | ClrScr();
jsr (%a0)
bra sketch_loop | goto sketch_loop
This is pretty straightforward stuff. I'll quickly go over what we're
doing here. We call ngetchx to wait for a key press. If the key is ESC,
we goto the end_loop label. If the key is CLEAR, we clear the screen and
return to sketch_loop. Otherwise, we goto check_left.
check_left:
cmpi.w #KEY_LEFT,%d0 | if not LEFT, goto check_right
bne check_right
cmpi.w #2,%d3 | if we can't move left
bcs sketch_loop | goto sketch_loop
subq.w #2,%d3 | move the x-position
Next we check for the left arrow. If we don't get it, we move on to the
check_right label.
The next code introduces a new opcode, the bcs (Branch if Carry Set)
instruction. Remember that compare subtracts the destination from the
source and sets the appropriate flags of the status register. One of
these flags, the carry flag, tells us whether a borrow occurred during
our math. So, if d3 - 2 results in a borrow, we goto the sketch_loop.
The only way we would get a borrow is if our current x position
(remember this is stored in the d3 register) was less than 2. Our
program is going to move horizontally 2 pixels at a time, so we can't
move left if we are any closer to the edge than 2 pixels.
If we didn't branch out, then we adjust the the x position left by 2. We
use the subq (SUBtract Quick) opcode for this. It's just like addq, but
for subtraction.
draw_pixel:
clr.l %d0
clr.l %d1
move.w %d4,%d0 | d0 = row (y) = ((240 / 8) * d4)
mulu #240/8,%d0
move.w %d3,%d1 | d1 = column (x) = (d3 / 8)
divu #8,%d1
move.l %d1,%d2 | d2 = (d3 % 8)
clr.w %d2
swap %d2
add.w %d1,%d0 | d0 = row (y) + column (x) = byte
moveq #7,%d1 | d1 = 7 - (d3 % 8) = bit
sub.w %d2,%d1
bset.b %d1,(%d0,%a3) | LCD_MEM[d0] |= (1 << d1)
subq.b #1,%d1 | --d1;
bset.b %d1,(%d0,%a3) | LCD_MEM[d0] |= (1 << d1)
bra sketch_loop
This next section, the draw_pixel label looks very complex, and it takes
some time to understand it, but this is the heart of our program. Let's
go over first what we want to accomplish.
Every time the cursor is moved, we need to draw two pixels. We draw one
at our current (x,y) position, and one at (x-1,y). We used two pixels
rather than one so the pen is a little larger. It would work the same
way with a 1 pixel pen. So, now that we know we are going to draw two
pixels everytime the cursor is moved, let's talk about what that means.
The LCD uses memory mapped I/O. This means writes to the LCD memory will
produce changes on the screen directly. We have the screen represented
as an area of memory 240 pixels by 128 pixels (the size of the
TI-92+/V200 screen). This gives us a screen area of 30,720 pixels. Since
the LCD is monochrome (one color), we can represent each of the pixels
in a single bit of memory. 30,720 bits is 3,840 bytes, the size of the
LCD memory. The LCD memory is mapped starting at address 0x4C00. There
are 30 bytes per screen row, with every pixel in the row mapped
sequentially starting from the top-left pixel on the screen to the
bottom-right.
To find a particular pixel in the LCD memory using an (x,y) position, we
start by finding the row. The start of the row is at 30 bytes (240
pixels / 8 bits per byte) * the y coordinate. Once we have the row, we
can look for the byte in the row that contains our pixel. To find this,
we divide our x coordinate by 8. This will give us the byte within the
row that our pixel is in. Finally, the remainder of that division is the
bit within the byte. However, there is a catch here, because bits are
generally numbered right-to-left, i.e. the right-most bit is bit 0, so
we need to subtract this bit number from 7 (the left-most bit) to get
the correct bit number. Pixel 7 in the byte is actually at bit 0, while
Pixel 0 is actually at bit 7.
Don't feel bad if you don't understand all that right now. I know it's a
little confusing. It took me quite awhile to get it down, and I still
had to think about it for awhile when I wrote the example program.
Hopefully with the english description and the code, we will come to an
understanding. So let's start going over the code.
We start by clearing the d0 and d1 registers. We will be using these for
our arithmetic. Next we move the y coordinate (d4) into d0 and multiply
it by 30. To do this, we introduce the mulu (MULtiply Unsigned)
instruction. mulu multiplies two unsigned 16-bit values and gives us an
unsigned 32-bit result. Unsigned means we are using positive values
only. This will give us our row.
Next we move the x coordinate (d3) into the d1 register and divide it
by 8. To do this, we use the divu (DIVide Unsigned) instruction. divu
divides a 32-bit unsigned quantity into a 16-bit quotient and a 16-bit
remainder. This is the byte within our row, or if you like, our column.
The remainder of divu is stored in the upper 16-bits of the destination
register. To get this remainder, we will copy the result of our division
into d2, clear the quotient, and swap the quotient with the remainder.
We use the swap instruction to swap the lower 16-bits with the upper
16-bits. This puts our remainder into the lower word of the d2 register.
Next we add the column to our row to get the LCD memory byte we want to
access. We take 7 - our remainder to get the correct bit number, rather
than the pixel number. Now we have the byte we want to change, and the
bit number within that byte. I just want to point out the add and sub
opcodes because they are new. I'm sure you can guess what they do. We
have seen variations of these like addq and subq for quick immediate
values. The regular add and sub are for more general operands like two
registers.
The only thing left to do now is to set the bits in the LCD memory.
Remembering that we have the LCD_MEM address stored in a3, all we need
to do is use the bset (Bit SET) instruction to set these bits. Our bit
number is in d1, while our byte is in d0. So, set the bit specified by
the d1 register of the byte at the address pointed to by a3 + d0. We
subtract 1 from the bit number and set the bit right next to it as well.
Give that a little time to sink in. I know it's a little complicated.
It helps if you do the math. Take some random coordinate and see that
you will end up with the right pixel by doing all these things. Let's
try it real quick. We'll find the bit for the pixel at (143,87). The
row = (240 / 8) * 87 = 2610. This is the starting byte of that row. To
get the column byte, oolumn = 143 / 8 = quotient 17 remainder 7.
Add our row and column byte together to get the actual byte in the LCD
memory, which is 2627. So, it's the 7th pixel in byte 2627. The 7th
pixel is bit 0 (the right-most pixel), so the bit = 7 - 7 = 0;
I want to skip down slighly into the check_right label to cover one of
the new opcodes we used here.
cmpi.w #WIDTH-2,%d3 | if we can't move right
bcc sketch_loop | goto sketch_loop
Here we see the bcc (Branch if Carry Clear) instruction. It is the
opposite of bcs. In this case, subtracting the x position by the right
edge of the calculator - 2 will almost always result in a borrow. The
only time we don't get a borrow is if the cursor is already at the
right edge. So, if our carry is clear (i.e. we are at the right edge),
then we can't move right and we go back to sketch_loop.
I'm going to leave the rest of the program as the rest of the code is
just like the check_left label, but for the other directions. The
differences should be pretty easy to understand.