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

ソースコードを覗く〜なんか動作がおかしくなった編〜

 

第1回:ToF距離センサの仕組み
第2回:加速度センサの仕組み
第3回:温度センサの仕組み
第4回:光学式マウスのチップを拝む
第5回:チップを拝む〜互換チップの世界〜
第6回:ソースコードを覗く〜GPIO編〜
第7回:ソースコードを覗く〜analogWrite編〜

 

しっかりとした正しい知識を基礎から学び、長く電子工作を楽しむことができるようになることを目的とした今回の連載。分かりやすく解説してくれるのは、金沢大学電子情報通信学類教授の秋田純一先生です。第6回から始まった新シリーズ「ソースコードを覗く」の3回目となる今回は、なにか動作がおかしくなった際に見ていきたい部分を読んでいきます。それでは早速始めましょう!

 

目次

  1. マイコンのハードウエアリソースは有限
  2. タイマーを使うライブラリと併用するときは注意が必要
  3. タイマーを使っていなさそうなライブラリでも注意が必要
  4. オマケ:Arduinoのmain()はどこにある?

 

1. マイコンのハードウエアリソースは有限

learning-electronics-08-01

 

前回、analogWrite()のソースコードを見ながら、アナログ出力が、マイコンのタイマーを使ったPWM出力でおこなわれていることを見てきました。このタイマーのように、マイコンには、プログラムを実行するCPUとプログラムや変数を保存しておくメモリのほかに、さまざまな周辺回路が入っています。タイマーもそのひとつで、アナログ出力ではPWM出力の生成を自動でおこなってくれていました。タイマーには、その他にも、周期的に割り込みを発生させてプログラムの実行を中断させたり(むしろこれが本来のタイマーの機能ですね)、入力ピンに入ってくるパルスの幅を計測するのにも使えます。

タイマーの他にも、ATmega328Pにはアナログ電圧を取得するA/Dコンバータ、シリアル通信を行うUART(USART)、SPI通信を行うSPI、I2C通信をおこなうTWI、システム監視用のウオッチドッグライマ(WDT)などの周辺回路があり、プログラムの実行とは独立に動作して、それぞれの機能を果たすことができます。

これらの周辺回路はうまく使うと便利なのですが、マイコンの中にはそれぞれひとつしかありませんので、同時に別の機能を果たすことはできません。プログラムのあるところではこの機能に設定し、別のところで別の機能を設定しても、実際にはどちらか一方(あとで設定したほう)の機能にしかなりません。これらの周辺回路を、自分でプログラムを書いて制御する分には、別の機能を割り当てようとすることはないと思いますが、ライブラリを使うと、自分のプログラムでは意図していなくても、ライブラリの中で別の機能に割り当てられてしまうことがあります。

自分ではそのような機能割当をしているつもりはないので、「あれ?なんか動作がおかしくなった?」と思ってしまいそうです。その原因の究明はけっこうやっかいで、ライブラリの仕様(どの周辺回路をどう使うか、という情報)や、場合によってはライブラリのソースコードを読み込んでいかなければいけない場合もあります。特に注意が必要なのが、いろいろな機能があってよく使われるタイマーです。

 

2. タイマーを使うライブラリと併用するときは注意が必要

前回見たように、ArduinoUNOのアナログ出力は、ATmega328P内のタイマーを使っていました。それはつまり、タイマーを別の用途に使うと、意図しない動作をする場合がある、ということになります。具体例をいくつかみていきましょう。

まずArduino純正ライブラリではないのですが、よく使われるMsTimer2というライブラリがあります。これは名前の通り、Timer2を使ってミリ秒単位のタイマー割り込み(周期的な関数呼び出し)ができるものです。
このMsTimer2を使うと、Timer2がPWM出力ではなくタイマー割り込みに使うように設定されます。前回analogWrite()の中身をみたように、Timer2はD3とD13のアナログ出力で使われていますので、MsTimer2を使うとD3, D13のPWM出力が使えなくなります。
「使えなくなる」というのは、Timer2がD3, D13のPWM出力の生成ではなく、タイマー割り込みに使われるように設定されますので、D3, D13のPWM出力が出なくなる(常に0になる)ことになります。

learning-electronics-08-02

 

analogWrite()でアナログ出力した状態のD3(上)とD5(下)の波形。左がMsTimer2を使わないとき、右がMsTimer2を使うとき。

