Arduino Uno – PWM周波数を”自由に”変更する

PWM周波数の変更方法について書いてくれてるブログやサイトはたくさん見つかるけど、じゃ結局どうすりゃ変わるのよってところでつまづいたので、自分なりにまとめてみました。

(丁寧に説明してくれてるとこもあるんですけど、丁寧のベクトルが違うっていうか、私みたいな電子工作初心者にはわかりづらい内容だったり…)

(あと検索法としては、Arduinoで検索するよりも マイコンICである328Pであったり、AVRの情報を直接探した方が情報が多かった。-> というより、そっちで調べたらいっぱいありましたw

こういう小難しいことをする人達はArduinoの状態では納得がいかないようだ(´・ω・`) )

実行環境:
  • Arduino Uno R3 (ATmega 328P)
  • Arduino IDE 1.8.0
  • Windows 10 Home (どれ使っても一緒だろ!!いい加減にしr)
  • 出力ピン – 10ピン (タイマ1を使用しているため)
目的:

PWM制御のモーターの磁励音 (DCだけどこれでいいのかな?) を可聴域まで下げたい。

コード:

とりあえず”自由に”周波数変更できるコードだけ見せろって方もいらっしゃるかと思いますので、実例を。

#include <avr/io.h>

#define PWMPin 10

unsigned int frq = 440; // 周波数
float duty = 0.5; // 指定したいデューティ比

void setup() {
  pinMode(PWMPin, OUTPUT);
}

void loop() {

  // モード指定
  TCCR1A = 0b00100001;
  TCCR1B = 0b00010010;

  // TOP値指定
  OCR1A = (unsigned int)(1000000 / frq);

  // Duty比指定
  OCR1B = (unsigned int)(1000000 / frq * duty);
}

これで 440Hz – デューティ比 50% の 矩形波 が出力されるはず。

内心ミスってるんじゃないかと思いつつ  実験した結果がこちら。

周波数 440Hz – デューティ比 50% の綺麗な波が出力されました。

もちろん、周波数やデューティ比の変数をいじってあげれば”自由に”波を作れます。

(こちらのオシロは秋月で売ってるキットです。組み立て の前段階の抵抗値の選別 が大変だった。

周波数表示機能とかもあるみたいですが、うまく動かない… 誰か教えて頂けると嬉しいです。)

注意点:

”自由に”波を作れると書きましたが、上記コードでは 16Hz – 1MHz までの波しかつくれません。

(1MHzというのは理論値です。実際に出力されるかは試してないので分かりません。

また、1MHzに近づくにつれデューティ比の指定が困難となるので、10kHzくらいが限界ではないでしょうか。)

分周比の設定などによって変わる為、この範囲外の周波数を扱いたい方は下の解説っぽいものを読んで、理解して下さい。

周波数が変化するコードを書いて、不具合が起きたらこの範囲外になっていないかチェックしてみて下さい。

また、上記コードでは出力ピンは10ピン限定です。

PWMPin の値を変えても波は出力されません。

解説(のような何か)

PWMの動作原理

Arduino Uno には PWM出力に使われる タイマ/カウンタ が 0,1,2 の三個 あります。

あらかじめ設定しておいた TOP値 まで カウンタ をインクリメントし、OCRxA / OCRxB (xはカウンタ番号。カウンタにつき2つの出力ピン A,B が割り当てられている) と一致したときにピンの出力を変化させます。

Phase Correct PWM (位相基準PWM)、Phase and Frequency Correct PWM (位相・周波数基準PWM) では、カウンタが TOP値 に到達するとデクリメントし、0になるまで同様の動作をします。

カウンタのインクリメントスピードは IC の動作周波数に依存しますので、TOP値 が大きいほど出力される周波数は低くなります。

0,2は8bit、1は16bit で動作する為、TOP値 の最大値はそれぞれ 255, 65535 となります。

可聴域まで周波数を下げたいので、上記コードでは 16bit の タイマ1 を使用しています。

また、インクリメントスピードは 分周比 の設定によってざっくりと変更することが出来ます。(1/8/64/256/1024 の中から選択)

分周比 ってなんぞや?って方は以下のコードを見ればなんとなく理解して頂けるかと思います。

#define div 8 // 分周比

int counter = 0; // カウンタ
int flg = 0;

void loop() {

  if (flg == div) {
    counter++;
  }

  flg++;

}

 

従って、出力される周波数は、以下の式で表せます。

f = IC の動作周波数 / (分周比 * TOP値)

また、Phase Correct / Phase and Frequency Correct ではデクリメントもすることから、この式からさらに2で割ります。

Phase and Frequency Correct を使用した Arduino Uno の場合、以下の式となります。

f = 16,000,000 / (分周比 * TOP値 * 2) = 8,000,000 / (分周比 * TOP値)

逆に TOP値 を求める式をつくると下のようになるということです。

TOP = 8,000,000 / (分周比 * f)

より詳しいデータや動作概念は garretlab さんの HP にわかりやすくまとめられておりますので、そちらをご覧下さい。(決してめんどくさくなったわけではない。)

実際にスケッチに書くのは?

ライブラリのインクルード

#include <avr/io.h>

レジスタを直接いじるには、上記のライブラリをインクルードする必要があります。

これを忘れて 30分 ほどハマりました(´・ω・`)

レジスタの設定

まず、タイマーを動かしたり、分周比を変更したりする、レジスタ の設定をします。

設定するレジスタは TCCRxA / TCCRxB (xはカウンタ番号)です。

(タイマ1にはTCCR1Cがあるのですが、強制出力モードの指定に使うレジスタらしいです。そのような使い方はしないのでここでは無視します。

強制出力は標準 / CTCモードのみで有効なようです。使い道がよくわかりません。)

モードの詳細については garretlab さんの HPうしこさん の HP をご覧になった方が早いと思われますので、割愛させて頂きます。

PWMモードは Phase Correct and Frequency Correct モードを指定します。

同モードの指定には WGM10 が異なる2つの選択法がありますが、これは ICR1OCR1A のどちらかを選択し、 TOP値 として割り当てることが出来るためです。

ICR1 を TOP値 とすれば、OCR1A/B の両方で同周波数の波を出力出来ます。

OCR1A を TOP値 として割り当てると、ダブルバッファが行われるとのことで、基本的にはこちらを選択する方が良いかと思われます。

今回は 10ピン のみ出力しますので、TOP値OCR1A に設定します。

よって、WGM13 / WGM12 / WGM11 / WGM10 はそれぞれ、1,0,0,1 となります。

COM1B0 / COM1B00, 0無出力0, 1トグル動作 (一致時の出力を反転)、

1, 0 は カウンタ がOCR1A/B – TOP間 にある場合 LOW0 – OCR1A/B間HIGH となるよう出力、

1, 1 は1, 0 の逆です。

スケッチを書く際にわかりやすいので 1, 0 を選択します。

分周比8 を 選択していますので、CS12 / CS11 / CS10 はそれぞれ、0, 1, 0 となります。

全てまとめると以下のようになります。

TCCR1A = 0b00100001;
TCCR1B = 0b00010010;

2進数で指定していますので、0bを先頭に追加します。Bとか0Bとかでも大丈夫っぽいです。

TCCR1A には 0bHGFEDCBAH / G / F / E / D / C / B / A にそれぞれ COM1A1, COM1A0, COM1B, 無指定, 無指定, WGM11, WGM1010 で設定します。

同様に、TCCR1B には それぞれ、無指定(ICNC1), 無指定(ICES1), 無指定, WGM13, WGM12, CS12, CS11, CS10 を設定します。

このレジスタ設定はビット演算子を用いて指定している方が多いようですが、私は初心者ですので、混乱を防ぐため単純に代入して指定しています。

周波数、デューティ比の指定

OCR1A には動作原理の方で求めた TOP値 を代入してやればおk

OCR1B には指定したデューティ比で切り替わるように TOP値 * デューティ比 の値を代入します。

また、どちらの値16bit の上限である、65535 を超え、オーバーフローすることのないよう、unsigned int でキャストしてやります。

(Arduino には word という型が unsigned int と同じように使えるらしい。しかし、Arduino以外では使われておらず、キモい 不便なのでここではunsigned int を使ってます。)

#define frq 440 // 周波数
#define duty 0.5 // デューティ比

OCR1A = (unsigned short) 1,000,000 / frq;
OCR1B = (unsigned short) 1,000,000 / frq * duty;

これで思った通りの出力がされるはずです。

出力を止めたいとき

デューティ比0 にするか、COM1B1/00, 0 にするなり

個人的には後者の方がしっかりしてる気がする(小並感)

終わりに

Arduinoは解説ページや入門書などが多く、電子工作初心者向けにとても良いツールだと思います。

ただ、今回のようにマイコンの世界に一歩踏み入れたようなところでは、解説ページがデータシートと同じように、読む人任せ になっちゃってる部分が多いですね。

データシートは仕方ないにしても、マイコンの世界での”解説ページ”は、小難しく書くことをよしとしている風潮 があるというか、初心者に優しくない気がします…

マイコンの世界に限らず、マイナーな世界はどこもそんな感じですよね…

(単純に自分が理解してることを第三者に伝えるのがダルいだけかもしれませんがw)

解説ページ書くならわかりやすく書きやがれっておもいました(小並感)

私はそういうマイナーな趣味が多いので、わかりやすさを第一に色んな記事を書いていきたいなー、と思ってます。

(そもそも覚書だし、わかりやすくないと意味がない)

分かりづらいところがあったら是非コメントを残していって下さい!

参考:

http://garretlab.web.fc2.com/arduino/inside/arduino/wiring_analog.c/analogWrite.html

https://www.arduino.cc/en/Tutorial/SecretsOfArduinoPWM

http://usicolog.nomaki.jp/engineering/avr/avrPWM.html

http://www.avrfreaks.net/forum/icr1-vs-ocr1a-top-value-pwm

http://www.avrbeginners.net/architecture/timers/timers.html

Arduino Uno – PWM周波数を”自由に”変更する」への24件のフィードバック

  1. きゅうなな

    マイコン初心者の学生です。ブログ拝見させていただいたところ、非常にわかりやすくためになりました。これを機にarduinoにも手を出してみたいと考えています。
    そこで質問なのですが、このPWM周波数の変更はブラシレスモーターにも適用可能でしょうか?
    手元にHDDのモーターがあるので回してみたかったのですが、どうせならVVVF制御できないものかと思い、いろいろ調べてみました。するとPWM周波数をキャリア波に、duty比をモーターの位相にすることで回転できそうなことがわかりました。またデフォルトのPWM出力であれば、analogWriteを3つ記述することでduty比の異なる3つの信号を送出できることがわかりました。
    しかし本記事を読んだところ、タイマの周波数を変更した場合、出力ピンは10ピン限定だということで、3つの信号が送出できない可能性があることがわかり、すると回転しないのではと考えました。
    3つのタイマはそれぞれ仕様が違うようなので、できれば一つのタイマのみでPWM制御できないかと考えています。
    arduinoでPWMの周波数を変更しつつ、duty比の異なる三相の信号を異なるピンで出力することは可能でしょうか?教えていただけると幸いです。

    返信
    1. S2KTS. 投稿作成者

      きゅうなな さん
      コメントいただきありがとうございます!

      Arduino、というよりも、ATmega328Pの仕様なんですが、1つのタイマからは2ピンまでしか出力できません。
      なので、ご希望の方法では「不可能」、というのが結論になるかと思います・・・。

      また、この記事で、出力ピンが10ピン限定になってるのは、スケッチが煩雑にならないようにするためであって、解説部にあるように、ICRをTOP値として指定するよう、WGM10を0に変更すれば9ピンに出力することもできるはずです (未検証ですが・・・)。

      Duty比の異なる3つの信号をanalogWrite()にてPWM出力することはもちろん可能です。
      ただし、三相モーターを回すには磁界を回転させる必要があります。
      三相モーターの理想的な電流は、正弦波であり、回転させるためにはそれぞれの波の位相をずらす必要がある、ということについてはご存知でしょうか?

      もちろん、綺麗な正弦波を出力するのはそこそこ難しいので、通常は以下のようなデジタル制御方法が用いられます。
      https://www.orientalmotor.co.jp/tech/reference/brushless01/

      カクカク動かすイメージですね(^^)/

      また、仮にanalogWrite()でDuty比の異なる信号を出力したとしても、位相が全て同じ信号が出力されてしまうので、残念ながら、モーターは回転しません・・・。

      従いまして、個人的には、上記のリンクのような制御方法を用いて、デジタル制御するのが最も現実的な方法かなぁと思っています。(実はこのスケッチを書くにあたって、私も三相モーターについて色々調べておりましたw)

      digitalWriteだと速度不足・・・ということでしたら以下の記事のように、AVRマイコンを直接動かすような書き方をすると、高速化することができると思います!
      http://ehbtj.com/electronics/speedup-arduino/

      返信
  2. RyogenTanaka

    arduinoUNOのPWM速度を最大近くまで上げてオーディオ再生する方法がいろんなところにありますよ。
    PWM関係のレジスタを直接いじるとローファイながら音声再生が可能です。
    DDS技術を応用し、180ポイントの正弦波ないし三角波のテーブルを用意、参照位置を60ポイントづつずらして、三つ同時にPWM出力すれば位相がずれた波形を出力できるのではないでしょうか?。

    返信
    1. S2KTS. 投稿作成者

      RyogenTanaka さん
      ご指摘いただきありがとうございます!

      今回の記事では、その ”PWM関係のレジスタを直接いじる” 方法 を説明していたつもりです。(わかりにくいようでしたらすみません。。。)

      DDSモジュールというものがあるんですね~
      これなら確かに位相をずらしたり、綺麗な正弦波を出したりするのも自由自在ですね!

      ご教授頂きありがとうございます!

      返信
  3. ふろーと

    arduinoを夏頃から始めた学生です。記事を拝見させて頂き、とてもためになりました。
    この記事を元に、arduinoでモータを2つ駆動したいと考えております。
    2つのピンのPWMの周波数をどちらも10kHzにし、
    それぞれのピンとモータドライバを接続しようと思っております。
    2つのピンをどちらも10kHzに変更するにはどのようにしたらよいでしょうか?
    お忙しいと存じますが、教えて頂けますでしょうか。

    返信
    1. S2KTS. 投稿作成者

      ふろーと さん
      コメントいただきありがとうございます!

      単純に、
      unsigned int frq = 440;
      の部分を
      unsigned int frq = 10000;
      に変更すれば動作すると思います!

      (わかりづらいようでしたので、スケッチにコメントを追加いたしました。)

      返信
      1. ふろーと

        お忙しいところお返事ありがとうございます。

        なかなか理解が追い付かず、大変申し訳ないのですが、

        9番ピンで10kHz、デューティー比:50%
        10番ピンで10kHz、デューティー比:80% を出力しようとしたら、
        どのようなスケッチになるのでしょうか?

        お忙しいと存じますが、教えて頂けますでしょうか。

        何度も何度もすみません…

        返信
        1. S2KTS. 投稿作成者

          ふろーとさん

          以下のようなスケッチになるかと思います。
          GitHub – 2Pin_example

          一つのレジスタから異なるデューティ比の信号を取り出すには、以下の点が重要となります。

          同モードの指定には WGM10 が異なる2つの選択法がありますが、これは ICR1 か OCR1A のどちらかを選択し、 TOP値 として割り当てることが出来るためです。

          9ピンから出力するには、OCR1A の値を指定しなくてはならないわけですが、今回の記事のスケッチではこれをTOP値として指定しています。
          2つの出力を行うためには WGM10 に ICR1 を指定する必要があります。

          返信
          1. ふろーと

            お忙しいところお返事ありがとうございます。

            お答えいただいたプログラムで試してみたのですが、
            9番ピンからの出力が0Vのままで何も出力されませんでした。

            何度も何度もすみませんが、教えていただけると幸いです。
            お忙しいところすみません…

          2. S2KTS. 投稿作成者

            ふろーとさん
            動作確認せずにアップロードしてしまったので色々とミスがありました・・・。失礼しました・・・。
            先程更新しましたのでご確認ください。
            綺麗な10kHzの波が出力されましたよ(^^)/

  4. てらむら

    こんにちは。ふろーとさんと同じ悩みを持つ者です。掲載されているスケッチを試してみましたが、IOに出力すると490Hz(デフォルト)のままです。根本的に出力の仕方が間違っているのでしょうか?ご教授頂きたく、よろしくお願い申し上げます。

    返信
    1. S2KTS. 投稿作成者

      てらむらさん
      コメントいただきありがとうございます!

      10kHzなど、比較的高周波で動作させる場合は、分周比 に注目してください。
      440Hzのスケッチでは 分周比は 8 に設定しています。(ヒトの可聴域には分周比8がちょうどいいみたいです。)
      これを 1 にすることでより高い周波数を得られるかと思います。

      ・・・ただ、デフォルトのまま、ということは分周比以前に、何かが間違っているような気がします。
      analogWrite() 関数は使いませんので、ご注意ください。

      返信
      1. てらむら

        こんにちは。早急なご回答有難うございました。
        analogWrite()関数以外での出力を知りませんでした。皆さまはどの様に出力されているのでしょう?始めたばかりの初心者で恐れ入ります。
        御教授頂けませんでしょうか。宜しく御願い致します。

        返信
        1. S2KTS. 投稿作成者

          てらむらさん

          Arduinoは(一部の例外を除き)AVRマイコンを使用しています。
          AVRマイコンでは使われない、analogWrite() 関数などの、Arduino独自の関数の多くは、内部的にはAVRマイコンのコマンドを実行させています。
          PWM出力などの、少し複雑なものは、本来ならばこのページのように、レジスタの登録などのめんどくさいことをしないといけません。
          Arduinoがこれを簡単にしてくれてるわけですね(^^)/

          このページでやっていることは、正にその逆で、Arduinoの基となるAVRマイコンの部分をいじって周波数をコントロールしています。
          analogWrite() 関数が行っていることも含めて実行しているので、逆に同関数を使ってしまうと意図しない動作をしてしまいます。
          (analogWrite() 関数 を同時に2回呼び出してしまっているのと同じことになります。)

          従って、PWMを異なる周波数で出力したい場合は、analogWrite() 関数 は使ってはいけない、ということになります。

          返信
          1. てらむら

            度々ありがとうございます。S2KTS.さんがGitHubにあげられているコードを動かしてみましたが、周波数カウンタ及び電圧チェックで出力されていませんでした。
            根本的なところを間違えているような気がするのですが何かアドバイス頂けないでしょうか。
            ボードはArduinoMEGAを使用しています。

          2. S2KTS. 投稿作成者

            てらむらさん

            残念ながら、こちらの記事及びサンプルスケッチはArduino Uno向けのものです。
            搭載されているマイコン自体が違いますので、恐らくArduino Megaでは動かないかと思われます。。。

            該当情報については、こちらのフォーラムや、データシートなどをご参照ください。

            Atmel社のマイコンなので、細かい違いはあれど、コントロール方法自体は同じかと思います。

  5. ふろーと

    お忙しいところ何度も何度もお返事ありがとうございます。
    教えていただいたプログラムで、無事に動くことが確認できました。
    何度も何度もすみませんでした。
    ありがとうございました。

    返信
    1. S2KTS. 投稿作成者

      ふろーとさん
      こちらこそ色々とすみませんでした…
      お役に立てたようであれば嬉しいです!

      返信
  6. てらむら

    教えて頂いたリンク先を基に31kHzまで周波数UPできました。
    これから任意の値に変更できるよう挑戦します。
    重ね重ねありがとうございました。

    返信
    1. S2KTS. 投稿作成者

      てらむらさん
      お役に立てたようであれば嬉しく思います!
      楽しいArduinoライフを~!

      返信
  7. ピンバック: PWM出力を使ってパルス波の周波数, Duty比を変更する [Arduino UNO] | Tanko Blog

  8. ピンバック: 【電子工作】PWM制御の周波数を変えてパッシブブザーで電子オルゴールをつくってみる(Arduino Uno R3で遊ぼう日記 その4) - 映画と旅行とエンジニア

  9. ピンバック: ArduinoのPWMの最高周波数は?1MHzを出せるか?

  10. shunn

    arduinoを始めたばかりの学生です。記事を拝見させていただき,とてもためになりました.
    ふろーとさんと似たような悩みでこの記事を元にarduinoで4つのドライバ回路を駆動させたいと考えています.
    PWMの周波数は10kHzで考えています.
    Arduino Uno ではデジタル出力の 3, 5, 6, 9, 10, 11 の 6 個PWMに利用できるので4つに拡張することも可能であると考えたのですが当方の力不足で詰まってしまいました.
    お忙しいと存じますが,御教授頂けませんでしょうか.宜しく御願い致します.

    返信

コメントを残す