第5回 PWM制御(4)〜PWM機能を実装する〜

今回は、基礎編で製作したタイマープログラムにPWM機能を実装します。

目次

実装する内容

前回までに、PWM制御方法を二通り確認しました。

ひとつは、__delay_us()関数を使用して時間制御を含めて全てプログラムで制御する方法、もう一つは、PICマイコンのPWM制御機能を使って制御する方法です。

せっかくPICマイコンのPWM機能の使い方を習得しましたので、タイマープログラムに実装してみたいと思います。

とはいっても、PWM機能はタイマーにどうしても必要というわけではありません。ちょっと無理矢理感がありますが、なんとか実装してみようと思います。


仕様としては「タイマーで設定時間になったらブザーを鳴らすと同時にLEDをスムーズに点滅させる」という内容にします。

基礎編で製作したタイマーは、時間が来るとブザーを鳴らし、LEDを点灯させる、という動作でした。

この「LEDを点灯させる」という部分を「PWM機能を使ってスムーズに点滅させる」という動作に変更します。

実装仕様

実装仕様としては、次のようにLEDを点滅制御することにします。

  • PWMの周期は1ms
  • LEDの明るさの制御は、デューティ比を5%〜100%の間で変えることにより行う
  • デューティ比は1ms毎に0.1%ずつ変える。5%→100%まで1ms毎に0.1%ずつ大きくし、100%→5%まで1ms毎に0.1%ずつ小さくする、という動作を繰り返す

デューティー比は0%〜100%で変化させても良いと思います。

5%から開始しているのは特に意味はありませんので、一度動作させてみてご自分の好きな点灯パターンで調整してみてください。

プログラム

ベースにするプログラム

ベースのタイマープログラムは、基礎編第28回チャレンジ課題1の解答例を使用することにしました。

主な仕様は次のとおりです。

  • 動作開始後、LEDを点灯状態にする
  • スイッチが押されたらタイマー動作開始
  • 1秒に1回、LEDを点滅
  • タイマー時間になったらアラーム音「ピッピッピ」を3回繰り返す

このプログラにLEDのPWM制御動作を追加することにします。

PWM制御追加プログラム

ベースプログラムでは、タイマー時間になったらLEDを点灯状態に保ち、アラーム音を鳴らします。

ここまでの動作でPWM制御するのは難しいので、プログラムをなるべく簡単にするために「アラーム音が終わったらLEDのPWM制御をする」というようにしてみました。

PWM制御のために追加したプログラム部分について説明します。

なお、動作確認しやすくするために、タイマー時間は3秒にしています。

PWM制御ピン割り当て設定

RA5ピンをPWM制御ピンに設定しますので、前回までの説明の通り、次のコードにしました。

    APFCONbits.CCP1SEL = 1;     // PWM機能をRA5ピンに設定
    CCP1CONbits.CCP1M = 0b1100; // PWM機能を有効、active-highに設定
    CCP1CONbits.P1M = 0b00;     // RA2ピンはGPIOに設定

PWM制御の周期とデューティーサイクル設定

周期は1msに設定し、デューティーサイクルは100%(=1ms)に設定しておきます。

タイマー時間になったらLEDが点灯状態、つまりデューティーサイクル100%の状態になりますので、そこからスムーズに点灯制御するためです。

次のコードにしました。

    T2CONbits.T2CKPS = 0b00;    // プリスケーラ値を1に設定
    PR2 = 249;                  // 周期を1msに設定 (249 + 1) x 4 x 1us = 1000us = 1ms
    CCPR1L = 1000/4;            // デューティーサイクルを1msに設定
    CCP1CONbits.DC1B = 1000;

ただ、このコード場合ビルド時に警告が出ます。

理由は、CCP1CONbits.DC1Bが2ビット幅に対し、1000を代入しようとしているためです。

意図してこのようにしているので問題ありませんが、警告を出さないようにするには次のようにビットマスクします。

    CCP1CONbits.DC1B = 1000 & 0b11;  // 下位2ビットを代入する

PWM制御部分

これでPWM制御ができるようになりましたので、あとはデューティー比を1msごとに変えていけばOKです。

