追記:初出の記事はPWMの分解能に誤解があったが修正しておいた
PWMでDA変換
前の記事でNCOがシンセサイザー音源として使えそうって話を書いたが、矩形波しか出ないんだからダメダメだろう、最低でも正弦波、矩形波、ランプ波、三角波程度は必要だよねって考えてたら世の中には猛者が居た。NCO1とPWMデューティでDA変換をすることで任意波形が出せるってことなんだが、同じようなことを以前にちょっと考えたことはある。ただ、ちょっと色々「足りない」感じがしていたのだが、もう一回、真面目に考え直してみた。
記事では時間方向には32分割していてサンプリング周波数をNCO1で可変にしている。内部に32要素の波形テーブルを持っていて、ここにPWMデューティ(0〜31)を入れてある。この波形テーブルを正弦波ではないものに入れ替えれば任意の波形が出せることになる。
PWMのデューティの説明をした図を以下に示す。PWM周波数はFOSC32MHz/4/32=250kHzである。
pragma-config.h
コンフィギュレーションはFOSCが32MHzの典型的な設定だ。メインルーチンでOSCCONを設定しなくても、普通に32MHzで駆動される。
// CONFIG1
#pragma config FEXTOSC = OFF
#pragma config RSTOSC = HFINT32
#pragma config CLKOUTEN = ON
#pragma config CSWEN = OFF
#pragma config FCMEN = OFF
// CONFIG2
#pragma config MCLRE = ON
#pragma config PWRTE = OFF
#pragma config WDTE = OFF
#pragma config LPBOREN = OFF
#pragma config BOREN = OFF
#pragma config BORV = LOW
#pragma config PPS1WAY = ON
#pragma config STVREN = OFF
#pragma config DEBUG = OFF
// CONFIG3
#pragma config WRT = OFF
#pragma config LVP = OFF
// CONFIG4
#pragma config CP = OFF
#pragma config CPD = OFF
初期設定
メインルーチンの冒頭はアナログ無効、P IN出力無効のあとOSCTUNEでHFINTOSCの周波数調整を行なった。これは440Hzを発生させた状態でオシロスコープで周波数を測定して調整値を決めた。0b100000が最小値なのでそれにオフセットを足している。
void main(void) {
ANSELA=0;
TRISA=0xff;
OSCTUNE=0b100000+27;
PWMとしてはPWM5モジュールを使用する。クロック源はTMR2であり分解能32が得られるようPR2には32-1を入れた。実際のPWM分解能はこの時点で5bit+2bit=7bitだが、デューティレジスタのPWM5DCLを無視する予定なので、やはり5bit分解能である。
T2CONbits.TMR2ON=1;
PR2=31;
PWM5CONbits.PWM5OUT=1;
PWM5CONbits.PWM5EN=1;
RA2PPS=0b00010;
TRISAbits.TRISA2=0;
NCO1モジュールはPFMモードで割り込みタイマとしてだけ使う。
NCO1CONbits.N1EN=1;
NCO1CONbits.N1PFM=1;
PIE2bits.NCO1IE=1;
これで初期値を設定してINTCONの割り込み許可をするだけで良い。メインループは今は空だが将来的にはUARTからのMIDI信号の処理をポーリングで行うことになる。なお、ノート番号69は440Hzに相当し、incH[]とincL[]にはノート番号毎のインクリメント値が収められている。NCOへの入力はデフォルトでHFINTOSCの16MHzでありFOSCではない。発音周波数の32倍の周波数で割り込むようにインクリメントを計算しておく。
uint8_t note=69;
NCO1INCU=0;
NCO1INCH=incH[note];
NCO1INCL=incL[note];
INTCONbits.PEIE=1;
INTCONbits.GIE=1;
while(1) {}
割り込み
NCO1でタイマー割り込みになっていて、idxが0から31までぐるぐる回るようになっており、これをPWMデューティ制御しているだけだ。ここで、PWM5DCHにだけ5bitデューティを指定している。つまり7bit分解能の上位5bitだけを操作していることになるわけだ。
void __interrupt() isr(void) {
if(PIR2bits.NCO1IF) {
PIR2bits.NCO1IF=0;
static uint8_t idx = 0;
idx = (idx + 1) & 0x1F;
PWM5DCH = sine32[idx];
}
}
PWMとフィルタ出力
PWMはRA2から出力されるが、これを10kか20kHzくらいの簡単なCRフィルタに入れた結果を以下に示す(上がPWM出力、下がフィルタ出力)。それなりにカクカクはしているが、ちゃんと正弦波に見える。周波数も440Hz程度に安定している。
当然ながらノート番号を変えれば望みの周波数というかトーンが得られる。波形テーブルをRAMに持ち、例えばMIDIのシステムエクスクルーシブで置き換えられるようにすれば往年のウェイブテーブルシンセサイザーと言えなくもない。
だが待てよ…
今回のこの構成は5ビットのDA変換になっているのだが、DA変換といえばそのままズバリの5bitDACが乗ってなくね?何でPWMにしたんだっけ?
ちなみにDAC1を用いても全く同じというかアナログフィルタなしでディジタル臭い同じような正弦波が得られる。ただしDAC出力は RA0固定なのでICSPを繋いだままだと結果を確認できない。
メインルーチンからTMR2とPWM5の初期化部分を外してDACを有効にする。
void main(void) {
ANSELA=0;
TRISA=0xff;
OSCTUNE=0b100000+27;
DACCON0bits.DAC1EN=1;
DACCON0bits.DAC1OE=1;
NCO1CONbits.N1EN=1;
NCO1CONbits.N1PFM=1;
PIE2bits.NCO1IE=1;
uint8_t note=69;
NCO1INCU=0;
NCO1INCH=incH[note];
NCO1INCL=incL[note];
INTCONbits.PEIE=1;
INTCONbits.GIE=1;
while(1) {}
}
割り込みルーチンもDACの電圧レベル設定を書き込むだけで終了だ。
void __interrupt() isr(void) {
if(PIR2bits.NCO1IF) {
PIR2bits.NCO1IF=0;
static uint8_t idx = 0;
idx = (idx + 1) & 0x1F;
DACCON1 = sine32[idx];
}
}
結論
ソフトウェアでDDS構成にして20bitを少し拡張すればもう少し周波数分解能を上げられる。もしくは前に書いた記事のように、LFINTOSCを使ってNCO1への入力周波数を下げられればかなりいいところまで行けそうだ。ただし、こうしてしまうとLFINTOSCはOSCTUNEが効かないのでインクリメント値のテーブルで調整しなければいけなくなる。
DCOとして機能させられそうなものはできたがこれだけではかなりチープな感じだ。もしかして何個か並行に繋げて厚みを出すべきか。。。
投稿者の人気記事

-
akira.kei
さんが
前の土曜日の19:11
に
編集
をしました。
(メッセージ: 初版)
-
akira.kei
さんが
前の土曜日の19:18
に
編集
をしました。
-
akira.kei
さんが
前の土曜日の19:18
に
編集
をしました。
-
akira.kei
さんが
前の土曜日の22:55
に
編集
をしました。
-
akira.kei
さんが
前の土曜日の23:49
に
編集
をしました。
-
akira.kei
さんが
前の日曜日の0:33
に
編集
をしました。
-
akira.kei
さんが
前の日曜日の0:56
に
編集
をしました。
-
akira.kei
さんが
前の日曜日の0:59
に
編集
をしました。
-
akira.kei
さんが
前の日曜日の1:56
に
編集
をしました。
-
akira.kei
さんが
前の日曜日の1:59
に
編集
をしました。
ログインしてコメントを投稿する