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.