その他勉強

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

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

備忘録。

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

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

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

はっきり言ってより良いライブラリを探して使った方が、シンプルでいいのかなと感じます。

ですがArduinoのPWMライブラリには1つ問題があります。

それはキャリア周波数が1kHzという低い周波数に制限されていることです。(ものによってはそんなに問題ないかもしれませんが・・・)

キャリア周波数はいろいろ捉え方があると思いますが、基本的には制御の滑らかさを決めるような要素と考えていいと思います。

LEDを1Hzで、Duty50%で制御したら、0.5秒おきに点滅しますが、これは目で見てわかる点滅具合です。

一方で、LEDを1kHzで、Duty50%で制御したら、0.0005秒おきに点滅します。LEDの明るさが電源に直結したときに比べてだいたい半分くらいになっていることはわかりますが、LEDが点滅しているかどうかは、人の目では判別できません。

タイマ機能を使用してPWM制御を行った場合、キャリア周波数を10kHz、20kHz程度の高い周波数にも設定可能です。(自由度UP)

今回は特に使用目的は決めず、実験的にやってみました。

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

最初に最初にPWM制御についておさらいしておこうと思います。

PWM制御は一定の出力(例えば乾電池の1.5V)を一定の周波数(キャリア周波数)で刻み、その時間内(キャリア周期)内でのONとOFFの割合を制御するものです。

乾電池1.5V、キャリア周期1秒を例に、ONの時、OFFの時、PWMの時の違いを図で表すと以下のようになります。

このうちキャリア周期に対するONの時間(上の図では0.6s)の割合のことを、DUTYと呼びます。

式で表すと以下のようになります。

出力としては、最大電圧 or 0V しか出力していないので、実際にはアナログに電圧を可変しているわけではありません。

時間的に電圧を調整することで、疑似的に電圧を可変させている、そんなイメージです。

PWMの仕組み

PWM制御の仕組みについても簡単にまとめておきます。

PWM制御はマイコンのタイマ機能によって実現されています。

タイマ機能は簡単に言うと、時間を測ることができる機能です。

私たちが普段使用している時計は、1秒おきに時間をカウントアップし、3600秒経過したら1時間経過します。それをさらに12回繰り返すと、時計の針が一周します。

これをタイマ機能的に言うと、「時計は1秒という周期でカウントアップを行い、3600×12秒経過したらリセットされる」と言えます。

カウントアップする時間の刻み幅が変わってくるので、マイコンのデータシートなどではカウントの数を用いて説明されることが多いです。

これを図にまとめてみると、以下のようになります。

このタイマ機能を利用してPMW制御を実現することを考えてみます。

このためONとOFFを切り替えるための基準が必要になります。

PWM制御では、タイマ機能でカウントしている値の現在値と、比較値を比較することにより、これを実現します。

カウントアップ時間Xをを 0.5 us に、カウント上限値Nを200に決めた場合、
X×N = 0.5us × 200 =0.1 ms (=10kHz) のPWM制御が実現できます。

これに加えて比較値を設定し、この値を超えたらマイコンのピンからの出力を反転させるようにすると、PWM出力が実現できます。

図で表してみると、以下のようになります。

上記の図では、カウント上限値に200を設定しているので、比較値に100 - 1を設定したときがDUTY50%となります。
(カウント上限値に対する比較値に設定できる数は場合によります。カウント上限値に対する比較値の割合がDUTYです。)

比較対象の値を変更してDUTYを変更する(出力波形を変える)イメージとしては、以下の図のようになります。

動作を言葉でまとめると以下のようになります。

・X×Nのキャリア周期を経過したら、出力を L → H に変更する。
・カウントアップしている際、比較対象の値を経過したら出力をHからLへ変更する。

注意点として、キャリア周期は一定の周期Xでカウントアップを行い、最大値に到達した結果、作られるものです。

ここまでの説明について、少し複雑に見えますが、カウントアップやカウント値と比較値の比較、マイコンのピンの出力の反転などはすべてタイマ機能が自動で行ってくれます。

