PIC24で出力コンペアを使用して,指定した周波数で矩形波を出力する

PIC24Fには、タイマモジュールを使用した出力コンペア機能があります。
出力コンペア機能には、以下の3つのモードがあります。

1.シングル比較一致モード
 
2.デュアル比較一致モード
        単一出力パルスモード
        連続出力パルスモード
 
3.単純パルス幅変調モード
        フォルト保護入力付き
        フォルト保護入力なし



今回はこの中で、2.デュアル比較一致モードの連続出力パルスモード機能を使用してみます。
この機能を使用すると、任意の周波数/デューティー比で矩形波を出力でき、矩形波の出力によってスピーカから音を出したり、モータのPWM制御を行えます。

今回の動作確認にはPIC24FJ32GA002を使用しました。



作業の流れ

出力コンペアを使用した矩形波出力のプログラムは、以下の手順で設定を行います。

コンフィグレーションビットの指定
クロックの定義
Timer2の設定
RPピンの役割マッピング設定
出力コンペアモジュールの設定



出力コンペア機能を使用するためには、どのピンで波形の出力を行うかをRPピンの役割マッピングで定義する必要があります。また、コンペア対象となる値をタイマーで作成するため、タイマーモジュールも使用します。タイマーのカウントスピードはCPUのクロック周波数にも依存するため、クロックやコンフィグの定義も確認しておきます。

それでは、各ステップの具体的な作業を順に確認していきます。


コンフィグレーションビットの指定

出力コンペア機能の使用有無に限らず、PICのプログラムを作成する場合は、PICの振る舞いを決めるためにコンフィグレーションビットの設定が必要です。

今回は以下の設定にしました。

#include <p24FJ32GA002.h>
 
//--------------------------------------
// コンフィグレーションビットの設定
//--------------------------------------
_CONFIG2(   POSCMOD_NONE  &       // Primary oscillator disabled
            IOL1WAY_OFF   &       // IOLOCK may be changed via unlocking seq
            OSCIOFNC_ON   &       // OSC2/CLKO/RC15 functions as port I/O (RC15)
            FCKSM_CSDCMD  &       // Clock switching and Fail-Safe Clock Monitor are disabled
            FNOSC_FRC     &       // Fast RC Oscillator (FRC)
            WUTSEL_LEG            // Legacy Wake-up Timer
        );
 
_CONFIG1 (  FWDTEN_OFF &          // Watchdog Timer is disabled
            ICS_PGx1   &          // Emulator EMUC1/EMUD1 pins are shared with PGC1/PGD1
            COE_ON     &          // Reset Into Clip On Emulation Mode
            BKBUG_ON   &          // Device resets into Debug mode
            GWRP_OFF   &          // Writes to program memory are allowed
            GCP_OFF    &          // Code protection is disabled
            JTAGEN_OFF            // JTAG port is disabled
         );



コンフィグレーションビットでは、FNOSC_FRCで内部オシレータを使用させています。
ほかの定義は、今回のプログラムの本質ではないので説明は省略します。


クロックの定義

次にクロックの定義をします。
今回は、PIC内蔵のオシレータを8MHzで使用します。

    //------------------------------------
    // クロックの定義
    //------------------------------------
    OSCCONbits.COSC  = 0; // CPUクロックにFast RC Oscillator (FRC)を使用
    CLKDIVbits.DOZE  = 0; // クロックのレート1:1(postscaler)
    CLKDIVbits.RCDIV = 0; // 8Mhz(prescaler)
    // OSCTUNbits.TUN = 0; // クロックの誤差微調整 0:調整なし
    // OSCCONbits.SOSCEN = 1;// セカンダリオシレータ(32kHz)を有効にする



内部オシレータを使用するには、_CONFIG2()でFNOSC_FRCの定義が必要です。
今回は使用しませんが、オシレータの個体差によるずれを微調整するためのレジスタとしてOSCTUN.TUNが有ります。

Timer2の設定

出力コンペアモジュールは内部的にタイマモジュール(TMRn)を使用します。
このタイマーの周期にしたがって波形が出力されるため、この値は出力される周波数に関係します。

    //------------------------------------
    // Timer2の設定
    //------------------------------------
    PR2             = 4000; // Timer2の最大値(この値でTMR2を0にリセットする)
    T2CONbits.TCKPS = 0;    // Timer2 プリスケーラ 0:1/1, 1:1/8, 2:1/64
    T2CONbits.TON   = 1;    // Timer2 ON



