Table of Contents

Get the sourcecode

There is currently no binary package of VBCC available. This will change when a new official release is made, or when it is working well enough for a beta release from me.

The sources are in several parts:

It is also a good idea to download and read the manuals for each of them.

Compiling

VASM

make CPU=unsp SYNTAX=std

Copy vasmunsp_std and vobjdump somewhere in your PATH

make

Copy vlink executable in your PATH

VBCC

make TARGET=unsp

The first time this asks you various questions about the host system. If you're not on a strange platform, the default answers should be correct.

Copy vc and vbccunsp executables from the bin/ directory in your PATH

Target configuration

The vc executable is a compiler frontend. It does not itself compile the C code, but it knows how to call the compiler, assembler and linker to perform the usual steps of a compilation.

However, this knowledge is not hardcoded in the tool, instead, it is loaded from a configuration file. This way, a single version of the compiler frontend can be used to work with many different CPU architectures and platforms.

Create a vbcc directory somewhere. When building anything with VBCC, you need to set the VBCC environment variable to point to that directory:

export VBCC=/path/to/vbccdir

The content of this directory:

The content of each file or how to generate them is detailed below.

The configuration file

This file located in config/vsmile is the entry point for the configuration. It is enabled by passing the +vsmile option to the vc compiler frontend.

-cc=vbccunsp -I"$VBCC"/targets/unsp-vsmile/include -quiet %s -o= %s %s -O=%ld
-ccv=vbccunsp -I"$VBCC"/targets/unsp-vsmile/include %s -o= %s %s -O=%ld
-as=vasmunsp_std -quiet -ile -Fvobj %s -o %s
-asv=vasmunsp_std -ile -Fvobj %s -o %s
-rm=rm %s
-rmv=rm %s
-ld=vlink -ole -b rawbin1 -Cvbcc -T"$VBCC"/targets/unsp-vsmile/vlink.cmd -L"$VBCC"/targets/unsp-vsmile/lib "$VBCC"/targets/unsp-vsmile/lib/startup.o %s %s -o %s -lvc
-ldv=vlink -ole -b rawbin1 -Cvbcc -T"$VBCC"/targets/unsp-vsmile/vlink.cmd -L"$VBCC"/targets/unsp-vsmile/lib "$VBCC"/targets/unsp-vsmile/lib/startup.o %s %s -o %s -lvc -Mmapfile
-l2=vlink -ole -b rawbin1 -Cvbcc -T"$VBCC"/targets/unsp-vsmile/vlink.cmd -L"$VBCC"/targets/unsp-vsmile/lib %s %s -o %s
-l2v=vlink -ole -b rawbin1 -Cvbcc -T"$VBCC"/targets/unsp-vsmile/vlink.cmd -L"$VBCC"/targets/unsp-vsmile/lib %s %s -o %s -Mmapfile

This defines the commands to run for each step of the compilation: cc for compiling, as for assembling, and ld for linking. The variants with a v suffix are for verbose output.

Some of the paths used refer to the VBCC environment variable we have set before.

For the details of each option used, refer to the documentation of the corresponding tool, but in short:

The frontend looks at the file extension of the input and output, as well as some of the options (like -c, to compile and generate a .o file, but not link), and automatically determines which tools to call.

The linker script

The file vlink.cmd contains the linker script. This tells vlink about the memory layout and what to do with the code and data.

MEMORY
{
        ram : org = 0x0000, len = 0x2800
        lorom : org = 0x0000, len = 0xfff4
        res : org = 0xfff5, len = 11
        rom : org = 0x10000, len = 0x3f0000
}
 
SECTIONS {
        .bss (NOLOAD): { *(.bss) } > ram
 
        .empty: { RESERVE(0x4000); } > lorom
        .rodatal: { *(.rodata) } > lorom
        .rodata: { *(.rodata) *(.rodata2) } > rom
        .textl: { *(.text) } > lorom
        .text: { *(.text) } > rom
        .ctorsl: { *(.ctors) } > lorom
        .ctors: { *(.ctors) } > rom
        .dtorsl: { *(.dtors) } > lorom
        .dtors: { *(.dtors) } > rom
        .data: { *(.data) } > ram AT > lorom
 
        .res: { *(.res); } > res
 
        __BS = ADDR(.bss);
        __BL = SIZEOF(.bss);
        __DS = ADDR(.data);
        __DD = LOADADDR(.data);
        __DL = SIZEOF(.data);
 
        __STACK = 0x2800;
}
 