プログラムする人は、
・カウントアップ周期:X
・カウント上限値:N
・比較値
の3つを設定するだけになります。

10kHzのPWM出力を行うプログラム

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

・PWMのキャリア周波数は10kHzで固定(基本周波数10kHz、基本周期0.1msec)
・比較値を設定してDUTY比(ONの割合のこと)を変更可能

#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) //カウントアップ時間設定
  
  ICR1   = 200-1;                                  //カウント数上限値を設定:キャリア周期を決定
  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() {
  
}

上記のプログラムにおいて、TCCR1AやOCR1Aなどは、マイコン内部のレジスタを指します。

マイコン内にはタイマ機能をはじめとした各種機能がたくさん入っています。

レジスタに0や1を設定することで、どの機能をどのように使うのかを設定できます。

自分で自由に設定を行いたい場合は、マイコン(ArduinoならATmega328P)のデータシートを確認しながら、レジスタに0/1を設定していくことになります。

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

上記のコードで比較値をOCR1Aというレジスタに、カウンタ上限値をICR1というレジスタに設定しています。

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

200カウント到達するたびに現在のカウント値を200→0にリセット&マイコンのピンの電圧をL➡Hレベルに、
100カウントを超えるたびにマイコンのピンのH➡Lレベルにする動作が、マイコン内蔵のタイマ機能により繰り返されます。

このプログラムを利用する場合、OCR1Aの値とICR1の値を自由に設定することで、キャリア周期とDUTYを変化させることができます。

設定内容についての説明

上記で載せたプログラムを利用すれば、キャリア周波数とdutyを自由に変えられるPWM信号を作ることができます。

ここからは実際にタイマ機能を自分で設定して使いたい人向けに、各レジスタの設定などについて説明したいと思います。

カウントアップ速度の決定:分周比の設定

最初に、タイマ機能ではカウントアップを一定周期で行っているという話をしました。

このカウントアップ速度はプログラムする人が自由に設定可能ですが、実はマイコンによってある程度範囲が決まります。

カウントアップのもとになるのは水晶発振子やマイコン内蔵の発振回路などになります。

ですがこれらは○○MHzといった具合に周波数が決められています。これだと速すぎたりするので、分周して周波数を下げたりします。

タイマ機能のカウント値が無限にカウントし続けられれば良いのですが、実は上限値が決まっています。

あまりカウントアップ速度が速すぎると、長い時間を測ることができなくなります

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

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

このカウントアップの速度はタイマ機能に入力されるクロック信号で決められます。

データシートを確認してみると、8分周にして2MHzのクロックにするには、TCCR1BレジスタのCn2を0に、CSn1を1に、CSn0を0にせよとの指示があるので、レジスタのビットをこれ通りに設定します。

今回はタイマ1を使用するので、データシートのCSn2~0のnには1を入れて考えます。

TCCR1B |=(1 << CS11);  //カウントアップ時間設定

タイマの動作モードを選択する

次のタイマ機能の動作モードを決定します。

タイマ機能はPWMののほかにも様々な使い方ができます。

Arduino(ATmega328P)の場合、PWM機能一つをとっても、様々な動作ができるようになっています。

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

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

WGM11ビットはTCCR1Aレジスタに、WGM13、WGM12ビットはTCCR1Bレジスタに配置されているので、それぞれに設定します。

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

マイコンのピンの出力パターンを決める

次にマイコンのピンの出力について決めます。

最初の説明で「現在のカウント値が、比較値を上回ったら出力をH➡Lに、カウント上限値に達したらL➡Hに」と述べましたが、各イベントごとにL➡Hにするのか、H➡Lにするのかはプログラムする人が選択できるようになっています。

今回は初めに説明した通り、「現在のカウント値が、比較値を上回ったら出力をH➡Lに、カウント上限値に達したらL➡Hに」に設定したいので、TCCR1AレジスタのCOM1A1ビットを設定します。

