ArduinoではanalogWrite(pin,value)でPWM変調した矩形波を出力することができます。

通常のPWM周波数は490Hzや997Hzなのですが、

今回は、PWM周波数を1Hzとしたお話です。

analogWrite()は使わず、AVRライクな書き方をして、実現します。

ATmega328のデータシートとニラメッコしながらレジスタを設定します。

できればレジスタなんか面倒くさいしよくわからないから触りたくなかったんですけど、それしか方法がない!

PWMとは

この辺は自分の覚書でもあるので、わかる方はすっ飛ばしてください(^^)

PWMは Pulse Width Modulation の頭文字をとったもので、文字通りパルス幅変調です。

矩形波の幅を自在に調整して、平均電圧を上げたり下げたりできます。

例えば、5Vの電圧をONしっぱなし(5Vだしっぱなし)なら平均電圧はもちろん5Vです。

ですが、PWMで周期T中のオン時間Tonの割合Duty( = Ton / T )が50%の矩形波を出していれば、平均電圧は

5V * 50% = 2.5V

となり、半分の電圧を出していることになります。

これをLEDに繋げれば、ちょっと薄暗く点きますね。

こんな感じでDutyを色々変えれば、電圧を調整できることがわかります。

analogWrite関数では、analogWrite(pin,value)のvalueがDutyに相当します。

valueは0~255(= 8bit = 2^8 = 256)の値とします。

なぜ1Hz?

詳しくは別記事で紹介予定ですが、低温調理器で使うSSRを制御するためです(^^)

SSRはAC電圧(交流電圧)を制御します。

PWMは、DC電圧(直流電圧)ではDutyで平均値として取り出せますが、ACだとDuty=50%としても半分の電圧にはなりません。

また、50Hzという遅い周波数を制御するので、ArduinoデフォルトのPWM周波数では早すぎました。

そこで、1Hzとしました。

ArduinoのPWM

ところでPWMはタイマーという機能で実現しています。

これはArduinoに限らず、PICやmbedなどの他のマイコンもそうです。

ArduinoはATmega328というMicrochip社(旧Atmel社)のマイコンをメインチップとして載せています。

Arduinoには(というか、ATmega328)には以下の3つのタイマー(正確にはTimer/Counter)があり、Arduinoではそれぞれ以下のような役割になっています。(Unoの例です)

Timer/Counterビット数Arduinoでのpin番号Arduinoでの役割ArduinoでのPWM周波数
Timer08bit5,6普通のデジタルピン,delay(),millis(),micros()PWM出すなら997Hz
Timer116bit9,10普通のデジタルピンPWM出すなら490Hz
Timer28bit3,11普通のデジタルピン,tone()同上

タイマーは3つのうちどれを使ってもいいですが、Arduinoの機能としてあるdelay()やtone()でTimer0やTimer2を使っているので、Timer0や2のPWM周波数を変更してしまうと影響が出てしまいます。

例えば、delay(500)と書けば500msのウエイトが本来発生しますが、変更すると同じように書いても500msではなくなってしまいます。

ですので、何も使ってなさそうなTimer1(9か10pinのどちらか)を使うことにします。

ビット数も他の倍の16bitあるので、遅い周波数でも対応できそうです(このことは詳しくは後述します)。

どうやって1Hzを実現する?

この記事がそのまんま答えです(笑)

というのも酷なので、自分が理解したことをまとめます!

割り込みも使っているのでちょっと変えてます。

PWMのしくみ

まず、PWMの作り方をおさらいしておきます。

これが分かれば、プログラムも読みやすくなります(^^)

PWMを作るには、のこぎり波(三角波)と基準値(Ref)が必要です。

のこぎり波とRefを比較器で比較して、矩形波を作ります。

上の図の例だと、のこぎり波がRefを下から上に抜けた時(①)に矩形波が立ち上がり、のこぎり波がRefを上から下に抜けた時(②)に矩形波が立ち下がります。

これでRefが上下すれば、それに応じて矩形波のオン幅が狭まったり広がったりするのがわかると思います。

