今回はLEDをPWM制御するプログラムを作ります。
今回やってみること
前回はPWM制御の概要を確認しました。
今回は、PWM制御のプログラムを作成して実際にLEDの明るさを調整してみます。今回は以下の手順で進めます。
- 回路は基礎編で製作したブレッドボードをそのまま使用します
- プログラムは新しいプロジェクを作成してプログラムを作ります
プログラムを考える
それでは早速どのようにプログラムを作成すればよいか考えてみましょう。
まず、LEDをどのように制御するか検討します。LEDの点滅が1秒間に100回〜200回程度以上であれば、実際にはLEDが点滅していても人間の目には点滅しているようには見えないので、「周期」は1秒間に200回となるようにします。
例えばPWM制御のデューティー比を50%にした場合、どのようなプログラムになるか考えてみます。LEDを1秒間に200回点滅制御すると、制御信号はこの図のようになります。
ここで時間の計算をしてみます。この図のピンク色文字の周期と、青文字のデューティーサイクル(DC)はそれぞれ何秒になるでしょうか。
まず、周期ですが、この周期が1秒間に200回あるわけですから、1秒 ÷ 200回 = 0.005秒です。また、デューティーサイクルはその半分ですので、0.0025秒です。
それでは早速プログラムを考えてみます。まずは日本語交じりの表現で考えてみると、以下のようになります。
while(1){
LEDを点灯する;
0.0025秒待つ;
LEDを消灯する;
0.0025秒待つ;
}
PWM制御って言葉は難しいですが、実際のプログラムは随分簡単ですよね。って思ったのも束の間、ここから大変になります。
上の日本語交じりのプログラムを実際のPICマイコンのプログラムで書いてみます。LEDの点滅制御も時間待ちも基礎編で出てきましたよね。それを使えばいいのですが、、、
while(1){
LATA5 = 1;
__delay_ms(???);
LATA5 = 0;
__delay_ms(???);
}
__delay_ms()
関数は、引数の単位はミリ秒です。となると引数の値がすぐにわからないですよね。つまり、0.0025秒と計算したのはいいけど、ミリ秒(ms)にすると値はどうなるのか、もうちょっと計算が必要です。
時間の計算
PWM制御をする場合、時間の計算が出てくるので、これを機会にミリ秒、マイクロ秒の計算に慣れましょう。読むだけではなく、実際にご自分で計算してみてください。
この時間の計算ですが、PWM制御に限らず、その他の電子回路で時間計算をする場合はmsやμsがよく使われます。先ほど計算した「0.005秒」や「0.0025秒」はmsやμsで表すとどうなるか、頭の中で計算できるようにしましょう。この後は、秒を「s」、ミリ秒を「ms」、マイクロ秒を「μs」と表します。
まず、msとsの関係を整理しておきましょう。
1s = 1000ms
です。これは秒の単位ですが、他の身近な例ですと、「1L(リットル) = 1000mL(ミリリットル)」がありますよね。m(ミリ)は1000分の1を意味します。
ではsとmsの変換方法を確認してみましょう。
msからsに変換するには、1000で割り算すればOKです。1000msは、1000 ÷ 1000 = 1sとなります。他には例えば500msであれば、500 ÷ 1000 = 0.5sです。
では、1msは何sになるかというと、1 ÷ 1000 = 0.001sです。
1ms = 0.001s
逆に、sからmsに変換するにはどうしたらよいでしょうか。先ほどと逆に1000で掛け算すればOKです。この例の場合、0.001sは、0.001 x 1000 = 1msとなります。
この関係は、msとμsでも同じです。単位変換の方法をまとめると、以下のようになります。
それでは、先ほどのPWM制御のプログラムに戻ります。プログラムでは、0.0025s待ちがあります。「__delay_ms()」関数を使用する場合、この関数の単位はmsですので、sからmsに変換するには1000をかければいいので、0.0025 x 1000 = 2.5msとなります。
ただ、この計算方法ですと、少数が出てきてしまいますので暗算しづらいですよね。そこで、普通は以下のように暗算します。
1s ÷ 200を計算する場合、結果は明らかに小数点になるので、1s ÷ 200の「1s」を最初に「1000ms」に直しておきます。つまり「1s ÷ 200」をすぐに計算するのではなく、割られる数が割る数より大きくなるように「1000ms ÷ 200」に直しておきます。次に「1000 ÷ 200」を計算します。この結果の単位はmsですので、「5ms」ということがわかります。
例えば、1秒間に500周期繰り返したい場合、1周期の時間は「1s ÷ 500」を計算してからmsに直すのではなく、先に「1000ms ÷ 500」に直してから計算します。答えは「2ms」です。
PWM制御プログラム
ということで、1秒間に200周期の繰り返し、デューティー比が50%の場合、プログラムは以下のようにすればOKそうです。
while(1){
LATA5 = 1;
__delay_ms(2.5);
LATA5 = 0;
__delay_ms(2.5);
}
残念ながらこのプログラムをビルドするとエラーになります。というのは、__delay_ms()
関数の引数は自然数である必要があります。少数は受け付けてくれません。
ではどうすればいいかというと、実はこの関数のμs版がありますので、それを使用します。「__delay_us()」という関数です。「__delay_」のあとは、ローマ字の「u(ユー)」「s(エス)」です。本当は「__delay_μs()」というような関数があればもっとわかりやすいですが、「μ」という文字は1バイト文字セットの中にはないため、「μ」に近い1バイト文字のローマ字の「u」が使用されています。
上のプログラムを「__delay_us()」関数を使用して正しく動作するように書き直すと以下のようになります。2.5msをμsに直すには1000をかければいいんでしたよね。2.5ms x 1000 = 2500μsです。
while(1){
LATA5 = 1;
__delay_us(2500);
LATA5 = 0;
__delay_us(2500);
}
それでは、実際に動作するプログラムを書きましょう。
最初にPIC12F1822の新規プロジェクトを作成します。プロジェクト名は「PWMControl」にしましょう。新規プロジェクトの作り方は、基礎編第17回を参照してください。
新規プロジェクトを作成したら、新規メインファイルを作成し、以下のプログラムをコピペします。
/*
* File: main.c
* Author: Tool Labs
*/
#include <xc.h>
// PIC12F1822 Configuration Bit Settings
// CONFIG1
#pragma config FOSC = INTOSC // Oscillator Selection (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = OFF // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
#pragma config CP = OFF // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config CPD = OFF // Data Memory Code Protection (Data memory code protection is disabled)
#pragma config BOREN = ON // Brown-out Reset Enable (Brown-out Reset enabled)
#pragma config CLKOUTEN = OFF // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF // Internal/External Switchover (Internal/External Switchover mode is disabled)
#pragma config FCMEN = OFF // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)
// CONFIG2
#pragma config WRT = OFF // Flash Memory Self-Write Protection (Write protection off)
#pragma config PLLEN = OFF // PLL Enable (4x PLL disabled)
#pragma config STVREN = OFF // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will not cause a Reset)
#pragma config BORV = LO // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LVP = OFF // Low-Voltage Programming Enable (High-voltage on MCLR/VPP must be used for programming)
// クロック周波数指定
// __delay_ms(), __delay_us()関数が使用する
#define _XTAL_FREQ 1000000
void main(void) {
// PICマイコン設定
OSCCON = 0b01011010; // 内部クロック周波数を1MHzに設定
ANSELA = 0b00000000; // すべてのピンをデジタルモードに設定
TRISA = 0b00001000; // すべてのピンを出力モードに設定(ただしRA3ピンは常に入力モード)
// LED PWM制御
while(1){
// LEDを2.5ms点灯
LATA5 = 1;
__delay_us(2500);
// LEDを2.5ms消灯
LATA5 = 0;
__delay_us(2500);
}
// 以下の命令は実行されない
return;
}
ファイルを保存したら、ビルド、PIC12F1822に書き込み、動作させてみてください。
ある程度の明るさで光ると思いますが、元の明るさを忘れてしまったので、果たして暗く光っているのかわからない、ということもあるかもしれません。
それでは、デューティー比を10%にしてみましょう。1秒間に200周期の繰り返し、デューティー比が10%の場合は以下のような制御になりますよね。必ずご自分でも計算してみてください。
このように制御流には、while文のところを以下のように書き換えればOKです。
// LED PWM制御
while(1){
// LEDを0.5ms点灯
LATA5 = 1;
__delay_us(500);
// LEDを4.5ms消灯
LATA5 = 0;
__delay_us(4500);
}
このようにプログラムを変更して、再度、ビルド、書き込み、動作させてみてください。今度はかなりLEDが暗く光ったと思います。
PWM制御は、周期とデューティー比、時間計算ができればかなり簡単ですよね。。。
と言いたいところですが、実はこのあと大変なことになります。大変になる前に、ここまでの知識を定着させるために、プチチャレンジ課題をやってみましょう。
ミニチャレンジ課題
LEDをPWM制御すれば、LEDの明るさを調整できることがわかりましたが、上のプログラムですと、暗くなったのか、なんとなくわかりづらいですよね。ということで、いくつかミニチャレンジ課題にトライしてみましょう。
1秒ごとに明るさを変えてみる
1秒ごとにLEDの明るさを変えてみましょう。PWM制御の周期は5ms(=1秒間に200周期)にします。
動作としては、「1秒間デューティー比80%で点灯、1秒間デューティー比20%で点灯」という動作をずっと繰り返すプログラムを作成して、動作させてみてください。
明るさを10段階制御する
デューティー比が違うとどのくらい明るさが変わるのかを確認するために、デューティー比を10%から100%まで変えるプログラムを作ってみましょう。
周期は5ms、動作としては、「1秒ごとにデューティー比を100%から10%まで、10%ずつ減らしていく」という動作をずっと繰り返すプログラムを作成して、動作させてみてください。
それでは、次回はPWM制御の大変なところに進みます。
更新履歴
日付 | 内容 |
---|---|
日付 | 内容 |
2016.12.31 | 新規投稿 |
2018.11.29 | 誤記訂正 |
わかりやすく丁寧な説明で、ありがたく拝読しています。子供向けの電子工作を作りたいと思いpicマイコンを勉強中の者です。明るさを10段階制御するプログラム作成して動かしてみまし。一応動いているようです。
ところで__delay_us(値)関数ですが、値に変数を入れることはできますか。変数にしてbuildすると「constantーー、実数をいれろ」とerrorが出てきます。
delay関数の有効な使い方を教えてください。この次は内蔵PWMへ進むつもりです。
//明るさを10段階制御するプログラム
void main(void) {
// PICマイコン設定
OSCCON = 0b01011010; // 内部クロック周波数を1MHzに設定
ANSELA = 0b00000000; // すべてのピンをデジタルモードに設定
TRISA = 0b00001000; // すべてのピンを出力モードに設定(ただしRA3ピンは常に入力モード)
//d:10段階
//d1;duty
//d2;周期ーduty
unsigned short int timer , d , d1 ,d2 ;
// LED PWM制御
while(1){
for( d= 10 ; d>0 ; d–){
for( timer=0 ; timer<200 ; timer++){
for( d1=0 ; d1< d ; d1++){
LATA5 =1 ;
__delay_us(500) ;
}
for( d2 =0 ; d2< 10-d ; d2++){
LATA5 = 0 ;
__delay_us(500);
}
}
}
}
// 以下の命令は実行されない
return;
}
ご質問どうもありがとうございます。
子供向けの電子工作をされているということでなんだかワクワクしますね。
お子さんは光ったり音が出たりするとこちらが思っている以上に嬉しがりますので、
作りがいがありますよね。
ところで、__delay_us()と__delay_ms()なんですが、実はこれは関数ではなく#defineでマクロ定義されているだけなんです。そのためパラメータに変数を利用できないです。
コメント欄に書かれたプログラムですが、いろいろなサンプルを見ると、このようなプログラムを作っている方が多いです。つまり必要回数「__delay_us(定数)」を呼ぶ、という解です。
以下のようなプログラムを見かけることもあります。
このような関数にすると、関数内のfor文の処理に時間がかかるので正確ではなくなってしまいます。特にdelay_us関数では、__delay_us(1);の処理時間に比べて、for文の処理時間が目立ってきますので、例えば以下のように100usを回数分呼ぶようにすると誤差が少なくなると思います。
ただ、PWM制御はONとOFF比率で決まりますので、最初のプログラムでも十分制御可能だと思います。
スッキリしたご回答でなく申し訳ございません。何か不明点ありましたらお手数ですがご質問いただければと思います。
早速回答いただきありがとうございます。__delay_us()が関数でなくマクロであること納得しました。教示いただいた方法もトライしたいと思います。
お世話になります。
マイコン初学者の私にも大変分かり易くまとめられていて、大変助かっております。
プチチャレンジ課題の2つ目について質問を投稿させてください。
明るさを10段階に変更する上で、以下のような処理を行ってみたところ、正常に作動しませんでした。
(本来でしたら、200、500、5000等のマジックナンバーを変数で定義すべきところなのかもしれませんが、ご容赦ください…)
unsigned short timer;
unsigned int i,j;
while(1){
i=0;
j=0;
for(i=500, j=5000-i; i<=5000; i+=500){ timer = 0; while(timer < 200){ LATA5 = 1; __delay_us(i); LATA5 = 0; __delay_us(j); timer+=1; } } } 実行したところ、LEDがずっと同じ明るさで光りっぱなしとなり、しばらく経過した時点で一瞬明るくパッと光ったかと思うと、その直後また先と同じ明るさに戻り…以下、同じ動作が繰り返されてしまいました。 そこで、for文を使用するのをやめ、愚直にwhile(timer < 200)のループ文を10個並べて書いたところ、今度は上手く動きました。 for文を使用した時と、そうでない時とで、なぜこのような差が生じてしまうのでしょうか…ご教示頂けましたら幸いです。何卒よろしくお願い致します。
勉強させてもらってます。丁寧な説明だと思います。
PWMのプチチャレンジ課題の1つめ、以下のようにしてみました。が他のやりかたがあったら紹介願えればとおもいます。
while(1){
// LEDを点灯
for(timer=0; timer<200; timer++){
GP5 = 1;
__delay_ms(4);
// LEDを消灯
GP5 = 0;
__delay_ms(1);
}
for(timer=0; timer<200; timer++){
GP5 = 1;
__delay_ms(1);
// LEDを消灯
GP5 = 0;
__delay_ms(4);
}
}
eagleを使用してプリント基板も試作してみました。少々不安でしたが、5枚発注(2枚おまけつき)で届きました。送付はOSCを利用して発注から合計8日で届きました。
wakazoさま、
コメントどうもありがとうございます!
作成されたプログラムで正しいですし、わかりやすいプログラムとしてはこのぐらいしか思いつかないです。
ただし、プログラムでPWM制御をする場合(=第4回で説明するPICマイコンのPWM制御機能を使用しない)場合、PWM出力を行う関数を作るとプログラムが作りやすくなると思います。
具体的な関数ですが、例えば以下のように
void PWMOutput(unsigned char period, unsigned char cycle, unsigned char duty_ratio);
period: 何秒間PWM制御するか(〜255秒)
cycle: PWMの周期(〜255ms)
duty_ratio: PWMのデューティー比(0〜100%)
というような関数を(頑張って)作れば、
PWMOutput(1, 5, 20);
と書けば、周期5ms、デューティー比20%のPWM信号を1秒間出力する、ということができるようになります。ここまでくるとPICマイコンのPWM機能を使った方が早いと思いますが、プログラミングの練習としてはいいかもしれません。
EAGLEでプリン基板作成されたんですね! 初めて自分で作ったプリント基板を手にした時、とても嬉しいですよね。自分もそうでしたが、モノづくりのツールが増えてくると、自分のできることが広がってきますので人生楽しくなると思います!
それではまた不明点ありましたらコメントいただければ゛と思います。
ありがとうございました。
初心者なので、関数すらさらりと作れないので今後のことも考えて作ってみます。
PICマイコンのPWM機能は、今日Webを参考に、作成した基板にのせたPIC12F683で拡張用パターンのところにLEDを追加してGP2ピンに接続し試していました。LED1つはSW制御、もう1つはHW制御で1秒毎にdutyを同時に変えてみてました。683のデータシートもにらめっこして説明されている1822との差を把握しながら進めました。
プログラムですが、まずは動けばOKということで先に進んだ方がいいと思いますよ。プログラムの規模がだんだん大きくなってきたら関数化とか検討されると良いと思います。
手を動かして実際に動かすと楽しいので、まずは楽しさ優先で進めるといいと思います!
この先もちょっと難しいところがありますので、わからないことがありましたらお気軽にご質問ください。
はじめまして!
ここまで丁寧な解説をされているサイトは初めてです^^
参考にさせていただいています!
質問ですが、クロックの設定に水晶振動子を使おうと考えているのですが、プログラミングは特に変えなくても大丈夫ですか?(使う振動子は20MHzなので、数値は変えるのですが…)
Matsudateさま、
ご質問どうもありがとうございます。
クロックに水晶振動子を使われる場合、PICマイコンの設定は簡単になります。
まず、コンフィグレーション設定のFOSCを
#pragma config FOSC = HS
にします。あと周波数設定レジスタのOSCCONですが、設定する必要はありません(=プログラムには記述せず、デフォルト設定のままで構いません)。
理由ですが、20MHzの水晶振動子を使用しますので、周波数はハードウェア的に20MHz固定になります。つまりPICマイコン側での設定は、周波数は設定の必要はなく、「周波数は外部の高速振動子を使用する」という設定、つまりFOSC = HSのみの指定でOKです。
ただ、PIC12F1822のようにピン数が少ないマイコンではI/Oピンとして使用できるピン数が少なくなってしまいますので、正確性を要する場合は外部振動子、それほど正確性が必要ない場合は内部周波数発振を使用するとよいと思います。
他に何か不明な点がございましたらご質問いただければと思います。
素早いご回答をありがとうございます!
PWMで電圧を変化できさえすればいいので正確性を求めない内部クロックを使用することにしました!
ところで、
実際にPWM制御(4)のプログラミングを書き込んでみました。
ピンヘッダにPICkit3を差し込んでいる間はたしかに、PWM制御をしてくれるのですが、
問題がいくつか発生しています。
1,スイッチを押すことなくLEDが点滅を開始する
2,書き込みをしていない同種のPIC12F8122を使っても動作する
3,PICkit3を抜くと動作しなくなる
何か原因に心当たりはないでしょうか??
正確性を求めないのであれば内部クロックでも十分ですが、精密な時間制御やUSB通信を行う場合はやはり外部クロックの方が安定していますので、用途に応じてご検討いただければと思います。
ところで、PICKit3の接続ですが、基本的には
1) PICKit3でプログラムを書き込み
2) 書き込みが成功したらPICKit3を外す
3) 回路に電池ボックスの電源を接続してスイッチを入れる
という手順で動作確認をすればおそらく問題は発生しないと思います。なお、状況によっては、(1)のPICKit3のプログラム書き込み後、スイッチを押さないでも動作が開始してしまうことがありますが、そのような場合は一度PICKit3を抜いて、(2)、(3)の手順で動作確認していただければと思います。
また、PICKit3を接続しただけで、プログラムの書き込みをしていないPIC12F1822が動作するということは原因はなかなか思い当たらないです。
まずは動作確認の手順をご確認いただけませんでしょうか。
2日間粘った末、なんとか正常に作動させることができました!原因はおそらく、ブレッドボードが一部開放?していた、あるいは、ジャンパー線の不備だと考えられます。本当にありがとうございます!
(おそらく)最後の質問なのですが、
PWM制御(4)にあるプログラミングはデューティー比を徐々に変えるものだと認識していますが、最初に決めたデューティーサイクル0.5msというのはデューティー比の初期値に値するものという認識で大丈夫でしょうか?
ブレッドボードの不備が原因のよう、とのことで解決してよかったです! コメントをいただいたあと、自分の回路でも再現しないか確認していたのですが、なかなかご連絡いただいた状況は再現できず困っていました。自分の方でも早めにいろいろな原因の可能性を考えてご回答すべきでした。それにしてもご自分で解決されたのはすごいと思います!
ところで、最初のデューティーサイクルは初期値という考え方で問題ないと思います。
周期を1ms、デューティー比を5%から始めますので、デューティー比の初期値(開始値?)は0.05msになりますが、これを10%から始めても全く問題ありません。ただ、記事を書く前にプログラムを検討したところ、デューティー比を0%から始めると、なんとなく落ち着かない点滅に感じましたので、5%〜100%の変化にしました。
ただ、このあたりは個人の好みもありますので、いろいろ試されてみると面白いと思います。
ということで、長くなってしまいましたが、デューティー比5%というのはLEDを消灯させないため、たまたまこの値にして、特に深い意味があるわけではありません。(回答が的を射ていないようでしたらその旨お教えください)
それではまた何か不明点ございましたらコメントいただければと思います。