直近の記事ではお題目をArduinoとしつつ、ATmega328p自体を組込開発している趣旨の話ばかり取り上げておりました。
今回はArduino IDEからプログラミングできる範囲でArduinoを使って、より柔軟なPWM波を得る場合の方法を少し考えます。
なお、ATmega328pからの低レベルなPWMの出力方法は以下の記事でまとめております。

合同会社タコスキングダム|蛸壺の技術ブログ
【Arduino工作〜発展編】AVR-RustでAtmega328pからPWM波形を出力したい!

AVR-Rustライブラリを使ってAtmega328pの機能からPWM波形を出力するための方法を紹介します。


ArduinoのデフォルトのPWM機能について




まず最初にArduino標準のPWM機能に関して、基本的なことを確認しましょう。

こちらのPDF資料 に詳しくまとめられております。
Arduino Unoも当然ながらAtmega328p内部のタイマー0/1/2から各A/Bチャンネルの計6つのPWM波形を出力できます。


タイマー PWM波形モード 周波数 チャンネル 出力ポート
0 8bit高速PWM 977Hz A D6
0 8bit高速PWM 977Hz B D5
1 8bit位相標準PWM 490Hz A D9
1 8bit位相標準PWM 490Hz B D10
2 8bit位相標準PWM 490Hz A D11
2 8bit位相標準PWM 490Hz B D3


見てのように、タイマー0で
977Hz 、タイマー1/2で 490Hz 固定で、内部クロックの16MHzに対して非常に遅い出力クロックになっています。
主に遅さの原因になっているのは、分周比がデフォルトで64に設定されていることです。
また8bitの分解能で動作しているので、PWM波形のTOP値が255で固定されています。
このことから、タイマー0の高速PWMの実効周波数
f0f_0 は、

f0=16 [MHz]64×(TOP+1)977 f_0 = \frac{16\ \mathrm{[MHz]}}{ 64 \times (\mathrm{TOP} + 1) } \simeq 977 Eq. (1)



タイマー1/2の8bit位相標準PWMの実効周波数
f1,2f_{1,2} は、

f1,2=16 [MHz]64×2×TOP490 f_{1,2} = \frac{16\ \mathrm{[MHz]}}{ 64 \times 2 \times \mathrm{TOP} } \simeq 490 Eq. (2)



というような換算式で表されます。

このようなArduinoデフォルトのPWM波形かそのまま使えるかというとケースバイケースです。
目的によっては、そのままだと“使えない”PWMになる可能性があるので、Arudinoでも目的に応じたPWMが柔軟に作りだせるようにカスタマイズする必要があります。


組み込み的手法でハードウェアPWM波形を作る



実際、Arduino IDEもC言語のラッパーですので、Arduinoにより低レベルな組込みプログラミングを行うことが可能です。
Arduinoでも周波数やデューティ比を柔軟に指定できるPWM波形を作ろうとすると、PWM波形モード、プリスケーラ値、TOP値を計算して、TCCR*A/Bレジスタを適切に書き換えるようにします。
ただしPWM波形モードも変更して、例えば高速PWM・TOP(ICR1)モードへ移行すると、タイマーのAチャンネルのポートは比較の基準信号に使われるため、PWM波形としては機能しません。
ですので、PWM波形が出力できるのが、主にD3・D5・D10の3つのピンに絞られてしまうことに注意しましょう。
以下は、440Hzでデューティ比50%のPWM波形(位相標準PWM・TOP(=OCR1A)モード)を、D10(OCR1B)から出力する場合のソースコード例です。


            #include <avr/io.h>

//👇D10(タイマー1のチャンネルB)を指定
#define PWMOUT 10

unsigned int frq = 440; //周波数[Hz]
float duty = 0.5; //デューティ比50%

void setup() {
    pinMode(PWMOUT, OUTPUT);
    //👇TCCR*A/Bレジスタを直接弄る
    TCCR1A = 0b00100001;//👈D10ピンのPWM(ノン・インバートモード)
    TCCR1B = 0b00010010;//👈分周比8に書き換え
    //👇TOP値を変更
    OCR1A = (unsigned int)(1000000/frq);
    //👇Duty比の設定
    OCR1B = (unsigned int)(1000000/frq*duty);
}

void loop() {}

        


