ラベル ネットワーク の投稿を表示しています。 すべての投稿を表示
ラベル ネットワーク の投稿を表示しています。 すべての投稿を表示

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月10日火曜日

ENC28J60をPICで動かす


さて、NTP時計を作るために真っ先にネットワーク関係のプログラムを構築しなくてはなりませんね。
ENC28J60はSPIインターフェースでマイコンと接続することができるので、Arduinoなどでもライブラリが作られていて、結構ポピュラーに使われているようです。PICに関して言えば、ENC28J60とともにMicrochipの商品ということもあり、Microchipが無料でライブラリを出しています。

Microchip Libraries for Applications

さて、早速ダウンロードと行きたいところですが、このページの英語を注意深く読むと、最新版のライブラリ(2013/12/20リリース)ではTCP/IPのサポートがされていないことが書いています。なので、このページ下部のLegacy MLAタブを選択して、2013/06/15のバージョンをダウンロードする必要があります。

さて、ダウンロードしてきてインストールすると、何やらいろいろなデモプログラム等がありますが、どこをどういじっていいのか見当も付きません。
しかし、いろいろと調べていると、さすがPICの神様。後閑さんのウェブページにかなり有用な記事があることがわかります。

TCP/IPスタック Ver4.5xの使い方

ここの「2.プログラム全体構成」の中にプログラムの構成のしかたが書いてあります。さすがです。

ライブラリの microchip_solutions_v2013-06-15\Microchip\TCPIP Stack にあるソースコードの中から、今回は以下のファイルを必要ファイルとしてプロジェクトに組み込みました。
ファイル名を見ればどれがどの役割を果たすプログラムかはなんとなくわかると思います。
また、Includeファイルのある場所にパスを通しておく必要があります。
MPLAB Xではプロジェクトのプロパティから左ペインでxc16-gccを選択し、Option categoriesをPreprocessing and messagesにするとインクルードディレクトリのパスを設定する項目が出てきます。

あとひとつ、最後にコンパイルのオプションを指定したインクルードファイルを用意する必要があります。用意するのは
  • HardwareProfile.h
  • TCPIPConfig.h
の2つです。 microchip_solutions_v2013-06-15\TCPIP\Demo App などから拾ってきて適当に改造すればいいですが、その「適当」がまた難しいところではあります。

HardwareProfile.hに関しては、ほぼ跡形もなく改造することになります。
めんどくさいんでもうコード貼っちゃいます。

#ifndef HARDWARE_PROFILE_H
#define HARDWARE_PROFILE_H

#include "Compiler.h"

// Define a macro describing this hardware set up (used in other files)
#define EXPLORER_16

#define MAXIMUM_PIC_FREQ  (32000000ul)


// LED Definition
#define MIN_1  (LATAbits.LATA4)
#define LED_7SEG_D (LATAbits.LATA7)
#define SEC_1  (LATAbits.LATA8)
#define MIN_10  (LATAbits.LATA9)
#define LED_7SEG_C (LATBbits.LATB2)
#define LED_7SEG_A (LATBbits.LATB3)
#define SEC_10  (LATBbits.LATB4)
#define LED_7SEG_DP (LATBbits.LATB14)
#define LED_7SEG_E (LATBbits.LATB15)
#define LED_7SEG_B (LATCbits.LATC0)
#define LED_7SEG_F (LATCbits.LATC1)
#define LED_7SEG_G (LATCbits.LATC2)
#define LED_COLON (LATCbits.LATC3)
#define HOUR_1  (LATCbits.LATC4)
#define HOUR_10  (LATCbits.LATC5)

#define MASK_7SEG_RA ((uint16_t)0x0080)
#define MASK_7SEG_RB ((uint16_t)0xC00C)
#define MASK_7SEG_RC ((uint16_t)0x0007)

#define MASK_7SEGTR_RA ((uint16_t)0x0310)
#define MASK_7SEGTR_RB ((uint16_t)0x0010)
#define MASK_7SEGTR_RC ((uint16_t)0x0030)

// Switch Definition
#define SW_RED  (PORTAbits.RA10)
#define SW_BLUE  (PORTBbits.RB13)
#define SW_GREEN (PORTBbits.RB12)
#define SW_YELLOW (PORTBbits.RB11)


// These directly influence timed events using the Tick module.  They also are used for UART and SPI baud rate generation.
#define GetSystemClock()  (MAXIMUM_PIC_FREQ)   // Hz
#define GetInstructionClock() (GetSystemClock()/2) // Normally GetSystemClock()/4 for PIC18, GetSystemClock()/2 for PIC24/dsPIC, and GetSystemClock()/1 for PIC32.  Might need changing if using Doze modes.
#define GetPeripheralClock() (GetSystemClock()/2) // Normally GetSystemClock()/4 for PIC18, GetSystemClock()/2 for PIC24/dsPIC, and GetSystemClock()/1 for PIC32.  Divisor may be different if using a PIC32 since it's configurable.

