第3回 PWM制御(2)〜PWM制御プログラムを作成する〜

今回はLEDをPWM制御するプログラムを作成して、LEDの明るさを調整してみます。

目次

今回やってみること

前回はPWM制御の概要を確認しました。

今回は、PWM制御のプログラムを作成して実際にLEDの明るさを調整してみます。

今回の回路とプログラムは次のようにします。

  1. 回路は基礎編で製作したブレッドボードをそのまま使用します
  2. 新しいプロジェクを作成してプログラムを作成します

制御方法を考える

制御信号

それでは最初に、プログラムでどのようにLEDを制御すればよいか考えてみましょう。

LEDの点滅が1秒間に100回〜200回程度以上であれば、実際にはLEDが点滅していても人間の目には点滅しているようには見えません。

そこで、PWM制御の「周期」は、1秒間に200回点滅するような値にしたいと思います。

ここで、PWM制御のデューティー比を50%にした場合を考えると、制御信号は次のようになります

Pic app 3 cycle calculation

制御時間の計算

ここで、この制御信号の時間の計算をしてみます。

上の図のピンク色文字の「周期」と、青文字の「デューティーサイクル(DC)」はそれぞれ何秒になるでしょうか。

まず、周期ですが、この1周期分のパターンが1秒間に200回あるわけですから、1秒 ÷ 200回 = 0.005秒です。

また、デューティーサイクルはその半分ですので、0.0025秒です。

Pic app 3 cycle calculation in s

制御する時間までわかりましたのでプログラムを検討していきましょう。

制御プログラム

プログラムをどのように組めば良いか考えてみます。

まずは日本語交じりの表現で考えてみると、以下のようになります。

while(1){
    LEDを点灯する;
    0.0025秒待つ;
    LEDを消灯する;
    0.0025秒待つ;
}

PWM制御って言葉は難しいですが、実際のプログラムは随分簡単ですよね!

って思ったのも束の間、ここから大変になります。

上の日本語交じりのプログラムを実際のPICマイコンのプログラムで書いてみます。

LEDの点滅制御は変数LATA5、時間待ちは関数__delay_ms()を使えば実現できますが…

while(1){
    LATA5 = 1;
    __delay_ms(???);
    LATA5 = 0;
    __delay_ms(???);
}

__delay_ms()関数は、引数の単位はミリ秒です。

計算結果としては、0.0025秒なのですが、これをミリ秒で指定する必要があります。

ということで、0.0025秒をミリ秒にすると値はどうなるのか、もうちょっと計算が必要です。

時間の計算

PWM制御をする場合、時間の計算はよく出てきます。

これを機会にミリ秒、マイクロ秒の計算に慣れておきたいと思います。(時間の計算に慣れている方は読み飛ばしていただいて構いません)


時間の計算ですが、PWM制御に限らず、他の電子回路で時間計算をする場合「ms」「μs」がよく使われます。

先ほど計算した「0.005秒」や「0.0025秒」は「ms」や「μs」で表すとどうなるか、計算できるようにしておきたいと思います。

このあとの説明では、秒を「s」、ミリ秒を「ms」、マイクロ秒を「μs」と表します。


まず、msとsの関係を整理しておきましょう。

1s = 1000ms

これは秒の単位ですが、「m(ミリ)」という補助単位は他でもよく使われます。

身近な例ですと、「1L(リットル)= 1000mL(ミリリットル)」がありますよね。m(ミリ)は1000分の1を意味します。

それではsとmsの変換方法を確認してみましょう。

「s」から「ms」への変換

sからmsに変換するには、1000で掛け算すればOKです。

例えば1sをmsに変換したい場合、1 × 1000 = 1000ですので、1s = 1000ms になります。

他には例えば、0.001sは、0.001 x 1000 = 1ms となります。

「m(ミリ)」をつける場合は1000をかける、というとになります。

「ms」から「s」への変換

msからsに変換するには、先ほどとは逆に1000で割り算すればOKです。