for文を使って100%から5%、5%から100%に変化するようにして、この処理をずっと繰り返すようにしてみました。

    while(1) {
        // 100% -> 5%の制御
        for(uint16_t duty=1000; duty>=50; duty--) {
            CCPR1L = duty / 4;       // 上位8ビット
            CCP1CONbits.DC1B = duty; // 下位2ビット
            __delay_ms(1);
        }
        // 5% -> 100%の制御
        for(uint16_t duty=50; duty<=1000; duty++) {
            CCPR1L = duty / 4;       // 上位8ビット
            CCP1CONbits.DC1B = duty; // 下位2ビット
            __delay_ms(1);
        }
    }

なお、このコードの場合、先ほどと同様の理由で警告が出ます。

理由は、8ビットのCCPR1Lに、16ビットの計算結果を代入しようとしているためです。

さらに、2ビットのCCP1CONbits.DC1Bに、16ビットの値を代入しようとしているためです。

警告が気持ち悪いようでした、次のように変数を8ビットでキャストすると解決できます。(というより、本来は最初からこのようにきちんとプログラムを作成するべきかもしれませんね…)

    while(1) {
        // 100% -> 5%の制御
        for(uint16_t duty=1000; duty>=50; duty--) {
            CCPR1L = (uint8_t) duty / 4;    // 上位8ビット
            CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
            __delay_ms(1);
        }
        // 5% -> 100%の制御
        for(uint16_t duty=50; duty<=1000; duty++) {
            CCPR1L = (uint8_t) duty / 4;    // 上位8ビット
            CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
            __delay_ms(1);
        }
    }

完成プログラム

今までの内容をまとめてみました。

今後このプログラムをベースにするため、警告が出ないようにしてあります。

/*
 * PICマイコン電子工作入門 応用編 第5回
 *   LEDのPWM制御
 */

#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ピンは常に入力モード)

    // ブザーをOFF、LEDをONにする
    LATA4 = 0; // ブザー
    LATA5 = 1; // LED

    // スイッチが押されるまで待つ
    while( RA3 ) {
    }
    
    // LEDを一定回数点滅する
    for(uint16_t timer=0; timer<3; timer++) {
        // LEDを950ms消灯する
        LATA5 = 0;
        __delay_ms(950);            
        // LEDを50ms点灯する
        LATA5 = 1;
        __delay_ms(50);
    }

    // アラーム音を3回鳴らす
    for(uint8_t i=0; i<3; i++) {
        // ピッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        __delay_ms(70);
        // ピッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        __delay_ms(70);
        // ピーッ
        LATA4 = 1;
        __delay_ms(70);
        LATA4 = 0;
        // 1回分の音パターンの間を少し空ける
        __delay_ms(800);
    }

    // LEDをPWM制御してスムーズな点滅制御をする
    //
    // PWM機能のピン割り当て設定
    APFCONbits.CCP1SEL = 1;     // PWM機能をRA5ピンに設定
    CCP1CONbits.CCP1M = 0b1100; // PWM機能を有効、active-highに設定
    CCP1CONbits.P1M = 0b00;     // RA2ピンはGPIOに設定

    // 周期とデューティーサイクルの設定
    //   周期は1ms
    //   デューティーサイクルは100%から開始するので1msに設定
    T2CONbits.T2CKPS = 0b00;    // プリスケーラ値を1に設定
    PR2 = 249;                  // 周期を1msに設定 (249 + 1) x 4 x 1us = 1000us = 1ms
    CCPR1L = 1000/4;            // デューティーサイクルを0.5msに設定
    CCP1CONbits.DC1B = 1000 & 0b11;

    // PWM制御スタート
    T2CONbits.TMR2ON = 1;

    // LEDをPWM制御
    // デューテー比は10%〜100%の制御にする
    while(1) {
        // 100% -> 5%の制御
        for(uint16_t duty=1000; duty>=50; duty--) {
            CCPR1L = (uint8_t) (duty / 4);    // 上位8ビット
            CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
            __delay_ms(1);
        }
        // 5% -> 100%の制御
        for(uint16_t duty=50; duty<=1000; duty++) {
            CCPR1L = (uint8_t) (duty / 4);    // 上位8ビット
            CCP1CONbits.DC1B = duty & 0b11; // 下位2ビット
            __delay_ms(1);
        }
    }

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

