PWM制御

 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ポートに出力を行う。

  • IOポートのON時間(1を出力している時間)が長ければ、それだけ多くの電流がLEDに流れるので、明るく光る。
  • IOポートのOFF時間(0を出力している時間)が長ければ、電流が少なくなって、LEDは暗くなる。

 このような長方形の電気信号(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で使用

#include  < avr/io.h >      // 入出力のポートアドレスを定義してある
#include  < util/delay.h >  // _delay_ms()を使うために必要


#define  CYCLE_WIDTH  1000  //1周期の長さ(1/100秒)
#define  DUTY  10           //10/1000=0.01 よってデューティ=1%

int main()
{
    CLKPR = 0x80; CLKPR = 0;  //16MHzにする設定

    DDRB = 0xFF;              // PortBは全て出力に設定

    while(1)
    {
        PORTB = 0xFF;    // LED ON
        _delay_us(DUTY);                //  ON時間を作成(マイクロ秒単位)

        PORTB = 0x00;    // LED OFF
        _delay_us(CYCLE_WIDTH - DUTY);  // OFF時間を作成(マイクロ秒単位)
    }
}

上記のプログラムの動作を、IOポートの出力を縦軸、横軸に時間を取ってプロットすると、下図の様になる。

  • dutyの値を変えることで、LEDの明るさが変化する。
  • 1周期の長さ(CYCLE_WIDTH)は一定
  • dutyの値は30回、パルスを出力するごとに可変する。(30回毎というのは、明るさの変化を視認し易くするための待ち時間


※ _delay_us(0)としても、実行に若干の時間が掛る(= dutyを0にする事ができない)ので、このやり方では、LEDを完全に消灯する事は出来ない。

実装例2 (※の問題を解決し、10%ステップで明るさが変化するようにしてみたもの)

#define F_CPU 16000000UL    //16MHz動作 _delay_ms()に必要 delay.hで使用

#include  < avr/io.h >      // 入出力のポートアドレスを定義してある
#include  < util/delay.h >  // _delay_ms()を使うために必要


#define  CYCLE_WIDTH  1000  //1周期の長さ(1/100秒)

int main()
{
    unsigned int duty, time;
    long nd;

    CLKPR = 0x80; CLKPR = 0;  //16MHzにする設定

    DDRB = 0xFF;              // PortBは全て出力に設定

    while(1)
    {

        for(duty=0; duty < CYCLE_WIDTH; duty++)
        
{
            for(nd=0; nd < 500000; nd++)
            {

                for(time=0; time < duty; time++)
                {

                    PORTB = 0xFF;  //LED ON
                }
                for(time=duty; time < CYCLE_WIDTH; time++)
                {

                    PORTB = 0x00;  //LED OFF
                }
            }
        }
    }
}

 この例では、Duty比=0の時にLEDは(ほぼ)完全に消灯している。これで一応問題無いのであるが、明るさの変化に違和感を感じないだろうか。(感じてほしい!!!)
 暗い時の明るさの変化が急激で、明るい時の変化が僅かであるように感じないだろうか?
 (別にプログラムがバグっているわけではない)

 この様に感じる原因は、人の目(視覚の感度)は、下図の様に、見ている対象の輝度(明るさ)に対して対数的に反応する事による。

      
 従って、直線的な輝度変化を表現したいなら、LEDに累乗的な輝度変化を与える必要がある。

  本来であれば、視覚の特性に合った形で輝度変化を与えるべきであるが、簡易的な方法として以下の様な方法で実装することができる。

実装例3(明るさの変化を、より自然に見えるように改良したもの)

#define F_CPU 16000000UL    //16MHz動作 _delay_ms()に必要 delay.hで使用

#include  < avr/io.h >      //入出力のポートアドレスを定義してある
#include  < util/delay.h >  //_delay_ms()を使うために必要

#define CYCLE_WIDTH 16384   //2の14乗 2^14

int main()
{
    unsigned int duty, time, nd;

    CLKPR = 0x80; CLKPR = 0;  //16MHzにする設定

    DDRB = 0xFF;              //PortBは全て出力に設定

    while(1)
    {
        //ビットシフトにより待ち時間を2倍する。
        for(duty=1; duty <= CYCLE_WIDTH; duty<<=1)
        {
            for(nd=0; nd < 10; nd++)    //LEDの変化を確認するための待ち時間
            {
                for(time=0; time < duty; time++)
                {
                    PORTB = 0xFF;  //LED ON
                }
                for(time=duty; time < CYCLE_WIDTH; time++)
                {
                    PORTB = 0x00;  //LED OFF
                }
            }
        }
    }
}

 

ハードウエアによるPWMは、こちら

最終更新:2017年10月04日 16:56