ラベル NTP の投稿を表示しています。 すべての投稿を表示
ラベル NTP の投稿を表示しています。 すべての投稿を表示

2015年1月8日木曜日

NTP時計のうるう秒対応

2015年7月1日(JST)にうるう秒が挿入されることが決まりました。

うるう秒実施日一覧

すなわち、JSTで1秒ごとに8:59:59→8:59:60→9:00:00と時間が進んでいくことになります。
これは、現在、国際的な標準時を決めるための原子時計と地球の自転速度のズレ(自転速度がそもそも一定ではないため)によって発生する時間差を調整するためのものです。これを導入しないと、だんだん朝、昼、夜と言った太陽の昇り沈みに関する感覚と時刻が対応しなくなってきてしまいます。
最近に導入されたのは2012年7月1日(JST) で、慣例で12/31と6/30の24時直前(UTC)すなわち、1/1と7/1の9時直前(JST)に挿入されることが多いようです。また、いまのところ1分が61秒になる追加パターンしか実施されたことがないないようです。

さて、このうるう秒ですが、対応している時計は8:59:60の表示がされるそうです。
しかし、例えばWindowsをはじめとして多くの時計は対応しておらず、うるう秒が過ぎた後の時刻合わせで正確な時刻に合わせられるというのが実情のようです。そもそもうるう年と違ってうるう秒は不定期に挿入されるものなので電波時計やNTP等の外部と通信しているものでなければ原理上うるう秒に対応させられませんし、ずれは1秒しかなく、日常においては些細な差でしかありません。そのため実装が進まないんでしょうね。
もちろん、今回のNTP時計もうるう秒に対応しておりません。うるう秒の後の同期で正常な時刻になるだけです(自動内蔵オシレーター調整機能があるのでそれが多少ずれると考えられますが、後に収束すると考えられます)。

ですが、せっかくのうるう秒なので、これを機に実装してみることにしました。

まず、うるう秒をどうやって知るかです。
NTP時計では、SNTPプロトコルでインターネットを介して時刻情報を仕入れているわけですが、実はこのSNTPプロトコルにはうるう秒を通知する仕組みがあります。

http://tools.ietf.org/html/rfc5905#page-20

Leap Indicator (LI)と呼ばれる2bitのフィールドがSNTPのパケットにあり、これを見ることでその日にうるう秒が挿入されているかどうかがわかります。00(2進数)のときにうるう秒無し、01(2進数)のときに1日の最後の1分が61秒になる、10(2進数)のときに1日の最後の1分が59秒になることを示しています。もちろんこれはUTCなので、JSTでは午前9時を境にこのフィールドが変わります。

ネットワ-クによる時刻情報提供サービス(NTPサ-ビス)のうるう秒対応

上記のリンクには、2005年末に挿入されたうるう秒に関するNTPのLIの動きとNTP時刻の動きが示されています。
2005/12/31 0:00:00(UTC)になると同時にLIが01になり、2006/1/1 0:00:00(UTC)にLIが00に戻っています。そして、2005/12/31 23:59:60と2006/1/1 0:00:00はUTCタイムが同じ値になっています。
ここには60秒から00秒への遷移の間の細かいことは書いていませんね。サーバーが使用しているプログラムによっては、NTP時刻が3345062400.99から3345062400.00に戻る『時間逆行』仕様になっているものもあるらしいですし、時間が逆行しないように時間の進みを遅めている仕様になっているものもあるらしいので、このあたりはNTPサーバーに問い合わせをしないほうが無難かと思います。もしも時間逆行仕様だったら、単にLIを23:59:60か0:00:00かの判定に使って秒未満の桁をそのまま採用することができますが、本来のNTPの仕様では逆行しないようにするように言われている(まあ物理に沿っていると言えばこっちのほうが正しいですよね)ようなので、このあたりは本当に微妙なところかと思います。


さて、それでは実装の話に移ってみましょう。

typedef enum _tagLeapIndicator {
    LI_NoWarning = 0,
    LI_Increase = 1,
    LI_Decrease = 2,
} LeapIndicator;