動作確認

プログラムをビルドしてPICマイコンに書き込んで動作確認しましょう。

動作確認すると、確かにLEDの発光がだんだん明るくなったり暗くなったりしています。

でも、なんとなくですが、すぐに明るくなって、しばらく明るい期間が続き、急に暗くなる、というような動作に見えるかもしれません。

プログラム上は5%から一定間隔で明るくしていますが、実際に目視確認すると急に明るくなっているように感じると思います。

実は、これには理由があるんです。

例えばデューティー比が50%の場合、確かに明るさは半分になっているはずなのですが、人間の目にはLEDの明るさが半分になったようには見えないためです。

人間の目は明るい方の変化はそれほど感じませんが、暗い方の変化は敏感に感じるようになっています。(数学的には、人間の目は明るさに対して対数的な感度を持っているためです。これは視覚だけではなく聴覚も同じです。LEDのPWM制御は1次関数でデューティー比を制御していますが、人間の目の感度は対数関数であるため、ズレが生じています)


動作確認するとは言っても、目視では正しくPWM制御できているかわかりませんよね。

そこで、今回もオシロスコープ(電圧の波形の測定装置)でRA5ピンの電圧を確認しました。

PWM制御ではデューティー比を1ms毎に変えていますので、どのように変化しているか、今回は動画をアップしますので確認してみてください。(大した内容ではないですが…)

更新履歴

日付内容
2017.1.19新規投稿
2018.11.30プログラムテンプレートをMPLABX IDE v5.10に更新
2025.5.1プログラムを警告が出ないように変更
プログラム解説追加
通知の設定
通知タイミング
guest
12 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
KENZO
KENZO
2 年 前

はじめまして、PICプログラム初めてまだ1週間の初心者です。
蛍の疑似ライトを作ろうと勉強はじめました。
ヒメボタルは単調なパターンでPWMは不要だったのですがゲンジボタルの再生はじんわりと光初めてじんわりと消えていくためPWMが最適かなと思い勉強を始めこのページにたどり着きました。
とても丁寧で解りやすい説明で参考になります。

ソースを参考にじわーっとした点滅は再現できたのですが0.2秒ほど消灯させたくて色々試みたのですがうまくいかなくて
   while(1) {
       // 5% -> 100%の制御
       for(duty=50; duty<=1000; duty++) {
           CCPR1L = duty / 4;         // 上位8ビット
           CCP1CONbits.DC1B = duty;   // 下位2ビット
           __delay_ms(1);
       }
       // 100% -> 5%の制御
       for(duty=1000; duty>=50; duty–) {
           CCPR1L = duty / 4;         // 上位8ビット
           CCP1CONbits.DC1B = duty;   // 下位2ビット
           __delay_ms(1);
       }
   //ここに0.2秒の消灯プログラムを入れればいいのですか?
   }
   // 以下の命令は実行されない

もう一つ制御の時間間隔を長くする場合
1回の点滅を5秒とかにする場合どこを直せばいいのでしょうか?

始めたばかりで初歩的な質問ですみません。
よろしくお願いします。

KENZO
KENZO
返信  KENZO
2 年 前

なんとかなりました。

       // 100% -> 1%の制御
       for(duty=1000; duty>=10; duty–) {
           CCPR1L = duty / 4;         // 上位8ビット
           CCP1CONbits.DC1B = duty;   // 下位2ビット
           __delay_ms(1);
       }
        // LEDの制御1秒消灯   
                 duty = 0;//LED消灯
         __delay_ms(1000);//1秒待機                            
   }

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

大地男女
大地男女
4 年 前

とても分かりやすい説明のお陰で始めたばかりの者ですが、どうにかここまで理解できました。PIC12F1822についてのPWM制御は実機で再現できました。

私は PIC12F1501 を使用して4つのLEDをPWMしたいと思い、ツールラボさんのコードやデータシートを参考にして以下のコードまで書くことができました。

本当に知識がないのでこの記事とは場違いかもしれませんが質問させて頂きます。

 質問は4つのPWMピンを使ってスムーズな明暗をさせる場合、while文を同時に4つ処理する、またはwhile文を使わずに処理することは可能でしょうか?