アナログ回路だと、縦軸が電圧で横軸が時間の、のこぎり波を実際に作って、Refに相当するDC電圧とコンパレータ(比較器)で比較してPWM波形を作っています。

マイコンではちょっと違います。

原理的には同じですが、相当することをやっています。

マイコンは実際にのこぎり波を出しているのではなく、Timer/Counterで1ずつカウントアップしています。階段みたいな感じですね。

ですので縦軸は電圧ではなく、カウント値になります。横軸は時間なので同じです。

そのカウント値とRefに相当する数値を比較して、カウント値をRefが越えていたら○○pinにHigh(5V)を出す、越えていなかったら〇〇pinにLow(0V)を出す、という風に動いてPWM波形を出しています。

1カウントする時間は、Unoなら16MHzなので1 / 16MHz = 62.5ns(分周なし)です。

分周していたら62.5nsよりも遅くなります。分周についてはプログラムの説明部分でまたでてきます(^^)

基本的な仕組みは上記で、あとは各メーカのマイコンで色々特色があります。

例えば、TiのマイコンはRefをのこぎり波が上抜けする時、下抜けする時で別々の動きを指定でき、さらに2つRefを持っていて2本のPWMを出せるなど、かなり多彩です(ePWM)。

回路図

仕組みがわかったところで、続いて回路図です。

LEDを光らせます。

Timer1を使うので、9か10pinのどちらかを使います。

ですのでPWMの確認用で10pinにLEDをつけることにします。LEDのアノード(足の長い方)は1kΩの抵抗を介して電源5Vにつなぎ、カソード(短い方)を10pinに繋ぎます。これで、10pinの出力がLow(0V)の時にLEDが点灯します。

PWMの割り込みの確認用にBuilt inのLEDを使います。割り込みがかかるごとに状態を反転させます。つまり、1secごとにLEDが点いたり消えたりします。

割り込みは本題ではないですが、PWMを使うときは大体割り込みも一緒に使うことが多いと思うので記載しました(^^)

PWM周波数が正しく設定できているかはオシロスコープがあれば確認できます。というか無いとどうやって確認するんだろ?今回は1Hzなので目視で余裕ですが、1kHzとかは無理ですね(笑)

Amazonで3000円位で売っているので、この際ゲットするのをオススメします(^^)

私はこれを使っています。

1chしかないですが、波形を見れるか見れないかはトラブルシューティングにも大きな違いです!

また、電源が9VのACアダプタですが、USBの5V→9V昇圧ケーブルを変わりに使っています。

これとモバイルバッテリーを合わせれば、ポータブルオシロの完成です!(このケーブルは9Vと12Vがありますので、注意。12V入力すると壊れます。入力定格は10Vまで)

プローブを10pinに繋ぎ、プローブの黒いクリップ(GND)をArduinoのGNDピンにつないで波形を確認します。

プログラム

プログラム全体

/*
システムのクロック : 16MHz
OCR1A output compare register 1 A
setup fast PWM(のこぎり波) at 1Hz in Digital pin D10
*/

#include <avr/io.h> // レジスタ設定するにはavr/io.hのインクルードが必要
// #include <avr/interrupt.h> // 割り込みを機能させる時はインクルードしますが、今回は無くても割り込みがかかるのでコメントアウト

boolean state = false; // Builtin LEDのステータス用

ISR(TIMER1_CAPT_vect){ // PWMの割り込みルーチン 1Hz(=1sec)ごとにこのルーチンに入る
 if(state){ // 割り込みのルーチンに入るごとにstateを反転させ、Builtin LEDを点滅させます
 digitalWrite(LED_BUILTIN,HIGH);
 }
 else{
 digitalWrite(LED_BUILTIN,LOW);
 }
 state = !state;
}

