Thursday, September 5, 2013

Getting the most out of CA65 - Part 1

I aim to complete a quality game for the NES using the CA65 macro assembler as one of my primary tools to do so. I seem to enjoy much of my time playing with ideas to extend the assembler itself via macro code. I find its macro language enjoyable to work with and have come up with a few things that may be worth sharing, even if just to show how to utilize the macro language for your own goals.

 Some of these ideas I don't really need at this time, but looking at what I may need from the perspective of a very large NES project, I have tried to implement things I may need in the future.

High Level code constructs

This is slowly evolving as I find things that don't quite work perfectly or something small gets added. See the documentation here: https://www.assembla.com/spaces/ca65hl/wiki
There is no code created by these macros. They work by creating labels, which CA65 can do utilizing the .sprint() and .ident() built-in functions. I may explain how they work at the macro level in more detail later, but as far as the IF-ENDIF structure, once a stack macro is implemented, tracking logical blocks and spitting out labels is not too difficult. The more difficult part was implementing the flexibility of what can be included as a 'boolean expression'.

Organizing code with libraries and header files

This is a simple but very nice way to organize chunks of code that can be reused later. This simple example is NES controller reading. You could code the routine once, and never have to worry about it again. (This example ignores the DPMC conflict.)

First, write the 'library' code. I've standardized on prefixing all filenames with 'lib_', so this is: lib_controller.inc
The .inc indicates that this is to be included somewhere in the project, it could be in a separate module. (Note, in this code func is a custom macro.)

Code:

.ifndef   _LIB_CONTROLLER_
_LIB_CONTROLLER_ = 1

.scope _LIB_CONTROLLER_ ; keep everything in its own scope

.pushseg
.segment "ZEROPAGE"

    pressed:          .res    2    ; new this frame - held down
    pressedLastFrame: .res    2    ; last frame / call
    pressedNew:       .res    2    ; newly pressed since last frame
    releasedNew:      .res    2    ; newly released since last frame

.popseg

exportfunc         readPort
exportfunc         readPort0
exportfunc         readPort1

.export pressed
.export pressedLastFrame
.export pressedNew
.export releasedNew


func readPort0
    ldx #0
    .byte $CD           ; compare absolute, skip ldx #1
endfunc

func readPort1
    ldx #1
endfunc

func readPort
; IN : player number in reg X = 0, or 1 => 1 or 2
; OUT: Y: undefined, A: PadsNewReleased, X: unchanged


    lda pressed,x         ; save last frame's joystick
    sta pressedLastFrame,x

    lda #1
    sta pressed,x         ; set bit0 to activate carry in do-while loop
    sta $4016
    lsr a                    ; a has 0
    sta $4016

    :
        lda $4016,x
        and #3               ; famicom
        cmp #1              ; friendly reads
        rol pressed,x
    bcc :-

    lda pressed,x
    eor pressedLastFrame,x
    tay
    and pressed,x
    sta pressedNew,x
    tya
    eor pressedNew,x
    sta releasedNew,x
    
    rts
    
endfunc

.endscope

.endif

If this has been included somewhere, now you can access this functionality from any module by including the header file: int_iController.h
; Interface for lib_controller.inc

.ifndef _INT_CONTROLLER_H_
_INT_CONTROLLER_H_ = 1

.scope iController
    
    importfunc readPort0
    importfunc readPort1
    importfunc readPort
    
    
    .importZP pressed
    .importZP pressedLastFrame  
    .importZP pressedNew 
    .importZP releasedNew
    
.endscope

.endif

Any module that includes the header file can access the controller reading functionality and the variables via the iController scope:

Somewhere in code:

; include the header:
.include "int_iController.h"
; elsewhere:
lda iController::pressednew
and #BUTTON_A
bne buttonApressed

This has the advantage of quickly reusing code and speeding up assembly of large projects, since the library code will never have to be assembled again if it is not changed.

Some minor improvements

Pass "-I headerFilefolder -I libraryFileFolder" to the command line of CA65 and keep all headers and libraries in a standard location.

I have standardized on a header file format of prefixing with int_ and created a macro to help make things a bit nicer.

Somewhere in code:

uses iController

This will attempt to include "int_iController.h" More to come next time.

2 comments:

  1. Hey, will you ever finish this tutorial? I'm trying to start coding on the NES and ca65 with you macros seems the best way to go :)

    ReplyDelete
  2. I didn't stick with the blog thing as much as I would have liked. I am working on some things still, but still some ways away from really releasing anything. See: http://ca65hlx.comeze.com
    Send me (Movax12) a message on forums.nesdev.com to contact me effectively.

    ReplyDelete