I’ve been wondering for a while if I could run C-based code on the 65816 using the cc65 toolset. Browsing through the cc65 documentation and C libraries it’s pretty clear that the 65816 isn’t specifically supported. In fact, the C libraries are primarily targeted towards the 6502 with just modest support for the 65C02. The cc65 program documentation notes for the –cpu option “Specifying 65C02 will use a few 65C02 instructions when generating code. Don’t expect too much from this option: In most cases the difference in size and speed is just 1-2%.“ The 65816 isn’t even listed as supported. Will the compiler even run with it selected and if it compiles, will the program run? The short answer is yes. My C-based hello world example project compiles and runs just fine targeting the 65816.
The question of adding support for the 65816 to cc65 has been asked and answered by the current developers. Given the limited support for the 65C02, it isn’t surprising that they don’t contemplate adding support for the 65816. They did point the questioner to an LCC version for the 65816 though, so that might be worth checking out. LCC is a small, retargetable compiler for the ANSI C programming language. A first glance at the 65816-based repository doesn’t seem too encouraging though. It seems to lack 65816 specific documentation and the associated github.io repository states “LCC-816 is an experimental, highly unusable, C compiler targeting the 65816/Apple IIgs”, though this could just simply be out of date. If not, one has to wonder why they’re pointing to it as an option.
The library I created for the hello world example project is very minimal. While the –cpu option noted above gives some comfort that the C libraries aren’t going to trip us up in running C-based code on the 65816, verifying we won’t have a problem is a bit more complicated than just compiling and running a simple hello world program. The C libraries would be incompatible if they make unqualified use of the extended 65C02 instructions RMB, SMB, BBR and BBS as the opcodes used for these instructions on the 65C02 are mapped to other instructions on the 65816. A quick search of the C libraries shows that these instructions aren’t used. In fact, very few of the 65C02 specific instructions are used and their use is either qualified or specific to a library targeting a machine with the 65C02. So, instruction wise we should be fine using the C libraries with the 65816. Just note that the C code will not utilize any of the efficiencies that could be gained with the 65816. But will the 65816 operating modes trip us up?
There are a few references to the 65816 in the C libraries, many noting that they assume the processor is running in emulation mode. These are mainly geared towards specific builds and shouldn’t be an issue for us. But do they imply that we’re limited in some way to only running in emulation mode? I wouldn’t think so. We should be fine as long as we remember that the C libraries use 8-bit registers and switch to this mode and back when appropriate. We’ll also need to ensure that the other various registers are set appropriately for call to C functions. This makes me wonder whether the C libraries hardcode any manipulations of the hardware stack. That could also place some restrictions on us. For now, though, I just keep it simple, using a typical 6502-based configuration to start.
With that in mind, I set out to port my n-queens project to the 65816. I’ll write a bit more about this later, but for now, here is an image of the port running in db65xx.
As shown in the image above, it’s not hard getting cc65 C-based code to run on the 65816. In that case everything is running in Bank 0. For a bit more of a challenge, I tried adding n-queens to my Forth operating system. The key difference is that Bank 0 in my Forth operating system is reserved for data and code that must be in or benefits from being in Bank 0. As such I tried to put C related segments in Banks 1 and 2. I didn’t have any luck with this as the standard C library is written assuming Bank 0 placement and placing various segments elsewhere generates range errors. I’m sure these could be easily resolved, but at the expense of having to maintain a separate C library. I may take a stab at it just for the n-queens project but beyond that it begs the question, why not just create a 65816 specific library.
I was too optimistic about getting the C-based n-queens program into Banks 1 and 2. Of course, some of the data needs to reside in Bank 0, such as the zero-page symbols and the C stack. That means the HEAP segment is in Bank 0 too.
I placed the DATA and BSS segments in Bank 0 as well to avoid rewriting the zerobss and copydata routines. It’s unlikely that these could go into Bank 2 as I wanted anyway as I’m guessing that the C library relies on the absolute address mode for some of this data and I can’t switch the data bank register back and forth between the banks without a major rewrite of the C library. That left just trying to put the C related code in Bank 1.
On my first try, the ld65 linker crashed, with a strange error. I’ve found that when this happens, you’re doing something that the developers didn’t anticipate. It’s often hard to determine the problem. Turning on the verbose linker option, I found that the linker was crashing as it was trying to link the ONCE segment. It wasn’t clear what was causing the problem from the ld65 source code, so I just moved that segment to Bank 0 as well. It was only 38 bytes and fit within the ROM I reserved for hardware code.
With those changes, a tweak to the startup code and a few stubs to transfer control to the Bank 1 C related code and back, I was able to create the n-queens project binary. On running though, I just got garbage output where it should have printed “Enter the number of queens to use …”. I needed to enable the debug information option to track the problem down.
Turns out that the printf function uses self-modifying code in the data segment to jump to the “output” routine. Of course, that assumes that the DATA segment is in the same bank as the printf code. As it was, I was just jumping to a random piece of code in Bank 1. This wouldn’t be simple to fix. and I’m guessing that this is just the first of many such coding techniques I’d find in the C library. Modifying the C library wasn’t going to be easy.
As such, it’s just easier to set aside a portion of Bank 0 for C and continue to use the C library as is. I’ve reserved four 12k segments in Bank 0 for separate instances of my Forth operating system. I’ve never really used more than a single instance, except in testing so for now, I’ll take one of those segments and dedicate it to C. I may make it expandable if I do much C coding. But we’ll see how it goes first.
Here is my Forth operating system running with the C-based N-queens project in the Bank 0 segment reserved for the fourth instance of the operating system.
The main code of my operating system resides in Bank 1 and utilizes Bank 2 RAM and Bank 0 for direct page, stacks and hardware related code. With C residing in Bank 0 I need to set the bank register to it prior to calling a C function which needs to use long addressing for the call and return. I also need to switch to 8-bit registers that the C library utilizes since my operating system runs with 16-bit registers. These need to be restored upon return.
What’s Up Next
Just for kicks (and a bit more experience with the cc65 C library) I’m going to try modifying the C library modules I’m using for n-queens to (1) allow placing code and heap outside Bank 0 and (2) utilize 16-bit registers except where 8-bit registers are appropriate. This could require edits to up to 90 library modules that make up the n-queens library. This isn’t a small task, but significantly less than modifying the entire library which consists of over 400 modules.
I’m not familiar enough with the cc65 compiler to know whether my second goal is possible. If the compiler uses the 8-bit register size internally, simply modifying the library modules will not be sufficient and the library could be locked to using 8-bit registers. Also, while these modifications will allow the compiler to take advantage of some of the 65816 efficiencies, I may not be able to take full advantage of the 65816 features. Stay tuned for more details in my next post.
It didn’t take long to figure out that my second goal, to use 16-bit registers in code generated from C for the 65816 wasn’t going to be useful from just modifying the cc65 C library modules. I had thought of the library modules more as the building blocks that the cc65 compiler uses in generating assembly source code. In reality, they’re just commonly used routines that the compiler codes subroutine calls to after setting the processor registers with the arguments used by the modules. Those registers are assumed to be 8-bit in size so to pass a 16-bit value to a module, it’s commonly passed with the least-significant byte in the A register and the most-significant byte in the X register. For example, the strlen(buffer)-1 statement from line 82 in the image above is compiled as:
65xx code generated by the cc65 compiler for strlen(buffer)-1
lda #<(_buffer) ldx #>(_buffer) jsr _strlen jsr decax1
where _strlen and decax1 are library modules. The 16-bit address pointer to buffer is passed to the strlen routine in the A and X registers. The length of the string in the buffer is then returned in these registers and then passed to the decax1 routine.
So, while I could modify the library modules to take advantage of 16-bit registers, the arguments passed in registers to the modules would still be 8-bit values. Having 16-bit registers in this case is actually bad since I’d have to convert these to 16-bit values and then convert back to return the result as 8-bit values. To get any real value from modifying the modules to 16-bit would require passing the arguments as 16-bit values.
I still think modifying the library modules to be able to place the C-related code, data and heap in other Banks is possible. If so, I’ll add another post. Otherwise, I add another update here.