まずはなんといってもLeapIndicatorの定義です。NTPパケットのビットフィールドにそのまま連動して、このような列挙型を作ってあげました。
次に、受信パケットについてLIの処理を追加します。(今まではこのようになっていました)

case SM_UDP_RECV:
// Look for a response time packet
if(!UDPIsGetReady(MySocket)) 
{
    if((TickGet()) - dwTimer > NTP_REPLY_TIMEOUT)
    {
        // Abort the request and wait until the next timeout period
        UDPClose(MySocket);
        //dwTimer = TickGetDiv64K();
        //SNTPState = SM_SHORT_WAIT;
        SNTPState = SM_HOME;
        MySocket = INVALID_UDP_SOCKET;
        break;
    }
    break;
}

// Get the response time packet
w = UDPGetArray((BYTE*) &pkt, sizeof(pkt));
UDPClose(MySocket);
dwTimer = TickGetDiv64K();
SNTPState = SM_WAIT;
MySocket = INVALID_UDP_SOCKET;
bForceSync = FALSE;

// Validate packet size
if(w != sizeof(pkt)) 
{
    break;    
}

// Set out local time to match the returned time
dwLastUpdateTick = TickGet();
dwSNTPSeconds = swapl(pkt.tx_ts_secs) - NTP_EPOCH;
// Do rounding.  If the partial seconds is > 0.5 then add 1 to the seconds count.
if(((BYTE*)&pkt.tx_ts_fraq)[0] & 0x80)
    dwSNTPSeconds++;

{
    QWORD now,  qwReceive, qwTx, qwDelay, qwPeriod;
    
    now = MillisecondToNTPTimestamp(SNTPGetUTCMilliseconds());
    qwPeriod = now - qwLastUpdateNTPTimestamp;
    if((liLastUpdate != LI_Increase) && (pkt.flags.leapIndicator != LI_NoWarning))
        qwPeriod += (QWORD)1 << 32;
    else if((liLastUpdate != LI_Decrease) && (pkt.flags.leapIndicator != LI_NoWarning))
        qwPeriod -= (QWORD)1 << 32;

    qwLastUpdateTick = TickGetQWord();

    qwReceive = PacketToNTPTimestamp(pkt.recv_ts_secs, pkt.recv_ts_fraq);
    qwTx = PacketToNTPTimestamp(pkt.tx_ts_secs, pkt.tx_ts_fraq);
    qwDelay = (now - qwLastSendNTPTimestamp - (qwTx - qwReceive)) / 2;

    qwLastUpdateNTPTimestamp = qwTx + qwDelay;

    if(qwPeriod > 0) {
        dOscillatorError = (double)((LONGLONG)(now - qwLastUpdateNTPTimestamp)) / qwPeriod;
        dwSyncCount++;
    }
}

Stratum = pkt.stratum;
liLastUpdate = pkt.flags.leapIndicator;

break;

主に改造したのは最後のブロック部分です。
まず、同期周期のqwPeriodですが、今まではうるう秒を何も考慮しておりませんでした。今回の修正では、以前の同期でLIが1秒増しで今はLI無しだった場合、以前の同期以降にうるう秒が挿入されたと言えるのでqwPeriodに1秒を足しています。同様に、LIが1秒減を示していたら1秒減らす処理をしています。
あとは、最後にliLastUpdateとして、pkt.flags.leapIndicatorを保存してあげています。

次は、LIを外から取得できるように、そういったメソッドを作ってあげます。

LeapIndicator GetLeapIndicator()
{
    QWORD lastupdate = SNTPGetLastUpdateUTCMilliseconds();
    QWORD delta = GetDeltaMs();

    switch(liLastUpdate) {
     case LI_NoWarning:
         return LI_NoWarning;
     case LI_Increase:
        if((lastupdate / 86400000) != ((lastupdate + delta - 1000) / 86400000))    //Now leap second has already passed.
            return LI_NoWarning;
        else
            return liLastUpdate;
     case LI_Decrease:
        if((lastupdate / 86400000) != ((lastupdate + delta + 1000) / 86400000))    //Now leap second has already passed.
            return LI_NoWarning;
        else
            return liLastUpdate;
    }
    return liLastUpdate;
}

