編集履歴一覧に戻る
akira.keiのアイコン画像

akira.kei が 2026年02月15日01時59分53秒 に編集

コメント無し

本文の変更

-

[<前の記事](https://elchika.com/article/255eb8fa-f3fd-4546-89e2-786402b65667/) : [次の記事>](https://elchika.com/article/f1ee4828-abab-4617-8071-ffd5e4f429bf/)

+

[<前の記事](https://elchika.com/article/51e991b2-8436-40e1-889e-3b7cd551ccff/) : [次の記事>](https://elchika.com/article/f1ee4828-abab-4617-8071-ffd5e4f429bf/)

++追記:初出の記事はPWMの分解能に誤解があったが修正しておいた++ ## PWMでDA変換 前の記事で[NCOがシンセサイザー音源として使えそう](https://elchika.com/article/0945b5b9-6d41-409d-89b5-f641314eb5d6/)って話を書いたが、矩形波しか出ないんだからダメダメだろう、最低でも正弦波、矩形波、ランプ波、三角波程度は必要だよねって考えてたら[世の中には猛者が居た](https://ameblo.jp/hajimeteno-pic/entry-12717203632.html)。NCO1とPWMデューティでDA変換をすることで任意波形が出せるってことなんだが、同じようなことを以前にちょっと考えたことはある。ただ、ちょっと色々「足りない」感じがしていたのだが、もう一回、真面目に考え直してみた。 記事では時間方向には32分割していてサンプリング周波数をNCO1で可変にしている。内部に32要素の波形テーブルを持っていて、ここにPWMデューティ(0〜31)を入れてある。この波形テーブルを正弦波ではないものに入れ替えれば任意の波形が出せることになる。 PWMのデューティの説明をした図を以下に示す。PWM周波数はFOSC32MHz/4/32=250kHzである。 ![キャプションを入力できます](https://camo.elchika.com/f46c42a1f64ea6ddb9881b59e69713ea7e2f08b1/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f33363031626666382d633530632d343762382d613765362d3531663731393163323336642f63373535653464382d346261372d343039652d623133342d356432636235313263373133/) ## pragma-config.h コンフィギュレーションはFOSCが32MHzの典型的な設定だ。メインルーチンでOSCCONを設定しなくても、普通に32MHzで駆動される。 ```c: // 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が最小値なのでそれにオフセットを足している。 ```c: void main(void) { ANSELA=0; TRISA=0xff; OSCTUNE=0b100000+27; ``` PWMとしてはPWM5モジュールを使用する。クロック源はTMR2であり分解能32が得られるようPR2には32-1を入れた。実際のPWM分解能はこの時点で5bit+2bit=7bitだが、デューティレジスタのPWM5DCLを無視する予定なので、やはり5bit分解能である。 ```c: T2CONbits.TMR2ON=1; PR2=31; PWM5CONbits.PWM5OUT=1; PWM5CONbits.PWM5EN=1; RA2PPS=0b00010; TRISAbits.TRISA2=0; ``` NCO1モジュールはPFMモードで割り込みタイマとしてだけ使う。 ```c: NCO1CONbits.N1EN=1; NCO1CONbits.N1PFM=1; PIE2bits.NCO1IE=1; ``` これで初期値を設定してINTCONの割り込み許可をするだけで良い。メインループは今は空だが将来的にはUARTからのMIDI信号の処理をポーリングで行うことになる。なお、ノート番号69は440Hzに相当し、incH[]とincL[]にはノート番号毎のインクリメント値が収められている。NCOへの入力はデフォルトでHFINTOSCの16MHzでありFOSCではない。発音周波数の32倍の周波数で割り込むようにインクリメントを計算しておく。 ```c: 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だけを操作していることになるわけだ。 ```c: 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程度に安定している。 ![キャプションを入力できます](https://camo.elchika.com/f494a5a5609aacd1855163463aa912b2db51824f/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f33363031626666382d633530632d343762382d613765362d3531663731393163323336642f33306662663037322d356536352d343732612d383762382d386134396637343535613535/) 当然ながらノート番号を変えれば望みの周波数というかトーンが得られる。波形テーブルをRAMに持ち、例えばMIDIのシステムエクスクルーシブで置き換えられるようにすれば往年のウェイブテーブルシンセサイザーと言えなくもない。 ## だが待てよ… 今回のこの構成は5ビットのDA変換になっているのだが、DA変換といえばそのままズバリの5bitDACが乗ってなくね?何でPWMにしたんだっけ? ちなみにDAC1を用いても全く同じというかアナログフィルタなしでディジタル臭い同じような正弦波が得られる。ただしDAC出力は RA0固定なのでICSPを繋いだままだと結果を確認できない。 メインルーチンからTMR2とPWM5の初期化部分を外してDACを有効にする。 ```c: 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の電圧レベル設定を書き込むだけで終了だ。 ```c: void __interrupt() isr(void) { if(PIR2bits.NCO1IF) { PIR2bits.NCO1IF=0; static uint8_t idx = 0; idx = (idx + 1) & 0x1F; DACCON1 = sine32[idx]; } } ``` ![キャプションを入力できます](https://camo.elchika.com/db2374dea22c03303b6c84d48531791497624a1b/687474703a2f2f73746f726167652e676f6f676c65617069732e636f6d2f656c6368696b612f76312f757365722f33363031626666382d633530632d343762382d613765362d3531663731393163323336642f36343331626135332d363137322d346133372d626136332d376238646439323034633564/) ## 結論 ソフトウェアでDDS構成にして20bitを少し拡張すればもう少し周波数分解能を上げられる。もしくは[前に書いた記事](https://elchika.com/article/0945b5b9-6d41-409d-89b5-f641314eb5d6/)のように、LFINTOSCを使ってNCO1への入力周波数を下げられればかなりいいところまで行けそうだ。ただし、こうしてしまうとLFINTOSCはOSCTUNEが効かないのでインクリメント値のテーブルで調整しなければいけなくなる。 DCOとして機能させられそうなものはできたがこれだけではかなりチープな感じだ。もしかして何個か並行に繋げて厚みを出すべきか。。。