今回は、8ビットのPIC16F18313に搭載されているNumerically Controlled Oscillator (NCO)とCCPモジュールを使い1オクターブ分の正弦波音階を出すプログラムです。

1 NCOとPWMを使った正弦波発生

CCP (Capture/Compare/PWM)モジュールが重要割を果たします。正弦波を発生する出力ピンの電圧を可変するのに、このCCPモジュールPWM機能を使用します。PWMは、出力方形波波形のデューティサイクルを調整し実質的な出力平均電圧を、0-100%可変することができるモジュールですが、今回は、PWM分解能を128カウント、PWM周期は 250KHzに設定しました。

 

 

まず上図のように、サイン波の一周期分を32ステップに分割し、最大値が 127になるよう各ステップの値を計算して配列定数 sinWave[ ]にまとめます。

この配列値を一定間隔で CCPのデューティサイクルを決定するレジスタ CCPRxに書き込めば、希望のサイン波形が得られます。

デューティサイクルを変更するタイミング信号を、きめ細かく周波数を調整できるNCOで発生させれば、高精度の正弦波発生器の完成です。

 


NCOは、20bit長の主レジスター(NCO1ACC)と20bitの加算器から構成されており、NCO1_clk 入力 (250KHz)のたびごとに、一定の値(NCO1INC)が主レジスターに加算されます。加算される値が小さければ、加算結果でOverFlow発生に時間がかかり、大きければすぐにOverFlowが発生します。OverFlow発生の繰り返し周波数は、以下の式で表されます。

式:NCO1の周波数

 

Freq. Overflow = NCO1 Clock Freq. x Increment Value 2^20

 

出力する正弦波周波数の32倍のNCO出力周波数にする必要があるため、例えば「ラ」440Hzの正弦波を出すには、NCO出力は 14.08KHzが必要になります。

 

詳しい NCOの解説は、はじめてのPICホームページをご覧ください。

2 使用する回路

前回記事と同じ回路です。部品数が少ないので、ブレッドボードに回路を組みました。音の実効出力が低くなるとめ、コンデンサーを介したスピーカーでは小さな音になってしまします。外部アンプのついたアクチブスピーカーがおすすめです。

 

 

 

 

3 プログラム概要

このプログラムでは、20bit長 (最大値 : 2^20)のアキュムレータ(NCO1ACC)に20bitの一定の値(NCO1INC)を、一定間隔 ( 250 KHz )で加算します。

NCOのクロック

Timer2で Fosc/4を 1/32して、250KHzが作られます。この信号をNOCのクロック入力にするのですが、Timer2と NCOを直接接続することができないので、CLC1を経由して接続します。

 

音の高さを決める定数

希望する正弦波出力周波数を得る加算値は、NOCのOverflowが 32回で正弦波の一周期なので、以下の式で表されます。

Increment Value = Freq. x 32 x 2^20250,000

 

つまり、

「ラ音( 周波数 440 Hz)」を得るには、上記式の Freq に 440 を代入し計算すると、加算値は、59056 (0xE6B0) になります。

 

各音に対応する加算値を上の式で計算し配列  inc_tbl[ ] にしてプログラムに書き込みます。

 

計算結果は以下のようでした。

 

--- ファ
音周波数 261.64 293.67 329.63 349.23 392.00 440.00 493.88 523.25
加算値  35115 39415 44242 46873 52613 59056 66288 70230
(HEX)  0x892B 0x99F7 0xACD2 0xB719 0xCD85 0xE6B0 0x102F0 0x11256
出力 Hz 261.63 293.66 329.63 349.23 392.00 440.00 493.88 523.25
エラー  0.00% 0.00% 0.00% 0.00% 0.00% 0.00% 0.00% 0.00%

音の出力処理

波形を発生させるために、加算値レジスターのNCO1INC (NCO1INCU, NCO1INCH, NCO1INCL の3 bytes) に値を保存します。各音の演奏時間 500msのディレーの後に、次の音の加算値に書き換えます。

 

出力正弦波周波数の32倍で NCOのIverflow割り込みが発生するので、割り込み処理プログラムの中で、PWMのデュティー値をsinWave[ ] 配列の値で順に更新します。

 

1オクターブ分の演奏が終了すると、SWの監視をし、SWが押されると再び演奏を始めます。

4 プログラムリスト

下にプログラムのリストを掲載します。

 

