その他勉強

【Arduiono】Arduinoでタイマ割込みを使用してPWM制御を行う方法を紹介する

どうも、バイト戦士です。

今回はArduiono、いや、Arduinoに搭載されたマイコン「ATmega328P」のタイマ機能を使用してPWM制御を行ってみました。

備忘録。

※本記事ではATmega328PBのデータシートを引用しています。(リンク)

Arduinoでタイマ機能を使用してPWM制御することのメリット

Arduinoはライブラリ関数が非常に豊富なので、PWM関数も標準で用意されています。

ただしこのPWM関数はキャリア周波数が1kHz程度に設定されているので結構荒いです。

タイマ機能を使用してPWM制御を行った場合この制限を抜け出せるので、10kHz、20kHz程度の滑らかな※PWM制御を行うことも可能です。

モータ制御などで可聴域の音を減らしたい場合などに有効だったりします。

今回は実験的に、特に用途は考えずにやってみました。

※実際はマイコンの能力によってDUTYの更新速度が決まるので、PWMのキャリア周波数がPWM波形の出力の滑らかに直結する、とは言えないです。ただPWMの刻み幅は小さくなるので、、、、うーん、これはなんとも言いにくいね。。。。

具体的な使い方の例

今回このプログラムを若干変更し、電源装置を自作しました。

タイマ機能とPWMの使い方としては結構いい例だったと思います。(後半にArduinoを使用した部分があります)

PWM制御について簡単にまとめる

今回はArduinoのタイマ機能を使用してPWM制御を行う方法をまとめますが、最初にPWM制御についてまとめておこうと思います。

PWM制御は比較対象の値をノコギリ波や三角波と比較し、それより大きければパルスをH、またはLにして出力※するものです。

以下の図では比較対象の値を赤線で、キャリア波をノコギリ波で表現しています。

PWM制御において最低限抑えておくべき用語は3つあります。

・基本周期(キャリア周期)→対:基本周波数(キャリア周波数)
・DUTY(デューティ)
・パルス幅

基本周期はPWMのきめ細かさを決めるキャリア波の周期です。これが小さければ小さいほど、PWM出力の波形はリプルが小さくなります。

DUTYは基本周期1回の内、Hレベルである割合を指します。上の図の場合、左から順に50%、80%、80%がDUTYになります。

パルス幅はHレベルの時間、Lレベルの時間、基本周期の時間などを指すときに使います。「ONのパルス幅は50msec」みたいな感じ。

PWM制御に関してはネット上にかなりの文献があるのでこれ以上は割愛します。

この辺の用語は結構呼び方がコロコロ変わったりする(1回目はキャリア周波数、二回目はPWM基本周波数、、とか)ので、ある程度把握しておくといろいろ理解しやすいかも。

※キャリア波より比較値が大きい場合に出力をHにするか、Lにするかはアプリによって異なります。
 アクティブレベルがH、アクティブレベルがL、なんて呼ばれたりします。

今回完成させたプログラム

まずは結論から。今回か完成させたプログラムを示します。仕様は以下の通り。

・PWMのキャリア波の周波数は10kHzで固定(基本周波数10kHz、基本周期0.1msec)

・Dutyを0~1で指定する。(PWM周波数の1周期(10kHzなら0.1msec)の中で、ONの時間の割合を0~100%で表現)

割と適当に作ってますが、DUTYを何か別の計算結果などに指定すれば、10kHzのPWMで出力を行うことができます。
(ArduinoデフォルトのPWM機能はキャリア周波数が1kHzで固定。10kなので10倍の細かさでPWM機能を使えるぜ。)

#include <math.h>
#include <float.h>

float sys_frequency=10000;                         //システム周波数(とりあえず書いてるだけ)

void setup() {  
  pinMode(9,OUTPUT);                               //今回使う出力ポートの許可設定
  TCCR1A  = 0;                                     //タイマ1設定用レジスタの初期化
  TCCR1B  = 0;                                     //タイマ1設定用レジスタの初期化
        
  TCCR1A |=(1 << COM1A1)|(1<< WGM11);              //出力ピンのモード指定、高速PWM動作指定(1/2)
  TCCR1B |=(1 << WGM13)|(1 << WGM12)|(1 << CS11);  //高速PWM動作(2/2) //分周比設定:8
  
  ICR1   = 200-1;                                  //カウント数上限(TOP値)を設定:任意の割り込み時間の調整
  TIMSK1 |= (1 << OCIE1A);                         //Timer1 割り込み許可
  
  //現在の設定
  //割り込み周波数=16MHz/8=2MHz -> 0.5us -> 0.5us * 200 = 0.1ms -> 1/0.1ms -> 10kHz
}