BOOL IsNowLeapSecond()    //Now Leaping 23:59:60
{
    if(liLastUpdate == LI_Increase) {
        QWORD now = SNTPGetLastUpdateUTCMilliseconds() +  GetDeltaMs();

        if((now / 86400000) != ((now - 1000) / 86400000))    //Now leap second
            return TRUE;
    }
    return FALSE;
}

GetLeapIndicator()のほうですが、これは単にそのまま保存したLIの値を返してしまうと、直前のNTPサーバーとの同期は0:00:00(UTC)以前なのに今は0:00:00(UTC)過ぎという時刻のときに、その日は別にうるう秒があるわけではないのにLIが値を持ってしまうことがあります。なので、0:00:00(UTC)を過ぎたかどうかを判定して、そうでない時のみLIを返すようにしています。
じゃあ逆にうるう秒の日になったとき、うるう秒の日の前日の情報が残っててLIがNoWarningのままないんじゃね?という発想も当然出てくるかと思います。ですが、そういった場合はNTPサーバーに問い合わせない限りわかりませんし、別にそのタイミングはうるう秒が挿入されるわけでもなんでもないのでそんなに大した問題じゃないですね。

IsNowLeapSecond()は、現在うるう秒かどうかを調べるメソッドです。すなわち、23:59:60(UTC)の1秒間のみTRUEを返すメソッドです。

QWORD SNTPGetUTCMilliseconds(void)
{
    QWORD now = SNTPGetLastUpdateUTCMilliseconds() + GetDeltaMs();

    if(GetLeapIndicator() != liLastUpdate) {
        switch(liLastUpdate) {
         case LI_Increase:
            return now - 1000;
         case LI_Decrease:
            return now + 1000;
         default:
            break;
        }
    }
    return now;
}

つづいて、UTCの積算ミリ秒を取得するメソッドです。
GetLeapIndicator()は先ほど述べた通りすでにうるう秒を過ぎていたらLI_NoWarningを返しますが、liLastUpdateはNTPサーバーと同期しない限り更新されません。なので、この2者が異なる=うるう秒が過ぎてからNTPサーバーと同期されるまでの間ということになります。その場合はうるう秒に応じて1秒足したり引いたりした値を現在時刻として返しています。

さて、UTCミリ秒を表示するとき、うるう秒で1秒減のときは単に23:59:59(UTC)がスキップされるだけですので全然問題がありません。しかし、1秒増のときは23:59:60(UTC)が挿入されるので通常の手法では表示できませんよね。というわけで、そのあたりの実装をしました。

void UTCMillisecondToLocaltime(QWORD utcms, int diffmin, struct tm *pTime)
{
    if(pTime != NULL) {
        BOOL IsLeap = IsNowLeapSecond();

        if(IsLeap)
            utcms -= 1000;

        time_t tick = ((DWORD)(utcms / 1000) + diffmin * 60) & 0x7FFFFFFF;
        struct tm *ptm = localtime(&tick);
        memcpy(pTime, ptm, sizeof(struct tm));

        if(IsLeap)
            pTime->tm_sec = 60;
    }
}

diffminはUTCとのずれの分です。JSTはUTC+9なので、9 * 60を渡してもらうということになります。
1秒増のうるう秒のとき、IsLeapがTRUEになります。そのときにはNTP的には翌日0:00:00扱いになるので、とりあえず1秒引いて23:59:59にした上でlocaltime()を呼んでいます。そして、その後にstruct tmのtm_secメンバーを60にして23:59:60を示すようにしました。


これでうるう秒周りの実装が終わりました。





このように、見事にうるう秒で増える場合、増えない場合の動作ができました。

ちなみに、おまけとしてLIの値を表示する機能も追加しておきました。



さて、これで今年の7月1日が楽しみですね。

2014年6月13日金曜日

SNTP.cの改造

さて、今回はNTP時計の真髄であるSNTP.c周りの改造についてお話したいと思います。

