The first thing the function does is multiplies the quadrant by 4 then adding the high byte of quadrant 0 to this. This is the equivalent of base_address = 1024 + quadrant_base. Next we want to multiply Y by 32 and add it to this address. I take advantage of the fact that the result of this multiplication would result in the upper 5 bits of Y being added to the high order byte so a tiny bit of bit manipulation saves a lot of looping logic. Finally, as we know the lower 5 bits of the low-order byte are clear, we can simply OR the X value with the PPU address. This may seem complicated, but looking at the code should help.
; Calculate PPU Address for screen quadrant
; Add high portion of 32*y to high PPU address
; Calculate low byte of y * 32
; AND #$07 not necessary as 0 is always shifted in
; add X to it
LDA $2002 ; read PPU status to reset the high/low latch
STA $2006 ; write the high byte of screen address
STA $2006 ; write the low byte of screen address
The PrintString function uses the SOURCE_PTR to hold the string to print, which it prints at the current PPU address. This means that SetScreenXY should be called before printing. Concurrent calls to PrintString will continue printing where the last call left off. The cursor position is not tracked, though if you needed this functionality it would be fairly easy to add. This is not something I will need so the printing functions are being kept to their simplest form. This function is a loop that prints characters until either 256 characters are printed or a zero character is reached. The length-checking code is free as a JMP would be needed if the BNE wasn't used.
The problem with these functions is that there a bunch of code required to set up the parameters. Wouldn't it be great if we could call a function like this: "CallPrintStringAt labelOfString, x,y,screenQuardrant"? This is what macros are for. Different assemblers will have different ways of setting up macros. This is the way it is done in NESAsm:
The \# sections are the parameters. The code is inserted as shown with the parameters being pasted as they are into the macro. For complicated functions that require setting up multiple registers and variables, macros can save a lot of time. They can also make code easier to read. It is important to remember that the code for the macro is inserted into the program every time the macro is used so it is not a substitute for functions. But they certainly make calling functions easier. The downside to macros is that the code is just pasted as is so some optimizations that could be done (such as if you know a register or memory address is already correct) are not.
Printing works great except for one problem. Color. As was explained in earlier posts, setting attributes is kind of complicated and colors are set for 2x2 blocks. The ClearScreen function sets the attributes. We will not cover the ClearScreen function as we have written similar code for HelloNES and HelloSprites already. Next we will be looking at string copying and comparisons which will include a bit of a rant.