第6回 PWM制御(5)〜チャレンジ課題〜

今回はPWM制御に関するチャレンジ課題です。

目次

PWM制御

PWMの最後はチャレンジ課題で締めくくりたいと思います。

前回までに、LEDを2通りの方法でPWM制御しました。(__delay_msなどの関数を使う方法・PICマイコンのPWM機能を使う方法)

PWMの原理自体はとても簡単だったと思いますが、周期やデューティーサイクルの算出が難しかったと思います。さらにPICマイコンのPWM機能を使うのはちょっと複雑でしたよね。

PWM制御のチャレンジ課題は、ちょっと地味ですが周期とデューティーサイクルの計算に慣れていただく内容にしてみました。

チャレンジ課題

PWM機能の説明で作成したプログラムは、LEDの点滅が認識できないように1秒間に200回点滅制御しました。

でも1秒間の点滅回数を少なくしていくと、いずれ点滅しているように見えます。

そこで、どの程度の点滅回数にするとLEDが点滅しているように見えてしまうのか、確認するプログラムを作成してみてください。

動作仕様もいろいろ工夫してみてください。

チャレンジ課題解答例

動作仕様

制御方法はいろいろありますが、今回は「周波数100Hzから開始して、20Hzになるまで10Hzずつ周波数を減らしながらLEDをPWM制御する」という動作にしてみます。

例えば100Hzで3秒間PWM制御したあと1秒間消灯して、次は90Hzで3秒間PWM制御して1秒間消灯して、という動作を20Hzまで繰り返すようにしてみます。

PICマイコン動作設定

PWM制御する場合、PICマイコンの動作設定をどうするか検討が必要です。

具体的には、PICマイコンの動作周波数をどのような値にすれば良いか、などの検討です。


このシリーズでは、PICマイコンの動作周波数は1MHzに設定していました。

このとき、PWMの周期はどの程度まで設定可能か確認してみます。周期は8ビットのPR2レジスタ(0〜255)で設定し、周期は次の計算式になります。

周期 = ( PR2 + 1 )× 4 × PICマイコン動作周波数の周期 × プリスケーラ値

PICマイコン動作周波数の周期は、1MHz動作の場合1μsになります。またプリスケーラ値は1、4、16、64が選択できます。

一番長い周期にする場合、プリスケーラ値を最大の64にすると、周期の最大値は次の式で計算できます。

周期 = ( 255 + 1 )× 4 × 1μs × 64 = 約65ms

一番長い周期で約65msですので、周波数は15Hzになります。

また、同じプリスケーラ値では一番短い周期は次の式になります。

周期 = ( 0 + 1 )× 4 × 1μs × 64 = 256μs

一番短い周期は256μsですので、周波数は4kHz程度になります。


ということで、1MHz動作でもPWM周波数を20Hzから100Hzまでは対応可能です。

ただ、今回は練習のため、PICマイコンの動作周波数を250kHzにしてみたいと思います。(特に深い意味はありませんが、動作周波数が低い方が消費電力が少なくなるので周波数を少し下げてみました。62.5kHzでプリスケーラ値4でも同じ最大周期になりますが…)

動作周波数が250kHzの場合、PICマイコン動作周波数の周期は、4μsになります。また、プリスケーラ値を16にすれば、一番長い周期を先ほどと同じ周期にできます。

周期 = ( 255 + 1 )× 4 × 4μs × 16 = 約65ms

ということで、今回は次のような動作設定でプログラムを作成してみます。

設定項目設定値
PICマイコン動作周波数250kHz
プリスケーラ値(TMR2)16

プログラム例

プログラムは、周期を変えるのではなく周波数を変えますので、周期設定のPR2レジスタと、デューティーサイクルのCCPR1LCCP1CON.DC1Bレジスタは、周波数から計算する必要があります。

周波数の計算

周期は次の式です。

周期 = ( PR2 + 1 )× 4 × 4μs × 16

周波数は周期の逆数ですので、周波数で表現すると次の式になります。

1 ÷ 周波数 = ( PR2 + 1 )× 4 × 4μs × 16

この式からPR2を求めると、次の式になります。(小数は丸めています)

PR2 = 3906 ÷ 周波数 − 1

PR2レジスタの設定はこの式を使用することにします。

デューティーサイクルの計算

デューティーサイクルは次の式で計算できます。(CCPR1LCCP1CON.DC1BレジスタをまとめてCCP1と表現しています)

デューティーサイクル = CCP1 × 4μs × 16

