Q&A (電気電子)

【PIC】PICマイコンでPWM制御を行う方法(CCP-PWM機能を使う)を紹介する

今回はPIC16F1827について、CCPモジュールを使ってPWM出力を行ってみます。

PICのPWM機能は若干癖がある

マイコンの一般的な機能であるPWMについて、今回はPICマイコンでテストしてみます。

PICマイコンのPWM機能ですが、タイマのレジスタの制約により設定に若干の癖があります。

今回は簡単な使い方の紹介と、タイマ関連の詳細設定についてまとめてみたいと思います。

PWM制御のサンプルプログラム

まず最初に、PWM制御のサンプロプログラムを紹介します。

作成したプログラムの内容は下記です。

PIC16F1827の2ピン、RA3ポートからPWM信号を出力する

PMW制御の概要については下記の記事で説明したので参考にしてください。

マイコンが異なるのでレジスタ名など変わりますが、基本的な考え方は同じです。ネットにもたくさん文献があります。

PWMについて
【Arduiono】Arduinoでタイマ割込みを使用したPWM制御を行う方法を紹介する

今回はArduiono、いや、Arduinoに搭載されたマイコン「ATmega328P」のタイマ機能を使用してPWM制御を行ってみました。 備忘録。 ※本記事ではATmega328PBのデータシートを ...

続きを見る

一応、最小限の設定でPWM出力するプログラムになっていると、、、思います。

/* PIC16F1827 Configuration Bit Settings */

/* 'C' source line config statements */

/* 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 = ON       // MCLR Pin Function Select (MCLR/VPP pin function is MCLR)
#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 = ON        // Internal/External Switchover (Internal/External Switchover mode is enabled)
#pragma config FCMEN = ON       // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is enabled)

/* CONFIG2 */
#pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
#pragma config PLLEN = ON       // PLL Enable (4x PLL enabled)
#pragma config STVREN = OFF      // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will cause a Reset)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#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. */

/*==========================================================================================================================================*/



/*
 * File:   main.c
 * Author: ICE_MEGANE
 *
 * Created on 2021/04/2, 22:26
 */

#include <xc.h>



#define     CLEAR   0
#define     SET     1


typedef  unsigned char   UC;
typedef  unsigned short  US;
typedef  unsigned long   UL;


void main(void );
static void register_setup( void );
static void rs_ccp3_dutyset( US us_duty );


/**************************************************************/
/*  Function:                                                 */
/*  main task                                                 */
/*                                                            */
/**************************************************************/
void main(void)
{   
    register_setup();

    while(1)
    {
        //rs_ccp3_dutyset( (US)1023U );       /* duty100% */
        rs_ccp3_dutyset( (US)512U );          /* duty50% */
    }
}


/**************************************************************/
/*  Function:                                                 */
/*  register setup                                            */
/*                                                            */
/**************************************************************/
static void register_setup( void )
{
    /* Oscillator setuo */
    OSCCON =  0x70;         /* |1|1|1|1|-|1|#|0|0| */                        
    OSCTUNE =  0x00;        /* |#|#|0|0|-|0|0|0|0| */
    /* Option register setup */
    //OPTION_REG =  0xC8;     /* |1|1|0|0|-|0|1|1|1| */
    /* Alternate pin function setup */
    APFCON0 =  0x08;        /* |0|0|0|0|-|1|0|0|0| */
    APFCON1 =  0x00;        /* |x|x|x|x|-|x|x|x|0| */
    /* Interrupt setup */
    INTCON =  0x20;         /* |0|0|1|0|-|0|0|0|0| */
    PIE3 = 0x00;            /* |x|x|1|1|-|1|x|1|x| */
    /* Port setup */
    TRISA = 0x00;           /* |0|0|0|0|-|0|0|0|0| */
    /* Timer4 setup (Timer4 assigned to CCP3) */
    TMR4 = 0xFF;
    T4CON = 0x07;           /* |x|0|0|0|-|0|1|1|1| */
    PR4 = 0xFF;             /* Timer4 Overflow val (PWM period) setup : 8bit MSB */
    /* CCP setup */
    CCPTMRS = 0x93;         /* |1|0|0|1|-|0|0|1|1| */
    CCP3CON = 0x0C;         /* |0|0|0|0|-|1|1|0|0| */
    CCPR3L = 0x00;          /* |0|0|0|0|-|0|0|0|0| */
    PIR3 &= 0xEF;           /* |x|x|x|0|-|x|x|x|x| */
}