設定はデータシートの下記の表に倣います。

▲mega628PBデータシート_P97

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

//これまでの設定に追記(※論理和で入力)
TCCR1A |= (1 << COM1A1) | (1 << WGM11);
TCCR1B |= (1 << WGM13) | (1 << WGM12) | (1 << CS11); 

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

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

キャリア周波数を決める

次にキャリア周波数を決定します。

ここまでで、分周比の設定で分周比8に設定し、カウントアップの速度を2MHzに設定しました。

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

マイコンにはこのカウントアップの上限を設定するレジスタがあります。

このマイコンの場合は「OCRnx」と「ICRn」のどちらかになります。

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

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

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

▲mega328PBデータシート_P98

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

▲mega328データシート_P93

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

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

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

今回はPWMにより、現在のカウント値が比較値を超えた時点で出力をH➡Lに変更したいのですが、OCRnAがカウント上限値の保存用に使われるとマイコンのピンの出力を変える方法がありません。

よってカウントの上限値を保存するレジスタをICRnに、比較値を保存するレジスタはOCRnAにします。

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

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

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

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

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

割り込み許可

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

割り込み許可を行うことで、タイマ機能によりマイコンのピンの出力を変更したり、カウント値をリセットしたりすることができるようになります。

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

下記の記事で割込み処理について紹介しています。

DUTYを設定する

次にPWMのDUTY(キャリア周期に対するON時間の割合)を設定します。

ATmega328ではOCRnAに設定された値が、タイマの現在のカウント値と比較され、
・現在のカウント値<比較値 ➡ マイコンのピンの出力はH
・現在のカウント値>比較値 ➡ マイコンのピンの出力はL
になります。

ここでは比較値を決めていきます。

▲mega328PBデータシート_P93

最初に述べましたが、DUTYはカウントの上限値に対する比較値の割合で決まるので、ICRnに対するOCRnAの数値の割合がDUTYになります。

イメージとしては以下のようになります。※最初に使った図と同じです。

今回はDUTY50%にしたいので、以下のように記述します。

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

”ISR(TIMR1_COMPA_vect)”についてですが、これはArduino(ATmega328P)でタイマ機能(タイマ割込み)を使用するための記述です。

この関数の中に書いた処理は、タイマ1の割込みが発生するたびに実行されます。

今回はこの関数の中でdutyを設定していますが、この中でなくてもよいです。

たとえば「”void loop()”関数のなかで、ボタンが押されたらOCR1Aの値を変更する」というプログラムを作った場合、ボタンを押すたびにDUTYが変更されます。

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

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

#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) //カウントアップ時間設定
  
  ICR1   = 200-1;                                  //カウント数上限値を設定:キャリア周期を決定
  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%
}

出力波形は以下のようになりました。(黄色:マイコンのピンの出力 / 青色:LPFを通して平滑した電圧)

以下の画像では横軸が25us/divなので、1周期で100usとなっており、キャリア周波数が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となっており、キャリア周波数が10kHzになっていることが確認できます。
(キャリア周波数はICRnで設定したので、duty(OCR1A)を変えても変化はありません。)

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

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

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

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

まとめ

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

・Arduinoのタイマ機能を使用して、任意のキャリア周波数でPWM出力を行った
・マイコンの設定について確認を行った
・任意のキャリア周波数のPWMにおいて、DUTYを変えて出力電圧を調整できることを確認した

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

また何かあればまとめてみたいと思います。

それでは、また。

参考:Arduinoの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値)を設定:キャリア周波数10kHz
  TIMSK1 |= (1 << OCIE1A);                         //Timer1 割り込み許可
  
  //現在の設定
  //割り込み周波数=16MHz/8=2MHz -> 0.5us -> 0.5us * 200 = 0.1ms -> 1/0.1ms -> 10kHz
}


void loop() {
  OCR1A = map(analogRead(A0), 0, 1023, 0, 199);    //可変抵抗の電圧からdutyを決定
}

-その他勉強