【演習】A/D変換とPWM出力の連動

PWM出力の使い方

PWM設定方法

PWM出力を実現する方法として、CCP1/CCP2という機能でPWM出力する方法と、PWM3/PWM4という機能でPWM出力する方法があります。

両者の違いは以下になります。

項目CCP1 / CCP2PWM3 / PWM4
モジュールCCPPWM専用
Capture○×
Compare○×
PWM○○
PWM分解能最大10bit最大10bit
Duty設定10bit10bit
PWMタイムベースTimer2/4/6系Timer2/4/6系
PPSによる出力○○
単純なPWM用途○◎
将来Capture/Compareに転用○×

単純にPWM出力をするだけであれば、PWM3/PWM4という機能で十分です。
CCP1/CCP2は、CaptureやCompareという機能を併用することが可能です。

CCP1/CCP2は、以下の様な機能と連動させてPWM出力ができます。
外部信号のエッジ時刻を取り込む → Capture
Timer値と比較してイベントを発生させる → Compare
上記の機能と連動させてPWMを生成する → PWM

今回はそこまでの機能連動は不要なので、PWM3/PWM4の単純にPWM出力するだけの機能を使いたいと思います。

PWM3機能でのPWM出力

今回はRC0が空いているので、このピンをPWM出力ピンへ設定してみましょう。

デフォルトでは、RC0のピンはI/Oポートとなっています。
これをPWM機能として使う場合、I/Oポートから機能ポートへ変更する設定が必要になります。
その設定を行うのが以下のレジスタになります。

RxyPPSというレジスタ名のxyには、ポート名が入ります。
今回ですと、RC0に割り当てしたいので、RC0PPS というレジスタ名として設定を行います。
実際の設定値は青字記載の通り、Table 13-2 を参照します。

赤枠で囲った部分が設定したい機能であるPWM3_outです。
最左列が設定値になり、2進数で記載されています。
なので、レジスタ名RC0PPSに01110を設定すれば、RC0ポートはPWM3出力機能に割り当てされます。

PWM周波数の設定

現在の演習では、マイコンの駆動設定は以下になっています。
内蔵発信クロック周波数:16MHz

今回はPWMを以下に設定してみましょう。
PWM周波数:1KHz
DUTY:50%
周波数とDUTYを設定するタイマ:Timer2

PWM周波数を設定する公式は以下になります。

内蔵発信周波数(FOSC)が16MHzなので、PWM出力のクロック源は以下のレジスタの赤枠を設定する必要があります。
T2CLKCONレジスタのCS<3:0>を0000に設定します。

PWM周期を1KHzにするには、公式内のPR2の値調整が必要となります。
PR2というのは、Timer2のプリスケーラ―になります。

厳密には仕様書に掲載された公式に準じて計算をする必要がありますが、目安となる設定値が一覧になっており、仕様書に掲載されています。

ちょうどの設定組み合わせがないので、1.22KHzを参考に近い値を目安にしながら計算をしてみます。

1KHzの周波数にするために、以下の赤枠部であるプリスケーラ―を1:16にしたいので、
T2CONのCKPS<2:0>を011に設定。
Timer2をカウント開始する為に、ONを1に設定とします。

プリスケーラ―を1:16にしたので、この設定でPR2レジスタの値をセットします。
PR2は以下の公式を変換して求める値を計算します。

PWM周波数の公式を変換するとPR2は以下で計算ができます。

TOCSは2つ上の図内のNoteにも記載がありますが、 1/FOSCです。
FOSCは内蔵発信周波数 16MHzとなりますので、1/16MHzです。

DUTYの設定

DUTYの設定は以下の公式で値を算出します。

TOSCは1/16MHz = 62.5nsとなります(仕様書にも1/FOSCと書かれています)
Prescalerは、Timer2のプリスケーラ―値となり、1:16の設定をしたので16になります。

PWM3DCHとPWM3DCLとは以下の仕様書抜粋のレジスタの通り、
PWM3DCHは<7:0>となっているので8bit。
PWM3DCLは<7:6>となっているので2bit。
両方合わせて10bitの値を設定する必要があります。