// ENC28J60 I/O pins
#define ENC_CS_TRIS   (TRISBbits.TRISB10) // Comment this line out if you are using the ENC424J600/624J600, MRF24WB0M, or other network controller.
#define ENC_CS_IO   (LATBbits.LATB10)
// SPI SCK, SDI, SDO pins are automatically controlled by the
// PIC24/dsPIC SPI module
#define ENC_SPI_IF   (IFS0bits.SPI1IF)
#define ENC_SSPBUF   (SPI1BUF)
#define ENC_SPISTAT   (SPI1STAT)
#define ENC_SPISTATbits  (SPI1STATbits)
#define ENC_SPICON1   (SPI1CON1)
#define ENC_SPICON1bits  (SPI1CON1bits)
#define ENC_SPICON2   (SPI1CON2)

// LCD I/O pins
#define LCD_CS_IO   (LATBbits.LATB8)
#define LCD_RS_IO   (LATBbits.LATB7)
#define LCD_SDO_IO   (LATBbits.LATB5)
#define LCD_SCK_IO   (LATBbits.LATB6)

#endif // #ifndef HARDWARE_PROFILE_H


もとのデモプログラムのHardwareProfile.hを見てくれてもわかりますが、ほとんどがENC28J60と関係ない表記です。何のピンが何に割り当てられているかとか、そういったことが書いてあります。なので、このNTP時計のHardwareProfile.hもそんな感じになっています。
重要なのはENC28J60 I/O pinsのところで、そこにENC28J60と接続するのに使うレジスタと、ソフトウェアから制御するチップセレクトピン(CS)を定義しています。
ちなみに、もとのデモプログラムではこの中でコンフィグレーションビットの設定をしています。が、#ifdefはできるだけ避けたいのと、自分はいつもmain.cの中で定義していたので、我流にしたがってこちらからは削除しています。

つづいてTCPIPConfig.hですが、これはデモプログラムのものに手を加えるという形にします。
割と最初のほうにApplication Optionsという項目がありますので、ここを必要最低限のものを残してコメントアウトします。

// =======================================================================
//   Application Options
// =======================================================================

/* Application Level Module Selection
 *   Uncomment or comment the following lines to enable or
 *   disabled the following high-level application modules.
 */
//#define STACK_USE_UART     // Application demo using UART for IP address display and stack configuration
//#define STACK_USE_UART2TCP_BRIDGE  // UART to TCP Bridge application example
//#define STACK_USE_IP_GLEANING
#define STACK_USE_ICMP_SERVER   // Ping query and response capability
#define STACK_USE_ICMP_CLIENT   // Ping transmission capability
//#define STACK_USE_HTTP2_SERVER   // New HTTP server with POST, Cookies, Authentication, etc.
//#define STACK_USE_SSL_SERVER   // SSL server socket support (Requires SW300052)
//#define STACK_USE_SSL_CLIENT   // SSL client socket support (Requires SW300052)
#define STACK_USE_AUTO_IP               // Dynamic link-layer IP address automatic configuration protocol
#define STACK_USE_DHCP_CLIENT   // Dynamic Host Configuration Protocol client for obtaining IP address and other parameters
//#define STACK_USE_DHCP_SERVER   // Single host DHCP server
//#define STACK_USE_FTP_SERVER   // File Transfer Protocol (old)
//#define STACK_USE_SMTP_CLIENT   // Simple Mail Transfer Protocol for sending email
//#define STACK_USE_SNMP_SERVER   // Simple Network Management Protocol v2C Community Agent
//#define STACK_USE_SNMPV3_SERVER   // Simple Network Management Protocol v3 Agent
//#define STACK_USE_TFTP_CLIENT   // Trivial File Transfer Protocol client
//#define STACK_USE_GENERIC_TCP_CLIENT_EXAMPLE // HTTP Client example in GenericTCPClient.c
//#define STACK_USE_GENERIC_TCP_SERVER_EXAMPLE // ToUpper server example in GenericTCPServer.c
//#define STACK_USE_TELNET_SERVER   // Telnet server
//#define STACK_USE_ANNOUNCE    // Microchip Embedded Ethernet Device Discoverer server/client
#define STACK_USE_DNS     // Domain Name Service Client for resolving hostname strings to IP addresses
//#define STACK_USE_DNS_SERVER   // Domain Name Service Server for redirection to the local device
//#define STACK_USE_NBNS     // NetBIOS Name Service Server for repsonding to NBNS hostname broadcast queries
//#define STACK_USE_REBOOT_SERVER   // Module for resetting this PIC remotely.  Primarily useful for a Bootloader.
#define STACK_USE_SNTP_CLIENT   // Simple Network Time Protocol for obtaining current date/time from Internet
//#define STACK_USE_UDP_PERFORMANCE_TEST // Module for testing UDP TX performance characteristics.  NOTE: Enabling this will cause a huge amount of UDP broadcast packets to flood your network on the discard port.  Use care when enabling this on production networks, especially with VPNs (could tunnel broadcast traffic across a limited bandwidth connection).
//#define STACK_USE_TCP_PERFORMANCE_TEST // Module for testing TCP TX performance characteristics
//#define STACK_USE_DYNAMICDNS_CLIENT  // Dynamic DNS client updater module
//#define STACK_USE_BERKELEY_API   // Berekely Sockets APIs are available
//#define STACK_USE_ZEROCONF_LINK_LOCAL // Zeroconf IPv4 Link-Local Addressing
//#define STACK_USE_ZEROCONF_MDNS_SD  // Zeroconf mDNS and mDNS service discovery