なお、デューティーサイクルは50%にしますので、周期の半分になります。上の式は次のようになります。

周期 ÷ 2 = CCP1 × 4μs × 16

さらに、周期は周波数の逆数ですので、次の式になります。

1 ÷ (周波数 × 2) = CCP1 × 4μs × 16

この式からCCP1を求めると、次の式になります。(小数は丸めています)

CCP1 = 7812 ÷ 周波数

この値をCCPR1LCCP1CON.DC1Bレジスタに代入することにします。

プログラムは次のようにしてみました。

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

#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)


// クロック周波数指定(250kHz)
// __delay_ms()関数が使用する
#define _XTAL_FREQ 250000

void main(void) {

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

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

    // PWMのプリスケーラ値を16に設定
    T2CONbits.T2CKPS = 0b10;
    
    // [PR2の計算]
    // 周期の計算は次の式
    //    周期 = (PR2 + 1) x 4 x 4us x 16
    // 周波数の計算式に直すと次の式になる
    //    1 / 周波数 = (PR2 + 1) x 4 x 4us x 16
    // この式からPR2を求める式に直すと次の式になる
    //    PR2 = (3906 / 周波数) - 1

    // [デューティーサイクル値(CCPR1L+CCP1CONの10ビット)の計算]
    // デューティーサイクルは周期の半分(50%)にするので、
    // 10ビットのデューティーサイクル(CCP1とする)の計算式は次の式
    //    周期 / 2 = CCP1 x 4us x 16
    // 周波数に直すと次の式1
    //    1 / (周波数 x 2) = CCP1 x 4us x 16
    // デューティーサイクル値に直すと次の式
    //   CCP1 = 7812 / 周波数

    // 周波数100Hzで点灯開始しておく
    uint8_t frequency_hz = 100;
    PR2 = 3906 / frequency_hz - 1;
    uint16_t dc_value = 7812 / frequency_hz;
    CCPR1L = dc_value / 4;
    CCP1CONbits.DC1B = dc_value & 0b11;    
    T2CONbits.TMR2ON = 1; // PWMスタート

    // LEDをPWM制御
    while(1) {
        // 100Hzから開始
        frequency_hz = 100;

        // 20Hzまで10Hzずつ減らしながら繰り返す
        while( frequency_hz >= 20 ) {
            // PWMの周期とデューティーサイクル設定
            PR2 = 3906 / frequency_hz - 1;
            uint16_t dc_value = 7812 / frequency_hz;
            CCPR1L = dc_value / 4;
            CCP1CONbits.DC1B = dc_value & 0b11;

            // PWM制御3秒間継続
            __delay_ms(3000);

            // LEDを消して1秒待つ
            CCPR1L = 0;
            CCP1CONbits.DC1B = 0;
            __delay_ms(1000);
            
            // 周波数を10Hz減らす
            frequency_hz -= 10;
        }
    }

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

PWMの次のステップ

この入門シリーズでは、LEDをPWM制御しました。

使用したLEDは単色ですので明るさの調整のみでしたが、フルカラーLEDという部品を使用すると様々な色の表示が可能になります。

フルカラーLEDは以下のようなものです。

5mmフルカラーLED(秋月電子通商)

フルカラーLEDは、単に赤色、緑色、青色の3つのLEDが1つのパッケーシに収められている部品です。赤、緑、青は光の三原色ですから、赤、緑、青のそれぞれをPWM制御して明るさを変えれば様々な色の表示が可能になります。


また、サーボモーターというモーターがあります。

サーボモーター(秋月電子通商)

通常のモーターは電圧を加えるとモーターの軸がぐるぐる回転しますが、このサーボモーターは0度〜180度の角度の範囲内で回転します。

用途としてはロボットアームやラジコンカーの前輪角度制御など、可動範囲の角度が限られている場合などです。

サーボモーターを制御するにはPWM信号を与えます。

例えばサーボモーターを制御する場合、次のような信号が必要になります。

  • PWMサイクル:20ms
  • 制御パルス:0.5ms 〜 2.4ms(0度〜180度に対応)

PWMの周期は20ms、デューティーサイクルを0.5msから2.4msまで変化させることにより、0度〜180度まで回転制御ができる、というわけです。

サーボモーターの制御信号は、PICマイコンの出力ピンを直接接続できますので、比較的簡単な回路で制御回路が実現できます。


いずれも、結局はPWMの周期とデューティーサイクルの制御を行うものです。

PWM制御の知識とアイデアがあれば、色々なものが作れるようになりますので、ぜひPWMをマスターしてみてください。

次回からA/Dコンバータの説明になります。

更新履歴

日付内容
2017.1.22新規投稿
2018.12.1点滅周波数の注意点追加
2025.5.9チャレンジ課題内容変更・解答例追加
通知の設定
通知タイミング
guest
0 コメント
新しい準
古い順 一番投票が多い
本文中にフィードバック
全てのコメントを見る
大日向幹夫
大日向幹夫
4 年 前

お世話になります。
WAKAZOU様への返信で
1) 細かく設定する方法で、この要領でCCPR1L、DC1Bも周波数freqで計算できると思います。
 と説明がありますが、CCPR1Lは理解できましたが、DC1Bの計算方法が分からないので
 ご説明お願い致します。