void setup() {
 cli(); // 割り込み禁止(初期設定中に割り込みがかからないように禁止する。まずかからないと思うが一応。)
 pinMode(10, OUTPUT); // PWM出力する10pinを出力モードにする
 pinMode(LED_BUILTIN,OUTPUT); // Builtin LEDのピンを出力モードにする

 // TCCR1A : Timer/Counter 1 Control Register A
 // timer1の設定ができます。
 TCCR1A = _BV(COM1B1) | _BV(WGM11); // 比較一致(Compare Match)でLow(0V),bottomでpin10(OC1B)にHigh(5V)を出力 fastPWM(TOP=ICR1)
 // TCCR1A = 0b00100010; // このように2進数で書いても意味は同じです。マクロ表記と好きな方で書いてOK。

 // TCCR1B : Timer/Counter 1 Control Register B
 // またtimer1の設定をするレジスタです
 TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS12); // fastPWM 256分周

 // ICR1 : Input Capture Register 1
 // PWMのTOP値として使う
 ICR1 = 62499; // 1Hz : 16MHz /(256分周 * 1Hz) - 1 = 62499count。10進数で書いて良い。

 // OCR1B : Output Compare Register 1 B
 // のこぎり波と比較する値。ブログ中で説明しているRefに相当する。
 OCR1B = 6249; // 仮に、Duty = 10%:62499 * 10% = 6249 としている。整数のみ。

 // TIMSK1 : Timer/Counter 1 Interrupt Mask Register
 // 割り込み設定
 TIMSK1 = _BV(ICIE1); // ICR1に達したときに割り込みがかかるようにする

 sei(); // 割り込み許可
}

void loop() {
 // ループでは何もしない
}

動作

10pinにつないだLEDは1秒ごとに点滅して、Built in LED(Arduinoの基板上のやつ)は1秒おきに点滅します。

プログラムの説明

プログラムがコメントだらけで逆に分かりづらくなっちゃったかな?スンマセン!

ATmega328のデータシートを交えながら説明します(^^)

データシートは量が多いし、書いてあることがよくわからなし、読み方がわからないし、英語だしで気力を失いますが、暫く眺めると慣れてきます。嫌でも読まないと理解がいつまで経ってもできませんので頑張りましょう(笑)できるだけわかりやすくなるように努めますので(^^)

幸い、ATmega328は日本語に翻訳してくださったデータシートがあるのでそれを眺めても良いです。しかし、所々誤字があったので英語のデータシートを合わせて見ることをオススメします。

余計なところで躓いてしまいますから(私は躓きました・・・)

Microchipの最新のデータシートから引用して紹介します(^^)

まず、ヘッダーファイルのインクルードです。

#include <avr/io.h>           // レジスタ設定するにはavr/io.hのインクルードが必要

// #include <avr/interrupt.h> // 割り込みを機能させる時はインクルードしますが、今回は無くても割り込みがかかるのでコメントアウト

これを書かないと、レジスタの設定が反映されません。また、ArduinoではなくATmega328単体で使う時は、avr/interrupt.hをインクルードしないと割り込みが機能しなかったのですが、Arduinoはインクルードしなくても(書かなくても)動きました。なんでだろ?

割り込みルーチンの説明は後に回します。

初期設定のsetup()

初期設定のsetup(){}です。

  cli();                                        // 割り込み禁止(初期設定中に割り込みがかからないように禁止する。まずかからないと思うが一応。)

これは割込み禁止の記述です。割り込みの設定中に割り込みが入ることを防止するために記述しています(が、まぁ、setup()に記述している時点で割り込みがかかることはないと思いますが一応ね)

  pinMode(10, OUTPUT);                          // PWM出力する10pinを出力モードにする

10pinからPWMを出力するので、出力モードにしています。

次からレジスタ関連です。

引用:ATmega328データシート DS40002061A-page 133

引用:ATmega328データシート DS40002061A-page 142

10pinにfastPWM(のこぎり波)を256分周、Top値=ICR1で動かす設定をします。

それには、上記Table16-4のMode14のように設定します。