ISR (TIMER1_COMPA_vect) {                          //タイマー1割り込みー比較Aでdutyを可変する
  //OCR1A=100-1;    //duty50%
  OCR1A=150-1;  //duty75%
}

void loop() {
  
}

上記のプログラムで指定した内容を図的に示すと以下のようになります。

▲ICR1で周期を固定し、OCR1Aでdutyを変える説明図

上記のコードで比較値をOCR1Aというレジスタに、キャリア波のノコギリ波の値の最大値をICR1というレジスタに保存しています。

PWMのキャリア周波数はICR1(0~199の200カウント)、PWMのDUTYはOCR1A(0~99の100カウント)で決定しています。

マイコン内部では基本的にクロックと呼ばれる基準信号を元に値をカウントします。

200カウント数えるたびに波形をHレベルに、100カウント数えるたびに波形をLレベルにすることを繰り返せば、DUTY50%のパルスが出力されます。

このプログラムではOCR1Aの値をICR1の値に対して割合で設定することにより、DUTYを変化させることができます。
つまりOCR1Aを操作すれば、ArduinoのライブラリにあるPWM関数と同様に使用することができます。

ちょっと詳しく説明してみる

ここからはレジスタの設定等、データシートを読んで少し詳しく説明してみます。

最初に示したコードを使用すれば10kHzのキャリア周波数のPWM出力を使うことはできるので、ここから先はもうちょい変えて遊んでみたい人向けかもしれん。

全部丁寧に書くとすげぇ量になるので今回は使った部分だけ抽出して書きます。

データシートを少し読めばいろいろつながると思います。

分周比の設定

キャリア波のノコギリ波は、実際には1クロックごとにカウントアップしていく階段状の値です。

まずはそのカウントアップの速度を決めていきます。

カウントアップのもとになるのは水晶発振子などの振動部品ですが、これらは○○MHzといった具合に周波数が決められています。これだと速すぎたりするので、分周して周波数を下げたりします。

今回は10kHzで割り込みを行いたいと考えているので、分周比8に設定してカウントアップ速度を 16MHz / 8 = 2MHzにします。

この速度でカウントアップを行い、200カウントごとにパルスをHレベルにする(割り込みを発生させる)ことで、PWMの基本周波数が2MHzの1/200、つまり0.01MHz ->10kHzになります。(2MHzから10kHzに設定する処理は後で行います。ここでは2MHzでの設定だけ。)

この設定をどこで行えばよいかですが、データシートに以下のような記述があります。

マイコンにおけるSFRレジスタ、つまり設定を行うような特別なレジスタは、基本的にスイッチ(0 or 1)がいっぱい格納されたもの、というイメージでいいと思います。

以下の場合、8分周にするにはCSn2を0に、CSn1を1に、CSn0を0にせよとの指示があるので、レジスタのビットをこれ通りに設定します。

PWMの種類を選択する

PWMの動作の種類を決めます。

今回はノコギリ波をキャリア波に持つPWMを例に説明しましたが、キャリア波が三角波の場合や比較を三角波の登りと下りで2回行う場合など、いろいろな比較の仕方、動作の仕方があります。

今回のプログラムで使用したのは高速PWM動作モードなので、WGMn3,2,1,0ビットをデータシートの表通りに設定します。

高速PWMには14と15の2つ選択肢がありますが、今回は14番を使用します。(15を使わない理由は後述)

設定の書き方は色々ありますが、今回は以下のようにビットの名前を直接書いてビットシフトで1にするという書き方を採用してみました。

TCCR1A |= (1 << WGM11)
TCCR1B |= (1 << WGM13) | (1 << WGM12) | (1 << CS11); 

比較後の動作を決める

ここでは比較値とキャリア波のカウント値を比較した際、出力をHにするのかLにするのかを決めます。
(アクティブレベルを決めます。)

これらはTCCR1AレジスタのCOMnx1,0で設定できます。

一つ上で設定したPWMの種類によって使える出力のモードなども変わってくるので、この辺りはデータシートを読み込む必要があるね。

▲mega628PBデータシート_P97

今回は比較値が現在のキャリア波のカウント値よりも大きければHレベルを出力したいので、アクティブレベルをHに設定します。

大きければHレベル、小さければLレベルということで反転していないので、非反転出力を選択します。

よって、以下のようになります。

//※先ほどのプログラムに追記

TCCR1A |= (1 << COM1A1) | (1 << WGM11);
TCCR1B |= (1 << WGM13) | (1 << WGM12) | (1 << CS11); 

ここで、PWMの種類を決めたWGMビットがTCCR1AレジスタとTCCR1Bレジスタの二つにまたいでいますが、こういうのは結構あります。