ENTRY(_vectortable)

First of all this script defines several memory regions:

Note that the “low ROM” section starts at 0 and overlaps the RAM. This is unusual, but it is how V.Smile cartridges are done: they have 4000 words at the start which are unused and unreachable by the CPU.

The script then defines sections. These follow the usual conventions of C compilers:

Finally, the script defines a few variables that will be usable from assembler code, to indicate the start and size of the bss section and of the data section as well as the stack pointer initial value. These are also used by the startup file.

And the last line defines the vector table as the “entry point”. This allows the linker to follow references from this entry point to other parts of the code (functions, variables, …) and determine what is actually used. Functions that are not called from anywhere can be removed at this stage, for example.

FIXME this linker script needs some changes to allow developers to more easily put some things explicitly in the “low ROM” section. For example, reset vectors have to be there. Also check how alignment works for the things that need it.

The startup file

; TODO move this font out of here into the projects that use it.
.section .rodata
.globl _RES_FONT_BIN_SA
_RES_FONT_BIN_SA:
.incbin font.bin
 
.section .text
 
; Default handler for all interrupts (except RESET).
; Just return from the interupt without doing anything.
; Defined as weak symbols, so, if an application redefines them, the version from the app will
; be used instead.
.weak BREAK,FIQ,IRQ0,IRQ1,IRQ2,IRQ3,IRQ4,IRQ5,IRQ6,IRQ7
 
BREAK:
FIQ:
IRQ0:
IRQ1:
IRQ2:
; IRQ3:
IRQ4:
; IRQ5:
IRQ6:
IRQ7:
        RETI
 
; Handler for the RESET vector.
; Does the early initialization, then jumps into the main function.
 
_start:
        ; Disable interrupts since we're probably not ready to handle them yet
        IRQ OFF
        ; Set up the stack at the end of RAM
        LD SP, 0x27FF
 
        ; Copy the initialized variables into RAM
        ; __DL = length of variables section
        ; __DS = start of variables section in RAM
        ; __DD = start of variables section in ROM
        LD R1, __DS
        LD R2, __DD
        LD R3, __DL
        JZ gomain
        ADD R3, R1
 
_startloop:
        LD R4, (R2++)
        ST R4, (R1++)
        CMP R3, R1
        JNE _startloop
 
        ; Finally, jump into the main function
gomain:
        GOTO main
 
; Handlers for indirect calls. Used by VBCC to handle function pointers, because unSP does not have
; an indirect call (CALL R1 or similar) operation and it is not trivial to emulate one.
.globl __indirect_R1
.globl __indirect_R2
.globl __indirect_R3
.globl __indirect_R4
__indirect_R1:
        LD PC,R1
__indirect_R2:
        LD PC,R2
__indirect_R3:
        LD PC,R3
__indirect_R4:
        LD PC,R4
 
; The interrupt vectors section, contains pointers to the interrupt handlers
.section .res,"adr"
_vectortable:
.globl _vectortable
.size _vectortable, 11
.2byte BREAK
.2byte FIQ
.2byte _start
.2byte IRQ0
.2byte IRQ1
.2byte IRQ2
.2byte IRQ3
.2byte IRQ4
.2byte IRQ5
.2byte IRQ6
.2byte IRQ7

This file defines the initialization routine and a few other things.

The first thing it currently does is including the font binary file used for rendering text.

Then come default definitions for most of the interrupt vectors. They all execute a RETI instruction without doing anything. Currently, the ones used by the application need to be commented out here. Later, this should be replaced with weak symbols that can be replaced if the application provides something else.

The _start function is the reset vector, and so it is the first code that will run when the CPU resets. It turns off the interrupts, initializes the stack pointer, and copies the initial values for variables in the data section into RAM. Then, it jumps into the main function.

Finally, there is the table of reset vectors.

Of course, vbcc will expect this in vobj (.o) format, so let's assemble it:

vasmunsp_std -ile -Fvobj -o startup.o crt0.asm

and place the resulting file startup.o in the target/unsp-vsmile/lib/ directory, where our frontend config file says to look for it.

Note: the -ile option tells vasm that files included using the incbin directive are in little endian. This gives the same results as the official unSP toolchain when importing binary files.

The C library

vbcc does not come with an open source C library. A full one from other sources could be compiled, but we don't need a complete C library.

