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

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日が楽しみですね。

2015年1月7日水曜日

ウォッチドッグタイマーの盲信(湘南モノレール出発信号冒進事故から学ぶ)

今回はちょっといつもとはテイストの違う記事を書いてみようかなって思います。

もう7年近く前の事故になりますが、湘南モノレールが本来停止すべき西鎌倉駅を止まれずに出発信号を冒進し、対向列車とあわや正面衝突になりかけた事故がありました。

http://jtsb.mlit.go.jp/jtsb/railway/detail.php?id=1744

VVVFインバーターが誤作動して力行状態のままに固定されてしまい、最終的に運転士が非常ブレーキ保安ブレーキを取り扱ったにもかかわらず十分な減速が得られなかったというのがこの事故の原因です。
幸い、死傷者等はいませんでしたが、何重にも安全装置が付けられている鉄道で安全に止まれなかったのかというのは非常に興味深いところではあります。運転士が異常を感じたにもかかわらずただちに停止しようとしなかったというヒューマンエラー的なところから、ノイズを受けやすい配線構造、VVVFインバーターの制御装置がノイズによって異常な信号を受信したときにフェイルセーフにならないプログラムのバグなど、さまざまな原因が絡み合って、最終的にこのような事故といった形で表面に現れてきました。

ここはプログラミング系のブログということもあり、特にそのプログラムのバグのあたりから得られる教訓と設計思想について考えていきたいと思います。


VVVFインバーターの制御プログラムは下図のようになっていたようです。

 (事故調査報告書 P.10図1より引用)

ウォッチドッグタイマー(WDT)はこの手のソフトウェア監視システムとしては非常に一般的なものです。ウォッチドッグ=番犬がメインプロセッサーやクロックから分離された別系統として独自に動作しており、一定時間以上メインプロセッサ側からWDTリセット命令が実行されなかったらシステム全体にリセットを掛ける仕組みになっています(文中で使われる『リセット』という言葉が、WDTをリセットすることなのか、WDTがシステム全体をリセットすることなのかがわかりにくくなっていますが、是非混同しないようにお願いします)。万が一ソフトウェアがハングアップしたり、その他のハードウェア故障等でクロックが停止した場合にリセットを掛けられるということですね。

さて、フローチャートを見ると割り込みによって時間をカウントし、そのカウントされた時間によって10msごとにメインプログラムで加減速シーケンスを処理するようになっています。至ってシンプルで、誰でも作りそうなプログラムです。ここに一体どういう問題が起きたのでしょう。

右側に不正割り込みがあったときの処理が書いてあります。ここで、他の割り込みを禁止するようになっています。どういう意図で割り込みを禁止して戻るだけにしたのか、もしくはハードウェアの制約等でそうなってしまったのかはわかりませんが、こういう仕様になっていたそうです。
また、3両編成の車両の1両のみケーブルの断面積が小さいためノイズがアースに流れにくいなどといった条件も組み合わさった結果、電源装置から発生したノイズがVVVFインバーターの制御装置へ入り込み、それを不正な割り込みとして拾ってしまいました。そうなったら最後、タイマーはインクリメントされないがWDTがリセットされ続けるという状態になってしまい、加減速シーケンスが今後一切処理されることの無いまま、WDTも自らが監視している装置に不具合が起きていることを認識できない状態になってしまいました。そのため、ノイズによる不正割り込みが起こる直前の状態、すなわちこの時は力行状態に固定されてしまったのです。


さて、このトラブルからどういった教訓が得られるでしょうか。

「ウォッチドッグタイマーは一定時間以上リセットされなくなったとき、すなわちハングアップしたときにリセットを掛ける機能」という理解だけでは甘いことがあるということです。「一定時間以上リセットされないのならハングアップした」は必要十分条件ではなく十分条件でしかないということですね。すなわち、「ループのいつも通るところにリセット命令を置いておけばいいや」と機械的にプログラムを書くのでは、本当に有事のときにWDTが正常に作動してくれないことがあるということです。

特に、割り込みはいわばマルチスレッドプログラミングです。例えばタイマー割り込みで一定時間ごとにWDTをリセットするプログラムが意味が無いということはわかるでしょう。なぜならば、メインプログラムがハングしてもプロセッサーが動いてさえいれば割り込みは受け付けられますから延々とWDTはリセットされ続けます。
またその逆でも問題は起こりえます。割り込みですべての処理をしていたとして、メインプログラムでWDTをリセットし続けたら、例えば何らかの要因で割り込みが発生しなくなってしまって一切の処理が行われない状態になってしまっても、WDTは延々とリセットされ続けます。
また、今回のように処理をするにあたる条件かどうかの分岐の外でWDTのリセットを掛けるプログラムにしても問題が起きます。
なので、このような状態ではWDTは何も本来の意味をなさないわけです。WDTのリセット命令を置く位置は、本当に慎重に吟味しなければなりません。


今回のこの事故を受けて、湘南モノレールの車両メーカーは非常ブレーキが投入された場合に主回路を遮断する処理の追加と、WDTのリセット処理を加減速シーケンスの直後への変更を制御プログラムに行ったようです。(事故報告書 P.67 6.2)

前者はよりフェイルセーフを確固なものにするための変更ですね。今まで独立していた非常ブレーキとモーター制御に対して、非常ブレーキ作動時にモーターへ電流が流れないようにプログラムを変更したということですね。

後者はWDTのリセット位置の再検討ですね。これは当然の対応です。

(事故調査報告書 P.10図1を一部編集)

おそらく、事故報告書の文面から上図のようなフローチャートのプログラムに変更されているはずです。これによって、加減速シーケンス処理が一定以上行われていないことがすなわちWDTへのリセット命令が実行されないということになり、今回のケースにおけるバグは解消されました。


また、これ以外にも、運転の取り扱いマニュアルに「非常ブレーキを掛けても減速感が無い場合はレバースハンドル(進行方向を逆転するためのスイッチ)を「切」にする(筆者注:おそらくモーターへの電流を遮断するという意味だと思われる)」といったことも付け加えられたようです。(事故報告書 P.67 6.1)
実はこれはとても重要です。プログラミングをする人ならば、プログラムへのバグの混入は避けられないものであることは承知しているかと思います。どんなにデバッグしても、どんなに動作検証をしても、思いもよらないところでたまにバグることがあるかと思います。もちろん、ソフトウェアの構造設計やプログラミングのスキルによってそれはある程度低減させることはできますが、「100%完璧なプログラムでいかなる状態でも確実に動作します」なんて言える人はどこにもいません。なので、このようにソフトウェアの外で安全を担保する仕組みはとても重要なのです。



さて、それではWDT使用時の注意点をいくつか私なりにまとめてみました。今回の事故から得られる教訓以外にも、重要だと思う点は挙げたつもりです。

プログラムの検証段階からWDTを使用しない

これはとても重要です。まだ検証が十分に行われていないプログラムは余裕でバグります。
しかし、WDTを使用していたら、そのバグによって起こった異常状態からすぐさまリセット処理を行い回復するでしょう。そうなった場合、プログラムのバグが人間の目に届きにくくなります。
WDTによるリセット処理が行われるのは正常な状態ではありません。そのようなリセット処理は出番が無いのが一番です。 なので、WDTの導入はソフトウェアが完成した、もうこれでリリースできるぞというときに導入すべきです。

WDTのリセット命令を複数か所に入れない

これもとても重要です。複数か所にリセット命令を入れてしまうと、万が一1か所で問題が起きても他の場所に設置されたリセット命令でWDTがリセットされ続け、正常にWDTによるリセット処理がされないという状態になりえます。WDTのリセット命令は1か所だけ、これは鉄則です。

割り込み処理中にWDTリセット命令を置かない

これは上でも少し触れました。割り込みはメインプログラムで何が起きていようと発生するため、割り込み処理中にWDTリセットを行うとメインプログラムでのハングアップに気づけないことが十分にあり得ます。なので、WDTのリセット命令はもっとも優先度の低いプログラムに置くというのは鉄則です。
また、今回の事故のように、メインプログラムでも割り込みに依存する場所で問題が起きる場合が考えられるので、そのあたりも含めて慎重に検討しましょう。

WDTリセット命令はクリティカルな処理の近くに置く(不用意にリセットしまくらない)

これが今回の湘南モノレールの事故を踏まえた教訓ですね。
クリティカルな処理を行うかどうかの分岐の外側にWDTリセット命令を設置してしまったがために、 クリティカルな処理が行われないままWDTがリセットしまくられるということになってしまいました。なので、クリティカルな処理の近くで、かつ周期的にリセットがかけられる位置に置くべきです。
「クリティカルな処理は不定期に行われる」という場合は困りますね。ケースにもよるでしょうが、どこかその周辺の定期的に呼ばれる場所に置くしかないんですかね。ちなみに、「クリティカルな処理が行われる周期が大きすぎてWDTのリセット周期に間に合わない」とか「複数クリティカルな処理がある」といったケースはもはや信頼性を求められる機器では設計を考え直せという話になるかと思います。

WDTを盲信しない

最後にこれです。異常をソフトウェア外で感知した場合、もしくは人間が緊急停止の操作をした場合などは、ハードウェアレベルでそれを優先し安全に停止するようなシステムを検討すべきです。WDTによって想定しえないトラブルから回復できるかもしれないですが、でもそれは万能ではありません。あくまでも保険だという気持ちを持って、WDTを信用しすぎないことが重要です。想定しえないようなトラブル時に復帰する機能が正常に動くかなんて検証することすら難しいですもんね。


いかがでしょうか。

