• 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)