公開日:2019/05/04
title.png

5th Season


#5-6 正弦波を生成する

 今回は、PWMを使って正弦波を生成してみようと思います。
 PWMについては、これまでに何度も使ってきていますので、あまり細かい説明は省略します。簡単に言うと、一定期間内のHIGH幅とLOW幅の比(デューティ)を変化させることによって、平均的な電圧値をアナログ的に制御する手法です。
 「#5-4 デジタルオシロスコープ(STM32対応)」では、LPF(Low Pass Filter、CR回路)を通すことによって、PWM波形がアナログ的な平均電圧に変換される様子を観測しました。この技術を応用すると、正弦波だってPWMで作り出せるはずです。

目標波形

 今回生成するのは、以下数式で表される正弦波です。

  y=(sin (2π×x/T)+1)/2

 sin()関数の値の範囲は−1〜1ですが、上の式によって、0〜1の範囲にシフトしています。Tは正弦波の周期です。

 もう簡単ですね。現在時間を計測してxを決定したら、yが求まりますので、そこからデューティを計算して、analogWrite()するだけ。

  void loop (void)
  {
    uint32 x;
    float y;

    x=millis ();
    y=(sin (2.*M_PI*x/T) + 1.)/2.;
    analogWrite (pin_SIN, int (y*255.));
  }


 以上終了。はやっ!

カウンタの動作

 良いのですよ。もうテーマは達成できていますから。でも、お時間があるなら、もう少しお付き合いください。

 上のプログラムは、万一、loop()関数内の他の処理が複雑かつ時間がかかるようになってくると、analogWrite()の実行間隔が空いてしまって、正弦波形が崩れてしまうかもしれません。逆に、loop()関数内の処理が少なくて、短時間で繰り返されたとしても、実は、analogWrite()の時間解像度がそんなに高くないので、延々と無駄な計算をしていることになります。
 もう少し、analogWrite()によるPWM出力の原理を知って使った方が、「賢そう」に見えると思いませんか?

 これまでに何度か登場していますが、analogWrite()で出力されるPWMの周波数は、約490Hzです。正確には、fcpu/(2×TOP×64)≒490.20Hz(周期で2,040μs)となります。……TOP=255。64はカウントクロックの分周比。

※Arduino UNO等が出力するPWMにはもう一つ、980Hzの物がありますが、こちらはシステムタイマ(TC0)を使ったPWM出力となります。システムタイマはmillis()やdelay()などの関数に使われていて、アプリケーションで変更して使うのは危険なので、今回は対象外として除外します。

 以降、タイマをTC1に限定して話を進めます。その方が具体的に例示しながら議論を進められるからです。ただ、以下の説明はタイマを使ったPWM出力の基本的な話なので、TC2等、他のタイマを使った場合も、ほぼ、同じ議論が成立します。

 Arduino UNOのシステムが起動すると、TC1は以下の様な設定になって、既にカウントを開始しています。

レジスタ名 アドレス 設定値 説明
TCCR1A 0x0080 0x01 Normal port
PWM, Phase Correct, 8-bit
TCCR1B 0x0081 0x03 PWM, Phase Correct, 8-bit
clkio/64
TIMSK1 0x006f 0x00 TC1から割り込みを要求しない

 「PWM, Phase Collect, 8-bit」モードでは、カウンタの上限は255(8ビット)となっており、0から255までカウントアップすると、今度はカウントダウンに切り替わり、0まで到達すると、またカウントアップ……、を繰り返します。カウントクロックはclkio/64で、clkioは16MHzですので、周期は2×255/(clkio/64)=2,040μsとなるわけです。

 ここで、TC1を使ったPWM出力端子D9(またはD10)に対して、analogWrite()を実行します。

  analogWrite (pin_D9, 64);

 すると、TCCR1Aが0x01→0x81に変化して、D9に引き出されているOC1Aと言う信号線が、「Clear OC1A on Compare Match when upcounting. Set OC1A on Compare Match when downcounting.」という動作モードに設定されます。
 肝心なコンペアマッチレジスタOCR1Aには、analogWrite()の第二引数(上例では64)が設定されます。

