Skip to content

Latest commit

 

History

History
446 lines (328 loc) · 14.7 KB

File metadata and controls

446 lines (328 loc) · 14.7 KB

Functional guidelines

Subroutines

Passing parameters to subroutines

In most cases a subroutine requires one or more input parameters. Number of parameters is basically unlimited. The size of each parameters can also vary (however usually it is 1 or 2 bytes). There are three methods of passing such parameters to the subroutine:

  • Using CPU registers.

  • Using fixed memory locations.

  • Using call stack.

The "CPU regs" method is the simplest and the most efficient, however, due to limited number and size of MOS 6502 registers (A, X, Y, each 1 byte in size) its application is very limited.

The "Fixed memory locations" method does not have this size limitation and also, when using zero page, can be quite performant. This method can be rather tricky in application, because memory locations must be carefully choosen not to clash between different, possibly cooperating subs. The most pragmatic approach to this method is to use [Macro-hosted subroutines], where these fixed memory locations can be configured via macro attributes.

The "call stack" method uses CPU stack located at page 1 to pass parameters. This way it is possible to pass more data than via CPU regs method and does not require fixed memory locations to be used. The stack size is of course very limited (256 bytes max, but each jsr consumes at least 3 bytes).

It is possible and quite often useful to combine "CPU regs" and "Call stack" method: some arguments are passed via registers and some via stack. We’ll call this method a hybrid invocation.

Support for call stack parameters passing

The common library contains helper macros for parameter passing via the stack. Recursive calls to the subroutines are not supported.

Related sources
  • c64lib/common/lib/invoke.asm.

  • c64lib/common/lin/invoke-global.asm.

Parameters should be pushed onto the stack just before calling subroutine (which is done via jsr instruction). Within the subroutine, parameters must be fetched from the stack in reverse order and stored elsewhere. Before this is done, a return pointer must be preserved in order to enable return from the subroutine (a rts instruction).

There are four macros for pushing parameters to the subroutine:

pushParamB

Pushes single byte as a parameter. The byte is provided via macro parameter.

pushParamW

Pushes two bytes as a parameter. The word is provided via macro parameter.

pushParamBInd

Pushes single byte as a parameter. The address of this byte is provided via macro parameter.

pushParamWInd

Pushes two bytes as a parameter. The starting address of this two-byte value is provided via macro parameter.

All these macros are destroying A register, that should be considered when using hybrid invocation (first push params via macros, last set the A parameter value).

Example 1. copyLargeMemForward invocation.

The copyLargeMemForward subroutine copies up to 64K of data from one memory location to another memory location. These memory locations can overlap as long as we copy from lower address to higher address. This subroutine uses call stack invocation method and requires 6 bytes of input parameters:

  • Source data address.

  • Destination address.

  • Data size.

Let’s assume we want to copy 1024 bytes from $3000 to $4000. Example of the subroutine invocation is following:

pushParamW($3000)
pushParamW($4000)
pushParamW(1024)
jsr copyLargeMemForward

The subroutine, once being called, must do the following:

  1. Pull return address from the stack and store it in temporary place.

  2. Pull all parameters from the stack in reverse order and store them in some internal placeholders.

  3. Push return address back to the stack.

Preserving and restoring of return address may be done via invokeStackBegin and invokeStackEnd macros. Each of these macros takes single argument that denotes temporary address of 2-byte large placeholder where return address can be preserved.

The invokeStackBegin should be called before any parameter is pulled from the stack. The invokeStackEnd should be called before a rts instruction is called.

There are three macros for pulling parameters from the stack (remember to call them in reverse order, this is how the stack works):

pullParamB

Pulls a single byte from the stack and stores it under address given as a macro parameter.

pullParamW

Pulls two bytes from the stack and stores them under address given as a macro parameter.

pullParamWList

Pulls two bytes from the stack and stores them under multiple addresses provided as an input parameter list.

Example 2. rotateMemRight subroutine implementation.

The rotateMemRight subroutine rotates up to 256 bytes of the memory to the right. It uses hybrid invocation: a memory address (2 bytes) should be passed via stack, and size of the memory window should be set into X register.

It uses self modifying code technique and copies input address directly into four address location in its own code.

  rotateMemRight: {

  invokeStackBegin(returnPtr)
  pullParamWList(List().add(loadFirst, loadNext, staNext, staLast))

  lda loadFirst:$ffff, x
  sta preserve
  loop:
    dex
    lda loadNext:$ffff, x
    inx
    sta staNext:$ffff, x
    dex
  bne loop
  lda preserve
  sta staLast:$ffff

  invokeStackEnd(returnPtr)
  rts
  // local vars
  returnPtr:      .word 0
  preserve:       .byte 0
  }

