今回は、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を利用した電子オルゴールを紹介します。