出力コンペアを使用する場合、元にするタイマはTimer2かTimer3のどちらかが使用できますが、今回はTimer2を使うことにします。タイマモジュールも幾つかの動作モードが有りますが、今回は16bitタイマのモードを分周せずに使う事にします

16bitモードなので、最大では0~65535までカウントさせる事が出来ますが、今回は説明を簡単にさせるために、4000までカウントしたらリセットさせる事にします。
タイマーの最大値はPR2レジスタにセットします。

値をセットした後、T2CONレジスタのTONを1にするとカウンタが動作します。
デバッガやシミュレータで、ステップ実行させてTMR2レジスタを観察すれば値が増えていく事がわかります。

RPピンの役割マッピング設定

タイマが用意できたら、次は波形を出力させるピンを決めます。
PIC24FJxxGA002では、出力コンペア結果の出力先をRP0~15のピンに割り当てる事が可能です。

今回は、RP5のピンに割り当てる事にしました。

    //------------------------------------
    // RPピンの役割マッピング設定
    //------------------------------------
    RPOR2bits.RP5R = 18;   // RP5ピンの機能を, 18:OC1に割り当てる
//  RPOR3bits.RP6R = 19;   // RP6ピンの機能を, 19:OC2に割り当てる


RPxの出力ピン機能割り当てはRPORxレジスタにFunctionCodeをセットすることで設定します。
RP5の機能設定はRPOR2レジスタのRP5R、出力コンペア1(OC1)のFunctionCodeは18なので、上記のコードとなります。

コメントアウトされている行では、 RP6ピンの機能を19:OC2に割り当てています。
PIC24FJxxGA002では、出力コンペアモジュールが5個内蔵されているので、18:OC1~22:OC5のFunctionCodeが指定できます。



出力コンペアモジュールの設定


次は、いよいよ出力コンペアモジュールの設定です。
ここまでの設定でOC1,Timer2モジュールを使用する事にしたので、以下の様な設定になります。

    //------------------------------------
    // 出力コンペアモジュールの設定
    //------------------------------------
    OC1CONbits.OCTSEL = 0;      // 0:Timer2 Clock 1:Timer3
    OC1CONbits.OCM    = 5;      // 5:ダブル比較連続パルス(OC1RでHigh,OC1RSでLow)
    OC1R              = PR2;    // Highになるタイミング
    OC1RS             = PR2/2;  // Low になるタイミング


出力コンペアモジュールの1番目を使用するので、設定するレジスタは、OC1CON、OC1R、OC1RSになります。
またこのモジュールが使用するタイマーはTimer2なので、OC1CONbits.OCTSELに0:Timer2を指定します。


ここまでの設定で、出力される波形の周波数、On/Offの比率(デューティー比)が決まります。
周波数は、以下の式で求まります。

周期: (PRx+1) * 1/クロック周波数 * Timer2の分周 * 2
      = (4000+1) * 1/8Mhz * 1 * 2 
      ≒ 1/1000


という訳で周期が1mSecとなり、1kHzの波形が出力されます。
周波数は、式を見て分かるとおり、PRの値・Timer2の分周・CPUクロックに応じて決まります。


矩形波がOn/OffするタイミングはOC1R,OC1RSの値で決まります。
今回は、OC1RをPR2と同じ値、OC1RSがPR2の半分に設定したので、TMR2レジスタの値が0~2000の時はLow、2001~4000の時はHighというDuty比50%の波形になります。



サンプルプログラム

という訳で、今回作成したサンプルのプログラムです。

#include <p24FJ32GA002.h>
 
//--------------------------------------
// コンフィグレーションビットの設定
//--------------------------------------
_CONFIG2(   POSCMOD_NONE  &       // Primary oscillator disabled
            IOL1WAY_OFF   &       // IOLOCK may be changed via unlocking seq
            OSCIOFNC_ON   &       // OSC2/CLKO/RC15 functions as port I/O (RC15)
            FCKSM_CSDCMD  &       // Clock switching and Fail-Safe Clock Monitor are disabled
            FNOSC_FRC     &       // Fast RC Oscillator (FRC)
            WUTSEL_LEG            // Legacy Wake-up Timer
        );
 
_CONFIG1 (  FWDTEN_OFF &          // Watchdog Timer is disabled
            ICS_PGx1   &          // Emulator EMUC1/EMUD1 pins are shared with PGC1/PGD1
            COE_ON     &          // Reset Into Clip On Emulation Mode
            BKBUG_ON   &          // Device resets into Debug mode
            GWRP_OFF   &          // Writes to program memory are allowed
            GCP_OFF    &          // Code protection is disabled
            JTAGEN_OFF            // JTAG port is disabled
         );
 
