 Site Navigation [ News | Our Software | Calculators | Programming | Assembly | Downloads | Links | Cool Graphs | Feedback ]
 Main    Site News Our Software Legal Information Credits Calculators    Information C Programming Assembly Introduction Keyboard Input Basic Graphics Part I Part II C & Assembly Downloads Miscellaneous    Links Cool Graphs Feedback Form ## TIGCC Assembly Programming Lessons

### Lesson 3: Basic Graphic Techniques

Step 1 - Intro to Graphics Hardware

Before we can start programming graphics on the calculators, we should look at how the graphics hardware works on the calculator.

The calculator has a basic LCD screen which on the TI-92+/V200 is 240 pixels wide by 128 pixels long (x,y) ranging from (0,0) to (239,127) on a Cartesian coordinate scale. The TI-89 is 160 pixels wide by 100 pixels long ranging (0,0) to (159,99).

However, to make it easy to program on both environments, the hardware works exactly the same on both calculators. There is a large area of memory starting at 0x4C00 where the LCD memory is stored. Since the LCD is black and white (there is no 'real' grayscale on the calculator, but we can simulate it using tricks we'll learn later), the memory stores a 1 for each pixel that should be black, and a 0 for each pixel that should have be white (or, in reality, no color).

There are two basic methods for drawing on the LCD screen, directly manipulating the memory pixel by pixel, and using sprites or tiles to address logical blocks of the screen. There are various ways to use these two techniques to get the results you want, but this is where we will start.

Step 2 - Method 1: Directly Manipulating LCD Memory

You can download the source and program files for this lesson from our archives.

Start TIGCC with a new project. Name it sketch. Create a new C Source file and a GNU assembly file. Name the C file main and the assembly file sketch. The code for main.c is in lesson 1 as always. Modify the assembly file to look like this:

sketch.s

```    .include "os.h"

.equ AMS_jumptable,0xC8
.equ KEY_ESC,264
.equ KEY_CLEAR,263

.equ USE_TI89,1

.ifne USE_TI89
.equ KEY_LEFT,338
.equ KEY_RIGHT,344
.equ KEY_UP,337
.equ KEY_DOWN,340
.equ WIDTH,160
.equ HEIGHT,100
.else
.equ KEY_LEFT,337
.equ KEY_RIGHT,340
.equ KEY_DOWN,344
.equ KEY_UP,338
.equ WIDTH,240
.equ HEIGHT,128
.endif

.text
.xdef asm_main

|-------------------------------------------------------------------------------
| asm_main - our program starts here
|

asm_main:
movem.l %d3-%d4/%a3-%a5,-(%sp)  | save registers

moveq   #WIDTH/2,%d3            | (x,y) = (xCenter,yCenter)
moveq   #HEIGHT/2,%d4

movea.l AMS_jumptable,%a5       | load AMS jumptable into a5

movea.l 4*ClrScr(%a5),%a0       | ClrScr();
jsr     (%a0)

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

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

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

check_right:
cmpi.w  #KEY_RIGHT,%d0          | if not right, goto check_up
bne     check_up

cmpi.w  #WIDTH-2,%d3            | if we can't move right
bcc     sketch_loop             | goto sketch_loop

addq.w  #2,%d3                  | move the x-position
bra     draw_pixel              | goto draw_pixel

check_up:
cmpi.w  #KEY_UP,%d0             | if not up, goto check_down
bne     check_down

cmpi.w  #1,%d4                  | if we can't move up, goto sketch_loop
bcs     sketch_loop

subq.w  #1,%d4                  | move the y-position
bra     draw_pixel

check_down:
cmpi.w  #KEY_DOWN,%d0           | if not down, goto sketch_loop
bne     sketch_loop

cmpi.w  #HEIGHT-1,%d4           | if we can't move down, goto sketch_loop
bcc     sketch_loop

addq.w  #1,%d4                  | move the y-position
bra     draw_pixel

end_loop:
movem.l (%sp)+,%d3-%d4/%a3-%a5  | restore registers
rts```

Because of the arrow keycodes being different on the two calculators, we will need separate versions again. The default is setup for the TI-89, but just change the USE_TI89 constant to 0 if you are using the TI-92+/V200.

Build the project and send it to TiEmu. It will look something like this:  Step 2a - Program Analysis

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

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.

Step 3 - Conclusions

It's nice to know how the LCD screen works, but this is a bit inefficient to actually use. I can't imagine too many people directly manipulating the screen like we've done here (though it was done in the past), unless you are just playing around, or trying to tweak your graphics (maybe an on-calc Sprite editor?) Other than that, we need a better way of addressing the LCD.

So, let's take a look at sprites, and see how they play a role in graphics.