一つのレジスタ内にあるビットを設定すれば1つの設定が完了するというわけではなく、1つの設定を行うのに2つ以上のレジスタをいじらなければいけないこともあるので注意。

この辺もデータシートを読み読みする必要があるね。

PWM基本周波数を決める

次にPWM基本周波数を決定します。

分周比の設定で分周比8に設定し、カウントアップの速度を2MHzに設定しました。2MHzのカウントアップで200カウント数えるたびにパルスをHレベルにリセットすればキャリア周波数は10kHzになるので、ここではその設定を行っていきます。

マイコンにはこのカウントアップの上限を設定するレジスタがあります。このマイコンの場合は「OCRnx」と「ICRn」の2つがあります。

ここでどちらを選ぶかについてですが、今回はカウントアップの上限をICRnレジスタに設定します。

理由ですが、OCRnレジスタは別の場所で使わなければいけないためです。(以降説明)

先ほどのPWMの種類を選択したところで少し述べましたが、高速PWM動作を行う場合は2つの設定パターンがあります。
違いはTOP値を格納するレジスタにICRnをつかうかOCRnAを使うかです。

▲mega328PBデータシート_P98

どちらもカウントの上限値を設定するレジスタとして使用することができますが、仕様に関しては若干の違いがあるので注意する必要があります。ICRnとOCRnの違いはデータシートに記載があります。

▲mega328データシート_P93

書き方がややこしいですが簡単にまとめてみると、

・カウンタのTOP値(PWM基本周波数を決める値)は、OCRnAとICRnのどっちに保存してもOK
・OCRnAは操作によって値を変えても正しく値が更新されるけど、ICRnはタイミング悪いと失敗することがある。(基本周期がずれる)
・カウンタのTOP値を固定で使う(PWM基本周波数を固定で使う)なら、ICRnをTOP値を決めるレジスタとしてありな選択だよ
OCRnAは出力ピンに紐づいており、ここに設定したカウント数で割り込みが発生するよ

といった内容になってます。

今回はDutyを可変して出力を行いたいので出力を操作するためにOCRnAを自由に使えるようにしておく必要があります。

よってカウントの上限値を保存するレジスタはICRnになります。

このような理由でPWMの種類を選択するところで「14番の方の高速PWM」を選択しました。

ICR1Aへの値の設定では直接数値を代入します。

2MHzのカウントアップで200カウント数えるたびにパルスをHレベルにリセットすればキャリア周波数は10kHzになります。
200を設定したいところですが、0始まりの200カウントなので実際は1引いた値となります。

よって、以下のように199を設定します。

ICR1A   = 200-1;                             //カウント数上限:任意の割り込み時間の調整(10kHz)

割り込み許可

最後に割り込み許可を設定します。

割り込みはPWMと分けて説明した方が適切だと思いますが、ここではPWM波形を200カウントごとにリセットする処理を有効にする、みたいな認識で十分かと思います。

  TIMSK1 |= (1 << OCIE1A);                     //Timer1 割り込み許可

DUTYを設定する

ここではPWM波形におけるON時間、DUTYを設定します。

ATmega328ではOCRnAに設定された値が、キャリア波のカウント値と比較され、結果がマイコンのピンから出力されます。

先ほど比較後の動作で「非反転動作」を選択したので、OCRnAに設定した数値でON時間、DUTYを決めることができます。

▲mega328PBデータシート_P93

先ほど少し述べましたが、DUTYはカウントの上限値に対する割合で決まるので、ICRnに対するOCRnAの数値の割合がDUTYになります。

イメージとしては以下のような感じ。最初に使った図を再掲します。

▲ICR1で周期を固定し、OCR1Aでdutyを変える説明図

コードとしては、以下のように記述します。

//タイマー1割り込み-比較Aでdutyを可変する
ISR (TIMER1_COMPA_vect) {
  OCR1A=100-1;    //duty50%
  //OCR1A=150-1;  //duty75%
}

TIMER1、COMPAなどが書いてありますが、これはマイコン内にタイマ1、タイマ2といった具合に複数のタイマがあったり、比較レジスタA、B、Cといった具合に使用するレジスタが複数あったりするためです。

この場合はタイマ1の比較Aレジスタを用いて割り込みを発生させ、PWM機能を使用する、みたいな感じ。

完成したプログラム(再掲)

改めてプログラムを全体を載せます。

#include <math.h>
#include <float.h>

float sys_frequency=10000;                         //システム周波数(とりあえず書いてるだけ)