It is noteworthy, that semi-local variables are declared as additional 3 bytes at the end of the subroutine (guarded by preceding rts instruction). These variables are used to preserve return address and additional single byte for rotation.

Memory operations

The common library helper macros helps with common memory related operations.

Related sources
  • c64lib/common/lib/mem.asm.

  • c64lib/common/lib/mem-global.asm.

Setting the memory

There are a bunch of set* pseudocommands that can be used to quickly set given memory cells to some value. The set8 works with 1-byte values, the set16 works with 2-byte values (words).

The following sets content of memory address $B000 to 0:

set8 #0 : $B000

The following sets content of memory address $C000 to a value stored under address $0F:

set8 $0F : $B000

It is also possible to set two consecutive bytes to a given 2-byte value, but one needs to use the following macro for that (sets $B000 to $02 and $B001 to $01):

set16($0102, $B000)

There is also a macro for setting one byte value which can be used as long as immediate addressing is needed:

set8($01, $B000)

In order to fill specified memory block with given value a fillMem subroutine can be used.

Example 3. Filling memory with a value.

In order to use this subroutine import it and place a label before it so it can be called later.

fillMem: #import "common/lib/sub/fill-mem.asm"

This subroutine uses hybrid invocation, push start address of the memory block into the stack and set value in A and block size in X. As you see, this subroutine is limited to 255 as maximum size of the block.

The following code can be used to fill 200 bytes of memory starting from $B000 address with value 0.

pushParamW($B000)
lda #0
ldx #200
jsr fillMem

Memory transfer

You can use copy8 pseudocommand to copy one byte from one place to another using A:

copy8 $B000:$C000

You can use copy16 pseudocommand to copy two consecuive bytes from one place to another:

copy16 $B000:$C000

For fast, unrolled copying of data block use copyFast macro. To copy 20 bytes from $B000 to $C000 use the following:

copyFast($B000, $C000, 20)

Remember, that copyFast macro will consume a lot of space if count parameter (the last one) will be big.

There is a "slow" copy subroutine that is handy for "unpacking" a PRG file and moving arbitrary sized blocks of data to the target location. This subroutine can be used to move SID data (music) into target location as well as to move VIC-II data (charsets, sprites) into the VIC-II addressable bank. Target and source spaces can overlap as long as target address is bigger than source address.

Example 4. Copying large blocks of data

In order to use this subroutine import it and place a label before, so that it can be called later.

copyLargeMemForward: #import "common/lib/sub/copy-large-mem-forward.asm"

This subroutine requires three WORD parameters to be placed on stack: * source address * target address * data size (can be more than 256)

pushParamW($A000)
pushParamW($E000)
pushParamW(1024)
jsr copyLargeMemForward

16-bit math

The common library helper macros helps with common math related operations. They "extend" a basic set of math opcodes used to do arithmetics with macros and pseudocommands that operates on two-byte sized values (words).

Related sources
  • c64lib/common/lib/math.asm.

  • c64lib/common/lib/math-global.asm.

Increments, decrements

You can easily increment and decrement 16-bit counters using following macros:

inc16($2000) // increments counter located in $2000,$2001
dec16($3000) // decrements counter located in $3000,$3001

Adding, subtraction

You can add / subtract value to / from a value stored in memory via add16 and sub16 macros.

In example to add 315 to the value stored under $B000:

add16(315, $B000)

Alternatively you can use add16 and sub16 pseudocommands:

add16 $B000 : $C000

You can add / subtract value stored in one memory address to / from a value stored in another memory address via addMem16 and subMem16, respectively.

In example to add value from address $B000:$B001 to the value from address $C000:$C001:

addMem16($B000, $C000)

Far conditional jumps

Well known limitation of MOS 6502 machine code is that conditional jump instructions use 1 byte for relative jump shift. This means that if you want to jump too far, you have to use conditional / absolute jump combination, which is cumbersome.

Related sources
  • c64lib/common/lib/common.asm.

  • c64lib/common/lib/common-global.asm.

The c64lib offers few macros that can simplify this task:

fbne(label)

It calculates the jump length and if it is too far it replaces simple bne with beq/jmp combination.

fbmi(label)

It calculates the jump length and if it is too far it replaces simple bmi with bpl/beq`jmp` combination.

