2014年6月11日水曜日

自動オシレーター調整

今回もNTP時計の話題ですが、一旦ネットワークの話から離れてみようと思います。

PICのオシレーターは様々な種類が選べます。時計にするならば、32.768kHzのクロックをタイマ1に付けて1Hzの割り込みを発生させたり、誤差±1ppmとかの超高精度発振器を使うなどの手もあります。
いずれにせよ、水晶発振子が比較的周波数の精度もよく、温度による変動も少ないので向いていると考えられます。

当初はそう思っていました。しかし、まあNTP時計ですしべらぼうに高精度なものを使う必要もないだろうということで、とりあえずセラロックを使っていました。

しかし、PIC24FJ64GA004には8MHzの高速RCオシレーターが内蔵されていて、これはOSCTUNレジスタを使って発振周波数を微調整することができます(内蔵RC発振以外は調整できません)。RCオシレーターは温度による誤差の変動が大きく、精度もあまり良くなく、一般に時計のようなものには向かないと言われています。しかし、NTP時計はNTPサーバーとの同期間隔と同期した時の時計のズレから誤差を知ることができるので、OSCTUNで微調整することができるRC発振がそういう意味で大きなメリットになります。周波数自動調整で、手動調整無しに精度が出せるならそれはそれで面白い!

そこで早速プログラムを書いてみました。

すると、どうもOSCTUNは値が1変わると0.4%くらいの時間のずれが生まれることがわかりました。0.4%って小さいようにも見えますが、実際は1時間で14.4秒もずれます。1日で6分近くずれます。時計としてはちょっと残念ですね。

そこで考えました。

「OSCTUNの値をPWM制御のように短期間で1つ隣の値と切り替え続ければ実質的にチューニング段数を増やせるんじゃね?」

はい。実際にはそのPWMの時間を制御するクロックがOSCTUNで動かされてしまうので正確に比例関係になるわけではありませんが、少なくとも大小関係くらいはちゃんと出るはずです。

というわけで、OSCTUNはもともと6bitなんですが、13bitまで増やしてしまいました。
7bit増えたので精度は128倍で、つまり、ずれは最大で1時間あたり0.1秒程度ということになりますね。

#include <p24FJ64GA004.h>
#include <stdlib.h>
#include "SNTPx.h"
#include "OscTuning.h"

#define TUNE_BIT        13    //この関数としてのチューニングビット数
#define OSCTUN_BIT        6    //OSCTUNレジスタのビット数
#define OSCTUN_MASK        ((0x0001 << OSCTUN_BIT) - 1)
#define TUNE_LOWER_BIT    (TUNE_BIT - OSCTUN_BIT)
#define TUNE_LOWER_MASK    ((0x0001 << TUNE_LOWER_BIT) - 1)
#define TUNE_MAX        ((0x0001 << (TUNE_BIT - 1)) - 1)
#define TUNE_MIN        ((int16_t)(0x0001 << (TUNE_BIT - 1)) * (-1))
#define TuneValueToOscTune(tune)    (((uint16_t)tune >> TUNE_LOWER_BIT) & 0x3F)

#define ERROR_COEFFICIENT    ((250ul << TUNE_LOWER_BIT))    //OSCTUNが1変わるにはおよそ1/250=0.4%のずれが起きる
#define BIG_ERROR            (5.0 / ERROR_COEFFICIENT)    //TuneValueを1変えるに値する誤差の5倍の誤差あれば大きな誤差

int16_t TuneValue;

void Osc_SetTune(int16_t tune)
{
    tune = max(tune, TUNE_MIN);
    tune = min(tune, TUNE_MAX);

    TuneValue = tune;
}

int16_t Osc_GetTune()
{
    return TuneValue;
}

void Osc_ProcessOsc()
{
    static uint16_t cnt;
    static uint64_t LastLastUpdate;
    int16_t tune = Osc_GetTune();
    uint8_t osctun = TuneValueToOscTune(tune);
    uint64_t lastupdate = SNTPGetLastUpdateUTCMilliseconds();

    if((cnt++ & TUNE_LOWER_MASK) >= ((uint16_t)tune & TUNE_LOWER_MASK))
        OSCTUNbits.TUN = osctun;
    else
        OSCTUNbits.TUN = (osctun == 31) ? 31 : ((osctun + 1) & 0x3F);

    if(lastupdate != LastLastUpdate) {
        double OscError = GetOscillatorError();

        if((OscError > BIG_ERROR) || (OscError < -BIG_ERROR))
            Osc_SetTune(tune - (int16_t)(OscError * ERROR_COEFFICIENT + 0.5));
        else if(OscError > 0)
            Osc_SetTune(tune - 1);    // if oscillator is too fast, decrease OSCTUN.
        else if(OscError < 0)
            Osc_SetTune(tune + 1);    // if oscillator is too slow, increase OSCTUN.

        LastLastUpdate = lastupdate;
    }
}

そんなに厳密性を求めても仕方ないということで、タイマ割り込み等は使わずにmain関数のループの中でOsc_ProcessOscを呼び出すことで処理しています。

この拡張した擬似チューニング値を1変えると生まれる時間のずれが1/ERROR_COEFFICIENTです。時計合わせをした時のずれの絶対値がこのずれの5倍以内なら擬似チューニング値を1増やすか減らすかして微調整し、それより大きな誤差だったらOSCTUNが1変化すると0..4%ずれるという値に基づいて一気に目標値に近づけようとするような計算をしています。

これで1時間に1回時計合わせとかで実用上問題無いレベルの誤差に収まりました。


が、やっぱり温度依存性はすごいですね。
多分、室温が5℃くらい変わると擬似チューニング値が20くらい変わります。 0.01%/Kくらいのずれでしょうか。

詳しい温度との相関は取ってないですけど、一部からはRC温度計だの、NTP温度計だのと揶揄されていますw

0 件のコメント:

コメントを投稿