Skip to content

Instantly share code, notes, and snippets.

@kassane
Last active December 7, 2023 12:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kassane/523e03afcbd706eff88b7131d327d933 to your computer and use it in GitHub Desktop.
Save kassane/523e03afcbd706eff88b7131d327d933 to your computer and use it in GitHub Desktop.
Running D language with Zephyr

Development environment

  • LDC
  • lld
  • qemu
  • ovmf(Open Virtual Machine Firmware)

Also, although it is not required, I use just as the command runner, just like the reference source. Make is fine, but I felt like I could understand the FAQ's philosophy, so I tried using it.

UEFI target

This time I posted everything to ldc2.conf. Switches are the best when it comes to cross-compiling.

default:
{
    switches = [
        "-mtriple=x86_64-unknown-windows",
        "--linker=lld-link",
        "-L/nodefaultlib",
        "-L/subsystem:EFI_APPLICATION",
        "-L/entry:efi_main",
        "--betterC",
        "--boundscheck=off",
        "--defaultlib=",
        "--debuglib=",
        "--platformlib=",
        "-mattr=-mmx,-sse,+soft-float",
        "-disable-red-zone",
        "-relocation-model=static",
        "-code-model=large"
    ];
    post-switches = [
        "-I%%ldcbinarypath%%/../import",
    ];
    lib-dirs=[];
}

In the original version, there are various references to OS memory allocation and libc functions, but here we use things -betterC like to --defaultlib='' to eliminate dependence on the runtime, so this is not a particular problem.

UEFI bootloader

The UEFI-related definitions are as follows. There's not much to see here extern(C). The only difference is that it is defined at the top level.

module uefi;

extern (C):

alias EfiStatus = uint;
alias EfiHandle = void*;

struct EfiTableHeader
{
	ulong signature;
	uint revision;
	uint headerSize;
	uint crc32;
	uint reserved;
}

struct EfiSystemTable
{
	EfiTableHeader header;
	wchar* firmwareVendor;
	uint firmwareRevision;
	EfiHandle consoleInHandle;
	void* conIn;
	EfiHandle consoleOutHandle;
	SimpleTextOutput* conOut;
	EfiHandle standardErrorHandle;
	void* stdErr;
	void* runtimeServices;
	void* bootServices;
	uint numTableEntries;
	void* configTable;
}

struct SimpleTextOutput
{
	void* reset;
	EfiStatus function(SimpleTextOutput*, wchar*) outputString;
	void* testString;
	void* queryMode;
	EfiStatus function(SimpleTextOutput*, uint) setMode;
	void* setAttribute;
	EfiStatus function(SimpleTextOutput*) clearScreen;
	void* setCursorPos;
	void* enableCursor;
	void** mode;
}

enum : EfiStatus
{
	EfiSuccess = 0,
	EfiLoadError = 1
}

The main code looks like this.

Being able to write naked function and use inline assembly is an essential requirement for a system programming language.

output String requires a null-terminated UTF-16 string, but the D language has a built-in UTF-16 string type wstring, so this is easy \0. Be careful to include at the end wchar* and cast to to use it.

import ldc.attributes : naked;
import ldc.llvmasm;

import uefi;

extern (C):

@naked void exit(int status)
{
	__asm(`
	.loop:
		cli
		hlt
		jmp .loop
	`, "");
}

void d_main() {}

EfiStatus efi_main(EfiHandle imgHandle, EfiSystemTable* sysTable)
{
	d_main();
	wchar* msg = cast(wchar*) "Hello, World\0"w.ptr;
	sysTable.conOut.clearScreen(sysTable.conOut);
	sysTable.conOut.outputString(sysTable.conOut, msg);
	exit(0);
	return EfiLoadError;
}

This completes Hello, World! It's easy!

image

Reference

D language application method on microcontroller (ARM Cortex-M)

Goals

Try to use Dlang language to write a microcontroller program. The target chip is STM32f401cc. The cortex-m4 HOST host is a windows system.

List of advantages

D language is a programming language without "language religion", similar to the design of C/C++. There are no order restrictions on function writing, no non-debuggable definitions, and no complex header files.

No need to build a complex cross-compilation environment when using the LDC compiler

Programming on a microcontroller mainly uses a subset of the D language BetterC. This subset includes the implementation of most of the D language. The following functions are not implemented:

Serial Number Function Comment
1 Memory GC In terms of microcontroller programming, this part still requires programmers to consider trade-offs.
2 structural information TypeInfo and ModuleInfo , the main application of this part is GC, and there are some shared library applications
3 Class ---
4 Built-in threads ---
5 dynamic array ---
6 Exceptions ---
7 Built-in synchronization function ---
8 Static constructors and destructors ---

Of course, you can also use the complete D language, but the cost is higher (FLASH/RAM). Generally speaking, the running state of the program developed by the microcontroller can be inferred. From an efficiency perspective, it is not recommended to use GC management or dynamic loading.

Embedded development of Linux will not be discussed in this article. Environmental preparation

First we determine what we need:

Serial Number Function Currently available
1 IDE Vscode is recommended, plus the webfreak.dlang-bundle plug-in and marus25.cortex-debug debugging plug- in used by dlang
2 translater LDC, implemented through LLVM, can directly download the binary version without the need for RT libraries.
2.1 Connector lld, it is recommended to use the LDC built-in directly
2.2 Miscellaneous tools LLVM, including many tools that you are not used to, can also be used with arm-none-eabi
3 Build tools This is not available for the time being. Let’s write a batch process to get it done 😀
4 debugger GDB, in theory it can support the gdb protocol, but I haven’t found a better solution yet.
5 Simulation qemu-arm, this simulator can simulate multiple series of stm32. There is a problem with the floating point part of the hardware, and errors will occur during simulation.

Specific implementation methods source code

Entrypoint

module start;
/*
  filename: start.d
*/

version(LDC) {
  import ldc.llvmasm;
}

alias void function() ISR;
extern(C) immutable ISR ResetHandler = &OnReset;

void SendCommand(int command, void* message) {
  __asm(
    "mov r0, $0;
    mov r1, $1;
    bkpt #0xAB"
    command, message
  );
}

void OnReset(){
  while(true){
    // Creating semihosting message
    uint[3] message = [
      2,                             // stderr
      cast(uint) "hello\r\n".ptr,   // ptr to string
      7                            // size of string
    ];
    
    // Creating semihosting message
    SendCommand(0x05, &message);
  }
}

Let’s talk about some limitations in the code. These limitations are mainly caused by the implementation of TLS and the fact that this part of the function is not implemented in the library that comes with ldc. The relationship between D language function declaration and C between D language function declaration and C++ Relationship

Definition SECTIONS region
__gshared int t .bss
__gshared int t=0x33 .data
shared int t .data
shared int t=0x33 .data
shared int t .bss
static int t .bss
static int t=0x33 .data
int t .tbss
int t=0x33 .tdata

The trouble encountered here mainly comes from the TLS part, which lacks implementation functions and bugs in the LLD part of LLVM (the binary file output after using TLS is very huge ) . It is recommended not to use ABI's TLS and use emulated-tls instead.

However, there is no need for TLS on a " streaking " microcontroller. If you are using an embedded system, it is recommended to implement the emulated-tls method.

Linker script

/*
  filename: ldscript.ld
*/
MEMORY
{
    FLASH (RX): ORIGIN = 0X08000000, LENGTH = 256K
    SRAM (WXAR): ORIGIN = 0X020000000, LENGTH = 64K
}

_stackStart = ORIGIN(CCRAM) + LENGTH(CCRAM);

SECTION
{
/*
  We don't need exceptions, and discarding these sections prevents linker errors with LDC
*/
    /DISCARD/ :
    {
      *(.ARM.extab*)
      *(.ARM.exidx*)
    }
    
    .text:
    {
      LONG(_stackStart);              // Initial stack pointer
      KEEP(start.o(*.ResetHandler)); // Interrupt vector table (entrypoint)
      
      // text code
      *(.text)
      *(.text*)
      
      // for "hello\r\n" string
      . = ALIGN(4);
      *(.rodata)
      *(.rodata*)      
    }>FLASH
  /*
    Need .data, .bss, .ctors and probably more as program becomes
    More complex
  */
}

This connection script does not implement SECTIONS for memory operations, it is just for this demonstration!!! I will post a linker script with comments later.

Compilation directives

The first heart-pounding moment has come,

ldc2 --conf= --mtriple=thumb-none-eabi --mcpu=cortex-m4 --exception-model=sjlj --defaultlib= --platformlib= --betterC --nogc --static --link-internally -L=-Tldscript.ld -L=-nostdlib -L=--oformat -L=binary start.d --of start.bin
instruction Function
--conf= Disable default profile
--mtriple=thumb-none-eabi Specify target framework
--mcpu=cortex-m4 Specify cpu series
--exception-model=sjlj Specify exception handling method
--defaultlib= Clear the default loading library
--platformlib= Clear the default framework library
--betterC Use betterC subset
--static static
--link-internally Use built-in LLD
-L=-Tldscript.ld Linker script used
-L=-nostdlib Parameters passed to LLD, disable default library
-L=--oformat -L=binary Pass to LLD parameters to set the output format, refer to LLD's oformat
start.d Target file, explain that LDC supports wildcards and file list @<file list>
--of start.bin Specify output file