このファイル、眺めてて真っ先に思うことは「これ、正確な時間じゃない!!!!」ってことなんですね…。はい。改造前のSNTP.cには様々な問題点が含まれています。
しかし、これは全部PIC18シリーズでも動くことを念頭に置いて設計した結果なんです。すなわち、処理を徹底的に軽くして、とりあえずだいたいの現在時刻が分かればいいな、くらいのものだってことなんですね。それはそれでコーディングのポリシーがあるから悪いことだとは思いません。

しかし、今回このSNTPパケットの解釈をし、時計合わせするプロセッサはPIC24FJ64GA004です。PIC18シリーズに比べたらかなりの処理能力の向上が見込めます。なので、多少重い処理でも、より正確な時刻を求めて計算させることくらいきっと大丈夫!

というわけで、まずはSNTP.cの改造をするために問題点を見極めてみましょう。
まず見た場所は、SNTPの主たるネットワーク処理をしている関数、SNTPClient関数です。ここでは、呼び出される度に何らかの処理をし、条件を満たしたらSNTPStateという変数を適宜変えていきます。そうすると、再びSNTPClient関数が呼ばれたときにそのSNTPStateをもとにswitchし、特定の処理をして、またSNTPStateの値を更新していくんですね。C#で言うyield的なやつ?
SNTPにはUDPが使われていて、NTPサーバーに対して時刻を要求するパケットを送ったら、時刻のUDPパケットが返ってくるわけですね。なので、その受信したパケットを処理するところが、時計合わせの最もコアになる部分なわけです。

        case SM_UDP_RECV:
            // Look for a response time packet
            if(!UDPIsGetReady(MySocket)) 
            {
                if((TickGet()) - dwTimer > NTP_REPLY_TIMEOUT)
                {
                    // Abort the request and wait until the next timeout period
                    UDPClose(MySocket);
                    //dwTimer = TickGetDiv64K();
                    //SNTPState = SM_SHORT_WAIT;
                    SNTPState = SM_HOME;
                    MySocket = INVALID_UDP_SOCKET;
                    break;
                }
                break;
            }
            
            // Get the response time packet
            w = UDPGetArray((BYTE*) &pkt, sizeof(pkt));
            UDPClose(MySocket);
            dwTimer = TickGetDiv64K();
            SNTPState = SM_WAIT;
            MySocket = INVALID_UDP_SOCKET;
            
            // Validate packet size
            if(w != sizeof(pkt)) 
            {
                break;    
            }
            
            // Set out local time to match the returned time
            dwLastUpdateTick = TickGet();
            dwSNTPSeconds = swapl(pkt.tx_ts_secs) - NTP_EPOCH;
            // Do rounding.  If the partial seconds is > 0.5 then add 1 to the seconds count.
            if(((BYTE*)&pkt.tx_ts_fraq)[0] & 0x80)
                dwSNTPSeconds++;

            break;

Microchip製のコードはこんな感じになっています。
UDPGetArray関数でUDPの受信パケットを読みだして受信サイズがちゃんとUDPパケットのサイズと合っているか確認しています。そのあとは、NTPパケットはビッグエンディアンなのでそれをリトルエンディアンに変換しています。
NTPパケットのタイムスタンプは1900年1月1日午前0時からの秒数を64bitの固定小数点型で返してきます。その64bitのうち、上位32bitが整数部で、下位32bitが小数部です。C言語の多くの処理系では現在時刻を1960年1月1日0時からの秒数で管理しており、XC16も例には漏れていないので、NTP_EPOCHで表されるその差を引いてやっています。
そして、小数部の最上位ビットを見て、そこが1なら四捨五入ということで秒を1秒繰り上げています。
はい、このライブラリは秒以下の精度を管理していないんです。せっかくのNTP時計ですからミリ秒単位で正確な時刻で刻みたいですよね。というより、コロンを0.5秒ごとにON/OFFを切り替えているので必然的にミリ秒単位の精度が必要になってきます。この点は改善が必要ですね。

