Edited at
D言語Day 22

D言語で組み込みやる

More than 3 years have passed since last update.

こんにちは。D言語パーフェクトガイド10週年を迎えましたね。SHOOです。

(このD言語ACの記事、本気でD言語パーフェクトガイド10週年をネタにしたクソ記事にしようかどうか迷いました)

本日は、D言語がシステムプログラミング言語であることを裏付けるような、組み込みへの適用についてお話します。


経緯

6~7年前、私が大学の頃の状況では、趣味で組み込みと言ったら8MHzとかのPICやAVRマイコンを使ったものが一般的で、ユニバーサル基板にハンダ付けとかしたりして、3000円~1万円くらいのライタ買って書き込んだりしたもんですが…。(多分その前の時代はもっとしんどい。ライタが10万円とかする時代。)

時代は変わります。

RaspberryPiとかで簡単にリッチなハードウェアと戯れることが出来るようになったり、72MHzのARMマイコンの乗った評価ボードが1500円くらいで普通に売られてたり、mbedとかのオンラインIDEでテンプレート探してきてコンパイルボタン一発の3分程度で実際に動かせるバイナリがダウンロードできたり、書き込みもUSBメモリ感覚でできたり……。

まぁ、入り口はだいぶ楽になりました。 入り口は。

一方その頃。

「D言語をAndroidであれこれしようという輩がいるらしい。」

そんな噂を耳にしたわけじゃありませんが、公式のプルリクエストのタイトルに「ARM」の文字が踊るようになって久しく、Androidで"Hello, world"成功したよなんて声も聞こえるようになった昨今、そろそろ組み込み適用に向けての検討をしてみてもいいかなって思い始めたのが今年の夏のことでした。


ねんがんの ARMマイコンボードを てにいれたぞ!

さて、今回、D言語でいろいろやってみようと思い立ち、まずは目標を立て、それを実現できるハードウェアの選定を行いました。

目標は、まずは簡単な、でも重要な一歩である Lチカ (LEDチカチカ)。I/Oポートをいじることが出来るというのは偉大な一歩です。なんと物理的な電圧値を2値ながらも操作できるようになるのです。出力先がリレー(電圧をトリガにした機械的なスイッチ)なら100Vの扇風機をON/OFFできたりしますし、夢が広がります。

LチカはHello, worldみたいな、基本的にはどんなマイコンだろうが出来ないわけがないレベルのことなので、とりあえず買ったのは以下の4つ。

リッチ度の順で言えば


  • Raspberry Pi B+

  • LPCXpresso11U68

  • NUCLEO STM32F334

  • STM32F103RBT6 マイコンボード

でしょうか。

私はもっと泥臭い、マイコンとの殴り合いがしたいのです。

かと言って、出力先のハードウェアを買ったり、スタートアッププログラム自作とかみたいな、足がかりが全くない状況からのスタートも手間です。

というわけで、とりあえずハードウェアは NUCLEO STM32F334 を選ぶことにしました。1500円とUSBミニBケーブルがあればLチカできるというお手軽さのハードです。

ザクッと思い描いていた作業工程はこう。


  • とりあえず標準的な方法(mbed)でコンパイルして動かす

  • なぜか書き込めないとかありそう。最悪マイコンマニュアルのにらめっことかテスタでの導通チェックで眉間にしわが増えそう。

  • C++のプログラムをGCCのコンパイラでコンパイルして動かす。GCCでARM用のバイナリが吐けることはわかっているのだし、きっと出来るはず。mbedからの移植が手軽にできなければ苦労しそう。

  • D言語のクロスコンパイラ(GDC)のコンパイル。

  • GDCをコンパイルするのに四苦八苦する。バージョン間の違いとかで吐血しそう。

  • 標準ライブラリ(druntime+Phobos)をコンパイルするのに四苦八苦する。GCとかファイルシステムとか無理ゲーなのをどうするかで死ぬ思いしそう。

  • コンパイルとかリンクできないのを四苦八苦してコンパイル・リンクできるようにする。リンカスクリプトとか、スタートアップ関数がないとか言われて涅槃に至りそう。

  • 動かないのを四苦八苦して動くようにする。スタートアップが呼ばれないとか、呼ばれたかどうかわからないとか、デバッグできないとか、ポートが違うけど気づけないとかで地獄を見そう。

  • 何とか動いて転生しそう。