まあ、私は趣味のプログラマーなのでそこまで人命にかかわるような装置の組み込みプログラムを書くことは滅多に無いのですが、でも安全は大切です。WDTを導入するようなプログラムを書くときは、是非ともこれくらいは頭に入れながら設計したいものですね。
もしかしたら、現場のプロのプログラマーの間ではこれくらい当たり前で、さらにもっと体系化された知識があるのかもしれません。そういう情報を入手できる機会があったら是非ともまた吸収していきたいなと思います。


あと、盲信は禁物です。この記事を読んでWDTを導入したけどうまく動作しなくて命の危機に見舞われたとか、そんなことの無いように、あくまでも自分で責任を持って考えながら、もしくは勉強しながらやっていただけたらなと思います。

2014年7月15日火曜日

PIC24FJ64GB002でUSBメモリーにアクセスする

最近、秋月電子でPIC24FJ64GB002の取り扱いが始まりました。
USB-OTGに対応したPICとしてはかなり有名どころで、多くの書籍やウェブページでも取り上げられており、資料は豊富にあります。にもかかわらず入手性は最悪で、国内の通販では共立電子くらいでしか取り扱っていなくて、さもなくばDigiKeyとかで海外から取り寄せるくらいしか方法はありませんでした。
しかし、ついに秋月電子で取り扱いが始まりました。秋葉原に行くだけで買える!しかも、共立電子の半額以下という破格の値段です。

というわけで、早速買ってきました。

 
手前がPIC24FJ64GB002で、奥がPIC32MX250F128Bです。
この2つ、前者は16bitマイコン、 後者は32bitマイコンでアーキテクチャも何もかも全く違うのかなと思ったら、なんとまあピンコンパチ(多分)でした。少なくとも電源やUSB周りは同じで、PIC32MXのほうをいじってみたときに使った回路をそのまま転用することができました。
というわけで、そのPIC32MXをいじったときに最終的にいきついたFatFsを動かすお話に用意したプログラムをPIC24FJ64GB002に移植してみました。


と言ってもほとんど苦労はしません。

そもそもこのデモプログラムはPIC24FJ64GB004でも動くように作られてて、多分これと002の違いはピン数とかパッケージとかその程度です。また、FatFsは完全にデバイス非依存で、型の大きさとかに気をつけておく程度でちゃんと動作はしてくれるはずです。というわけで、プログラムの修正はほとんど必要ありません。


まずは前回のプロジェクトのプロパティからDeviceをPIC24FJ64GB002に変更します。
…と言いたいところですが、PIC32MXのほうの設定を消すのはもったいないです。かと言ってバックアップとってゴニョゴニョやるのもめんどくさい。というわけで、プロジェクトのコンフィグレーションを追加してしまいましょう。MPLAB Xのプロジェクトはコンフィグレーションと呼ばれるものを複数作っておくことで、同じ構成のプログラムを別のデバイスに簡単に移植できるようになっているようです。もちろん、コードがそれに対応していたらの話ですが。

プロパティのカテゴリーの欄の下にある「Manage Configurations...」というボタンを押すとこんな画面が出てきます。


Duplicateで既存のコンフィグレーションをコピーできますが、まあコピーしてもコンパイラが違うとコンパイラの設定が全部吹っ飛んじゃうんであまり意味無いでしょう。ということで、Newを押して、適当な名前のコンフィグレーションを作ります。そして、Set Activeボタンを押せば、そのコンフィグレーションがアクティブになります。


そして、作ったPIC24FJ64GB002のほうのコンフィグレーションに対して、設定をしていきます。デバイスが未設定なので、右上のデバイスの欄からPIC24FJ64GB002を選択します。そして、コンパイラをXC16に設定します。そして前回同様、インクルードファイルのパスとヒープ領域の容量を設定してあげます。


次に、ソースコードの修正を若干します。修正箇所は、コンフィグレーションビットの設定とマイコンの初期化のプログラムのみです。

とは言っても、デモプログラムのほうにPIC24FJ64GB004のコードが入っているので、プリプロセッサの__PIC24FJ64GB004__を__PIC24FJ64GB002__に変更してやるだけです。というより、#if definedで追加してやる形にしました(具体的なコードはMicrochipの著作物なので掲載は遠慮しておきます。各自MLAのサンプルコードからコピーしてください)。

#if defined(__PIC24FJ64GB002__) || defined(__PIC24FJ64GB004__)
    //中略
#elif defined( __PIC32MX__ )
    //中略
#else
    #error Unsupposed Processor
#endif

コンパイルは、クリーンビルドをしてください。PIC32MXのほうの何かが残っているとリンクで謎のエラーが起きて焦ります。

そして、書き込みが終わればUSBメモリーを挿して、めでたくサンプルファイルの書き込みがされて完成です。

めでたしめでたし。



ここまで書いてなんですが、値段で見ると、PIC24FJ64GB002が340円、 PIC32MX250F128Bが360円です。20円差でより高性能なPICが買えるなら、別にPIC32MXのほうでいいんじゃないかなーって気はします。
まあでも最初に言った通り、PIC24のほうは資料の豊富さは格別です。気が向いたらBluetooth Stackとかに手を出してみようかなー。

2014年7月14日月曜日

NTP時計がフリーズするバグの特定

さて、ここのところあまりブログでは話題にしていなかったNTP時計ですが、全く何もやっていたわけではありません。実は、恐怖の「数日~十数日に1回フリーズするバグ」が起きていました。はい、バグの中では最もタチの悪いやつですね…。

このバグの原因がわかるまでは、いろいろな原因を疑っていました。算術エラー割り込みだとか、A/D変換完了待ちループにおける不具合だとか…。
症状はこんな感じでした。
  • 時計のカウントが止まる(時計の値が進まなくなる)
  • 7セグは正常に表示されている(ダイナミック点灯制御は生きている)
ここからわかることは、メインルーチンがどこかで止まっているであろうってことと、PICのクロックが停止したとかそういうハードウェア寄りの問題ではないことです。
NTPサーバーより取得した時刻はタイマ1でカウントしていますが、 「タイマ1の値を取得して時刻に変換して7セグ表示値として設定する」という作業はメインルーチンでやっています。そして、ダイナミック点灯制御は割り込みでやっています。ダイナミック点灯制御が生きていて時刻が進まないということは、割り込み自体は起こり続けているということなので、CPUが止まっているわけではありませんが、何かしらの理由でメインルーチンが動いていないということです。そのため、何かソフトウェアに由来する問題であろうことが予想されます。

頻度ですが、割とすぐに起こるようなものではなく、止まった日時は
6/26 18:30頃
7/1 7:00頃
7/4 10:00頃
7/14 6:00頃
でした。一部記録が曖昧ですが、まあおおまかな日時はこれであっているはずです。フリーズするバグだけどもダイナミック点灯制御は生きているバグだったので、止まった時刻が容易にわかるのがせめてもの救いです。
しかし、これだけ間隔があいていると、なかなかデバッグも容易ではないですね…。


まずは、算術演算エラーを疑いました。
いわゆる、ゼロ除算エラーです。PIC24Fシリーズはゼロで除算すると無限ループにはまったりするわけでもなく、ちゃんとエラーとして割り込みを起こしてくれるようです。
この割り込みのハンドラは作っていなかったので、何かの拍子にゼロ除算が起こって割り込みが発生した上で、 ハンドラで割り込みフラグをリセットしていないから割り込みから抜け出せず、しかし、PIC24Fは多重割り込みが受け付けられるので、ダイナミック点灯制御の割り込みが算術演算エラーの上で割り込まれて動作していると考えました。
しかし、この予想には、1つ重大な間違いがありました。算術演算エラー割り込みは、割り込みの優先度が11に設定されています(ユーザー変更不可)。しかし、周辺モジュールや外部割込みの優先度は0~7にしか設定できず、すなわち、どんな周辺モジュール割り込みよりも先に算術演算エラー割り込みは起こり、算術演算エラー割り込み中に周辺モジュールが割り込みを起こすことはありえません。
すなわち、この予想は間違っていると言えます。

次に、A/D変換の終了待ちの問題を疑いました。
A/D変換は時間がかかるので、通常、開始の指示を出してからしばらくして終了しているかを確認し、値を読み出します。その終了の認識の方法は、無限ループでフラグをポーリングし続けて確認するだとか、割り込みで確認するだとか、いくらかあります。今回、NTP時計では無限ループの方法を使用していました。

AD1CON1bits.SAMP = 1;
while(!AD1CON1bits.DONE);

これ自体は何もおかしくないコードです。実際、MicrochipのA/D変換のドキュメンテーションにもこのように無限ループでポーリングし続けて変換完了を待つサンプルプログラムが書いてあります。
もしも、これが何かしらの問題で変換が終わってもAD1CON1bits.DONEが1になってくれなかったら、ここでプログラムが止まってしまいます。 要するにハードのバグを疑いました。しかし、そんな記事ググっても無ければ、実際にここで止まっている保証もありません。しかし、原因がわからないと疑心暗鬼になってしまって、特にこういう条件次第では無限ループになりそうなところを疑ってみたくなってしまうんですね…。


フリーズの原因を特定するために、いわゆるprintfデバッグと呼ばれるものの類のコードを実装してみました。
液晶は電源さえ入っていれば表示内容を保持するので、マイコンが何かしらの原因で止まっても、液晶は表示を保ってくれます。なので、特定のコードを通過したときに特定の何かしらの文字列を表示させるプログラムを実行しておけば、何が表示されているかでフリーズ箇所を絞れるというものです。