/*************************************
  * File: NCO sine wave
  * System ClockはConfigで内部32MHzに設定
  * ccp1 Dutyをサイン波形になるよう変更
  * NCO で Dutyを変更するタイミングを作る
  * NCO クロックは、TMR2の250KHzをCLCを介して供給
  * PIC16F18313
  * Created on 2021-12-26
  **************************************/

 #include <xc.h>
 #define _XTAL_FREQ 32000000      // delay_ms(x) のための定義
 #define SP  LATA2               // LED
 #define SW  RA5                 // Push SW

 #pragma config FEXTOSC = OFF,RSTOSC = HFINT32
 #pragma config CLKOUTEN = OFF,CSWEN = OFF,FCMEN = OFF
 #pragma config MCLRE = ON,PWRTE = OFF,WDTE = OFF
 #pragma config LPBOREN = OFF,BOREN = OFF,BORV = LOW
 #pragma config PPS1WAY = OFF,STVREN = ON,DEBUG = OFF
 #pragma config WRT = OFF,LVP = ON,CP = OFF,CPD = OFF

 // 音の高さを決める定数
  const long inc_tbl[] = {
 //    Do,    Re,    Mi,    Fa,    So,    Ra,    Si,     Do
     0x892B,0x99F7,0xACD2,0xB719,0xCD85,0xE6B0,0x102F0,0x11256
  };
// sine定数 32分割(11.25°毎) 範囲:0 - 127
const char sinWave[] = {
    0,1,5,11,19,28,39,51,64,76,88,99,108,116,122,126,
    127,126,122,116,108,99,88,76,64,51,39,28,19,11,5,1
};
     
char idx = 0;

 void main(void) {
     LATA   = 0;
     TRISA  = 0b111011;     // A2を出力、A5を入力に設定
     ANSELA = 0;            // 全てのpinをデジタルに設定
     WPUA   = 0b111111;     // 入力に弱プルアップ

//------------ Initialize PWM --------------------------
    T2CON  = 0b00000100;      // Timer 2 PS1/1設定
    PR2    = 0x1F;            // Timer2 周期 250KHz設定
    CCP1CON= 0b10001111;      // Duty 下位 PWMモード
    RA2PPS = 12;              // CCP1をRA2に出力
    // CLC1 設定 ****************************************
    // T2_matchをNCO Clockに接続する
    CLC1POL  = 0x02;    // LC1G2POL 反転
    CLC1SEL0 = 0x1A;    // LC1D1S T2_match; 
    CLC1SEL1 = 0x00;    // LC1D2S 接続なし
    CLC1SEL2 = 0x00;    // LC1D3S 接続なし
    CLC1SEL3 = 0x00;    // LC1D4S 接続なし
    CLC1GLS0 = 0x02;    // LC1G1D1T enabled 
    CLC1GLS1 = 0x00;    // LC1G2 全入力なし
    CLC1GLS2 = 0x00;    // LC1G3 全入力なし
    CLC1GLS3 = 0x00;    // LC1G4 全入力なし 
    CLC1CON  = 0x80;    // LC1EN 割り込みなし AND-OR; 
    // NCO 設定 ****************************************
    NCO1CLK = 0x02;     // LC1OUTがクロック
    NCO1IF = 0;         // NCO 割込みフラッグをクリア
    NCO1IE = 1;         // NCO 割込みを使用許可
    PEIE = 1;           // 周辺機能割込みの使用許可
    GIE  = 1;           // 全割込みの使用許可     

     while(1){
         for(char i = 0;i < 8;i++){        // 8回繰り返し
             NCO1INCU = inc_tbl[i] >> 16;  //  データをセット
             NCO1INCH = inc_tbl[i] >>  8;  //
             NCO1INCL = inc_tbl[i] & 0xFF; //
             NCO1CON = 0b10000000;         //  NCO ON
             __delay_ms(500);              //  500ms演奏
         }
         NCO1CON = 0;                      // NCO OFF
         while(SW);                        // SW ONを待つ
     }
 }
//----------------------割込み処理------------------------
void __interrupt() NCO_isr(void) {
   if(NCO1IF){                  // NCO1からの割込みを確認
       NCO1IF = 0;              // 割込みフラッグをクリア
       CCPR1L = sinWave[idx++]; // デュティサイクルを設定
       idx &= 0x1F;             // サイン波形の繰り返し
   }
}

 

5 次回の予定

次回は、NCO, PWMを利用した電子オルゴールを紹介します。