想像だに満身創痍ですが、大抵のプログラマはそういうのに快感を覚える変態ですので、問題ありません。

……と思っていた時期が私にもありました。


mbedでLチカ

ほぼ説明不要。mbedの公式ページでアカウントとって、NECLEO 32F344用のLチカプログラムをインポート、コンパイル、ダウンロード、USB接続で書き込み。次の瞬間にはLチカする。

以上。

心配していた書き込みできないとかもなく、すんなり動く。

ここまでたどり着くのに1週間位を想定してたけど、3分で完了する。


GCCのコンパイラで動かす

ARM向けのGCCのコンパイラはこことかで配布されている。

これを使って、さっきmbed公式からダウンロードしたバイナリと同じものを作ればいいだけ。

mbedにはGCCでコンパイルできるようにエクスポートする機能が付いていたりします。が、この時点ではNUCLEO STM32F334は対応していませんでした。ので、自分で対応させる必要があります。


mbedからの移植が手軽にできなければ苦労しそう。


奇しくもこの予想は当たることになってしまいました。

対応にあたってはこことか参考になりました。

具体的なことは先の記事に書いてあるため省略しますが、STM32F334特有の問題も有りました。

リンカスクリプトとスタートアップのアセンブラが無いという事。

適当に近そうなハード(TARGET_NUCLEO_F103RB)のそれを持ってきてみるも、全く動きません。

よくよく見てみると、リンカスクリプトのFLASH領域の設定が間違っており、64kBしかないのにかかわらず、128kBも割り当てようとしていました。ほかにも、ISR(割り込みサービスルーチン)のベクタテーブルのサイズも違っていました。動かないわけです。


mbed/targets/cmsis/TARGET_STM/TARGET_NUCLEO_F334R8/TOOLCHAIN_GCC_ARM/STM32F3XX.ld

/* Linker script for STM32F3xx */

/* Linker script to configure memory regions. */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000188, LENGTH = 12k - 0x188
}
:
:
:


とりあえずこれでC/C++でコンパイルして動かすことはできるようになりました。


さあ、D言語のコンパイラのコンパイルだ

こ こ か ら 長 い 旅 が 始 ま る ………

というか、3ヶ月以上頑張っているけど、未だまともなコンパイラを得ていません…。

それでも一応コンパイラっぽいものはできて、なんとかD言語のコードをコンパイルすることはできるようになりました。

しかしながら、もはや3ヶ月前に何に苦労していたのか覚えていないので体験談としても賞味期限切れです。

一応、覚えているものをかいつまんで。


GCCをコンパイルするのに必要な物って…?

ま と も な 環 境

たぶん、結構重要です。

つぎはぎに改造を重ねたMSYS環境で作業していましたが、そのせいでまるでコンパイルできず…。

Cygwin heapがどうのこうのだの、lto-pluginのコンパイル中に謎のハングでコンパイルが終わらないだの、awkスクリプトがどうのこうのだの…awkなんて知らないよ…。

1回のビルドに3時間くらいかかるのに、ビルドするたびに起こるエラーが変わるとか…。

とりあえず、私が構築したそれは、以下。


  • mingw-get-setup.exe (2014/9/14時点で最新のmsys)

  • gmp-6.0.0

  • mpfr-3.1.2

  • mpc-1.0.2

  • isl-0.12.2

  • cloog-0.18.1

  • gcc-4.9.1

  • newlib-2.1.0

  • gdc-4.9

これに加えられた変更は、多い。

おそらくこれらGDCを構築するのに必要なソースコード群には、旬なるものが存在する。

各種組み合わせによってコンパイルできる・できないが変わってくると思われます。

最初の2週間位は、なんとか手を加えずにコンパイルできる方法が無いか必至に検討しましたが、結局見つけられず。

