D on Arduino

Posted 2022-10-10

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

Walter's interview

The interview Mike did with Walter is a kinda fun listen.

DConf Online 2022

The submission deadline for it just passed. I didn't formally submit but hinted I might improvise something anyway if they want. So we'll see what happens.

What Adam is working on

I adjusted adrdox to not break local links to dub packages that call themselves std.experimental. I'm also writing more little docs in the arsd repo. I wanna do more of that in between work and stuff.

And now, our feature presentation:

Hello Arduino

I have an Arduino Uno here in the house that I have barely used, but was reminded of it when a person on the D newsgroup asked about D on Arduino and someone on Stack Overflow asked what the -gcc switch does.

The ng thread: https://forum.dlang.org/post/hhvarlzfqaowgjjprxfr@forum.dlang.org

My SO answer: https://stackoverflow.com/a/73762836/1457000

There's a wiki page about it written by WebFreak, and there's a link there to a project with some basic code ported to D. It is a bit out of date - you don't have to build LDC anymore (I just edited this out of the page, but you can still see it in history if you are curious) and doesn't go into detail of how to port. So let me put an example here to discuss it a bit more.

The wiki page: https://wiki.dlang.org/D_on_AVR

You can use a standard ldc2 install, just grab a binary build from the ldc website. You don't have to compile it yourself; avr support has been included in the normal download for a while now.

After getting ldc, first thing you'll want to do is install the arduino tools. I'm sure there's other ways to do it, but what I did on my computer is simply install the Arduino Studio. I put its files in /opt/arduino/arduino-1.8.10/. You'll want to adjust that path for your computer's setup. In there, you'll see the arduino command, a hardware folder, among others.

me@arsd:/opt/arduino/arduino-1.8.10$ ls
arduino          arduino-linux-setup.sh  hardware    java  libraries  revisions.txt  tools-builder
arduino-builder  examples                install.sh  lib   reference  tools          uninstall.sh

Now, let's make a test program:

1 import ldc.llvmasm;
2 
3 // Ports from the delay_basic.h in the thing
4 void _delay_loop_1(ubyte __count) {
5 	// this template param is required to avoid
6 	// assertion `!Call.getType()->isVoidTy() && "Bad inline asm!"' failed.
7 
8         __asm!ubyte (
9                 "1: dec $0\n\tbrne 1b",
10                 "=r,0", (__count)
11         );
12 }
13 void _delay_loop_2(ushort __count) {
14         __asm!ushort (`
15 	1: sbiw $0,1
16 	brne 1b
17 	`,
18 	`=w,0`,
19 	__count);
20 }
21 
22 // port from delay.h in arduino thing
23 enum F_CPU = 1000000UL;
24 
25 // this was _delay_ms but i did something wrong and changed double to int and i still don't love it
26 void _delay(int __ms) {
27 	ushort __ticks;
28 	ulong __tmp = (F_CPU * __ms) / 4000;
29 	if(__tmp < 1)
30 		__ticks = 1;
31 	else if(__tmp > 65535) {
32 		__ticks = cast(ushort) (__ms * 10.0);
33 		while(__ticks) {
34 			_delay_loop_2(cast(ushort) (((F_CPU) / 4e3) / 10));
35 			__ticks--;
36 		}
37 		return;
38 	} else
39 		__ticks = cast(ushort) __tmp;
40 	_delay_loop_2(__ticks);
41 }
42 
43 
44 // and this is from WebFreak's sample code on the D wiki
45 
46 enum AVR_ARCH = 5; // AVR architecture of your MCU
47 
48 static if (AVR_ARCH >= 100) {
49  enum SFR_OFFSET = 0x00;
50 } else {
51  enum SFR_OFFSET = 0x20;
52 }
53 
54 enum ubyte* MMIO_BYTE(ubyte memAddr) = cast(ubyte*) memAddr;
55 enum ubyte* SFR_IO8(ubyte ioAddr) = MMIO_BYTE!(ioAddr + SFR_OFFSET);
56 
57 enum ubyte* PINB = SFR_IO8!(0x03);
58 enum ubyte* DDRB = SFR_IO8!(0x04);
59 enum ubyte* PORTB = SFR_IO8!(0x05);
60 
61 extern(C) void main() {
62   import core.volatile;
63 
64   volatileStore(DDRB, 0xFF);  // Set all PORT B to output
65 
66   // and I added the delays
67 
68   while (true) {
69     volatileStore(PORTB, 0xFF); // Set all PORT B to high
70     foreach(i; 0 .. 10)
71     	_delay(1000);
72     volatileStore(PORTB, 0x00); // Set all PORT B to low
73     foreach(i; 0 .. 20)
74     	_delay(1000);
75   }
76 }