実際にanalogWrite(3, 128)とanalogWrite(5, 128)で、D3とD5にパルス幅がパルス周期の50%のPWM出力を出した状態を、オシロで観測した波形が上図の左側です。ちなみにD5のPWM出力にはTimer0が使われていますが、この波形を見ると、D3(Timer2)とD5(Timer0)でPWM出力の周期が違うことがわかります。これはTimer0とTimer2で別のパルス周期が設定されているようです。ちなみになぜ周期が違うかは、ArduinoIDEのソースコードを読んでいくと初期化のところで書いてありますので、興味のある人は探して読んでみてください。main.cppで呼ばれる初期化の関数init()が定義されているwiring.cに書いてあります。詳細はATmega328Pのデータシートの、なかなか難解なタイマーの説明を読むことになりますが、Timer0とTimer1/2でPWMの生成モードが異なるり、後者の周期が前者の1/2になるように設定されています

 

3. タイマーを使っていなさそうなライブラリでも注意が必要

一見ではタイマーを使っているようには見えなくても、実は内部でタイマーを使っている、というライブラリもあって、これもなかなか原因究明がやっかいです。例として、サーボモータの制御に使うServoライブラリを見てみましょう。Servoライブラリとアナログ出力を一緒に使う、というのは、いかにもありそうなシチュエーションですよね。

Servoライブラリのソースはこちらにあります。

Windows
<ArduinoIDEインストール先>Arduino-???/libraries/Servo/

Mac
/Applications/Arduino.app/Contents/Java/libraries/Servo/

まずヘッダファイルのServo.hをみると、ATmega328PなどのAVRマイコンを使っている場合はavr/ServoTimer.hを読み込んでいます。

Servo.h

---------
// Architecture specific include
#if defined(ARDUINO_ARCH_AVR)
#include "avr/ServoTimers.h"
---------

ServoTimer.hでは、ATmegaマイコンの種類ごとに使うタイマーを定義しています。

ServoTimer.h

---------
// Say which 16 bit timers can be used and in what order
...
#else  // everything else
#define _useTimer1
typedef enum { _timer1, _Nbr_16timers } timer16_Sequence_t;
#endif
---------

ATmega328Pでは、このようにTimer1を使うように定義されています。Servorライブラリの中でTimer1をどのように使っているかは、ソースコードのServo.cppに書かれています。複数のサーボの制御信号を生成するために、ちょっと複雑なプログラムになっていますが、Timer1をタイマーとして使い、タイマー割り込みを使っています。

ArduinoUNOでは、Timer1はD9, D10のアナログ出力で使っていましたので、この2つは同時に使えない、ということになります。

このように、ライブラリのソースコードを読み込んでいくことで、「あれ?なんか動作がおかしくなった?」という場合の原因がわかることがありますね。特にタイマーを使うライブラリは多い(しかもぱっと見でわかりにくい)ので、要注意ですね。

 

4. オマケ:Arduinoのmain()はどこにある?

learning-electronics-08-03

 

最後にソースコードを読むオマケとして、main()関数を探してみましょう。C言語でプログラムを書いたことがある人なら、「あれ?Arduinoってmain()関数がないの?」と不思議に思った人も多いかと思います。

wiring_digital.cなどと同じフォルダに、main.cppというソースファイルがあります。その中に、main()関数があります。

main.cpp

---------
...
int main(void)
{
	init();

	initVariant();

#if defined(USBCON)
	USBDevice.attach();
#endif
	
	setup();
    
	for (;;) {
		loop();
		if (serialEventRun) serialEventRun();
	}
        
	return 0;
}
---------

main()関数では、init()などでハードウエアの初期化をしたあと、setup()関数を呼び、そのあとはloop()関数を無限ループで実行している、ということがわかります。(無限ループ内には、シリアル通信データの受信を行う処理も入る)
これは、まさにArduinoのプログラムにおける、setup()とloop()の動作そのものですね。つまりsetup()もloop()も、main()関数から呼ばれる関数、ということがわかります。

今回で3回に渡ってお送りしてきました「ソースコードを覗く」シリーズは終了となります。最後までお読み頂きありがとうございました!

 

 

今回の連載の流れ

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

アバター画像

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

自動水やりマシンを作ろう