っていうか、これについて話し始めるとD言語ACっていうかGCCビルドACみたいになりそうなので、要点だけ説明します。


  • ターゲットはarm-none-eabi

  • gccはライブラリや各種コンパイラ等を一括でコンパイルする方法を提供しているが、まず成功しない(人聞きが悪い。多分私のせい)。失敗したら個別のライブラリごとにmakeを実施してビルドエラーを確実に潰していく。

  • configure.inやMakefile.inなどを書き換えて頑張ってコンパイルが成功するようにする。ビルドの作業ディレクトリ内のファイルは弄っても再度ビルドをしようとすると消されるので根本から修正する。

  • druntime/Phobosのコンパイルだが、GC等メモリまわり, Mutexなどスレッド関係, std.datetime, std.stdioなどI/O関係, std.fileなどファイル関係は version (ARM) {} else: というおまじないですっ飛ばす。(のちに出ることになる大量のリンクエラーは多分このせい)



  • ↑の空行には色々いいたいけど言葉に表せないいろいろが詰まっている。

  • ……諦めない。


コンパイラができたものとする

できてからも道のりは遠い。


nano?なにそれ美味しいの?

GCCのリンカに怒られる。nano.specsなる設定がないそうだ。

LaunchpadのGCCでは標準ライブラリにnewlib-nanoなるものを使用しているそうで?それをコンパイルしていないから?ダメ?よくわかりませんでした。

というか、mbedのGCCへのexport機能。これ、たぶんLaunchpadが前提なんじゃないのか…?

nosys.specsで代用する。


_read/_writeなどが重複定義でリンクエラー

標準入出力関係でしょうか。mbedのエクスポートでも定義されているし、コンパイルしたnewlibのほうでも定義されているし、で、どちらを使ってリンクしたらいいのかわかりません!というエラー内容だと思います。newlib-nanoだったらリンクエラーしないのかね?

newlibのコンパイル時にconfigureの設定で--disable-newlib-supplied-syscallsすればこれらがコンパイルされないnewlibが完成するようです。

……しかしこれをするとGCCのビルドがまるでできない。なぜかlibbacktraceのビルドでこけまくる。

GCCのコンパイラを作成後にnewlibを個別にビルドすることでなんとかひと通りのビルドが通りました。


ビルドできるも、なぜか動かない。

さて、作ったGCCでLaunchpadのGCCを置き換えてLチカプログラムをビルドしてみたところ、ビルドに成功。

なんとかNUCLEO STM32F334で動かせるはずのバイナリがビルドできた……!

………と思いましたが、これを転送しても、全く動かない……。

どうやら標準ライブラリのコンパイルオプションが怪しそうですが、この時点ですでにこのAdvent Calendarに間に合わなそうなので方針を転換することにしました。


諦め

とりあえずゴールは下方修正することにしました。arm-none-eabi-gdc.exeというバイナリを得られれば勝ちとする。(私は何と戦っているのだ)

make all-gcc

make install-gcc

とかやればとりあえずのものはできるでしょうか。

なお、この時点でarm-none-eabi-gdc.exeこれはすでに出来ていたので、これをそのまま使います。

GDCはこれでよしとして、他のライブラリやC/C++のコンパイラはLaunchpadのコンパイラを利用することとします。


D言語でLチカ


多少強引にコンパイルする

完全なD言語の機能を使用することは度重なる苦行に耐えかねて諦めてしまったので、以下の方針を採用することにしました。


  • スタートアップルーチン、ハードウェアの初期設定はmbedに任せる

  • main関数はC++で書く

  • D言語とC++の橋渡しとしてC言語の呼び出し規約でやりとりを行う

  • C++のmain関数からD言語で書いたmainD関数を呼ぶ

  • mainD関数からはC++で書いたchangeLedを呼ぶ

  • changeLedではC++で書かれたライブラリを利用する

  • D言語を利用する上で必要な関数や変数は適当に定義してごまかす

ということで、できたソースは以下。


main.cpp

#include "mbed.h"

// LEDを扱うためのクラスライブラリのオブジェクトインスタンス
DigitalOut myled(LED1);

extern "C"
{
// C言語の呼び出し規約で定義された関数でC++ライブラリにアクセス
void changeLed(unsigned char i)
{
// C++の代入演算子オーバーロード
myled = i;
}

// D言語で書かれたmainD関数のプロトタイプ宣言
int mainD(void);

// D言語のモジュールを定義する際に必要っぽい。
// とりあえずアドレスだけあれば良いんじゃないかな。
void* _Dmodule_ref; // start of linked list
}

// C++で書かれたメイン関数からD言語の関数をC言語の呼び出し規約で呼び出す
int main()
{
return mainD();
}



main_d.d

// C言語で書かれたmbedのライブラリ関数