If nothing else, you should get a start.bin.

Connect Simulation Simulation

Run within the emulator:

qemu-system-gnuarmeclipse -mcu STM32F405RG -no-reboot -nographic --image start.bin

If you plan to start the command through debugging such as GDB, you should modify it to

qemu-system-gnuarmeclipse -mcu STM32F405RG -no-reboot -nographic -s -S --image start.bin
  • -s: gdb port listening
  • -S: waits for gdb debugging when starting at 127.0.0.1:1234
gdb connection:
arm-none-eabi-gdb.exe start.bin
(gdb) target remote 127.0.0.1:1234

If you want to get more debugging information, add the ldc compilation parameters and --gcuse elf as the output format to get more debugging information. Rest of instructions

You can use the debugging function of vscode to debug, and a documentation will be written later.

Reference

Running D language with Zephyr

The repository is here: https://github.com/kubo39/zephyr_ldc_hello

So (?) I won't give much detailed explanation.

Environment

I think you can build the Zephyr environment without any problems if you look at https://docs.zephyrproject.org/latest/develop/getting_started/index.html .

Please see https://dlang.org/download.html and install the LDC used this time in the D language compiler .

Run with Qemu

This time, qemu_cortex_m3my first goal was to move it.

Therefore, the LDC target thumbv7em-none-linux-musl-gnueabi is set. I also wanted to use newlib functions, so CONFIG_NEWLIB_LIBC=y I defined them in prj.conf. Why is musl specified in target triple even though newlib is used? That part will be explained later.

All that's left to do is specify it, define it as an ExternalProject from CMake, call it, arrange it as you like, and it should work.

$ cd $ZEPHYR_BASE
$ west build -b qemu_cortex_m3 samples/ldc_hello
$ west build -t run
(...)
Hello from 'LDC'!
assertion "array index out of bounds" failed: file "d_src/hello.d", line 29, function: hello.d_main
exit

D language code

The code now looks like this:

import ldc.attributes : cold;

@nogc:
nothrow:

// newlib
extern (C)
{
    pragma(printf)
    int printf(scope const char* fmt, ...);

    noreturn __assert_func(scope const char* file, int line, scope const char* func, scope const char* failedexpr);
}

// Wrapping newlib's __assert_func.
//
// LDC: https://github.com/ldc-developers/ldc/blob/9976807e0e1acf24edfb4ba35d28c19a3f0227f2/gen/runtime.cpp#L367
//     void __assert(const char *msg, const char *file, unsigned line)
// newlib: https://github.com/bminor/newlib/blob/80cda9bbda04a1e9e3bee5eadf99061ed69ca5fb/newlib/libc/stdlib/assert.c#L68-L70
//     void __assert(const char *file, int line, const char *failedexpr)
private extern (C) @cold noreturn __assert_fail(const(char)* msg, const(char)* file, int line, const(char)* func)
{
    __assert_func(file, line, func, msg);
}

extern (C) noreturn d_main()
 {
    string ldc = "LDC";
    printf("Hello from '%.*s'!\n", cast(int)ldc.length, ldc.ptr);
    int[2] arr;
    int x;
    foreach (i; 0..3)
        x = arr[i];  // assertion "array index out of bounds" failed!
    while (true) {}
}

I won't explain the code itself, but here are some features of the D language used here:

musl target and __assert

The D language compiler supports array bounds checking. At that time, you can select the behavior when an out-of-bounds access occurs with the --checkaction option from halt/C/D/context, but if you select here, if an out-of-bounds access occurs, the C language --checkaction=C The process is to call the assert function. However, rather than calling the assert function directly, it calls a function equivalent to assert, which has a different definition for each platform architecture.

In terms of the compiler, the following is an example of this processing.

// C assert function:
// OSX:     void __assert_rtn(const char *func, const char *file, unsigned line,
//                            const char *msg)
// Android: void __assert(const char *file, int line, const char *msg)
// MSVC:    void  _assert(const char *msg, const char *file, unsigned line)
// Solaris: void __assert_c99(const char *assertion, const char *filename, int line_num,
//                            const char *funcname);
// Musl:    void __assert_fail(const char *assertion, const char *filename, int line_num,
//                             const char *funcname);
// uClibc:  void __assert(const char *assertion, const char *filename, int linenumber,
//                        const char *function);
// else:    void __assert(const char *msg, const char *file, unsigned line)

