Arduinoのブートローダや起動処理について仕組みを理解することで、組み込み機器がどのように起動するのかを知ることができます。
ここではArduino Unoがどのような原理で起動しているのかをまとめていきたいと思います。
ブートローダは何のためにある?
概要
Arduinoは、まずArduino IDE(開発環境)でプログラムを作成してArduinoに書き込み、次に電源を接続すると書き込んだプログラムが動き始めます。
この仕組みは、マイコンの中に自分で書き込むプログラムの他に、ブートローダと呼ばれるプログラムがあらかじめ工場で書き込まれていることで実現されています。
Arduinoに電源を入れると、まずブートローダが起動しUARTと呼ばれる通信でプログラムデータがパソコンから送られてこないか監視をします。
一定時間たってもプログラムが送られてこなければ、ユーザーが書き込んだプログラムを起動させます。
ArduinoにはATmega328Pというマイコンが実装されていますが、本来、このようなマイコンは専用の装置を使ってFlashメモリにプログラムを書き込む必要があります。
もし、マイコンにブートローダが書き込まれていなければ、ユーザーはプログラムを書き込む装置を購入して、毎回自分で書き込まなくてはいけなくなります。
ブートローダのような、「UART通信でプログラムを受け取って指定の場所に書き込む」機能のみを持った専用のプログラムがあることで、ユーザーのプログラムを簡単に書き換える、便利な使い方ができるようになっているわけです。
多くの組み込み機器はこれと同じような仕組みで動作していることが多いです。
新しいバージョンのプログラムを開発してお客様に配布するときに、専用の装置を使ってFlashメモリを書き換えてもらうのは難しいです。
ブートローダを書き込んでおけば、WebやCD、USBなどからプログラムを受け取り、指定の場所に書き込むだけで簡単にプログラムの書き換えができます。
簡単にバージョンアップができる
ブートローダ
Aruduino Unoにはoptibootというブートローダのバージョン4.4が書き込まれているようです。
私の環境では以下のパスにプログラムが保存されています。
C:\Users\(ユーザー名)\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.5\bootloaders\optiboot\optiboot.c
README.TXTに記載がありますが、optibootの詳細は以下のgithubに記載があります。
実際にoptibootの中がどのようになっているか、要点だけ見てみたいと思います。
アプリ起動処理
// Adaboot no-wait mod
ch = MCUSR;
MCUSR = 0;
if (!(ch & _BV(EXTRF))) appStart();
MCUSR(MCUステータスレジスタ)のEXTRF(外部割込み要因によるリセット)ビットが立っていれば、appStart()は呼ばれず、EXTRFビットが立っていない場合はappStart()を呼びます。
つまり、EXTRFビットが立っている場合だけ以降の処理が実行され、それ以外の場合はappStart()が呼ばれてoptibootが終了しユーザーのプログラムが起動されます。
ウォッチドッグタイマー
watchdogConfig(WATCHDOG_1S);
ウォッチドッグタイマーを1秒に設定しています。
ATmega328Pはウォッチドッグタイマーと呼ばれる機能を持っています。
設定した時間が経過すると以下のどれかを実行します。
- 割り込み
- システムリセット
- 割り込みしてシステムリセット
例えば「割り込みしてシステムリセット」で動作する場合、1秒の間に次のタイマーが設定されなければ、ウォッチドッグタイマーはプログラムに異常があるとみなして、まず割り込み信号を出します。
割り込みハンドラで大切なデータなどを保存した後、強制的にシステムリセットを実施します。
このような仕組みから、プログラムが異常な処理に落ちたときに自動的にリセットをかけ、正常な状態に戻すことができます。
#define WATCHDOG_1S (_BV(WDP2) | _BV(WDP1) | _BV(WDE))
optibootはWDTCSR(ウォッチドッグタイマーコントロールレジスタ)のWDP2、WDP1、WDEビットを立てています。
- WDP2、WDP1
WDP0~WDP3の設定の組み合わせで、ウォッチドッグタイマーのタイマーを設定できます。
今回はWDP0:0、WDP1:1、WDP2:1、WDP3:0、の組み合わせなのでデータシートによると1秒後にタイマーが設定されます。 - WDE
ウォッチドッグによるシステムリセットを有効に設定できます。
よって、optibootは「システムリセット」で動作させているので、1秒後にウォッチドッグタイマーを再設定しなければ強制的にリセットされます。
メインループ
for (;;) {
/* get character from UART */
ch = getch();
if(ch == STK_GET_PARAMETER) {
unsigned char which = getch();
verifySpace();
if (which == 0x82) {
/*
* Send optiboot version as "minor SW version"
*/
putch(OPTIBOOT_MINVER);
} else if (which == 0x81) {
putch(OPTIBOOT_MAJVER);
} else {
/*
* GET PARAMETER returns a generic 0x03 reply for
* other parameters - enough to keep Avrdude happy
*/
putch(0x03);
}
}
else if(ch == STK_SET_DEVICE) {
// SET DEVICE is ignored
getNch(20);
}
else if(ch == STK_SET_DEVICE_EXT) {
// SET DEVICE EXT is ignored
getNch(5);
}
for文で無限ループを作っています。
基本的にプログラムはこのように無限ループするように作られており、何かの割り込みや指示を待っています。
/* get character from UART */
ch = getch();
UART通信で1Byteのデータをパソコンから受信しています。
optibootはパソコンから送られてきたコマンドを元に実行する処理を決めます。
後ろのif文で、受信したコマンドによって処理を分けています。
コマンドの詳細は前述のgithubを参照してください。
void verifySpace() {
if (getch() != CRC_EOP) {
watchdogConfig(WATCHDOG_16MS); // shorten WD timeout
while (1) // and busy-loop so that WD causes
; // a reset and app start.
}
putch(STK_INSYNC);
}
有効なコマンドを受け取ると必ずverifySpace()が呼ばれて、その中でウォッチドッグタイマーが設定されているので、通信が成功しているうちはリセットがかかることはありません。
このメインループの中で、パソコンのArduino IDE(開発環境)からユーザーのプログラムを受け取りFlashメモリに書き込んでいます。
パソコンとの通信が終わったときや、そもそもパソコンと通信がされなかったときは、ウォッチドッグタイマーの時間が経過してシステムリセットがかけられます。
すると、前述のアプリ起動処理でMCUSRレジスタのEXTRFビットは立たず、WDRF(ウォッチドッグタイマー要因によるリセット)ビットが立つので、ユーザーのアプリが起動されます。
まとめ
Arduinoにはoptibootというブートローダが書きこまれているので、ユーザーはArduino IDEを使って自由にプログラムを書き換えることができます。
順次、バージョンアップをしなくてはいけないような組み込み機器も、同じような仕組みで簡単にプログラムを書き換える(バージョンアップ)できるようになっています。
Arduinoのoptibootの仕組みを理解して、組み込み機器へ応用できるようになるといいですね。
以下の記事では、実際にOptibootとArduino IDEがUARTで通信するデータを解析しています。
良ければ続けてお読みください!
コメント