Skip to content

Writing a Standalone C Program for ByteCradle with CC65

This tutorial walks through the process of building a standalone C program for the ByteCradle platform using the cc65 toolchain. You'll learn how to set up the startup code, write a simple main() function, configure memory mapping, and compile the final binary.

Tip

You can find several example programs in the official repository.

Prerequisites

  • A Linux system with the cc65 toolchain installed (ca65, cc65, ld65, ar65)
  • Basic familiarity with 6502 architecture and C programming
  • Make and a standard build environment

Tip

More information on the toolchain is provided here.

Project Structure

The project consists of the following files:

File Description
crt0.s Startup code (called before main())
main.c Your main application in C
io.h Helper for system-level I/O routines
prog.cfg Memory layout and segment configuration for the linker
Makefile Automates the build process

Step 1: Write the Startup Code

The startup assembly file initializes the stack, clears the BSS section, copies initialized data, and finally calls main(). It also defines an _exit routine for cleanup.

; ---------------------------------------------------------------------------
; crt0.s
; ---------------------------------------------------------------------------

.import __HEADER_LOAD__                     ; import start location

.export   _init, _exit
.import   _main, _charout

.export   __STARTUP__ : absolute = 1        ; Mark as startup
.import   __RAM_START__, __RAM_SIZE__       ; Linker generated

.import    copydata, zerobss, initlib, donelib

.include  "zeropage.inc"

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

.segment "HEADER"

.word __HEADER_LOAD__

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

.segment  "STARTUP"

; ---------------------------------------------------------------------------
; Set cc65 argument stack pointer
; ---------------------------------------------------------------------------
_init:

          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
          RTS

Step 2: Create I/O Header

This header declares a function pointer to the system's ROM-based string printing routine at address $FFE8. This lets us call system functions from C.

#ifndef _IO_H
#define _IO_H

#include <stdint.h>

// ByteCradle OS: Print null-terminated string with newline (ROM $FFE8)
void (*putstrnl)(const uint8_t*) = (void (*)(const uint8_t*))0xFFE8;

#endif // _IO_H

Step 3: Write the Main C Program

This is a simple "Hello World" program that prints to the screen using putstrnl.

#include "io.h"

int main() {
    putstrnl("Hello World!");
    return 0;
}

Step 4: Configure Memory Layout

The linker configuration (prog.cfg) defines where various sections of the program go in memory, including a 2-byte header, zero page usage, and stack space.

MEMORY {
    ZP:     start = $50, size = $B0, type = rw, define = yes;
    HEADER: start = $0800, size = 2, file = %O;
    RAM:    start = $0802, size = $7700-$0802, type = rw, define = yes;
    STACK:  start = $7700, size = $0800, type = rw, define = yes;
}

SEGMENTS {
    ZEROPAGE: load = ZP,        type = zp,  define   = yes;
    HEADER:   load = HEADER,    type = ro,  define   = yes;
    STARTUP:  load = RAM,       type = ro;
    CODE:     load = RAM,       type = ro;
    ONCE:     load = RAM,       type = ro,  optional = yes;
    RODATA:   load = RAM,       type = ro;
    DATA:     load = RAM,       type = rw,  define   = yes, run = RAM;
    BSS:      load = RAM,       type = bss, define   = yes;
    HEAP:     load = RAM,       type = bss, optional = yes;
}

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;
}

Step 5: Create the Makefile

The Makefile automates the build by assembling and linking your C and assembly files into a .COM binary that can be run on ByteCradle.

PROG = HELLOC.COM

all: $(PROG)

$(PROG): prog.cfg crt0.o main.o prog.lib
    ar65 a prog.lib crt0.o
    ld65 -C prog.cfg -m main.map main.o -o $(PROG) prog.lib

crt0.o: crt0.s
    ca65 --cpu 65c02 crt0.s

main.s: main.c
    cc65 -t none -O --cpu 65c02 main.c

prog.lib: crt0.o
    cp -v /usr/share/cc65/lib/supervision.lib prog.lib 
    ar65 a prog.lib crt0.o

main.o: main.s
    ca65 --cpu 65c02 main.s

clean:
    rm -v *.o *.lib main.s $(PROG)

Step 6: Build the Program

From the terminal, run:

make

Step 7: Run the program

Copy the resulting HELLOC.COM file to the ByteCradle's SD card or upload it using the appropriate method. Then, on the ByteCradle shell:

:/> helloc
Hello World!
:/>

Tip

Instructions on how to deploy the program to your ByteCradle board, including for the TinyROM which does not have an SD-CARD, are provided here