D at 8-bit platform - some experiences
Dukc
ajieskola at gmail.com
Thu Jul 6 17:18:26 UTC 2023
D is not designed to operate at platforms where the native
integer/pointer size is under 32 bits. Nonetheless, I've lately
been learning AVR programming with D, and thought I'd share my
experiences.
I'm not the first one to do this. [Ernesto
Castellotti](https://forum.dlang.org/post/kctkzmrdhocsfummllhq@forum.dlang.org), [Adam Ruppe](https://dpldocs.info/this-week-in-d/Blog.Posted_2022_10_10.html#hello-arduino) and others have experimented before me, but this still seems a fringe area for D.
Overall D works passably. There are shortcomings, but since you
do everything yourself anyway on platforms like this, and the
alternative would be C, D so far seems a viable option. I think
that with relatively little modification, AVR and similar
platforms could be made a first-class citizen for D (albeit
without a full standard library support since they're still
bare-metal platforms -
[LWDR](https://forum.dlang.org/post/unfecovccvpocxtkprxs@forum.dlang.org) on the other hand could probably work).
`size_t` is defined as `uint`, which is maybe technically wrong,
but in practice makes dealing with array lengths much easier than
it'd be otherwise. The problem is that LDC doesn't currently
[fully work](https://github.com/ldc-developers/ldc/issues/2520)
with this scheme. As a result, array indexing does not work
unless you disable bounds checking. My current solution is to
define a custom indexing function instead:
```D
pragma(inline, true)
@trusted pure ref ix(El, Idx)(El[] arr, Idx idx)
{ // I haven't yet hooked the D assert failure handler so
// using a custom replacement instead.
// Casting to ushort necessary to work around the mentioned
issue.
if(idx >= cast(ushort) arr.length) assert0();
return arr.ptr[idx];
}
```
Another big limitation on AVR isn't due to the bit width, but
it's Harvard architechture. LLVM considers pointers to program
memory a different type from data pointers, but LDC declares
function pointers as data pointers, resulting in LLVM type error
trying to compile their usage code. One would have to define some
sort of custom function pointer type, implementing it's
invocation in assembly or LLVM IR.
The good news for D?
First off, when something doesn't work, you can usually hack
together something custom, and put it behind a reasonably usable
API. Even portable D code offers a far better arsenal of tricks
than most languages, as that custom indexing function shows
(returns by ref, works with any type, gets inlined, can be done
in the first place despite requiring potentially type system
breaking system code). When that isn't enough, LLVM intrinsics,
inline IR and assembly let one to implement almost anything. For
example, need a special target-specific return statement for an
interrupt handler?
```D
// Timer/Counter1 compare match A interrupt
extern(C) @trusted void __vector_11()
{ /* ...
Normal D code here - no need to implement
the whole handler in assembly!
...
*/
// Return while enabling interrupts
__asm("reti", "");
// Prevent emitting a reduntant regular return instruction.
// (Yes this works! I checked the object assembly!)
// OTOH risking UB to save one word of program memory isn't a
good tradeoff
// even on atmega328p so in my own code I'm using assert0()
instead.
assume0();
}
// I'm not going to use the definition in ldc.llvmasm
// because it's defined as @trusted, which doesn't fit at all.
pragma(LDC_inline_ir) R llvmIR(string s, R, P...)(P) pure nothrow
@nogc;
// Calling this function is always undefined behaviour.
alias assume0 = llvmIR!("unreachable", noreturn);
```
I could improve this even further by hiding `reti` inside an
inlined `noreturn` D function, or failing that, a mixin.
Second, even on a bare-metal microcontroller with no
preimplemented memory allocator, all business logic can be `@safe
pure`. As with application code, only low-level type
manipulation, I/O and memory / global variable management need to
be impure and/or `@system`/`@trusted`. Since there's no garbage
collector, some algorithms need to be written differently. A
simple approach is to give the needed working memory to a
function as an array argument. Otherwise, it's not that different
from your regular desktop application.
Third, you have the tools to make the object code as compact as
you want. For some reason, the linker doesn't recognise unused
functions by default, even with `-L--gc-sections` and
`--fvisibility=hidden`. I spent a long time fighting the linker,
once finally finding out that `--function-sections` flag for the
compiler is needed. From there, it was smooth sailing. My build
(and disassembly) script:
```bash
ldc2 -betterC -O1 --function-sections -mtriple=avr
-mcpu=atmega328p --gcc=avr-gcc --Xcc=-mmcu=atmega328p
-L--gc-sections delay.d
avr-objdump -x -D delay >delay.s
avr-objcopy -O ihex delay delay.hex
```
Note that I don't use `-Oz`. For some reason that tries to link
to GCC-defined function that isn't in my GCC library (maybe I
have a GCC version mismatch). But I find -O1 emits almost as
compact code anyway, while being clearer to read in disassembly.
`-Os` actually emits a bigger binary than `O1`. With this one can
define all sorts of inlined or CTFE functions for convenience,
with no effect on the final binary size.
Just thought to share my experiences, in case someone is
interested in D on 8-bit. Note that I've been using LDC all
along. I'm pretty sure GDC can also be used for AVR, but I don't
know how the experience would compare. For LDC, I think the
experience is better than expected considering it's an
environment D isn't designed for. Solving a few worst codegen
bugs might make it about as good there as on any bare-metal
platform. As always though, LDC is a volunteer project so I'm not
saying that anyone should tackle them.
Maybe I'll publish my avr code at some point, but not promising.
More information about the Digitalmars-d
mailing list