ところで、NTPパケットはもちろん、NTPサーバーを出てからインターネットを経由して手元まで届きます。NTPサーバーからNTP時計までの間にはもちろん伝送経路の遅延があり、すなわち、その間に時間が進んでしまいます。なので、手元にパケットが到着した時点でそれはもはやすでに正確な時刻ではないのです。
pkt.tx_ts_secsメンバは、まあ見た目の通り、NTPサーバーがパケットを送信する瞬間の時刻です。NTPパケットには、これ以外にもう1つサーバーが送ってくる時刻情報が入っています。それは、NTPサーバーにパケットが到着した時の時刻です。すなわち、NTP時計側で時刻要求パケットを送信した時刻とその返事を受け取った時刻を記録していれば、NTPサーバーがパケットを受信してから返信するまでの時間を差し引いて1/2にすれば伝送経路の遅延時間がわかりますね。
そうやって、さらにより正確な時計合わせをすることができるようになります。ここまで説明した通り、このプログラムはNTPサーバーをパケットが出発した時刻に合わせた正確じゃない時計合わせですが、まあ、もともと1秒単位の時計合わせしかしていない時点でそんな伝送遅延にこだわるようなレベルじゃないですし、これはこれで軽い処理としての妥協点としては別に(正確な時計として使わないのならば)問題ないレベルでしょう。

もう一つ、このプログラムには問題点があります。
このプログラムでは、NTPパケットを受信したときにそのパケットに示されていた時刻をグローバル変数のdwSNTPSecondsに保管し、それと同時にGetTick関数で現在のTMR1のカウント値(32bit)を同じくグローバル変数のdwLastUpdateTickに保存しています。
後にSNTPGetUTCSeconds関数を呼び出したときに 現在のTickから前回のNTPパケットの取得時のTickを引くことで時間差がわかるので、それをNTPパケットの取得時の時刻に足して返すという仕組みになっています。
はい、 32bitなんですよ、32bit。TMR1のカウント値は、プリスケーラやポストスケーラは何も挟んでいないので、32MHzで動いているPICでは1秒で16メガ増えます。すなわち、5分弱でオーバーフローしてしまうんですね。
というわけで、SNTPGetUTCSeconds関数にはその対策が取られています。この関数が呼ばれたときはdwSNTPSecondsを現在の時刻に変更し、それと同時にdwLastUpdateTickも更新しているのです。
しかし、この仕様では「dwLastUpdateTickは変数の名前に反して前回NTPサーバーに接続した時刻ではない」「SNTPGetUTCSecondsを5分弱以上呼び出さないとバグる」という問題が残ることになります。前者は気持ちの問題ですが、後者は致命的ですね。もちろん、main関数側の実装でカバーすることはできますが、こういう仕様は私は大っ嫌いです。
Tick.cのほうの記事でも触れましたが、Tick.cではTickを48bit値でカウントしているので、SNTP.cのほうの改造だけでさらに16bit増やすことができます。しかし、それでも203.6日間SNTPGetUTCSeconds関数を呼ばなければオーバーフロー起こしてバグります。長いって言えば長いですが、到底NTPが2036年問題を起こすときやC言語の処理系が2038年問題を起こすときには及びません。だから、Tick.cのほうでTickを64bitで管理するように変更したんです。それなら36,000日以上もちます。

はい、まとまりました。
Microchip謹製のライブラリの問題点は
  • 小数以下を四捨五入している程度の精度
  • 伝送遅延を考慮していない実装
  • SNTPGetUTCSeconds関数を頻繁に呼ばないとバグる仕様
