シュトレーレ回路 (4) -- シュトレーレ PWM (2)

シュトレーレ近似の値を PWM によるアナログ電圧値として出力する Arduino 用のスケッチを示します。
シュトレーレ近似の最大誤差は ±0.2 % 程度で 8 ビット相当ですから、ATmega168 内蔵の 10 ビット ADC で傾向がとらえられるのではないかと考え、PWM 出力電圧を読み取る機能も加えました。
しかし、実際には、後で示すように満足な結果とはなりませんでした。
シリアル版のスケッチを下に示します。 Arduino IDE の最新バージョン「Arduino-0017」用になっています。

/*******************************************/
/* Strehle : Strehle PWM anti-log test     */
/* for "Arduino-0017"                      */
/* 2009/8/14  created by PCM1723           */
/*******************************************/
#include <inttypes.h>
#include <avr/sleep.h>
#include <MsTimer2.h>

#define NAVERAGE (1000)
#define COM_BPS  (38400)
#define ADPIN_GND (15)

uint8_t led_pin = 13; // Idle LED
uint8_t pwm_pin = 10; // PWM output 
uint8_t vin_pin = 4;  // voltage input

volatile uint8_t fsflag = 0;  // fs timing flag
int16_t ave_cnt;
int32_t adc_ave = NAVERAGE-1;
int32_t adc_ofs;
int16_t x, num, den;

// 1 ms tick routine
void mstick( void )
{
  fsflag = 1;
} // void mstick()

void set_pwm_regs(int16_t num, int16_t den)
{
  noInterrupts();
  OCR1B = num; // numerator
  OCR1A = den; // denominator
  interrupts();
} // void set_pwm_regs()

void PWM_setup(uint8_t pwm_pin)
{
//  modify Timer1 PWM setting
  analogWrite(pwm_pin, 0); // enable PWM output
  TCCR1B  = 0x11; // WGM13:12 = 1:0, 1/1 clk (16 MHz)
//TCCR1A  = 0xA1; // 8-bit Phase and Frequency correct PWM
  set_pwm_regs(144, 288);  // set (144/288)
} // void PWMDAC_setup()

void setup(void) 
{
  pinMode(led_pin, OUTPUT); // set Idle LED pin to output
  PWM_setup(pwm_pin);
  set_sleep_mode(SLEEP_MODE_IDLE);
  MsTimer2::set(1, mstick);
  MsTimer2::start();
  Serial.begin(COM_BPS);
  Serial.println("Strehle PWM anti-log test");
  adc_ofs = 0;
// convert GND level to cancel offset
  for (int16_t i = 0; i < NAVERAGE; i++) {
    adc_ofs += analogRead(ADPIN_GND);
  } // for 
} // void setup() 

void loop(void) 
{ 
  if (fsflag) { // 1ms tick occurred
    digitalWrite(led_pin, LOW); // Idle LED OFF
    fsflag = 0; // reset 1ms tick flag
    adc_ave += analogRead(vin_pin);
    if (0 > (--ave_cnt)) { // averaging
      ave_cnt  = NAVERAGE-1;  // reset average counter
      adc_ave -= adc_ofs; // cancel offset
      adc_ave += NAVERAGE / 2; // rounding
      adc_ave /= NAVERAGE; // averaging
      Serial.print("X=");
      Serial.print(x);
      Serial.print(", V=");
      Serial.println(adc_ave);
      if (Serial.available()) { // input found
        x = 0x5F & Serial.read();
        if ('A' <= x)  x = x - 'A' + 10;
        x &= 0x0F;
        if (12 < x) x = 12;
// Strehle approximation
        num = (144 + 5*x);
//         -------------
        den = (288 - 7*x);
        set_pwm_regs(num, den);
      } // if (Serial.available()) ...
    } // if (0 > (--ave_cnt)) ...
    digitalWrite(led_pin, HIGH); // Idle LED ON
  } else { // if (fsflag) ...
    sleep_mode(); // sleep until timer interrupt
  } // if (fsflag) ... else ...
} // void loop() 