/**************************************************************/
/*  Function:                                                 */
/*  main task                                                 */
/*  Set CCP3 module duty                                      */
/**************************************************************/
static void rs_ccp3_dutyset( US us_duty )
{
    CCPR3L = (UC)( us_duty >> 2 );     /* 0b0011-1111-1111 -> 0b0000-1111-1111-(11) */

    us_duty &= 0x0003;
    us_duty = us_duty << 4;
    
    CCP3CON &= 0xCF;                    /* DCxB Clear */
    CCP3CON |= (UC)( us_duty );         /* 0b0000-0000-0011 -> 0b0000-0011-0000 */
}


このプログラムを実行した場合、下記のような波形となります。

この波形から、下記の2点が確認できます。

・パルスのキャリア周波数は488.68kHz
・パルスのon-dutyは50.004%

パルスのdutyは下記の関数で指定しています。

static void rs_ccp3_dutyset( US us_duty )
{
    CCPR3L = (UC)( us_duty >> 2 );     /* 0b0011-1111-1111 -> 0b0000-1111-1111-(11) */

    us_duty &= 0x0003;
    us_duty = us_duty << 4;
    
    CCP3CON &= 0xCF;                    /* DCxB Clear */
    CCP3CON |= (UC)( us_duty );         /* 0b0000-0000-0011 -> 0b0000-0011-0000 */
}

少し詳細な説明

これ以降は細かい説明をしていきたいと思います。

レジスタ設定について、上から順に追っていきます。

PICマイコンの内蔵発振器の周波数設定

まずはPICマイコンで計算速度を決めるクロック周波数を設定します。

マイコンにはクロック発生源を外部に用意し、外から矩形波上のパルスを入力して計算を行う場合と、マイコンに内蔵された周波数発振器を使用して計算を行うタイプがあります。

PIC16F1827は両方を選ぶことができ、今回は部品点数の少ない内蔵発振器を使用した方で設定を行います。

設定内容は下記になります。

/* CONFIG1 */
#pragma config FOSC = INTOSC    // Oscillator Selection (INTOSC oscillator: I/O function on CLKIN pin)

/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
    OSCCON =  0x70;         /* |1|1|1|1|-|1|#|0|0| */                        
    OSCTUNE =  0x00;        /* |#|#|0|0|-|0|0|0|0| */

まずはブロック図を確認し、設定の全体像を把握します。

PIC16F1827のクロック選択は、データシートの55ページの下記の図で示されます。

赤色の矢印で示したルートが、今回行う設定のルートです。

次に文章を確認し、レジスタ設定の詳細について調べていきます。

データシートの59ページには下記のような記載があります。

この文章から、OSCCONレジスタのIRCFbit(3-0)を設定することで、HFINTOSC(16MHz)の高速内蔵発振クロックを使用できるとわかります。

また他に、FOSC(2-0)ビットを100にするorOSCCONレジスタのSCSビットを「1x」にする必要があると、記載があります。

また60ページには、下記のような記載があります。

この文章から、HFINTOSC(High Frequency INternal OSCillator:高速内蔵発振器)の設定が32MHzから31kHzの中で選択できるとわかります。

また今回設定する一番高速の32MHzを使用する場合、4倍のPLL機能を使用する必要があると、記載があります。

4倍のPLLについては、先ほどのブロック図中の下記の部分で、8MHzの部分から分岐して「4xPLL」へつながるルートに記載があります。

PIC16F1827では4倍のPLL機能を使うことにより、8MHzのクロックを4倍した32MHzの高速クロックへ変換することが可能です。