の3点のわけですね。
今回の改造では、これらを改善するコードを追加しましょう。



        case SM_UDP_RECV:
            // Look for a response time packet
            if(!UDPIsGetReady(MySocket)) 
            {
                if((TickGet()) - dwTimer > NTP_REPLY_TIMEOUT)
                {
                    // Abort the request and wait until the next timeout period
                    UDPClose(MySocket);
                    //dwTimer = TickGetDiv64K();
                    //SNTPState = SM_SHORT_WAIT;
                    SNTPState = SM_HOME;
                    MySocket = INVALID_UDP_SOCKET;
                    break;
                }
                break;
            }
            
            // Get the response time packet
            w = UDPGetArray((BYTE*) &pkt, sizeof(pkt));
            UDPClose(MySocket);
            dwTimer = TickGetDiv64K();
            SNTPState = SM_WAIT;
            MySocket = INVALID_UDP_SOCKET;
            bForceSync = FALSE;
            
            // Validate packet size
            if(w != sizeof(pkt)) 
            {
                break;    
            }

            // Set out local time to match the returned time
            dwLastUpdateTick = TickGet();
            dwSNTPSeconds = swapl(pkt.tx_ts_secs) - NTP_EPOCH;
            // Do rounding.  If the partial seconds is > 0.5 then add 1 to the seconds count.
            if(((BYTE*)&pkt.tx_ts_fraq)[0] & 0x80)
                dwSNTPSeconds++;

            {
                QWORD now,  qwReceive, qwTx, qwDelay, qwPeriod;
                
                now = MillisecondToNTPTimestamp(SNTPGetUTCMilliseconds());
                qwPeriod = now - qwLastUpdateNTPTimestamp;
                qwLastUpdateTick = TickGetQWord();

                qwReceive = PacketToNTPTimestamp(pkt.recv_ts_secs, pkt.recv_ts_fraq);
                qwTx = PacketToNTPTimestamp(pkt.tx_ts_secs, pkt.tx_ts_fraq);
                qwDelay = (now - qwLastSendNTPTimestamp - (qwTx - qwReceive)) / 2;

                qwLastUpdateNTPTimestamp = qwTx + qwDelay;

                dOscillatorError = (double)((LONGLONG)(now - qwLastUpdateNTPTimestamp)) / qwPeriod;
                dwSyncCount++;
            }

            Stratum = pkt.stratum;

            break;

受信部分はこんな感じにしました。
にしてもC言語のcase文は根底の考え方がラベルとジャンプで、各caseの中はブロックにする必要が無いので変数の定義ができずめんどくさいですね。
後半部分に改造した部分が主に入っていて、特にwhileとかifとかにもくっついていないブロックのところです。
タイムスタンプは64bitで保管し、整数部32bit、小数部32bitの固定小数点型として保存しています。はい。NTPのパケットそのままです。ただし、リトルエンディアンに変換はしています。
固定小数点は特に固定小数点型みたいなものを用意しなくても、足し算引き算に関しては整数の演算として正しい演算ができますね。というわけで、この形で伝送遅延qwDelayを計算しています。そして、送信された時刻+伝送遅延を現在時刻として保存しています。

同時に、オシレーターの誤差も計算しておいています。
現在の時刻とNTP時計によって得られた時刻情報との差を同期間隔で割っています。
また、そのような計算をしている都合上、2回目の同期以降じゃなければその誤差は有効な計算とならないので、dwSyncCountで何回同期されたかをカウントしています。

その他、Stratumの保存も行っています。StratumはNTPサーバーの時刻の純度とでも言いましょうか。NTPサーバーの時刻は別のNTPサーバーによって合わせることもできるので、そのようにして合わせた場合はStratumを増やして純度が下がっていることを示す仕組みが取られています。Stratumが1が最も正確な時計で、例えば原子時計やGPSをもとに時計合わせされたNTPサーバーがそれに値します。

ちなみに、 qwLastSendNTPTimestampはSM_UDP_SENDの中で保存しています。

        case SM_UDP_SEND:
            // Make certain the socket can be written to
            if(!UDPIsPutReady(MySocket))
            {
                UDPClose(MySocket);
                SNTPState = SM_HOME;
                MySocket = INVALID_UDP_SOCKET;
                break;
            }

            // Transmit a time request packet
            memset(&pkt, 0, sizeof(pkt));
            pkt.flags.versionNumber = 3;    // NTP Version 3
            pkt.flags.mode = 3;                // NTP Client
            pkt.orig_ts_secs = swapl(NTP_EPOCH);
            qwLastSendNTPTimestamp = MillisecondToNTPTimestamp(SNTPGetUTCMilliseconds());

            UDPPutArray((BYTE*) &pkt, sizeof(pkt));    
            UDPFlush();    
            
            dwTimer = TickGet();
            SNTPState = SM_UDP_RECV;        
            break;

