You need to be very careful programming for this because switching read mode between main and aux banks affects everything the Apple II is doing, not just reading data for your program but also reading memory for running your program as well as accessing the BASIC ROM and the Monitor ROM.
Fortunately Apple provided a solution for this using what's called AUXMOVE in the tech reference listed in Janka's answer. This can copy data from main to aux, aux to main, without itself crashing.
There is also XFER, which transfers control of running programs between main and aux memory.
Running code in other memory banks is all the more complex because the zero page and stack areas are handled differently. Code running in aux memory can use the main ZP/stack or the aux ZP/stack, and this can crash programs due to things becoming inconsistent between them.
If you decide to use the alternate ZP/stack for your code running in aux banks, you need to remember to switch back to the main ZP/stack before transferring control back to code / ROMs in the main bank, or they will likely crash.
Also this question is more complex if you are talking about systems with more than 128k as there needs to be a way to choose which of the many different banks you want to access. One example is the Applied Engineering RamWorks. These other memory expansion systems will each have their own special way to access their additional memory.
Applied Engineering RamWorks III advertisement: http://apple2online.com/web_documents/AE%20RAMworks%20III%20Brochure.pdf
It is potentially possible to try to detect what hardware is installed, what amount is available, and then use the appropriate memory access methods to match the installed hardware.
All this is made much simpler on the IIgs which provides a way to access more than 64k directly, with extended 24-bit addressing opcodes.
A simple example of the problems:
- Boot Apple IIe, IIc, IIgs
- CALL -151
- 300:8D 00 C0 8D 03 C0 8D 05 C0 8D 02 C0 8D 04 C0 60
- 300G
- Crash!
300: 8D 00 C0 STA $C000 ; Make sure 80STORE is off
303: 8D 03 C0 STA $C003 ; Read from AUX
306: 8D 05 C0 STA $C005 ; Write to AUX
309: 8D 02 C0 STA $C002 ; Read from MAIN
30C: 8D 04 C0 STA $C004 ; Write to MAIN
30F: 60 RTS
This crashes immediately after writing to $C003 to enable reading from the AUX bank, because the program counter is now pointing at $306 in the AUX bank, it tries to read the next line of program code from there, which contains nothing (00 00....) and the program crashes. (The bank modes get reset to normal by the Monitor when the error line prints.)
A really dumb and inefficient programming workaround that I discovered is to simply duplicate your code in the same memory locations in both the main and aux banks. So now you can freely toggle back and forth between reading or writing to the main and aux banks and your program won't crash.
Same code as before, but using AUXMOVE to copy $300 to $30F to the AUX bank before it runs:
- Boot Apple IIe
- CALL -151
- 300:8D 00 C0 8D 03 C0 8D 05 C0 8D 02 C0 8D 04 C0 60
- 310:A9 00 85 3C 85 42 A9 10 85 3E A9 03 85 3D 85 43 85 3F 38 20 11 C3 60
- 310G
- 300G
- Works! Exits with no errors.
310: A9 00 LDA #$00
312: 85 3C STA $3C
314: 85 42 STA $42
316: A9 10 LDA #$10
318: 85 3E STA $3E
31A: A9 03 LDA #$03
31C: 85 3D STA $3D
31E: 85 43 STA $43
320: 85 3F STA $3F
322: 38 SEC
323: 20 11 C3 JSR $C311
326: 60 RTS
I'm sure there are much more elegant ways to do this, but if you are just starting out, and just want to get the job done, without being a super-brain expert at it, and saving bytes and all that, this does work.
(The "elegant" solution involves storing your program code above $CFFF because $200-BFFF is the region affected by $C003/$C005 $C002/$C004, but then you must deal with swapping out either the BASIC ROM or the Monitor ROM. Ugh.)
Just make sure that if you use this hack and you are hand-coding in the monitor, that you run AUXMOVE before running your code, or you will have two different programs that are interleaved together across the two memory banks, whenever it toggles back and forth between reading the MAIN or AUX bank.
Self modifying counting code is tricky due to swapping back and forth between reading and writing in MAIN or AUX. It would be better to use the zero page for counters, as it is not swapped out by $C003/$C005. Using memory above $CFFF for your program avoids these problems.
Also, before your program code exits, be sure to write to $C002 and $C004 to go back to read/write from the Main bank.
For completeness, I believe this is how this is done using the RAM located at $D000-FFFF to store you main/aux swapping code without it itself getting screwed up.
300: AD 89 CA LDA $C089 ; Double read: Read ROM, write RAM
303: AD 89 CA LDA $C089 ; $D000-FFFF, use bank 1 $D000-DFFF
306: A0 00 LDY #$00 ; ------
308: A9 00 LDA #$00 ; Use Monitor MemCopy to copy
30A: 85 3C STA $3C ; the Monitor ROM $F800-FFFF
30C: 85 42 STA $42 ; to RAM at $F800-FFFF in
30E: A9 FF LDA #$FF ; preparation to read/write
310: 85 3E STA $3E ; $D000-FFFF without it.
312: 85 3F STA $3F ;
314: A9 F8 LDA #$F8 ;
316: 85 3D STA $3D ;
318: 85 43 STA $43 ;
31A: 20 2C FE JSR $FE2C ;-------
31D: AD 8B C0 LDA $C08B ; Double read: Read/Write RAM
320: AD 8B C0 LDA $C08B ; $D000-$FFFF, use bank 1 $D000-DFFF
323: A0 00 LDY #$00 ; ------
325: A9 43 LDA #$50 ;
327: 85 3C STA $3C ; This is running the copied
329: A9 03 LDA #$03 ; Monitor routines in RAM to
32B: 85 3D STA $3D ; use MemCopy again, this time
32D: A9 56 LDA #$60 ; to copy from $350-$35F to
32F: 85 3E STA $3E ; $D000 bank 1.
331: A9 03 LDA #$03 ;
333: 85 3F STA $3F ;
335: A9 00 LDA #$00 ;
337: 85 42 STA $42 ;
339: A9 D0 LDA #$D0 ;
33B: 85 43 STA $43 ;
33D: 20 2C FE JSR $FE2C ; ------
340: 4C 00 D0 JSR $D000 ; Run copied code at $D000
; When code above $D000 returns, do
343: AD 8A C0 LDA $C08A ; Read ROM, no write, $D000 bank 1
346: 60 RTS ; to go back to normal
350: 8D 00 C0 STA $C000 ; ------
353: 8D 03 C0 STA $C003 ; Same as before, switch to
356: 8D 05 C0 STA $C005 ; read/write aux memory, then
359: 8D 02 C0 STA $C002 ; switch back to main memory
35C: 8D 04 C0 STA $C004 ; -----
35F: 60 RTS
(Whoops found a bug.... you can't switch back to reading from ROM while running code above $D000, that dun work.)
After the aux/main switching code is copied up to $D000, the memory area from $300-$35F can be overwritten and reused for something else.
Once the code has been loaded above $CFFF, if you are using BASIC, you only need a short stub to access it and the copied Monitor.
6000: AD 8B C0 LDA $C08B ; Double read: Read/Write RAM
6003: AD 8B C0 LDA $C08B ; $D000-$FFFF, use bank 1 $D000-DFFF
6006: 20 00 D0 JSR $D000
6009: AD 8A C0 LDA $C08A ; Reactivate BASIC in ROM
600C: 60 RTS
Also in this example I am copying the Monitor ROM to RAM, because I am hand-assembling and coding this directly without using a formal compiler. If an error occurs with $D000-FFFF as RAM and you didn't copy the Monitor ROM to RAM, the system will just simply freeze because the ROM routines needed to even display an error message are no longer present. It makes hand-debugging a real pain to not keep the ROM code available in RAM.