Running D on bare metal ARM
By combining the GDC ARM compiler with the stripped runtime concept we used in Chapter 11, D for Kernel Coding, we can also run D on a bare metal ARM board. Going from this concept to a working program will take a lot of work, but we will get started with a "hello world" program.
Getting ready
Get an emulator such as QEMU and the GDC cross-compiler targeting ARM. The same cross-compiler for Raspberry Pi that we used previously will work here too. Also, copy the minimal object.d
from Chapter 11, D for Kernel Coding, to a new folder for the ARM project.
How to do it…
To run D on bare metal ARM, we need to execute the following steps:
Create a
startup.s
file that sets up the stack, calls the D function, and then endlessly loops. The code is as follows:.global _start _start: LDR sp, =stack_top @ prepare a stack BL _d_run_main @ call our D entry point B . @ endlessly loop
Create a linker script,
linker.ld
, which puts the startup code at the beginning of the file and reserves stack space. The code is as follows:ENTRY(_start) SECTIONS { . = 0x10000; .startup . : { startup.o(.text) } .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss COMMON) } . = ALIGN(8); . = . + 0x1000; /* 4kB of stack memory */ stack_top = .; }
Modify
object.d
to add an ARM version which calls_Dmain
and loops with regular D code instead of inline assembly. The code is as follows:void _d_run_main() { version(ARM) { _Dmain(); while(true) {} } }
Write a
hello.d
file that says hello by writing to the serial port's memory address. The code is as follows:enum serialPort = cast(shared(uint)*) 0x101f1000; void printToSerialPort(string s) { foreach(c; s) *serialPort = c; } void main() { printToSerialPort("Hello world from D!\n"); }
Create a
Makefile
that assemblesstartup.s
, compilesobject.d
andhello.d
with exceptions disabled and the release mode flag turned on, links it together with the linker script, and then converts the output to a binary image. The code is as follows:all: arm-gdcproject-linux-gnueabi-as -mcpu=arm926ej-s -g startup.s -o startup.o arm-gdcproject-linux-gnueabi-gdc -frelease -fno-exceptions -c -mcpu=arm926ej-s -g object.d -o object.o arm-gdcproject-linux-gnueabi-gdc -frelease -fno-exceptions -c -mcpu=arm926ej-s -g hello.d -o hello.o arm-gdcproject-linux-gnueabi-ld -T linker.ld hello.o startup.o object.o -o hello.elf arm-gdcproject-linux-gnueabi-objcopy -O binary hello.elf hello.bin
Run the program using
qemu-system-arm -M versatilepb -m 128M -kernel hello.bin –sdl
.
In the serial console view of QEMU (press Ctrl + Alt + 3 to switch to it), you'll see the "hello world" message.
How it works…
Bare metal code for ARM computers follows the same basic principles as for x86 PCs, and the D code to get you started is substantially similar. The biggest difference is the use of the GDC compiler instead of dmd
, because GDC can generate code for ARM targets and dmd
cannot.
GDC also does not support inline assembly syntax of dmd
or naked
functions (though the gcc
backend does support naked
functions for ARM targets, so support for that may be added later). Instead, GDC supports inline assembly based on the gcc
syntax, which uses strings of assembly code with input, output, and destroyed register specifiers.
Other significant differences between dmd
and GDC
will only become apparent if exceptions are enabled, and since we disabled them for the minimal runtime, we didn't have to worry about that here.
Of course, x86 and ARM hardware is significantly different in regards to details such as memory mapped addresses, but the principle is the same: use a pointer to the correct address to communicate. Here, we used a pointer to the UART0 serial port and wrote our string to it, saying hello.
See also
http://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html explains the inline assembler syntax of
gcc
, which is also used by GDC.