Lチカを超えて電子工作をちゃんと知るための「n講」【第6回】

ソースコードを覗く〜GPIO編〜

 

第1回:ToF距離センサの仕組み
第2回:加速度センサの仕組み
第3回:温度センサの仕組み
第4回:光学式マウスのチップを拝む
第5回:チップを拝む〜互換チップの世界〜

 

しっかりとした正しい知識を基礎から学び、長く電子工作を楽しむことができるようになることを目的とした今回の連載。分かりやすく解説してくれるのは、金沢大学電子情報通信学類教授の秋田純一先生です。第5回までは「チップを拝む」をお送りしてきましたが、今回からは新シリーズ「ソースコードを覗く」がスタート。初回は、Arduino等のライブラリのソースコードを階層的に読んで、APIとタイマ等のハードウエアリソースとの関係をつなげていきたいと思います。それでは早速始めましょう!

 

目次

  1. Arduinoの便利さ?
  2. マイコンのデータシート
  3. Arduinoのソースコードを読む

 

1. Arduinoの便利さ?

learning-electronics-06-01

 

みなさん、Arduinoって使ったことありますよね。パソコンがなくても、単体でセンサなどをついないで動く装置を作るのに便利ですし、マイコンの入門としてもお手軽ですよね。

Arduinoが登場したのは2005年ですが、それ以前にもマイコンボードはいろいろありました。ただ、「初めて触ったマイコンがArduino」という方には想像がつかないと思うのですが、それ以前のマイコンは、「その筋」の人でないと使おうとは思わないですし、使うこともできない、なかなかハードルが高いものでした。まず開発ソフトをインストールして有料のコンパイラを買って、RS-232ポートにマイコンボードをつないで、ボードのスイッチを書き込みモードに切り替えてリセットして…と、はじめの一歩の「Lチカ」までが、非常に遠いものでした。

個人的には「Arduinoが便利すぎてマイコンの世界を変えた要因」は、①USB1本で完結(給電と通信)、②DTRリセット(パソコンからリセットをかけられる)、③IOコネクタがメスソケット(ジャンパワイヤを挿せる)、の3つだと思うのですが、いずれにしても、便利な世の中になりました。

さて、普段Arduinoを使っていると意識することは少ないのですが、中身は小さいながらもコンピュータです。つまり一番奥の方では、機械語のプログラムが実行されていて、更にその奥には、そのプログラムを実行するための論理回路が動いているわけです。これらのコンピュータの階層構造はかなり深く、なかなか理解するのはしんどいのですが(拙著「揚げて炙ってわかるコンピュータのしくみ」はこの理解に取り組む内容です)、普段はあまりそこまで意識しなくても使えますし、それで困ることもあまりありません。

 

learning-electronics-06-02

拙著「揚げて炙ってわかるコンピュータのしくみ」

 

ただ、もう少し奥の深いところまで覗いて理解できると、よりArduinoを深く使いこなせますし、それより、「よくわからないブラックボックス」の中身が多少なりとも見えるのは、面白いものです。今回は、Arduinoのライブラリのうち、出力ピンを制御するdigitalWrite()関数のソースコードを題材に、Arduinoというコンピュータの中身を少し覗いてみましょう。

 

2. マイコンのデータシート

まずはArduino(UNO)に載っているマイコンであるATmega328P (Microchip社)について覗いてみましょう。Microchip社のWebページから、ATmega328Pのデータシート(仕様書)をダウンロードできます。560ページほどあって、全部読むのは無理ですが、ポイントを絞って調べたいところを読む、というのがオススメです。今回はdigitalWrite()に関する情報を調べたいので、GPIO(汎用I/O: General Purpose Input/Output)に関するp.77からの部分を見てみます。といってもこの節もけっこう分量がありますが、途中p.79に、こんな表があります。

 

learning-electronics-06-03

ATmega328PのGPIOの機能とレジスタ

 

これは、次に見ていくATmega328P内部のレジスタ(メモリの一種)の内容とピンの機能がまとめられています。例えばDDRxn = 0、PORTXn = 0のとき、ピンの機能は入力ピン(input)となることがわかります。ArduinoだとpinMode(pin, INPUT)でピンを入力に設定できますが、要はこれらの設定をしているわけです。

ちなみにDDRxnとは、Data Direction Registerの略で、xはポート名、nはポート内のピンの番号です。例えばArduinoUNOのD0ピンは、ATmega328PのPortDの0番ピンにつながっています(これは回路図をみるとわかります)から、D0ピンの設定は、DDRD0(DDRDレジスタのビット0)とPORTD0(PORTDレジスタのビット0)の値で設定できるわけです。

データシートのもう少し先のp.95に、これらのレジスタのもう少し詳しい情報が載っています。

 

learning-electronics-06-04

ATmega328PのPortD関連のレジスタ

 

これを見ると、例えばPORTDレジスタはPORTD0~PORTD7の8ビット分あり、それぞれPortDの0~7ビットの機能を設定できます。それぞれR/W、つまり読み出しも書き込みもできて、リセット直後の初期値はすべて0、とのことです。また、このレジスタの「アドレス」は0x2B(16進数で2B)であるとのことです。詳細は順に見ていきますが、このアドレスは、レジスタへのアクセス(値を読み出したり書き込んだり)するときの目印となります。ちなみにDDRD、PINDレジスタのアドレスは、それぞれ0x2A, 0x29ですね。

 

