C using CC65

From DenialWIKI
Revision as of 04:51, 12 September 2016 by Hawk (Talk | contribs)

(diff) ←Older revision | view current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

Introduction

This section is not intended to be a tutorial on how to program in C. There are many tutorials and books on C programming in the internet. This is intended to be information on how to use the freeware C compiler CC65 to write software for the VIC-20.

There are pros and cons associated with using C to program a device with as few resources as the VIC-20. For small tasks, the overhead of the compiled code and associated libraries is significant when compared to the smaller interpreted programs used by BASIC. However, for slightly larger programs, C offers a comprimise between the speed and efficiency of assembly language, and the size of BASIC. However, why you would choose to program in C is like asking "Why program for the VIC-20 at all?" And the only real answer that's needed is "Because I want to."

In many respects, programming in C for the VIC-20 can be considered programming an embedded system when compared with programming for a modern desktop PC. There are additional factors that programmers need to consider when targeting hardware that has severe limitations. In this respect, the articles and tutorials at sites like Embedded.com are more suited than sites that target how to program using Visual C++.

Overview

The CC65 homepage has information regarding the origins of the compiler and it's limitations. Suffice it to say that it's an "almost" ISO C compatible compiler, with many of the features missing from the implementation not appropriate for the target environment anyway. It is well supported by both it's developer, Ullrich von Bassewitz, and it's large user base across all the compatible target platforms. CC65 is more than just a C compiler. It is a suite of tools including a C compiler, assembler, linker and librarian for the 6502 family of microprocessors. Libraries have been written to support the various home computers and games consoles that utilize the 6502.

The CC65 is a cross compiler and is available to run on various host platforms. This tutorial will attempt to be as platform independent as possible regarding the host platform that the compiler is running on.

Linker Config File

The key to controlling how CC65 creates it's output file is the linker config file. The linker config file says where to compile the code to, where to allocate variables both static and dynamic. By changing the linker config file, a program can be made to run in an Unexpanded memory configuration, a 3K, 8K or even ROM image at $A000. (Although ROM images also need some changes to ensure that the ROM header is included.)

The default linker config that is included for the VIC-20 is for an unexpanded memory model. The default 1K stack size is likely to be HUGE compared with what's really needed.

#Obsolete - will not work with the latest version of CC65

MEMORY {
    ZP: start =  $0002, size = $001A, type = rw, define = yes;
    RAM: start = $0FFF, size = $0E01, define = yes, file = %O;
}
SEGMENTS {
    STARTUP:  load = RAM, type = ro;
    LOWCODE:  load = RAM, type = ro,               optional = yes;
    INIT:     load = RAM, type = ro, define = yes, optional = yes;
    CODE:     load = RAM, type = ro;
    RODATA:   load = RAM, type = ro;
    DATA:     load = RAM, type = rw;
    BSS:      load = RAM, type = bss, define = yes;
    HEAP:     load = RAM, type = bss, optional = yes; # must sit just below stack
    ZEROPAGE: load = ZP,  type = zp;
}
FEATURES {
    CONDES: segment = INIT,
	    type = constructor,
	    label = __CONSTRUCTOR_TABLE__,
	    count = __CONSTRUCTOR_COUNT__;
    CONDES: segment = RODATA,
	    type = destructor,
	    label = __DESTRUCTOR_TABLE__,
	    count = __DESTRUCTOR_COUNT__;
    CONDES: segment = RODATA,
	    type = interruptor,
	    label = __INTERRUPTOR_TABLE__,
	    count = __INTERRUPTOR_COUNT__;
}
SYMBOLS {
    __STACKSIZE__ = $400;	# 1K stack
}

The following is a Linker Config file that has been setup to generate a ROM image. Note that the start address doesn't make any allowance for the two byte load address of a .prg file. Also, all code and non-volatile data is generated to ROM memory. The ROM memory needs to have the fill flag set to yes to ensure that all of the contents of ROM will be padded out with zeros.

#Obsolete - will not work with the latest version of CC65

MEMORY {
    ZP: start =  $0002, size = $001A, type = rw, define = yes;
    RAM: start = $1200, size = $0E00, type = rw, define = yes;
    ROM: start = $A000, size = $2000, type = ro, define = yes, file = %O, fill = yes;
}
SEGMENTS {
    STARTUP:  load = ROM, type = ro;
    LOWCODE:  load = ROM, type = ro,               optional = yes;
    INIT:     load = ROM, type = ro, define = yes, optional = yes;
    CODE:     load = ROM, type = ro;
    RODATA:   load = ROM, type = ro;
    DATA:     load = ROM, run = RAM, type = rw, define = yes;
    BSS:      load = RAM, type = bss, define = yes;
    HEAP:     load = RAM, type = bss, optional = yes; # must sit just below stack
    ZEROPAGE: load = ZP,  type = zp;
}
FEATURES {
    CONDES: segment = INIT,
	    type = constructor,
	    label = __CONSTRUCTOR_TABLE__,
	    count = __CONSTRUCTOR_COUNT__;
    CONDES: segment = RODATA,
	    type = destructor,
	    label = __DESTRUCTOR_TABLE__,
	    count = __DESTRUCTOR_COUNT__;
    CONDES: segment = RODATA,
	    type = interruptor,
	    label = __INTERRUPTOR_TABLE__,
	    count = __INTERRUPTOR_COUNT__;
}
SYMBOLS {
    __STACKSIZE__ = $400;	# 1K stack
}