このように数百Hz程度の低周波音帯のPWM波形なら安定して発生できるものの、興味深いところで、PWMの周波数をどこまで高くできるかというところも気になります。
Arduinoのシステムクロックは16MHzですので、16bitのタイマー1を分周比1、高速PWMモードでTOP値2というもっとも高そうな設定にすると、理論的には8MHzのデューティ比50%固定の波形が得られそうなものです。
実際には、ピンの電圧を切り替えるのに最低限必要な数サイクルの時間が掛かるので、その切り替え時間よりも短い周期は指定することは出来ませんし、かなり近しい時間を指定してもPWMとは言えないほど歪んだ波形が出力されてしまいます。
このような理由もあって、ArduinoのハードウェアPWMでは~70kHz程度が実用上の上限周波数とされています。
つまり、もっと速いPWM周波数がほしいなら、ベースクロックのもっと速いMPUマイコンの機器が必要になります。

動作途中でのPWMの止め方



PWMは一旦設定したら即時波形が流れ続けます。
PWMの出力を動作途中で止めさせるためには、

            TCCR1B = 0b00000000;

        

とすると良いそうです。


delayMicroseconds関数でソフトウェアPWM波形を作る



もう一つのやり方は、ソフトウェアPWMを使うことです。
ソフトウェアPWMですので、どのデジタル出力ピンからでもPWMを擬似的に発生することができるのですが、周波数指定できるのはunsign int型の16bit整数で指定可能な65536までが周波数もしくは待機時間の設定上限値になります。
ソフトウェアPWMは、loop関数の中で待機関数を使うことで実現します。
通常、引数の中に待機時間としてミリ秒を指定する
delay 関数は良く目にしますが、更に細かい待機時間を利用したい場合には、マイクロ秒を指定する delayMicroseconds 関数もビルドイン関数として利用できます。
なお、delayMicrosecondsの最低設定時間は3μsとされているので、それなりに高い周波数のPWMを手軽に発生することが可能です。
以下はデジタルピン5番から周波数2.5kHz(=周期400μs)狙いのデューティ比50%のソフトウェアPWMを出力する例です。

            #include <Arduino.h>

//👇待ち時間200μs
unsigned int waitingTerm = 200;

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

void loop() {
    digitalWrite(5, HIGH);
    //PORTD |= _BV(5);※後述

    delayMicroseconds(waitingTerm);

    digitalWrite(5, LOW);
    //PORTD &= ~_BV(5);※後述

    delayMicroseconds(waitingTerm);
}

        


問題点として、ソフトウェアPWMでは待機間隔を制御してOn/Offしているだけで、電圧のスイッチングに掛かる時間はおおよそどんぶり勘定ですので、正確な周波数を持つPWMを得たい場合には不利なやり方です。
周波数(=周期)をきっちりと欲しい場合には、前述の「ハードウェアPWM」のやり方を検討すべきです。

余談〜高い周波数ではdigitalWriteを使わない



digitalWriteの内部処理は演算負荷が比較的高く、44サイクルで処理されるようです。
そこで、以下のようにビット演算で直接ポート操作すると3サイクルまで抑えることができます。

            PORTB |= _BV(2); //digitalWrite(10, HIGH);と同じ
PORTB |= _BV(5); //digitalWrite(5, HIGH);と同じ
PORTD |= _BV(5); //digitalWrite(13, HIGH);と同じ

        

これで各ポートの出力はHIGHになります。

_BV関数 を使わずにHIGHにしたい場合には予約変数を利用して、

            PORTB |= (1 << PB2); //digitalWrite(10, HIGH);と同じ
PORTB |= (1 << PB5); //digitalWrite(5, HIGH);と同じ
PORTD |= (1 << PD5); //digitalWrite(13, HIGH);と同じ

        

を使うことも良く目にします。
出力をLOWにしたい時は、

            PORTB &= ~_BV(2); //digitalWrite(10, LOW);と同じ
PORTB &= ~_BV(5); //digitalWrite(5, LOW);と同じ
PORTD &= ~_BV(5); //digitalWrite(13, LOW);と同じ

        

とします。
ポートのグループが一緒であれば、ビット演算でまとめて処理が可能です。

            //まとめてHIGH
PORTB |= _BV(2) | _BV(5);
PORTB |= (1 << PB2) | (1 << PB5);

//まとめてLOW
PORTB &= ~(_BV(2) | _BV(5));
PORTB &= ~((1 << PB2) | (1 << PB5));

        


他にも
_BV関数 を使わずに直接生バイトを与えるポート操作もあります。


            PORTB |= 0b00010100; //まとめてHIGH
PORTB &= ~0b00010100; //まとめてLOW
PORTB = 0b00010100; //まとめてHIGH・LOW切り替え

        

_BV関数を使わないことで、簡潔なポート操作が出来ますが、可読性が低くなり0や1の位置にミスも多くなるかもしれません。


参考サイト