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.
SetScreenXY:
; Calculate PPU Address for screen quadrant
ASL A
ASL A
CLC
ADC #$20
STA PPU_HIGH
; Add high portion of 32*y to high PPU address
TYA
LSR A
LSR A
LSR A
CLC
ADC PPU_HIGH
STA PPU_HIGH
; Calculate low byte of y * 32
TYA
; AND #$07 not necessary as 0 is always shifted in
ASL A
ASL A
ASL A
ASL A
ASL A
; add X to it
STA PPU_LOW
TXA
ORA PPU_LOW
STA PPU_LOW
LDA $2002 ; read PPU status to reset the high/low latch
LDA PPU_HIGH
STA $2006 ; write the high byte of screen address
LDA PPU_LOW
STA $2006 ; write the low byte of screen address
RTS
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.
PrintString:
LDY #0
PrintString_loop:
LDA [SOURCE_PTR],Y
BEQ PrintString_done
STA $2007
INY
BEQ PrintString_done
JMP PrintString_loop
PrintString_done:
RTS
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:
CallPrintStringAt .macro
LDA #LOW(\1)
STA SOURCE_PTR
LDA #HIGH(\1)
STA SOURCE_PTR+1
LDX #\2
LDY #\3
LDA #\4
JSR SetScreenXY
JSR PrintString
.endm
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.
No comments:
Post a Comment