大元の発振器が16MHzでそれをPostscalerで分周して8MHzに、それを4倍して32MHzに、、、と、若干回りくどいようですが、マイコンの設計上の都合、もともとは16MHz~32kHzの11パターンの分周周波数を選ぶようにしか設計してなかった等、いろいろあるのかな~と思います。
(とりあえずブロック図に従って設定していけばOKです。)

↓↓↓データシートは読み込めたので、次は実際に設定を行っていきます。↓↓↓

まずは内蔵発振器に「HFINTOSC」を選択し、「4xPLL」を有効にする設定を行っていきます。

PICマイコンでは基本的なレジスタ設定は代入形式で実施しますが、Configuration Word などの一部の初期設定については、#pragmaセクションで実施することも可能です。(#pragmaセクションでの設定が基本的に高優先度・・・)

MPLABの場合、#pragma関連の設定は、MPLABの「Set Configuration Bits」からコードを生成することが可能です。

下記のように、プロジェクトの「Production」から選択します。

次に表示される画面では、下記のように設定します。

設定が完了したら下記の「Generate Source Code to Output」ボタンを押し、表示されるコードをコピーします。

その後、main.cの#includeなどの定義より上の、一番上に張り付けます。

次にOSCCONレジスタの設定を行っていきます。

OSCCONレジスタは下記のように8bitのレジスタになっています。

bit:7 では、PLLの動作許可/禁止設定を行います。

今回は先ほど#pragmaセクションでPLLを有効にしているので、ここでの設定に寄らず4xPLLは常に動作許可になります。

bit:6 では、内蔵発振器の16MHzを分周する、PostScallerの設定を行っていきます。

今回は先のブロック図より、8MHzの出力を4xPLLにつないで・・・という使い方をするので、「1110」を設定します。

bit:2 では特に設定用にビットが割り当てられていないため、0にしておきます。

bit:1、0 ではシステムクロックの設定もとを登録します。

今回は「Internal osscilator block」を使用するので、bit1を1に設定しておけばOKです。bit0はどっちでも可。

#pragmaセクションにて、FOSCはINTOSCにすると設定したので、「00:FOSCでの設定に依存」にしておいてもOKです。

次に、OSCTUNEレジスタを設定します。

OSCTUNEレジスタでは内蔵発振器の周波数に対し、若干の補正を行うことができます。

内蔵クロックを使用する時点で数%ずれるので、どこまで正確に設定できるかが微妙なところですが、工場出荷時の設定外でも、ユーザーが調節できるようになっています。

細かい修正が希望であれば、任意で値を設定します。

以上でPICマイコンの内蔵発振器周りの設定は完了です。

ポート割り当てを設定する ※必要に応じて

次に、ポート割り当て機能を設定していきます。

    /* Alternate pin function setup */
    APFCON0 =  0x08;        /* |0|0|0|0|-|1|0|0|0| */
    APFCON1 =  0x00;        /* |x|x|x|x|-|x|x|x|0| */

マイコンには通信やタイマ、PWMなどの様々な機能がありますが、基本的にそれらの特殊機能が使用できるのは特定のピンのみになります。

しかし、マイコンによってはこれらの機能をどのピンに設定するかを、ユーザーが選択できるものがあります。

PIC16F1827では、APFCON0、APFCON1レジスタを設定することにより、これらの特殊機能の割当先を決めます。

今回はPWM出力を「CCP3」という機能を使って実現するのですが、CCP3の出力はRA3=2番ピンからしか出力できません。

よってAPFCON0、APFCON1レジスタでの設定は影響しません。

CCP1、CCP2を使用する場合は設定が必要です。

割込み許可の設定 ※必要に応じて

次に、割込み許可の設定を行っていきます。

が、これについては必要に応じて設定すればよく、タイマ機能を使ったPWM出力をする分には割込みを許可しなくても問題ないです。