ar56_ocr1a.png

 結果、上図の様にデューティ64/255のPWMが出力されます。
 つまり、OCR1Aに設定する値によって、出力されるPWMのデューティが変化します。では、もし、上図の赤破線位置でOCR1Aレジスタに新たな値を書き込んでしまったら、どうなるでしょうか?すでに、新しい値64のカウントは過ぎてしまっているので、この周期のLOWパルスが消失してしまい、予期しないパルス(glitch、グリッチ)を発生させてしまうことになります。
 ATmegaのタイマは、このようなグリッチを発生させないように、OCR1Aレジスタを「ダブルバッファ」形式に実装してあります。MCU側からアクセスできるレジスタと、実際にタイマカウンタ(TCNT1)と一致比較するレジスタが二重になっていて、MCUから新しい値を書き込んでも、カウンタがオーバフロウするまで、一致比較側のレジスタが更新されないようになっているのです。

※ATmegaに限らず、PWMなどの波形出力に対応したタイマユニットでは、大抵、ダブルバッファ形式になっていますので、データシートなどで確認して使ってください。

 アップ⇔ダウンカウントを繰り返すこのモードで、オーバフロウとは、ダウンカウントでカウンタが0になった時を示しており、上図の緑破線位置で一致比較側のレジスタが新しい値に更新されます。

 ひっくり返すと、どんなに短い間隔で頻繁にOCR1Aを変更したとしても、結局PWM出力に反映されるのは、約490Hzの1周期(2,040μs)毎、という訳です。

割り込みを使う

 だったら、初めから、タイマのオーバフロウ割り込みを受けて、割り込み処理ルーチン(ISR)内で、OCR1Aレジスタを変更するのが効率的です。ついでに、一定時間(2,040μs)毎に割り込みがかかるので、時間も計測してしまいましょう。

  ISR (TIMER1_OVF_vect)
  {
    // TOI1 interrupt service routine
    int dty;
    float sin_wav;

    // elapsed time +2040[us]
    usec_sin += 2040;

    // sin wave: 0~2
    sin_wav=sin ((2.*M_PI/1000.)*usec_sin/msec_peri) + 1.;
    // sin wave: sin_ampl/100~sin_amph/100
    sin_wav=(sin_wav*sin_amph + (2. - sin_wav)*sin_ampl)*(1./200.);

    // set next duty
    dty=int (sin_wav*256.);
    dty=(dty<0)? 0: (dty>255)? 255: dty;
    OCR1A=dty;
  }


※気になる算数が目に付くかもしれませんが、少しでも処理を高速化するためのアシストです。例えば、200で割るより、1/200を掛け算する方が処理としては高速になります(ATmega328には乗算命令があるので、除算より速い)。

 オーバフロウ割り込みがかかった時には、既に、一致比較側レジスタが更新された後ですので、ISR内で新たに設定したデューティは、次の割り込みがかかる直前に更新されることになり、カウンタ1周期分遅れて更新されることを頭に入れておいてください。もし、タイムラグが許されない用途では、予め1周期先の値を計算して設定する必要があります。

 setup()関数にて、TC1のオーバフロウ割り込みを有効にすることも忘れてはなりません。

  void setup (void)
  {
    // TC1 setting
    TCCR1A |= 0x80;  // OC1A output
    TIFR1=0x01;  // TOV1 clear
    TIMSK1=0x01;  // TOIE1 set
  }

実験準備完了

 全ての準備が整いました。実験用プログラムをリンクしておきます。

  実験用プログラム:test_sin_wave.ino

 使用したタイマ(TC1)は、ATmega32U4にも存在するので、UNO、Pro Mini等ATmega328機に加え、Leonardo、Micro等、ATmega32U4機でも動くと思います(筆者は未確認)。
 筆者はUNOを使い、上のプログラムを転送しました。いつものようにシリアルモニタからコマンドを受け付けるようになっているのですが、実行開始した時点で、既にPWMがD9端子から出力されています。D9端子に、適切な電流制限抵抗をつけたLEDを接続するか、UNOであれば、D9端子とD13端子(オンボードLED)をジャンパ線で接続してみてください。
 デフォルトでは、周期2sの正弦波PWMが出力されており、(オンボード)LEDが正弦波で明滅するはずです。例えるなら、「蛍の光」の様に。

 以下、シリアルモニタで受け付けるコマンドの仕様です。

 引き続き、実際のPWM波形を確認して行きましょう。
 Logiscopeを使うので、もう1機Arduinoを起動します。短いパルスを観測する必要があるので、筆者は高速なSTM32ボードを使いました。STM32でLogiscopeを起動して、UNOと以下の様に接続します。

UNO 接続 STM32
GND G
D9 PA8 (5Vトレラント端子)

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【sin,40】
  OK: sin wave pwm: 40[ms], 0~100[%]

  実験サンプル:【 】内はLogiscopeからの送信データ
  【trg,8,r,50000】 …… UNOのD9端子と接続した、STM32のPA8端子をキャプチャする