10bitなので0~1024の範囲で設定することになります。
Dutyを50%にしたいので、どの様な値を設定すればよいかはPWMの周期に対して10bitの値を当てはめた場合を計算する必要があります。

今回、PWM周期を設定するのにPR2に249を設定しました。
このPR2を用いてPWM周期に対して、DUTYの設定値を当てはめる為に以下の計算式で算出します。

4 (PR2 + 1) = 4×(249 + 1) = 1000

今回のPWM周期に対して設定するDUTYの値が取り得る範囲は、0~1000となります。
DUTY 50%に設定したいならば、以下になります。

1000 × 0.5 = 500

この500をPWM3DCHとPWM3DCLレジスタへ10bitの値として設定します。
2進数にすると以下になります。

500 = 01111101 00

PWM3DCHは上位8bitを設定し、PWM3DCLには下位の2bitを設定します。

PWM3DCH = 0x7D; //2進数なら 0111 1101
PWM3DCL = 0x00; //2進数なら 00

念のためにPWMのHIGH時間を計算で確認してみましょう。
PWM周波数は今回1KHzに設定しました。

その為、1周期の時間は、1/1KHz = 1msとなります。
50%のDUTYにしたいので、500usのHIGH時間、500usのLOW時間であれば正しいことになります。

PWM3DCHとPWM3DCLレジスタへ設定したのは500でした。
PWM周期に対して、DUTY50%の時間は以下で計算します。

500×TOSC×Prescaler = 500 × 62.5ns × 16 = 500us

計算上はきちんと500usとなり、1msの50%の値になりましたので問題なさそうです。

PWMには、出力極性という概念があります。
HIGH状態をActiveとするのか、LOW状態をActiveとするのかを設定することができます。

PWMxPOLというbitが極性の設定になります。
1を設定すれば、LOWがActive
0を設定すれば、HIGHがActiveとなります。

Activeとは以下の様なイメージを指します。
HIGHがActiveのイメージ

              ┌───┐
              │      │
───────┘      └──────────────
                ←25%→

LOWがActiveのイメージ

───────┐      ┌──────────────
              │      │
              └───┘
                ←25%→

この様に極性を設定することができます。
今回の様に50% DUTYだと、どちらも均等時間になるため意味がないものですが、
DUTY割合によって必要性が出てきますので注意が必要です。

同様にPWMxEN bitを1に設定して機能を有効化しておく必要があります。

ここまでの内容をプログラムにしてみましょう。
これまでの演習と同様に、#pragma、#includeの部分は過去の演習で設定している内容をそのまま使用してください。

#include <xc.h>

#define _XTAL_FREQ 8000000UL