かなり多彩な設定ができますが、PWM周波数を○○Hzのように設定するにはTOP値を自分で指定できる必要があり(今回はTOP値=ICR1としている)、また、1Hzというシステムクロック16MHzに比べたらとてつもなく遅い周波数は大きなmax値のカウンタと分周を駆使しないと実現できないので16bit(=65535カウントmax)を使えるモードとしてMode14を選びました。

(例えばMode5,6,7では同じfastPWMでも8bit(=255カウントmax),9bit(=511カウントmax),10bit(=1023カウントmax)となります)

似たようなモードでMode15があります。Mode14との違いはTop値がICR1とするか、OCR1Aとするかです。Mode15を選ばなかった理由としては、今回は10pinに出力するので実はMode14でも15でもどっちでも良いのですが、9pinに出力する場合にMode15では問題になるからです。

のこぎり波と比較してDutyを決める値として、10pinなのでコンペアレジスタOCR1Bを使用しています。これが、もし9pinからPWMを出すとなると、Dutyを決める値としてOCR1Aを使いたいし、Top値としても使いたいしで、かぶってしまい希望の動作をしなくなります。(PWMはフィードバック制御にも使うことが多いですが、その場合だいたい、Dutyは制御値をフィードバックした値が入り、常に動いているので、かぶっていると良くないですね)

今回はサンプルなので良いですが、実際に組み込む時に、基板上の配置結線などの都合で同じ機能のピンならピンを交換することもありえます。そこでの汎用性を考えてMode14(Top値=ICR1)としています。

設定はTCCR1A(Timer/Counter 1 Control Register A)とTCCR1B(Timer/Counter 1 Control Register B)の2つのレジスタで行います。

まず、TCCR1Aから。

  TCCR1A = _BV(COM1B1) | _BV(WGM11);            // 比較一致(Compare Match)でLow(0V),bottomでpin10(OC1B)にHigh(5V)を出力 fastPWM(TOP=ICR1)

 // TCCR1A = 0b00100010; // このように2進数で書いても意味は同じです。マクロ表記と好きな方で書いてOK。

10pin(OC1B)に出力するので、5bit目 : COM1B1と4bit目 : COM1B0で設定します。比較一致でClear = Low( = 0V)、Botton(カウント値 = 0)でset = High( = 5V)となるようにするので、5bit目 : COM1B1を1にします。

もし9pin(OC1A)に出力する場合なら、7bit目と8bit目を同じ感じで設定することになります。

また、Mode14にするので、Table16-4より、1bit目 : WGM11を1にします。

2進数でゼロの羅列で書いても同じですが、それだとどのビットをいくつに設定しているかがパット見でわかりずらいので、私はマクロ表記の方が好きです。

引用:ATmega328データシート DS40002061A-page 140

引用:Arduino Uno Rev3回路図
https://www.arduino.cc/en/uploads/Main/Arduino_Uno_Rev3-schematic.pdf

引用:ATmega328データシート DS40002061A-page 12

次に、TCCR1Bです。

  // TCCR1B : Timer/Counter 1 Control Register B

 // またtimer1の設定をするレジスタです
 TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS12); // fastPWM 256分周

同様に、Mode14にするので、Table16-4より、4bit目 : WGM13と3bit目 : WGM12を1にします。(WGM11はTCCR1Aで先程設定済み)

256分周にするので、Table16-5より2bit目 : CS12を1にします。

引用:ATmega328データシート DS40002061A-page 142

引用:ATmega328データシート DS40002061A-page 143

256分周としたのは、上でも少し説明しましたが、1Hzを作るためです。

1Hz=1secをカウントするためには、少なくとも、16bitのtimer1が1secの間にオーバーフローしないようにしなければなりません。

つまり、1secを16bit = 2 ^16 = 65536カウント(実際は65536カウント目はオーバーフローして0に戻るので65536 – 1 = 65535カウント)以内にカウントしなければなりません。

例えばシステムクロックは16MHzですが、すでに上で書いたように、1カウントする時間は、Unoなら16MHzなので1 / 16MHz = 62.5ns(分周なし)です。

これでは、1secカウントするどころか、62.5ns * 65535 = 4.095ms でオーバーフローしてしまいます。