実際には文字列を出力したわけではなく、点を液晶に打ちました。実際に時計として使いながらのデバッグなので、あまり表示内容を乱したくないですしね。
最初はある一定の処理を終えるごとに点を1つずつずらしていくプログラムにしましたが、いざフリーズしてみると、確かに点が表示されているのはわかるものの、それが何段目のピクセルなのか判別がとてもじゃないですがつきません。それくらい予想してプログラム書けばよかった…。

というわけで、次は線にして、線の長さを順に伸ばしていくようにしました。それなら、一生懸命ドット数を数えればエラー箇所がわかります。
実際は、小型液晶ゆえにドットの個数を肉眼で数えるのは厳しいです。なので、フリーズしたら写真で撮って拡大して見ました。

はい、その写真がこちらになります。


画面の右下にドットを打ってありますが、6つのドットが表示されていることがわかります。

で、6ドット分の線が表示されているときに実行中のプログラムはどこかなと思ってソースコードを見てみたら、A/D変換を終えて7セグの明るさの設定値をセットする関数でした。A/D変換の完了待ちではありません。
で、問題のコードがどうなっていたかというと、こうなっていました。

void LED7SEG_SetBrightness(uint8_t value)
{
    uint32_t buf = value + 1ul;

    buf = (uint64_t)buf * buf * buf * PWM_PERIOD / (256ul * 256 * 256);

    PWMOnStart = PWM_PERIOD - buf;
    PWMOffStart = buf;

    PWMOnStart = min(PWMOnStart, PWM_PERIOD);
    PWMOffStart = min(PWMOffStart, PWM_PERIOD);

    Brightness = value;
}

この中で疑わしいところを探します。
掛け算とか割り算とかキャストとかは実際XC16コンパイラがどんなふうに展開しているかが見えないところなのでそういうところについつい目が行ってしまいますが、そこは信用するとした場合、もう残っているのはPWMOnStart変数等に代入しているところくらいしかありません。

( ˘⊖˘) 。o(待てよ…)

このPWMOnStartやPWMOffStartという変数は、7セグのダイナミック点灯用の割り込み中にも呼び出されるものです。
こういう、メインルーチンと割り込みルーチンの両方から参照される変数には細心の注意を払う必要があります。過去から現在にわたってプログラマーを苦しめ続けているマルチスレッドのトラウマ的存在としてとても有名なものです。

void _ISR __attribute__((__auto_psv__)) _T2Interrupt(void)
{
    static uint8_t cnt;
    IFS0bits.T2IF = 0;

    if(cnt++ & 0x01) {
        TMR2 = PWMOnStart;

        Show7SEG();
        LED_COLON = Colon;
    } else {
        TMR2 = PWMOffStart;

        LATA &= ~(MASK_7SEG_RA | MASK_7SEGTR_RA);
        LATB &= ~(MASK_7SEG_RB | MASK_7SEGTR_RB);
        LATC &= ~(MASK_7SEG_RC | MASK_7SEGTR_RC);
        LED_COLON = 0;
    }
}

こうやって割り込み中に7セグのON時間やOFF時間を制御するのに使っています。
PWMOnStartとPWMOffStartは0~PWM_PERIODの値になり、TMR2はPWM_PERIODになると割り込みが入るようにコンパレーターをセットしています。PWMOnStartやPWMOffStartが極端にPWM_PERIODに近かった場合、割り込み処理中に次の割り込みが入ってしまい、すなわち、_T2Interrupt関数を抜けた瞬間に再び_T2Interrupt関数に入ってしまいますが、PWMOnStartとPWMOffStartは足してPWM_PERIODになるようにしてありますので、片方が極端にPWM_PERIODに近くてももう片方は近くなくなるので、そっち側でメインルーチンにCPUを割くことができます。

はい、上の表現、めちゃくちゃ強調している意味、わかるひとにはわかりますね。

本当に、常に足してPWM_PERIODになるの?


こういう複数のスレッドにまたがって使われる変数は、一連の変数書き換えシーケンス中に割り込みが起こった場合を想定する必要があります。

もうお分かりでしょう。
LED7SEG_SetBrightness関数の5行目、PWMOnStartの値が代入されてから、次の行でPWMOffStartの値が代入されるまでの間にタイマ2の割り込みが入ったらどうなるでしょう?
しかも、もともとはPWMOffStartが極端にPWM_PERIODに近い値で、 次にPWMOnStartを極端にPWM_PERIODに近い値にしようとしたタイミングでの割り込みだったらどうなるでしょう?

両方とも、極端にPWM_PERIODに近い値で割り込みが入っちゃうんです。

そうなった場合、もうおしまいです。7セグのON表示処理をし終わったら直ちにOFF表示処理がされ、それが終わったら直ちにON表示処理を…といった状態になってしまい、二度と割り込みルーチンから抜けることがなくなってしまいます

これが今回のバグの原因だったんですね。


さて、ここまででこれがバグの原因だったということで片がつきました。しかしそれは情況証拠であって検証したわけではありません。そもそも、数日経ってやっと発動するかどうかのバグなのに、安定動作してると主張するには一体何日待てば良いのよって話です。
しかし、ラッキーなことに、この変数書き換え中の割り込みがフリーズの原因だった場合、フリーズの状態からそれを裏付けることができます。

それは、7セグの明るさです。

バグがあったとされたのは7セグのON処理時とOFF処理時の時間の長さを変えることで7セグの明るさを調節するプログラムですが、このバグが発動して割り込みルーチンから抜けられていないときは、ON処理とOFF処理の時間がともに最小で終わってしまっているということです。すなわち、デューティー比はおよそ1:1、少なくとも「明るいモード」や「暗いモード」の間の明るさに見えるはずです。

はい、実際に見比べてみましょう。
そのためには、外が真っ暗な時間に部屋の蛍光灯を付けて、同じ位置から同じ露出設定で写真を撮ってみれば、写真として客観的に明るさを見られますね。


上からフリーズ状態、輝度最高、輝度最低のモードです。
本当は三脚使って完全に定点撮影にすればよかったんでしょうが、面倒だったので同じ位置に立って手持ちで撮影しました。

もう一目瞭然ですね。写真でも明るさの違いがよくわかります。

というわけで、原因はこれで確定ってことで良いでしょう。

対策は簡単です。PWMOnStartとPWMOffStartの書き換え中は割り込みを禁止してやればいいだけです。

void LED7SEG_SetBrightness(uint8_t value)
{
    uint32_t buf = value + 1ul;

    buf = (uint64_t)buf * buf * buf * PWM_PERIOD / (256ul * 256 * 256);

    LED7SEG_DisableInterrupt();

    PWMOnStart = PWM_PERIOD - buf;
    PWMOffStart = buf;

    PWMOnStart = min(PWMOnStart, PWM_PERIOD);
    PWMOffStart = min(PWMOffStart, PWM_PERIOD);

    Brightness = value;

    LED7SEG_EnableInterrupt();
}

はい、これでPICを書き換えて完成です。
このように、途中でスレッドが切り替わってはいけない部分のことを、プログラミング用語で「クリティカルセクション」と言います。C言語にはそのような機能はありませんが、言語によってはそれを実現するための機能があるものもあります。
マルチスレッドでは、片っ端から「ここでもし割り込みが起こったらどうなる?」ということを念頭に置いてプログラムを書かなければなりません。すごく大変なことですが、それを怠ると今回みたいなバグを誘発してしまいますし、何しろこの手のバグは再現性がなかなか無いので原因を探るだけでも一苦労です。こういうバグを出さない、もしくは出してもこれが原因だと断定できるようになるまでは結構なプログラミングの習熟度が必要かと思いますが、まあ、この記事を読んでくださった皆さんももしこのような再現性の低いバグに出会った時は、この記事のことを思い出してくれればなと思います。


これで安定して動作してくれ~~~~~~

2014年7月1日火曜日

I2C小型液晶を動かす

最近、秋葉原のaitendoが閉店セールか何かで50%OFFセールをしています。なんか店舗を移転するそうですね。
そこで、16X2-SPLC792-I2CというI2C接続の液晶が売っていました。無論、液晶モジュール単体だけでは使い勝手が悪いので、変換基板セットで買いました。
それにしても、変換基板セットで税抜き375円。だいぶ安いです。3枚以上買おうとすると通常価格になるようなので、1枚当たりの価格が一気に跳ね上がるという"逆数割"ですが、まあ個人で使う分にはなかなかそんなに大量に買わないので問題ないでしょう。

 

買ってきたものがこんなかんじです。部品は全部自分で実装しますが、チップ部品も比較的大きめのものを採用してくれているので、配線はし易いと思います。 ただ、液晶の裏表は間違えないように気をつけてください。バックライトモジュールに上手くはまる方向を意識しながら組み立てれば大丈夫です。


組み上げるとこんなかんじになります。インターフェースにはピンヘッダを取り付けておきました。

ところで、この液晶モジュールにはSPLC792Aという液晶コントローラーが使われているようですが、どうも秋月で売っているI2C小型液晶AQM0802Aのコントローラーと互換性があるらしいです。実はこのAQM0802A、NTP時計を作る前に作ったプロトタイプで使っていたんですね。面白いことに、このaitendoの液晶変換基板のピンアサインまで秋月の液晶のピンアサインと同じになっています。どちらが先か知りませんが、互換性を意識して作ったんですかねw

というわけで、プロトタイプの基板に差し替えて使ってみましたが、上手く動きません…

I2Cスレーブアドレスが違うんじゃないかとか思ってデータシートを見たところ、コントローラーのピンを2つ分使って2bit分のアドレスを自由にカスタマイズできるとかいてありましたが、コントローラーのピンが表に出ているわけもなく、結局4通り手当たり次第にやってみることにしましたが、やはり動きません。