void PWM3_Init(void)
{
    /*----------------------------------------
     * RC0 : PWM3 output
     *---------------------------------------*/

    // RC0を一旦入力にしてPWM設定中の出力を止める
    TRISCbits.TRISC0 = 1;

    // RC0をデジタルI/Oとして使用
    ANSELCbits.ANSC0 = 0;

    // PWM3 -> RC0
    // PPS設定済みなら、この行は不要
    RC0PPS = 0x0E;


    /*----------------------------------------
     * PWM3停止
     *---------------------------------------*/

    PWM3CON = 0x00;


    /*----------------------------------------
     * PWM3のタイマにTimer2を選択
     *---------------------------------------*/

    CCPTMRSbits.P3TSEL = 0x00;


    /*----------------------------------------
     * Timer2設定
     *
     * FOSC = 16MHz
     *
     * TOSC = 1 / 16MHz
     *      = 62.5ns
     *
     * PWM Period
     *   = (PR2 + 1)
     *     × 4
     *     × TOSC
     *     × Prescaler
     *
     * Prescaler = 1:16
     *
     * 1ms
     *   = (PR2 + 1)
     *     × 4
     *     × 62.5ns
     *     × 16
     *
     * PR2 = 249
     *---------------------------------------*/

    T2CLKCON = 0x00;       // Timer2 Clock = FOSC/4

    T2HLT = 0x00;          // Free-running mode
    T2RST = 0x00;

    TMR2 = 0x00;           // Timer2 counter clear
    PR2  = 249;            // PWM = 1kHz


    /*----------------------------------------
     * Duty = 50%
     *
     * Dutyの最大値
     *   = 4 × (PR2 + 1)
     *   = 4 × 250
     *   = 1000
     *
     * 50%
     *   = 1000 × 0.5
     *   = 500
     *
     * 500 = 0x1F4
     *
     * PWM3DCH      = 上位8bit = 0x7D
     * PWM3DCL<7:6> = 下位2bit = 00
     *---------------------------------------*/

    PWM3DCH = 0x7D;
    PWM3DCL = 0x00;


    /*----------------------------------------
     * PWM3 Enable
     *---------------------------------------*/

    PWM3CONbits.POL = 0;    // Active High
    PWM3CONbits.EN  = 1;


    /*----------------------------------------
     * Timer2 Start
     *
     * CKPS = 100 : Prescaler 1:16
     * OUTPS = 0000 : Postscaler 1:1
     *---------------------------------------*/

    T2CONbits.CKPS  = 0b100;
    T2CONbits.OUTPS = 0b0000;
    T2CONbits.ON    = 1;


    /*----------------------------------------
     * RC0出力開始
     *---------------------------------------*/

    TRISCbits.TRISC0 = 0;
}

void main(void)
{
    // 内部発振16MHz設定
    OSCCON = 0x7A;   /* 内部発振周波数 16MHz 設定 0b01111010 */

    PWM3_Init();

    while(1)
    {
        // PWM3はハードウェアで自動出力
    }
}

これでPWM出力させたいピンから、周波数1KHzで50%DUTYの信号が出力されます。

A/D値に応じてPWM出力させる

前回の演習であるA/Dサンプリング処理と連動させてみましょう。

A/Dサンプリングした値によって、PWM出力させるDUTYを変える様なプログラムを作ってみたいと思います。
A/D入力部には、可変抵抗を実装して値が変化するようにしておきたいと思います。

VR1から電圧変化をA/Dで取り込み、その値に応じてRC0からPWM出力させるDUTYを連動変化させることが目的です。
TL1はオシロスコープなどでモニターするとVR1の可変に対してどの様な信号が出ているか確認することが可能です。

手順としては以下です。
①前回の演習でやったA/Dサンプリングのコードを用意する
②今回の演習でやったPWM出力のコードを用意する
③A/D値に対して、PWM周波数のDUTY値に変換する演算を考えて反映する

回答になりますが、以下にそのプログラムを記載します。

#include <xc.h>
#include <stdint.h>

#define _XTAL_FREQ 16000000UL

volatile uint16_t ad_value = 0;
volatile uint8_t  ad_complete = 0;

float vin = 0.0f;


/*==========================================================
 * PWM3 Duty設定
 *
 * duty : 0 ~ 1000
 *
 * PR2 = 249 のため
 * PWM周期のDutyカウント数
 *
 * 4 × (PR2 + 1)
 * = 4 × 250
 * = 1000
 *
 * duty =    0 →   0%
 * duty =  500 →  50%
 * duty = 1000 → 100%
 *=========================================================*/
void PWM3_SetDuty(uint16_t duty)
{
    if (duty > 1000)
    {
        duty = 1000;
    }

    // Duty上位8bit
    PWM3DCH = (uint8_t)(duty >> 2);

    // Duty下位2bit → PWM3DCL<7:6>
    PWM3DCL = (uint8_t)((duty & 0x0003) << 6);
}


/*==========================================================
 * PWM3初期化
 *
 * FOSC       = 16MHz
 * PWM        = PWM3
 * PWM出力    = RC0
 * PWM周波数  = 1kHz
 * Duty初期値 = 0%
 *
 * Timer2 Clock = FOSC / 4 = 4MHz
 * Prescaler    = 1:16
 *
 * PWM Period
 * = (PR2 + 1) × 4 × TOSC × Prescaler
 *
 * PR2 = 249
 *=========================================================*/
