Bank Switching in assembly
In the Commander X16 the memory range from $a000 to $bfff, 8k of ram, will be banked. This will allow the computer to access up to 2048k (2Mb)
I am planning on creating a game using assembly and loading all of the graphics/sprites/text in the initial load and then switch banks to access them. Looking at the forum I couldn’t find any posts about bank switching and very little on using the emulator debugger. So I decided to do a guide after I figured out something useful.
The code listed in this guide was created for the ca65 cross-compiler. I used Visual Studio Code as my text editor with git version control. For use on r36 of the X16 emulator.
I did some reading of the Commander X16 Programmers Reference guide and found the reference to banked memory. The manual did direct me to the I/O Programming section but it didn’t help me much. This listed $9F61 as the byte to adjust banking 0-255. The X16 will have a minimum of 512k so that is banks 0-63. Personally, I hope they just ship all of them with 2048k, banks 64-255. One less configuration option to make it easier for the programmers.
The below code is my initial config before my main loop. I have created the string which I am going to copy into the banks. I’ve declared VIA1 at $9F61. I’ve declared a few kernal functions that I will use. I’ve declared three variables and allocated memory locations for them. bank_selection will track which memory bank is currently selected. message_position will track which character in the string I am currently accessing. message_in_bank is the memory location I will write to. Then I initialised two of the variables. The bank_selection variable is set to 1 as it appears there is some code already in 00 $a000. (I found overwriting the contents of 00a000 did strange things and I didn’t sort it out until I had worked out how to use the debugger down below). The message_position variable is set to 0 for the first character.
;----------------------------------------------------------------------------- .segment "DATA" ; here is where you put raw data / binary / sprites / text blobs / etc message: .asciiz "this is a string which we will put in to the different banks of $a000" ;----------------------------------------------------------------------------- .segment "CODE" ; constants VIA1 = $9F61 ; give kernal functions names CHROUT = $ffd2 ; CHROUT outputs a character (C64 Kernal API) CHRIN = $FFCF ; CHRIN read from default input ; variable locations bank_selection = $0070 message_position = $0071 message_in_bank = $A000 ; variables set lda #1 sta bank_selection ; I am starting at bank 1 because it appears there is something written in bank 0 at $a000-$a041 lda #0 sta message_position
You may have noticed in the above code there is no basic launcher code. Previous to this project I had been setting my memory start location to $0801 and including a ‘basic loader’ string. $0c,$08,$0a…. etc and then my actual assembly code starting at $0810. For this project, I had been doing a sys2061 to start it while I was working on it. One time I accidentally typed run, and it ran! That wasn’t supposed to happen! I had a look at my prg in a hex editor. Sure enough, I saw 9E, 20, 32,30,36….. It appears that ca65 and its linker create a little basic loader.
The below code is my main loop. I am checking to see which bank is currently selected and if less then 20 we will run the code. If bank 20 is currently selected will we branch out to a function which will end the program. I then set the bank selection. I put the message position counter into register x. Then I put a character from the message, register x (register x is the offset in the message string) in to register a. I do a quick check to make sure the character is not null. If it is null then we have reached the end of the string and we branch out to a function which will change banks and reset counters. Then we store the current character into message_in_bank variable with the same offset. Then we increment the message_position so next time it will read the next character. Then we loop back to the beginning and go again.
;----------------------------------------------------------------------------- .proc main lda bank_selection cmp #20 ;lets only do x banks bcs wait_user ; if it equals x then quit ; set RAM bank lda bank_selection sta VIA1 ; get location of next character from our screen message ldx message_position lda message,x cmp #0 ; CoMPare accumulator with value 0 which is null terminator beq update_counter ; output it to bank sta message_in_bank,x ; increase out message position inc message_position ; loop back to start jmp main .endproc
The below code is my two support functions. update_counter resets the message_position variable back to 0 so the main loop will start to read from the first character again. I also increase the bank selection so I can write to the next memory bank. Then I jump back to the main loop. wait_user just prints out a single character to the screen then waits for the user to press return/enter then it ends.
.proc update_counter ; lets go back to the first character in message lda #0 sta message_position ; increase the next bank inc bank_selection jmp main ; lets jmp back to the mail loop with updated counters .endproc .proc wait_user ; send a * out to the screen so we know its finish lda #42 jsr CHROUT jsr CHRIN ; Read input until Enter/Return is pressed rts ; Return to caller .endproc
Intro to x16emu debug
When writing up this code I realised I wasn’t going to get any feedback if it was successful. I needed a way to see what was happening in memory. I was getting strange problems. In the past, I’ve had debuggers, breakpoints and been able to step each line of my code etc. I didn’t know how I was going to do that with X16emu. I did find a post on the forum by codewar65 which reminded me to read the actual documentation. In the x16emu readme.md there was a section on the debugger! I changed my visual studio code run task to include the “-debug” and started it up. Pressing F12 to break into the debugger.
Now to learn how to use it better! I started to use F10 to try and step ‘over’ routines and F11 to step over each instruction being executed. Happy! Then I wanted to see what was in memory. codewar65 had mentioned that page up and page down would let you browse memory. So I paged down to A000 and noticed it had FF:A000.
I suspected that FF:A000 meant I was looking at bank FF. This proved frustrating until I was re-reading readme.md for x16emu. I could type m %x were %x was the address. Originally my code started at bank 00. I tried it m 00a000. There was some code there already, not the blank 00 00 00 I expected. Then I stepped each instruction and I would get strange results. So I quickly decided to avoid 00a000 and changed my starting bank to 1. Compiled, ran and moved to 01a000 and there it was, the string had been written to memory.
I then looked at a few different banks. 02:A000, 03:A000… 13:A000 my string was in all of them. 14:A000 my string wasn’t which made sense. I had stopped the main loop when the bank was decimal 20 which is hex 14.
With the free plan on WordPress, I can’t upload text files or zip files. So I’ve turned it into a pdf for people who want all of the code in a single document to copy and paste from.
I would encourage any feedback or comments to improve this guide.