Arduino Unoはブートローダー(Optiboot)が工場で書き込まれているので、Arduino IDEから「書き込み」を押すだけでプログラムをFlashメモリに書き込むことができます。
ここでは、Arduino Unoのブートローダ(Optiboot)が、Arduino IDEとどのような信号をやり取りしているのか実際に通信データを解析してみたいと思います。
通信データを見ると、より理解が深まるよね
ブートローダ
Aruduino UnoにはOptibootというブートローダが工場で書き込まれた状態になっています。
Optibootは起動後にパソコンからUARTでデータが送られてこないか監視をします。
データ(プログラム)が送られてきた場合はFlashメモリの指定位置に書き込み、書き込んだプログラムを起動します。
データが送られなければ、元々書き込まれていたプログラムを起動します。
Optibootの詳細は以下の記事で紹介していますので、興味のある方は読んで頂けると嬉しいです。
準備物
OptibootとArduino IDEはUARTと呼ばれるシリアル通信でデータをやり取りします。
UARTの通信データを見るために安価なロジックアナライザと、解析ソフトを使用します。
●Arduino uno
●ロジックアナライザ
このロジックアナライザを使用すれば、UART、IIC、SPIなどのシリアル通信のデータを見ることができます。
●解析ソフト pluseview
上記のロジックアナライザのデータを見るために、pluseviewというフリーソフトを使用します。
https://sigrok.org/wiki/Downloads
以下のサイト様でインストール方法から使用方法まで詳細に説明されているので、参考にしてください。
Arduino Unoとロジックアナライザの接続
Arduino UnoはUSBで取得したデータを、USB-シリアル変換を行うチップでUARTの信号に変換して、マイコン(ATmega328P)の2番ピン・3番ピンへ入力します。
このマイコンの2番ピン・3番ピンはArduino UnoのD0ピン・D1ピンにもつながっているので、ロジックアナライザのCH0・CH1をD0ピン・D1ピンへ接続することで、UARTの信号を横取りすることができます。
また、ロジックアナライザのGNDをArduino UnoのGNDピンへ接続します。
UART
UARTはシリアル通信の1つです。
調歩同期式なのでお互いでルールを決めて、そのルールに従って信号をやり取りします。
OptibootとArduino IDEのUARTは以下の設定で通信を行うので、pluseviewに設定をします。
通信データ
早速、OptibootとArduino IDEのUART通信のデータを見てみることにします。
今回は、Arduino IDEがスケッチ例として準備している、LEDを光らせるプログラム「Blink」をArduino Unoに転送する通信データを確認します。
通信の確立
まず、Arduino IDEから「0x30」「0x20」のデータが送られています。
これに対して、Optibootは「0x14」「0x10」を返しています。
/* Forever loop */
for (;;) {
/* get character from UART */
ch = getch();
Optibootはループの初めにUARTから1Byteのコマンドを読み取り、そのコマンドによって処理を分けています。
今回は「0x30」が送られていますがどこのif文にもコマンドが定義されていないので、以下のelse処理に落ちます。
else {
// This covers the response to commands like STK_ENTER_PROGMODE
verifySpace();
}
putch(STK_OK);
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()の関数ではさらに送られてきた信号を1Byte読み取り、「CRC_EOP(0x20)」であれば「STK_INSYNC(0x14)」を送り、else文を抜けて続けて「STK_OK(0x10)」を送ります。
接続が確立したことを確認しあっているように見えます。
どうやら正しくデータが取れていそうです。
バージョンの確認
次に、Arduino IDEから「0x41」コマンドが2回立て続けに送られています。
1つ目は「0x41」「0x81」「0x20」2つ目は「0x41」「0x82」「0x20」のデータが送られています。
「0x41」コマンドは、Optiboot上で「STK_GET_PARAMETER」を意味しており、その次に送られてくるデータが「0x82」であればマイナーバージョンを、「0x81」であればメジャーバージョンを返す仕様のようです。
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);
}
確かに、今使っているOptibootのバージョンは4.4と定義されているので、正しいデータが返されているようです。
#define OPTIBOOT_MAJVER 4
#define OPTIBOOT_MINVER 4
デバイスの識別
上記の後に何個かコマンドが流れましたが、Optibootでは無視されるコマンドだったのでここでは割愛します。
次に意味のあるコマンドとしてArduino IDEから「0x75」コマンドが送られています。
「0x75」コマンドは、Optiboot上で「STK_READ_SIGN」を意味しており、device signatureを取得するためのコマンドのようです。
else if(ch == STK_READ_SIGN) {
// READ SIGN - return what Avrdude wants to hear
verifySpace();
putch(SIGNATURE_0);
putch(SIGNATURE_1);
putch(SIGNATURE_2);
}
全てのAVRマイコンにはデバイスを識別するための3Byteのコード(device signature)があり、これを取得してマイコンがATmega328Pであることを確認しているようです。
データシートによると、ATmega328Pのdevice signatureは「0x1E」「0x95」「0x0F」です。
正しくデータが取得できています。
ロードアドレスの指定
次に、Arduino IDEから「0x55」コマンドが送られています。
「0x55」コマンドは、Optiboot上で「STK_LOAD_ADDRESS」を意味しており、ユーザーのプログラムを書き込むメモリの位置を指定します。
else if(ch == STK_LOAD_ADDRESS) {
// LOAD ADDRESS
uint16_t newAddress;
newAddress = getch();
newAddress = (newAddress & 0xff) | (getch() << 8);
#ifdef RAMPZ
// Transfer top bit to RAMPZ
RAMPZ = (newAddress & 0x8000) ? 1 : 0;
#endif
newAddress += newAddress; // Convert from word address to byte address
address = newAddress;
verifySpace();
}
Arduino IDEはアドレス0x0000番地にプログラムを書き込むよう指示しています。Optibootはこのアドレスを変数addressに保持して、そこへプログラムを書き込みます。
マイコンは通常、電源が入るとプログラムカウンタレジスタが0x0000番地から処理を始めると思っていましたが、ATmega328Pは違うようです。
データシートによると、アプリケーションとブートプログラムの書き込み位置は以下の関係にあるようです。
ブートFlashセクションの開始アドレスはレジスタで設定ができるようです。
「BOOTSZ1」「BOOTSZ2」レジスタヒューズビットと呼ばれる設定項目のなかにあり、ヒューズビットはブートローダを書き込むときに一緒に書き込まれているようです。
「boards.txt」内で設定内容を見ることができます。
今回は「BOOTSZ1:1」「BOOTSZ2:1」なので、ブートローダーは「0x3F00」番地に書き込むことになるようです。
ATmega328Pは全ての命令が2Byteで構成されるので、正しくは「0x7E00」番地に書き込まれています。
ややこしい
試しに、Optibootのビルド後のバイナリファイル「optiboot_atmega328.hex」を覗いてみましょう。
私の環境ではArduino IDEをインストールした時に、以下に保存されています。
C:\Users\ユーザー名\AppData\Local\Arduino15\packages\arduino\hardware\avr\1.8.5\bootloaders\optiboot
以下のコードはhexファイルの初めの3行を抽出したものです。
:107E0000112484B714BE81FFF0D085E080938100F7
:107E100082E08093C00088E18093C10086E0809377
:107E2000C20080E18093C4008EE0C9D0259A86E02C
hexファイルは黄色で下線を引いたところが、データを書き込むアドレス位置を指定しています。
「0x7E00」からデータを書き始めていることがわかります。
プログラムの書き込み
いよいよ最後のプログラム書き込みです。
Arduino IDEから「0x64」コマンドが送られています。
「0x64」コマンドは、Optiboot上で「STK_PROG_PAGE」を意味しており、これからプログラムデータが書き込まれることを意味しています。
以下のコードはコマンド「STK_PROG_PAGE」を受けた時の処理の初め数行を抜き出しています。
else if(ch == STK_PROG_PAGE) {
// PROGRAM PAGE - we support flash programming only, not EEPROM
uint8_t *bufPtr;
uint16_t addrPtr;
getch(); /* getlen() */
length = getch();
getch();
3Byteのデータの長さなどを読みだした後、プログラムの転送が始まります。
以下がスケッチ例「Blink」のhexファイルの初め3行を抜き出したものです。
:100000000C945C000C946E000C946E000C946E00CA
:100010000C946E000C946E000C946E000C946E00A8
:100020000C946E000C946E000C946E000C946E0098
青色の下線を引いたところが、実際のプログラムのデータです。
このhexファイルは「0x0000」アドレスから順番に青色の下線データを書き込むよう指示したファイルです。
正しくプログラムが転送されているのがわかります。
まとめ
Arduino Unoに書き込まれているブートローダ「Optiboot」と、Arduino IDEの通信内容を確認しました。
マイコンのアドレスの概念や、hexファイル、データシートの解読など、組み込み技術者として必要な知識が多く出てきて、改めて勉強になりました。
また、通信データを簡単に見れるのはとても楽しかったです!
通信データを見るだけで賢くなった気がする
コメント