int main( void )
{
    //------------------------------------
    // クロックの定義
    //------------------------------------
    OSCCONbits.COSC  = 0; // CPUクロックにFast RC Oscillator (FRC)を使用
    CLKDIVbits.DOZE  = 0; // クロックのレート1:1(postscaler)
    CLKDIVbits.RCDIV = 0; // 8Mhz(prescaler)
 
    // OSCTUNbits.TUN = 0; // クロックの誤差微調整 0:調整なし
    // OSCCONbits.SOSCEN = 1;// セカンダリオシレータ(32kHz)を有効にする
 
    //------------------------------------
    // Timer2の設定
    //------------------------------------
    PR2             = 4000; // Timer2の最大値(この値でTMR2を0にリセットする)
    T2CONbits.TCKPS = 0;    // Timer2 プリスケーラ 0:1/1, 1:1/8, 2:1/64
    T2CONbits.TON   = 1;    // Timer2 ON
 
    //------------------------------------
    // RPピンの役割マッピング設定
    //------------------------------------
    RPOR2bits.RP5R = 18;   // RP5ピンの機能を, 18:OC1に割り当てる
//  RPOR3bits.RP6R = 19;   // RP6ピンの機能を, 19:OC2に割り当てる
 
 
    //------------------------------------
    // 出力コンペアモジュールの設定
    //------------------------------------
    // OC1(出力コンペア1)をオープンする(OC1CONの値、OC1RSの値,OC1Rの値)
    //  周期: (PRx+1) * 1/クロック周波数 * 2
    //        = (4000+1) * 1/8Mhz * 2 
    //         ≒ 1/1000    -> なので1kHzの波形が出るはず
    OC1CONbits.OCTSEL = 0;      // 0:Timer2 Clock 1:Timer3
    OC1CONbits.OCM    = 5;      // 5:ダブル比較連続パルス(OC1RでHigh,OC1RSでLow)
    OC1R            = PR2;      // Highになるタイミング
    OC1RS           = PR2/2;    // Low になるタイミング
 
 
    while( 1 ) {
        /* nop */;
    }    
 
    return 0;
}



このプログラムを実行させると、RP5のピンから1kHzの波形が出力されます。
本当に波形が出ているかは、オシロスコープなどを使用しないと分からないのですが、1kHzだったらスピーカを繋ぐことで確認する事ができます。


手元にあった8オーム,0.5Wの小さなスピーカを直結してみましたが、十分な音量で聞くことが出来ました。1kHzの音がどれくらいの高さかは、以下のwavファイルで確認できます。
1kHz.wav
500Hz.wav
440Hz.wav

PR1レジスタの値を変えると周期(音の高さ)を変えることが出来ます。
PR2=4000 -> 8000に変更すると周期が2倍になるので、500Hzになります。
500Hzはドレミで言うと、”シ”(B3)の音に近いです。

応用:スピーカーを使ってメロディを奏でてみる


今回作成したプログラムでは、決められた周期の波形を出すものでした。
これを、タイミングにあわせてPR1レジスタの値を変えていくことで音楽を奏でる事が出来ます。

試しに”カエルの歌”を演奏させるプログラムを作成してみました。

#include <p24FJ32GA002.h>
 
// 各音階のdefine定義
#define R    0
#define C3   1
#define CS3  2
#define D3   3
#define DS3  4
#define E3   5
#define F3   6
#define FS3  7
#define G3   8
#define GS3  9
#define A3   10
#define AS3  11
#define B3   12
#define C4   13
#define CS4  14
#define D4   15
#define DS4  16
#define E4   17
#define F4   18
#define FS4  19
#define G4   20
#define GS4  21
#define A4   22
#define AS4  23
#define B4   24
#define C5   25
#define CS5  26
#define D5   27
#define DS5  28
#define E5   29
#define F5   30
#define FS5  31
#define G5   32
#define GS5  33
#define A5   34
#define AS5  35
#define B5   36
//--------------------------------------
// コンフィグレーションビットの設定
//--------------------------------------
_CONFIG2(       POSCMOD_NONE  &       // Primary oscillator disabled
            IOL1WAY_OFF   &       // IOLOCK may be changed via unlocking seq
            OSCIOFNC_ON   &       // OSC2/CLKO/RC15 functions as port I/O (RC15)
            FCKSM_CSDCMD  &       // Clock switching and Fail-Safe Clock Monitor are disabled
            FNOSC_FRC     &       // Fast RC Oscillator (FRC)
            WUTSEL_LEG            // Legacy Wake-up Timer
        );
 
