その他勉強

【Arduino】Arduinoのタイマ割り込みでDutyを変えてPWM出力する方法を解説する

2023年1月15日

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

今回はArduinoのタイマ割り込みを使用した際、Dutyを可変してPWM出力を行い、制御などに応用できるようにするプログラムを完成させました。

備忘録。

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

PWM基本周波数を変える=Dutyを変える、ではないYO

今回の記事を書く前にまず示しておきたいのですが、PWM基本周波数を変える=Dutyを変えることではないです。

PWMとはパルス幅変調の英訳ですが、その原理はキャリア波である鋸歯や三角波と目標値を比較して出力をHighかLowに振り分ける、というものです。

基本的にこの大小比較は基準となる1周期中に1回行われるのですが、その基本周期がPWM基本周波数であり、dutyはその周期のなかでどれくらいの割合でHighの状態になるか指すものです。

なので以下の場合はPWM周期が変化しているのであって、Dutyが変化しているわけではないです。

・PWM基本周波数1Hz-duty50%:0.5秒間だけHigh

・PWM基本周波数10Hz-duty50%:0.05秒だけHigh

以下の例がPWMの基本周波数が変わった例です。

・PWM基本周波数1Hz-duty30%:0.3秒間だけHigh

・PWM基本周波数1Hz-duty50%:0.5秒だけHigh

PWM基本周波数を可変にすればdutyを変えてるように見えなくはないのですが、それは言ってしまえばタイミングを時間によって可変しているだけのなので、何に対するdutyだよ、という問題になるかと思います。(つまりDutyは何らかの値で固定になってる。)

つまり、

◎:duty変えたぜ!=1周期のパルスのHighとLowの時間の割合を変えたぜ

✖:duty変えたぜ!=割り込みの発生頻度(1周期の長さ)を変えたぜ

という感じ。

以下にどこが「周期」でどこが「duty」なのかをまとめておきました。

▲dutyを変えるとはどの部分を言っているのか?

今回は後々PI制御などで使えるプログラムにしたいと思っているので、上で述べたような

「ある閾値と大小を比較して、自分で決めたPWM1周期内(固定)でONとOFFの時間の割合をパルス幅で再現するプログラム」を作っていきたいと思います。

今回完成させるプログラムの内容

今回完成させるプログラムは、

・割り込みが発生する周波数は10kHzで固定(=PWM基本周波数)

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

というものです。

これなら何らかの制御で作り出した出力をパルス幅変調で出力できると思います。
(ArduinoデフォルトのPWM機能はキャリア1kぐらいなのでなかなかに遅い。より高いキャリア周波数で使える)

説明の前に一度、完成したプログラムを以下に示します。

#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() {
  
}

割り込み周波数(PWM基本周波数)などの設定をおこなう

まずは設定です。

レジスタの初期化、分周比の設定

分周比の設定を行っていきます。

まず最初にタイマ1用のレジスタを初期化します。

void setup() {
  pinMode(9,OUTPUT);                          //今回使う出力ポートの許可設定(なぜここなのかは後で解説)
  TCCR1A  = 0;                                //タイマ1設定用レジスタの初期化
  TCCR1B  = 0;                                 //タイマ1設定用レジスタの初期化

次に分周比の設定を行います。

今回は10kHzで割り込みを行いたいと考えているので、分周比8で 16MHz / 8 = 2MHz、2MHzを200カウント行うことで1 / 200の周波数に落とす、つまり0.01MHz ->10kHz、とします。

タイマ1の動作を決めるレジスタはTCCR1A及びTCCR1Bレジスタであり、以下の二つの表からWGMnとCSnビットを操作すれば動作モードを決められることが分かります。
今回は高速PWMを使用したいので、14番を使用します。(15を使わない理由は後述)

今回は高速PWM動作を使うので、WGM12とWGMn0を1に、分周比8を設定するのでCS11を1に設定しました。

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

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

動作モードを決める

次に動作モードを決めます。

比較一致によるPWMを行うといっても、カウント値より比較値が大きくなったらHighにするのか、それともLowにするのかなど、いろいろ設定できます。

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

今回は高速PWM動作を使用するように設定したので(WGMn)、COMnx1,0に何らかの設定を行った場合は以下のようになります。

▲mega628PBデータシート_P97

今回は通常の出力(非反転出力)を行いたいので、COMnA1を1に設定します。

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

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

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

PWM基本周波数を決める

次にPWM基本周波数(割り込み周期)を決定します。

上記までで分周比8、つまり2MHzに設定できたので、次は10kHzで割り込みを発生させるためにカウントのTOPの値を設定します。

2MHzを200分の1にするとちょうど10kHzになります。
つまり2MHzでカウントアップするTCNTnが200に到達したときにカウントをリセットするようにすれば、ちょうど10kHzで動作します。

ここで、カウントのトップ値を設定するレジスタには「OCRnx」と「ICRn」の二つがあります。

先ほどのモード選択の表を見ると、実は高速PWM動作を選択するパターンが二つあります。
違いはTOP値を格納するレジスタにICRnをつかうかOCRnAを使うかです。

▲mega328PBデータシート_P98

要するにWGMn0を1にするか0にするかでTOP値を格納する場所を選べます、ということです。

OCR1AとICRnは基本的には同じ役割になる、ということですが、使用に関しての注意点も記載されていました。

▲mega328データシート_P93

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

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

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

今回はDutyを可変して出力を行いたいので出力を操作するためにOCRnAを自由に使えるようにしておく必要があります。
なので一定のタイミングで動いてほしいPWM周期のカウントトップ値を決めるレジスタはICRnということになります。

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

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

割り込み許可

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

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

dutyを可変にする記述を行う

ここまででPWMの基本周波数を決める設定が終わりました。次はDutyを可変する部分について説明していきます。

mega328ではOCR1Aに設定された値と現在のカウント値を比較し、その結果を出力ピンから出します。

つまり、現在のカウント値であるTCNTnがOCR1Aよりも小さければHighを出力し、逆ならLowを出力します。

▲mega328PBデータシート_P93

現在PWM割り込み周波数は10kHzに設定したので、1周期は 0.1 msec となります。
カウント数でいうと、200回カウントした時に0.1msになります。

なのでOCR1Aに代入する値をICR1に設定したカウント最大値に対して割合で設定することでdutyを変えられます。

図にすると以下のようになります。

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

つまりICR1で基本周期を決まり、OCR1AでDutyが決まります。

今回はdutyを固定で動かすとして、以下のようにベタ打ちでやりました。

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

最大のカウント値が200の時に100回までカウントした時点で出力をHigh→Lowに切り替えれば、Duty50%のパルスが出力されます。

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

ここまででプログラムは完成です。

以下にプログラム全体を載せます。

#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() {
  
}

出力ピンの確認を行う

ここまでで準備は終わりました。次にPWMの出力をオシロスコープで確認して指定した動作が行えているかをチェックします。

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

▲mega328データシート_P11

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

今、マイコンは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%の出力が達成できていることが分かります。

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

まとめ

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

・でPWMの基本周波数(キャリア周波数10kHz)を任意の値に設定した

・PWMの基本周波数を固定のままDutyを変化させて出力を確認した

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

それでは。

-その他勉強