Open Club  ·  36 members

# 3E+ and macros are your friend

## Recommended Posts

Posted (edited)

A guide to using 3E+ bankswitching

3E+ is an Atari 2600 bank-switching format designed by Thomas Jentzsch, derived from the DASH
bank-switching scheme designed by Andrew Davie, which in turn was derived from the 3E bank-switching
scheme designed by Armin Vogl which was in turn derived from  the method used in Tigervision carts.

The scheme divides the '2600 address space into 4 "slots".
Each slot (numbered 0 to 3) lives in its own address space in the '2600 memory.

We shall use the address range \$F000 - \$FFFF to refer to the 4K area the '2600 accesses.
Each slot is 1K in "size"

0       F000 - F3FF
1       F400 - F7FF
2       F800 - FBFF
3       FC00 - FFFF

A slot can reference either a RAM "bank" or a ROM "bank".
A "bank" refers to a unique block of RAM or of ROM in the game's binary.

ROM banks are numbered from 0 to 63 (maximum)
RAM banks are numbered form 0 to 63 (maximum)

To "switch in" a ROM bank to a SLOT, write the encoded SLOT/BANK number to \$3F
The encoding of SLOT/BANK numbers is as follows...

D7-D6       the slot number (0 - 3)
D5-D0       the bank number (0 - 63)

Thus, if you write \$C7 to \$3F, then

C7 in binary is %1100111
D7-D6 = %11 = 3 = SLOT 3
D5-D0 = %00111 = 7 = BANK 7

So, this would switch the 8th 1K bank (counting from 0) from the game binary into SLOT 3 (which is

RAM banks are similar to ROM banks, except they are only 512 bytes long, and to switch you write to
\$3E instead of \$3F.  So, \$3E is the bank-switching address for RAM, and \$3F is for ROM. Note that
you can only have either ROM OR RAM (not both) switched in to any single SLOT. But you can have
combinations of ROM and RAM switched into the 4 slots, total.  SLOT 0 could be RAM and SLOTS 1, 2, 3
could be ROM... or any other combination you care to name. One type per SLOT.

All of this may seem a bit confusing, but fortunately there are a whole bunch of very simple
macros to get it all happening fairly automatically for you.

First,

SLOT n
This will let the assembler know that the following code is assembled for slot number "n" (where "n"
is from 0 to 3).  The RORG and other internal dasm stuff is handled automatically. All you need to
do if you change your mind which SLOT code should live in, is change the "n" to the new slot.

Now, if you have code that is running in SLOT 0 (for example), then it is somewhere in the address
range F000 - F3FF.  If you wanted to bank-switch some code or data you wanted to access, then you
would first "switch in" the desired bank and then do the access.

So, how do you "switch in" a desired bank into a desired slot?
Well we covered this a bit earlier...

```    lda #(SLOT*64) + BANK
sta \$3E                 ; switch in RAM```

But if we continued down this path, we'd have a LOT of numbers to remember, and to change if we
moved things from different banks to other banks and used different slots for something. We'd have
to go through all our code and fixup stuff. A nightmare.

So, instead of that, we're going to use more macros to do all the work for us.

The first macro to facilitate this is the "SLOT" macro mentioned above.

Let's introduce a new way of declaring functions.
Normally in dasm, you'd just type the name of the function in the first column (so, it's a "label")
and dasm will assign it an address when it assembles. That's fine, but we need more than an
address - we need a bank # as well.  So, the macro "DEF" (stands for define) does this for us.
Instead of typing the function name in the first column, we just tab out and type

`    DEF myFunction`

(where "myFunction" is the name of your function/subroutine/label).
Now the first thing this macro does is simply put the "myFunction" as a label as if you typed it in
the first column yourself. Totally identical, except now you have to type more... "DEF".  But
there is great value in doing this...

Firstly, DEF now also defines a new label you can use called "BANK_myFunction"
So if we want to switch in a bank containing the function, and then call the function, we can do
this...

```    lda #BANK_myFunction
sta \$3F
jsr myFunction

```