void PWM3_Init(void)
{
    // RC0を設定中は入力にしておく
    TRISCbits.TRISC0 = 1;

    // RC0をデジタル端子として使用
    ANSELCbits.ANSC0 = 0;

    // PWM3 → RC0
    // PPS設定済みなら不要
    RC0PPS = 0x0E;

    // PWM3停止
    PWM3CON = 0x00;

    // PWM3のタイマとしてTimer2を選択
    // CCPTMRS P3TSEL<1:0> = 00
    CCPTMRS &= 0xCF;

    // Timer2停止
    T2CON = 0x00;

    // Timer2 Clock = FOSC / 4
    T2CLKCON = 0x00;

    // Free Running Period mode
    T2HLT = 0x00;

    // 外部Reset未使用
    T2RST = 0x00;

    // Timer2カウンタクリア
    TMR2 = 0x00;

    // PWM 1kHz
    PR2 = 249;

    // 初期Duty = 0%
    PWM3_SetDuty(0);

    // PWM3 Enable
    // bit7 EN  = 1
    // bit4 POL = 0 : Active High
    PWM3CON = 0x80;

    // Timer2
    //
    // bit7   ON    = 1
    // bit6:4 CKPS  = 100 : 1:16
    // bit3:0 OUTPS = 0000 : 1:1
    //
    // 1100 0000 = 0xC0
    T2CON = 0xC0;

    // RC0出力許可
    TRISCbits.TRISC0 = 0;
}


/*==========================================================
 * ADC初期化
 *
 * RA4 = AN3
 * VREF+ = VDD
 * 10bit ADC
 *=========================================================*/
void ADC_Init(void)
{
    // 内部発振 16MHz
    OSCCON = 0x7A;

    // RA4 = AN3
    TRISAbits.TRISA4 = 1;
    ANSELAbits.ANSA4 = 1;

    // ADC設定
    ADCON1bits.ADFM   = 1;      // 右詰め
    ADCON1bits.ADCS   = 0b111;  // FRC
    ADCON1bits.ADPREF = 0b00;   // VREF+ = VDD

    // AN3選択
    ADCON0bits.CHS = 0b00011;

    // ADC割り込み
    PIR1bits.ADIF = 0;
    PIE1bits.ADIE = 1;

    // 周辺割り込み許可
    INTCONbits.PEIE = 1;

    // 全体割り込み許可
    INTCONbits.GIE = 1;

    // ADC ON
    ADCON0bits.ADON = 1;
}


/*==========================================================
 * main
 *=========================================================*/
void main(void)
{
    uint16_t pwm_duty;

    // クロック・ADC初期化
    ADC_Init();

    // PWM3初期化
    PWM3_Init();

    // 最初のAcquisition Time
    __delay_us(10);

    // 最初のA/D変換開始
    ADCON0bits.GO_nDONE = 1;

    while (1)
    {
        if (ad_complete)
        {
            ad_complete = 0;

            //----------------------------------------------
            // ADC値 → 電圧
            //----------------------------------------------
            vin = ((float)ad_value * 5.0f) / 1023.0f;


            //----------------------------------------------
            // ADC値 0~1023
            // ↓
            // PWM Duty 0~1000
            //
            // 0    →   0%
            // 512  → 約50%
            // 1023 → 100%
            //----------------------------------------------
            pwm_duty = (uint16_t)(((uint32_t)ad_value * 1000UL) / 1023UL);

            PWM3_SetDuty(pwm_duty);


            //----------------------------------------------
            // 次のA/D変換
            //----------------------------------------------

            // Acquisition Time
            __delay_us(10);

            ADCON0bits.GO_nDONE = 1;
        }
    }
}


/*==========================================================
 * 割り込み
 *=========================================================*/
void __interrupt() ISR(void)
{
    if (PIE1bits.ADIE && PIR1bits.ADIF)
    {
        PIR1bits.ADIF = 0;

        // 10bit ADC結果取得
        ad_value =
            ((uint16_t)ADRESH << 8)
            | ADRESL;

        ad_complete = 1;
    }
}