Defining a Custom cc65 Target <author>Bruce Reidenbach <abstract> This section provides step-by-step instructions on how to use the cc65 toolset for a custom hardware platform (a target system not currently supported by the cc65 library set). </abstract> <!-- Table of contents --> <toc> <!-- Begin the document --> <sect>Overview<p> The cc65 toolset provides a set of pre-defined libraries that allow the user to target the executable image to a variety of hardware platforms. In addition, the user can create a customized environment so that the executable can be targeted to a custom platform. The following instructions provide step-by-step instructions on how to customize the toolset for a target that is not supported by the standard cc65 installation. The platform used in this example is a Xilinx Field Programmable Gate Array (FPGA) with an embedded 65C02 core. The processor core supports the additional opcodes/addressing modes of the 65SC02, along with the STP and WAI instructions. These instructions will create a set of files to create a custom target, named SBC, for <bf>S</bf>ingle <bf>B</bf>oard <bf>C</bf>omputer. <sect>System Memory Map Definition<p> The targeted system uses block RAM contained on the XILINX FPGA for the system memory (both RAM and ROM). The block RAMs are available in various aspect ratios, and they will be used in this system as 2K*8 devices. There will be two RAMs used for data storage, starting at location $0000 and growing upwards. There will be one ROM (realized as initialized RAM) used code storage, starting at location $FFFF and growing downwards. The cc65 toolset requires a memory configuration file to define the memory that is available to the cc65 run-time environment, which is defined as follows: <tscreen><code> MEMORY { ZP: start = $0, size = $100, type = rw, define = yes; RAM: start = $200, size = $0E00, define = yes; ROM: start = $F800, size = $0800, file = %O; } </code></tscreen> ZP defines the available zero page locations, which in this case starts at $0 and has a length of $100. Keep in mind that certain systems may require access to some zero page locations, so the starting address may need to be adjusted accordingly to prevent cc65 from attempting to reuse those locations. Also, at a minimum, the cc65 run-time environment uses 26 zero page locations, so the smallest zero page size that can be specified is $1A. The usable RAM memory area begins after the 6502 stack storage in page 1, so it is defined as starting at location $200 and filling the remaining 4K of space (4096 - 2 * 256 = 3584 = $0E00). The 2K of ROM space begins at address $F800 and goes to $FFFF (size = $0800). Next, the memory segments within the memory devices need to be defined. A standard segment definition is used, with one notable exception. The interrupt and reset vector locations need to be defined at locations $FFFA through $FFFF. A special segment named VECTORS is defined that resides at these locations. Later, the interrupt vector map will be created and placed in the VECTORS segment, and the linker will put these vectors at the proper memory locations. The segment definition is: <tscreen><code> SEGMENTS { ZEROPAGE: load = ZP, type = zp, define = yes; DATA: load = ROM, type = rw, define = yes, run = RAM; BSS: load = RAM, type = bss, define = yes; HEAP: load = RAM, type = bss, optional = yes; STARTUP: load = ROM, type = ro; ONCE: load = ROM, type = ro, optional = yes; CODE: load = ROM, type = ro; RODATA: load = ROM, type = ro; VECTORS: load = ROM, type = ro, start = $FFFA; } </code></tscreen> The meaning of each of these segments is as follows. <p><tt> ZEROPAGE: </tt>Data in page 0, defined by ZP as starting at $0 with length $100 <p><tt> DATA: </tt>Initialized data that can be modified by the program, stored in RAM <p><tt> BSS: </tt>Uninitialized data stored in RAM (used for variable storage) <p><tt> HEAP: </tt>Uninitialized C-level heap storage in RAM, optional <p><tt> STARTUP: </tt>The program initialization code, stored in ROM <p><tt> ONCE: </tt>The code run once to initialize the system, stored in ROM <p><tt> CODE: </tt>The program code, stored in ROM <p><tt> RODATA: </tt>Initialized data that cannot be modified by the program, stored in ROM <p><tt> VECTORS: </tt>The interrupt vector table, stored in ROM at location $FFFA A note about initialized data: any variables that require an initial value, such as external (global) variables, require that the initial values be stored in the ROM code image. However, variables stored in ROM cannot change; therefore the data must be moved into variables that are located in RAM. Specifying <tt>run = RAM</tt> as part of the DATA segment definition will indicate that those variables will require their initialization value to be copied via a call to the <tt>copydata</tt> routine in the startup assembly code. In addition, there are system level variables that will need to be initialized as well, especially if the heap segment is used via a C-level call to <tt>malloc ()</tt>. The final section of the definition file contains the data constructors and destructors used for system startup. In addition, if the heap is used, the maximum C-level stack size needs to be defined in order for the system to be able to reliably allocate blocks of memory. The stack size selection must be greater than the maximum amount of storage required to run the program, keeping in mind that the C-level subroutine call stack and all local variables are stored in this stack. The <tt>FEATURES</tt> section defines the required constructor/destructor attributes and the <tt>SYMBOLS</tt> section defines the stack size. The constructors will be run via a call to <tt>initlib</tt> in the startup assembly code and the destructors will be run via an assembly language call to <tt>donelib</tt> during program termination. <tscreen><code> FEATURES { CONDES: segment = STARTUP, type = constructor, label = __CONSTRUCTOR_TABLE__, count = __CONSTRUCTOR_COUNT__; CONDES: segment = STARTUP, type = destructor, label = __DESTRUCTOR_TABLE__, count = __DESTRUCTOR_COUNT__; } SYMBOLS { # Define the stack size for the application __STACKSIZE__: value = $0200, type = weak; } </code></tscreen> These definitions are placed in a file named "sbc.cfg" and are referred to during the ld65 linker stage. <sect>Startup Code Definition<p> In the cc65 toolset, a startup routine must be defined that is executed when the CPU is reset. This startup code is marked with the STARTUP segment name, which was defined in the system configuration file as being in read only memory. The standard convention used in the predefined libraries is that this code is resident in the crt0 module. For this custom system, all that needs to be done is to perform a little bit of 6502 housekeeping, set up the C-level stack pointer, initialize the memory storage, and call the C-level routine <tt>main ()</tt>. The following code was used for the crt0 module, defined in the file "crt0.s": <tscreen><code> ; --------------------------------------------------------------------------- ; crt0.s ; --------------------------------------------------------------------------- ; ; Startup code for cc65 (Single Board Computer version) .export _init, _exit .import _main .export __STARTUP__ : absolute = 1 ; Mark as startup .import __RAM_START__, __RAM_SIZE__ ; Linker generated .import copydata, zerobss, initlib, donelib .include "zeropage.inc" ; --------------------------------------------------------------------------- ; Place the startup code in a special segment .segment "STARTUP" ; --------------------------------------------------------------------------- ; A little light 6502 housekeeping _init: LDX #$FF ; Initialize stack pointer to $01FF TXS CLD ; Clear decimal mode ; --------------------------------------------------------------------------- ; Set cc65 argument stack pointer LDA #<(__RAM_START__ + __RAM_SIZE__) STA sp LDA #>(__RAM_START__ + __RAM_SIZE__) STA sp+1 ; --------------------------------------------------------------------------- ; Initialize memory storage JSR zerobss ; Clear BSS segment JSR copydata ; Initialize DATA segment JSR initlib ; Run constructors ; --------------------------------------------------------------------------- ; Call main() JSR _main ; --------------------------------------------------------------------------- ; Back from main (this is also the _exit entry): force a software break _exit: JSR donelib ; Run destructors BRK </code></tscreen> The following discussion explains the purpose of several important assembler level directives in this file. <tscreen><verb> .export _init, _exit </verb></tscreen> This line instructs the assembler that the symbols <tt>_init</tt> and <tt>_exit</tt> are to be accessible from other modules. In this example, <tt>_init</tt> is the location that the CPU should jump to when reset, and <tt>_exit</tt> is the location that will be called when the program is finished. <tscreen><verb> .import _main </verb></tscreen> This line instructs the assembler to import the symbol <tt>_main</tt> from another module. cc65 names all C-level routines as {underscore}{name} in assembler, thus the <tt>main ()</tt> routine in C is named <tt>_main</tt> in the assembler. This is how the startup code will link to the C-level code. <tscreen><verb> .export __STARTUP__ : absolute = 1 ; Mark as startup </verb></tscreen> This line marks this code as startup code (code that is executed when the processor is reset), which will then be automatically linked into the executable code. <tscreen><verb> .import __RAM_START__, __RAM_SIZE__ ; Linker generated </verb></tscreen> This line imports the RAM starting address and RAM size constants, which are used to initialize the cc65 C-level argument stack pointer. <tscreen><verb> .segment "STARTUP" </verb></tscreen> This line instructs the assembler that the code is to be placed in the STARTUP segment of memory. <tscreen><verb> JSR zerobss ; Clear BSS segment JSR copydata ; Initialize DATA segment JSR initlib ; Run constructors </verb></tscreen> These three lines initialize the external (global) and system variables. The first line sets the BSS segment -- the memory locations used for external variables -- to 0. The second line copies the initialization value stored in ROM to the RAM locations used for initialized external variables. The last line runs the constructors that are used to initialize the system run-time variables. <tscreen><verb> JSR _main </verb></tscreen> This is the actual call to the C-level <tt>main ()</tt> routine, which is called after the startup code completes. <tscreen><verb> _exit: JSR donelib ; Run destructors BRK </verb></tscreen> This is the code that will be executed when <tt>main ()</tt> terminates. The first thing that must be done is run the destructors via a call to <tt>donelib</tt>. Then the program can terminate. In this example, the program is expected to run forever. Therefore, there needs to be a way of indicating when something has gone wrong and the system needs to be shut down, requiring a restart only by a hard reset. The BRK instruction will be used to indicate a software fault. This is advantageous because cc65 uses the BRK instruction as the fill byte in the final binary code. In addition, the hardware has been designed to force the data lines to $00 for all illegal memory accesses, thereby also forcing a BRK instruction into the CPU. <sect>Custom Run-Time Library Creation<p> The next step in customizing the cc65 toolset is creating a run-time library for the targeted hardware. The easiest way to do this is to modify a standard library from the cc65 distribution. In this example, there is no console I/O, mouse, joystick, etc. in the system, so it is most appropriate to use the simplest library as the base, which is for the Watara Supervision and is named "supervision.lib" in the lib directory of the distribution. The only modification required is to replace the <tt>crt0</tt> module in the supervision.lib library with custom startup code. This is simply done by first copying the library and giving it a new name, compiling the startup code with ca65, and finally using the ar65 archiver to replace the module in the new library. The steps are shown below: <tscreen><verb> $ copy "C:\Program Files\cc65\lib\supervision.lib" sbc.lib $ ca65 crt0.s $ ar65 a sbc.lib crt0.o </verb></tscreen> <sect>Interrupt Service Routine Definition<p> For this system, the CPU is put into a wait condition prior to allowing interrupt processing. Therefore, the interrupt service routine is very simple: return from all valid interrupts. However, as mentioned before, the BRK instruction is used to indicate a software fault, which will call the same interrupt service routine as the maskable interrupt signal IRQ. The interrupt service routine must be able to tell the difference between the two, and act appropriately. The interrupt service routine shown below includes code to detect when a BRK instruction has occurred and stops the CPU from further processing. The interrupt service routine is in a file named "interrupt.s". <tscreen><code> ; --------------------------------------------------------------------------- ; interrupt.s ; --------------------------------------------------------------------------- ; ; Interrupt handler. ; ; Checks for a BRK instruction and returns from all valid interrupts. .import _stop .export _irq_int, _nmi_int .segment "CODE" .PC02 ; Force 65C02 assembly mode ; --------------------------------------------------------------------------- ; Non-maskable interrupt (NMI) service routine _nmi_int: RTI ; Return from all NMI interrupts ; --------------------------------------------------------------------------- ; Maskable interrupt (IRQ) service routine _irq_int: PHX ; Save X register contents to stack TSX ; Transfer stack pointer to X PHA ; Save accumulator contents to stack INX ; Increment X so it points to the status INX ; register value saved on the stack LDA $100,X ; Load status register contents AND #$10 ; Isolate B status bit BNE break ; If B = 1, BRK detected ; --------------------------------------------------------------------------- ; IRQ detected, return irq: PLA ; Restore accumulator contents PLX ; Restore X register contents RTI ; Return from all IRQ interrupts ; --------------------------------------------------------------------------- ; BRK detected, stop break: JMP _stop ; If BRK is detected, something very bad ; has happened, so stop running </code></tscreen> The following discussion explains the purpose of several important assembler level directives in this file. <tscreen><verb> .import _stop </verb></tscreen> This line instructs the assembler to import the symbol <tt>_stop</tt> from another module. This routine will be called if a BRK instruction is encountered, signaling a software fault. <tscreen><verb> .export _irq_int, _nmi_int </verb></tscreen> This line instructs the assembler that the symbols <tt>_irq_int</tt> and <tt>_nmi_int</tt> are to be accessible from other modules. In this example, the address of these symbols will be placed in the interrupt vector table. <tscreen><verb> .segment "CODE" </verb></tscreen> This line instructs the assembler that the code is to be placed in the CODE segment of memory. Note that because there are 65C02 mnemonics in the assembly code, the assembler is forced to use the 65C02 instruction set with the <tt>.PC02</tt> directive. The final step is to define the interrupt vector memory locations. Recall that a segment named VECTORS was defined in the memory configuration file, which started at location $FFFA. The addresses of the interrupt service routines from "interrupt.s" along with the address for the initialization code in crt0 are defined in a file named "vectors.s". Note that these vectors will be placed in memory in their proper little-endian format as: <p><tt> $FFFA - $FFFB:</tt> NMI interrupt vector (low byte, high byte) <p><tt> $FFFC - $FFFD:</tt> Reset vector (low byte, high byte) <p><tt> $FFFE - $FFFF:</tt> IRQ/BRK interrupt vector (low byte, high byte) using the <tt>.addr</tt> assembler directive. The contents of the file are: <tscreen><code> ; --------------------------------------------------------------------------- ; vectors.s ; --------------------------------------------------------------------------- ; ; Defines the interrupt vector table. .import _init .import _nmi_int, _irq_int .segment "VECTORS" .addr _nmi_int ; NMI vector .addr _init ; Reset vector .addr _irq_int ; IRQ/BRK vector </code></tscreen> The cc65 toolset will replace the address symbols defined here with the actual addresses of the routines during the link process. <sect>Adding Custom Instructions<p> The cc65 instruction set only supports the WAI (Wait for Interrupt) and STP (Stop) instructions when used with the 65816 CPU (accessed via the --cpu command line option of the ca65 macro assembler). The 65C02 core used in this example supports these two instructions, and in fact the system benefits from the use of both the WAI and STP instructions. In order to use the WAI instruction in this case, a C routine named "wait" was created that consists of the WAI opcode followed by a subroutine return. It was convenient in this example to put the IRQ interrupt enable in this subroutine as well, since interrupts should only be enabled when the code is in this wait condition. For both the WAI and STP instructions, the assembler is "fooled" into placing those opcodes into memory by inserting a single byte of data that just happens to be the opcode for those instructions. The assembly code routines are placed in a file, named "wait.s", which is shown below: <tscreen><code> ; --------------------------------------------------------------------------- ; wait.s ; --------------------------------------------------------------------------- ; ; Wait for interrupt and return .export _wait, _stop ; --------------------------------------------------------------------------- ; Wait for interrupt: Forces the assembler to emit a WAI opcode ($CB) ; --------------------------------------------------------------------------- .segment "CODE" .proc _wait: near CLI ; Enable interrupts .byte $CB ; Inserts a WAI opcode RTS ; Return to caller .endproc ; --------------------------------------------------------------------------- ; Stop: Forces the assembler to emit a STP opcode ($DB) ; --------------------------------------------------------------------------- .proc _stop: near .byte $DB ; Inserts a STP opcode .endproc </code></tscreen> The label <tt>_wait</tt>, when exported, can be called by using the <tt>wait ()</tt> subroutine call in C. The section is marked as code so that it will be stored in read-only memory, and the procedure is tagged for 16-bit absolute addressing via the "near" modifier. Similarly, the <tt>_stop</tt> routine can be called from within the C-level code via a call to <tt>stop ()</tt>. In addition, the routine can be called from assembly code by calling <tt>_stop</tt> (as was done in the interrupt service routine). <sect>Hardware Drivers<p> Oftentimes, it can be advantageous to create small application helpers in assembly language to decrease codespace and increase execution speed of the overall program. An example of this would be the transfer of characters to a FIFO (<bf>F</bf>irst-<bf>I</bf>n, <bf>F</bf>irst-<bf>O</bf>ut) storage buffer for transmission over a serial port. This simple action could be performed by an assembly language driver which would execute much quicker than coding it in C. The following discussion outlines a method of interfacing a C program with an assembly language subroutine. The first step in creating the assembly language code for the driver is to determine how to pass the C arguments to the assembly language routine. The cc65 toolset allows the user to specify whether the data is passed to a subroutine via the stack or by the processor registers by using the <tt/__fastcall__/ and <tt/__cdecl__/ function qualifiers (note that there are two underscore characters in front of and two behind each qualifier). <tt/__fastcall__/ is the default. When <tt/__cdecl__/ <em/isn't/ specified, and the function isn't variadic (i.e., its prototype doesn't have an ellipsis), the rightmost argument in the function call is passed to the subroutine using the 6502 registers instead of the stack. Note that if there is only one argument in the function call, the execution overhead required by the stack interface routines is completely avoided. With <tt/__cdecl__</tt>, the last argument is loaded into the A and X registers and then pushed onto the stack via a call to <tt>pushax</tt>. The first thing the subroutine does is retrieve the argument from the stack via a call to <tt>ldax0sp</tt>, which copies the values into the A and X. When the subroutine is finished, the values on the stack must be popped off and discarded via a jump to <tt>incsp2</tt>, which includes the RTS subroutine return command. This is shown in the following code sample. Calling sequence: <tscreen><verb> lda #<(L0001) ; Load A with the high order byte ldx #>(L0001) ; Load X with the low order byte jsr pushax ; Push A and X onto the stack jsr _foo ; Call foo, i.e., foo (arg) </verb></tscreen> Subroutine code: <tscreen><verb> _foo: jsr ldax0sp ; Retrieve A and X from the stack sta ptr ; Store A in ptr stx ptr+1 ; Store X in ptr+1 ... ; (more subroutine code goes here) jmp incsp2 ; Pop A and X from the stack (includes return) </verb></tscreen> If <tt/__cdecl__/ isn't specified, then the argument is loaded into the A and X registers as before, but the subroutine is then called immediately. The subroutine does not need to retrieve the argument since the value is already available in the A and X registers. Furthermore, the subroutine can be terminated with an RTS statement since there is no stack cleanup which needs to be performed. This is shown in the following code sample. Calling sequence: <tscreen><verb> lda #<(L0001) ; Load A with the high order byte ldx #>(L0001) ; Load X with the low order byte jsr _foo ; Call foo, i.e., foo (arg) </verb></tscreen> Subroutine code: <tscreen><verb> _foo: sta ptr ; Store A in ptr stx ptr+1 ; Store X in ptr+1 ... ; (more subroutine code goes here) rts ; Return from subroutine </verb></tscreen> The hardware driver in this example writes a string of character data to a hardware FIFO located at memory location $1000. Each character is read and is compared to the C string termination value ($00), which will terminate the loop. All other character data is written to the FIFO. For convenience, a carriage return/line feed sequence is automatically appended to the serial stream. The driver defines a local pointer variable which is stored in the zero page memory space in order to allow for retrieval of each character in the string via the indirect indexed addressing mode. The assembly language routine is stored in a file names "rs232_tx.s" and is shown below: <tscreen><code> ; --------------------------------------------------------------------------- ; rs232_tx.s ; --------------------------------------------------------------------------- ; ; Write a string to the transmit UART FIFO .export _rs232_tx .exportzp _rs232_data: near .define TX_FIFO $1000 ; Transmit FIFO memory location .zeropage _rs232_data: .res 2, $00 ; Reserve a local zero page pointer .segment "CODE" .proc _rs232_tx: near ; --------------------------------------------------------------------------- ; Store pointer to zero page memory and load first character sta _rs232_data ; Set zero page pointer to string address stx _rs232_data+1 ; (pointer passed in via the A/X registers) ldy #00 ; Initialize Y to 0 lda (_rs232_data) ; Load first character ; --------------------------------------------------------------------------- ; Main loop: read data and store to FIFO until \0 is encountered loop: sta TX_FIFO ; Loop: Store character in FIFO iny ; Increment Y index lda (_rs232_data),y ; Get next character bne loop ; If character == 0, exit loop ; --------------------------------------------------------------------------- ; Append CR/LF to output stream and return lda #$0D ; Store CR sta TX_FIFO lda #$0A ; Store LF sta TX_FIFO rts ; Return .endproc </code></tscreen> <sect>Hello World! Example<p> The following short example demonstrates programming in C using the cc65 toolset with a custom run-time environment. In this example, a Xilinx FPGA contains a UART which is connected to a 65c02 processor with FIFO (<bf>F</bf>irst-<bf>I</bf>n, <bf>F</bf>irst-<bf>O</bf>ut) storage to buffer the data. The C program will wait for an interrupt generated by the receive UART and then respond by transmitting the string "Hello World! " every time a question mark character is received via a call to the hardware driver <tt>rs232_tx ()</tt>. The driver prototype uses the <tt>__fastcall__</tt> extension to indicate that the driver does not use the stack. The FIFO data interface is at address $1000 and is defined as the symbolic constant <tt>FIFO_DATA</tt>. Writing to <tt>FIFO_DATA</tt> transfers a byte of data into the transmit FIFO for subsequent transmission over the serial interface. Reading from <tt>FIFO_DATA</tt> transfers a byte of previously received data out of the receive FIFO. The FIFO status data is at address $1001 and is defined as the symbolic constant <tt>FIFO_STATUS</tt>. For convenience, the symbolic constants <tt>TX_FIFO_FULL</tt> (which isolates bit 0 of the register) and <tt>RX_FIFO_EMPTY</tt> (which isolates bit 1 of the register) have been defined to read the FIFO status. The following C code is saved in the file "main.c". As this example demonstrates, the run-time environment has been set up such that all of the behind-the-scene work is transparent to the user. <tscreen><code> #define FIFO_DATA (*(unsigned char *) 0x1000) #define FIFO_STATUS (*(unsigned char *) 0x1001) #define TX_FIFO_FULL (FIFO_STATUS & 0x01) #define RX_FIFO_EMPTY (FIFO_STATUS & 0x02) extern void wait (); extern void __fastcall__ rs232_tx (char *str); int main () { while (1) { // Run forever wait (); // Wait for an RX FIFO interrupt while (RX_FIFO_EMPTY == 0) { // While the RX FIFO is not empty if (FIFO_DATA == '?') { // Does the RX character = '?' rs232_tx ("Hello World!"); // Transmit "Hello World!" } // Discard any other RX characters } } return (0); // We should never get here! } </code></tscreen> <sect>Putting It All Together<p> The following commands will create a ROM image named "a.out" that can be used as the initialization data for the Xilinx Block RAM used for code storage: <tscreen><verb> $ cc65 -t none -O --cpu 65sc02 main.c $ ca65 --cpu 65sc02 main.s $ ca65 --cpu 65sc02 rs232_tx.s $ ca65 --cpu 65sc02 interrupt.s $ ca65 --cpu 65sc02 vectors.s $ ca65 --cpu 65sc02 wait.s $ ld65 -C sbc.cfg -m main.map interrupt.o vectors.o wait.o rs232_tx.o main.o sbc.lib </verb></tscreen> During the C-level code compilation phase (<tt>cc65</tt>), assumptions about the target system are disabled via the <tt>-t none</tt> command line option. During the object module linker phase (<tt>ld65</tt>), the target customization is enabled via inclusion of the <tt>sbc.lib</tt> file and the selection of the configuration file via the <tt>-C sbc.cfg</tt> command line option. The 65C02 core used most closely matches the cc65 toolset processor named 65SC02 (the 65C02 extensions without the bit manipulation instructions), so all the commands specify the use of that processor via the <tt>--cpu 65sc02</tt> option. </article>