SNSへはこちら

D言語でSTM32マイコンプログラミング

続いてD言語で STM32F303K8T6 を動かしたいと思います。

環境

arm-none-eabi-gcc のある状況を仮定しています。また、macOS で、Homebrew をインストールしていることとします。

また、完成したプロジェクトを上げてあるリポジトリはこちらです↓

Bare Metal Programming for STM32 on D. Contribute to shima-529/stm32OnDlang development by creating an account on GitHub.

導入

D 言語のコンパイラと言ったら dmd ですが、ここではクロスコンパイルに対応した LLVM をベースにした ldc2 を使います。やっぱすごいですね、LLVM。

サクッとインストールしちゃいましょう。

$ brew install ldc

やること

Rust と違って、やることは非常に少ないです。とりあえず Makefile を作っておきましょうか。

Makefile

CC=arm-none-eabi-gcc
SIZE=arm-none-eabi-size
DC=ldc2
DC_MODULE=$(addprefix src/stm32/,startup.d vector.d stm32.d)

CPU=cortex-m4
CFLAGS= -mcpu=$(CPU) -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -ffunction-sections -fdata-sections -Os -g -nostartfiles -nostdlib
DFLAGS=-mtriple=thumbv7em-none-linux-gnueabihf -mcpu=$(CPU) -static -disable-linker-strip-dead -O0 -g
OBJS=$(notdir $(subst .c,.o,$(wildcard src/*.c)) $(subst .d,.o,$(wildcard src/*.d)))
OBJS:=$(filter-out startup.o,$(OBJS))
OBJS:=$(filter-out vector.o,$(OBJS))
OBJS:=$(filter-out stm32.o,$(OBJS))

all: output.elf
    $(SIZE) $^

output.elf: $(OBJS)
    $(CC) $(CFLAGS) -Wl,-T,LinkerScript.ld,-Map=output.map,--gc-sections -o $@ $^

%.o: src/%.d
    $(DC) $(DFLAGS) -c -of=$@ $(DC_MODULE) $^

flash:
    echo output.elf | xargs -I {} openocd -f interface/cmsis-dap.cfg -f target/stm32f3x.cfg -c 'init;program {};reset;exit'

clean:
    $(RM) *.o *.elf *.map

%.c のターゲットは必要ないので削除していますが、最後にオブジェクトファイルをリンクする際に arm-none-eabi-gcc を使用しています。

その他

ここまで来て実際作るファイルに触れてもどうしようもないことに気が付きました。
ということで以下は組み込み用途として使う D言語の特徴・コツについて述べたいと思います。

ガーベッジコレクタを無効にする

D言語には GC の概念がありますが、組み込み用途ではプログラムサイズの肥大化を起こしますし、不要なので削除します。そのために各ソースファイル(.d)の先頭におまじないとして以下を記述しておきます。さもないとよくわかりませんが、ldc がセグフォで落ちます。

extern(C):
@nogc:
nothrow:
pragma(LDC_no_moduleinfo);

インライン展開を無効にする

ビジーウェイトで処理待ちをする時があると思います。そのときに関数を呼んでもその場でインライン展開が行われてしまうと困りますね。ということで以下のプラグマを関数内に書くことで、その関数が展開されるのを防ぐことが出来ます。

pragma(LDC_never_inline);

本リポジトリでは、SysTick タイマを使用した ms_wait に書いています。

void ms_wait(uint ms) {
    pragma(LDC_never_inline); // ここ!!
    SysTick.LOAD.st(1000 - 1);
    SysTick.VAL.st(0);
    SysTick.CTRL.bset(0);

    foreach (_; 0..ms) {
        while( !SysTick.CTRL.bitIsSet(16) ) {
        }
    }
    SysTick.CTRL.bclr(0);
}

こうすることで 500.ms_wait; としてもインライン展開されません。

alias によるレジスタ定義

C言語ではレジスタをマクロで定義していて、コンパイル時には即値に展開されるのでした。D言語ではマクロが存在しないものの、エイリアスでほぼ同等のことが出来ます。例えば RCC レジスタを見てみましょう。まず _RCC という構造体を定義しておいて…

alias RCC_BASE = Alias!0x4002_1000;
alias RCC = Alias!(cast(_RCC*)RCC_BASE);

とすれば、以下の関数によってレジスタにデータを書き込みすることが出来ます。

void st(T)(ref T reg, T val)
if( isNumeric!(T) ) {
    volatileStore(&reg, val);
}

ref は参照渡しをするという意味です。ところで、「メモリ上に RCC_BASE を置くのはどうなの」と思われるかもしれません。が、それはだめです(実装が汚くなる、ROM を消費してしまうという点で)。
例えば次のようにするのは間違いです。

_RCC* RCC = cast(_RCC*)0x4002_1000;

こうすると RCC 自体が配置されるので上の st ではその配置を指してしまい、また refref とすると実際に式を書くときに非常に汚くなってしまいます。ということで alias 万歳というわけです。

割り込み関数でのマングリング

割り込み関数を書く際にそのまま void TIM2_IRQHandler() {} とすると D言語仕様に関数名がマングリング(仕様に合うように勝手に変更)されてしまいます。ARM では C言語で書くと、コード中につけた名前そのままでオブジェクトファイルにシンボルが記述されるので、ARM のみ extern (C) でマングリングを回避することが出来ます。その他のアーキテクチャは知りませんが、恐らく調べればそういう物があると思います。

extern(C) void TIM2_IRQHandler() { // こうすると、ARMにおいてマングリングを無効化できる
}

…とは言ったものの、結局 C言語の仕様に従う必要はないわけで、割り込みベクタテーブルに登録された関数が定義されていれば良いのです。現時点ではリポジトリにおいてC言語の仕様にわざわざ合わせていますが、これは必要なくて、好みに応じてD言語仕様にしても全く問題ないと思いました。ですので、

  1. Weak 属性を付ける部分、そしてそれとは別にユーザーが定義する部分どちらにも extern(C) をつけておく
  2. 上の両者のどちらにも extern(C) を付けない

のどちらかを行えば全く問題ないということですね。

変数のビット長が決まっている

C言語とは違って、D言語では変数のビット長が厳格に決まっています。C言語のintの定義が以下に適当であるかは調べてもらえば分かるでしょう。D言語の場合はこちらにリストがありますので、見てもらうと分かる通り言語仕様として決まっています。処理系依存の定義ではないのでよかったですね。
しかしながら、C言語みたいな uint32_t という書き方のほうがビット長をそのまま型に書くのでわかりやすいんですがね。