static const char *getCAssertFunctionName() {
  const auto &triple = *global.params.targetTriple;
  if (triple.isOSDarwin()) {
    return "__assert_rtn";
  } else if (triple.isWindowsMSVCEnvironment()) {
    return "_assert";
  } else if (triple.isOSSolaris()) {
    return "__assert_c99";
  } else if (triple.isMusl()) {
    return "__assert_fail";
  }
  return "__assert";
}
void DtoCAssert(Module *M, const Loc &loc, LLValue *msg) {
  const auto &triple = *global.params.targetTriple;
  const auto file =
      DtoConstCString(loc.filename ? loc.filename : M->srcfile.toChars());
  const auto line = DtoConstUint(loc.linnum);
  const auto fn = getCAssertFunction(loc, gIR->module);

  llvm::SmallVector<LLValue *, 4> args;
  if (triple.isOSDarwin()) {
    const auto irFunc = gIR->func();
    const auto funcName =
        irFunc && irFunc->decl ? irFunc->decl->toPrettyChars() : "";
    args.push_back(DtoConstCString(funcName));
    args.push_back(file);
    args.push_back(line);
    args.push_back(msg);
  } else if (triple.isOSSolaris() || triple.isMusl() ||
             global.params.isUClibcEnvironment) {
    const auto irFunc = gIR->func();
    const auto funcName =
        (irFunc && irFunc->decl) ? irFunc->decl->toPrettyChars() : "";
    args.push_back(msg);
    args.push_back(file);
    args.push_back(line);
    args.push_back(DtoConstCString(funcName));
  } else if (triple.getEnvironment() == llvm::Triple::Android) {
    args.push_back(file);
    args.push_back(line);
    args.push_back(msg);
  } else {
    args.push_back(msg);
    args.push_back(file);
    args.push_back(line);
  }

  gIR->CreateCallOrInvoke(fn, args);

  gIR->ir->CreateUnreachable();
}

However, there was one problem here.

The argument order is different between the assert function in a Linux environment other than glibc/musl in LDC and the newlib assert function.

// LDC: https://github.com/ldc-developers/ldc/blob/9976807e0e1acf24edfb4ba35d28c19a3f0227f2/gen/runtime.cpp#L367
//     void __assert(const char *msg, const char *file, unsigned line)
// newlib: https://github.com/bminor/newlib/blob/80cda9bbda04a1e9e3bee5eadf99061ed69ca5fb/newlib/libc/stdlib/assert.c#L68-L70
//     void __assert(const char *file, int line, const char *failedexpr)

Therefore, if you simply call it as is, a seemingly incomprehensible error message will be displayed.

$ west build -t run
(...)
Hello from 'LDC'!
assertion "" failed: file "array index out of bounds", line 40362

Originally, I would like to modify the compiler side, but currently LLVM does not support newlib in target triple.

LDC has also given up on handling at the moment: https://github.com/ldc-developers/ldc/blob/master/driver/main.cpp#L625-L628 (PS: I wrote a patch because it worked normally ldc-developers/ldc#4351)

So this time, I set the target to Musl, __assert_fail defined a function, __assert_func wrapped the newlib function, and modified it so that the arguments corresponded as expected.

The reason for using musl here is that it __assert is considered to have the least environmental impact among those that can avoid overloading. The compiler tries to call musl target, but newlib naturally doesn't have such a thing, so it calls the one you defined yourself.__assert_fail

// Wrapping newlib's __assert_func.
//
// LDC: https://github.com/ldc-developers/ldc/blob/9976807e0e1acf24edfb4ba35d28c19a3f0227f2/gen/runtime.cpp#L367
//     void __assert(const char *msg, const char *file, unsigned line)
// newlib: https://github.com/bminor/newlib/blob/80cda9bbda04a1e9e3bee5eadf99061ed69ca5fb/newlib/libc/stdlib/assert.c#L68-L70
//     void __assert(const char *file, int line, const char *failedexpr)
private extern (C) @cold noreturn __assert_fail(const(char)* msg, const(char)* file, int line, const(char)* func)
{
    __assert_func(file, line, func, msg);
}

In the future, I hope that I can wait for LLVM to support newlib and then modify the compiler side.

Reference