管理者
管理者
返信  大日向幹夫
4 年 前

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

デューティーサイクルは10ビットで設定する仕様ですが、PICマイコンのレジスタは8ビットです。レジスタ1つでは設定できませんので、PIC12F1822ではCCPR1Lの8ビット + DC1Bの2ビットを10ビットとして扱います。デューティーサイクルの値からCCPR1LとDC1Bの値を求める方法はこのシリーズの第3回の記事に詳しく説明しておりますので、ご確認いただければと思います。

https://tool-lab.com/pic-app-3/

大日向幹夫
大日向幹夫
返信  管理者
4 年 前

早速のご返信ありがとうございます。
PR2、CCPR1L、DC1B、周波数の相関関係をようやく理解することができました。デューティーサイクルと1クロック分の時間及び周波数の関係を混同していたのと、2進数と10進数を混同していました。コピペしていてもなかなか理解が進まづ、チャレンジ課題の応答等で大分参考になりました。今後ともよろしくお願いいたします。

wakazo
wakazo
7 年 前

こんばんは、自信がないので教えてください。
チャレンジ課題を考えはじめましたが、ひとまず周期だけ20Hzにして点灯・消灯してみようと思ったのですが私の実験環境,試作プリント基板搭載のPIC12F683では80Hzが下限(PR2=194,プリスケーラ16まで)になるとの計算になりました。点滅は見えてないのですが、プリスケーラ64が設定できる他のPICを使用しないと上記チャレンジ課題はできないのでしょうか?
それとも理解不足でしょうか?

ーーー 初期化、Config除く
//チャレンジ課題
//PWM HW 設定
CCP1CONbits.CCP1M = 0b1100; // PWM有効 Acitve High
T2CONbits.T2CKPS = 0b10; // プリスケーラ 1:16
PR2 = 194; // 周期 12.5ms
CCPR1L = 394/4; // dutycycle 50%(6.3ms/16) 上位8ビット
CCP1CONbits.DC1B = 394; // 下位2ビット

//LEDの点滅周期を変更
//デューティー比は50%固定
while(1){
//80Hz 5秒間点灯 1秒間消灯
T2CONbits.TMR2ON = 1; //PWM制御 LED GP2 ON
__delay_ms(5000);
T2CONbits.TMR2ON = 0; //PWM制御 LED GP2 OFF
__delay_ms(1000);
}

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

wakazoさま、
コメントどうもありがとうございます。

結論としては、wakazoさまのご理解で正しいです。
PWMの周期は

( (PR2) + 1 ) x 4 x 1クロック周期 x (TMR2プリスケーラ値)

ですので、20Hz周期にしたい場合、1周期は50msになりますが、この式によると最大の周期は、

( 255 + 1 ) x 4 x 1μs x 16 = 約16.4ms

となり、20msは実現できません。

このような場合ですが、解決策としては邪道と言われそうですが、クロック周期を長くすると対応できます。クロック周波数は1MHzにしていると思いますが、この場合の最大周期は16.4msですので、例えば、クロック周波数を250kHzにすると、クロック周期は4μsになります。例えば20ms周期にしたい場合、

( 77 + 1 ) x 4 x 4μs x 16 = 約20ms

ですので、PR2を77、プリスケーラを16にすれば約20msの周期を得ることができます。また、クロック周波数250kHzの状態で周期を2msにしたい場合は、PR2を30、プリスケーラを4にすれば近い値が得られると思います( (30 + 1) x 4 x 4μs x 4 = 1.984ms)。

ということで、チャレンジ課題としてはちょっと良問とは言い難いものになってしまいました。すみません。