OK, there's a few parts there that I'll come back to, but first, the make process:

# Makefile
$ cat Makefile
all:
        ldc2 delay.d -betterC -Oz -mtriple=avr -mcpu=atmega328p -Xcc=-mmcu=atmega328p -gcc=avr-gcc
        avr-objcopy -O ihex -R .eeprom delay delay.hex
        avrdude -F -V -c arduino -p ATMEGA328P -P /dev/ttyACM0 -b 115200 -U flash:w:delay.hex -C /opt/arduino/arduino-1.8.10/hardware/tools/avr/etc/avrdude.conf

Please note the path to my arduino install there on the last line again.

To run that, set the PATH to your arduino install and run make, like this:

PATH=/opt/arduino/arduino-1.8.10/hardware/tools/avr/bin:$PATH make

This will compile the D code, then copy it over to your arduino hardware and start running it. The light (and anything else you have attached to the pins) will blink on and off.

If you looked at the wiki page linked above, most of this will look familiar. I copy/pasted liberally.

A few important things to notice:

  • You can't call the delay_ms from the arduino C headers with extern(C) because it is defined as an inline function. Inline functions don't actually appear in the library or object file; you have to port the source.
  • WebFreak ported the source here: https://github.com/WebFreak001/avrd/blob/master/source/avr/util/delay.d and did a better job than I did, but I wanted to describe how to do it.
  • Inline asm in gcc and ldc are slightly different. Here's the _delay_loop_1 in the file:
    void
    _delay_loop_1(uint8_t __count)
    {
    	__asm__ volatile (
    		"1: dec %0" "\n\t"
    		"brne 1b"
    		: "=r" (__count)
    		: "0" (__count)
    	);
    }

    And here's my D:

    import ldc.llvmasm;
    // Ports from the delay_basic.h in the thing
    void _delay_loop_1(ubyte __count) {
    	// this template param is required to avoid
    	// assertion `!Call.getType()->isVoidTy() && "Bad inline asm!"' failed.
    
    	__asm!ubyte (
    		"1: dec $0\n\tbrne 1b",
    		"=r,0", (__count)
    	);
    }

    The asm is different. In gcc, it used __asm__. In ldc, it used ldc.llvmasm.__asm. They're similar but not the same:

    • gcc used %0 for placeholders. ldc used $0.
    • gcc used a colon followed my the magic constraint string. ldc uses a second parameter to the function. All the constraints must be in the one string to ldc, just comma separated in one string.
    • gcc appears to figure out the size automatically. ldc does not - failing to specify will trigger a compiler assertion failure. I don't actually know what the type here means, but making it match the input seems to be the right thing to do.
  • I did something wrong as my delay does not actually seem to work in milliseconds but I can loop it more and make it work, so good enough.
  • The build and deploy process all comes out of the arduino tools, so aside from setting the paths, it is all the same as with C and such.
  • I used -gcc, though --link-internally is also an option. See my Stack Overflow link for more discussion, but this means the little C stubs are available here, letting that main function work among other things.

I know this is a surface-level addition to info already out there, but my hope is this little thing can get someone going and we can expand this a bit more. If you do, please email me (destructionator@gmail.com) so I can write about it and help update the wiki too.

edit: Maxter on the discord chat suggested we read this for info on the constraints: https://www.nongnu.org/avr-libc/user-manual/inline_asm.html

Can importC work here?

In theory, importC can mean we can include the C inline functions directly, but in practice it doesn't work at this time, and there doesn't seem to be a way forward on that right now. I can imagine a gdc cross-compiler build could make that work very well though, just reading those .h files directly and using the gcc inline asm without modification.

I used ldc here because it works out of the box on the download, no need to build it yourself. A gdc cross compiler could perhaps be offered for download once it gets working too, and I think that'd be a nice demo of importc helping get these things right.

But for now, I think you're best off writing it yourself, hence why I wanted to show that above. The inline asm is somewhat important for timing on these controllers so being able to do that is cool. (though I don't actually know what all those constraint strings mean. alas.)