ソースコードを覗く〜analogWrite編〜
第1回:ToF距離センサの仕組み
第2回:加速度センサの仕組み
第3回:温度センサの仕組み
第4回:光学式マウスのチップを拝む
第5回:チップを拝む〜互換チップの世界〜
第6回:ソースコードを覗く〜GPIO編〜
しっかりとした正しい知識を基礎から学び、長く電子工作を楽しむことができるようになることを目的とした今回の連載。分かりやすく解説してくれるのは、金沢大学電子情報通信学類教授の秋田純一先生です。第6回から始まった新シリーズ「ソースコードを覗く」の2回目となる今回は、Arduinoのライブラリのソースコードを読んでいきます。それでは早速始めましょう!
目次
1. Arduinoのアナログ出力
Arduinoでは、アナログ出力ができるピンがあります。ここにLEDをつないで光らせるのに、analogWrite()という関数を使って明るさを変えたりできますよね。「Lチカ」ならぬ、いわゆる「Lほわ」というそうです。
このアナログ出力、ArduinoUNOでは、どのピンでも使えるわけではなく、D3, D5, D6, D9, D10, D11の6本しか使えません。
なぜどのピンでも使えないの?っていうか、使えるピンの並びがなんでそんな不規則なの?と不思議に思いますよね。今回は、Arduinoのライブラリのソースコードを読んで、その理由を見ていきましょう。あわせて、Arduinoを使っているとたまにハマる、「2つのライブラリを一緒に使うと動作が怪しくなる」という現象の原因(の1つ)も探ってみましょう。
2. アナログ出力とPWM
前回と同じように、ArduinoIDEに含まれているライブラリのソースコードを見てながら、analogWrite()の仕組みを見ていきましょう。analogWrite()は、以下の場所にあるwiring_analog.cで定義されています。
Windows
<ArduinoIDEインストール先>Arduino-???/hardware/arduino/avr/cores/arduino/
Mac
/Applications/Arduino.app/Contents/Java/hardware/arduino/avr/cores/arduino/
まず、冒頭にコメントでこんな注意書きがあります。
wiring_analog.c --------- // Right now, PWM output only works on the pins with // hardware support. These are defined in the appropriate // pins_*.c file. For the rest of the pins, we default // to digital output. ---------
「PWM出力は、ハードウエアで使えるピンだけだよ」とあります。PWMというのは、Pulse Width Modulationの略で、日本語では「パルス幅変調」といいます。これは、周期パルスの”1″の部分の幅を変えることで、ピンの「値の平均値」を変える、という方法です。次の図では、3つのパルスの周期(繰り返しの時間)は同じですが、”1″の部分の幅が、上から順に長くなっています。
このピンにLEDをつないだ場合を考えると、”1″のときにはLEDに電流が流れて点灯し、”0″のときには点灯しません。つまりこのパルスの周期でLEDは点滅するわけです。そこでこのパルスの周期を1ミリ秒のように十分に短くしておけば、目で見てもその点滅はわかりません。そしてLEDが点灯している時間(の比率)が、上から順に長くなっているので、順にLEDが明るく見える、というわけです。
「え?アナログ出力って、出力ピンの電圧を変えてるんじゃないの?」と思った人は、鋭いです。一般的にマイコンの「アナログ出力」といえば、D/Aコンバータの出力で、指定した電圧(例えば1.2Vのような中間の電圧)を出力するものです。しかし、ArduinoではPWM出力を「アナログ出力」と言い切ってしまっているのですね。たしかにLEDをつなげば、その明るさをアナログ的に(8ビット256段階で)変えられますので、アナログ電圧が出ているように見えてしまいます。このあたりは、Arduinoの設計思想だと思いますが、使う側からすると、原理がどうであれ、「LEDの明るさをアナログ的に変えられるからアナログ出力」というのは、(少なくとも入門用としては)わかりやすくていいと思います。
さて、マイコンではこのようなPWM出力を生成するのに便利な「タイマー」というものが入っているものが多いです。ArduinoUNOに載っているATmega328Pのデータシートには、各ピンの名称と機能がまとまっている図があります。例えばアナログ出力が使えるD3ピンは、ATmega328PのPD3ピンにつながっていますが、そこには「PCINT19/OCB2B/INT1」と書いてあります。このうち「OC2B」というのが、ATmega328Pに内蔵されているタイマーのTimer2で使えるBチャンネルの出力、という意味です。
Timer2の詳細はデータシートに載っていますが、かなり複雑で、ちゃんと理解するのはほとんど無理なので、あまり気にしなくていいです。とりあえず、Timer2をきちんと設定すると、PWM出力が生成できる、ということと、Timer2でAチャンネルとBチャンネルの2つのPWM出力を独立に生成できることだけ知っておきましょう。なお、このTimer2の動作やPWM出力は、プログラムの実行とは無関係におこなわれるので、プログラムの方で動作を意識する必要はありません。初期化とPWMの幅の設定だけ、プログラムから指定すればOKです。
ちなみにATmega328Pには、3つのタイマー(Timer0, Timer1, Timer2)があります。さきほどのピン配置図で、「OC??」というピンを探すと、次のように6本見つかります。
- PD3(D3) OC2B Timer2-Bチャンネル
- PD5(D5) OC0B Timer0-Bチャンネル
- PD6(D6) OC0A Timer0-Aチャンネル
- PB1(D9) OC1A Timer1-Aチャンネル
- PB2(D10) OC1B Timer1-Bチャンネル
- PB3(D11) OC2A Timer2-Aチャンネル
ちょうどArduinoUNOでアナログ出力が使える6本のピンと同じですね。つまり不規則に見えたアナログ出力が使えるピンの並びは、ATmega328PのPWM出力ができるピン、つまりATmega328Pの仕様だったのですね。
3. analogWrite()のソースコードを覗いてみる
さて、アナログ出力の実体がわかったところで、analogWrite()のソースコードに戻りましょう。analogWrite()のソースを読み進めていくと、こんな部分があります。
wiring_analog.c --------- void analogWrite(uint8_t pin, int val) { ... if (val == 0) { digitalWrite(pin, LOW); } else if (val == 255) { digitalWrite(pin, HIGH); } ---------
valが0のときは、PWMの”1″の幅がゼロ、つまりずっと値が”0″ということですから、それはdigitalWrite()でLOWとして、出力値”0″に設定する、ということをしていることがわかります。そしてvalが最大値の255のときは、PWMの”1″の幅が最大の周期と同じ、つまりずっと値が”1″ということですから、出力値”1″に設定しています。つまりこの2つの場合は、PWMを使わずに直接ピンの出力値を設定しているわけですね。
ちなみに一般的なタイマーでのPWM生成では、この2つの場合を扱えないものが多いので、この2つだけはPWMを使わずに別に扱うことが多いです。
それ以外の値の場合は、いよいよタイマーの出番です。
wiring_analog.c --------- else { switch(digitalPinToTimer(pin)) { ---------
まず、アナログ出力をしようとするピン番号pinに対して、「digitalPinToTimer()」が呼ばれています。この関数はArduno.hで定義されています。
Arduino.h --------- #define digitalPinToTimer(P) ( pgm_read_byte( digital_pin_to_timer_PGM + (P) ) ) ---------
前回と同じように、プログラムメモリに書いてある配列digital_pin_to_timer_PGM[]の値を読んでいます。この配列はpins_arduino.hで定義されています。
pins_arduino.h --------- const uint8_t PROGMEM digital_pin_to_timer_PGM[] = { NOT_ON_TIMER, /* 0 - port D */ NOT_ON_TIMER, NOT_ON_TIMER, // on the ATmega168, digital pin 3 has hardware pwm #if defined(__AVR_ATmega8__) NOT_ON_TIMER, #else TIMER2B, #endif ... ---------
例えばpin=3(D3)の場合は、この配列の4番目の要素、つまりTimer2のBチャンネルを使うことを示す「TIMER2B」が戻り値になります。
wiring_analog.c --------- ... #if defined(TCCR2A) && defined(COM2B1) case TIMER2B: // connect pwm to pin on timer 2, channel B sbi(TCCR2A, COM2B1); OCR2B = val; // set pwm duty break; #endif ... ---------
この戻り値にあわせて、この部分が実行されますが、PWMのパルス幅を決めるvalの値は、そのまま「OCR2B」に代入されています。これは、Timer2の仕様を読んでみると書いてあるのですが、Timer2のBチャンネルのPWMのパルス幅を決めるレジスタです。
このように、D3ピンにアナログ出力すると、そこにつながっているTimer2のBチャンネルのパルス幅が変わることがわかりました。
なお、実際には初期化の関数などで、Timer2をPWM出力に使うような設定やPWM出力の周期を設定している部分があり、アナログ出力が使えるようになっています。
今回の連載の流れ
第1回:ToF距離センサの仕組み
第2回:加速度センサの仕組み
第3回:温度センサの仕組み
第4回:光学式マウスのチップを拝む
第5回:チップを拝む〜互換チップの世界〜
第6回:ソースコードを覗く〜GPIO編〜
第7回:ソースコードを覗く〜analogWrite編〜(今回)
第8回:ソースコードを覗く〜なんか動作がおかしくなった編〜