Thursday, May 25, 2017

Building a 68000 Single Board Computer - C Compiling Example

I earlier did some experimenting with using gcc to cross-compile C code for the embedded TS2 board. I recently spent a few hours making a longer example, and hooking it up to some of the TUTOR ROM routines.

I earlier built the GNU assembler, and while I was at it I also built the gcc compiler.

In this "bare metal" setup, there is no C run-time library so you can't call routines like printf() to do output. You can't even write code that might require external routines to do things that the 68000 can't do directly, like multiplying 32-bit integers.

My example code can be found here and includes a make file.

The basic steps are to first compile for the 68000 and no standard library. I used this command line:

m68k-elf-gcc -Wall -nostdlib -nodefaultlibs -m68000 -c demo.c

This produces an object file, demo.o. Next, you can link it. I used linker options to specify the addresses for the different sections that would work on my TS2 board. I also found I had to define the symbol _start to point to main to avoid a warning. This was the command line I used:

m68k-elf-ld --defsym=_start=main -Ttext=0x2000 -Tdata=0x3000 -Tbss=0x4000 --section-start=.rodata=0x5000 demo.o

This produces an ELF format excecutable called a.out. You can then generate an S record or Motorola hex file for downloading using a command like this:

m68k-elf-objcopy -I coff-m68k -O srec a.out

If you wanted to see the assembler output of the C compiler, we could have compiled with the -S option, as below, to produce a file demo.s:

m68k-elf-gcc -Wall -nostdlib -nodefaultlibs -m68000 -S demo.c

Given the S record file, we can directly download it to the TS2 using the TUTOR monitor's LO command, and then execute it.

To be able to produce output, I wrote some simple code to call TUTOR's firmware routines, which are accessed by a TRAP #14 instruction.

For example, to pass control to the TUTOR monitor you call function 228, by passing this value in register D7 and calling TRAP #14. Here is the code to do it in C using some in-line assembler code:

// Go to the TUTOR monitor using trap 14 function. Does not return.
void tutor() {
    asm("move.b #228,%d7\n\t"
        "trap #14");

To do output to the console I wrote a short routine that takes a character and calls TUTOR's OUTCH call which writes a character to the console. The code to do it is as follows:

// Print a character using the TUTOR monitor trap function.
void outch(char c) {
    asm("movem.l %d0/%d1/%a0,-(%sp)\n\t"  // Save modified registers
        "move.b #248,%d7\n\t"             // OUTCH trap function code
        "trap #14\n\t"                    // Call TUTOR function
        "movem.l (%sp)+,%d0/%d1/%a0");    // Restore registers

The code could be improved and made more robust, for example not
relying on the fact that gcc puts the passed parameter in register D0. But this worked well for a quick demo. I then wrote a routine in C to print a string by calling outch() for each character in the string:

// Print a string.
void printString(const char *s) {
    while (*s != 0) {

I wrote another routine in C to print a number in decimal. Both this and the previous routine could be implemented more efficiently by calling routines in TUTOR that can already do this.

The example program prints numbers from 1 to 7 along with their squares, value to the fourth power, and factorial. I stopped at 7 because I needed to use 16-bit short integers to avoid the need for run-time math routines and 7 factorial was the largest value that would fit in 16-bits.

Here is the output when run from the monitor:

TUTOR  1.3 > GO 2000
n  n^2  n^4  n!
1 1 1 1
2 4 8 2
3 9 27 6
4 16 64 24
5 25 125 120
6 36 216 720
7 49 343 5040

At some point in the future I might implement more routines, and then try building some larger applications like an interactive text adventure game that I wrote some time ago.

An update: By linking to libgcc, the compiled code can do 32-bit integer math and support the C "int" type. It can even do floating point math using the soft float support (although that make the code quickly become quite large). I've updated the code and make file accordingly. There is still no C run-time library for routines like printf(), but I am looking at some options for this that I will describe in a future blog post.


Just4Fun said...

Hi, thanks a lot for your explanations here!. It is the only place that I've found with a *real* explanation on how to adapt the m68k-elf-gcc cross-compiler to a custom HW. I've been able to run your demo.c on my custom 68008 board, with the needed modifications. Next step will be the newlib library.

Just a question... What about the various crt0.s, crti.s, crtbegin.o, crtend.o , crtn.s files, are they not really needed (It seems that you use the m68k-elf-gcc in a "bare-metal" mode, without "wrapping" the user code to these files)?


Just4Fun said...

Auto-answer after playing with your files a for a while...

The crt0.s stuff is not used in the first "bare-metal" compiling example here, and this is obvious as at this stage there can't be any library and any kind of customization for a given HW.

After the compilation/generation of the customized newlib, three library files are created that are the needed libraries with the right calls for a specific HW (the TS2 board in your case): libm.a (the math library), libc.a (the newlib output) and libg.a (the newlib with debug stuff).

More it is created the libts2.a with the specific HW routines for the previous libs.

And what about the crt0.s, crti.s, crtbegin.o, crtend.o files?
Well, in the ts2.ld script you have "hard-coded" the content of both crti.s and crtn.s files, so they are "inside" the linking process, and there is no need to add them as obj files.
The remaining crtbegin.o, crtend.o obj files should be linked as default having now the libc etc. when the gcc is invoked (-nostdlib -nodefaultlibs not needed/used anymore after libc generation...).