extern (C) void wait(float);
// C++で書かれたC言語の呼び出し規約を持つchangeLed関数
extern (C) void changeLed(ubyte);

// D言語でかかれたC言語の呼び出し規約の関数
extern (C) int mainD()
{
while (1)
{
changeLed(1);
wait(0.2);
changeLed(0);
wait(1.0);
}
return 0;
}


C言語とC++とD言語が複雑に絡んだ美味しいコードが出来ました。

Makefileをちょっといじって…。


Makefile

:

:
:
OBJECTS = .............. \
./main.o
OBJECTS_D = ./main_d.o
:
:
:
CC = $(GCC_BIN)arm-none-eabi-gcc
DC = $(GDC_BIN)arm-none-eabi-gdc
CPP = $(GCC_BIN)arm-none-eabi-g++
:
:
:
CPU = -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=$(FLOAT_ABI)
DC_FLAGS = $(CPU) -c
DC_SYMBOLS = -DTARGET_NUCLEO_F334R8 -DTARGET_M4 -DTARGET_STM -DTARGET_STM32F3 -DTARGET_STM32F334R8 -DTOOLCHAIN_GCC_ARM -DTOOLCHAIN_GCC -D__CORTEX_M4 -DARM_MATH_CM4 -D__FPU_PRESENT=1 -DMBED_BUILD_TIMESTAMP=1410522657.8 -D__MBED__=1 -DTARGET_FF_ARDUINO -DTARGET_FF_MORPHO
:
:
:
.SUFFIXES: .d
:
:
:
.d.o:
$(DC) $(DC_FLAGS) $(DC_SYMBOLS) -o $@ $<
:
:
:
$(PROJECT).elf: $(OBJECTS) $(OBJECTS_D) $(SYS_OBJECTS)
$(LD) $(LD_FLAGS) -T$(LINKER_SCRIPT) $(LIBRARY_PATHS) -o $@ $^ $(LIBRARIES) $(LD_SYS_LIBS) $(LIBRARIES) $(LD_SYS_LIBS)
:
:
:


動いた

Lチカ画像

やったー。Lチカ!!!


しかし

このD言語、構造体のメンバ関数とか書いただけでリンクできなくなる……。

scope (exit)もダメ。

つらい。

やはりdruntimeくらいは必須っぽいです。

しかしためしにできたlibgdruntimeとやらをリンクしてみると大量のリンクエラーが……!!

うーん、なんとかdruntimeを動かす方法を模索するしか………。


まとめ


とりあえず標準的な方法(mbed)でコンパイルして動かす

なぜか書き込めないとかありそう。最悪マイコンマニュアルのにらめっことかテスタでの導通チェックで眉間にしわが増えそう。


そんなに苦労せずに動いた


C++のプログラムをGCCのコンパイラでコンパイルして動かす。GCCでARM用のバイナリが吐けることはわかっているのだし、きっと出来るはず。mbedからの移植が手軽にできなければ苦労しそう。


mbedからの移植が手軽にできず、苦労したけどなんとかクリアした


D言語のクロスコンパイラ(GDC)のコンパイル。

GDCをコンパイルするのに四苦八苦する。バージョン間の違いとかで吐血しそう。


四苦八苦した。configure.inとか書き換えて何とかクリア。


標準ライブラリ(druntime+Phobos)をコンパイルするのに四苦八苦する。GCとかファイルシステムとか無理ゲーなのをどうするかで死ぬ思いしそう。


一応コンパイルは出来たけど結局動かず。大量のリンクエラーも出るし、わからん。なぜ動かないか現在調査中。


コンパイルとかリンクできないのを四苦八苦してコンパイル・リンクできるようにする。リンカスクリプトとか、スタートアップ関数がないとか言われて涅槃に至りそう。


たしかにリンカスクリプトのあれこれはあった。なんとかクリア。


動かないのを四苦八苦して動くようにする。スタートアップが呼ばれないとか、呼ばれたかどうかわからないとか、デバッグできないとか、ポートが違うけど気づけないとかで地獄を見そう。


………つらい。


何とか動いて転生しそう。


標準ライブラリ(druntime+Phobos)がないD言語で動いたって言えるでしょうか?

否。断じて否!!僕達の戦いはまだ始まったばかりだ……ッ!!

SHOO先生の次回作にご期待ください!

D言語Advent Calendar。次は23日目 @youxkei さんです。