PICマイコンにPWM制御機能が入っているのはいいですが、なんかこう、100%自分の思い通りになる、という感じではないですよね。人生も同じように思い通りになりませんが、PWM機能の場合はハードウエアで実現しているので仕方ないところもあります。PWM制御をする場合、プログラムでPWM信号を生成するか、PWMモジュールを使うか、目的に応じて使い分けられるといいと思います。

wakazo
wakazo
返信  管理者
7 年 前

ありがとうございます。
やってみます。 簡単ですが

wakazo
wakazo
返信  管理者
7 年 前

こんにちは、クロック周波数を250KHzに変更してチャレンジ課題をすすめてみました。60Hzから10Hzづつ下げて20Hzまで変更するようにしてみました。PWMの設定値を変更する作戦がよくわからずひとまず配列を使っていれてみました。


/*チャレンジ課題 PWM HW 設定 LEDの点滅周期を変更 60Hz –> 20Hz
デューティー比は50%固定*/

CCP1CONbits.CCP1M = 0b1100; // PWM有効 Acitve High
T2CONbits.T2CKPS = 0b10; // プリスケーラ 1:16
int i = 0;
int pr2[5] = {64,77,97,129,194};  //計算結果を配列に
int ccpr1L[5] = {33,39,49,65,99}; //計算結果を配列に
int ccp1con[5] = {13,156,192,260,391}; //計算結果を配列に

while(1){
for(i = 0; i < 5; i++){
PR2 = pr2[i];
CCPR1L = ccpr1L[i];
CCP1CONbits.DC1B = ccp1con[i];
//5秒間点灯 1秒間消灯
T2CONbits.TMR2ON = 1; //PWM制御 LED GP2 ON
__delay_ms(5000);
T2CONbits.TMR2ON = 0; //PWM制御 LED GP2 OFF
__delay_ms(1000);
}
}

2点質問させてください。
・もっと細かく変更したい場合、配列だと面倒なのですが何かスマートな方法はありますか?
・私の実験環境(PICが12F683で基本的な回路は同等)では、以上で動かすと5秒後にLEDが消えず、逆に1秒間明るく点灯してしまうことがあります。これはどのような状況なのか?

毎日こつこつ充実させ頂いてます。
ありがとうございます。

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

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

以前のご質問で初心者と書かれていましたが、ご質問内容を見ると中級者という感じで、かなり色々とチャレンジされてるんですね。色々と工夫されていらっしゃるで、ご自分で作りたいものが作れるようになってくると思いますよ。

ところで、ご質問の件の2番目については私の説明が悪かったようです。その内容も含めてご回答いたします。

1) 細かく設定する方法
 細かく設定したい場合ですが、配列で設定値を用意しておき、for文で順番に設定していくのはわかりやすい方法です。細かく設定したい場合は配列が大きくなりますので、PR2/CCPRL/DC1Bそれぞれをプログラムで計算して設定していくのがよいと思います。

 その考え方をご説明します。配列を使用したプログラムで、例えばpr2配列の1番目の要素(60Hzの設定値)は以下のように計算されたと思います。

(PR2 + 1 ) x 4 x 4μ x 16 = 1 / 60
を満たすPR2を計算する。
まず、上の式をμsに統一すると以下の式になります。
(PR2 + 1 ) x 4 x 4 x 16 = 1000000 / 60
この式から、PR2は
PR2 = 1000000 / (60 x 4 x 4 x 16 ) – 1 = 64 (少数切り捨て)

ということは、周波数を変数としてfreqとすると、PR2は

PR2 = 1000000 / ( freq * 4 * 4 * 16 ) – 1

となります。この要領でCCPR1L、DC1Bも周波数freqで計算できると思います。

次に、周波数を5Hz刻みで減らしていく場合、for文で変えていくのがよいと思います。具体的には以下のようになります。

for( freq = 60; freq >= 20; freq = freq – 5 ) {
// freqには周波数が入っているので、freqからPR2、CCPR1L、DC1Bを計算してそれぞれのレジスタに設定する
}

for文は、for(A, B, C)という書き方ですが、動作としては、最初の状態はAとして、Bが成立している間、Cを行いながら{}で囲った動作を繰り返します。

2) T2CONbits.TMR2ON = 0 にしてもLEDが消えない
 すみません、これは私の説明が悪かったです。PWM制御はタイマー2という内部モジュールが発生する信号を元にPWM信号を生成しています(タイマー2モジュールが生成する信号をカウントしながらピンのON/OFF を制御している)。この
T2CONbits.TMR2ONは、この元となる信号を生成するかしないかの設定になります。