3. Arduinoのソースコードを読む

さてここからはいよいよdigitalWrite()の中で何が起こっているのかを見ていきましょう。普段は意識しませんが、ArduinoIDEをインストールすると、digitalWrite()などの関数のソースコードも、以下のフォルダに入っています。

Windows
<ArduinoIDEインストール先>Arduino-???/hardware/arduino/avr/cores/arduino/

Mac
/Applications/Arduino.app/Contents/Java/hardware/arduino/avr/cores/arduino/

この中のwiring_digital.cというファイルを開いてみましょう。たくさん関数がありますが、その中にdigitalWrite()の中身が以下のように書かれています。

void digitalWrite(uint8_t pin, uint8_t val)
{
	uint8_t timer = digitalPinToTimer(pin);
	uint8_t bit = digitalPinToBitMask(pin);
	uint8_t port = digitalPinToPort(pin);
	volatile uint8_t *out;

	if (port == NOT_A_PIN) return;

	// If the pin that support PWM output, we need to turn it off
	// before doing a digital write.
	if (timer != NOT_ON_TIMER) turnOffPWM(timer);

	out = portOutputRegister(port);

	uint8_t oldSREG = SREG;
	cli();

	if (val == LOW) {
		*out &= ~bit;
	} else {
		*out |= bit;
	}

	SREG = oldSREG;
}

けっこういろいろな処理をしていますね。このうち、最後のif文のところでは、valがLOWならば「*out &= ~bit」を実行しています。

例えば”digitalWrite(0, LOW)”であれば、D0ピン(PortDの0ビット目でした)を0にする、ということですから、さきほどのレジスタで考えると、PORTDレジスタの0ビット目(PORTD0)を0にすればよいことになります。マイコンのC言語でのプログラムを書いたことがある方ならすぐわかるように、これは、

PORTD &= ~0x01

でできます。0x01は0ビット目のみが1の値(2進数で書くと00000001)、~はその否定(NOT)ですから2進数で書くと11111110となります。PORTDの値と、この11111110との論理積(&, AND)をとるので、PORTDの0ビット目だけを0に設定し、他のビットは変化させない、ということができるわけです。

ここから類推すると、このdigitalWrite()の中にある「*out &= ~bit」は、*out=PORTD、bit=0x01、となっていればよさそうです。

C言語では変数の前の*はポインタを表しますので、outがPORTDのアドレスである0x2Bであれば、「*out」はそのアドレスの内容、つまりレジスタPORTDの実体、ということになります。

ではこのoutとbitはどのように設定されているのでしょうか。ソースコードの少し前を見ると、以下のような記述があります。

uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *out;
...
out = portOutputRegister(port);

まずbitからいきましょう。digitalPinToBitMask()の引数のpinは、digitalWrite()のピン番号です。このdigitalPinToBitMask()は、wiring_digital.cと同じフォルダのArduino.hで定義されています。

Arduino.h
...
#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )
...

そのままpgm_read_byte()と定義されています。このpgm_read_byte()は、さきほどのwiring_digital.cがあるフォルダとは別の、以下のところにあるpgmspace.hで定義されています。

Windows
<ArduinoIDEインストール先>Arduino-???/hardware/tools/avr/avr/include/avr

Mac
/Applications/Arduino.app/Contents/Java/hardware/tools/avr/avr/include/avr

pgmspace.h

...
#define pgm_read_byte(address_short)    pgm_read_byte_near(address_short)
...
#define pgm_read_byte_near(address_short) __LPM((uint16_t)(address_short))
...
#define __LPM(addr)         __LPM_enhanced__(addr)
...
#define __LPM_enhanced__(addr)  \
(__extension__({                \
    uint16_t __addr16 = (uint16_t)(addr); \
    uint8_t __result;           \
    __asm__ __volatile__        \
    (                           \
        "lpm %0, Z" "\n\t"      \
        : "=r" (__result)       \
        : "z" (__addr16)        \
    );                          \
    __result;                   \
}))
...

pgm_read_byte()→pgm_read_byte_near()→__LPM→__LPM_enhanced、と4段階のdefineを経て、__asm__で記述されるアセンブラ命令までたどり着きました。最終的にはlpmmというアセンブラ命令になっています。これがどのような動作をおこなう命令かは、ATmega328Pを含むAVRマイコンの機械語のリファレンスマニュアルに書かれています。それによれば、プログラムメモリの内容を読み出す、というものです。読み出すアドレスはZレジスタ(その実体は2つのレジスタR31, R30をつなげて16ビットのレジスタとして扱うもの)で指定することになっています。

では、どのアドレスを読み出すかというと、さきほどのdigitalPinTobitMask()の定義をみると、以下のように「digital_pin_to_bit_mask_PGM」にP(つまりピン番号、今の例では0)を加えたもの、ということになります。

#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )

では「digital_pin_to_bit_mask_PGM」は、というと、以下のところにある「pins_arduino.h」で定義されています(フォルダ名から、Arduinoシリーズのマイコンや種類ごとに別れて定義されているファイルということもわかります)。

Windows
<ArduinoIDEインストール先>Arduino-???/hardware/arduino/avr/variants/standard

Mac
/Applications/Arduino.app/Contents/Java/hardware/arduino/avr/variants/standard

pins_arduino.h

const uint8_t PROGMEM digital_pin_to_bit_mask_PGM[] = {
	_BV(0), /* 0, port D */
	_BV(1),
	_BV(2),
...
};

このように「digital_pin_to_bit_mask_PGM」は配列で、前についているPROGMEMは、これをプログラムメモリ領域に配置する、という指定です。

だいぶ複雑でしたが、以上を整理すると、digitalWrite(0, LOW)は、以下の順で展開されていくことになります。

uint8_t bit = digitalPinToBitMask(0); // pin=0

uint8_t bit = ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (0) ) ) // P=0

uint8_t bit = __LPM((uint16_t)( digital_pin_to_bit_mask_PGM + (0) ))

__asm__ __volatile__ "lpm bit, (digital_pin_to_bit_mask_PGM + (0))"

「digital_pin_to_bit_mask_PGM」は、配列digital_pin_to_bit_mask_PGM[]のポインタですので、結局プログラムメモリの digital_pin_to_bit_mask_PGM にある内容が変数bitに読み出されるわけです。配列digital_pin_to_bit_mask_PGMの先頭の要素は「_BV(0)」ですが、これは1を0ビット左シフトした値(つまり0x01、2進数では00000001)、という意味ですから、最終的にbitの値は0x01、となります。

ずいぶん込み入っていましたが、たしかにbitの値は0x01になることが確認できました。

もう一つの変数outも、同じ方法で追いかけてみましょう。digitalWrite()の中身では、以下の順になっています。

uint8_t port = digitalPinToPort(pin);
volatile uint8_t *out;
...
out = portOutputRegister(port);

まずdigitalPintoPort()で、ピン0が含まれているポート名を取得していて、port=PD(PortDのこと、値としては4)となることになります。(以下は定義箇所などの要点のみを順に示しています)

#define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )

const uint8_t PROGMEM digital_pin_to_port_PGM[] = {
	PD, /* 0 */
	PD,
	PD,

#define PD 4

続くportOutputRegister()で、引数に”PD”(値は4)を与えて、そのポートのPORTレジスタのアドレスを取得しています。

#define portOutputRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_output_PGM + (P))) )
↓
const uint16_t PROGMEM port_to_output_PGM[] = {
	NOT_A_PORT,        // 0
	NOT_A_PORT,        // 1
	(uint16_t) &PORTB, // 2
	(uint16_t) &PORTC, // 3
	(uint16_t) &PORTD, // 4

結局portOutputRegister(PD)の戻り値であるoutは、port_to_output_PGM[4]、つまりPORTDレジスタのアドレスということになります。(C言語で変数名の前につく&は、アドレスを表すのでした)

以上が準備で、やっと最後のここで、PORTDレジスタの0ビット目を0または1に設定しているわけです。これでPortDの0ビット目であるD0ピンに出力される値が0または1に設定されました。

if (val == LOW) {
	*out &= ~bit;
} else {
	*out |= bit;
}

 

learning-electronics-06-05

 

長かったですね。おそらくArduinoシリーズでいろいろなマイコンが使えるように、汎用性を重視した書き方にしているからだと思うのですが、かなりまどろっこしい表記をしているようにも感じますが、ともかくdigitalWrite()を読み込んでいって、最後はATmega328Pの機械語のlpm命令までたどりついて理解することができました。だいぶ長い道のりですが、つながっていましたね。

ちなみにArduinoIDEのコンパイルの過程でアセンブラのプログラムソースとして見ることもできますので、興味のある方は、その他のC言語で書かれた部分も、どのようなアセンブラのプログラムに変換されているかを見てみると、面白い(けっこう大変ですが)と思います。

 

参考サイト

Arduinoソフトウエアの内部構造

ArduinoIDEでアセンブリ出力

AVR命令

 

 

今回の連載の流れ

第1回:ToF距離センサの仕組み
第2回:加速度センサの仕組み
第3回:温度センサの仕組み
第4回:光学式マウスのチップを拝む
第5回:チップを拝む〜互換チップの世界〜
第6回:ソースコードを覗く〜GPIO編〜(今回)
第7回:ソースコードを覗く〜analogWrite編〜
第8回:ソースコードを覗く〜なんか動作がおかしくなった編〜

アバター画像

金沢大学 電子情報通信学類 教授。子どもの頃から半田の煙で育ち、集積回路の研究の道へ。ホビーとしての電子工作とのつながりとして、集積回路がMakerの道具となる世界を目指して、LチカLSI動画なども研究テーマにおいている。無駄な抵抗コースターなどMakerとしても活動。好きな半田はPb:Sn=37:63、好きなプロセスはCMOS0.35um。

高専ロボコン2019出場ロボット解剖計画