// PIC12F1501 Configuration Bit Settings

// ‘C’ source line config statements

// CONFIG1
#pragma config FOSC = INTOSC // Oscillator Selection Bits (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 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)

// CONFIG2
#pragma config WRT = OFF // Flash Memory Self-Write Protection (Write protection off)
#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 LPBOR = OFF // Low-Power Brown Out Reset (Low-Power BOR is disabled)
#pragma config LVP = ON // Low-Voltage Programming Enable (Low-voltage programming enabled)

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.

// クロック周波数指定
// __delay_ms(), __delay_us()関数が使用する
#define _XTAL_FREQ 1000000 //1clok=1us

#include

void main(void) {

// PICマイコン設定
OSCCON = 0b01011010; // 内部クロック周波数を1MHzに設定
ANSELA = 0b00000000; // すべてのピンをデジタルモードに設定
TRISA = 0b00001000; // すべてのピンを出力モードに設定

// 変数宣言
unsigned short timer; // 時間計測
unsigned short duty; // PWMのデューティーサイクル

LATA0 = 1;

//5ピンの設定
PWM1CON = 0b11000000; //RA=2をPWM有効かつactive-highに設定
T2CON = 0b00000000 ; // TMR2プリスケーラ値を1倍に設定
//初期化
PWM1DCH = 0 ; // デューティ値は0で初期化
PWM1DCL = 0 ;
TMR2 = 0 ; // タイマー2カウンターを初期化
//PWMの周期
PR2 = 249 ; // PWMの周期を設定(1us*1000=1ms)
PWM1DCH = 0b11001000 ; // デューティ値は0で初期化
PWM1DCL = 00 ;
T2CONbits.TMR2ON = 1; // TMR2(PWM)スタート

while(1) {
// 5% -> 100%の制御
for(duty=50; duty<=1000; duty++) { PWM1DCH = duty / 4; // 上位8ビット PWM1DCL = duty; // 下位2ビット __delay_ms(1); } // 100% -> 5%の制御
for(duty=1000; duty>=50; duty–) {
PWM1DCH = duty / 4; // 上位8ビット
PWM1DCL = duty; // 下位2ビット
__delay_ms(1);
}

}

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

管理者
管理者
返信  大地男女
4 年 前

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

PIC12F1501を使用してご自身でプログラムを作成された、というのはすごいですね! PICマイコンはちょっと使いづらいですが、使いこなせるようになるとPICマイコン自体はかなり安いので、いろいろなものが安く作れるようになりますよね。

ところで、PIC12F1501はPWMモジュールが4個ありますので、それぞれのPWMを個別に制御できます。例えば4個同時に制御したい場合、以下のようにwhile文の中で4個分のPWM設定を書けばOKです。

while(1) {
// 5% -> 100%の制御
for(duty=50; duty<=1000; duty++) { PWM1DCH = duty / 4; // 上位8ビット PWM1DCL = duty; // 下位2ビット PWM2DCH = duty / 4; // 上位8ビット PWM2DCL = duty; // 下位2ビット PWM3DCH = duty / 4; // 上位8ビット PWM3DCL = duty; // 下位2ビット PWM4DCH = duty / 4; // 上位8ビット PWM4DCL = duty; // 下位2ビット __delay_ms(1); } 以下同様 } なお、PWMモジュールの設定は4個分(PWM1CON〜PWM4CON)必要になります。 まずは基本的に4個のPWMモジュールを同じように制御する上のようなプログラムで動作確認いただいて、それをベースにいろいろ変えてみるといいかな、と思います。 回答になっているか分かりませんが、不明点ありましたらお手数ですがご質問いただければと思います。

大地男女
大地男女
返信  管理者
4 年 前

ご返信ありがとうございます。
おかげで、正に思い通りの制御をすることができました!
この先の記事も、しっかり理解しながら読み進めていきます。
ありがとうございました。

junjun
junjun
7 年 前

毎回 分かりやすく丁寧な ご説明ありがとうございます。
おかげさまで マイコン・C言語ともに初心者の私も、
ここまでたどり着くことができました。

PWM 制御に関しては まるで迷路に迷い込んでしまった様な状態でして、
今回も 一つ疑問があります。
 ・周期を一定とするなら Duty Cycle を変更する
 ・ Duty Cycle を一定とするなら周期を変更する ……
つまり Duty Ratio を変えることで PWM 制御が成り立っている というのが
ここまでの理解なのですが …… 。

79行目 : PR2 の設定で周期を決めた後は、
90行目以降の for 文で Duty Cycle を変更することにより
LED の明るさが連続的に変化するという ロジックであって、
 80行目 : CCPR1L = 500/4;
 81行目 : CCP1CONbits.DC1B = 500;
この二行は不要かと思われるのですが …… 。

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

junjunさま、

ご質問どうもありがとうございます!
鋭い質問です、と言うよりプログラム修正した方がよさそうです。

まず、ご指摘の80行目と81行目でDuty Cycleを指定している理由ですが、84行目でPWM制御をスタートする前に初期設定をしておきたかったためです。80行目と81行目の設定がなくても、CCPR1LとCCP1CONbits.DC1Bには何らかの値が設定されていますので、PWM制御自体はされますが、どのような値かわからないので、自分で初期値を設定したかったため、後から設定するにも関わらず80/81行目で設定していました。

ただ、設定する値を0.5ms(デューティー比50%)にしていますが、for文は5%から始めますので、初期設定は80/81行めは5%(=0.05ms=50)にすべきだと思います。このままですと、80/81行目の意味がわかりませんよね。

なお何かの機能設定を行う場合、思わぬ動作にならないようになるべく初期設定をする癖をつけておいた方がいいと思いますが、今回のように変な設定をすると、後から「何で自分はこんなことをしたんだろう」ということになりますので、コメントもしっかり書いておいた方がいいですね。(自分に対する戒めです…)

junjun
junjun
返信  管理者
7 年 前

丁寧な解説ありがとうございます。

想定外の動作を防ぐという意味をも併せ持った
初期設定の重要性 …… は考えたことがありませんでした。

これまで私が学んできた程度のレベルでは、
プログラムの途中で数値が変わるような場合は、
数値の代わりに変数を宣言しておき、
後から必要に応じて その変数に具体的な数値を代入する
というのが一般的な流れであったような?

Duty Ratio が どうなるのか分からないまま、
T2CONbits.TMR2ON = 1; によって、
とりあえず? PWM制御をスタートさせてしまい、
その後で Duty Ratio を変更するといった流れは違和感が
ありました。

だからと言って、前もって何も指定せずに
後から for 文の中で いきなり値を代入するというのもスッキリしない訳で …… 。

今回の解説で モヤモヤが解消できました。
どうもありがとうございました。
また 宜しくお願い致します。

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

基本的に何かの機能を使う場合には、初期設定する癖をつけておいたほうがいいと思います。

データシートを見ると、電源投入時/リセット時のレジスタの初期値が書かれています。その初期値でよければ初期化をしなくても問題ありませんが、プログラム上で初期値を明示しておいたほうが、あとでプログラムを見返したときにわかりやすくなると思います。

ただ、この記事のプログラムのように謎の初期設定をすると、あとでハマることになりますのでその点注意が必要です。

記事のプログラムを修正しようとしましたが、よくない例として残しておいてもいいかな、と思いました。

きにちち
きにちち
8 年 前

こんにちは。まったくのど素人ですがこちらのサイトで勉強させていただいております。
大変わかりやすい説明で助かっています。

今回のプログラムの90行目なんですが
for(duty=50; duty=<1000; duty++) { 
となっていますがこのままコピペしたらビルド時にエラーがでたので調べてみたら
比較演算子が =< ではなくて <= ではないでしょうか。
直したらビルドできました。

自分なりにサイトの内容をまとめてノートに書いて勉強しているのですが、できれば書籍化してほしいですw
出たら絶対買いますw

管理者
管理者
返信  きにちち
8 年 前

きにちちさん、
こんにちは。コメントどうもありがとうございます。

失礼しました! 比較は「<=」が正解です。いつもMPLABXでビルド、動作確認したソースコードをコピペしているのですが、コピペ元を間違ってしまいました。失礼いたしました。コメントでご指摘いただいてとても助かります。

記事の更新ペースは遅いですが、引き続きご愛顧宜しくお願いします。

目次