その他勉強

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

どうも、メガネです。

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

調べながらやっているので多少ミスってるかもしれませんが、参考にしてみてください。

導入として、dutyは可変できませんが任意の周期でPWM出力する方法について以下の記事にまとめていますので、データシート等を参照しながら参考にしてみてください。

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

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

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

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

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

なので、

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

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

という感じであり、PWM基本周波数を変えたからduty変わったぜ☆!ではないです。

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

USBセレクタを自作する

どうもメガネです。 ノートパソコンとデスクトップを並べて使う人って結構いると思うんですが、そういうときってマウスとかキーボードとかどうしてますか? 個人的にいままではそれぞれ二つ用意して使ってたんです ...

続きを見る

上記の記事では、Arduinoにて任意の周波数で割り込みを行う際、のこぎり波や三角波のカウント上限を変えることで割り込みを発生させる頻度を変え、1秒間あたりに発生させる割り込み回数(周波数)を変えることができることを示しました。

ですがこれは、1秒間に対してのパルスの発生回数が変化しているだけであって、dutyが変化しているわけではないです。

つまり、

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

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

ということです。(何を変えているのか把握しよう)

図で示すと以下のような感じで、「なんの幅をPWMのdutyといっているのか」をしっかり把握しておいた方が良い気がします。

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

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

「ある閾値と大小を比較して、自分で決めたPWM1周期内(固定)でONとOFFの時間の割合をパルス幅で再現する

といったdutyを可変して出力できるプログラムについて解説していきます。

Arduinoで言うPWMはとりあえず幅が変わればいい、みたいな認識だろうけど、たぶん割り込みを使いたいって思って調べる人が求めてるのはこっちなんじゃないかな。

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

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

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

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

というものにしたいと思います。

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

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

ArduinoIDE等にいったん張り付けて照らし合わせながら読むと分かりやすいかともいます。

#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基本周波数)などの設定をおこなう

レジスタの初期化や分周比の計算は前回の記事でやったので若干省略します。

今回はduty可変で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で設定できるとの記載があります。

▲mega628PBデータシート_P97

今回は高速PWM動作を使用すると、WGMnで設定を行ったので、COMnx1,0に何らかの設定を行った場合は上記の表のように設定されます。

今回は通常の出力(非反転出力)を行いたいので、COMnA1を1に設定します。(今n=1のカウンタを使用してる)

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

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

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

PWM基本周波数を決める(詳細な周波数を決める)

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

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

前回の記事ではカウントのTOP値をOCR1Aで決めていたのですが、これは、出力ピンの操作を行うOC1Aというビットに紐づいているらしく、これを使用してしまうとPWM出力のをそうさできなくなってしまうみたいです。

FastPWM動作ではカウント値のTCNTnとの比較の結果をPWM出力として出力ピンから出したいので、これにOCR1Aを使うことを考えると別のものを使わなければならないようです。

(まとめると、出力ピン操作にはOCR1Aが必要なので、TOPの指定に使うとPWMの結果をピンから出せない、という事。)

▲mega328PBデータシート_P93

先ほどのモード選択の表を見ると、実は高速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値を決めるレジスタとしてありな選択だよ
ICRnをTOP値に使うならOCRnAがフリーで使える、つまりOCnAを操作できるからPWM出力を使えるよ

という事だと思います。

このあたりの記述で「PWM出力でパルス幅を変える」ことと、「割り込み間隔を変えてパルスの発生間隔を変えている」の違いが分かったかなぁと思います。(どうやって決めたパルスを出しているの?という問題。)

今はPWM出力を使いたいと考えているので、TOP値をICRnで決める以外の選択肢はないですね。

よって、以下のように設定しました。

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

データシート見た感じ、ICRnに関する記載はなかったんだけど、普通に10進数で記載していいレジスタっぽい?

ICRnAHみたいなよくわからんところもあったけどあんまり関係なさそうだった。

割り込み許可

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

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

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

さて、ここまでで設定は終わりましたので、次に動作確認を行っていきたいと思います。

先ほども書きましたが、mega328ではdutyをOCR1Aで設定することでPWM出力ピンを操作できるようなので、ここに設定する値を変えれば1周期内でのHighとLowの時間の割合を変えられることになります。

下の図と対応させて考えると、
・OCRnAとカウンタ値であるTCNTnの比較によって、COMnAを操作できる。
ICRnでTOP値を指定したことにより周期は固定になるので(下の図は周期が可変だよ)、OCRnAとの比較結果により変化するCOMnAの結果は1周期中のHigh/Lowの割合になる

▲mega328PBデータシート_P93

たぶん、考え方は合っていると思います。

現在PWM割り込み周波数は10kHzに設定したので、1周期は 0.0001 sec となります。つまり、1~200カウント(プログラム的には0 ~ 200 - 1 カウント)が、0.0001msに対応しているというわけです。

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

わかりやすく図にすると以下のような感じだと思います。

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

つまり、
ICR1:OCR1A = Highの時間:1周期 ということです。   ※非反転の場合

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

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

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

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

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

#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の出力をオシロスコープで確認して指定した動作が行えているかをチェックします。が、その前に今書いたプログラムがどこのピンから出るのかわからないのでその確認をデータシートを用いて行います。

先ほどデータシートに、「OCnAでのPWM出力を生成するためにOCRnAが自由に使えます。」との記載がありました。

つまり、OCRnAで指定したdutyはOCnAに渡され、外部のピンへ出力されるという事だと思われます。

データシートにてOC1Aを「Ctrl+F」キーで検索すると、11ページに以下のような記載がありました。

▲mega328データシート_P11

表から読み取るに、OC1Aはmega328の13番ピン(ポートパッド"PB1")に接続されているようです。

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

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

Arduino - Uno3 datasheet

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

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

※そのため最初にpinModeで9番ピンを許可したわけです。

こんな感じでやるとどのポートがどのようにプログラムと対応しているのかわかると思います。

マイコンのチップのデータシートとマイコンボードのデータシートを照らし合わせながら作業できると幅が広がりそうな感じがするなぁ。

書き込み&確認

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

単にD9ピンを見るだけでもいいですが、せっかく10kHzという高い周波数でスイッチングしていますので、LPFを使って出力がきれいに平均化されている様子を見てみたいと思います。

LPFは手元にある適当な抵抗とコンデンサを用いて、750Ωと1uFのCRローパスフィルタを使いました。

カットオフ周波数は120Hz程度にして、以下のサイトで適当に計算しました。

http://sim.okawa-denshi.jp/CRtool.php

▲回路図

duty50%の場合

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

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

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

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

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

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

う~ん、ピッタリで気持ちイイネ☆!

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

まとめ

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

・ArduinoのPWM機能をタイマ割り込みを使用することでキャリア波を任意の値に設定した

・キャリア波の周波数を固定のままdutyを可変することで出力を変えるパルス幅変調を実現した

・タイマ割り込みによって出力を操作し、指定通りの動作をすることを確認した

以上になります。

いかがだったでしょうか。

個人的にマイコンに載ってるチップのデータシート等をじっくり見る機会などはあまりないので、ちょっととっつきにくいところもあったんですが、割と読めばできなくはないかなぁという感じでした。

すこしでも参考になれば幸いです。

それでは。

-その他勉強