C Runtime File

Another important file used by CC65 is the runtime file. It contains the initial entry point of the program, with the startup code. The default startup code also includes the BASIC header code, with a single line with a SYS call that jumps to the entry point of the program. This allows the program to autostart when it is loaded, rather than having to manually perform the SYS call to the entry point. The runtime file is linked into the program at the location specified in the Linker Config file.

;Obsolete - will not work with the latest version of CC65

;
; Startup code for cc65 (Vic20 version)
;
; This must be the *first* file on the linker command line
;

	.export		_exit
.export         __STARTUP__ : absolute = 1      ; Mark as startup
	.import		initlib, donelib, callirq
       	.import	       	zerobss, push0
	.import	     	callmain
        .import         RESTOR, BSOUT, CLRCH
	.import	       	__INTERRUPTOR_COUNT__
     	.import		__RAM_START__, __RAM_SIZE__	; Linker generated

        .include        "zeropage.inc"
     	.include     	"vic20.inc"

; ------------------------------------------------------------------------
; Place the startup code in a special segment.

.segment       	"STARTUP"

; BASIC header with a SYS call

        .word   Head            ; Load address
Head:   .word   @Next
        .word   .version        ; Line number
        .byte   $9E             ; SYS token
        .byte   <(((@Start / 1000) .mod 10) + $30)
        .byte   <(((@Start /  100) .mod 10) + $30)
        .byte   <(((@Start /   10) .mod 10) + $30)
        .byte   <(((@Start /    1) .mod 10) + $30)
        .byte   $00             ; End of BASIC line
@Next:  .word   0               ; BASIC end marker
@Start:

; ------------------------------------------------------------------------
; Actual code

	ldx    	#zpspace-1
L1:	lda	sp,x
   	sta	zpsave,x	; Save the zero page locations we need
	dex
       	bpl	L1

; Close open files

	jsr	CLRCH

; Switch to second charset

	lda	#14
	jsr	BSOUT

; Clear the BSS data

	jsr	zerobss

; Save system stuff and setup the stack

       	tsx
       	stx    	spsave 		; Save the system stack ptr

	lda    	#<(__RAM_START__ + __RAM_SIZE__)
	sta	sp
	lda	#>(__RAM_START__ + __RAM_SIZE__)
       	sta	sp+1   		; Set argument stack ptr

; Call module constructors

	jsr	initlib

; If we have IRQ functions, chain our stub into the IRQ vector

        lda     #<__INTERRUPTOR_COUNT__
      	beq	NoIRQ1
      	lda	IRQVec
       	ldx	IRQVec+1
      	sta	IRQInd+1
      	stx	IRQInd+2
      	lda	#<IRQStub
      	ldx	#>IRQStub
      	sei
      	sta	IRQVec
      	stx	IRQVec+1
      	cli

; Push arguments and call main()

NoIRQ1: jsr     callmain

; Back from main (This is also the _exit entry). Reset the IRQ vector if we
; chained it.

_exit: 	pha  			; Save the return code on stack
	lda     #<__INTERRUPTOR_COUNT__
	beq	NoIRQ2
	lda	IRQInd+1
	ldx	IRQInd+2
	sei
	sta	IRQVec
	stx	IRQVec+1
	cli

; Run module destructors

NoIRQ2: jsr	donelib

; Copy back the zero page stuff

       	ldx	#zpspace-1
L2:	lda	zpsave,x
	sta	sp,x
	dex
       	bpl	L2

; Place the program return code into ST

	pla
	sta	ST

; Restore the stack pointer

  	ldx	spsave
	txs

; Reset changed vectors, back to basic

	jmp	RESTOR


; ------------------------------------------------------------------------
; The IRQ vector jumps here, if condes routines are defined with type 2.

IRQStub:
	cld    	       		   	; Just to be sure
       	jsr    	callirq                 ; Call the functions
       	jmp    	IRQInd			; Jump to the saved IRQ vector

; ------------------------------------------------------------------------
; Data

.data

zpsave:	.res	zpspace
IRQInd: jmp     $0000

.bss

spsave:	.res	1

The above crt0 file does some other intersting things during startup. It changes the character set to Mixed Case, rather than Upper + Graphics. It also includes cleanup for when the main() routine returns. If you never intend your main() routine to exit then this code could be eliminated, and a few bytes of memory saved.

The following modification to the above crt0 file demonstartes how the startup code can be modified to suit the generation of a ROM file.

If the first "Load address" is included in the output, then the file will be in the form of a .prg file suitable for loading into memory. If this line is commented out (and the memory location of the STARTUP segment changed by the same 2 bytes) then the file generated is suitable for attaching as a ROM file in an emulator, or burning to an EEPROM.

; ------------------------------------------------------------------------
; Place the startup code in a special segment.

.segment        "STARTUP"

;       .word   Head            ; Load address
Head:   .word   start
        .word   restore
        .byte   $41,$30,$C3,$C2,$CD
restore:
        jsr     IUDTIM          ; Update time
        jsr     STOP            ; Scan STOP key
start:	jsr     RAMTAS          ; Initialise System Constants
	jsr     RESTOR          ; Restore Kernal vectors
	jsr     IOINIT          ; Initialise IO
	jsr     CINT

; ------------------------------------------------------------------------

External Links

cc65 Wiki

Embedded.com