1000msは、1000 ÷ 1000 = 1s となります。

他には例えば500msであれば、500 ÷ 1000 = 0.5sです。

しつこいですが、1msは 1 ÷ 1000 = 0.001s です。

「s」「ms」「μs」の関係

この関係は、msとμsでも同じです。

単位変換の方法をまとめると、次のようになります。

Pic app 3 unit conversion

PWM制御プログラム

それでは、先ほどのPWM制御のプログラムに戻ります。

PWM制御部分のプログラム

プログラムでは、0.0025s待ちがあります。

__delay_ms()関数を使用する場合、引数の単位はmsですので、sからmsに変換するには1000をかければいいので、0.0025 x 1000 = 2.5ms となります。

ということで、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()というように、「u(ユー)」ではなく「μ(マイクロ)」を使った関数名であればもっとわかりやすいですよね。

でも「μ」という文字は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);
}

PWM制御プログラム

それでは、実際に動作するプログラムを書きましょう。

最初にPIC12F1822の新規プロジェクトを作成します。

プロジェクト名は「PWMControl」にしてみました。(新規プロジェクトの作り方を忘れてしまった場合は、基礎編第17回を参照してください)

新規プロジェクトを作成後、新規メインファイルを作成し、以下のプログラムをコピペします。

/*
 * File:   main.c
 */
#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制御 (デューテー比50%)
    while(1){
        // LEDを2.5ms点灯
        LATA5 = 1;
        __delay_us(2500);
        // LEDを2.5ms消灯
        LATA5 = 0;
        __delay_us(2500);            
    }

    // 以下の命令は実行されない
    return;

}

ファイルを保存したら、ビルド、PIC12F1822に書き込み、動作させてみてください。


ある程度の明るさで光ると思いますが、元の明るさを忘れてしまいましたよね。

果たして正しく暗く光っているのかわからない、ということもあるかもしれません。

そこで今度は、デューティー比を10%にしてみたいと思います。

1秒間に200周期の繰り返し、デューティー比が10%の場合は以下のような制御になりますよね。

Pic app 3 duty ratio 10

このように制御するには、while文の処理を以下のように書き換えればOKです。

    // LED PWM制御 (デューテー比10%)
    while(1){
        // LEDを0.5ms点灯
        LATA5 = 1;
        __delay_us(500);
        // LEDを4.5ms消灯
        LATA5 = 0;
        __delay_us(4500);            
    }

このようにプログラムを変更して、再度、ビルド、書き込み、動作させてみてください。

今度はかなりLEDが暗く光ったと思います。

PWM制御は、周期とデューティー比を理解して、時間計算ができればかなり簡単ですよね!

と言いたいところですが、実はこのあと大変なことになります。

大変になる前に、ここまでの知識を定着させるために、ミニチャレンジ課題をやってみましょう。

ミニチャレンジ課題

LEDをPWM制御すれば、LEDの明るさを調整できることがわかりました。

でも上のプログラムの場合、暗くなっているのか、なんとなくわかりづらいですよね。

ということで、明るさがわかるようにしてみたいと思います。

ミニチャレンジ課題

次のような動作仕様のプログラムを作成してみてください。

【動作仕様】
1秒ごとにLEDの明るさを変えてみたいと思います。
PWM制御の周期は5ms(= 1秒間に200周期)にします。
動作としては、「1秒間デューティー比80%で点灯、その後1秒間デューティー比20%で点灯」という動作をずっと繰り返すようにしてみます。

それでは、次回はPWM制御の大変なところに進みます。

ミニチャレンジ課題解答例

周期5ms、デューティー比80%と20%ですので、ON時間は整数になりますので、__delay_ms()関数を使ってみました。また、数値は関数に直接書いていますが、#defineで定義して周期から計算で設定したほうがいいかもしれません。

今回はわかりやすさを優先してみました。

/*
 * PICマイコン電子工作入門 応用編 第3回
 * ミニチャレンジ課題1 解答例
 */

#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()関数が使用する
#define _XTAL_FREQ 1000000