Well that's a bit nicer. We now don't have to worry where the function is.  But hang on, what about
which SLOT we have just switched in?  Well, that was set by an earlier use of the SLOT macro.

So, here's the basic setup so far...

```    SLOT 2          ; all following code lives in slot #2 (from F800-FBFF)

DEF myFunction
; code
rts

SLOT 0          ; all following code lives in slot 0 (from F000-F3FF)

lda #BANK_myFunction
sta \$3F                     ; switch ROM (and SLOT 2)
jsr myFunction```

OK, hopefully that makes sense. We define SLOT to indicate where following code actually lives
in the '2600 memory footprint, and we use DEF to automatically generate the BANK value we use
for switching banks. Good so far....

While we're at it, let's stop using \$3E and #3F -- too easy to confuse.  There are two equates
for these

```SET_BANK = \$3F
SET_BANK_RAM = \$3E```

So, we'll use those from now on...

```    lda #BANK_myFunction
sta SET_BANK                    ; switch ROM/SLOT
jsr myFunction```

Now, that's the sort of construct we'll probably see a lot. Switch in a bank, and then call a
function in that bank. But, consider the following...

```    SLOT 0
DEF myFunction
rts

... and in some other bank....

SLOT 0

lda #BANK_myFunction
sta SET_BANK
jsr myFunction```

This will fail. Horribly. It will fail because we're running code that lives in SLOT 0, and we
have just tried to switch in the bank cotaining myFunction into that slot. The slot we're
currently running code from. This is something we definitely want to avoid, and fortunately
this can be automated.

Two macros allow this.

`CALL function`

This macro will insert exactly the above bit of code (the bankswitch/jsr) but first it checks
the bank of the current code and the code being called to make sure they do not live in the
same SLOT. That is, make sure that we're not effectively "pulling the rug from under our own feet"
by switching out the bank we're running in.

So, the above code, rewritten...

```    SLOT 0
DEF myFunction
rts```

... and in some other bank....

```    SLOT 0
CALL myFunction```

This will now generate an assemble-time error - "attempt to call function in incompatible bank"
In other words, you're saved from a difficult-to-debug runtime error by an assemble-time check.
Instead of CALL, there is another similar macro to use for jmp -- JUMP -- which performs the
same check on the current and destination banks.

OK, that's pretty cool.  How do we define which BANK some code lives in.

Well the short answer is, we don't. The BANK something is in really depends on where it is in
the binary file itself.  At least the ROM bank behaves that way.  There's a macro to start a new
ROM bank, and that uses SLOT to also indicate which memory area the code in the bank should be
assembled for.  That is, if we have code that we switch into SLOT 1, then that code should
be assembled to live in that slot's address range (that is, \$F400-\$F7FF).  Rather than having
to do that ourselves, it's all automatically done for us with the ROMBANK macro.

General usage is thus...

```    SLOT 3
ROMBANK DEMO

DEF fn
rts

SLOT 2
ROMBANK B2

DEF main
CALL fn```

In the above example, we have defined two BANKS. The first has a function named "fn", which
is assembled for use in SLOT 3 (ie. FC00-FFFF range).  The second has a function named "main" which
is assembled for use in SLOT 2 (ie. F800-FBFF range). The "main" function CALLs "fn" by switching
the BANK that "fn" lives in (which in the above example would be 0) and then doing a jsr to the
function.  dasm will be happy because the CALL sees that "fn" and "main" are in different SLOTs.

OK, so for RAM banks, we use RAMBANK instead of ROMBANK.

Thus,

```    SLOT 0
RAMBANK BUFFER
DEF Buffer
ds 32

SLOT 1
ROMBANK DEMO
DEF main
lda #BANK_Buffer
sta SET_BANK_RAM
lda Buffer+16               ; load byte from RAM```

So, "main" switches in the RAM bank, and retrieves a byte from the buffer.

These macros also define a label correctly referencing the SLOT/BANK for the name of the bank.
These are, for the above...

```RAMBANK_BUFFER = 0
ROMBANK_DEMO = \$41```

Why \$41?  Well, because it's SLOT #1 and BANK #1. %01000001 (D7D6=1), (D5-D0=1)