つまり、分周なしでカウントすると4.095msまでしか数えられません。

そこで分周です。

256分周なら、256倍遅くなるということなので、

62.5ns * 256分周 * 65535 = 1.04856 sec でオーバーフローします。

ということで、これなら1sec数えることができます。

次に、Top値に設定する、ICR1(Input Capture Register1)の設定です。

  // ICR1 : Input Capture Register 1

 // PWMのTOP値として使う
 ICR1 = 62499; // 1Hz : 16MHz /(256分周 * 1Hz) - 1 = 62499count。10進数で書いて良い。

先程の分周を踏まえて、1HzとなるようなTop値を設定します。すると、62499カウントとなります。同様に考えれば、タイマーがオーバーフローしない限り、好きな周波数を作れることがわかると思います。

次は、のこぎり波と比較してDutyを決めるコンペアレジスタOCR1B(Output Compare Register 1 B)の設定です。

  // OCR1B : Output Compare Register 1 B

 // のこぎり波と比較する値。ブログ中で説明しているRefに相当する。
 OCR1B = 6249; // 仮に、Duty = 10%:62499 * 10% = 6249 としている。整数のみ。

繰り返しになりますが、10pinなのでOCR1Bです。9pinにPWMを出すならOCR1Aとなります。

仮に、Duty = 10%としてみると、OCR1B = 6249カウントとなります。

これで5Vが10%のPWM波形が出力されます。

次は割り込みの設定をTIMSK1 : Timer/Counter 1 Interrupt Mask Registerで行います。

  // TIMSK1 : Timer/Counter 1 Interrupt Mask Register

 // 割り込み設定
 TIMSK1 = _BV(ICIE1); // ICR1に達したときに割り込みがかかるようにする

ICR1(Input Capture)に達した時に割り込みがかかるようにしたいので、5bit目 : ICIE1(Input Capture Interrupt Enable)を1にして、割り込みを有効にします。

これに対応する割り込み処理のベクタ名はISR(TIMER1_CAPT_vect){}中のTIMER1_CAPT_vectです。

引用:ATmega328データシート DS40002061A-page 144

割り込みがかかるタイミングは、他のbitを設定することで色々設定できます。例えば、このようにTop値ではなく、1bit目 : OCIE1A(Output Compare Interrupt Enable)や2bit目 : OCIE1Bを1に設定すれば、コンペアマッチ(比較一致)した時に割り込みをかけることができます。

この場合、割り込み処理のベクタ名は変わりますので注意してください。(TIMER1_CAPT_vectではないです)

初期設定が終わったので、割り込みを許可します。

  sei();                                        // 割り込み許可

これでsetup(){}は終わりです。長かったですね(笑)

メインループ(何もしてない)

メインループは何もしてません。

割り込みルーチン

続いて割り込みルーチンです。

ISR(TIMER1_CAPT_vect){        // PWMの割り込みルーチン 1Hz(=1sec)ごとにこのルーチンに入る

 if(state){ // 割り込みのルーチンに入るごとにstateを反転させ、Builtin LEDを点滅させます
 digitalWrite(LED_BUILTIN,HIGH);
 }
 else{
 digitalWrite(LED_BUILTIN,LOW);
 }
 state = !state;
}

このルーチンにはカウント値がTop値であるICR1の値になった時に割り込みがかかり、入ります。

中身は大したことはしてなくて、ルーチンに入るたびにstateの状態を反転させ、それによってBuilt in LEDを消したりつけたりしているだけです。

ISR()は上にも書きましたが、対応する割り込みの種類で書き方が決まっています。今回の割り込みでは、ベクター名(ISR()の()の中身部分)はTIMER1_CAPT_vectです。

波形

動作確認しましょう(^^)

オシロでみると、無事、1Hz、Duty=10%の波形が出力されていることが確認できました(^^)

(GNDがなぜかこのレンジだと浮いてしまう。20ms/div以下にすると浮かなくなる。なんでだ?)

おしまい(^^)