_CONFIG1 (  FWDTEN_OFF &          // Watchdog Timer is disabled
            ICS_PGx1   &          // Emulator EMUC1/EMUD1 pins are shared with PGC1/PGD1
            COE_ON     &          // Reset Into Clip On Emulation Mode
            BKBUG_ON   &          // Device resets into Debug mode
            GWRP_OFF   &          // Writes to program memory are allowed
            GCP_OFF    &          // Code protection is disabled
            JTAGEN_OFF            // JTAG port is disabled
         );
 
int main( void )
{
    volatile unsigned long int loop;
 
    // 各音階に応じたPR2レジスタの値
    int pr2data[] = { 0, 15289, 14431, 13621, 12857, 12135, 11454, 10811, 10204, 9632, 9091, 8581, 8099, 7645, 7216, 6811, 6428, 6068, 5727, 5406, 5102, 4816, 4545, 4290, 4050, 3822, 3608, 3405, 3214, 3034, 2863, 2703, 2551, 2408, 2273, 2145, 2025, };
 
    // 奏でるメロディ
    int melody[]  = { C3, D3, E3, F3, E3, D3, C3, R,  
                      E3, F3, G3, A3, G3, F3, E3, R, 
                      C3, R, C3, R, C3, R, C3, R, 
                      C3,C3, D3,D3, E3,E3, F3,F3, E3, E3, D3, D3, C3, C3, R, R, R  };
 
    //------------------------------------
    // クロックの定義
    //------------------------------------
    OSCCONbits.COSC  = 0; // CPUクロックにFast RC Oscillator (FRC)を使用
    CLKDIVbits.DOZE  = 0; // クロックのレート1:1(postscaler)
    CLKDIVbits.RCDIV = 0; // 8Mhz(prescaler)
 
    //------------------------------------
    // Timer2の設定
    //------------------------------------
    PR2   = 4000;   // この値でTMR2を0クリアする
    T2CONbits.TCKPS = 0;    // Timer2 プリスケーラ 0:1/1, 1:1/8, 2:1/64
    T2CONbits.TON   = 1;    // Timer2 ON
 
    //------------------------------------
    // RPピンの役割マッピング設定
    //------------------------------------
    RPOR2bits.RP5R = 18;   // RP5ピンの機能を, 18:OC1に割り当てる
 
    //------------------------------------
    // 出力コンペアの設定
    //------------------------------------
    // OC1(出力コンペア1)をオープンする
    // 周期: (PRx+1) * 1/クロック周波数 * 2
    OC1CONbits.OCTSEL = 0;      // 0:Timer2 Clock 1:Timer3
    OC1CONbits.OCM    = 6;      // 5:コンペアごとにOC1を反転 5:ダブル比較連続パルス(OC1RでHigh,OC1RSでLow)
    OC1R            = PR2;      // Highになるタイミング
    OC1RS           = PR2/2;    // Low になるタイミング
 
 
    //-------------------
    // 音楽を鳴らす
    //-------------------
    while( 1 ) {
        int loop2;
        for ( loop2 = 0; loop2 < sizeof( melody ) / sizeof( melody[0] ); loop2++ ) {
            // この値でTMR2を0クリアする-> 周波数を決める
            PR2   = pr2data[ melody[loop2] ];
 
            if ( melody[loop2] == 0 ) {
                // R:休符のときは音を止める
                PR2 = 0;
            } else {
                OC1R            = PR2;    // Highになるタイミング
                OC1RS           = PR2/2;  // Low になるタイミング
            }
 
            // 指定した音をこの時間鳴らす(テンポの設定)
            for ( loop = 0; loop < 130000; loop++ ) {
                /* nop */;
            }
 
            // 小さな無音時間(区切り)を作る
            PR2 = 0;
            for ( loop = 0; loop < 100; loop++ ) {
                /* nop */;
            }
 
        }
    }
 
    return 0;
}



pr2dataの配列に、各音階に応じたPR2の値をセットしています。
値は近似値で誤差が有るので、ちょっと音痴なメロディーになっているかもしれません。
こんな感じで再生されます。 → kaeru.wma

演奏する曲は、melodyの配列で指定してます。
簡単なサンプルなので音の長さは指定できませんが、値を色々変えてみて遊んでみてください(ループの130000を可変にすることで、音の長さを調整できます)。

また、PR2に設定するデータ指定を配列からではなく、外部に接続したスイッチからにすれば電子オルガンを作ることも出来ます。

関連記事

コメントを残す

メールアドレスが公開されることはありません。