ArduinoのPWM出力
Arduino UNOではATmega328と呼ばれるMicrochip社のマイコンをメインチップとして使っています。
Arduinoのプログラムを考える上で、深みにハマっていくとマイコンのデータシートを見ていかなければならなくなりますので、一回見てみると面白いかと思います。(事実今がそう)
Arduinoには三つのタイマー(Timer/Counter)があります。
このタイマーはArduinoプログラムの時間に関係しています。
delay()やtone()などの関数はこのタイマーを使って計測されています。
Timer/Counter | Pin番号 | ビット数 | 役割 | PWM周波数 |
Timer0 | 5, 6 | 8 bit | Arduinoの時間を管理 delay(), millis(), micros()など | 977 Hz |
Timer1 | 9, 10 | 16 bit | Servoライブラリなど | 490 Hz |
TImer2 | 3, 11 | 8 bit | tone()など | 490 Hz |
今回このタイマーをいじることでPWMの出力を変えるということをします。
ちなみに、応用するとタイマーの根本を変えるので、delay()などの関数もいい感じに変えたりすることができたりするかも?(どちらかというと影響を受けて狂ったりすることの方が多そう)
TImer0は基本的にシステム全体に関わることが多いので、TImer1を使用することをお勧めします。
参考になるサイト集
https://playground.arduino.cc/Main/TimerPWMCheatsheet/
https://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM
https://atooshi-note.com/arduino-1hz-pwm/
http://blog.kts.jp.net/arduino-pwm-change-freq/
http://garretlab.web.fc2.com/arduino/inside/hardware/arduino/avr/cores/arduino/wiring_analog.c/analogWrite.html
プログラムの概要
プログラムの全体の方針として、Arduinoのタイマーをレジスタの設定を変えることで自由にPWM出力の周波数を変更することができるようにします。
目的としては、低い周波数を出力できるようにしたく、10 Hzを出力できるようなプログラムにします。
今回は10番ピンにLEDを繋いで、その光の周波数、Duty比を自由に変更できるようにするプログラムを作成します。
10番ピンなので、TImer1を使用します。
プログラムコード
//レジスタの設定を変えるためのもの
#include <avr/io.h>
int PWMPin = 10;
//関数の定義
//frq:周波数 (1Hz~指定できる)
//duty:指定したいduty比
void HzWrite(int frq, float duty) {
// モード指定
TCCR1A = 0b00100001;
TCCR1B = 0b00010100; //分周比256を用いる
// TOP値指定
OCR1A = (unsigned int)(31250 / frq);
// Duty比指定
OCR1B = (unsigned int)(31250 / frq * duty);
}
void setup() {
pinMode(PWMPin, OUTPUT);
}
void loop() {
HzWrite(10, 0.5);
delay(5000);
digitalWrite(PWMPin, LOW);
delay(5000);
}
プログラムの説明
まず、レジスタの設定を変えるために、<avr/io.h>をインクルードします。
#include <avr/io.h>
次に、delay()と一緒に使って、10 Hzの出力を5秒ごとに繰り返すという関数を作るため、HzWriteという関数を指定します。カッコ内には周波数、Duty比を指定できるようにします。
void HzWrite(int frq, float duty) {
}
次にモードの指定です、。
今回使うレジスタはTCCR1A/TCCR1Bです。(TCCR: Timer/Counter Control Register)
ここで1というのはTImer1を示しており、Timer2を使いたければTCCR2A/TCCR2Bを使います。
PWM周波数を〇〇Hzとして設定するには、TOP値を自分で指定する必要があります。
TOP値が大きければ大きいほど出力される周波数は低くなります。
今回は10 Hzで例を取っているので、16MHzというシステムクロックと比べると非常に遅いので、大きなTOP値と分周を指定する必要がある。よって、なるべく大きいTimer1を使用します。
Timer0, 2の8 bitではTOP値の最大値は255(2^8 – 1)であり、Timer1の16 bitでは65535(2^16 – 1)となる
(最大値が-1となるのは、0~255もしくは0~65535となるため、数字の数が2^xになるが、最大値は-1しなければならない。)
システム的には、TOP値までカウンタをインクリメント(0, 1, 2, と数えていく)し、OCRxA/OCRxB(xはカウンタ番号、カウンタについて2つの出力ピンA, Bが割り当てられている。)と一致した時にピンの出力を変化させる(LOW→HIGHのように)。そして、カウンタがTOP値まで到達すると今度は0までデクリメント(65535, 65534, 65533と上から数えていく)し、インクリメントの時と同様にOCRxA/OCRxBと一致した時にピンの出力を変化させます。
Arduino UNOではこのカウンタのインクリメントするスピードは分周比の設定を変えることによって、ある程度変えることができます。(ある程度とは、1/8/64/256/1024 の中から選択できるということ)
分周比は周波数を分周(周波数を1/n倍すること)するときの比(n)です。
つまり、1000 Hzを分周比10で分周すると100 Hzになります。
ちなみに、分周比1ではArduino UNO(ATmega328)のシステム動作クロックである16MHzで操作します。
つまり、TOP値とOCRxA/OCRxBの値、分周比を変えることによって、自由に出力の変換点を変えることができます。
今回は10Hzを指定したいので余裕を持って分周比256で行います。
1カウントする時間は、Unoなら16MHzなので1 / 16MHz = 62.5ns(分周1)
分周比を256にすると、TOPまでカウントした際には62.5ns x 256 x 65535 = 1.04856 secなので、1 Hzを指定できるようになる。
今回このプログラムでは1 Hz ~ 31250 Hzまでの周波数を作り出すことができる。
しかし、31250に近づくにつれてDuty比の指定ができなくなっていきます。
Duty比を細かく設定したい際には300 Hz程度までしか作成することができません。
このプログラムでは分周比の設定を変更することで、自分の使用用途にあった周波数帯を作れるようなプログラムを作ることができます。
TCCRaA/TCCR1Bには何を指定するのかを書きます。
ここら辺の細かいところはなんか難しいので、データシートを使って、なんとなく指定していきましょう。
今回は2進数で示しているので、最初に0bがついています。TCCR1AではCOM1A1, COM1A0, COM1B, 無指定, 無指定, WGM11, WGM10 を 1 か 0 で設定します。
TCCR1B には それぞれ、無指定(ICNC1), 無指定(ICES1), 無指定, WGM13, WGM12, CS12, CS11, CS10 を設定します。
まず、今回はModeはPWMモードがPhase Correct and Frequency Correct であるMode9を指定する。
今回TOP値はOCR1Aに指定する。
よって、WGM13 / WGM12 / WGM11 / WGM10 はそれぞれ、1,0,0,1
COM1B0 / COM1B0 は 0, 0 が 無出力、0, 1 が トグル動作 (一致時の出力を反転)、
1, 0 は カウンタ がOCR1A/B – TOP間 にある場合 LOW、0 – OCR1A/B間 は HIGH となるよう出力、
1, 1 は1, 0 の逆です。
今回は周波数をOUTPUTのLEDをLOWとHIGHで変えるため、1B0 / COM1B0 は1, 0の値を指定します。
スケッチを書く際にわかりやすいので 1, 0 を選択します。
今回分周比は256を指定するため、CS12/CS11/CS10は1, 0, 0を指定する。
まとめると、以下のようになる。
TCCR1A = 0b00100001;
TCCR1B = 0b00010010;
次にOCR1AとOCR1Bを設定し、周波数とDuty比を指定したもので出力してもらうように指定します。
// TOP値指定
OCR1A = (unsigned int)(31250 / frq);
// Duty比指定
OCR1B = (unsigned int)(31250 / frq * duty);
今回Phase and Frequency Correct PWMを用いる際には、インクリメントしディクリメントすることから、出力される周波数は以下のように示すことができます。
周波数frq = IC の動作周波数 / (分周比 * TOP値 * 2)
逆にTOP値を指定するときは
TOP値 = IC の動作周波数 / (分周比 x frq x 2)
今回Arduino UNOで分周比256とした際には
TOP値 = OCR1A = 16,000,000 / (256 x frq x 2) = 31250 / frq
LOWとHIGHが入れ替わるタイミングはOCR1A/OCR1B = Duty比としたいため、
OCR1B = 31250 / frq x duty
オーバーフローを防ぐため、Unsigned intを指定している。
出力のメインはこちら
void setup() {
pinMode(PWMPin, OUTPUT);
}
void loop() {
HzWrite(10, 0.5);
delay(5000);
digitalWrite(PWMPin, LOW);
delay(5000);
}
PWMPinつまり10ピンをOUTPUTで指定し、HzWrite()で周波数とDuty比を指定する。
delay()でまった後に、digitalWrite(PWMPin, LOW)で出力をなくし、delay()する。
以上がプログラムの全貌と解説です。
後書き
PWM出力の周波数を変更する方法のブログに関して、Arduinoで検索するよりも、AVRとか、ATmega328, 328Pなどのように検索した方が多く出てきました。
今回はざっくりとしたブログでしたが、より深く知りたい人は調べてみてください。
コメント
ありがとうございます。
お役に立てて非常に嬉しいです。
またご要望等ありましたら、ぜひコメントよろしくお願いします。
Jimnyにデジタルスピード計を作ろうと計画中です。車速パルスの代わりに低速のパルスジェネレータが欲しくて探していてこちらを拝見しました。
初心者にも分かり易く解説されていてとても助かりました!早速使わせていただきます。
有難うございました。