void main(void) {

    // PICマイコン設定
    OSCCON = 0b01011000;  // 内部クロック周波数を1MHzに設定
    ANSELA = 0b00000000;  // すべてのピンをデジタルモードに設定
    TRISA  = 0b00001000;  // すべてのピンを出力モードに設定(ただしRA3ピンは常に入力モード)

    while(1) {

        // 1秒間80%の明るさで点灯
        for(uint8_t i=0; i<200; i++) {
            LATA5 = 1;
            __delay_ms(4);
            LATA5 = 0;
            __delay_ms(1);
        }

        // 1秒間20%の明るさで点灯
        for(uint8_t i=0; i<200; i++) {
            LATA5 = 1;
            __delay_ms(1);
            LATA5 = 0;
            __delay_ms(4);
        }

    }
    
    // 以下の命令は実行されない
    return;
}

更新履歴

日付内容
2016.12.31新規投稿
2018.11.29誤記訂正
2025.4.28説明補足
ミニチャレンジ課題解答例追加
通知の設定
通知タイミング
guest
14 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
wakaba007
wakaba007
3 年 前

わかりやすく丁寧な説明で、ありがたく拝読しています。子供向けの電子工作を作りたいと思い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;

}

wakaba007
wakaba007
返信  管理者
3 年 前

早速回答いただきありがとうございます。__delay_us()が関数でなくマクロであること納得しました。教示いただいた方法もトライしたいと思います。

hiroyuki
hiroyuki
6 年 前

お世話になります。
マイコン初学者の私にも大変分かり易くまとめられていて、大変助かっております。

プチチャレンジ課題の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文を使用した時と、そうでない時とで、なぜこのような差が生じてしまうのでしょうか…ご教示頂けましたら幸いです。何卒よろしくお願い致します。

wakazo
wakazo
7 年 前

勉強させてもらってます。丁寧な説明だと思います。
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
7 年 前

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でプリン基板作成されたんですね! 初めて自分で作ったプリント基板を手にした時、とても嬉しいですよね。自分もそうでしたが、モノづくりのツールが増えてくると、自分のできることが広がってきますので人生楽しくなると思います!

それではまた不明点ありましたらコメントいただければ゛と思います。

wakazo
wakazo
返信  管理者
7 年 前

ありがとうございました。
初心者なので、関数すらさらりと作れないので今後のことも考えて作ってみます。
PICマイコンのPWM機能は、今日Webを参考に、作成した基板にのせたPIC12F683で拡張用パターンのところにLEDを追加してGP2ピンに接続し試していました。LED1つはSW制御、もう1つはHW制御で1秒毎にdutyを同時に変えてみてました。683のデータシートもにらめっこして説明されている1822との差を把握しながら進めました。

管理者
管理者
返信  wakazo
7 年 前

プログラムですが、まずは動けばOKということで先に進んだ方がいいと思いますよ。プログラムの規模がだんだん大きくなってきたら関数化とか検討されると良いと思います。

手を動かして実際に動かすと楽しいので、まずは楽しさ優先で進めるといいと思います!

この先もちょっと難しいところがありますので、わからないことがありましたらお気軽にご質問ください。

Matsudate
Matsudate
7 年 前

はじめまして!
ここまで丁寧な解説をされているサイトは初めてです^^
参考にさせていただいています!

質問ですが、クロックの設定に水晶振動子を使おうと考えているのですが、プログラミングは特に変えなくても大丈夫ですか?(使う振動子は20MHzなので、数値は変えるのですが…)

管理者
管理者
返信  Matsudate
7 年 前

Matsudateさま、
ご質問どうもありがとうございます。

クロックに水晶振動子を使われる場合、PICマイコンの設定は簡単になります。

まず、コンフィグレーション設定のFOSCを

#pragma config FOSC = HS

にします。あと周波数設定レジスタのOSCCONですが、設定する必要はありません(=プログラムには記述せず、デフォルト設定のままで構いません)。

