第0章 はじめに
本記事は、コミックマーケット94に出展した「クミコミD言語 特集 D言語組み込みプログラミング入門」の本編を2018年D言語Advent Calender向けに公開したものです。
本記事をわざわざC94まで足をお運びいただいてご購入いただいた方々がいるにもかかわらず無償での公開に至った理由は、ひとえにD言語による組み込みプログラミング技術の発展に寄与するためです。
組み込みプログラミングはいまだC言語が主力選手でありながら、年々大規模化・複雑化が進んでおり、開発効率向上は極めて重要です。そのような状況の突破口の一つとして、D言語の組み込みプログラミング分野への対応は、希望の光になりうるポテンシャルを持っていると思います。
しかしながらD言語の組み込みプログラミングの技術はいまだ発展途上で、ユーザー数もごく少数(ただでさえ少ないD言語ユーザーの、さらにその中でもレアリティの高い人種)です。このような状況の打破につながればと思い、本記事を公開することにいたしました。
※もちろん、C94にて頒布した紙面には、@theDeeBeeによる某有名技術系月刊誌をオマージュした丁寧なデザイン、随所にちりばめた小ネタなど、本記事にはない見どころが多くあります。
何卒、ご理解のほどをよろしくお願いいたします。
第1章 ハードウェア
評価ボードで作る簡単組み込みプログラミング
評価ボードって何?
図1-1 に示すのは、STMicro製の32bitマイコンの評価ボードNucleo F401REの外観です。この回路に組み込まれたマイコン(STM32F401RE)と、そこに書き込まれたプログラムによってマイコンの性能をある程度評価することができます。
マイコン(MCU)は、そのICチップの中にCPU(MPU)やSDRAMなどの一時記憶装置、コードフラッシュROMやデータフラッシュROMなどの二次記憶装置といった基本的なコンピューターの構成要素に加え、タイマーモジュールやA/D変換モジュール、PWM信号生成モジュール、UART通信モジュールなどの周辺機器(ペリフェラル)が組み合わされて構成されています。この組み合わせや個数によってマイコンそれぞれに特徴が出てきます。
目的となる機器を動かすために必要な構成要素を持っているマイコンを適切に選ぶ必要があります。
本特集ではARMマイコンとしてSTM32F401REを用い、それを動作させる簡単で便利な評価ボードのNucleo F401REでプログラムを動かしていきます。
Nucleo F401REの機能
Nucleo F401REには、以下のような特徴があります。
機能 | 内容 |
---|---|
電源 | USBまたは外部供給(3.3V/5V/7~12V) |
LED | ユーザー用×1(LD2) |
スイッチ | ユーザー用×1(BT) |
デバッガ | ST-LINK/V2(ボード備え付け) |
その他 | Arduino対応 |
これに組み込まれているSTM32F401REマイコンには以下のような特徴があります。
機能 | 内容 |
---|---|
コアアーキテクチャ | ARM 32bit |
ピン数 | 64pin(LQFP64) |
最高動作クロック | 84MHz |
内臓RAM | 96kB |
内臓FlashROM | 512kB |
タイマー | 16bit多機能タイマー×1、 汎用タイマー×7 |
通信 | SPI×3、I2S×2、 I2C×3、USART×3、 USB-OTG×1、SDIO×1 |
A/Dコンバータ | 12bit A/Dコンバータ×1、 10チャンネル |
GPIO | 50 |
電源
動かすための電気や、それを供給する回路です。電気には直流と交流がありますが、ここでは直流電源を指します。マイコンに交流電気を流したらたぶん壊れます。
LED
光ります。いわゆる「Lチカ」はLEDチカチカ。このデバイスを光らせることに由来します。
$I_f$ (順方向電流)によって光り方が変わったり$V_f$(順方向降下電圧)のためにトランジスタ使ったりと、使いこなすためには実はそこそこノウハウがいります。でも今回の特集では込み入った内容は説明しません。
スイッチ
押せます。タクトスイッチとか言われる素子です。
ボタンも、機械的な接点をもつ素子ですので、源信号はノイズまみれです。(チャタリングといいます)
ノイズを除去して、正確な信号をとらえるためにはこれもまたチョットしたノウハウが必要です。
デバッガ
Nucleo F401REにはあらかじめST-LINK/V2というデバッガが基板上にのっかっています。パソコンと接続するには、あらかじめ基板に搭載されているUSB-miniポートにUSBで接続するだけ。接続するとUSBメモリのようにリムーバブルディスクとして認識され、そこにプログラムの実行ファイルを転送してあげるだけで書き込みができます。
コアアーキテクチャ
マイコンの心臓部分が何でできているかで、STM32F401REではARMアーキテクチャを採用しています。ほかのアーキテクチャとしては、Intelのx86とかRenesasのRXとかがあります。
D言語ではARM向けの機械語を生成することができるので、これを利用します。
ピン数
ピンの数です。同じマイコンでも、ピン数が違う別のパッケージが提供されていたりします。ピン数が多くなると、例えばGPIOの本数が多かったり、A/Dコンバータの本数が多かったり、PWMの本数が多かったりします。
最高動作クロック
マイコンにくっついている発振子(Nucleo F401REには8MHzの水晶発振子がついています)からくる周波数を逓倍して、より高い周波数で動作させることができます。
本特集では第4章で扱うクロックにて解説します。
内臓RAM
マイコンのプログラムから扱うことのできる揮発性のメモリで、アクセスは高速ですが電源供給がなくなるとデータが消えるメモリ領域です。プログラムの世界ではスタック・ヒープ・グローバル変数・スタティック変数などが配置される場所です。STM32F401REでは96kBのRAMが搭載されていますが、96kB「も」あると思いますか?96kB「しか」ないと思いますか?
ちなみに、私のWindows機でいま消費中のメモリは、14.1GBだそうです。
内臓FlashROM
マイコンを動作させるプログラムの機械語などのデータが納められる領域で、電源供給が途絶えても書き込まれたデータが永続するメモリ領域です。電源無しでデータを保持することができますが、書き込みが遅かったり、消去がブロック(数バイト単位)だったり、書き換え回数に制限があったりします。プログラムでは、グローバル変数やスタティック変数の初期値、スイッチのテーブルなどが配置されたりします。STM32F401REでは512kBのFlashROMが搭載されていますが、512kB「も」あると思いますか?512kB「しか」ないと思いますか?
ちなみに、Windowsのdmdで"Hello, world!"のプログラムをコンパイルしたところ、実行ファイルは224kBに、Ubuntuのdmdだと895kBになりました。
タイマー
発振子が何回振れたかについて調べるための、マイコン内にあるカウンター回路です。たとえばSTM32F401REの動作クロックは84MHzですが、これは発振子が8400万回振れると1秒経過することを意味します。もちろんこのままだと使いにくいので、何秒経ったらフラグを立てる、とか、何秒経ったら割り込み処理を発生させる、みたいな使い方をします。もっと高度になると、何us経ったらどこどこのポートをONして、さらにus経ったらOFFする、それを周期的に発生させる…といったような使い方(PWM)をしたりします。具体的な使い方は第7章で解説します。
通信
マイコンとパソコン、あるいは別の機器と通信をさせたい、という場合に使う、マイコン内にある通信回路です。Ethernetなんかも通信の一部ですが、STM32F401REには搭載されていませんね。
組み込みの分野ではいまだシリアル通信は健在で、STM32F401REではUSARTがそれに該当します。
ちなみに、本特集では取り扱いません。
A/Dコンバータ
センサ機器などから入力されたアナログ信号(Analog:A)をデジタル信号(Digital:D)に変換するための、マイコン内にある標本化→量子化→符号化を行う回路です。
マイコンで外界の物理量を測定するには、基本的にこれを使います。
たとえば電流だったら電流センサーで電圧に変換してA/Dする、温度だったらサーミスタで抵抗値に変換して、抵抗変化を電圧に変換して、A/Dする、高い電圧は分圧してA/Dする。といった具合です。
STM32F401REでは0~3.3Vを4096段階に分割してデジタル量に量子化することができます。
ちなみに、本特集では取り扱いません。
GPIO
普通のポート入出力(General Purpose Input/Output)を行うためのマイコン内の回路です。といっても、いろんな入出力に対応しています。プルアップ__図1-2(a)__とかプルダウン__図1-2(b)__とかハイインピーダンス__図1-2(c)__とかオープンドレイン__図1-2(d)__とかプッシュプル__図1-2(e)__とかいろいろ種類があります。目的に応じて適切なものを選びます。たとえばLEDに信号を出力したければプッシュプルで、スイッチからの入力ならプルダウンとかです。
(a) プルアップ | (b) プルダウン | (c)ハイインピーダンス (Not Connected) |
(d)オープンドレイン | (e)プッシュプル |
図1-2_ |
第2章 ビルド環境
必要なものをそろえよう!
コンパイラ
コンパイラにはLDCを使用します。
D言語には複数のコンパイラがありますが、STM32F401REで使用できるARM向けのバイナリを出力できるのはLDCかGDCとなります。本特集ではLLVMベースのコンパイラであるLDCを使用します。
Linuxは次のインストールスクリプトでインストールすると便利です。
curl -fsS https://dlang.org/install.sh | bash -s ldc
Windowsの場合はGitHubのリリースページから ldc2-1.10.0-windows-x64.7z といったファイルをダウンロードすることができます。解凍してPATHを通すことで使えるようになります。
また、Windowsの場合はWindows向けの実行ファイルをコンパイルする場合、MicrosoftのVisual Studioか、Tools for Visual Studioが必要になります。ARM向けだけならば不要ですが、単体テスト等を行う場合は必要になるので、インストールしておくとよいと思います。
リンカ
GNU Arm Embedded Toolchain(arm-none-eabi-ld)
リンカとして、GNU Arm Embedded Toolchainという、ARM向けGCCクロスコンパイラに付属しているリンカを使用します。
また、本特集では(純度100%のD言語で組み込みプログラミングを行うため)一切使用しませんが、newlibなどのCランタイムを使用することもできます。
その他
その他準備しておくとよいものとして、OpenOCDというツールがあります。オンチップデバッグといって、マイコンの中で動いているプログラムを一時停止したり、メモリの中身を参照したりしてデバッグすることができます。本特集では扱いませんが、用意すると捗るかと思います。
ビルドするには?
dubは使える?
今現在、D言語のビルドにはdubが普及しています。しかしながら2018年7月現在、dubはクロスコンパイルのことまで考えて作られてはいないようです。
本特集で使用するSTM32F401REをST-LINK/V2経由で書き込むには、***.binというファイルが必要になりますが、例えばWindowsのdubではファイル名に勝手に.exeが付け加えられてしまうなど、ホストOS向けの実行可能ファイルのコンパイルにしか対応していません。ほかにも、--archオプションでターゲットのアーキテクチャを指定することができますが、--arch=armとか--arch=arm_thumbとすると、そんなアーキテクチャ知らない!と怒られてしまいます。
dubを使用するのは、未来に期待することにして、別の方法を検討しましょう。
ビルド用のスクリプト
dubがダメなら、Makefileか、.bat(Windows)や.sh(Linux)といったシェルスクリプト、もしくはD言語で簡単なビルドツール作るかといった選択肢があります。
Makefileやシェルスクリプトでもいいですが、D言語ならD言語知ってれば当然書けるだろうし、より細かな制御が効きますし、rdmdでスクリプトのように実行できるので便利です。
ビルド
ビルドの手順
__図2-1__に基本的な流れを示したフローチャートを示します。
大まかな手順はコンパイル(ldc2)、リンク(arm-none-eabi-ld)、バイナリ変換(arm-none-eabi-objcopy)です。
コンパイル
ビルドの最初に、まずコンパイルを行います。ソースコードをまとめてコンパイルし、オブジェクトファイルout.o
へとコンパイルします。
ポイントは、ここでいきなり実行ファイルを作成しないことです。いきなり実行ファイルを作成したいものですが、2018/7現在ではldc2が対応していないようで、リンクで失敗してしまいます。今後に期待するとしましょう。
コンパイルのフラグとして、-betterC
, -mtriple
, -relocation-model
を指定する必要があります。-betterC
を指定することでランタイムを使用せずにコンパイルすることが可能です。-mtriple
でターゲットとなるアーキテクチャを指定します。-relocation-model
でリロケーションモデルを指定します。リロケーションモデルをstaticとすることで、関数のアドレスやROMのアドレスが固定され、常に同じアドレスで実行することができます。(OSありのプログラムのように、動的に関数のアドレスが変わったりしないようにします)
また、-output-s
を指定することでアセンブラの出力を得ることができます。アセンブラでの出力は、D言語で記述した内容が意図したとおりの機械語になるかどうかを確認するのに役立ちます。たとえば確実にインライン化されているかどうかとか、テンプレートなどで削除されるべきコードがきちんと削除されているかどうかなどが確認できます。
ldc2 -betterC -mtriple=thumb-none-eabi -mcpu=cortex-m4 -relocation-model=static -release -O -output-s -output-o -boundscheck=off -c -of out.o src/main.d src/mcu/interrupts.d src/mcu/op.d src/mcu/peripherals.d src/mcu/regs.d src/mcu/startup.d
リンク
コンパイルしてできたオブジェクトファイルをリンクし、実行ファイルout.elf
を作成します。
-Map
とすることでマップファイルを作成できます。マップファイルは関数や変数のアドレス、あるいはFlashROM上でのサイズといった情報を確認することができます。
arm-none-eabi-ld --gc-sections -T stm32f401re.ld -Map out.map out.o -o out.elf
また、リンク時にはマイコンのFlashROM上のプログラムの配置先についての情報が必要です。この配置先の情報は「リンカディレクティブ」(*.ldファイル)によって指定します。
以下のリンカディレクティブの一部では、エントリーポイントはentryという関数で、メモリはROMがアドレス0x08000000から512KB、RAMがアドレス0x20000000から96KBありますよ、0x20000000+0x00018000(96KB)=0x20018000を_stackStartというシンボルとして参照できるようにしますよ、という指示が記載されています。ほかにもセクションに関する記載など、プログラムのどの種類の何をどこに配置するかについて、必要なものを記載します。
Linker script for STM32F401
/* Linker script for STM32F401 */
ENTRY(entry);
MEMORY
{
ROM (r!wxai) :
ORIGIN = 0x08000000,
LENGTH = 512K
RAM (rwxai) :
ORIGIN = 0x20000000,
LENGTH = 96k
}
_stackStart = ORIGIN(RAM)
+ LENGTH(RAM);
SECTIONS
{
/* We don't need exceptions, and discarding these sections
prevents linker errors with LDC */
/DISCARD/ :
{
*(.ARM.extab*)
*(.ARM.exidx*)
}
.isr_vector ORIGIN(ROM): ALIGN(4)
{
/* Initial stack pointer */
LONG(_stackStart);
/* Interrupt vector table (Entry point) */
KEEP(*(*.power_on_reset));
KEEP(*(*.exceptions));
KEEP(*(*.interrupts));
}>ROM
.text : ALIGN(4)
{
_stext = .;
/* the code */
*(.text)
*(.text*)
. = ALIGN(4);
/* constant datas */
*(.rodata)
*(.rodata*)
. = ALIGN(4);
/* glue arm to thumb code */
*(.glue_7);
*(.glue_7*);
. = ALIGN(4);
_etext = .;
_sidata = LOADADDR(.data);
}>ROM
.bss : ALIGN(4)
{
/* non-initilized data */
_sbss = .;
*(.bss)
*(.bss.*)
. = ALIGN(4);
_ebss = .;
}>RAM
.data : ALIGN(4)
{
_sdata = .;
/* initilized data */
*(.data);
*(.data.*);
. = ALIGN(4);
_edata = .;
}>RAM AT>ROM
/* Need .ctors and probably more as program becomes
* More complex */
}
arm-none-eabi-objcopy -O binary out.elf out.bin
第3章 スタートアップルーチン
純度100%のD言語
通常、プログラムというものは単一の言語で組まれているものは稀です。__図3-1__で示すように、スタートアップルーチンはアセンブラ、ランタイムはアセンブラとC言語とその他言語の組み合わせ、標準ライブラリはC言語の標準ライブラリを利用していて、ユーザープログラムだけは単一の言語、みたいな場合がほとんどです。
本特集では__図3-2__のように、スタートアップルーチンもD言語だけ、ランタイムも標準ライブラリも使わず、ユーザープログラムもD言語、という、頭のてっぺんからつま先まで100%D言語のプログラムでのみ構成されたプログラムでハードウェアを動かしていきます。
図3-1 | 図3-2 |
D言語だけでできるスタートアップルーチン
スタートアップルーチン
スタートアップルーチンとは、マイコンが立ち上がって、一番最初に動作するプログラムです。どのくらい最初かというと、main関数より前に、unittestより前に、staticコンストラクタより前に、ランタイムの初期化より前に、メモリの初期化前に、ハードウェアの初期化前に、実行されます。
というか、ハードウェアを初期化して、メモリを初期化して、ランタイムを初期化して、staticコンストラクタを呼び出して、unittestを呼び出して、main関数を呼び出すプログラムが、スタートアップルーチンです。
ランタイムもメモリ(グローバル変数など)すらも初期化されていない状態で動作するため、そのスタートアップルーチンでは-betterCを指定してなお厳しい制限がかかります。
これをコードにすると、以下のようになります。
extern (C) void entry() nothrow @nogc
{
version(LDC) pragma(LDC_never_inline);
initClock();
initBss(
getAddrOfExtLinkage!"_sbss"(),
getAddrOfExtLinkage!"_ebss"());
initData(
getAddrOfExtLinkage!"_sdata"(),
getAddrOfExtLinkage!"_edata"(),
getAddrOfExtLinkage!"_sidata"());
myMain();
while (1)
{
// Do nothing for unlimited loop.
// Program is terminated
// by this loop.
}
}
ハードウェアの初期化
主にFlashROMや、クロック、割り込みベクタなどの各種設定を初期化します。
初期化なしのグローバル変数を初期化
グローバル変数は、初期化付きかそうじゃないかで配置される領域が異なります。
グローバル変数やプログラムの本体といったデータは以下のようなデータの配置先に配置されます。
データ種類 | セクション名 | 配置先 |
---|---|---|
初期化なしグローバル変数 | .bssセクション | RAM |
初期化付きグローバル変数 | .dataセクション | RAM |
初期化付きグローバル変数の初期値 | .idataセクション | ROM |
プログラム本体 | .textセクション | ROM |
: | : | : |
上記の表では一部ですが、様々な種類のセクションがあります。
初期化なしのグローバル変数は、.bssセクションというRAM上のメモリ区画に配置されます。.bssセクションの中身は0でクリアすることで初期化します。
private @nogc nothrow:
void initBss(void* sbss, void* ebss)
{
uint* dst = cast(uint*)sbss;
while (dst < cast(uint*)ebss)
*dst++ = 0;
}
上記プログラムはsbssのアドレスからebssのアドレスまでをすべて0で埋めるプログラムです。
ポイントとしては、リンカディレクティブにてアラインメントを4バイト区切りにしておくことで、32bitずつ初期化することで効率化しています。
初期化付きのグローバル変数を初期化
private @nogc nothrow:
void initData(void* sdata, void* edata,
in void* sidata)
{
uint* dst = cast(uint*)sdata;
uint* src = cast(uint*)sidata;
while (dst < cast(uint*)edata)
*dst++ = *src++;
}
その他
必要ならば、適所にてスタック領域やヒープの初期化、ベースレジスタの設定などを行います。
第4章 クロック
意外と複雑!クロックの種類
__図4-1__にクロックの種類とそのクロックソースを示したブロック図を示します。
Nucleo F401REはいろいろなクロックソースを試すことができるように、低精度の内臓クロック、ST-LINKからの外部クロック(MCO)、未実装のX3水晶発振子(HSE)、X2による低速クロックの(LSE)、などの供給源を持っていて、基板上にハンダを盛ってジャンパにする程度の簡単な改造によって切り替えることができます。未改造で最大スペックを利用する場合、ST-LINKからの外部クロック(MCO)を使用します。
図4-1
ハードをよく理解してPLLを使いこなせ!
PLLって何?
ここでいうPLLというのは、Phase Locked Loop(位相同期回路)というハードのことで、これを使うことでクロックを逓倍してプログラムを高速動作させます。
ほかの技術分野では、リファレンスとなる波形に同期した信号を生成するみたいな使い方をする制御ロジックをPLLと呼んだりしますが、ここでは関係ありません。
クロック供給回路
Nucleo F401REのクロック供給のための回路はどのようになっているのでしょうか。図Xにクロックに関する回路を示します。
実際に逓倍してみよう
STM32F401REでは84MHzが最大の動作クロックですので、それに合わせた逓倍の設定を施します。また、USBへの供給は48MHzです。
一方クロックの供給源は外部クロックで8MHzですので、この周波数をうまいこと、各ハードウェアの動作可能な周波数に設定していきます。
VCOクロックは次式によって求めます。
$$ f_{VCO clock} = \frac{外部クロック \times PNN_N }{ PLL_M } $$
PLLクロックは次式によって求めます
$$ f_{PLL clock} = 84MHz = \frac{ f_{VCO clock} }{PLL_P} $$
USBへ供給するクロックは次式によって求めます
$$ f_{USB clock} = 48MHz = \frac{ f_{VCO clock} }{PLL_Q} $$
// 外部クロック(8MHz)を使用
RCC.PLLSRC = 1;
// VCOクロックは 8 * 336 / 8 = 336MHz
// PLLクロックは 336 / 4 = 84MHz
// USBクロックは 336 / 7 = 48MHz
RCC.PLLM = 8;
RCC.PLLN = 336;
RCC.PLLP = 0b01;
RCC.PLLQ = 7;
// PLL開始
RCC.PLLON = true;
PLLの注意点
PLLを有効にすると上がったクロックにハードウェアがついてこれなくなってしまうことがあります。
STM32F401REの場合はフラッシュアクセスが問題になります。PLLでクロックを上げると、フラッシュメモリからプログラムを呼び出して実行にかけるまでの時間より、実行時間のほうが速くなってしまって問題を生じさせるのです。これを回避するために、フラッシュ読み込みのキャッシュや命令のプリフェッチを有効化しておきます。
// FLASHの速度設定
FLASH.LATENCY = 2;
FLASH.PRFTEN = true;
FLASH.ICEN = true;
また、PLLはハードウェアによって行いますので、ハードウェアが安定するまでわずかながら時間がかかります。ハードウェアの安定までプログラムは動かさず、待機する必要があります。
// 安定待ち
while (!RCC.PLLRDY)
nop();
RCC.SW = 0b10;
while (RCC.SWS != 0b10)
nop();
周辺回路に供給するクロックをきめよう!
モジュールごとに適切なクロック供給
ハードウェアによって最大限供給できるクロックが違います。また、クロックを上げることで消費する電力も変わってきますので、適切なクロックを供給するようにします。AHB、APB1、APB2、RTCなど、複数のモジュールをひとまとめにした単位でクロックを供給します。
// 周辺(AHB)は84MHzで動作
RCC.HPRE = 0b000;
// 周辺(APB1)は42MHzで動作
RCC.PPRE1 = 0b100;
// 周辺(APB2)は84MHzで動作
RCC.PPRE2 = 0b000;
// RTC不使用
RCC.RTCPRE = 0b000;
第5章 GPIO
GPIOは回路によって使い分けろ!
GPIOの基本
GPIOは、目的によって異なる回路構成ごとに、適切な使用方法があります。また、マイコンのGPIOの内部回路にもさまざまな種類があり、適切な使い分けが必要となります。
回路の状態
デジタルな回路として、3種類の状態を覚えておきましょう。一つはLow、一つはHigh、もう一つは、Hi-Z(ハイインピーダンス)と呼ばれる状態です。
LowとHighはわかりやすいですね。回路のGND側と同電位になればLow、プラス側と同電位になればHighです。もう少し厳密にいうとマイコンの電気的特性によって1.5Vなどといった閾値があったり、ヒステリシスの特性(ON→OFFの閾値とOFF→ONの閾値が違ったりする特性、シュミットトリガ回路)があったりします。
もう一つの状態であるHi-Zはなじみが薄いかもしれません。「回路的に切断された状態」など、とても抵抗値(Z:インピーダンス)が高い状態が、Hi-Zの状態です。この状態はHighでもLowでもなく、電圧的には不安定な状態になります。その代わり、電流が流れないという特徴があります。逆に言うと、Highはマイコンから外に向かって電流が流れやすく、Lowはマイコンの内部へと電流が流れやすい状態になります(流れる電流にはポートごとに上限があります)。GPIOでは、「マイコン内の」これらの状態を適切にコントロールする機能が備わっています。
アクティブ論理
デジタルな状態のうち、ハードウェアを「アクティブ」な状態にする方法は異なります。それはLEDを点灯する回路にとっても同じです。すなわち、HighにしたらLEDが光るのか、LowにしたらLEDが光るのか、というのが回路によって異なります。__図5-1__の(a)にHighアクティブなLED点灯回路、(b)にLowアクティブなLED点灯回路を示します。
マイコンの内側の回路と外側の回路
マイコンの外側の回路は基板の回路によって固定ですが、マイコンの内側の回路はソフトの設定によって切り替えることができます。マイコンの内側の状態と、外側の状態が組み合わさることで初めて回路の状態が確定します。
回路構成:プルアップ
プルアップ回路というのは、電源のプラス側と抵抗を介して接続される回路のことです(図5-2)。
このようにすることで、ポートをHi-ZにすることでHighの状態に、ポートをHighにすればHighに、ポートをLowにすればLowにすることのできる回路となります。電圧に不安定な状態がなくなるのが特徴です。また、回路に使用する抵抗(プルアップ抵抗と呼びます)の抵抗値によって、流れる電流の上限が制限されます。プルアップ抵抗は回路保護に役立ちますが、電流が制限されるため大きな電力取り出せないこと(例えばLEDが十分な輝度で光らせられないなど)を意味します。
回路構成:プルダウン
プルダウン回路はプルアップ回路の逆で、GNDと抵抗を介して接続される回路のことです(図5-3)。
ポートをHi-ZにすることでLowの状態に、ポートをHighにすればHighに、ポートをLowにすればLowにすることのできる回路となります。プルアップ回路同様電圧に不安定な状態がなくなるのが特徴です。流れる電流の上限が制限されるのもプルアップ回路と同様です。
回路構成:素子直結
LEDやトランジスタなどの素子に対して、流れ込む/流れ出す電流が規定値以内であるならば、マイコンポートと直接接続してやることができます。図X(a)はLEDに直結、図X(b)はNPNトランジスタに直結しています。この場合、マイコンの内部をプッシュプル回路や、オープンドレイン回路にすることで電流の流し方を制御したりします。
回路構成:NC
NCはNot Connectedの意味で、何も接続されていない状態を意味します(図5-5)。
この状態でマイコンの設定もオープン(ハイインピーダンス)に設定すると、電圧の状態は不安定な状態になり、場合によっては静電気の影響で壊れてしまったりします。外側がNCの場合、マイコン内の回路はプルアップかプルダウンのいずれかにしておくのがよいでしょう。
マイコン設定:プルアップ
マイコン内部の回路をプルアップ回路とすることで、マイコンポートの状態をHighにすることができます(図5-6)。ただし、外側の回路同様電流などに制限があります。この回路構造は、入力設定でも出力設定でも使用することができます。
マイコン設定:プルダウン
マイコン内部の回路をプルダウン回路とすることで、マイコンポートの状態をLowにすることができます(図5-7)。ただし、外側の回路同様電流などに制限があります。この回路構造は、入力設定でも出力設定でも使用することができます。
マイコン設定:プッシュ・プル
マイコン内部の回路のトランジスタを使用することで、比較的大きな電流をマイコン内外へと出力することができる出力設定です(図5-8)。
プッシュで電流をマイコンの外に向かって出して、プルで電流をマイコン内に引っ張ります。プルはドレインの状態と同じです。
図5-8 プッシュ・プル設定
プッシュ・プル回路はオープン状態にならず、電流を外側に向けて流すプッシュか電流を内側に向けて流すプルかの2択の出力回路です。
マイコン設定:オープン・ドレイン
オープンでマイコン内部の回路をHi-Zに、ドレインですることができる出力設定です(図X)。
プルの状態と同じです。マイコン内部の回路のトランジスタを使用することで、比較的大きな電流をマイコン内へと流することができる出力設定です(図5-9)。オープン・ドレイン回路として使用する際にドレインと呼びます。ちなみに、内部回路がトランジスタだとコレクタと呼び、FET(電界効果トランジスタ)だとドレインと呼びます。STM32F401REだとオープン・ドレインと書いてあるので、内部にはFETが使われています。
図5-9 オープンドレイン
オープン・ドレイン回路は電流を外向きに出さず、Hi-Zか、ドレインで内側に流し込むかの2択の出力回路です。
これが組込界の"Hello, world!"Lチカだ!
Lチカの基本
LEDをチカチカさせるプログラム、通称Lチカ。IOポートから出力される信号によってマイコン外部に備え付けられたLEDを点滅させることでマイコンの中でプログラムが動いていることを確かめるプログラムです。
mbedのLチカプログラム
次のソースコードは、mbedのサンプルコードで提供されるC++の単純なLチカプログラムです。ちなみに、mbedというのはARM社のプロトタイピング用ワンボードマイコンおよびそのデバイスのプログラミング環境です。WEBベースのIDE(統合開発環境)で、豊富なサンプルと、ビルドすると即実行可能なバイナリをダウンロードすることができるお手軽さが、組み込みプログラミングのハードルをぐっと下げています。(ただしD言語には非対応です。)
#include "mbed.h"
DigitalOut myled(LED1);
int main() {
while(1) {
myled = 1; // LED is ON
wait(0.2); // 200 ms
myled = 0; // LED is OFF
wait(1.0); // 1 sec
}
}
mbedでは疑似的なOSのようなフレームワークを搭載しており、main関数の前段にて既存のスタートアップルーチンを動かし、ハードウェアの初期化とかグローバルなオブジェクトインスタンスのコンストラクタの呼び出しなどをして、きわめて簡単なプログラムで初歩的なコードを動かすことができるようになっています。
上記のサンプルコードでも、mbedの中身を読み解くと、スタートアップルーチンの中でクロックの設定や、0.2秒や1.0秒を正確にwaitするためのタイマモジュールの初期化、DigitalOutのコンストラクタ呼び出し内でのGPIO初期化など、見えないところでたくさんのプログラムが小難しいところを吸収していることがわかります。
LチカのためのGPIO
__図5-10__にNucleo F401REのボードの回路を示します。
図5-10 LD2点灯回路図(ボード66/69)
マイコンポートPA5から510Ωの抵抗を介してアノード側に接続され、LEDのカソード側がGNDに接続されています。これは素子に直接接続されたHighアクティブの回路構成です。
ユーザで使用できるLEDはLD2というLEDですが、この回路構成を見ると、LEDのアノード側が抵抗を介してマイコンポートに接続されています。プルアップもプルダウンもされていないため、素子に直接接続されたHighアクティブの回路構成になります。これに適したマイコン内部回路は、プッシュ・プル型の回路です。プッシュにすることでマイコンポートをHighにして電流をLEDに流し込んでLEDを点灯し、プルにすることでマイコンポートをLowにしてマイコン内へ電流を(0Aですが)流してLEDを消灯します。マイコン内外を合わせた回路は__図5-11__のようになります。
D言語でLチカしてみよう
mbedのサンプルプログラムと同様のことをするためには、waitのためにタイマを使用する必要がありますが、nop命令(何もしないけどクロックを消費する命令)をたくさん繰り返せば似たようなことができます。
まず、GPIOの設定を「プッシュ・プル型」の「出力ポート」に設定します。
void initGpio() nothrow @nogc
{
// GPIOAを有効にします
RCC.GPIOAEN = true;
// PA5を出力に
GPIOA.MODER5 = 0b01;
// プッシュプルに設定
// (デフォルトから変更しない)
GPIOA.OT5 = 0b00;
}
次に、ポートをプッシュ・プルに交互に切り替えるプログラムを作ります。
// プッシュにしてLEDをオンします
void setLedOn()() nothrow @nogc
{
GPIOA.BS5 = 1;
}
// プルにしてLEDをオフします
void setLedOff()() nothrow @nogc
{
GPIOA.BR5 = 1;
}
// 定常時実行処理
void runStead() nothrow @nogc
{
while (1) {
setLedOn();
wait();
setLedOff();
wait();
}
}
wait関数は、nop命令を使って次のようにしました。nop()にはインラインアセンブラを使用しており、最適化されないのが特徴です。
// 何もしないで待機する
void wait() nothrow @nogc
{
foreach (i; 0..1000000)
nop();
}
// 何も実行しない命令
pragma(inline)
void nop() pure nothrow @nogc @trusted
{
pragma(LDC_allow_inline);
__asm("nop", "");
}
実用的Lチカプログラム
何もしないでプログラムを待機するwaitを使ってLチカするのは非常に簡単で便利ですが、実際の組み込みプログラミングではそのようなことは普通しません。
WindowsやLinuxのようなOSを使ったマルチスレッドのように、時間を待機している間は別のプログラムを動かすことで様々な処理を並行して実行するのが普通です。
このようなことをするには、「タイマ」を裏で動かしつつ、タイマがタイムアウトしたタイミングで「割り込み」を発生させてやるのが良いでしょう。
__図5-12__は0.5秒おきにLEDを点けたり消したりするトグル動作を行うシーケンス図です。
タイマが0.5秒おきに駆動され、それが「Lチカ」の割り込み処理を実行し、「Lチカ」の処理を行っていないときには別プログラムが動作するというものです。このようにすることで効率よく様々な処理を動作させることが可能です。
図5-12 シーケンス図
第6章 割り込み
割り込み処理の基礎を押さえよう!
割り込み処理
タイマなどによって割り込み処理が実行される場合、別のプログラムが実行されることになりますが、その時マイコンの中では何が起こっているのでしょうか。
割り込み処理が実行されると、レジスタやプログラムカウンタなどが退避されて、スタックに割り込みサービスルーチン(ISR:Interrupt Service Routine)のスタックが積まれます。この動作は、OSのある環境での、マルチスレッドにおける「コンテキストスイッチ」に非常に似ています。
割り込みベクタ
割り込み処理はハードウェア等によって引き起こされ、要因に対応した関数が割り込み処理(ISR)として呼び出されます。その際、どの割り込み要因でどの関数を呼び出せばいいかを決めているのが、割り込みベクタと呼ばれるものです。割り込みベクタは、その名の通り割り込み処理の関数の先頭アドレスを配列(vector)にして並べた構造をしています。
割り込み要因
割り込み要因というのは、読んでそのまま、割り込みを発生させる要因のことです。特にハードウェアに特殊な設定を行うことで割り込みを生じさせるように指示を行うことができます。ここでいうハードウェアというのは「タイマ回路」だったり「A/D変換回路」だったり、「マイコンのIOポートの電圧立ち上がり」だったりします。割り込み要因となるものについてはマイコンごとに特徴があります。とりわけタイマだけに絞っても、細かな挙動はマイコンごとに異なり、その設定も千差万別です。
割り込み優先度
割り込みには優先度という考え方があります。優先度をどのように処理するかについてもマイコンによって異なりますが、多くのマイコンでは割り込み処理中、より優先度の高い割り込みが発生したらそちらを実行し、より優先度の低い割り込みが発生した場合は現在の処理が終わるまでペンディングするといった動作になる場合が多いです。
__図6-2__に、処理の優先度が適用されている様子を示します。割り込み優先度1が最も優先度高く、優先度15が一番低い場合で、5→8→2の優先度の割り込みが発生した場合を示しています。5よりも8のほうが優先度が低いため、8の割り込みが発生しても5の処理が終わるまで待機します。一方2の割り込みは5よりも優先度が高いため、5の処理の途中で2の処理が実行され、2の処理が終了したら5の処理が再開されます。そして5の処理が終了したら、8の処理が実行されるという流れとなります。
マルチスレッドのような何か
割り込み処理がマルチスレッドに似ているというのは、気を付けなければいけないことについてもとても似ています。一例として、共有資源の読み書きについては、同じような注意点があります。すなわち、「二つのスレッド(割り込み処理)で同時に読み書きしてはいけない」とか「クリティカルセクションの考え方」とか。
共有資源にsharedを使う
D言語のマルチスレッドに対する強力なツール「shared型」ですが、これが割り込み処理を使う上でも非常に役立ちます。
組み込みプログラムでは、ハードとソフトが非常に密接に関連する関係上、プログラムの大部分がSingletonのクラスになっていきます。Singletonと言えばかっこいいですが、要するにD言語のmoduleがオブジェクト指向の「クラス」のようなイメージとなり、グローバル変数を使うことで割り込み処理とユーザープログラム間の橋渡しをするのが楽ちんだったりします。
// グローバル変数 timerCount3
// メイン関数とタイマ割り込みで
// 読み書きを実施するため、
// 共有資源(sharedストレージクラス)
shared uint timerCount3 = 0;
スコープガード文で割禁
クリティカルセクションを比較的簡単に実現するための方法として「割り込み禁止」が挙げられます。
クリティカルセクションに入るときに割り込み禁止を行い、クリティカルセクションから出るときに割り込み許可を行います。
if (timerCount2 == 2000*5)
{
// このスコープはクリティカルセクション
disable_irq();
scope (exit)
enable_irq();
timerCount3 = 0;
}
デリゲート呼び出し
割り込み処理のハンドラとして、デリゲートを呼び出すことができると便利です。D言語のデリゲートには大きく分けて2種類あって、クロージャかそうでないかが大きな違いとしてあります。クロージャは動的なメモリ確保が必要ですので、nogcにおいてはクロージャを使用することはできません。一方、クロージャじゃない普通のデリゲート(メンバ関数をデリゲートにした場合など)はnogcでも使用することができます。以下のような関数を作って、delegateに関数を設定できるようにすると便利です。(Phobosのstd.functional.toDelegateのようなことをしています。このようなことをしても、ABI的には正しいです)
/// デリゲート
alias Callback =
void delegate() @nogc nothrow;
/// パラメータ無し関数
alias CallbackFunc =
void function() @nogc nothrow;
/// パラメータ付き関数
alias CallbackFuncWithParam =
void function(void* p) @nogc nothrow;
///パラメータ付き関数を設定
void set(ref Callback target,
CallbackFuncWithParam callback,
void* param)
pure @nogc nothrow @trusted
{
union CallbackImpl {
Callback dg;
struct {
void* contextPtr;
typeof(callback) funcPtr;
}
}
CallbackImpl impl = {
contextPtr: param,
funcPtr:callback
};
target = impl.dg;
}
/// パラメータ無しの関数を設定
void set(ref Callback target,
CallbackFunc callback)
pure @nogc nothrow @trusted
{
union CallbackImpl {
Callback dg;
struct {
void* contextPtr;
typeof(callback) funcPtr;
}
}
CallbackImpl impl = {
contextPtr: null,
funcPtr:callback
};
target = impl.dg;
}
/// デリゲートを設定
void set(ref Callback target,
Callback callback)
{
target = callback;
}
ISRを割り振ろう
割り込みベクタを定義する
割り込みベクタは、マイコンのFlashROM上の所定のアドレスに配置されなければなりません。LDCには、そのような用途として使用できるよう、@section
という特殊なUDAが存在しています。
@section(".isr_vector.interrupts")
extern(C) __gshared immutable ISR[85]
_isr_inerrupts = [
// 0x0040 WWDG
&isrDefaultHandler,
// 0x0044 EXTI16 / PVD
&isrDefaultHandler,
// 0x0048 EXTI21 / TAMP_STAMP
&isrDefaultHandler,
// 0x004C EXTI22 / RTC_WKUP
&isrDefaultHandler,
// :
// :
リンカディレクティブでの指定
@section
という特殊なUDAを使うと、コンパイラはどのセクションに配置するかについてオブジェクトファイルに情報を付与します。それを受けて、リンカがどのアドレスに何を配置するかをリンカディレクティブを見ながら決定していきます。
つまり、@section
に対応したセクションを定義したリンカディレクティブ(*.ldファイル)を作成する必要があります。
.isr_vector ORIGIN(ROM): ALIGN(4)
{
/* Initial stack pointer */
LONG(_stackStart);
/* Interrupt vector table (Entry point) */
KEEP(*(*.power_on_reset));
KEEP(*(*.exceptions));
KEEP(*(*.interrupts));
}>ROM
第7章 タイマー
タイマーの基本
タイマーでできること
タイマーは時間を測ることのできるモジュールです。ほかに時間を扱うものとして、RTC(Real Time Clock)がありますが、こちらは時間(Duration)というよりも時刻(DateTime)を取り扱うものといえるでしょう。
タイマーでは、RTCのように、その時点での時刻(何月何日何時何分何秒…)の情報を得ることはできません。時計よりも、ストップウォッチやキッチンタイマーのような使い方をイメージするとよいでしょう。
タイマーの基本的な機能としては、ある時点を起点として、何クロック分経過したかをハードウェアにて測定することのできるモジュールです。
この機能を使うことで、タイマーをスタートしてからの時間を得たり、ある時間が経過したらタイムアウトを知らせるフラグを立てたり、何らかのイベントを発生させるなどといった使い方をすることが可能です。ここでいうイベントというのは、例えば割り込みの発生であったり、データ転送やA/D変換といった別のハードウェアの起動だったり、ポート出力のON/OFFだったりします。
使い方
タイマーの動作原理はいたって単純なものとなっております。1クロックで1カウンタを増加させる、というのが基本(図X)で、たいていのマイコンに備わっている機能です。応用としては、マイコンによりますが、例えば3回間引いて、1000から1ずつ減少させる、といった間引きや、ダウンカウンタの動作(図X)をさせることもできます。図Xに示す例では、ある一定値まではアップカウンタ、その後は0になるまでダウンカウンタ、という三角波のような動作を行わせることもできます。また、図Xのようにとある値までカウントアップしたらカウント値をクリアして再度カウントアップを継続するといった、周期タイマーのような使い方をすることもあります。
また、図Xのように一定の値にマッチしたかどうかを監視する、コンペアマッチ機能が備わっているものもごく一般的なタイマーの機能として挙げることができます。
特にコンペアマッチ+ポート出力の切り替えを組み合わせたものを、PWMタイマーなどと呼んだりすることもあります。
精度
タイマーの精度は動作原理から分かるように、クロックの精度に依存します。クロックの発生源にもいろいろなものがありますが、一般的にはLC回路やCR回路による発振は精度が悪く(1日に数十分のずれ)、セラミック発振子の精度はそこそこ(1日に数百秒のずれ)、水晶発振子の精度は高い(1か月に数秒のずれ)として知られています。目的に応じてクロックの精度を選定する必要があります。どうしてもタイマーの精度が足りない場合には、タイマーに頼らないというのも手です。例えば商用電力系統(一般家庭のコンセントに来ている100Vの電圧)の50Hzや60Hzといった周波数が長期的に見れば精度の高い周波数として知られています(電力会社がピッタリになるように調整してくれているため)。
なお、Nucleo F401REには、水晶発振子によるクロックの発生源が実装されていますので、非常に高い精度を期待することができます。
秒数とカウント値と周波数
タイマーはクロックによって1ずつ増えるカウンタを利用しているため、実際の時間が何クロックかを計算して使用する必要があります。
秒数からタイマーのカウント値を算出する方法は以下の式で求めます。
xがタイマーの数値、tが秒数、fがタイマーの動作周波数です。
x = t \times f
逆に、カウント値から秒数を算出する方法は次式で求めます。
t = x \div f
周期タイマーの場合は、タイマーがクリアされる周波数が重要になることもあります。その周波数をFとすると、Fは次式で求めます。
F = f \div x
逆に、タイマーがクリアされる周波数からカウント値を求めたい場合は次式となります。
x = f \div F
タイマーの動作周波数が84MHzで20kHzの周期タイマーを作りたい場合は、クリアされるカウント値に $84000000 \div 20000 = 4200$ を設定すればよい、ということになりますね。
タイマーのカウント値と秒数や周波数を対応させる際の注意点として、タイマーの動作周波数がマイコンの動作周波数と異なる点に注意してください。
動作周波数
STM32F401REは動作周波数が最大84MHzですが、ペリフェラルの動作周波数は分周されることが多かったり、かと思えばタイマーだけ特別扱いで動作周波数を高くできたりします。たとえばTIM2(APB1に属しています)のペリフェラル動作周波数はマイコンの動作クロックを2分周する必要があり最大で$f=42MHz$なのですが、タイマーだけは特別扱いで、ペリフェラルの動作周波数を2分周していた場合に限ってペリフェラルの動作周波数が2逓倍されます。つまり、$84MHz \div 2 \times 2 = 84MHz$となります。TIM2ではこうなりますが、ほかのタイマーや設定によってはではこの限りでなかったりします。マイコンのハードウェアマニュアルとにらめっこして、よくよく調査を行いましょう。TIM2で100msのカウント値を求めたい場合は、$0.1 \times 84000000 = 8400000$となります。
タイマーのビット数
カウントできる最大値がタイマーのビット数に依る点で、例えば上記で求めた8400000という数値は16ビットタイマーでは扱うことができません。STM32F401REでは以下の表のようになります。
TIM2, TIM5以外のタイマーは16bitなので注意が必要です。
タイマ | ビット数 | ドメイン | 最大動作周波数 |
---|---|---|---|
TIM1 | 16bit | APB2 | 84MHz |
TIM2 | 32bit | APB1 | 84MHz |
TIM3 | 16bit | APB1 | 84MHz |
TIM4 | 16bit | APB1 | 84MHz |
TIM5 | 32bit | APB1 | 84MHz |
TIM9 | 16bit | APB2 | 84MHz |
TIM10 | 16bit | APB2 | 84MHz |
TIM11 | 16bit | APB2 | 84MHz |
プリスケーラ
タイマーのビット数が少ない場合、次に考えるのは間引きです。間引き数を指定するのは、「プリスケーラ」と呼ばれる機能によって実現します。
TIM3やTIM4は16bitタイマーですので、100msを測定したい場合はプリスケーラによって分周を行う必要があります。クロック1000回に一回カウントアップするといった処置を行うことで、カウントできる秒数は大幅に増加します。$ 0.1 \times 84000000 \div 1000 = 8400$となり、65535以下となるため、測定が可能になります。
タイマーで割り込み処理を呼び出そう
割り込みの設定
タイマーで割り込みを発生させるには、いくつかのステップが必要となります。__図7-1__にフローチャートで必要な工程を示します。
一口にタイマーの動作設定といっていますが、具体的にはアップカウント・ダウンカウントの選択や、カウントの初期値、プリスケーラやコンペアマッチの値などが必要となります。
割り込み要因との有効化と割り込みの有効化がありますが、前者はタイマーというハードウェアが割り込み要因として機能するかどうかの選択、後者はハードからの割り込み要因の発生をうけて割り込み処理を走らせるかどうかの設定です。
リアルタイム性
周期タイマーで割り込みを発生させる場合に重要になる考え方に、「リアルタイム性」というものがあります。これは、割り込みが発生してから、次の割り込みが発生するまでの間に処理を終えることができるかどうかの性質、とも言えます。例えば0.5秒ごとに割り込みが発生して割り込み処理を実行するのであれば、その割り込み処理は絶対に0.5秒を超えてはならないことを意味しています。これを守ることができない場合、性能の悪化(割り込みが一定時間ごとに発生することを前提にした処理ならば、結果が狂う)や、最悪マイコンの暴走(処理が終わらないうちに次の割り込みが入るのが重なってスタックオーバーフローするなど)に至る場合もあります。
かといって、0.5秒ごと割り込みに対して0.5秒フルに処理に時間を使えるわけでもありません。フルに処理時間を使ってしまうと、その割り込み処理以外の処理が動作しなくなってしまうためです。80%程度の負荷率(0.5秒ごとの割り込みなら0.4秒まで処理に充てられる)を目安にするとよいでしょう。
Lチカのためのタイマー設定をしよう
動作設定
LEDをチカチカ点滅させるという仕様に対して、適切なタイマー設定を行う必要があります。
0.5秒ごとに点いたり消えたりを繰り返すように設定したいと思います。しかしながら、単純に0.5秒で割り込みを発生させるタイマーを作ってもいいのですが、よりつぶしのききやすい方法として、タイマー割り込みはより細かなタイミングで呼び出し、ソフトウェアで間引きを行う方法があります。
タイマーの動作設定例
以下のコードは、TIM2を500usごとに1回割り込みが行われるように設定した例となっています。これをタイミングチャートにしたものが__図7-2__となります。
//----------------------------------
// タイマ2の設定
//----------------------------------
RCC.TIM2EN = true;
// APB1 timer clocksは84MHzで動作
// (APBxPRESC != 1 -> x2)
// プリスケーラ 1/8 * 84MHz = 10.5MHz
TIM2.PSC = 8-1;
// アウトプット(コンペアマッチ)モード
TIM2.CC1S = 0;
// カウント方向はアップカウント
TIM2.DIR = 0;
// オーバーフローで割り込み
TIM2.UIE = 1;
// カウント初期値0
TIM2.CNT = 0;
// 0.0005s * 10.5MHz
// ARRのマッチでゼロクリア
TIM2.ARR = 5250-1;
// コールバックの設定
onTim2.set(&onInterval);
// カウンタ開始
TIM2.CEN = true;
// 割り込み優先度設定
NVIC.IPR[35] = 7;
// 有効化時に割り込み処理を走らせる
NVIC.SETPEND28 = true;
// 割り込みを有効化
NVIC.SETENA28 = true;
500usごとにカウントアップして0.5秒を測定するソフトウェアタイマーを使用するので、割り込みハンドラは以下のようになります。
タイミングチャートにすると__図7-3__のようになります
__gshared uint timerCount = 0;
__gshared bool stsLedToggle;
/// 500us毎割り込み
void onInterval() nothrow @nogc
{
timerCount++;
if (timerCount > 1000)
{
timerCount = 0;
// 0.5s毎
if (stsLedToggle)
setLedOn();
else
setLedOff();
stsLedToggle = !stsLedToggle;
}
}
私のリポジトリ
D言語の言語機能は組み込み分野においても強力な性質を示します。一方で誤った使用方法をするとFlashROM上でのコードサイズを膨大なものとしたり、意図しないコードを紛れ込ませてしまったりすることもあります。
こういった事故を未然に防ぐには言語機能への適切な理解が不可欠です。
本稿では組み込みプログラミングにおいて有効なD言語の言語機能の使い方を紹介していきます。
テンプレートを使いこなそう!
不要なコードをコンパイルしないためのテンプレート
D言語のtemplateは、実際にインスタンス化されない限り、実際の機械語に落とし込まれることはありません。(つまり、コンパイル後のオブジェクトファイル*.o
ファイルに関数本体が含まれなくなります)
しかしながら、テンプレートは引数が異なる場合それぞれの引数に対してインスタンスが生成されるという特徴も持ち合わせます。
これを正しく理解せずに無駄にたくさんのインスタンスを生成してしまうと、あっという間にFlashROMを使い果たしてしまいます。
ところで、D言語のtemplateには、引数が必須ではありません。すなわち、引数がないテンプレートの場合、インスタンスの数は高々1つに限定されるため、不要に多くのインスタンスが生成されることもありません。この特徴を用いることで、無駄なコードサイズ増加を防ぐことが可能です。これは、特に「使うかもしれない」ライブラリのようなモジュールにおいて非常に強みとなってきます。
以下のコードはちょっとだけ待ち時間を挿入するwait関数をテンプレート化したものです。たぶん使わないけどデバッグ用には使うかもしれない、そんな関数のため、使わないならコンパイルされてほしくありません。そんな場合に便利ですね。
void wait()() nothrow @nogc
{
foreach (i; 0..1000000)
nop();
}
もっとも、使われない関数は arm-none-eabi-ld
と一緒に --gc-sections
を指定することで消し去ることができるのですが…。
ビットフィールド
マイコンの設定を行うためにたびたび出てくるレジスタは、基本的にビット単位で機能が異なり、アクセスもビット単位であることが非常に多いです。
こういった用途のために、D言語の標準ライブラリであるところのPhobosにはstd.bitmanip
というモジュールにbitfieldsというヘルパテンプレートがあります。以下のようなコードで、いい感じのデータ型を作ってくれます。
mixin(bitfields!(
uint, "x", 2,
int, "y", 3,
uint, "z", 2,
bool, "flag", 1));
しかしながら、druntimeやPhobosの使用を禁じられた-betterCにおいては、これを使用することはできません。テンプレートなので実際中身でdruntimeに依存していなければいけるだろうという思いもありましたが、残念ながら使用することができませんでした。
似たような機能を、自分で実装する必要があります。
ビットフィールドの実装
template _offsetSumOfBitfield(Args...)
{
static if (Args.length < 3)
{
enum _offsetSumOfBitfield = 0;
}
else
{
enum _offsetSumOfBitfield = Args[2] + _offsetSumOfBitfield!(Args[3..$]);
}
}
static assert(_offsetSumOfBitfield!("", "", 1, "", "", 1) == 2);
static assert(_offsetSumOfBitfield!("", "", 1, "", "", 2, "", "", 3) == 6);
static assert(_offsetSumOfBitfield!("", "", 1, "", "", 5, "", "", 8) == 14);
static assert(_offsetSumOfBitfield!("", "", 6, "", "", 5, "", "", 4) == 15);
enum _maskOfBitfield(uint x) = (1 << x) - 1;
static assert(_maskOfBitfield!1 == 0b00000000000000000000000000000001);
static assert(_maskOfBitfield!2 == 0b00000000000000000000000000000011);
static assert(_maskOfBitfield!3 == 0b00000000000000000000000000000111);
static assert(_maskOfBitfield!4 == 0b00000000000000000000000000001111);
static assert(_maskOfBitfield!5 == 0b00000000000000000000000000011111);
///
template defineBitfields(alias dat, Args...)
{
static foreach (i; 0..Args.length/3)
{
mixin(`pragma(inline) Args[i*3+0] ` ~ Args[i*3 + 1] ~ q{()() nothrow @nogc @trusted const @property
{
static if (is(Args[i*3+0] == bool))
{
static assert(_offsetSumOfBitfield!(Args[0..i*3]) < dat.sizeof*8);
return cast(bool)bt(cast(size_t*)&dat, _offsetSumOfBitfield!(Args[0..i*3]));
}
else
{
enum mask = _maskOfBitfield!(Args[i*3+2]);
if (!__ctfe)
{
return (volatileLoad(&dat) >> _offsetSumOfBitfield!(Args[0..i*3])) & mask;
}
else
{
return (dat >> _offsetSumOfBitfield!(Args[0..i*3])) & mask;
}
}
}});
mixin(`pragma(inline) void ` ~ Args[i*3 + 1] ~ q{()(Args[i*3+0] val) nothrow @nogc @trusted @property
{
static if (is(Args[i*3+0] == bool))
{
static assert(_offsetSumOfBitfield!(Args[0..i*3]) < dat.sizeof*8);
if (val)
{
bts(cast(size_t*)&dat, _offsetSumOfBitfield!(Args[0..i*3]));
}
else
{
btr(cast(size_t*)&dat, _offsetSumOfBitfield!(Args[0..i*3]));
}
}
else
{
enum ofsbits = _offsetSumOfBitfield!(Args[0..i*3]);
enum bitsmask = _maskOfBitfield!(Args[i*3+2]);
enum typeof(dat) mask = bitsmask << ofsbits;
enum typeof(dat) invmask = cast(typeof(dat))~cast(int)mask;
if (!__ctfe)
{
auto tmp = volatileLoad(&dat);
volatileStore(&dat, cast(typeof(dat))((invmask & tmp) | (val << ofsbits)));
}
else
{
dat = cast(typeof(dat))((invmask & dat) | (val << ofsbits));
}
}
}});
}
}
static assert((_maskOfBitfield!(1) << _offsetSumOfBitfield!(
uint, "PLLM", 6,
uint, "PLLN", 9,
uint, "PLLCFGR_reserved_bit15_15", 1,
uint, "PLLP", 2,
uint, "PLLCFGR_reserved_bit18_21", 4,
)) == 0b00000000010000000000000000000000);
static assert(~(_maskOfBitfield!(1) << _offsetSumOfBitfield!(
uint, "PLLM", 6,
uint, "PLLN", 9,
uint, "PLLCFGR_reserved_bit15_15", 1,
uint, "PLLP", 2,
uint, "PLLCFGR_reserved_bit18_21", 4,
)) == 0b11111111101111111111111111111111);
レジスタ設定は慎重に!
volatile
レジスタの値を変更する場合に注意しなければならない点として、「確実に目的のレジスタを変更する」ことです。
これを実現するため、C言語には「volatile」という予約語がありました。volatileとは「揮発性の」という意味で、確実に揮発性のメモリである一時記憶装置に対しての操作を約束するものです。これの一番大きな意義は、「汎用レジスタではない場所に書き込むことを保証してくれる」という点です。裏を返せば、volatileでない場合、最適化の結果、汎用レジスタだけで完結するために、実際にはメモリに対する操作が行われない場合がある、ということを意味しています。(これがvolatileが最適化を抑止するという一般的な知られ方につながっていきます)
しかしながら、残念なことにD言語にはvolatileのキーワードはありません。LDCではこれについて配慮されており、独自のpragmaが備わっています。
pragma(LDC_intrinsic, "ldc.bitop.vld")
uint volatileLoad(in uint* ptr)
pure nothrow @nogc @trusted;
pragma(LDC_intrinsic, "ldc.bitop.vst")
void volatileStore(uint** ptr, uint value)
pure nothrow @nogc @trusted;
これらのpragmaを指定した関数は自動的に定義され、これで読み書きを行うことで、汎用レジスタに最適化されることなく確実に目的のレジスタを操作することができます。
インラインアセンブラ
なるべく使用するべきではありませんが、マイコン特有の特殊レジスタにアクセスするためなど、どうしてもアセンブラの力を借りなければならないことも多いです。
そのようなときにはインラインアセンブラによって対処が可能です。例えば、何もしない命令であるNOP命令のインラインアセンブラは以下になります。
pragma(inline)
void nop() pure nothrow @nogc @trusted
{
pragma(LDC_allow_inline);
__asm("nop", "");
}
残念ながら、D言語の仕様でインラインアセンブラが定義されているものの、X86のインラインアセンブラに限った内容となっています。
https://dlang.org/spec/iasm.html
このため、ここで使用しているインラインアセンブラはLDCの独自仕様となっているようです。
https://wiki.dlang.org/LDC_inline_assembly_expressions#ARM
外部リンケージのシンボルにアクセス
レジスタとは直接関係ありませんが、似たようなものに外部リンケージのシンボルへアクセスすることがあります。具体的にはリンカディレクティブを指定するファイル*.ld
にて、定義するアドレスの参照などです。
この場合、アクセスのためのシンボル名が厳密で、D言語のmangle規則などが適用されてはいけないものとなります。以下のようにしてアクセスすると確実でしょう。
template getAddrOfExtLinkage(string name)
{
mixin(`
extern extern (C)
pragma(mangle, "`~name~`")
__gshared uint `~name~`;
pragma(inline) void* getAddrOfExtLinkage()
nothrow @nogc @trusted
{
return &`~name~`;
}
`);
}