LEDの明るさを色々変化させたいという要求があったとする。いちばん簡単なのは、LEDに供給するエネルギー(つまり電流)を制御する事だ。この事に疑問は無いと思う。
例えば、図の回路で、スライドボリュームを動かすと、抵抗値が変化する。抵抗値が変化すると、回路を流れる電流が変化する。電流が変化すると、LEDに与えられる電力(エネルギー)が変化するので、LEDの輝度(明るさ)が変化する。
これと同じ事を、マイコンのIOポートを使って行う事はできないか ...... ???
IOポートの構造について、振り返って見てみると、
以上のようになっている。出力の制御はTr1とTr2の2つのトランジスタで制御されており、このトランジスタは、ON/OFF制御で電流の量(つまり抵抗値)を制御するようにはできていない。(つまりお手上げってこと?)
AVRマイコンのIOポートは、ある瞬間の電流のON/OFFは出来ても、電流の量その物を調整する事は出来ない。...なので、そのままではLEDの明るさを制御する事は出来ない。さて、どうするか。
人の目は、ごく短い時間(1/100秒以下位か)の変化を捉えることができない。つまり、それより短い時間の変化は平均化してしまうってこと。
...であるなら、短い時間に点滅を繰り返すLEDはつきっぱなしのLEDより暗く見えるはずだ。この事を応用すると、IOポートでもLEDの明るさを制御できる。
具体的には、上図の様に、一定周期(人の目でちらつきを感じない程度に短い=1/100秒以下)の繰り返し波形を作成するように、IOポートに出力を行う。
このような長方形の電気信号(ON時間の)をパルス(Pulse)と呼び、パルスの幅を可変するして制御を方式をPWM(Pulse Width
Modulation~パルス幅変調)と呼ぶ。
また、周期に対するON時間の割合をデューティ比と呼び、以下の様に表す。
デューティ比 (Duty cycle) = ON時間 / 一周期の時間 [%] もしくは Duty = ON時間 / (ON時間 + OFF時間) |
【注意】
PWMを出力する場合、基本的に、変化させるのは周期に対するON時間の割合で、一周期の時間を変化させてはいけない。
プログラミングの実際
PWMの出力は、タイマなどの専用ハードウエアを使って実装する事も出来る(実際にはその方が一般的と思われる)が、ここでは、
IOポートの出力をプログラムで直接制御することで実現している。
実装例1 いちばん安易と思われる実装
簡単ではあるが、Duty=0に設定してもLEDが完全に消灯しないなどの問題がある。(→ 何故か?)
#define F_CPU
16000000UL //16MHz動作 _delay_ms()に必要 delay.hで使用 |
上記のプログラムの動作を、IOポートの出力を縦軸、横軸に時間を取ってプロットすると、下図の様になる。
※ _delay_us(0)としても、実行に若干の時間が掛る(=
dutyを0にする事ができない)ので、このやり方では、LEDを完全に消灯する事は出来ない。
実装例2 (※の問題を解決し、10%ステップで明るさが変化するようにしてみたもの)
#define F_CPU
16000000UL //16MHz動作 _delay_ms()に必要 delay.hで使用 |
この例では、Duty比=0の時にLEDは(ほぼ)完全に消灯している。これで一応問題無いのであるが、明るさの変化に違和感を感じないだろうか。(感じてほしい!!!)
暗い時の明るさの変化が急激で、明るい時の変化が僅かであるように感じないだろうか?
(別にプログラムがバグっているわけではない)
この様に感じる原因は、人の目(視覚の感度)は、下図の様に、見ている対象の輝度(明るさ)に対して対数的に反応する事による。
従って、直線的な輝度変化を表現したいなら、LEDに累乗的な輝度変化を与える必要がある。
本来であれば、視覚の特性に合った形で輝度変化を与えるべきであるが、簡易的な方法として以下の様な方法で実装することができる。
実装例3(明るさの変化を、より自然に見えるように改良したもの)
#define F_CPU
16000000UL //16MHz動作 _delay_ms()に必要 delay.hで使用 int main() CLKPR = 0x80; CLKPR = 0; //16MHzにする設定 DDRB = 0xFF; //PortBは全て出力に設定
while(1) |
ハードウエアによるPWMは、こちら。