11. Bank switching¶
The bank system on the HP-41 allows for up to four pages to reside in the same 4K address page. The bank switch mechanism is actually implemented by the memory system. The CPU itself is totally unaware of it.
In practice, there are two basic ways to approach writing bank switched code.
The first approach is to use (at least) two address pages. Bank switching can then be done in one of the pages (which is not bank switched), and the bank switch affect the other page. This makes it fairly easy to control and layout memory and is the approach used in the HP-41CX operating system.
The second approach is to switch bank inside the page you are executing in, essentially pulling the rug on yourself, or rather replacing it on the fly. For this to work, you need to place code so that the other bank is ready to take over immediately where you happen to be executing when the bank switch takes place.
The advantage of the first approach is that it simpler and allows for somewhat more flexibility. The disadvantage is that it occupies two address pages.
This tool set provides support to do either way and makes the latter single page approach virtually as simple to implement as the two page approach.
11.1. Hardware behavior¶
The original bank switch mechanism as defined by HP, relied on a
specific memory chip that was used to load the ROM images. This chip
had some peculiarities in that it put constraints on what instruction
could appear before the bank switch instruction. One can see this if
one study the code of the HP-41CX or the Advantage ROM, many bank
switch instructions are preceded by a goto instruction (branching to
the next line). HP also imposed some restrictions on that certain
areas in bank switched modules were required to contain specific
code, otherwise the load software HP used would refuse to accept the
module image.
As HP no longer produce these memory chips or provide any module building services, we are no longer restricted by these limitations and rules.
The reason for the imposed specific code was most likely to make it possible to enable bank pages from diagnostic software, which makes it possible to verify the checksum of banked pages.
Current hardware available are Clonix, NoV series and NEWT (41CL). The MLDL2000 is reasonable recent, but sadly not available anymore. All of them support running banked modules.
The MLDL2000 and NEWT provide bank switching by “pairing pages”, that is, the two pages that are covered by the same physical port are bank switched together. Some special rules apply for low memory in NEWT as it needs to handle the HP-41CX mainframe image where bank switching take place between page 3 and 5. Clonix differs in that bank switching takes place in all pages in the module, not just the two pages next to each other.
What it means in practice is that provided we follow the golden rule that we should never leave a secondary page active when we give control back to the operating system, then we are safe. Calling mainframe entry points are OK, provided that they return to the caller. For routines that will (or may) not return, we should only call them from the main bank (bank 1). We can also relax the rules that HP used to impose one banked modules.
If we use the approach to have two pages next to each other, where one switches the bank of the other and then make calls into it, the first page will actually also switch bank! This means that we need to mirror the unbanked page into the secondary banks of that page. Fortunately, there is no need to duplication the page, the hardware just need to the configured to show that page in the other bank too. This is either done automatically by the load tool, or is trivial to do in the hardware configuration, but can be worth knowing.
11.2. Single page¶
To make it possible to arrange code to appear in the right place in different banks, a mechanism exists for setting up a section fragment to shadow another one. Essentially, it means that you can specify that a section fragment (the shadow) is placed relative to another section fragment, but in a different memory bank.
To make it work you need to insert the appropriate bank switch instruction at the right place and provide linker rules to get the different section fragments to reside in the desired bank.
The .shadow directive creates a shadow relation. The following
macro makes it easy to switch bank on the fly:
switchBank: .macro n
enrom\n
10$:
.section Code\n
.shadow 10$
.endm
Provided you have you have defined this macro, you can use it to switch bank in the following way:
.name "ADD"
ADD: s9=0
gsbp FindBufferGetXSaveL
switchBank 2
c=regn X
This prelude of an ADD instruction does an initial few instruction
in the main bank, then switch over to bank 2 where the c=regn X
and following instructions are.
In a list file, it looks as follows:
1786 004a 084004 .name "ADD"
1786 004c 001
1787 004d 244 ADD: s9=0
1788 004e ...... ADD_2: gsbp FindBufferGetXSaveL
1788 0050 ...
1789 switchBank 2
\ 0051 180 enrom2
\ 0052 10$:
\ 0000 .section Code2
1790 0000 0f8 c=regn X
As can be seen, there is just a single enrom2 instruction inserted
in the code. The code starting with c=regn X appears in a new
section fragment.
Details of the switchBank macro¶
The macro takes a single argument, the bank it should switch to, which
can be any number from 1 to 4. This number is both used as part of the
enrom instruction and the new section fragment. It just happens
that the enrom instruction wants a number 1 to 4, and we can just
use make up a suitable section name based on it as well.
After executing the enrom instruction, the CPU will want to
execute the instruction immediately following it. As we just switched
bank, that instruction will be fetched from the bank we switched to.
To make it work, we want to align the code in the other bank so that
it ends up at the location following the enrom instruction of the
previous bank. To get a reference to that location, the macro defines
a label after the enrom instruction (the local 10$ label).
As the following code has to be in a different bank, the macro starts with a new section fragment and names it with the bank number as suffix. There is nothing magical about the section name here, it can be anything, but doing it this way provides a suitable name that contains the bank number.
The .shadow directive takes a location expression as its
argument. In this case we want the address after the enrom
instruction. It adds an annotation to the current section (the newly
created Code2 section fragment) that later will get emitted in the
object file to inform the linker.
The linker will find this .shadow annotation and arrange so that
the first instruction of this Code2 section fragment will be
placed at the address just after the enrom2 instruction.
For this to work, you need to specify that section Code2 should be
in bank 2 in your linker rules file. Here is how such file can look:
(define memories
'((memory Page1 (position independent) (bank 1) (address (#x0 . #xFFF))
(section (FAT #x0) Code1 (Tail #xFD4))
(checksum #xFFF hp41)
(fill 0))
(memory Page2 (position independent) (bank 2) (address (#x0 . #xFFF))
(section (Bank2Header #x0) Code2 (Tail2 #xFF1))
(checksum #xFFF hp41)
(fill 0))))
It is possible to switch banks more than once and it is also possible
to have more than one section fragment as shadows to the same (parent)
section fragment. However, only one .shadow directive is allowed
in a section fragment, meaning that you can only anchor yourself
relative to one other section fragment.
Note
Switching banks like this will cause small code slices to be sprinkled out in the other bank. To get best allocation performance out of that bank, it is a good idea to also provide routines in it that can be placed more freely, i.e. subroutines.
Note
As a general rule, it is better to have several smaller section fragments rather than few large ones, as the smaller section fragments give the linker more freedom to place code.
11.3. Exiting back¶
To exit back to the main bank from a secondary bank, you may want to go through some support routine you have defined that does necessary cleanup before exiting back to the operating system.
To exit from a secondary bank, you can provide an alternative entry
point for such routine that just consists of a single enrom1
instruction to enble bank 1 and place it so that it appears at the
address location immediately before the actual routine in bank 1.
Here is one way of doing it with the .shadow directive:
.section Code2
.shadow PutXDrop-1
PutXDrop_rom2:
enrom1
This just outputs a single enrom1 instruction in bank 2, which
gets located at the address before PutXDrop which is in bank 1.
From the secondary bank, we exit by jumping to PutXDrop_rom2
(i.e. using a goto, golong or golp instruction). Inside the
bank the PutXDrop_rom2 routine looks like a single bank switch
instruction located in the middle of nowhere. This instruction opens a
trap door and we drop into bank 1 and end up in the routine that
perform the exit.
Note
The PutXDrop routine does not need to be the first location of
the section fragment it belongs to, it can be located anywhere in
its section fragment.
Note
If you have more banks, you will need one routine like this for each
bank you plan do drop into PutXDrop from.
As mentioned, to use it simply jump there using either a goto,
golong or golp instruction from a bank 2 section:
someEntry:
...
golp PutXDrop_rom2
11.4. Calling between banks¶
Calling routines in another bank can be done, but it consumes one
additional subroutine level. To do it, a stub routine can be used that
temporarily switch the bank during the actual subroutine call,
basically surrounding it by switchBank macros on both sides:
.section Code2
findBufferUserFlags_rom2:
c=regn 14
rcr 12
st=c
switchBank 1
gsbp findBuffer
switchBank 2
cstex
rtn
If you are in a page relocatable module, the gsbp instruction
temporarily takes one additional subroutine level. Making calls like this
quickly consumes the preciously small subroutine stack.
In some cases it can still make sense to do it.
There may also be another routine like it in bank 1:
.section Code
findBufferUserFlags:
c=regn 14
rcr 12
st=c
gsbp findBuffer
cstex
rtn
In this case we get the same routine available in both bank 1 and 2 at
the cost of some small code duplication. Stack usage will be
the same in this case, and the called routine findBuffer is
defined only once.
Note
In most cases, some planning on how to arrange code to minimize the need of bank switching, is often well spent time.
Note
Small code bduplication can be a better way. In such cases, macros can help in defining the actual code sequence only once.