ar56_sinpwm.png

 PWMのデューティが1周期毎に変化しながら、デューティの大きい部分と小さい部分が存在していることが読み取れます。デューティの大きい(HIGH幅の広い)波形が密集している辺りが、正弦波の山の部分、デューティの小さい(HIGH幅の狭い)波形が密集している辺りが、正弦波の谷の部分に相当します。
 正弦波の1周期が40msで、PWMの1周期が約2msなので、正弦波1周期中にPWMが約20周期程入っている計算です。

 まあまあ、わかりますよ、言いたいことは(1^^;)っ。

  「どこが正弦波なの?」

 真顔で指摘されてしまうと、返ってちょっと不安ですが、LPFをかけてみましょう。以下図の様な回路をブレッドボード上に構成してください。

ar54_schem.png

 「#5-4 デジタルオシロスコープ(STM32対応)」で使ったSTM32用なので、端子名が異なっていますが、上図PA6にUNOのD9端子を接続してください。PA0は、STM32のPA0に接続するのですが、その前に、必ず、以下のコマンドをUNO側で実行しておいてください。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【sin,50,60】
  OK: sin wave pwm: 50[ms], 0~60[%]

 STM32のアナログ入力は3.3Vフルスケールです。5Vトレラントにはなっていないので、UNOの5V信号をそのまま接続すると、絶対最大定格違反となります。上のコマンドで、正弦波の振幅値を60%に抑えることによって、Vcc×60%=3V振幅になるように設定しています。

※うっかりUNOをリセットしてしまうと、デフォルト出力に戻って、5V電圧がSTM32のアナログ端子にかかってしまいます。STM32を壊してしまいかねないので、細心の注意を払って実験してください。自信がないなら、5V系のUNOやMicroなどでLogiscopeを起動しましょう。

  実験サンプル:【 】内はLogiscopeからの送信データ
  【osc,0,0,7,0】 …… LPF出力と接続した、STM32のPA0端子をキャプチャする

ar56_sin.png

※デジタルPWM波形(上)と、アナログ正弦波波形(下)は、同時には取得できない(Logiscopeの制約)ため、上図は、うまく位相が合うようなタイミングでキャプチャして合成したイメージ図です(だから、ちょっとずれているかも?)。

 何が「不安」だったかと言うと、上のギザギザの波形を見て、もう一度「どこが正弦波なの?」と、聞かれるのではないかと……。広い心で見ていただければ、正弦波ですよね?ギザギザは、CR充放電電圧波形なので、ある程度は残ってしまいます。もっと正弦波の周期を伸ばして、時定数CRを大きく取って行くと、多少はマシになるかもしれませんが、他の問題も……。
 上の図もそうですが、正弦波の谷部分が0Vまで落ち切っておらず、少し浮いています。フルスケール側も、計算上は3Vまで到達するはずなのですが、こちらも3Vに満たない。これは、時定数CR(上の回路図だと1kΩ×10μF=10ms)が大きすぎて電位変化が間に合わず、振幅が狭まってしまっているものと考えることができます。
 トレードオフですね。ギザギザを減らす為に時定数を大きくとると、振幅が狭まってしまう。

※時定数CRを調整する以外にも、PWMの周期を短く(周波数を高く)する方法もあります。ただし、ATmegaにとっては、float計算を実行しているISR内の処理が重たいので、周波数を上げようにも、すぐに上限にぶち当たります。

 と、言うより、時定数CR(10ms)に対して、正弦波の1周期50msが短すぎるのかな?もう少し長い周期の正弦波でも試してみましょう。

  実験サンプル:【 】内はシリアルモニタからの送信データ
  【sin,250,60】
  OK: sin wave pwm: 250[ms], 0~60[%]

  実験サンプル:【 】内はLogiscopeからの送信データ
  【osc,0,0,9,0】 …… LPF出力と接続した、STM32のPA0端子をキャプチャする

ar56_sin2.png

 ギザギザは消えないので、線幅が太く見えてしまっていますが、今度は、きちんと0〜3[V]の振幅で振れています。


 今回は、例題として正弦波を生成してみましたが、ISR内の波形生成式を変更すれば、正弦波以外のアナログ波形も作り出すことができる原理を学んだことになります。だって、5VのArduino UNOを使って、3Vフルスケールの正弦波を生成できたのですよ!


btn_arduino.png

btn_prev.png btn_next.png

home