Commodore Free Magazine, Issue 82 - Part 14
From
Stephen Walsh@39:901/280 to
All on Fri Aug 15 17:15:54 2014
der in which we push our registers to the
more standard (and natural) ordering; A, then X, and then Y.
Sorry, I'm just funny about things like proper ordering of stack pushes, primarily because it can foul you up on the other end of this function when
it is time to pull and restore these registers. Kernal interrupts save registers in A/X/Y order, as do most competent 65x assembly language programmers. It doesn't really matter to the computer which order you push
the registers, but we should always strive for consistency in assembly language.
ADJUST PC TO POINT TO OUR TEXT STRING
Next, we have to calculate where the string is in memory, which (we know)
is directly after JSR PRIMM. How do we do this? Well, we can get the
address by looking on the stack. When you perform a JSR the processor
needs to save a return address so that it will know where to continue
executing instructions after the function terminates. The 65x family of processors place the address of the last byte of the JSR $XXXX instruction, which in this case is one byte before our string. Later, when the
processor encounters the RTS instruction it will automatically increment
(add one) to the address it retrieves from the stack, thereby pointing to
the next instruction in the code stream. The C-128 method is to increment
.X four times, store the address in a zero page pointer, and then increment
the address pointer. The four INX instructions are necessary to skip past
the three register pushes (A, X, and Y) and to also account for the fact
that the 6502 SP always points to the next lower address on the stack a
future pushed byte will be placed. The last byte that was placed on the
stack is accessed as SP+1, so the C-128 method must include that extra
(4th) register increment.
So now we have the location of the return address, minus 1. The C-128
method then places this address into the zero page pointer at $BC-$BD, then does a double-byte increment of the address to make it point to the first
byte of our string. We don't need all that, however. We will just start
our Y-index at 1, which accounts for the return address (on the stack) initially pointing to one byte before our string.
Our improvement (as seen below) is to eliminate the four INX instructions
and simply hard-code the absolute indexed instruction directly:
$0104,x/$0105,x as opposed to $0100,x/INX/$0100,x. You can see the
significant memory (and time) savings in the listing. Each INX, by the
way, requires two cycles of your processor's time, so let's save cycles
where we can. Incidentally, I often see guys jump through hoops trying to
cut a cycle or a byte in places where it's not really necessary. Don't let cycle counting get in the way of a good program. It's often not worth the effort and can become all-consuming and hard to debug, although there are
times you may have to in order to make something work right. Good examples where "hoop-jumping" might be necessary are in serial bus timing and of
course, chasing the raster in a complicated demo.
One other thing I almost always do is use labels instead of hard-coding computer addresses such as $0104, $BC, etc. Why? Because, for example,
you may later find that the addresses you choose in zero page ($BC-$BD
here) are in conflict with the Kernal or BASIC and you have to change
locations for the pointer, which may now be used in many places scattered throughout your program. Using labels means you have to make the change in exactly and only one place - the label at the top of your source. This is
a fundamental principle in symbolic assembly language. Using labels
liberally is much more elegant and requires much less work in the long run,
and is certainly much more descriptive. Remember this tip - it will save
you countless debugging (and search/replace) sessions later.
C-128 Version
TSX ;get the stack pointer
INX ;increment the index
INX ;four times to account
INX ;for pushed A/X/Y
INX ;and 6502 stack return
;address peculiarity
LDA $0100,X ;use abs index addressing
STA $BC ;to retrieve the return
;address-1 (lo)
INX ;increment index
LDA $0100,X ;retrieve return
;address-1 (hi)
STA $BD ;store into zp ptr (hi)
INC $BC ;increment ptr at $BC
BNE PRIM1 ;branch ahead unless lo
;byte wraps to $00
INC $BD ;increment hi byte ($BD)
;if needed
Our Version
RETADDR = $0104 ;label referring
;to stack address
PTRSTR = $BC ;label referring
;to zp address
CHROUT = $FFD2 ;well-known Kernal
;PRINT function
PLOT = $FFF0 ;Kernal PLOT
;function
TSX ;get stack pointer
LDA RETADDR,X ;aka LDA $0104,X
;(return addr-1 lo)
STA PTRSTR ;store zp ptr (lo)
LDA RETADDR+1,X ;aka LDA $0105,X
;(return addr-1 hi)
STA PTRSTR+1 ;store zp ptr (hi)
Now, with our setup out of the way, it's time to print the string. We are going to use the Kernal function CHROUT located at $FFD2. CHROUT prints
the petscii character in the A Register, for those few that are not
familiar. We are going to use Indirect Indexed, Y addressing for this, and
the Y Register is going to do double-duty as a length counter for our
string. We will need the length in order to correct the program counter
(PC) when it comes time to return to the main program. We will not modify
this section of code as it is fine as is. The posted assembly listing is
what might be considered a "generic" listing, so in place of local (cheap) labels (look up usage in your assembler's manual) I have used "=*" to
denote label locations. This will work in nearly all 6502 assemblers.
C-128 Version
PRIM1 =*
LDY #$01 ;string index / length
PRIM2 =* ;top of the loop
LDA ($BC),Y ;get char from string
BEQ PRIM3 ;exit when terminating
;0-byte is reached
JSR $FFD2 ;print the char in .A
INY ;increment index/length
BNE PRIM2 ;branch back to top of
;loop (256 chars max!)
Our Version
PRIM1 =* ;label not needed
;for our version
LDY #$01 ;string index/string
;length counter
PRIM2 =* ;top of the loop
LDA (PTRSTR),Y ;get char
BEQ PRIM3 ;exit when 0-byte reached
JSR CHROUT ;print char in .A
INY ;increment index/length
BNE PRIM2 ;branch to loop top
;(256 chars max!)
We have now displayed our text screen on the screen at the current cursor location.
MODIFICATION TO INCLUDE SCREEN
COORDINATES
Earlier I mentioned embedding screen location coordinates into the string.
This is a very handy (and convenient) way of putting a string right where
you want it without any additional code in the main program and is commonly known as "PRINT AT" or something similar. I call mine "PlotString" and you
may have your own name for yours. So, how is this done?
The idea is two make the first two bytes determine the position your string
is displayed on-screen. We add the coordinates to the beginning of the
string as follows:
;replace X (0-39),Y (0-24) with
;the actual coordinates you need
.byte X,Y
;petscii string with a terminating 0
.text "Hello World.", 0
That's it as far as the string is concerned, but we have to add a wee bit
of code to our PRIMM function. The listing below shows the process. The
odd thing about the Kernal function PLOT ($FFF0) is that the X/Y
coordinates are transposed, so we must get them from the string in reverse order to preserve the Y Index Register.
PRIM1 =* ;our version doesn't
;need this label, it's
;here as a placeholder
LDY #$02 ;string index/string
;length counter
LDA (PTRSTR),Y ;get the Y-coordinate
;from position 1
TAX ;PLOT requires
;Y-coordinate in .X
DEY ;now .Y = 1
LDA (PTRSTR),Y ;get the Y-coordinate
;from position 0
TAY ;PLOT requires
;X-coordinate in .Y
CLC ;PLOT requires carry=0
--- MBSE BBS v1.0.01 (GNU/Linux-i386)
* Origin: Dragon's Lair ---:- bbs.vk3heg.net -:--- (39:901/280)