あくまでCPUへの割込み命令を実施し、PWMのキャリアやdutyのON/OFFの切り替わりで処理を実施したい場合だけ設定します。

    /* Interrupt setup */
    INTCON =  0x20;         /* |0|0|1|0|-|0|0|0|0| */
    PIE3 = 0x00;            /* |x|x|1|1|-|1|x|1|x| */

ちなみにですが、PWMのタイマの一致割込みを発生させると、スタックオーバーフローして無限にリセットがかかり続けます。(なので今回はdutyのON/OFFの切り替わり時の割込みは無効にしてます。)

Midrangeということもあり、あまり処理の速いマイコンではないため、高速で割込み要求しすぎると多重になっていつまでも最初の要求が処理されずあふれる・・・ようです。

ポートの入出力設定

次はポートの入出力設定を行います。

/* Port setup */
    TRISA = 0x00;           /* |0|0|0|0|-|0|0|0|0| */

PIC16F1827はRAポートとRBポートがあり、各ポートの入出力設定を「TRISA」「TRISB」レジスタで設定することが可能です。

PIC16F1827では、「1:入力 / 0:出力」となっています。

一部のポートは入力、出力が固定(0や1を設定しても切り替え不可能)なので注意が必要です。

余談ですが、「TRIS-A」のTRISは、「Tri - state A port」の略です。
「Tri - state」の「Tri」は、ポートの状態が入力、出力、Hi-Zの3状態を持つため、このように命名されているっぽいです。

今回は「RA3」ポートからPWM信号を出力して波形を確認しているため、「TRISA3」を「0」に設定します。

最初に述べ忘れましたが、基本的にレジスタ関連は 0bit からカウントし、8bit長のレジスタの場合は 0 - 7 bit で指定します。
(0始まりです。)

タイマの設定

次はPWM出力に使用するタイマの設定を行っていきます。

    /* Timer4 setup (Timer4 assigned to CCP3) */
    TMR4 = 0xFF;
    T4CON = 0x07;           /* |x|0|0|0|-|0|1|1|1| */ 

PIC16F1827にはTimer0、Timer1、Timer2、Timer4、TImer6の計5つのタイマーが搭載されています。

各TImerはそれぞれ、下記の表で示された機能を使用することができます。

今回はPWM機能を使おうとしているため、使用できるタイマは2、4、6の3つに限られます。

今回は例としてTImer4を使用することにします。

まずはブロック図から確認してみます。Timer2、Timer4、Timer6の構成は下記のようになっています。

Timerは基本的に設定したクロック周波数で0、1、2、...といった具合にカウントアップしていく機能になります。

上記の図を順を追って言葉にすると、

・Fosc/4 の周波数を、
・Prescalerで分周し、
・分周した周波数で0からカウントアップし、カウント中の値はTMRxレジスタに入る (≒TMRxレジスタでカウントアップ)
・PRxレジスタで設定した値とTMRxの現在のカウント値が一致したら、「Reset」を発生させてTMRxレジスタをゼロクリアする
・一致した際にイベントを起こしたい場合は、TMRxIFフラグを使用する
・一致した際のイベントを何回かに1度にしたい場合、Postscalerを設定して分周する

となります。

このブロック図中の設定は、「TxCON」レジスタで行います。xにはTImer2,4,6のいずれかの数字が入ります。

TxCONレジスタについて、設定内容を見ていきます。

bit:7は機能割り当てなしです。

bit:6, 5, 4, 3 では、TImerxの割込みをどのくらい分周するか、Postscaler を設定します。

注意点ですが、Postscalerはあくまで「割込みの発生回数を変える」だけであり、タイマの出力や動作には影響しません。

bit:2 では、Timerxの動作状態を設定します。

bit:1では、タイマのカウントアップ速度を決めるPrescalerを設定します、先ほどのPostscalerとは別物です。

Prescalerは4種類から選べます。

分周のイメージは下記のようになります。矢印が付いたタイミングで0→1→2→・・・といった具合にカウントアップを行いますが、分周によってタイミングが間引きされ、カウントアップの速度が遅くなります。