これは、主にStackTask.cでTCP/IP関係のプログラムを無限ループの中で呼び出していくときに使われます。定義されている項目のみ呼び出されるので、コメントアウトしたらそのモジュールは使用されません。容量の都合もありますし、不必要なものは徹底的に省いています。

そして、もう少し下へ行くと MY_DEFAULT_MAC_BYTE1~MY_DEFAULT_MAC_BYTE6というマクロが定義されています。これは、このネットワークアダプタのMACアドレスになります。
MACアドレスは6バイトのネットワークアダプタ固有のアドレスなので、世界中で重複したMACアドレスは存在しないことになっています。はい。まあ、自分は量産するわけでもないので適当に決め打ちで入力しちゃってます。デフォルトで入っているのはMicrochip社が購入しているMACアドレスですね。本気でMACアドレスを買おうとすると何百ドルもするようです…。


これで、だいたいのライブラリの構築は終わりました。

最後はmain関数の作成ですね。
ずばり、main関数はとてもシンプルになっています。

int main(void)
{
 __delay_ms(300);
 
 InitPeripheral();
 TickInit();
 InitAppConfig();
 StackInit();

 while(1) {
  StackTask();
  StackApplications();
  ProcessIO();
 }
 return (EXIT_SUCCESS);
}

非常にシンプルです。
起動してからは安定待ちとして適当に300msくらい待っています。気持ちの問題です。
InitPeripheral();はペリフェラルの初期化をしている関数です。ENC28J60で使うSPIのピン割り当てはここでする必要があります
TickInit();はライブラリのTick.cの中の関数です。詳しくは後日書くつもりですが、まあ時間を測るライブラリです。
InitAppConfig();はネットワーク関係でとても重要な関数ですので後述します。
StackInit();はStackTask.cの中にある、TCP/IPスタックを初期化する関数です。
StackTask();もStackTask.cにあり、TCP/IPスタックの処理をする関数です。
StackApplications();もStackTask.cにあり、SNTPとかのTCP/IPアプリケーションを動かす関数です。
ProcessIO();はENC28J60とは何も関係ないプログラムを処理するための関数です。

こんな感じっすかね。
InitAppConfigだけ詳しく説明しておきます。

APP_CONFIG AppConfig;
static ROM BYTE SerializedMACAddress[6] = {MY_DEFAULT_MAC_BYTE1, MY_DEFAULT_MAC_BYTE2, MY_DEFAULT_MAC_BYTE3, MY_DEFAULT_MAC_BYTE4, MY_DEFAULT_MAC_BYTE5, MY_DEFAULT_MAC_BYTE6};
static void InitAppConfig(void)
{
    // Start out zeroing all AppConfig bytes to ensure all fields are
    // deterministic for checksum generation
    memset((void*)&AppConfig, 0x00, sizeof(AppConfig));

    AppConfig.Flags.bIsDHCPEnabled = TRUE;
    AppConfig.Flags.bInConfigMode = TRUE;
    memcpypgm2ram((void*)&AppConfig.MyMACAddr, (ROM void*)SerializedMACAddress, sizeof(AppConfig.MyMACAddr));
    AppConfig.MyIPAddr.Val = MY_DEFAULT_IP_ADDR_BYTE1 | MY_DEFAULT_IP_ADDR_BYTE2<<8ul | MY_DEFAULT_IP_ADDR_BYTE3<<16ul | MY_DEFAULT_IP_ADDR_BYTE4<<24ul;
    AppConfig.DefaultIPAddr.Val = AppConfig.MyIPAddr.Val;
    AppConfig.MyMask.Val = MY_DEFAULT_MASK_BYTE1 | MY_DEFAULT_MASK_BYTE2<<8ul | MY_DEFAULT_MASK_BYTE3<<16ul | MY_DEFAULT_MASK_BYTE4<<24ul;
    AppConfig.DefaultMask.Val = AppConfig.MyMask.Val;
    AppConfig.MyGateway.Val = MY_DEFAULT_GATE_BYTE1 | MY_DEFAULT_GATE_BYTE2<<8ul | MY_DEFAULT_GATE_BYTE3<<16ul | MY_DEFAULT_GATE_BYTE4<<24ul;
    AppConfig.PrimaryDNSServer.Val = MY_DEFAULT_PRIMARY_DNS_BYTE1 | MY_DEFAULT_PRIMARY_DNS_BYTE2<<8ul  | MY_DEFAULT_PRIMARY_DNS_BYTE3<<16ul  | MY_DEFAULT_PRIMARY_DNS_BYTE4<<24ul;
    AppConfig.SecondaryDNSServer.Val = MY_DEFAULT_SECONDARY_DNS_BYTE1 | MY_DEFAULT_SECONDARY_DNS_BYTE2<<8ul  | MY_DEFAULT_SECONDARY_DNS_BYTE3<<16ul  | MY_DEFAULT_SECONDARY_DNS_BYTE4<<24ul;
}

