PICで電飾 PWM調光 その1 PWM調光を実現
まずは概要:PWM調光とは
長い(?)旅路を経て、PWM調光のところまでやってきました。
PWM調光とは何かを説明する前に、プログラムとして何を作るかを説明します。
我々がここで作るのは、5ミリ秒ごとにLEDが点滅するプログラムです。
そして、その5ミリ秒の中で何秒点灯して何秒消灯するかの時間を変えます。
下の図を見てください。どちらも5ミリ秒ごとに点滅していますが、上のほうが下の方より5ミリ秒の中での点灯時間が長くなっています。
どちらも、あまりに早く点滅しているので人間の目には点滅しているとは認識できません。連続して光って見えるのですが、点灯時間が長い上の発光パターンのほうが、下の発光パターンより明るく光っているように見えます。
これがPWM調光の理屈です。
点灯期間の設定:デューティ比
では早速始めましょう。
とりあえず3つのLEDがついた前回の回路をそのまま使って、それぞれ違う明るさで点灯させることにしたいと思います。明るさの差が分かりやすいように、ちょっと極端な数値例にします。つまり、
RB6のLEDは5ミリ秒の周期の全期間を点灯(100%)、
RB7のLEDは5ミリ秒のうち、1ミリ秒だけ点灯(20%)、
RA1のLEDはさらに少ない0.25ミリ秒だけ点灯(5%)
することにします。
これらの、5ミリ秒という1周期の中でLEDが点灯している時間の割合のことを、「Duty比」と呼ぶそうです。LEDを点灯させて務め(duty)を果たしている時間の割合ということですね。
プログラムではそれぞれのLED用に個別にDuty●●という変数を作り、点灯して欲しい期間を入れることにします。初期設定の所で、RB6のLED用ならDutyRB6という変数を作って、そこに点灯して欲しい期間、100を代入するわけです。RB7とRA1も同じようにして、
DutyRB6=100;
DutyRB7=20;
DutyRA1=5;
としておきます。
点滅のコントロール
点滅をコントロールするプログラムの構造は次のようなものです。
まず、割り込み処理を使って、50マイクロ秒に1回、割り込み関数へ飛ばします。割り込み関数の中では以下の処理を行います。
①カウンタ(変数名Counter)の数字を、割り込み関数に入るたびに1つずつ上げていきます。
②カウンタの数字が100になるたびにリセットします。
50マイクロ秒*100=5000マイクロ秒=5ミリ秒
ですから、カウンタは5ミリ秒に1回、リセットされるわけですね。これが点滅の1単位になります。
カウンタの数字はこんな感じで5ミリ秒かけて100まで上がって、またゼロに戻ります。
③このリセットのタイミングで、LEDを点灯します。
つまり、カウンターがゼロになったらLEDを点灯します。
④カウンタの数字があがってゆき、Duty●●を超えたら、LEDを消灯させます。
この図はDuty●●が50だった場合の例です。ちょうど5ミリ秒の半分の時間点灯して、残り半分は消えています。Duty●●の数字を増やせば点灯している時間が増えるので明るくなり、減らせば暗くなるわけです。
・RB6のDutyRB6は100なので、カウンタの数字がDutyRB6を超えることはありません。RB6は点灯しっぱなしです。
・RB7のDutyRB7は20なので、カウンタの数字が1から20の間は点灯します。5ミリ秒の20%ですから1ミリ秒点灯です。以降リセットまで消灯なので4ミリ秒消灯になります。
・RA1のDutyRA1は5なので、同じ理屈で0.25ミリ秒点灯、4.75ミリ秒消灯です。
点滅の理屈はおわかりいただけたでしょうか。Dutyの数字は0から100まで、自由に設定できます。LEDの明るさもそれに応じて変わります。
下掲のプログラムでは、この①から④までの作業がオレンジ色の字で示されています。//のコメントを見ながら内容を確認してみてください。
割り込み設定などなど:プログラム
5ミリ秒の周期を実現するには50マイクロ秒ごとの割り込みが必要です。
割り込みはタイマ0,1,2どれを使っても同じ…だと私は思っていて、そのように以前ここにも書いたのですが、この説明のやり方でPWM調光をするには、タイマ2を使うのが一番よいようです。
(色々試してみると、他のタイマではうまくいかなくなる場合がありました。なぜなのか原因がわかったらまた記事にします。)
タイマ2を50マイクロ秒に一回割り込みさせるパラメータは、
①クロック数 8MHz
②プリスケール1
③ポストスケール1
④PR2初期値99
です。
このパラメータ設定とタイマ2に必要なステートメントを下掲のプログラムの赤字のところで示しています。パラメータ設定は赤字で水色背景のところです。
カウンタとDutyの設定も必要です。カウンタ(Couner)と3つの変数(Duty●●)をint で宣言し、先ほど説明した100%,20%,5%のお勤め期間を代入しているのが青字のところです。
ところで今回のプログラムでは、メイン関数はレジスタを設定したら今回なにも仕事がありません。緑字のところを見てください。
while(1)
{
}
while(1){ }は、無条件で{ }内の仕事を繰り返す、という指示なのですが、その{ }の中が空っぽです。
後で、Duty●●の数字を徐々に変えるプログラムをここに足すと、明るさが徐々に変わるプログラムになるわけですが、今回はやらないので、メインには仕事がなく、割り込みがない間はここで何もせずにぐるぐるしているだけなのです。
ビルドと書き込み
プログラムの仕組みは了解いただけたでしょうか。それではプログラムをMPLABにコピーして、ビルドして書き込んで見て下さい。
ちゃんと3つのLEDの明るさが違ってみえるでしょうか。
一番明るいRB6は、5ミリ秒の周期の100%光りっぱなしです。その次のRB7 はその1/5の20%しか光っていません。明るさは5倍違うはずなのですが、写真ではそこまで違って見えないですよね。肉眼でもそうです。
光の量はそれほどリニアに人間の知覚する明るさに比例しないのかもしれません。実際の工作で意図した見栄えにするためには、自分で確かめながら数字を色々調整しないといけないようです。
(デジカメでも肉眼と似た感じに撮れるのは不思議ですが、CCDのデータそのままでなく人間の認識の仕方に合うように処理されたデータを吐き出しているんでしょうか…)
ともあれ完成ですね。いやー、長かった…しかしついにたどり着きましたよ、PWM調光に!
とうとうイスカンダルがそこに見えてるよ、位のところまで我々はやってきたわけですね。
後は応用編として幾つか発光パターンのプログラム例をお見せしたいと思います。
以下プログラムです。各LEDのDutyの数値をいろいろ変えて、どんなふうに見えるかぜひ確かめてみてください。
#include <xc.h>
#define _XTAL_FREQ 8000000
//コンフィグ設定
#pragma config FOSC = INTOSC
#pragma config WDTE = OFF
#pragma config PWRTE = ON
#pragma config MCLRE = ON
#pragma config CP = OFF
#pragma config CPD = OFF
#pragma config BOREN = ON
#pragma config CLKOUTEN = OFF
#pragma config IESO = OFF
#pragma config FCMEN = OFF
#pragma config WRT = OFF
#pragma config PLLEN = OFF
#pragma config STVREN = ON
#pragma config BORV = HI
#pragma config LVP = OFF
int Counter,DutyRB6,DutyRB7,DutyRA1;//変数の設定
//メイン関数ここから
void main(void)
{
//特殊レジスタの設定
OSCCON = 0b01110010 ;//内部クロック8MHz
ANSELA = 0b00000000 ;//RAの足をすべてデジタルI/Oとして使う
ANSELB = 0b00000000 ;//RBの足をすべてデジタルI/Oとして使う
OPTION_REG = 0b00000000;//内部プルアップ抵抗を使う
TRISA = 0b00000000 ;//RAの足はすべて出力に使う
TRISB = 0b00001100 ;//RBの足のうちRB2,RB3を入力に使う
WPUB = 0b00001100 ;//RB2,3に内部プルアップ抵抗をつなぐ
PORTA = 0b00000000 ;//RAの初期値はすべてゼロ
PORTB = 0b00000000 ;//RBの初期値はすべてゼロ
Counter=0;
DutyRB6=100;//Dutyの設定
DutyRB7=20;//Dutyの設定
DutyRA1=5;//Dutyの設定
// タイマー2設定
T2CON=0b00000100;//②プリスケール1、③ポストスケール1
PR2=99;//④PR2初期値99に
TMR2IF = 0; //タイマ2割り込みフラグゼロに
TMR2IE=1;//タイマ2割り込み許可
TMR2ON=1;//タイマ2スタート
PEIE=1;//周辺割り込み許可
GIE=1;//全体割り込み許可
while(1)
{
}
}//メイン関数ここまで
//割り込み関数ここから
void __interrupt() xxx(void)
{
if(TMR2IF)//タイマ2による割り込み
{
PR2=99;//PR2初期値99にもどす
Counter=Counter+1;//①カウントアップ
if (Counter==100)//②カウント100になったら
{
Counter=0;//②リセットしてカウントを0に
if (DutyRB6>0) RB6=1;//③カウント0と同時にRB6点灯
if (DutyRB7>0) RB7=1;//③カウント0と同時にRB7点灯
if (DutyRA1>0) RA1=1;//③カウント0と同時にRA1点灯
}
if (Counter>DutyRB6) RB6=0;//④カウント数がDutyRB6を超えたらRB6消灯
if (Counter>DutyRB7) RB7=0;//④カウント数がDutyRB7を超えたらRB7消灯
if (Counter>DutyRA1) RA1=0;//④カウント数がDutyRA1を超えたらRA1消灯
TMR2IF=0;//タイマ2による割り込みリセット
}
}//割り込み関数ここまで