We can use these values to switch banks/slots instead of the value for a routine in a DEF. Sometimes
this is the thing you need to do (particularly when you are copying ROM to RAM and then calling
the code in RAM - in that case you want to switch in the RAMBANK value, not the DEF value).

If we wanted to write a byte to the buffer, we'd need to add 512 for write-access.

```    lda #0
sta Buffer+16 + 512```

Now that's pretty awful to read. You guessed it, yes... there are macros for that.

In fact, for ALL non-zero-page access (which has the requirement of +512 on write), I use macros
which make it clear that (a) the variable is in non-zero-page RAM, and (b) take care of adding
that "512" automatically.

For ANY access to memory which uses non-zero-page RAM, I suffix the opcode with "@RAM". It seems
strange at first, but here's the usage... instead of the above example, we now write...

```    lda #0
[email protected] Buffer+16```

Likewise, if we are reading, it's

`    [email protected] Buffer+16`

Now the "[email protected]" macro doesn't actualy DO anything other than the straight load. But the code
is readable - we know that it's RAM access, but furthermore if you get into the habit of using
"@RAM" then the writes are guaranteed to work as required, and it's easy to remember.

There are a bunch of these "access" macros - for X, Y and A. So, [email protected]  etc...

OK, hopefully that all makes sense.
Now we move on to the wonder of the age - local variables!

There's a simple macro named "VAR" which can be used after any DEF to automatically allocate a
variable for use in the DEF function (or anywhere else, for that matter). The variables allocated
are global in scope (for good reasons), but generally should only be used locally.

Let's look at a simple definition and use of a local variable...  I prefix with an underscore to
make it easy to see we're dealing with a local...

```    DEF fn
VAR _ptr, 2         ; declare 2-byte zero-page local named "_ptr"

lda #>Buffer
sta _ptr+1
lda #<Buffer
sta _ptr

ldy #16
lda (_ptr),y            ; retrieve byte from Buffer```

OK, aside from the weird "VAR" thing, this is just like any other zero page variable. We've put

other routine "stomping" on the _ptr value. This is useless, right?

Well, as it is, it's pretty unusable. It needs a few "helper" functions to make it all fit
together.

But first, I'm glad you asked, _ptr is allocated from a "pool" of zero page variables, known as
the overlay area (or local variable area).  Functions are allocated memory in this "pool" for
their own use. This memory is GUARANTEED to be not-stomped on by functions that you may call
and you are GUARANTEED not to be stomping on local variables of functions that called YOU.

It has two helper-macros - REF and VEND

Let's have a look...

```    DEF fn2
VAR _Buffer, 16         ; delcare 16-byte buffer
VEND fn2

; fill _Buffer with something
rts

DEF fn
VAR _val, 1         ; declare 1-byte variable
VEND fn             ; end of variables for fn

lda #10
sta _val

jsr fn2

; _val is STILL 10 - GUARANTEED```

Now let's get our head around this. There's a function named "fn2" which declares a 16-byte buffer
which it fills with (say) random values. And that function is called by "fn" which declares its
own local variable "_val" into which it writes 10 before calling "fn2".  The comment says that
"_val" is still 10 - GUARANTEED.  How is this so, when both _Buffer and _val share the same
"local variable" block of memory.

Well, the trick is - you guessed it - another macro.

More specifically, we want to guarantee that _val does not overlap any other local variable in
any function that calls "fn" or any function that "fn" calls. Or any function that fn2 calls...
or any function that the function that fn2 calls... calls. Turtles all the way down.

Now, this is going to take some getting used to, but if you do this for ALL your functions, you
benefit from getting the assembler doing ALL the work for you and calculating addresses for the
local variables which are GUARANTEED not to overlap or clash. And once you realise the power of
using overlay ("local") variables, you'll suddenly start thinking about how FEW variables you
really need - and furthermore you can name them all with meaningful names. You could, for example,
have dozens/hundreds of zero page variables which all - because of their timing/usage - can share
a 10-byte zero page buffer.

So, the bit I suspect many will not like, but here goes... the REF macro (for reference).