TMR2ON=0にした場合、PWM信号はその時の状態で止まります。例えばPWM信号がONの時(= LEDが点灯している時)にタイマー2信号をOFFにすると、そこでPWM制御動作は停止して、LEDはONのままになってしまいます。また、TMR2ON=0にしたタイミングがたまたまLEDがOFFの時はLEDは消灯した状態で停止します。

ではどのようにすればいいかというと、PWM制御をしている場合、ON(LEDの100%点灯)、OFF(LED消灯)も含めてデューティー比の設定で制御します。ONにしたい場合はデューティー比100%、OFFにしたい場合はデューティー比0%にします。つまり、LEDを消灯したい場合、CCPR1L=0、CCP1CONbits.DC1B=
0にします。

TMR2ONの使い方がわかりづらいので、これを踏まえたプログラムは以下に示します。(インデントは無視されてしまいますので読みづらくなってしまいすみません…)

CCP1CONbits.CCP1M = 0b1100; // PWM有効 Acitve High
T2CONbits.T2CKPS = 0b10; // プリスケーラ 1:16
int i = 0;
int pr2[5] = {64,77,97,129,194};  //計算結果を配列に
int ccpr1L[5] = {33,39,49,65,99}; //計算結果を配列に
int ccp1con[5] = {13,156,192,260,391}; //計算結果を配列に

T2CONbits.TMR2ON = 1; //PWM制御生成開始

while(1){
for(i = 0; i < 5; i++){
PR2 = pr2[i];

//5秒間点灯
CCPR1L = ccpr1L[i];
CCP1CONbits.DC1B = ccp1con[i];
__delay_ms(5000);

//1秒間消灯
CCPR1L = 0;
CCP1CONbits.DC1B = 0;
__delay_ms(1000);
}
}

ということで、説明がかなり複雑になってしまいました。言葉だけでお伝えするのはなかなか難しいので、理解しづらいところがありましたらお手数ですがご質問いただければと思います。

wakazo
wakazo
返信  管理者
7 年 前

アドバイスありがとうございます。うまく動きました。
1つ教えてください。どうも、教えてもらった式ではうまくいったのですが、実は自分で作った計算式ではうまく動きませんでした。

もしかして、なにか基本的なことでしょうか?
PR2 = 1000000 / ( freq * 4 * 4 * 16 ) – 1;

PR2 = ((1/freq)*1000000)/256-1;

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

早速のご確認どうもありがとうございました。

ここまでくるとC言語の細かい話になってきましたね…

すみません、XC8コンパイラが生成した機械語の確認まではしていないのですが、おそらくこうだろう、ということでご回答いたします。

PR2 = ((1/freq)*1000000)/256 – 1;

という式ですが、おそらく最初に (1/freq)が計算されるのではないかと思います。この場合、特に指定はしていませんので、整数として扱われ、例えばfreqが60の時は、

1/freq = 1 ÷ 60 = 0
(※整数扱いのため少数は切り落とされる)

で計算されると思います。結果として、PR2 = -1 = 0xffになっているのではないかと思います。

少数が出る場合は(1/freq)は少数として扱ってもらうために、 (float)のキャストをつけて

PR2 = ( (float)(1/freq) * 1000000 ) / 256 – 1;

とすればよさそうですが、、、こちらでPIC12F1822で試そうとしたところビルドエラーになってしまいました。小数点計算はメモリを多く使用するので、無償モードでは対応していないというようなメッセージが表示されました。

このような計算は、途中に小数点が出ないような式にするとよいと思います。とはいってもこの辺りの話はC言語コンパイラの仕組みの領域に入ってくるので、式の途中の結果がなるべく小さい少数にならないように計算式を作るとよいと思います。

y = ( 1/x + 10 ) * 30;

とするのではなく、

y = 30/x + 300;

とするなど、途中に 1/x などの小さい少数の結果が出ないようにすればよいのではないかと思います。(すみません、この辺りまでくると私も自信がないです…)

wakazo
wakazo
返信  管理者
7 年 前

なるほど、わかりました。私もすぐに、はまる性質なのですが、キャスト?やfloatの使い方大変気になるところですが、ここは深追いしないで、次のADコンバータの実験をやってみます。

丁寧にありがとうございます。

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

気になるところがあると深追いしたくなりますよね。この先も深追いしたくなる場面がいろいろ出てくると思いますが、まずは全体を把握されるとよいと思います。また何か不明点ありましたらご質問いただければと思います。

目次