理由ですが、20MHzの水晶振動子を使用しますので、周波数はハードウェア的に20MHz固定になります。つまりPICマイコン側での設定は、周波数は設定の必要はなく、「周波数は外部の高速振動子を使用する」という設定、つまりFOSC = HSのみの指定でOKです。

ただ、PIC12F1822のようにピン数が少ないマイコンではI/Oピンとして使用できるピン数が少なくなってしまいますので、正確性を要する場合は外部振動子、それほど正確性が必要ない場合は内部周波数発振を使用するとよいと思います。

他に何か不明な点がございましたらご質問いただければと思います。

Matsudate
Matsudate
返信  管理者
7 年 前

素早いご回答をありがとうございます!
PWMで電圧を変化できさえすればいいので正確性を求めない内部クロックを使用することにしました!
ところで、
実際にPWM制御(4)のプログラミングを書き込んでみました。
ピンヘッダにPICkit3を差し込んでいる間はたしかに、PWM制御をしてくれるのですが、
問題がいくつか発生しています。
1,スイッチを押すことなくLEDが点滅を開始する
2,書き込みをしていない同種のPIC12F8122を使っても動作する
3,PICkit3を抜くと動作しなくなる
何か原因に心当たりはないでしょうか??

管理者
管理者
返信  Matsudate
7 年 前

正確性を求めないのであれば内部クロックでも十分ですが、精密な時間制御やUSB通信を行う場合はやはり外部クロックの方が安定していますので、用途に応じてご検討いただければと思います。

ところで、PICKit3の接続ですが、基本的には

1) PICKit3でプログラムを書き込み
2) 書き込みが成功したらPICKit3を外す
3) 回路に電池ボックスの電源を接続してスイッチを入れる

という手順で動作確認をすればおそらく問題は発生しないと思います。なお、状況によっては、(1)のPICKit3のプログラム書き込み後、スイッチを押さないでも動作が開始してしまうことがありますが、そのような場合は一度PICKit3を抜いて、(2)、(3)の手順で動作確認していただければと思います。

また、PICKit3を接続しただけで、プログラムの書き込みをしていないPIC12F1822が動作するということは原因はなかなか思い当たらないです。

まずは動作確認の手順をご確認いただけませんでしょうか。

Matsudate
Matsudate
返信  管理者
7 年 前

2日間粘った末、なんとか正常に作動させることができました!原因はおそらく、ブレッドボードが一部開放?していた、あるいは、ジャンパー線の不備だと考えられます。本当にありがとうございます!

(おそらく)最後の質問なのですが、
PWM制御(4)にあるプログラミングはデューティー比を徐々に変えるものだと認識していますが、最初に決めたデューティーサイクル0.5msというのはデューティー比の初期値に値するものという認識で大丈夫でしょうか?

管理者
管理者
返信  Matsudate
7 年 前

ブレッドボードの不備が原因のよう、とのことで解決してよかったです! コメントをいただいたあと、自分の回路でも再現しないか確認していたのですが、なかなかご連絡いただいた状況は再現できず困っていました。自分の方でも早めにいろいろな原因の可能性を考えてご回答すべきでした。それにしてもご自分で解決されたのはすごいと思います!

ところで、最初のデューティーサイクルは初期値という考え方で問題ないと思います。

周期を1ms、デューティー比を5%から始めますので、デューティー比の初期値(開始値?)は0.05msになりますが、これを10%から始めても全く問題ありません。ただ、記事を書く前にプログラムを検討したところ、デューティー比を0%から始めると、なんとなく落ち着かない点滅に感じましたので、5%〜100%の変化にしました。

ただ、このあたりは個人の好みもありますので、いろいろ試されてみると面白いと思います。

ということで、長くなってしまいましたが、デューティー比5%というのはLEDを消灯させないため、たまたまこの値にして、特に深い意味があるわけではありません。(回答が的を射ていないようでしたらその旨お教えください)

それではまた何か不明点ございましたらコメントいただければと思います。

目次