シグマデルタ変調 PWM (2)
普通の PWM では、いったんパルス・デューティー値を設定してしまえば、再びその値を変更するタイミングまでは何もする必要がありません。
それに対し、ここでは、PWM デューティーを細かく変化させて分解能を向上させるため、 PWM 周期ごとに処理をする必要があります。
具体的には、AVR の Timer1 を使って PWM 波を発生しているので、Timer1 のオーバーフロー割り込みサービス・ルーチンの中で ΣDM の計算を行い、アウトプット・コンペア・レジスタ OCR1A、OCR1B を設定しています。
Pakurino (Arduino) のクロック周波数は 16 MHz で、8 ビット Phase correct PWM を使っているので 512 分周されて 31.25 kHz が PWM 周波数となります。
実際のプログラムから、本質的な部分だけを抜き出したコードを下に示します。
extern uint16_t dac_l; // #define Q_MASK (0xFF00) #define Q(x) (Q_MASK & x) // // Timer1 overflow interrupt service routine // ISR(TIMER1_OVF_vect) { register uint16_t sdm_q1; static uint16_t sdm_acc1, sdm_acc2, sdm_q2; // // 1st stage sigma delta modulator // sdm_acc1 += dac_l; // accum. DAC input sdm_q1 = Q(sdm_acc1); // quantize to 8 bit sdm_acc1 -= sdm_q1; // calc quant. error // OCR1B = (sdm_q1 >> 8); // 1st order SDM out // sdm_q1 -= sdm_q2; // sub last Q of stage2 // // 2nd stage sigma delta modulator // sdm_acc2 += sdm_acc1; // 2nd stage accum. sdm_q2 = Q(sdm_acc2); // quantize to 8 bit sdm_acc2 -= sdm_q2; // calc quant. error // sdm_q1 += sdm_q2; // add curr. Q of stage2 OCR1A = (sdm_q1 >> 8); // 2 nd ordr SDM out } // ISR(TIMER1_OVF_vect)
このコードで下のブロック図を実現しています。
変数 sdm_acc1、sdm_acc2、sdm_q2 は、前回の割り込みルーチンの呼び出しで設定された値を保持している必要があるので、「static」属性を付けて宣言してあります。 sdm_q1 は割り込みルーチンが終わると必要なくなるので、レジスタ型の自動変数としています。
ΣDM PWM DAC への入力である「dac_l」は 16 ビット変数なので、上位、下位、それぞれのバイトをアクセスする間に割り込まれて不正な値が割り込みルーチンに渡されないように、メインルーチン側では dac_l のアクセスの前に割り込みを禁止して、アクセス終了後に割り込み可能にする必要があります。
具体的には Arduino IDE では、下のように、アクセス前に noInterrupts() 関数を呼び、アクセス終了後に interrupts() 関数を呼びます。
noInterrupts(); // disable interrupts dac_l = 1234; interrupts(); // enable interrupts
この 4 行
sdm_acc1 += dac_l; // accum. DAC input sdm_q1 = Q(sdm_acc1); // quantize to 8 bit sdm_acc1 -= sdm_q1; // calc quant. error OCR1B = (sdm_q1 >> 8); // 1st order SDM out
で、左の図の1 次 ΣDM 1 段分を実現しています。
16 ビット・タイマである Timer1 では 9 ビット PWM や 10 ビット PWM もサポートされているので、プログラムでも任意のビット数の PWM に対応できるようにしてあります。
一方、8 ビット PWM に限れば、Timer0 や Timer2 でも実現でき、また、コンパイル結果が効率的になるようなプログラムを書くことができます。
その例を下に示します。
ISR(TIMER1_OVF_vect) { register uint8_t sdm_q1; register uint16_t sdm_work; static uint8_t sdm_acc1, sdm_acc2, sdm_q2; // // 1st stage sigma delta modulator // sdm_work = (uint16_t)sdm_acc1 + dac_l; sdm_acc1 = sdm_work; sdm_q1 = (sdm_work >> 8); // OCR1BL = sdm_q1; sdm_q1 -= sdm_q2; // // 2nd stage sigma delta modulator // sdm_work = sdm_acc1 + sdm_acc2; sdm_acc2 = sdm_work; sdm_q2 = (sdm_work >> 8); // sdm_q1 += sdm_q2; OCR1AL = sdm_q1; } // ISR(TIMER1_OVF_vect)
sdm_acc1、sdm_acc2 は 8 ビット幅とし、小数部だけを保持するようにします。 また、量子化の結果である sdm_q1、sdm_q2 も 8 ビット幅ですから、16 ビット・タイマーである Timer1 の場合でもアウトプット・コンペアレジスタの下位レジスタだけに書き込むようにしています。
上のコードを「-S」オプションを付けてコンパイルし、アセンブラ・ソースに変換して、見やすいように修正を加えた結果の1段目の ΣDM の部分だけを下に示します。
// extern uint16_t dac_l; // register uint8_t sdm_q1; // register uint16_t sdm_work; // static uint8_t sdm_acc1; // // sdm_work = (uint16_t)sdm_acc1 + dac_l; lds r24,(sdm_acc1) lds r18,(dac_l) lds r19,(dac_l)+1 add r18,r24 adc r19,r1 // sdm_acc1 = sdm_work; sts (sdm_acc1),r18 // OCR1BL = sdm_q1; sts (OCR1BL),r19
これを見ると、AVR の命令 7 個に変換されており、ほぼ最適となっています。
後半の2段目の ΣDM では、量子化結果の sdm_q2 は 0 と 1 の値しか取らないので、アセンブリ言語なら、キャリー・フラグを活用した効率的なコードを書くことができますが、C 言語のコンパイル結果では、ちょっともどかしいようなコードに変換されています。
Arduino IDE では、gas ソースファイルを含めたスケッチを作成できないようなので、インライン・アセンブラを使用する必要があり、ちょっと面倒です。