void setup() {  
  pinMode(9,OUTPUT);                               //今回使う出力ポートの許可設定
  TCCR1A  = 0;                                     //タイマ1設定用レジスタの初期化
  TCCR1B  = 0;                                     //タイマ1設定用レジスタの初期化
        
  TCCR1A |=(1 << COM1A1)|(1<< WGM11);              //出力ピンのモード指定、高速PWM動作指定(1/2)
  TCCR1B |=(1 << WGM13)|(1 << WGM12)|(1 << CS11);  //高速PWM動作(2/2) //分周比設定:8
  
  ICR1   = 200-1;                                  //カウント数上限(TOP値)を設定:任意の割り込み時間の調整
  TIMSK1 |= (1 << OCIE1A);                         //Timer1 割り込み許可
  
  //現在の設定
  //割り込み周波数=16MHz/8=2MHz -> 0.5us -> 0.5us * 200 = 0.1ms -> 1/0.1ms -> 10kHz
}


ISR (TIMER1_COMPA_vect) {                          //タイマー1割り込みー比較Aでdutyを可変する
  OCR1A=100-1;    //duty50%
  //OCR1A=150-1;  //duty75%
}

void loop() {
  
}

ちょっと変えるだけでいろいろ遊べるので試してみると楽しいかもしれません。

出力ピンの確認を行う

今回は実際にプログラムを動かして10kHzのPWM出力オシロスコープで確認し、設定した動作が行えているかをチェックします。

確認にあたって、出力ピンの確認方法もまとめておこうと思います。

このマイコンではOCRnAで指定したカウント数と、現在のカウント数の比較結果がOCnAに渡され、外部のピンへ出力されます。データシートにてOC1Aを「Ctrl+F」キーで検索すると、11ページに以下のような記載があります。

▲mega328データシート_P11

表から読み取るに、OC1Aはmega328の13番ピンである"PB1"に接続されています。

今、マイコンはArduino上に載っているので、チップのピン番号ではなくArduinoの出力ポートとの対応が知りたいです。

なので今度は以下のArduino公式のデータシートを確認してみます。

Arduino - Uno3 datasheet

8ページを見てみると以下のような記載がありました。

上記の表と図より、ArduinoのD9ピンがPB1(OCR1A)と対応していると分かります。

今回 void setup() 内で9番ピンを有効にしたのはこのためです。

書き込み&動作確認

完成したプログラムを書きこんでオシロスコープで波形を確認してみます。

合わせてCRローパスフィルタ回路で出力を平滑し、設定したDutyとアナログ値が同じになっているかどうかを確認してみます。

▲回路図

duty50%の場合

まずdutyを50%に設定した場合の波形を確認してみます。
プログラムは以下の通りです。

ISR (TIMER1_COMPA_vect) {                          //タイマー1割り込みー比較Aでdutyを可変する
  OCR1A=100-1;    //duty50%
  //OCR1A=150-1;  //duty75%
}

出力波形は以下のようになりました。

以下の画像では横軸が25us/divなので、1周期で100usとなっており、PWM基本周波数が10kHzになっていることが確認できます。

またLPFによりフィルターされた平均電圧を見てみると、2.39Vになっていることが分かります。

ピンからの出力電圧のMAX値は4.8Vなので、ほぼ、50%の出力が達成できていることが分かります

レンジを広げてみると以下のような感じです。キャリアが10kHzと高いためにリプルが少ないことが確認できます

duty75%の場合

次にdutyを75%に設定した場合の波形を見てみます。

TOPのカウント数200に対して150なので、割合として75%になります。

ISR (TIMER1_COMPA_vect) {                          //タイマー1割り込みー比較Aでdutyを可変する
  //OCR1A=100-1;    //duty50%
  OCR1A=150-1;  //duty75%
}

出力波形は以下のようになりました。

以下の画像では横軸が25us/divなので、1周期は100usとなっており、PWM基本周波数が10kHzになっていることが確認できます。
(ここはICRnで設定したので、duty(OCR1A)を変えても変化はありません。)

またLPFによりフィルターされた平均電圧を見てみると、3.67Vになっていることが分かります。

ピンからの出力電圧のMAX値が4.8Vなので、ほぼ、75%の出力が達成できていることが分かります

こちらもリプルはかなり小さいです。(さっきと同じ)

ということで、無事確認できました。

まとめ

今回やったことをまとめてみます。

・Arduinoのタイマ機能を使用してPWM出力を行った
・PWMの基本周波数を任意の値(10kHz)に設定した
・任意の周期のPWMで、DUTYを変えて出力できることを確認した(+数値があっていることも確認した)

個人的にマイコンに載ってるチップのデータシート等をじっくり見る機会があまりなかったので勉強になりました。

多少でも参考になれればうれしいです。

それでは、また。

-その他勉強