ここで、なんでカウントアップの速度を調節する必要があるのかについてですが、それぞれのタイマはカウントできる数に上限があるためです。上限に到達してしまうと、0にもどって再度カウントアップを始めてしまいます。

PRxレジスタでは、カウントアップに使用するタイマがオーバーフローする数値を8ビットで設定します。

PRxレジスタについてはCCPモジュールと合わせて説明した方がわかりやすいため、後述します。

CCPモジュールの設定

次はPWM出力を制御する周辺機能である、「CCPモジュール」を設定していきます。

    PR4 = 0xFF;             /* Timer4 Overflow val (PWM period) setup : 8bit MSB */
/* CCP setup */
    CCPTMRS = 0x93;         /* |1|0|0|1|-|0|0|1|1| */
    CCP3CON = 0x0C;         /* |0|0|0|0|-|1|1|0|0| */
    CCPR3L = 0x00;          /* |0|0|0|0|-|0|0|0|0| */
    PIR3 &= 0xEF;           /* |x|x|x|0|-|x|x|x|x| */

CCPとは、「Capture」「Compare」「PWM」の3つの動作モードの頭文字をとった文字列です。

ざっくりですが、CCPモジュールでは下記のような動作が実現できます。

・Capture:マイコンのピンに矩形波を入力した場合、H->L、L-Hに切り替わる間隔の時間(パルス幅)を測る
・Compare:マイコンであらかじめ決めた時間が来たら、出力を変化させたりする
・PWM:PWM出力を行うことができる

今回はPWMについて説明します。

PWM動作における各レジスタの設定は、211ページに記載された下記のブロック図の関係になっています。

タイマの設定で説明した内容と一部被るのですが、PIC16F1827においては下記のような流れで設定を行い、PWM出力を実現します。

・タイマのカウントアップ速度を設定する
・PWMを制御するCCPモジュールで使うタイマを指定する
・PWMのキャリア周期をPRxレジスタに、dutyをCCPRLレジスタとCCPxCONレジスタのbit5,4に設定した値で制御する

今回のサンプルプログラムの例では、タイマにはTImer4を使用し、CCPモジュールを使ってPWM制御を行います。

ざっくりしたイメージですが、時間を測るのはタイマ、PWMのキャリア周期やパルス幅を制御するのはCCPモジュール、そんなところです。

CCPモジュールで使うタイマを設定する

まずはCCPモジュールで使用するタイマを決定してきます。

PIC16F1827には1から4の4つのCCPモジュールが搭載されています。

「CCPTMRS」レジスタでは、それぞれのCCPモジュールでどのタイマを使用するかを割り当てていきます。

今回はTimer4を使って時間を測り、CCP3を使用してPWM制御を行いたいので、C3TSEL1,0ビットに「01」を設定しました。

使わないものは適当に設定しておけば大丈夫です。

PWMのキャリア周波数を設定する

各レジスタの設定値と出力されるPWM波形のキャリア周期(Period)、ON-duty(Pulse Witdh)は下記の図で示されます。

PWMの設定では周波数、dutyが所望する値になるように調整を行っていきます。

まずはPWMのキャリア周波数を設定していきます。

PWMのキャリア周波数は、PRレジスタに設定した値をもとに、下記の式で決定されます。

言葉にして説明してみると、

・PWM周期は、
・PRxレジスタに設定した値+1の値を、
・4倍し、
・それにTosc(今回はTosc=1/32MHzです)をかけ、
・TMRxのカウントで設定した分周比を書けた値

ということになります。

例えば今回は、「PRx=0xFF」に設定しています。よってPWM周期(PWM Period)は、下記のようになります。

最初に示した波形をもう一度確認してみると、上記のPWM周波数と一致していることがわかります。

PWMのON-dutyを設定する

PWMのON-dutyは、下記の式で求めることができます。

ON-dutyを決める式なのですが、少しややこしいことになっています。

上記の記述を言葉にしてみると、