Handling of RLE compression

The c64lib supports very basic yet useful compression method called RLE. It is particularly useful for data consisting of repetitive patterns of bytes such as simple graphics, game maps and so on.

Related sources
  • c64lib/common/lib/compress.asm

  • c64lib/common/lib/compress-global.asm

One can call compressRLE(data) macro that crunches provided data during compilation of program. Such data must be loaded with LoadBinary(filename) function.

Compressed data can be decompressed (and relocated) with decompressRLE subroutine.

decompressRLE: #import "common/lib/sub/decompress_rle.asm"

This subroutine requires three WORD parameters to be placed on stack: * source address * target address

.var data = LoadBinary("data-to-be-compressed.bin")

sourceAddress: c64lib_compressRLE(data)

// ...

pushParamW(sourceAddress)
pushParamW(targetAddress)
jsr decompressRLE

// ...

targetAddress:

Commodore 64 memory layout management

The Commodore 64 is known to have extremely flexible memory layout configuration. The c64lib library provides simple macro to change memory configuration in single line of code.

Related sources
  • c64lib/chipset/lib/mos6510.asm.

  • c64lib/chipset/lib/mos6510-global.asm.

To change memory configuration one can run configureMemory(config) macro, where config can have one of the following values:

  • RAM_RAM_RAM

  • RAM_CHAR_RAM

  • RAM_CHAR_KERNAL

  • BASIC_CHAR_KERNAL

  • RAM_IO_RAM

  • RAM_IO_KERNAL

  • BASIC_IO_KERNAL

The BASIC_IO_KERNAL is an initial value after C64 is being powered on. Names of the labels above are self explanatory: each section represents one of switchable region: RAM or BASIC, RAM/IO/CHARSET and RAM or KERNAL.

Example 5. Switching to the most useful memory layout

You usually don’t need BASIC and KERNAL but still want IO region to be banked in.

You have to ensure that interrupts are disabled during configuration process. You would like to disable default interrupt sources (as KERNAL is banked out the interrupt vectors are too).

sei
c64lib_disableNMI()
c64lib_configureMemory(c64lib.RAM_IO_RAM)
c64lib_disableCIAInterrupts()
cli

VIC-II memory management

Configuring VIC-II memory bank

Configuration of which one out of four C64 memory banks is addressable by VIC-II chip can be done via reprogramming the CIA-2 chip.

Related sources
  • c64lib/chipset/lib/cia.asm.

  • c64lib/chipset/lib/cia-global.asm.

The change can be done via setVICBank`c64lib_setVICBank` macro.

The following code:

c64lib_setVICBank(c64lib.BANK3)

sets up the last memory bank ($C000-$FFFF) for VIC-II graphic chip.

VIC-II memory layout management

There are two distinct memory areas that have to be fconfigured for VIC-II. Their meaning is different depending on the screen mode. For text modes it would be screen memory and charset memory. For bitmap modes it would be screen memory and bitmap memory.

VIC-II NTSC detection

VIC-II IRQ handling

Text outputting

The text library offers macros and subroutines to output texts and numbers.

Related sources
  • c64lib/text/lib/text.asm.

  • c64lib/text/lib/text-global.asm.

  • c64lib/text/lib/sub/out-hex-nibble.asm.

  • c64lib/text/lib/sub/out-hex.asm.

The outTextXYC outputs text terminated with $FF that is no longer than 255 characters. As it is a macro-hosted subroutine, it must be instantiated with screen and color RAM addresses first. Such subroutine can be then used to output different texts at different screen position and with different color.

myOutTextXYC: c64lib_outTextXYC($1000, $D800)

text: .text "Commodore 64"; .byte $ff

c64lib_pushParamW(text)
ldx #5
ldy #12
lda #LIGHT_GREEN
jsr myOutTextXYC

The outNumberXYC outputs two digit number (a byte). As this is a macro-hosted subroutine, it must be instantiated with screen and color RAM addresses first. Such subroutine can be then used to output numbers at different screen position and with different color. This subroutine displays bytes in hexadecimal form unless number is BCD packed.

myOutNumberXYC: c64lib_outNumberXYC($1000, $D800)

number: .byte 3*16 + 6

c64lib_pushParamW(number)
ldx #5
ldy #12
lda #LIGHT_GREEN
jsr myOutNumberXYC // displays 36 at (5,12) using light green color

Text scrolling

coming soon

2x2 scrollable background

coming soon