Whenever you call a function from anywhere, you need to go to the DEF of that function and add
a REF to the calling function.  So I kind of lied - the above code will NOT generate unique

The above code would be rewritten like this...

```    DEF fn2
REF fn
VAR _Buffer, 16         ; delcare 16-byte buffer
VEND fn2

; fill _Buffer with something
rts

DEF fn
VAR _val, 1         ; declare 1-byte variable
VEND fn             ; end of variables for fn

lda #10
sta _val

jsr fn2

; _val is STILL 10 - GUARANTEED```

All that's changed here is we've added "REF fn" in the declaration block of the top function.
It's basically saying "hey, this function is called by the function named 'fn'".  That's useful
because it now gives the VAR macro enough information to determine where the true local variables
can start. But how does this all work, when we have references to references to references and
you can't know what the value might be because you haven't assembled other parts that will
affect the location, etc?  Well, fortunately, we let dasm worry about that.

Basically, dasm will do "another pass" when it sees a value change. So the macros for each function
set the end address of the variable block for that function (that's what the VEND macro does). The
REF macro will calculate the last used memory address in ALL of the REF statements for any DEF
function, and then set the declared variable's value, which in turn sets the functions VEND value.
If that VEND value is thus changed from any earlier value it has, then dasm will do another pass
and after several/many passes, ALL of the references/variables will be correct and consistent
and we have that GUARANTEE that our local variables/overlays will not stomp on each other.

But, as noted, it relies on you being meticulous about using those REFs.

Here's an actual example from Chess...

```    DEF GenerateAllMoves

REF ListPlayerMoves
REF aiComputerMove
REF quiesce
REF negaMax

VAR __vector, 2
VAR __pieceFilter, 1

VEND GenerateAllMoves

; code....

rts```

So, you see there are 4 references (other functions that call "GenerateAllMoves") and two local
variable declarations.  It's all self-organising, and now I can delcare local variables IN the
function that actually uses them. Very nice.

But, I'm glad you asked, what if you want to share variables between functions?  What if you
wanted to calculate something in a subroutine and "pass" the result back to the caller? Doesn't
this method stop me from using a local buffer/overlay to do that?  Because the whole setup is
designed to STOP the stomping/sharing.

For example, if I had something like this...

```    DEF fn
REF fn2
VAR _temp, 1
VAR _param, 3

rts

DEF fn2
VAR _stuff, 10              ; uses 10 bytes for something

jsr fn

; what's in _param????
; what's in _stuff???  has it been stomped?```

In this case, all is OK because fn knows that it is referred to by "fn2" and so the VAR declaration
will place _temp and _param AFTER _stuff's 10 bytes. No stomping. But it's unsafe because there
is no clear indication that "fn2" actually uses/requires the data in "_param" as a return value.

So, here's the "proper" way to share local variables"...

```    DEF params
VAR _param, 3
VEND params

DEF fn2
REF params              ; uses the above local variables!
REF fn2
VAR _temp1, 1
VEND fn2

; code,including writing to _param
; rts

DEF fn
REF params              ; ALSO uses the params local block!
VAR _stuff, 10
VEND fn

; code as before```

Hope that's clear. We've defined an independent local variable block - with no code - and then
in each function that shares that block we simply put a reference (REF). The auto-calculation code
will make sure (yes, GUARANTEE) that the variables don't clash.  In particular, if our overlay area
started at the beginning of zero page (\$80) then our variables would be allocated like this

_param = \$80    ; 3 bytes long
_stuff = \$83    ; 10 bytes long
_temp1 = \$8D    ; 1 byte long

We have no "stomping" and the variables are "intelligently" allocated. It's magic.

Putting it all together, here's the basic overlay of how it goes...

```    SLOT 1
ROMBANK One

DEF fn
VAR _temp, 1
VEND fn

lda #3
sta _temp

CALL fnX ; in slot 2

; "sees" _shared == 9
; "_temp" is still 3
rts

SLOT 2
ROMBANK Two

DEF fnX
REF fn
VAR _fxtemp, 1
VEND fnX

lda #9
sta _shared
rts

DEF sharedVar
VAR _shared,1
VEND sharedVar```

Some habits I've picked up...

I suffix all subroutine calls with the slot number as a comment, as it makes it easier to
double-check stuff. Now this is purely personal preference, but here's what it looks like...

```        lda #BANK_PIECE_VECTOR_BANK
sta SET_BANK;@2```

In other words, just by looking at that, I can see that it's loading into slot #2

So, that's how I've set myself up to program 3E+ games. I really really like this bankswitch scheme
and in particular these support macros. I can simply cut/paste a subroutine from one bank to another
and the assembler will not only work out all the references for me, but also tell me if that
results in an incompatible bank call somewhere.

I honestly don't expect anyone to take the time to review/learn this stuff - but now it's documented
so hopefully it will give someone else some ideas.

Here are my macros... there are a few missing init/equates, but if anyone tries to use this I'm sure
it will either be easy to work out or I'm happy to answer questions.

```; MACROS.asm

;---------------------------------------------------------------------------------------------------

MAC DEF ; {name of subroutine}

; Declare a subroutine
; Sets up a whole lot of helper stuff
;   slot and bank equates
;   local variable setup

SLOT_{1}        SET _BANK_SLOT
BANK_{1}        SET SLOT_{1} + _CURRENT_BANK         ; bank in which this subroutine resides
{1}                                     ; entry point
TEMPORARY_VAR SET Overlay
TEMPORARY_OFFSET SET 0
VAR_BOUNDARY_{1} SET TEMPORARY_OFFSET
_FUNCTION_NAME SETSTR {1}
ENDM

;---------------------------------------------------------------------------------------------------

MAC RAMDEF ; {name of subroutine}

; Just an alternate name for "DEF" that makes it clear the subroutine is in RAM

DEF {1}
ENDM

;---------------------------------------------------------------------------------------------------

MAC SLOT ; {1}

IF ({1} < 0) || ({1} > 3)
ECHO "Illegal bank address/segment location", {1}
ERR
ENDIF

_BANK_ADDRESS_ORIGIN SET \$F000 + ({1} * _ROM_BANK_SIZE)
_BANK_SLOT SET {1} * 64               ; D7/D6 selector

ENDM

;---------------------------------------------------------------------------------------------------
; Temporary local variables
; usage:
;
;   DEF fna
;       REF fnc
;       REF fnd
;       VAR localVar1,1
;       VAR ptr,2
;       VEND fna
;
; The above declares a functino named 'fna'
; The function declares two local variables, 'localVar1' (1 byte) and 'ptr' (2 bytes)
; These variables are given an address in the overlay area which does NOT overlap any of
; the local variables which are declared in the referring functions 'fnc' and 'fnd'
; Although the local variables are available to other functions (i.e., global in scope), care
; should be taken NOT to use them in other functions unless absolutely necessary and required.
; To share local variables between functions, they should be (re)declared in both so that they
; have exactly the same addresses.

; The relative offset into the overlay area for the next variable declaration...
TEMPORARY_OFFSET SET 0

; Finalise the declaration block for local variables
; {1} = name of the function for which this block is defined
MAC VEND
; register the end of variables for this function

VAREND_{1} = TEMPORARY_VAR
;V2_._FUNCTION_NAME = TEMPORARY_VAR
ENDM

; Note a reference to this function by an external function
; The external function's VEND block is used to guarantee that variables for
; the function we are declaring will start AFTER all other variables in all referencing blocks

MAC REF ; {1}
IF VAREND_{1} > TEMPORARY_VAR
TEMPORARY_VAR SET VAREND_{1}
ENDIF
ENDM

; Define a temporary variable for use in a subroutine
; Will allocate appropriate bytes, and also check for overflow of the available overlay buffer

MAC VAR ; { name, size }
;        ;LIST OFF
{1} = TEMPORARY_VAR
TEMPORARY_VAR SET TEMPORARY_VAR + TEMPORARY_OFFSET + {2}

OVERLAY_DELTA SET TEMPORARY_VAR - Overlay
IF OVERLAY_DELTA > MAXIMUM_REQUIRED_OVERLAY_SIZE
MAXIMUM_REQUIRED_OVERLAY_SIZE SET OVERLAY_DELTA
ENDIF
IF OVERLAY_DELTA + Overlay >= TOP_OF_STACK
LIST ON
VNAME   SETSTR {1}
ECHO "Temporary Variable", VNAME, "overflow!"
ERR
ECHO "Temporary Variable overlow!"
ENDIF
LIST ON
ENDM

MAC ROMBANK ; bank name
SEG ROM_{1}
ORG _ORIGIN
_BANK_START         SET *
{1}_START           SET *
_CURRENT_BANK       SET (_ORIGIN - _FIRST_BANK ) / _ROM_BANK_SIZE
ROMBANK_{1}         SET _BANK_SLOT + _CURRENT_BANK
_ORIGIN             SET _ORIGIN + _ROM_BANK_SIZE
_LAST_BANK          SETSTR {1}
ENDM

;---------------------------------------------------------------------------------------------------

MAC CHECK_BANK_SIZE
.TEMP = * - _BANK_START
ECHO _LAST_BANK, "SIZE =", .TEMP, ", FREE=", _ROM_BANK_SIZE - .TEMP
IF ( .TEMP ) > _ROM_BANK_SIZE
ECHO "BANK OVERFLOW @", _LAST_BANK, " size=", * - ORIGIN
ERR
ENDIF
ENDM

;---------------------------------------------------------------------------------------------------

MAC CHECK_RAM_BANK_SIZE
.TEMP = * - _BANK_START
ECHO _LAST_BANK, "SIZE =", .TEMP, ", FREE=", _RAM_BANK_SIZE - .TEMP
IF ( .TEMP ) > _RAM_BANK_SIZE
ECHO "BANK OVERFLOW @", _LAST_BANK, " size=", * - ORIGIN
ERR
ENDIF
ENDM

;---------------------------------------------------------------------------------------------------

MAC RAMBANK ; {bank name}

SEG.U RAM_{1}
ORG ORIGIN_RAM
_BANK_START         SET *
_CURRENT_RAMBANK    SET (ORIGIN_RAM / _RAM_BANK_SIZE)
RAMBANK_{1}         SET _BANK_SLOT + _CURRENT_RAMBANK
ORIGIN_RAM          SET ORIGIN_RAM + _RAM_BANK_SIZE
_LAST_BANK          SETSTR {1}

ENDM

;---------------------------------------------------------------------------------------------------

; Failsafe call of function in another bank
; This will check the slot #s for current, call to make sure they're not the same!

MAC CALL ; function name
IF SLOT_{1} == _BANK_SLOT
FNAME SETSTR {1}
ECHO ""
ECHO "ERROR: Incompatible slot for call to function", FNAME
ECHO "Cannot switch bank in use for ", FNAME
ERR
ENDIF
lda #BANK_{1}
sta SET_BANK
jsr {1}
ENDM

MAC JUMP ; function name
IF SLOT_{1} == _BANK_SLOT
FNAME SETSTR {1}
ECHO ""
ECHO "Cannot switch bank in use for ", FNAME
ERR
ENDIF
lda #BANK_{1}
sta SET_BANK
jsr {1}
ENDM

;---------------------------------------------------------------------------------------------------
; RAM accessor macros
; ALL RAM usage (reads and writes) should use these
; They automate the write offset address addition, and make it clear what memory is being accessed

MAC [email protected] ;{}
sta [RAM]+{0}
ENDM

MAC [email protected] ;{}
stx [RAM]+{0}
ENDM

MAC [email protected] ;{}
sty [RAM]+{0}
ENDM

MAC [email protected] ;{}
lda {0}
ENDM

MAC [email protected] ;{}
ldx {0}
ENDM

MAC [email protected] ;{}
ldy {0}
ENDM

MAC [email protected] ;{}
lda {0}
ENDM

MAC [email protected] ;{}
lda {0}
ENDM

MAC [email protected] ;{}
cmp {0}
ENDM

;---------------------------------------------------------------------------------------------------
;EOF```

Edited by Andrew Davie
• 4