So here is a very minimal one with enough support to run the compiler and to run Contiki, which has minimal needs from the C library (just a few string functions).

/*
 * Copyright (C) 2024 Adrien Destugues <pulkomandy@pulkomandy.tk>
 *
 * Distributed under terms of the MIT license.
 */
 
int strlen(const char* str)
{
        int i = 0;
        while(*str++) i++;
        return i;
}
 
int isprint(char c)
{
        return c >= 32;
}
 
void strcpy(const char* src, char* dst)
{
        char c;
        while (c = *src++)
                *dst++ = c;
        *dst = 0;
}
 
void memset(int* s, int v, int n)
{
        while(--n >= 0)
                *s++ = v;
}
 
int strncmp(const char* a, const char* b, int n)
{
        while(--n >= 0) {
                if (*a != *b)
                        return *a - *b;
                if (*a == 0)
                        return *b;
                if (*b == 0)
                        return -*a;
        }
}
 
int __div(int a, int b)
{
        int c = 0;
        while (a >= b) {
                a -= b;
                c++;
        }
        return c;
}
 
int __mod(int a, int b)
{
        while (a >= b) {
                a -= b;
        }
        return a;
}

To generate the libc.a file:

vc +vsmile -nostdlib -O3 -c libc.c
ar cru libvc.a libc.o

We also need some include files. ctype.h and stdlib.h can be empty, but they are used by Contiki so they must exist.

string.h has some basic functions declarations and type definitions:

#ifndef __STRING_H
#define __STRING_H 1
 
/*
  Adapt according to stddef.h.
*/
#ifndef __SIZE_T
#define __SIZE_T 1
typedef unsigned int size_t;
#endif
 
#undef NULL
#define NULL ((void *)0)
 
/*
  Many of these functions should perhaps be implemented as
  inline-assembly or assembly-functions.
 
  Most suitable are:
  - memcpy
  - strcpy
  - strlen
  - strcmp
  - strcat
*/
void *memcpy(void *,const void *,size_t n);
void *memmove(void *,const void *,size_t);
void *memset(void *,int,size_t);
int memcmp(const void *,const void *,size_t);
void *memchr(const void *,int,size_t);
char *strcat(char *,const char *);
char *strncat(char *,const char *,size_t);
char *strchr(const char *,int);
size_t strcspn(const char *,const char *);
char *strpbrk(const char *,const char *);
char *strrchr(const char *,int);
size_t strspn(const char *,const char *);
char *strstr(const char *,const char *);
char *strtok(char *,const char *);
char *strerror(int);
size_t strlen(const char *);
char *strcpy(char *,const char *);
char *strncpy(char *,const char *,size_t);
int strcmp(const char *,const char *);
int strncmp(const char *,const char *,size_t);
int strcoll(const char *,const char *);
size_t strxfrm(char *,const char *,size_t);
 
#endif

Running the compiler

Note: make sure you have set the VBCC environment variable as explained earlier, otherwise the compiler will not find the target configuration and will complain and not compile anything.

To compile a .c file into a .o file:

vc +vsmile -c file.c -o

To generate a binary file from .o files:

vc +vsmile -ole -o cartridge.bin *.o

And of course you can run the resulting cartridge in MAME if you want:

mame -debug -debugger qt -rompath /path/to/vsmile/bios/ vsmile -cart ./cartridge.bin -nomax

Generating debug symbols for MAME

Use the -v option to vbcc to enable verbose mode, this will also generate a mapfile containing info about the generated binary.

The mapfile list of symbols can be turned into a file that the MAME debugger can use to create comments:

sed mapfile -ne 's!  0x\(.*\) \(.*\):.*!comadd \1,\2!p' > symbols.mame

Then in MAME, use “source symbols.mame” to load it. You can switch the disassembly view to show comments, and it will be very helpful to understand where you are when stepping through the code. Even local labels are exported, which can be matched with .ic2 files to compare the intermediate representation with the generated code. Very useful when debugging the compiler.

TODO list

Bugs

Memory addressing

Startup and library

Optimizations

POP  R1, R1, [SP]
PUSH R1, R1, [SP]
LD R1, ...

The POP and PUSH can be eliminated, they are useless. This should be done using the peephole optimizer since the register spilling is generated by the backend and does not come from ICs.

LD R1, ...
CMP R1, 0
Move pointer to register
Add 1 to register
Store to pointer-register

(there are a few variants for pre and post increments, and some other ICs may be inserted in between, making this not so easy to track).