このAppConfigという変数は、TCP/IPスタックライブラリの中でも使われてる変数です。グローバル変数はstaticじゃなければ、他のファイルからでもexternで修飾することで使えますね。すなわち、この変数を定義してやらないとコンパイルエラーになります。
まあ、デフォルトのIPアドレスとかMACアドレスとかをロードしてるだけなんで難しいことは無いと思います。

これで、とりあえずENC28J60が動くようになるはずです。

は~長かった。

ちなみに、PIC24FJ64GA004で、ここまででプログラムメモリの使用率が50%近くなるはずです。
恐ろしや…。

(余談ですが、コンパイルはXC16 v1.21でやっています。C18でコンパイルしようとしたらコンパイルが通らなくて、どうも調べたところによるとC18のバグとかいう話でした。本当かどうかは知りませんが…。)

NTP時計

とあることをきっかけで知った、Microchip製のイーサーネットコントローラーのENC28J60というものを何かに使えないかな~と考えていたところ、NTP時計を作るということを思いつきました。
要するに、インターネットを介してNTPサーバーに接続し、常に正確な時刻を刻む時計です。

電波時計を使えばいいとか、GPSの時刻情報を使えばいいとか、いくらでもNTP時計じゃない正確な時計アイディアは出てきますし、とある電子部品店でNTP時計のキットを売っていたりするので、特に最適解的、もしくは新規性のあるアイディアではないですが、ENC28J60を使って遊ぶのを主目的として作ってみることにしました。

本来ならブログで作っていくところを紹介するのがいいんでしょうが、完成したこれを紹介したくてブログを開設した経緯もあって、もう完成してしまっています(汗
というわけで、完成した写真をいくつか紹介しましょう。


これが正面から見た写真です。専用基板を設計し、作成しました。時刻は7セグで表示し、細かな情報を表示するためにグラフィックLCDもつけています。真ん中に映ってるICがENC28J60ですね。ACアダプターで動きますが、一応停電時バックアップ用に充電用電池も付けています。(停電したらルーター止まるからネットにつながらねえだろ!とか、停電したって復活したらすぐに勝手に時計合わせが行われるから必要ねえだろ!なんていうツッコミは無しでwww
使われていないUSBポートが見えますが、これはただの電源供給用のUSBです。スマホとか充電できます。Wi-Fiから有線LANに変換する、いわゆる「イーサーネットコンバーター」っていう商品を使うことでこの時計を無線化することができますが、たいていのイーサーネットコンバーターはUSBで電源供給をする仕様になっているものが多いようで、そのために付けておきました。


裏を見るとこんな感じですね。左側に映ってる四角いのが、この時計のメインプロセッサです。PIC24FJ64GA004を使用しました。入手性がよく、小型で高性能なPICと言ったらやっぱりこれですね。
ズボラな性格なので、特に回路図とかを書くこともなく、そのまま適当にパターンを設計しちゃいました。そして、基板切削機(NCフライス的なやつ)を使って基板を作りました。
右上のプラネジの手前に写っているのが電源用のDC-DCコンバーターです。PICもENC28J60も3.3Vで動くので、5Vをこいつで落としてやってます。ダイナミック点灯制御用のFETとか、抵抗、コンデンサ等できる限りの部品を1608サイズや2012サイズのチップ部品を使って小型化を図っています(おかげではんだ付けが大変だった…

次回以降は各々の機能について設計や実装の観点から個別に特集していこうと思います。
まあ、暇があったらどんどん更新していくって感じで。