メインページ/Electronics/AVR/pwm sin
提供:MwWiki
目次[非表示] |
[編集] AVR による正弦波PWM出力の試み
以前、CQ出版社主催の、PICを使ったパワーエレクトロニクスの制御のセミナーを受けました。DC-DCコンバーターや、モーター制御などを行いましたが、dcPICという、私には馴染みのない石を使っていましたので、これをAVRで実現したいと思います。目標は三相交流モーターのPWM制御です。
[編集] 方法
正弦波出力ですので、デューティ比を1パルスごとに変えていかなければなりません。と、最初思っていましたが、きっちり1パルスごとである必要はないことに気が付きました。すなわち、同じデューティ比が2,3個続いても支障はなく、それは、搬送波周波数と生成正弦波周波数のかねあいで、決まるだけのことです。
ぐぐっていると、PIC で正弦波出力の実験をされている方がいました。この方は搬送波 4[kHz]、生成波 50[Hz] ということでしたので、試しに同じような出力が作れるかを試してみます。AVRは高速PWMモードを使用し、OCR0Bの値でデューティ比を 替えてやります。以下のプログラムでテストしました。
/*ATTINY2313 正弦波PWM出力の試み 高速PWMモード使用 */ #include <avr/io.h> int main(void) { DDRD |=_BV(PD5); PORTD |= _BV(PD5); //OC0B PWM出力 DDRA |=_BV(PA0); //PORT A0出力 OCR0A = 255; //=TOP値 OCR0B = 100; //Duty = OCR0B/OCR0A TCCR0A = 3; TCCR0B |= _BV(WGM02);//Mode7 波形生成モード TCCR0A |= _BV(COM0B1); //非反転動作、OC0Bピンへ出力 TCCR0B |= _BV(CS00); //1分周 //f = 1MHz/(1x(1+255) = 3906Hz while(1) { OCR0B++; if (OCR0B>200) OCR0B=100; PORTA |=_BV(PA0); PORTA =0; } }
タイマーを設定して PWM 出力を開始後、while ループ中でデューティ比を設定しています。デューティ比はとりあえず100/255 ~ 200/255 の間で変えるようにしました。ところで、タイマーで設定周期のパルスが出力されている間、while ループはどのくらいのスピードで実行されるのでしょうか。タイマー出力されている1パルスの間に最低1度は whileループの命令にも実行機会が来ないと、設定値が飛び飛びの値にしかならないので、どのくらい余裕があるのかみてみるため、while ループ中で PA0 ポートのON/OFFをしてみます。
実行結果
青が PWM 出力、赤が PA0 出力です。青1周期の間に赤は何度も実行されているので、余裕があります。
[編集] 正弦波を出力する
正弦波を出力するには、適当にデューティ比を設定したのでは、正しいカーブにならないので、sin 関数で得られる値が必要です。でも AVR で実行中に重い sin の計算などしてられないので、こういうマイコンではテーブルを参照します。home_iwamoto さんのページのプログラムを拝借されていただき、AVR用に正弦波出力プログラムを書きました。
/*ATTINY2313 */ #define F_CPU 1000000UL //1MHz #include <avr/io.h> #include <util/delay.h> const uint8_t sinValue[] = { 125,135,145,154,164,173,182,190,198,206, 213,220,226,232,236,240,244,247,248,250, 250,250,248,247,244,240,236,232,226,220, 213,206,198,190,182,173,164,154,145,135, 125,115,105, 96, 86, 77, 68, 60, 52, 44, 37, 30, 24, 18, 14, 10, 6, 3, 2, 0, 0, 0, 2, 3, 6, 10, 14, 18, 24, 30, 37, 44, 52, 60, 68, 77, 86, 96,105,115 }; int main(void) { uint8_t countDiv=0; DDRD |=_BV(PD5); PORTD |= _BV(PD5); //OC0B PWM出力 DDRA |=_BV(PA0); //PORT A0出力 OCR0A = 255; //=TOP値 TCCR0A = 3; TCCR0B |= _BV(WGM02);//Mode7 波形生成モード TCCR0A |= _BV(COM0B1); //非反転動作、OC0Bピンへ出力 TCCR0B |= _BV(CS00); //1分周 //f = 1MHz/(1x(1+255) = 3906Hz while(1) { OCR0B = sinValue[countDiv++]; // 4.5°毎の正弦波高を更新 if(countDiv >= 80) countDiv = 0; // 360°/4.5°= 80で繰り返す _delay_us(250); //50Hz = 20mSec. 1回に250us の待ち時間 // PORTA |=_BV(PA0);PORTA =0; //debug } return 0; }
while ループにディレイを入れて、50Hz になるように調整しています。
[編集] 出力
PD5 ポートにRCフィルタを付けて、搬送波を除去した信号です。フィルタも home_iwamotoさんのページと同じものです。
カットオフ周波数は f=1/2πRC=1/(2π*10^-3)=159[Hz] です。
きれいな正弦波が得られました。
フィルタの手前の信号と両方見ると、教科書に乗っているような波形を観測できます。
[編集] 細かくする
正弦波は出力できましたが、よく見ると波形はギザギザです。
while ループ実行はまだ余裕があるので、sin 波データを増やして粒度を細かくしてギザギザを減らしてみます。4.5度きざみだったデータを 2度きざみにします。データ生成は Java でプログラムを作りました。
public void genSinValues() throws Exception { int pos=0; double deg=0; while (deg<360) { long value=Math.round(Math.sin(Math.toRadians(deg))*127+127); //System.out.print((pos++)+"\t"); //System.out.print(deg+"\t"); System.out.print(value+","); //deg+=4.5; deg+=2.0; } }
180 個のデータを生成しました。が、これを ATTINY2313用にコンパイルすると、メモリ不足になってしまいました。EEPROMエリアを使う手もあるようですが、128バイトしか無いので足りません。
ATMega88 に変更
というわけで、マイコンをメモリ容量の多いMEGA88に変更しました。以下が新しいプログラムです。
#define F_CPU 1000000UL //1MHz #include <avr/io.h> #include <util/delay.h> const uint8_t sinValue[] = { 127,131,136,140,145,149,153,158,162,166, 170,175,179,183,187,191,194,198,202,205, 209,212,215,218,221,224,227,230,232,235, 237,239,241,243,245,246,248,249,250,251, 252,253,253,254,254,254,254,254,253,253, 252,251,250,249,248,246,245,243,241,239, 237,235,232,230,227,224,221,218,215,212, 209,205,202,198,194,191,187,183,179,175, 170,166,162,158,153,149,145,140,136,131, 127,123,118,114,109,105,101,96,92,88, 84,79,75,71,67,63,60,56,52,49, 45,42,39,36,33,30,27,24,22,19, 17,15,13,11,9,8,6,5,4,3, 2,1,1,0,0,0,0,0,1,1, 2,3,4,5,6,8,9,11,13,15, 17,19,22,24,27,30,33,36,39,42, 45,49,52,56,60,63,67,71,75,79, 84,88,92,96,101,105,109,114,118,123 }; int main(void) { uint8_t countDiv=0; DDRD |=_BV(PD5); PORTD |= _BV(PD5); //OC0B PWM出力 OCR0A = 255; //=TOP値 TCCR0A = 3; TCCR0B |= _BV(WGM02);//Mode7 波形生成モード TCCR0A |= _BV(COM0B1); //非反転動作、OC0Bピンへ出力 TCCR0B |= _BV(CS00); //1分周 //f = 1MHz/(1x(1+255) = 3906Hz while(1) { OCR0B = sinValue[countDiv++]; // 2°毎の正弦波高を更新 if(countDiv >= 180) countDiv = 0; // 360°/2°= 180で繰り返す _delay_us(111); } return 0; }
これで出力したPWMは以下の波形です。
うーむ、あまり改善されていないです。搬送波の周波数は決まっているので、このぎざぎざを滑らかにするにはsin データのきざみを細かくしても意味があまりないことに気が付きました。
まあ、実害は特にないので、このルーチンをベースに続けていくことにします。
[編集] 出力を増やす
[編集] 2相交流
単相PWM波の生成はできましたが、目指しているのは3相交流ですので、出力を増やさねばなりません。このマイコンでは同じ生成器からもう一個の出力を得られる仕様なので、追加してみます。
なお、今までのプログラムではPWMモード設定は高速PWM動作になっていました。(CTC モードを使っていると勘違いしていました。)
2本のPWM出力を使うには 8bit高速PWM動作を指定する必要があります。 (「高速PWM」と「8ビット高速PWM」は異なることに注意)
- CTC - OCR0A に任意の TOP 値を指定して、発振周波数を任意に設定できる。但し出力は1個。
- 8bit高速PWMモード - TOP値255 固定になるため、分周値で決まった周波数にしか出力できない。しかし、出力を2個得られる(同一周波数、異デューティ比)
/*ATMEGA88 */ #define F_CPU 1000000UL //1MHz #include <avr/io.h> #include <util/delay.h> const uint8_t sinValue[] = { 127,131,136,140,145,149,153,158,162,166, 170,175,179,183,187,191,194,198,202,205, 209,212,215,218,221,224,227,230,232,235, 237,239,241,243,245,246,248,249,250,251, 252,253,253,254,254,254,254,254,253,253, 252,251,250,249,248,246,245,243,241,239, 237,235,232,230,227,224,221,218,215,212, 209,205,202,198,194,191,187,183,179,175, 170,166,162,158,153,149,145,140,136,131, 127,123,118,114,109,105,101,96,92,88, 84,79,75,71,67,63,60,56,52,49, 45,42,39,36,33,30,27,24,22,19, 17,15,13,11,9,8,6,5,4,3, 2,1,1,0,0,0,0,0,1,1, 2,3,4,5,6,8,9,11,13,15, 17,19,22,24,27,30,33,36,39,42, 45,49,52,56,60,63,67,71,75,79, 84,88,92,96,101,105,109,114,118,123, //120度分の進みデータも入れておく 127,131,136,140,145,149,153,158,162,166, 170,175,179,183,187,191,194,198,202,205, 209,212,215,218,221,224,227,230,232,235, 237,239,241,243,245,246,248,249,250,251, 252,253,253,254,254,254,254,254,253,253, 252,251,250,249,248,246,245,243,241,239 }; int main(void) { uint8_t countDiv=0; DDRD |=_BV(PD5)|_BV(PD6); PORTD |= _BV(PD5)|_BV(PD6); //OC0B PWM出力 TCCR0A |= _BV(WGM01)|_BV(WGM00); //8ビット高速PWMモード TCCR0A |= _BV(COM0A1)|_BV(COM0B1); //非反転動作、OC0Bピンへ出力 TCCR0B |= _BV(CS00); //1分周 //f = 1MHz/(1x(1+255) = 3906Hz while(1) { OCR0A = sinValue[countDiv+30]; // 60°進み位相 OCR0B = sinValue[countDiv++]; // 2°毎の正弦波高を更新 if(countDiv >= 180) countDiv = 0; // 360°/2°= 180で繰り返す _delay_us(111); } return 0; }
出力は PD5 と PD6 から得られます。無事に60度ずれた2相交流が得られました。
[編集] 8ビット タイマ/カウンタ2 (PWM, 非同期動作付き)
さて、あと1個、PWM出力が必要です。8bitタイマーが3個以上付いているAVRはないものかと、ATMELのサイトで一覧などを見ていたのですが、どれも2個しかないようです。が、Mega88のデータシートをよく見ると、「8ビット タイマ/カウンタ2 (PWM, 非同期動作付き)」という項目があり、ほとんど 「8ビット タイマ/カウンタ0 (PWM)」と同じ内容が書いてあります。違いは、CPUクロックとは別の外部クロックで動作することができる、とあり、デフォルトは内部クロックで動くので、タイマ0と同じに使えそうです。とりあえず、動作テストです。
タイマ2を使ったプログラム
int main(void) { uint8_t countDiv=0; DDRD |=_BV(PD3); DDRB |=_BV(PB3); PORTD |=_BV(PD3); //PD3-OC2B PWM出力 PORTB |=_BV(PB3); //PB3-OC2A PWM出力 TCCR2A |= _BV(WGM21)|_BV(WGM20); //8ビット高速PWMモード TCCR2A |= _BV(COM2A1)|_BV(COM2B1); //非反転動作、OC0Bピンへ出力 TCCR2B |= _BV(CS20); //1分周 //f = 1MHz/(1x(1+255) = 3906Hz while(1) { OCR2A = sinValue[countDiv+30]; // 2°毎の正弦波高を更新 OCR2B = sinValue[countDiv++]; // 2°毎の正弦波高を更新 if(countDiv >= 180) countDiv = 0; // 360°/2°= 180で繰り返す _delay_us(111); } return 0; }
これはタイマ0のプログラムを単にタイマ2を使うようにレジスタ名を変更しただけです。ただし、出力ピンが OC2A, OC2B になります。出力ピンが独立しているので、8ビットタイマーでは4個のPWM出力を得ることができるということになります。
[編集] 搬送波周波数と正弦波出力周波数
3相の独立した周波数を得ることができました。これからは、FETを使って電力を扱う回路を作る必要がありますが、パーツが無いので、その前に、搬送波の周波数とそれに載せる正弦波との関係をちょっと考えてみます。
ところで、この[8ビット高速PWMモード]で可能な出力周波数を確認しておきます。関係するパラメータは、CPUクロック周波数、CKDIV8フラグ(CPUクロックを8分周するかしないか)、PWM分周比です。エクセルで計算しました。
周波数の高い順にソートしてあります。 4kHz以下がいきなり 500Hz になってしまいますが、とりあえず試してみます。
[編集] PWM周波数 500[Hz]、出力正弦波 50[Hz] の場合
波形は以下になりました。
これは話になりません。「パワー・エレクトロニクス回路の設計」(CQ出版社)によれば、搬送波周波数は出力正弦波の20倍以上必要、とのことでした。
[編集] PWM周波数 500[Hz]、出力正弦波 25[Hz] の場合
20倍の原則に則り、出力正弦波を 25[Hz]にしてみました。フィルタはカットオフ 15.9[Hz] にしました。まあまあですが、これ以上粗いと厳しい感じです。20倍、というのは理解できる値と思います。しかしながら、出力周波数がなぜか 15.7[Hz]になってしまいました。フィルタのせいではありません。PWM周波数を 4[kHz]にすれば、ただしく 25[Hz] で出力されましたが。。
[編集] 最終プログラムと回路
3相のPWMを出力するプログラムと回路は以下のようになりました。
/*ATMEGA88 2014/4/15 出力 U相 PD6 (OC0A) V相 PD5 (OC0B) W相 PD3 (OC2B) PB3 (OC2A) もあるが、MOSI と共用なので上記3ポートを使用する。 delay にて生成正弦波50Hz に調整 */ #define F_CPU 1000000UL //1MHz #include <avr/io.h> #include <util/delay.h> const uint8_t sinValue[] = { 127,131,136,140,145,149,153,158,162,166, 170,175,179,183,187,191,194,198,202,205, 209,212,215,218,221,224,227,230,232,235, 237,239,241,243,245,246,248,249,250,251, 252,253,253,254,254,254,254,254,253,253, 252,251,250,249,248,246,245,243,241,239, 237,235,232,230,227,224,221,218,215,212, 209,205,202,198,194,191,187,183,179,175, 170,166,162,158,153,149,145,140,136,131, 127,123,118,114,109,105,101,96,92,88, 84,79,75,71,67,63,60,56,52,49, 45,42,39,36,33,30,27,24,22,19, 17,15,13,11,9,8,6,5,4,3, 2,1,1,0,0,0,0,0,1,1, 2,3,4,5,6,8,9,11,13,15, 17,19,22,24,27,30,33,36,39,42, 45,49,52,56,60,63,67,71,75,79, 84,88,92,96,101,105,109,114,118,123, //240度分の進みデータも入れておく 127,131,136,140,145,149,153,158,162,166, 170,175,179,183,187,191,194,198,202,205, 209,212,215,218,221,224,227,230,232,235, 237,239,241,243,245,246,248,249,250,251, 252,253,253,254,254,254,254,254,253,253, 252,251,250,249,248,246,245,243,241,239, 237,235,232,230,227,224,221,218,215,212, 209,205,202,198,194,191,187,183,179,175, 170,166,162,158,153,149,145,140,136,131, 127,123,118,114,109,105,101,96,92,88, 84,79,75,71,67,63,60,56,52,49, 45,42,39,36,33,30,27,24,22,19 }; int main(void) { uint8_t countDiv=0; DDRD |=_BV(PD5)|_BV(PD6)|_BV(PD3);; DDRB |=_BV(PB3); PORTD |= _BV(PD5)|_BV(PD6)|_BV(PD3); //PD6-OC0A, PD5-OC0B, PD3-OC2B PWM出力 //PORTB |=_BV(PB3); //PB3-OC2A PWM出力 //タイマー0設定 TCCR0A |= _BV(WGM01)|_BV(WGM00); //8ビット高速PWMモード TCCR0A |= _BV(COM0A1)|_BV(COM0B1); //非反転動作、OC0A, OC0Bピンへ出力 TCCR0B |= _BV(CS00); //1分周 //タイマー2設定 TCCR2A |= _BV(WGM21)|_BV(WGM20); //8ビット高速PWMモード TCCR2A |= _BV(COM2A1)|_BV(COM2B1); //非反転動作、OC2A, OC2Bピンへ出力 TCCR2B |= _BV(CS20); //1分周 //f = 1MHz/(1x(1+255) = 3906Hz while(1) { // 2°毎の正弦波高を更新 (度数 = countDiv * 2) OCR0A = sinValue[countDiv]; //U相 OCR0B = sinValue[countDiv+60]; //V相 OCR2B = sinValue[countDiv+120]; //W相 countDiv++; if(countDiv >= 180) countDiv = 0; // データは2°刻みなので 180 = 360° _delay_us(111); } return 0; }
[編集] 今後の予定
フルブリッジ回路による3相交流生成のため、FETのドライブ回路を作成する。最初はこちらのページを参考にPチャンネルとNチャンネルFETをディスクリートで組むつもりでしたが、最近はハーフブリッジドライバなるICがあり、それを使ったほうが明らかに簡単です。3相接続についてのページが見当たりませんので、こちらのページの回路を参考につくろうと思います。