オプション・ライブラリの「MsTimer2」を使っているので、インストールしておいて下さい。
PWM 出力はディジタル 10 番ピン (OC1B) です。
これは、PWM の動作モードに OCR1A で「TOP」値を設定するタイプの「Phase and Frequency Correct PWM」 を使っている関係上、他のピンに振り替えることはできません。
PWM 出力から LPF を通して基本波が 27.78 〜 39.22 kHz となる PWM 周波数成分を除去し、電圧を測定します。
ATmega 内蔵の ADC を使う場合には、フィルタ出力をアナログ 4 番ピンにつなぎます。
これは、スケッチ中の

      uint8_t vin_pin = 4;  // voltage input

を書き換えれば、任意のアナログ入力ピンに変更できます。
このスケッチを実行すると、約 1 秒おきに AD 変換結果をシリアル出力に表示するとともに、シリアル入力をチェックし、'0' 〜 '9'、'A' 〜 'C' のキーが押されていればそれに対応して「x」に 0 〜 12 の値を代入してシュトレーレの近似式を計算し、PWM のレジスタを設定します。 'A' 〜 'C' は大文字でも小文字でもかまいません。 
また、シリアルのボーレイトはデフォルトで 38.4 kbps となっていますが、スケッチ中の

      #define COM_BPS  (38400)

を書き換えれば他のボーレイトに変更できます。
Arduino-0017 用であるというのは、0017 でバグ・フィックスされた analogRead() の機能を使っているからです。
まあ、「バグ」と言っていいのかどうか分かりませんが、0016 の analogRead() 関数の定義をしているソース・ファイル 「wiring_analog.c」では、ADC の入力マルチプレクサの設定部分で、コメントに書いてあることと、実際のコードが食い違っていました。
具体的には、コメントでは「low 4 bits」と書いてあるのに、コードでは (pin & 0x07) となっており、下 3 ビットしか有効ではありません。
これで何が困るかというと、実際のアナログ入力ピン (28 ピン版で 6 本、32 ピン版で 8 本) については問題ありませんが、MUX 入力 14 番の 1.1 V バンドギャップ電圧源、15 番の GND 入力を選択することができないのです。
それが 0017 ではコードの方も (pin & 0x0f) と修正されて、1.1 V 電圧源、GND、温度センサ (168P、328P のみ) も選択できるようになりました。
このスケッチでは、最初に ADC 入力を GND に落として変換データを読み取って憶えておき、このオフセット分を後の目的の計測値から差し引いています。
この方法ではオフセット値が正側に振れる場合にしか有効ではなく、実験に使ったデバイスではオフセットが負側に振れるようで、効果がありませんでした。
実験の結果を示す前に、シュトレーレ近似の誤差のグラフを再掲しますが、今回は PWM での実験に合わせ、x が 0 から 12 まで変化する定義でのグラフになっています。 x の値域も上下に少し拡大しています。

x = 0 と x = 12 で誤差ゼロになる 3 次式の形状のカーブで、その両側少しの部分まで誤差 0.2 % 程度におさまります。
音程でいうと、1 オクターブと半音程度です。
下は内蔵 10 ビット ADC で測定した結果です。

x = 0 の点を誤差ゼロとして基準にしていますが、x = 12 で誤差 +0.8 % 程度に達する「トレンド」があるのが分かります。
ADC の変換データが「1」と「1022」になるような電圧を外部から与えて、そのふたつの点を結ぶ直線を外挿して入力電圧ゼロの場合の変換出力を推定すると、約 -2.3 になりました。
マイナスのオフセットが存在することが分かったので、測定結果に恣意的にオフセットをキャンセルするような値を足しこんでグラフにしたのが次の図です。

この場合、計算値よりも大きい値 2.9 を足しています。
このグラフでは、少しデコボコはしていますが、なんとか 3 次式のカーブに沿っていることが分かります。
内蔵 ADC ではなく、外部の電圧計で計測した結果を下に示します。

グラフのカーブの形はきれいに出ていますが、X = 12 で誤差 -0.15 % 程度に達するトレンドがあるのが分かります。
LPF に使っている OP アンプのオフセットを疑って、オフセット電圧の小さいタイプに交換してみましたが、あまり変わりませんでした。
トレンドの量が微少なので、はっきりしませんが、この場合 PWM のデューティーだけでなく周期も変化させているのが原因かも知れません。
誤差のカーブの特徴は明確に出ているので、これで満足して、これ以上の深追いはしません。