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#動くと嬉しいな…。

0 件のコメント:

コメントを投稿