このようにして大まかな機能の拡張はできたので、後はこの拡張したデータを読み出す系の関数をたくさん実装してあげればいいですね。

QWORD SNTPGetLastUpdateUTCMilliseconds(void)
{
    return NTPTimestampToMillisecond(qwLastUpdateNTPTimestamp);
}

QWORD SNTPGetUTCMilliseconds(void)
{
    QWORD qwDeltaMs = (TickGetQWord() - qwLastUpdateTick) * 1000 / TICK_SECOND;
    
    return  SNTPGetLastUpdateUTCMilliseconds() + qwDeltaMs;
}

void UTCMillisecondToLocaltime(QWORD utcms, int diffmin, struct tm *pTime)
{
    if(pTime != NULL) {
        time_t tick = ((DWORD)(utcms / 1000) + diffmin * 60) & 0x7FFFFFFF;
        struct tm *ptm = localtime(&tick);
        memcpy(pTime, ptm, sizeof(struct tm));
    }
}

char *GetSNTPServerName(char *ptr, int length)
{
    length = (length > NTP_SERVER_LENGTH) ? NTP_SERVER_LENGTH : length;

    if((ptr == NULL) || (length <= 0))
        return NULL;

    strncpy(ptr, NtpServer, length - 1);
    ptr[length - 1] = '\0';

    return ptr;
}

void SetSNTPServerName(const char *ptr)
{
    if(strlen(ptr) < NTP_SERVER_LENGTH) {
        strcpy(NtpServer, ptr);
        
        bForceSync = TRUE;
    }
}

DWORD GetSNTPQueryIntervalSec()
{
    return NtpQueryIntervalSec;
}

void SetSNTPQueryIntervalSec(DWORD sec)
{
    NtpQueryIntervalSec = sec;
}

double GetOscillatorError()
{
    if(dwSyncCount >= 2)
        return dOscillatorError;
    else
        return 0;
}

BYTE GetSNTPStratum()
{
    return Stratum;
}

この関数に含められているので気づいた方もいるかもしれませんが、接続先のNTPサーバーの変更と、同期間隔の変更も可能にしてあります。
そのために、UDPOpenEx関数の呼び出しパラメーターを多少変えたり、SM_WAITでのINVALID_UDP_SOCKETへの変遷条件を若干変更したりしています。

こんなもんですかね。

2014年6月11日水曜日

Tick.cの改造

さて、NTP時計ですが、このネットワーク周りのライブラリにはTick.cというファイルがあります。
これは、タイマ1を使用して時間をカウントするいわばストップウォッチみたいなライブラリで、SNTPライブラリもこれを使用することで今何時かを管理しています。

しかし、このカウンタは6バイト構成となっており、PICを32MHzで動かすと下2バイトくらいが秒数の小数部分のカウントになり、秒としては実質的に4バイト値となってしまいます。

もともとNTPには2036年問題があり、多くのC言語処理系には2038年問題があるので、実質4バイトでもそんなに困らないような気もしなくもないです。が、6バイト値を読みだすためにライブラリでは結構めんどくさい処理とかしている(PIC18シリーズのXC8コンパイラでは64ビット整数がサポートされていないため)のも残念なので、まあせっかくなので64bit化してしまいましょうという話です。

お話はとても簡単で、QWORD型(64bit符号なし整数:unsigned long longをtypedefしたもの)のカウンタを別途用意し、そっちでもカウントしてるってだけです。
完全に上位互換なので、6バイトのほうのプログラムを消しちゃって、64bit整数側からその旧来の関数を実装するリファクタリングをやっちゃってもいいんですが、まあ、デバッグとかめんどくさいんでプログラムメモリが足りなくなったら考えることにします…。

今回は図も何もないお話になってしまいました。ごめんなさい><