そしていろいろとコードを眺めていたが末にわかったのですが、一連のI2Cのシーケンスを送った後に27usのディレイを、コマンドの方には入れていたのですが、データの方には入れていませんでした。秋月の液晶のほうはそれで動いてくれていたんですが、それを入れている記事等が見つかったので、実際に入れてみました。

見事動きました。秋月の液晶のほうが、コントローラーチップの処理速度が速いってことなんですかね。まあ、なんて言うか、規格外の動かしかたを今までしていたようです。データシートをよく読んでなかったからこんなことろで苦労してしまったんですね(汗

結果、プログラムはこんなかんじになっています。

#include <p24FJ64GA002.h>
#include <stdint.h>
#include "i2c_lcd.h"
#define FCY 16000000UL  //32MHz/2
#include <libpic30.h>

#define LCD_SLAVE_ADDR  (0x3E << 1)

void i2cl_WaitForBusy(void)
{
    while((I2C1CON & 0b0000000000011111) | I2C1STATbits.TRSTAT);
}

void i2cl_Start()
{
    i2cl_WaitForBusy();
    I2C1CONbits.SEN = 1;
}

void i2cl_Stop()
{
    i2cl_WaitForBusy();
    I2C1CONbits.PEN = 1;
}

uint8_t i2cl_Write(uint8_t data)
{
    i2cl_WaitForBusy();
    I2C1TRN = data;
    i2cl_WaitForBusy();

    return I2C1STATbits.ACKSTAT;
}


void i2cl_SendCmd(uint8_t cmd)
{
    i2cl_Start();
    i2cl_Write(LCD_SLAVE_ADDR);
    i2cl_Write(0x00);
    i2cl_Write(cmd);
    i2cl_Stop();
    __delay_us(27);
}

void i2cl_SendData(uint8_t data)
{
    i2cl_Start();
    i2cl_Write(LCD_SLAVE_ADDR);
    i2cl_Write(0x40);
    i2cl_Write(data);
    i2cl_Stop();
    __delay_us(27);
}

void i2c_Init()
{
    TRISB |= 0x0300;
    I2C1BRG = 0x0013;
    I2C1CON = 0x8000;
}

void i2cl_Init()
{
    i2c_Init();
    __delay_ms(40);
    i2cl_SendCmd(0x38);
    i2cl_SendCmd(0x39);
    i2cl_SendCmd(0x14);
    i2cl_SendCmd(0x70);
    i2cl_SendCmd(0x56);
    i2cl_SendCmd(0x6C);
    i2cl_SendCmd(0x38);
    i2cl_SendCmd(0x0C);
    i2cl_SendCmd(0x01);
    __delay_ms(1);
}

void i2cl_Write1stLine(char *text)
{
    i2cl_SendCmd(0x03);
    while(*text)
        i2cl_SendData(*text++);
}

void i2cl_Write2ndLine(char *text)
{
    i2cl_SendCmd(0xC0);
    while(*text)
        i2cl_SendData(*text++);
}

あ、プロセッサはPIC24FJ64GA002です。NTP時計は表面実装にしたかったので004を採用しましたが、この2つはアーキテクチャが非常に似ているのでかなりプログラムが移植しやすいです。

これで、見事ソフトウェアの変更なしで液晶を差し替えるだけで動いてくれるようになりました。



ソフトウェアの変更なしで差し替えるだけで動くってことはI2Cスレーブアドレスも同じなんですよね…。なんていうか、せっかくなら同じI2Cバスに並べて動かしたかったと思うと、うーん、違ってもよかったような…。

2014年6月21日土曜日

XC16でコンパイル最適化を掛ける

さて、ここ暫くの間NTP時計を家で運用していますが、特に大きなバグ等にはぶち当たっていません。めでたく安定動作しております。

ところで、コンパイラにはコンパイル最適化というものがあります。C言語のコードは基本的に人間様への可読性が命です。アルゴリズムとかそういう根本的なところではスピードやコードのコンパクトさを意識する必要はあるかもしれませんが、細かな記法において、わざわざ人間様への可読性を大幅に下げてまでスピードを上げるという考え方には懐疑の念を抱かざるを得ません。もちろん、熟練したプログラマーとして可読性を維持したまま高速・コンパクトな記法をできるのならばそれに越したことはありませんが。

はい、話がずれてきましたが、要は、コンパイラには「人間様が多少非効率なコードを書いたとしても、それをコンパイル時に効率的なコードに変換する機能」が求められます。それが、コンパイル最適化です。

しかし、コンパイル最適化には、特にPICのような組み込みマイコンではしばしば問題が起きます。

例えば、EEPROMアクセス。PIC16シリーズとかPIC18シリーズでは、EEPROMに書き込むときに、書き込みシーケンスとしてEECON2に0x55を書き込んだあとに0xAAを書き込み、そしてEEPROMにデータを書き込むという動作をしなければなりません。これは、偶発的なトラブルで不用意にEEPROMが書き換えられてしまわないように冗長性を持たせるという意味が込めてあります。
このようなクリティカルなコードを実行する場合、C言語の開発であってもインラインアセンブリで書いてしまうことも多いとは思いますが、 仮にC言語で書いたとしましょう。

EECON2 = 0x55;
EECON2 = 0xAA;
EECON1bits.WR = 1;

はい。こんな感じになるかと思います。
これはおそらく最適化を掛けなければ正常に動作するでしょうが、最適化を掛けた場合、コンパイラが「EECON2に0x55を書き込んだ直後に0xAAを書き込んでるし、その間では参照されていないから0x55を書き込む必要なんて無いね」と判断しかねません。もっと言えば、この先でEECON2を読み出すこともおそらく無いでしょうから、0xAAを書き込むコードすらコンパイル時に消されてしまうかもしれません。

これは、ときに非常に難解な問題になります。
どう見てもソースコードには書き込みシーケンスが書いてありますが、コンパイルして出来上がったオブジェクトファイル及びHEXファイルにはこのシーケンスは入っていません。しかし、一般にオブジェクトファイルやHEXファイルは人間様が読めないので、 その問題に気づけることは知らないとなかなか無いでしょう。そして「よくわからないけど動かない」という現象が起こるわけです。

はい。このような問題はしばしば起こります。なので、最適化においてはこのようなことを念頭において置かなければいけません。

では、上記のようなコードを書きたいときはどうするか?
はい。「volatile」という修飾子がC言語には用意されています。 英語の意味は「揮発性の」とか「変わりやすい」とかそういう意味だそうです。これを変数宣言時に付けると、その変数に関わる最適化を抑制することができます。なので、おそらく組み込みマイコンのSFRのアクセスに関してなんかは、ライブラリの中でその変数にvolatileが付けられた状態でtypedefなり何なりがされていることでしょう。


はい、要するに何が言いたいかと言いますと、最適化は「賭け」なんです。
コードの実行速度やコードサイズは小さくなるかもしれませんが、自分が意図しないところまで最適化されてしまうとソフトウェアが上手く動かなくなる可能性があります。特に、単にロジックをコードで表現しただけのようなプログラミングと違ってハードウェアを制御する組み込みプログラミングでは最適化でしばしば起こりそうな問題があるからです。
もちろん、コンパイラの最適化の仕様を熟知していて、さらに他人が作ったライブラリも含めて最適化を掛けられる前提でvolatileなどの修飾子を適切に付けてあるコードだったら問題は無いのでしょうが、そこまでやるのはなかなか難しいものです。

でもまあとりあえずやってみました。上手く動かなかったら最適化をやめてプログラムを書き直せばいいかなって。


最適化をやるのは簡単です。プロジェクトのプロパティからxc16-gccを選び、option categoriesをOptimizationsにします。そして、Optimization levelを設定すればいいだけです。細かな説明はOption Descriptionのタブに書いてくれています。
上位の最適化は有料版XC16などでしか有効にならないので、とりあえず無料版でできる最適化レベル1にしてコンパイルしてみましょう。


こちらが最適化レベル0(最適化なし)のときのコードサイズです。プログラムメモリは85%も使っています。


そして、最適化レベルを1にしたらこうなりました。見事に66%までコードサイズが減っています。

さて、問題の最適化の副作用ですが…今のところ特に無さそうですね。
まあ、様子見です。

2014年6月18日水曜日

グラフィック液晶の表示制御

さて、今回は少し戻ってまたNTP時計にまつわる話をしたいと思います。
今回は、C言語によるソフトウェアの設計技法のような話になります。

以前の記事でソフトウェアSPIを実装し、フォントを2種類実装したという話を書きました。しかし、それだけでは不十分で、このあたりはスイッチに合わせて表示内容をヒョコヒョコ切り替えなければいけませんね。例えば、7セグには表示されていない日付を表示するモード、もしくはIPアドレスやMACアドレスなどのネットワークの状態を表示するモード、CdSから取得した環境の明るさを表示したり7セグの明るさを変える閾値やその明るさを設定するモード、NTPサーバーを設定したりNTPサーバーとの同期間隔を設定するモードなどが必要になってきます。
表示モードを切り替えるくらいならばスイッチに合わせて表示内容を変えていけばいいのでしょうが、時には設定を編集したりするなどの複雑な処理をする場合は、スイッチが押された時の制御が結構複雑になってしまい、難解なソースコードになってしまいます。

難解なソースコードでは得する人はだれもいません。なので、できるだけわかりやすくプログラムを書かなければなりません。

こういうソフトウェアの設計に慣れた人だと、当然、スイッチが押されたときに呼ばれる関数は表示モードごとにあるべきで(=各モードの制御の中にスイッチの処理が入る)、例えばBlueButtonDown()関数などをシステムで1個用意し、そこから現在の表示モードによってswitch文でて処理内容を変える(=スイッチの制御の中に各モードの制御が入る)ような処理はスマートとは言えません。
もちろん、実装としては後者のほうが果てしなく楽というか、C言語の組み込みプログラミングでは自然なのですがね。スイッチが押されること自体はモードにかかわりなく発生するイベントですし、そもそも「処理をまとめる」という文化がほとんど無い言語ですから。

例えばこれがC言語ではなく、C++やC#などのオブジェクト指向言語ならばもうやることはとても楽になります。
抽象クラスでBlueButtonDown()やBlueButtonUp()、DrawDisplay()などの関数を定義し、デフォルトの処理を実装しておきます。そして、各表示モードではその抽象クラスを継承し、必要に応じてそれらの関数をオーバーライドします。システムにはその各表示モードのクラスのインスタンスを渡し、システムはそのインスタンスを配列として持っておけば、モードが切り替わるたびにその配列のどこを表示しているかを順々に変えていけば良いだけです。とてもスマートな実装になります。

しかし、C言語はこのような高度なオブジェクト指向プログラミングはできないんですよね。C++ならできます。しかし、XCコンパイラは8bit、16bitアーキテクチャ向けはC言語しかサポートしておらず、C++をやりたければ32bitマイコンのXC32++くらいになってしまいます。当然、NTP時計はPIC24FですのでC++は使えないことになってしまいます。

というわけで、できるだけC言語でこのオブジェクト指向に近い書き方をすることにしてみましょう。

typedef void (*DrawLCDCallback)(BOOL);
typedef void (*SwitchCallback)(void);

typedef struct _tagLCDPAGE {
    int SettingState;
    DrawLCDCallback DrawLCD;
    SwitchCallback RedOn;
    SwitchCallback RedOff;
    SwitchCallback BlueOn;
    SwitchCallback BlueOff;
    SwitchCallback YellowOn;
    SwitchCallback YellowOff;
} LCDPAGE, *PLCDPAGE;

まずはこのような構造体を定義します。
はい、まさに「関数ポインタ」というやつです。詳しくは割愛しますが、関数だってメモリ上のどこかにあるものだから、それを抽象化してポインタとして指せるような機能があってもいいよねってやつです。はい。ここで、LCDのページ(いわゆる各モードの表示内容とボタンが押された時の処理)の元となるいわゆる抽象クラスのようなものとしています。

SettingStateは設定のときの状態です。多くはカーソルとして使っていて、ある数が例えばある桁の編集に対応しています。アクティブではないページではこの値がゼロにリセットするようにしています。

あとはLCDの描画時に呼ばれる関数、各ボタンが押された時と離された時に呼ばれる関数を用意しています。 LCDの描画は、効率のために再描画する必要があるかどうかのフラグをシステムから渡すようにしています。例えばページが切り替わった直後は再描画する必要があるのでTRUEをシステムが渡してきます。しかし、それ以外の時は、ページが必要と思ったところのみを更新すればいいのでFALSEを渡してきます。

例えば、ホーム画面(今日の日付と直前にNTPサーバーと同期した日時の表示)のプログラムHome.cはこんな感じになっています。

#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include "AQM1248.h"
#include "PageBase.h"
#include "SNTPx.h"


void Home_Draw(BOOL bForceRedraw)
{
    char text[18];
    static char LastDate[18] = {0};
    QWORD utcms;
    static QWORD LastUpdateMsec;
    struct tm Time;

    //Show Date
    utcms = SNTPGetUTCMilliseconds();
    UTCMillisecondToLocaltime(utcms, 9 * 60, &Time);
    strftime(text, sizeof(text) / sizeof(char), "%Y/%m/%d (%a)", &Time);
    if(bForceRedraw || strcmp(text, LastDate)) {
        LCD_FillPattern(0xFF, 0, 0, LCD_COLUMN_MAX, 1);
        LCD_PrintText("Today's date", 0, 0, LCDFONT_SMALL | LCDFONT_REVERSE);

        LCD_PrintTextAtCenter(text, 1, LCDFONT_MEDIUM);
        strcpy(LastDate, text);
    }

    //Show Last Sync Time
    utcms = SNTPGetLastUpdateUTCMilliseconds();
    if(bForceRedraw || (LastUpdateMsec != utcms)) {
        LCD_FillPattern(0xFF, 0, 3, LCD_COLUMN_MAX, 1);
        LCD_PrintText("Synchronized at", 0, 3, LCDFONT_SMALL | LCDFONT_REVERSE);

        UTCMillisecondToLocaltime(utcms, 9 * 60, &Time);
        strftime(text, sizeof(text) / sizeof(char), "%m/%d %H:%M:%S", &Time);
        LCD_PrintTextAtCenter(text, 4, LCDFONT_MEDIUM);
        LastUpdateMsec = utcms;
    }
}

PLCDPAGE LCDPage_GetHome()
{
    static LCDPAGE page;
    static BOOL bInitialized = FALSE;

    if(bInitialized == FALSE) {
        page.SettingState = 0;
        page.DrawLCD = Home_Draw;
        page.RedOn = NULL;
        page.RedOff = NULL;
        page.BlueOn = NULL;
        page.BlueOff = NULL;
        page.YellowOn = NULL;
        page.YellowOff = NULL;

        bInitialized  = TRUE;
    }
    return &page;
}

ページのインスタンスはstaticインスタンスとして、初回にLCDPage_GetHome()を呼ばれた時に中身を初期化して返します。それ以降は初期化はしません。
main関数内ではLCDPAGE構造体の配列を持っており、こんな感じで初期化しています。

    //LCD Page
    pPages[0] = LCDPage_GetHome();
    pPages[1] = LCDPage_GetNetworkStatus();
    pPages[2] = LCDPage_GetNTPSetting();
    pPages[3] = LCDPage_GetNTPServerEdit();
    pPages[4] = LCDPage_GetOscillator();
    pPages[5] = LCDPage_GetCdSSettings();

至って簡単です。もしもページをこの後増やそうと思っても、配列の長さを増やしてここに追加してあげるだけでおkです。

void ProcessLCD()
{
    static uint8_t LastPageIndex;

    if(pPages[PageIndex]->DrawLCD != NULL)
        pPages[PageIndex]->DrawLCD(LastPageIndex != PageIndex);

    LastPageIndex = PageIndex;
}

main関数の無限ループの中で呼び出されるLCDの制御をする関数です。
こちらも至ってシンプルで、現在のページに対してLCD描画関数のDrawLCDがNULLじゃなければ呼び出します。呼び出すときは、以前のページと異なっていたら再描画の指示を出しています。

typedef struct _tagSWITCH_STATE {
    union {
        BYTE SwitchData;
        struct __PACKED {
            unsigned RED:1;
            unsigned BLUE:1;
            unsigned GREEN:1;
            unsigned YELLOW:1;
        } Switch;
    };
    QWORD UTCMillisecond;
} SWITCH_STATE;

void ProcessSwitch()
{
    static SWITCH_STATE ss[3];

    QWORD now = SNTPGetUTCMilliseconds();

    if(ss[0].UTCMillisecond != now) {
        SWITCH_STATE turn;

        ss[2] = ss[1];
        ss[1] = ss[0];
        ss[0].Switch.RED = SW_RED;
        ss[0].Switch.BLUE = SW_BLUE;
        ss[0].Switch.GREEN = SW_GREEN;
        ss[0].Switch.YELLOW = SW_YELLOW;
        ss[0].UTCMillisecond = now;

        turn.SwitchData = ~(ss[2].SwitchData) & ss[1].SwitchData & ss[0].SwitchData;
        if(turn.Switch.GREEN)
            Switch_GreenOn();    //OS Switch
        if(turn.Switch.RED && (pPages[PageIndex]->RedOn != NULL))
            pPages[PageIndex]->RedOn();
        if(turn.Switch.BLUE && (pPages[PageIndex]->BlueOn != NULL))
            pPages[PageIndex]->BlueOn();
        if(turn.Switch.YELLOW && (pPages[PageIndex]->YellowOn != NULL))
            pPages[PageIndex]->YellowOn();

        turn.SwitchData = ss[2].SwitchData & ~(ss[1].SwitchData) & ~(ss[0].SwitchData);
        if(turn.Switch.GREEN)
            Switch_GreenOff();    //OS Switch
        if(turn.Switch.RED && (pPages[PageIndex]->RedOff != NULL))
            pPages[PageIndex]->RedOff();
        if(turn.Switch.BLUE && (pPages[PageIndex]->BlueOff != NULL))
            pPages[PageIndex]->BlueOff();
        if(turn.Switch.YELLOW && (pPages[PageIndex]->YellowOff != NULL))
            pPages[PageIndex]->YellowOff();
    }
}

スイッチの処理は若干トリッキーなことをやっています。
今回のNTP時計にはチャタリング防止回路は入っていないので(ハードで入れるとハードが大きくなるという問題と、コンデンサを使うから瞬間的な大電流がスイッチに流れてスイッチが痛むという問題がある)ソフトウェアでそれをやる必要があります。1ミリ秒ごとにスイッチの状態をスキャンして、その3回分のデータがOFF→ON→ONとなっていたらスイッチがONされたとし、ON→OFF→OFFとなっていたらOFFになったと認識します。
あとは、まあ共用体と論理演算がわかってる人ならソースコード読めばだいたいどうなっているかがわかると思います。

緑スイッチだけはシステムで処理します。 つまり、各ページにはわたしません。緑スイッチが押されたらページを切り替えるので、その処理は絶対にページにオーバーライドはさせません。
それ以外のスイッチは、そのスイッチが変化して、かつそのページの処理関数のポインタがNULLでなければ呼び出すという非常にシンプルなものです。なので、例えばスイッチを使わないページがあったらそのポインタをNULLにしておけばいい、たったそれだけです。

C言語にはクラスはありませんが、一応1ファイル1クラスみたいな扱いとしてやることで、このようにそれなりにスッキリした実装ができました。
めでたしめでたし。

こういう実装を考えて完成させると達成感があって楽しいですが、やっぱりPICでC#動くと嬉しいな…。

USBメモリーをFatFsで操作する

さて、前回の時点でUSBメモリーをPIC32MX250F128Bからアクセスすることに成功しました。
FATファイルシステムもMicrochipのライブラリで制御しておりますが、やはりFATファイルシステムのライブラリと言ったらFatFsですね。(少なくとも私の中では)

FatFs 汎用FATファイルシステム・モジュール

もうね、このライブラリには何も不満は抱けないです。それくらい完成度の高いライブラリです。
ディスクの初期化や指定セクタの読み書きなどのほんのいくつかのデバイスに依存した関数をユーザーが用意するだけで、FAT16/32のアクセスができるようになります。
そのほか、非常に細かくオプションを指定できたりし(メモリが足りればLFNやUnicodeファイル名も使える)、その上、パフォーマンスもかなり良いです。言うこと無しです。

それでは導入していきましょう。

まずは上記のサイトよりFatFsのソースコードを入手し、ヘッダファイル、Cファイルをともにプロジェクトに登録します。ついでに、MicrochipのライブラリのほうのファイルFSIO.cを無効化しておきましょう。


次に、整数型の定義をします。integer.h内で、各ビット数に対応した型を定義してやるのですが、すでにPICのプロジェクトはGenericTypeDefs.hで同名の型が定義されており、二重定義エラーが起きてしまいます。しかし、BYTEだのWORDだの、この辺の定義はもはやビット数はだいたい決まっていて、FatFs内の定義とGenericTypeDefs.h内の定義が同じなので、integer.hでGenericTypeDefs.hをIncludeしてあげるだけにしました。WCHARの定義を残して、それ以外はコメントアウトします。

そしたら、一番重要なdiskio.cの定義をしてあげます。こんなかんじになります。

#include "diskio.h"
#include "ffconf.h"        /* FatFs lower layer API */

/* Definitions of physical drive number for each media */
#define USB        0


typedef struct
{
    BYTE    errorCode;
    union
    {
        BYTE    value;
        struct
        {
            BYTE    sectorSize  : 1;
            BYTE    maxLUN      : 1;
        }   bits;
    } validityFlags;

    WORD    sectorSize;
    BYTE    maxLUN;
} MEDIA_INFORMATION;

typedef enum
{
    MEDIA_NO_ERROR,                     // No errors
    MEDIA_DEVICE_NOT_PRESENT,           // The requested device is not present
    MEDIA_CANNOT_INITIALIZE             // Cannot initialize media
} MEDIA_ERRORS;

MEDIA_INFORMATION * USBHostMSDSCSIMediaInitialize( void );
BYTE USBHostMSDSCSIMediaDetect( void );
BYTE USBHostMSDSCSIWriteProtectState( void );
BYTE USBHostMSDSCSISectorRead( DWORD sectorAddress, BYTE *dataBuffer );
BYTE USBHostMSDSCSISectorWrite( DWORD sectorAddress, BYTE *dataBuffer, BYTE allowWriteToZero );


static BOOL isInitialized = FALSE;

/*-----------------------------------------------------------------------*/
/* Inidialize a Drive
*/
/*-----------------------------------------------------------------------*/

DSTATUS disk_initialize (
    BYTE pdrv                /* Physical drive nmuber (0..) */
)
{
    DSTATUS stat;

    switch (pdrv) {
    case USB:
        if(USBHostMSDSCSIMediaDetect()) {    //メディアがつながっているか?
            if(!isInitialized) {
                MEDIA_INFORMATION *mediaInformation;
                
                mediaInformation = USBHostMSDSCSIMediaInitialize();

                if(mediaInformation->errorCode != MEDIA_NO_ERROR)
                    stat = STA_NOINIT;
                else {
                    stat = 0;
                    isInitialized = TRUE;
                }
            } else
                stat = 0;

            if(USBHostMSDSCSIWriteProtectState())
                stat |= STA_PROTECT;
        } else
            stat = STA_NODISK | STA_NOINIT;

        return stat;
    }
    return STA_NOINIT;
}


/*-----------------------------------------------------------------------*/
/* Get Disk Status
*/
/*-----------------------------------------------------------------------*/

DSTATUS disk_status (
    BYTE pdrv        /* Physical drive nmuber (0..) */
)
{
    DSTATUS stat;

    switch (pdrv) {
    case USB :
        if(USBHostMSDSCSIMediaDetect()) {
            if(isInitialized)
                return 0;
            else
                return STA_NOINIT;
        } else
            return STA_NODISK | STA_NOINIT;

        return stat;
    }
    return STA_NOINIT;
}


/*-----------------------------------------------------------------------*/
/* Read Sector(s)
*/
/*-----------------------------------------------------------------------*/

DRESULT disk_read (
    BYTE pdrv,        /* Physical drive nmuber (0..) */
    BYTE *buff,        /* Data buffer to store read data */
    DWORD sector,    /* Sector address (LBA) */
    UINT count        /* Number of sectors to read (1..128) */
)
{
    UINT i;
    WORD SectorSize;

    if(disk_ioctl(pdrv, GET_SECTOR_SIZE, SectorSize) != RES_OK)
        return RES_ERROR;

    switch (pdrv) {
    case USB:
        for(i = 0; i < count; i++) {
            if(USBHostMSDSCSISectorRead(sector + i, buff + i * SectorSize) == FALSE)
                return RES_ERROR;
        }

        return RES_OK;
    }
    return RES_PARERR;
}



/*-----------------------------------------------------------------------*/
/* Write Sector(s)
*/
/*-----------------------------------------------------------------------*/

#if _USE_WRITE
DRESULT disk_write (
    BYTE pdrv,            /* Physical drive nmuber (0..) */
    const BYTE *buff,    /* Data to be written */
    DWORD sector,        /* Sector address (LBA) */
    UINT count            /* Number of sectors to write (1..128) */
)
{
    UINT i;
    WORD SectorSize;

    if(disk_ioctl(pdrv, GET_SECTOR_SIZE, SectorSize) != RES_OK)
        return RES_ERROR;

    switch (pdrv) {
    case USB :
        for(i = 0; i < count; i++) {
            if(USBHostMSDSCSISectorWrite(sector, (BYTE *)buff + i * SectorSize, TRUE) == FALSE)
                return RES_ERROR;
        }

        return RES_OK;
    }
    return RES_PARERR;
}
#endif


/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions
*/
/*-----------------------------------------------------------------------*/

#if _USE_IOCTL
DRESULT disk_ioctl (
    BYTE pdrv,        /* Physical drive nmuber (0..) */
    BYTE cmd,        /* Control code */
    void *buff        /* Buffer to send/receive control data */
)
{
    DRESULT res;
    int result;

    switch (pdrv) {
    case USB :
        switch(cmd) {
            case CTRL_SYNC:
                res = RES_OK;
                break;
            case GET_SECTOR_COUNT:    //フォーマットするときにしか使われない
                res = RES_ERROR;
                break;
            case GET_SECTOR_SIZE:
#if(_MAX_SS == _MIN_SS)
                *((WORD *)buff) = _MAX_SS;
                res = RES_OK;
#else
                res = RES_ERROR;
#endif
                break;
            case GET_BLOCK_SIZE:
                *((DWORD *)buff) = 1;
                res = RES_OK;
                break;
            case CTRL_ERASE_SECTOR:
                res = RES_OK;
                break;
            default:
                res = RES_PARERR;
                break;
        }
        return res;
    }
    return RES_PARERR;
}
#endif

typedef union _tagFATTIME {
    DWORD value;
    struct {
        unsigned SecDiv2 : 5;
        unsigned Min : 6;
        unsigned Hour : 5;
        unsigned Date : 5;
        unsigned Month : 4;
        unsigned YearFrom1980 : 7;
    };
} FATTIME;

DWORD get_fattime (void)
{
    FATTIME time;

    time.YearFrom1980 = 34;
    time.Month = 6;
    time.Date = 17;
    time.Hour = 23;
    time.Min = 16;
    time.SecDiv2 = 0;

    return time.value;
}

void disk_detatched(void)
{
    isInitialized = FALSE;
}

ディスクが初期化されたかどうかは結構厳密に聞いてくるようなので、isInitializedという変数を用意し初期化されたかを記憶しておきます。そして、disk_detached関数を作り、main.c側でUSBメモリーが取り外されたことがわかったらこの関数を呼び出し初期化されていないことを伝えます。

USBメモリーのアクセス関係の関数はusb_host_msd_scsi.cに入っているようです。
disk_initialize関数とdisk_status関数はUSBHostMSDSCSIMediaInitialize関数とUSBHostMSDSCSIMediaDetect関数をうまく使って処理しています。また、disk_writeとdisk_readはマルチセクタリード/ライトにも対応できるようなインターフェースをしていますが、USBHostMSDSCSISectorWrite関数とUSBHostMSDSCSISectorRead関数はマルチセクタリードに対応していないようなので、とりあえずfor文でそれをエミュレートしてあげています。デバイスドライバレベルでマルチセクタアクセスできたら相当速くなるんでしょうがね…。(少なくとも昔、SDカードのアクセスをやった時はそうでした。)

disk_ioctrl関数は一番めんどくさかったですね。
CTRL_SYNCはデバイスドライバレベルでキャッシュしてる場合にflushするための関数なようで、おそらくそういうことはしていないっぽいのでとりあえず何も処理をせずに成功を返すようにしました。CTRL_ERASE_SECTORは実装しなくても大丈夫なようなのでこちらも同様にそうしました。
GET_SECTOR_COUNTはセクタの個数だそうです。えっ、それってFATファイルシステム内読めばわかるでしょ?って思ったらどうもこれ、フォーマットするときに使うようですね。そりゃそうだ、フォーマットされていないディスクだったらディスクの容量はファイルシステムからじゃ読み出せませんからね。
…どうしろって言うんだ…。 usb_host_msd_scsi.cにはそれっぽいAPI無いですしね…。というわけで、フォーマットしなけりゃ問題ないっしょということで実装するのをやめました。はい。
GET_SECTOR_SIZEですが、普通は512バイトみたいですね。 最大セクタサイズと最小セクタサイズが同じだったらそれでいいっしょということでそれを返すようにしました。違ったらサポートしないってことで。結構適当な実装です。はい。ごめんなさい。

あとはget_fattime関数を定義してやる必要があります。これはファイルのタイムスタンプを書き込むための関数で、要するにRTCなどから読みだした現在時刻をここで返すようにしてやればいいわけです。
が、NTPだのGPSだの何だので時計合わせするようなハードはあいにく持ち合わせていないので、適当にコーディングしていた時の時刻を返すようにしました。FATファイルシステムはタイムスタンプを32bitに収めるために、 秒なんか2秒毎にしか記録できないんですね。シフト演算子を組合せて時刻を定義しても良いでしょうが、ここはFATTIMEという共用体を作ってあげることにしました。こうやって各時刻要素の構造体とDWORDを共用体にすることで、構造体に入れた値をそのままDWORDとして読みだすことができます。すごく素朴でコストの小さい書き方でとても良いと思います。

これで最低限は動きそうな感じになったと思うんで、最後にmain関数側からこれを呼び出します。

    while(1)
    {
        LATAbits.LATA0 = 0;
        //USB stack process function
        USBTasks();

        //if thumbdrive is plugged in
        if(USBHostMSDSCSIMediaDetect())
        {
            FRESULT result;
            FATFS fatfs;
            FIL file;

            deviceAttached = TRUE;
            LATAbits.LATA0 = 1;

            f_mount(&fatfs, "0:", 0);
            result = f_mkdir("0:testdir");
            if((result == FR_OK) || (result == FR_EXIST)) {
                result = f_open(&file, "0:testdir/file.txt", FA_CREATE_ALWAYS | FA_WRITE);
                if(result == FR_OK) {
                    char szText[32] = "Hello, FatFs!";
                    UINT bw;

                    f_write(&file, szText, strlen(szText) * sizeof(char), &bw);

                    f_close(&file);
                    f_mount(NULL, "0:", 0);

                    while(deviceAttached == TRUE)
                        USBTasks();
                }
            }
        }
    }

ディレクトリの生成の試験も兼ねて、ドライブの中にtestdirというディレクトリを生成し、その中にファイルを生成しています。


そして無事、ファイルを書き込むことができました。
Microchipのライブラリと違い、タイムスタンプもちゃんと入っています。

さらに、USBドライバとFatFsを入れた時点でのプログラムメモリの使用率はなんと54%なんですね。
プログラムメモリを78%も占有することになったMicrochip製のライブラリよりも相当コンパクトだと言えるでしょう。さっすがFatFsです。

2014年6月17日火曜日

PIC32MX250F128BでUSBメモリーに書き込む

前回につづいてPIC32MX250のお話です。

前回はプログラムのビルドが終わりました。今度は実際に回路を作ってみましょう。
必要最低限の回路です。こんな感じです。



セラロックは8MHzを付けています。PIC32MXのオシレーターのブロック図は結構複雑になっていて、USBは8MHzを1/2プリスケーラを通して4MHzにした後、24倍PLLを通して96MHzにし、さらに1/2ポストスケーラを通して48MHzにしています。CPUのメインクロックは1/2プリスケーラで4MHzにした後PLLで15倍の60MHzにして供給しています。結構高速で動いてくれるんですね。
PICは3.3Vで動きますが、USBは5Vを供給してやる必要があります。そのため、5VのACアダプタを使って、USBには5V、PICにはレギュレーターを介して3.3Vを供給してやっています。Vbusは5Vトレラントですが(そうじゃなかったら困る)、PIC32MXのピンは5Vトレラントのピンがだいぶ限られているので、もっと実用的なアプリケーションを開発するときには注意する必要がありそうですね。

LEDは必要ないって言えば必要ないですが、LEDデバッグをするために用意しています。


実物はこんなかんじになっています。Cサイズのユニバーサル基板に適当に組み上げました。

プログラムでは、USBメモリーを挿すとLEDが点灯し、抜くと消灯するようにしてみました。USBメモリーを挿してLEDが点灯してから引っこ抜き、パソコンで中身を確認します。


見事にファイルが書き込まれていました。めでたしめでたし。

一番苦労したのはPICの配線ですかね(汗
USB周りのピン(Vbus、Vusb3v3など)をどうつなぐのかを回路図とかでパッと説明してるサイトがパッと出てこずに少し苦労しました。でも、もう回路図は上に載せておいたのですんなりと動かせるかと思います。

このデモプログラムにはタイムスタンプを書き込む機能が付いていないようで、上のように更新日時が空になっています。メモ帳では読めるみたいですが、TeraPadは受け付けてくれませんでしたね。この辺、改良の余地はありそうです。

USBメモリーがマイコンで使えるとなると、ロガーなどの用途には一気に使いやすくなりますよね。
その他、ENC28J60と組合せてHTTPサーバーとか作れるかも。
夢は広がりんぐです。

2014年6月16日月曜日

PICkit3を改造するお話

さて、PIC32MXをいじるにあたって、PICkit3のハードウェアバグについてちょっと触れておきましょう。
前にPIC32MX220F032Bをいじったときに発生した問題です。
はい、何度設定を見なおしても全然PICkit3が正常にPICを認識してくれないんですね。
いろいろググりまくった結果、PICkit3のハードウェアバグという結論に。

PICkit 3 の低電圧動作に関する改修

ある一定のアセンブリ番号以前のPICkit3だと、保護用か何かの抵抗が悪さしちゃってうまく書き込めないらしいです。なので、PICkit3をばらして当該抵抗をスズメッキ線でバイパスしてあげました。

あくまでも改造は自己責任でどうぞ。

PIC32MX250F128BでUSBホストをやる

さて、NTP時計の次の連載になる気がしているPIC32MX250の記事をちょっと書こうと思います。



最近、秋葉原の某電子部品店で取り扱いが始まったPIC32MX250ですが、USB-OTGが付いております。そもそもUSBとはパソコン(ホスト)とデバイスが通信するためのものですが、デバイス同士の通信ニーズも出てきておりますので(スマホとUSBメモリー、カメラとプリンターの接続などなど)USB-OTG(On The Go)っていうのが定義されたらしいです。PICはパソコンじゃないのでそもそもはデバイス側になるのですが、OTGでそれがホストとして動作させられるってことですね。(細かい話はわかってないので違ってたらごめんなさい)

PICでUSBホストをやるとなると、やはりメジャーなのはPIC24FJ64GB002/004とかなのでしょうが、とにかくこれの入手性が悪いんですよね。秋葉原を探しまわっても見つけたことがありません。多分、DigiKeyとかじゃないと手に入らないんでしょうね…。


前置きはこれくらいにしておいて、早速プログラムのビルドに入ってみましょう。
ビルドできる見込みが立ったらハードを作るという流れで。

例によってMicrochip Libraries for Applicationsを使います。

Microchip Libraries for Applications

めんどくさいので、ENC28J60を動かすときに使った2013/06/15のバージョンでビルドをしていきます。使うのはこのデモです。

microchip_solutions_v2013-06-15\USB\Host - Mass Storage - Simple Demo

しかしこのデモ、どうもデフォルトのターゲットはPIC24FJ256GB106のようですね。しかし、PIC32MXシリーズで動かすことも想定された設計がされており、おそらくPIC32MXでもある程度容易にビルドできるだろうということがわかります。



このように左側のCategoriesペインに多くのビルドプロファイルがありますね。PIC32MX250こそ無いものの、PIC32MXシリーズがあるのでうまくいく可能性は結構あります。

プロジェクトに以下のようなファイルを登録しました。


ファイルがどこにあるかは、もとのデモプロジェクトを見ればわかります。
HardwareProfile.hとかその辺はいじらなければいけないオーラがものすごく出ていますが、まあとりあえずビルドを通すことを目標にして今回はパスしておきます。

例によってIncludeディレクトリのパスを通しておく必要があります。microchip_solutions_v2013-06-15\Microchip\Includeと、プロジェクトのディレクトリを通しておけば大丈夫です。


この他に重要なのは、ヒープ領域の容量を設定する必要があることです。
ヒープ領域とは、いわゆる関数の呼び出しやブロックの変数確保で使われるスタック領域とは違い、malloc等でメモリを動的に確保するときに使われるメモリ領域です。デモプログラムのどこかでmallocされてるってことなんでしょうね。(ファイルシステムのところの気がしています)

ヒープ領域はリンク時に用意されるものなので、xc32-ldの設定になります。デモプログラムでは2000bytes用意されていたので、とりあえず2000bytesにしておきましょう。


これでビルドが通りました。めでたしめでたし。
でもこれだけでもうプログラムメモリの78%を使ってしまうんですね…。

SPI接続グラフィック液晶のAQM1248Aを動かす

さて、とある電子部品屋で買える激安小型グラフィック液晶のAQM1248Aですが、これをNTP時計の補助表示器として使いました。

多分、これをSPIで制御するの何のって話は、付属のデータシートを見ればだいたいわかりますし、ググればいっぱいその記事が出てくると思うので細かい話は割愛しようと思います。

ところで、とてもとても試行錯誤したのですが、ハードウェアSPIが上手く動いてくれなかったんですね…。

PIC24FJ64GA004は2つのSPIを持っているので、1つはENC28J60、もう1つはSPI液晶に割り当てればいいんですが、なんかどうも動いてくれないんですね…。
原因はよくわからないのですが、PIC32MX220F032Bなら何も問題なく動いてくれるんですよねえ。PIC24FJ64GA004のSPIモジュールには「受信は使わない」っていうオプションが無いからそれが原因なんでしょうか。よくわかりません。

仕方ないので、ソフトウェアでSPIを実装しました。まあ、受信しないのでとても実装は楽でした。

void SendSPI(uint8_t data, BOOL IsData)
{
    int i;

    LED7SEG_DisableInterrupt();

    LCD_CS_IO = 0;
    LCD_RS_IO = IsData;

    for(i = 0; i < 8; i++) {
        LCD_SDO_IO = (data & 0x80) != 0;
        LCD_SCK_IO = 0;
        LCD_SCK_IO = 1;
        data <<= 1;
    }
    LCD_CS_IO = 1;

    LED7SEG_EnableInterrupt();
}

あと、なぜだかSPIの送信中に7セグの割り込みが入ると7セグの表示がバグるんですねえ。本当に不思議ですねえ。液晶ががバグるんじゃなくて7セグの表示がバグるんです。LATxレジスタを叩いてるからビット操作の問題も起きないはずだし、本当に不思議です。

XCコンパイラって例えばLATBbits.LATB5 = 1っていう制御、実際は一回LATBをワーキングレジスタにコピー→OR演算を実行→LATBに書き戻すっていう3ステップの処理とかをしているんですかね。それだったら、例えばLATBをWregに読みだした時点で割り込みが入ってそこでLATBをいじったとしても、割り込みから復帰した後には割り込みが起こる前のLATBに演算をした結果がLATBに入ってしまいます(マルチスレッドプログラミングにおけるクリティカルセクションの話題のテンプレ的な例)。しかし、PIC24FJ64GA004はビット操作命令を持っています。コンパイラがこれをビット操作に置き換えてくれれば1命令で終わるので、割り込みに関するそのような問題は起こりません。確かに構造体を使ったビット操作は、1ビットに限らず任意のビット数の変数を構造体内に確保できる文法です。なので、1ビット分の制御でもわざわざOR/AND演算を用いてくる可能性は十分に考えられます。でも、これを解明しようとしたら…HEXを逆アセンブルしてPIC24シリーズのアセンブリ読まなきゃいけないの…ちょっと無理ん…。 PIC18までならアセンブリ開発はよくやってましたが…

プログラミングで「なぜだかわからないけど」は本当は良くないんでしょうが、 現状これでしっかりと動いてくれています。SPIが動かない原因や割り込みでバグる原因がちゃんと分かる人いたら教えて下さい><




液晶には2種類のフォントを搭載しました。
この液晶は縦8bitのデータを1バイトのデータとして送るので縦は8ピクセルごとの制御が便利です。というわけで縦8ドットのフォントを用意したのですが、とても小さすぎました。
なので、縦16ドットのフォントも用意しました。フォントだけでプログラムメモリ20%くらい持って行かれました(汗

そういえば、NTP時計を作製するにあたって初めて知ったのですが、 C言語にはstrftimeっていう関数があるんですね。sprintf関数があれば何かのデータをフォーマットしてテキスト化するのに困りませんが、strftimeはtm構造体をフォーマットするのにあたっていくつか便利な感じになっています。
例えば、写真の液晶の上段は

    strftime(text, sizeof(text) / sizeof(char), "%Y/%m/%d (%a)", &Time);

こんな関数の呼び出しでフォーマットしています。便利ですねえ。

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日水曜日

7セグメントLEDの制御

こちらもネットワークとはあまり関係ない話です。

NTP時計では6桁分の7セグメントLEDを制御しています。
複数桁の7セグを制御するときは、通常、ダイナミック点灯制御と呼ばれる制御が使われます。この制御では制御のリアルタイム性が非常に重要になってくるので、タイマ2を使った割り込みで制御を行っています。

ところで、NTP時計ではこの7セグの明るさを変えたいというニーズが出てきます。
時計は24時間動き続けるもの。夜、部屋を消灯しても煌々と光り続けていたらとても目障りです。

なので、ダイナミック点灯制御に加えてPWM制御も行い、明るさを変えられるようにしました。さらに、CdSを使い、環境の明るさを取得し、設定したしきい値で自動的に7セグの明るさを変えるようにしています。

ところで、この話題をTwitterでしていたら、興味深い情報を教えてくれた人がいました。
実は昔、同じように7セグで時計を作ったことがあって、それにも同様にPWMによる明るさ制御はしていました。しかし、PWMのデューティー比が小さいうちはデューティー比を変えると目に見えて明るさが変わるのですが、デューティー比が大きくなるとまあ変わってはいるのでしょうが、そんなに大きく明るさが変わっているようには見えません。

3乗にどういう意味があるのかはわかりませんが、そのような経験から、明るさの設定値が大きくなればなるほどデューティー比を大きく変えるという相関は理に適っていると言えます。


次に、ダイナミック点灯制御の実装を考えます。
もともとこのPWMの明るさ制御をする前はタイマ2の割り込みで点灯させる桁を切り替えていましたが、PWMするとなるとさらに例えば256段階制御するならこの1/256の周期で割り込みを入れなければなりません(と思い込んでいた)。

割り込み周期は0x2000サイクルだったので、1/256にすると0x0020サイクル…32サイクル…。
当然、割り込み処理をしている間に次の割り込みが入り、永遠に割り込みが終わらない事態になってしまいました。

はい、お分かりの方も多いとは思いますが、割り込みが掛かったときにTMR2の値を適当に設定してやれば、(0xFFFF-TMR2の設定値) サイクル後に割り込みが入ることになりますね。
PIC24Fシリーズの割り込みは割り込みが発生する値をPRxレジスタに設定すればそこで割り込みが起こるので、こういう使い方をすっかり忘れていましたね。PIC18まではよくやっていたのに…。


というわけで、こんな感じになっています。

#define PWM_PERIOD ((uint16_t)0x2000)

void LED7SEG_SetBrightness(uint8_t value)
{
    uint32_t buf = value + 1ul;

    buf = (uint64_t)buf * buf * buf * PWM_PERIOD / (256ul * 256 * 256);

    PWMOnStart = PWM_PERIOD - buf;
    PWMOffStart = buf;

    PWMOnStart = min(PWMOnStart, PWM_PERIOD);
    PWMOffStart = min(PWMOffStart, PWM_PERIOD);

    Brightness = value;
}

消灯はしたくないので、0~255の設定値を1足して1~256にしてあります。そして、それを3乗して256の3乗で規格化したうえで、PWM_PERIODをかけています。PWMの周期です。
そして、TMR2のLEDがONになるときのスタート値とOFFになるときのスタート値を計算し、保存しておいています。

void _ISR __attribute__((__auto_psv__)) _T2Interrupt(void)
{
    static uint8_t cnt;
    IFS0bits.T2IF = 0;

    if(cnt++ & 0x01) {
        TMR2 = PWMOnStart;

        Show7SEG();
        LED_COLON = Colon;
    } else {
        TMR2 = PWMOffStart;

        LATA &= ~(MASK_7SEG_RA | MASK_7SEGTR_RA);
        LATB &= ~(MASK_7SEG_RB | MASK_7SEGTR_RB);
        LATC &= ~(MASK_7SEG_RC | MASK_7SEGTR_RC);
        LED_COLON = 0;
    }
}

割り込みはこのようにして、カウンタをずっとインクリメントしていき、下1bitが1のときか0のときかでONにする処理、OFFにする処理を使い分けています。


余談ですが、これ、設定値が小さい時は全然明るさ変わって見えないんですよね…。
3乗を導入したから仕方ないって言えばそうなんですが、 例えば、明るさ設定値+1が32以下のときのPWMのONサイクル数を示したのが以下の図です。

はい。12以下に至っては、ONサイクル数がゼロでずーっときちゃってますね…。
多分、13以降もある一定の値までは、割り込みルーチンを抜けるのにこれ以上時間が掛かってしまい、実質的に明るさがほとんど変わらないゾーンが出てくるはずです(それでもOFFサイクル数は若干変化するので少しは変わって見える?)。
うーんこの微妙な感じ。
PWMの周期を大きくすればここらへん明確に出てくるんでしょうが、そうするとダイナミック点灯制御がちらついて見えてしまいます。 となると解決策はマイコンを高速で動かすこと…もうこれ以上速度上がらないよ!

というわけで、まあ、結局3乗導入したけどそれはそれ残念なことが起こっちゃったテヘペロッ☆というオチでした。

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整数側からその旧来の関数を実装するリファクタリングをやっちゃってもいいんですが、まあ、デバッグとかめんどくさいんでプログラムメモリが足りなくなったら考えることにします…。

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

自動オシレーター調整

今回も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

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のバグとかいう話でした。本当かどうかは知りませんが…。)