・PWMのパルス幅(ON-duty)は、
・CCPRxLを上位8bit、
・CCPxCONレジスタの5,4bit目を下位2bitとした合計10bitの値で設定し、
・これにTosc(=1/32MHz)をかけ、
・TMRxのタイマのカウントアップで設定した分周比をかけた値、

となります。

ここで注意しなければならない点は、dutyを決めるために複数のレジスタを操作する必要があること、PWMのキャリア周期はPRxレジスタ1つで8bitの指定だったのに対し、ON-dutyは8+2の合計10bitで指定する必要がある、という点です。

基本的に上記の式に沿ってdutyを決めれば問題はないですが、カウントアップについて考える際は注意が必要です。

これについて、少し細かめに説明してみます。

PRxレジスタとCCPRxL+CCPxCONの2bitの関係について

先ほど示したCCPモジュールのブロック図を再度下記に示します。

上記の「Note1」に着目してみます。記載内容は下記の通りです。

TMRxレジスタ(2は誤記?)は、内部の2bitのシステムクロック(Fosc)に接続され、全体として10bitのタイムベースを構成する。

つまり、設定できるのは全体のうちの8bitだけど、下位には2bitのクロック状態があって、実際には10bitで動いているよ!ということです。

dutyは10bitで直接指定できるのに、キャリア周期は10bitのうちの上位8bitしか指定できない、、、何とも言えない感じです。。。

この点についてわかりやすくするために、PRx、内部の2bitのカウント、CCPRxL、CCPxCON<5:4>へ小さい数値を設定し、それぞれのレジスタで設定可能な範囲を図示してみると下記のようになります。

多少、わかりやすく示せたのではないかなと、思います。(かえってわかりずらかったらスマンヌ)

キャリア周期では上位8bitのみを設定しているというよりも、
ON-dutyがCCPxCONの5,4bitを拡張して10bitにできているだけ、という認識の方が理解しやすいような気もします。

なんかこの構成メリットあるんでしょうか・・・と、思わざるを得ないですね・・・・

残りはPIR3レジスタの設定ですが、これはCCP3の割込みを発生させるか否かを決めます。

タイマ出力を使い、ポートからPWM出力を出すだけであれば許可/禁止はどちらでも問題ありません。

長々と書いてしまいましたが、以上でPWM出力を行うための設定は完了です。

メイン関数

最後にPWM出力を実行するプログラムが動作する、メイン関数を見ていきます。

/**************************************************************/
/*  Function:                                                 */
/*  main task                                                 */
/*                                                            */
/**************************************************************/
void main(void)
{   
    register_setup();

    while(1)
    {
        rs_ccp3_dutyset( (US)512U );          /* duty50% */
    }
}

やっていることは単純です。

マイコン起動後にプログラムカウンタが最後まで行って停止しないよう、main()関数の中に無限ループを設け、そこでdutyを指定する関数を呼び出しています。

dutyを指定する関数は下記のようなものを用意してみました。

引数には2byteのshort型を用意(typedef US unsigned short)し、受け取った値の上位8bitをCCPR3Lに、下位2bitCCP3CONの5,4bitの位置までビットシフトして代入しています。

割とシンプルかなと思います。

/**************************************************************/
/*  Function:                                                 */
/*  main task                                                 */
/*  Set CCP3 module duty                                      */
/**************************************************************/
static void rs_ccp3_dutyset( US us_duty )
{
    CCPR3L = (UC)( us_duty >> 2 );     /* 0b0011-1111-1111 -> 0b0000-1111-1111-(11) */

    us_duty &= 0x0003;
    us_duty = us_duty << 4;
    
    CCP3CON &= 0xCF;                    /* DCxB Clear */
    CCP3CON |= (UC)( us_duty );         /* 0b0000-0000-0011 -> 0b0000-0011-0000 */
}

まとめ

今回はPIC16F1827でタイマ、CCPモジュールを使用してPWM出力を行うサンプルプログラム、データシートとの対応の説明などをやってみました。

結構書くのがめんどくせぇ。。。。

残りのCaptureとCompareも後日まとめてみます。

それでは、また。

-Q&A (電気電子)
-,