PICによる赤外線通信3

通信フォーマットを決めたので、送信側のプログラムを作成していきましょう。

単純ケース

まず単純なケースです。下のようなコードでリーダー部やアドレス部を送信することはできます。なお、IRT_On()とIRT_Off()は、過去に説明しています。

PICによる赤外線通信1
赤外線リモコン戦車を作るために、PICを使った赤外線通信装置を作成しました。そのときに経験したことを書き留めておきます。赤外線受信モジュール電子工作で赤外線送受信を行う場合によく使われるのが、下のような赤外線受信モジュールです。赤外線受信モ...
#define T 400
void SendLeader(void)
{
    IRT_On();
    __delay_us(8 * T);
    IRT_Off();
    __delay_us(4 * T);
}

void SendAddress(uint8_t address)
{
    for (uint8_t i = 4; i > 0; i--)
    {
        IRT_On();
        __delay_us(T);
        IRT_Off();
        __delay_us(T);
        if (address & 0x01)
        {
            // bit1を送信するので、Offを3T
            __delay_us(3 * T);
        }
        else
        {
            // bit0を送信するので、Offを1T
            __delay_us(T);
        }

        // address を右にシフトする
        address >>= 1;
    }
}

データ部などは省略しますが、アドレス部の送信と同じようなコードで可能です。処理が似ているので関数を作って共用化したほうが良いでしょう。

この方法は、On/Offを一定時間(8Tや4Tなど)続けるために、__delay_us()で時間を待つようにしているので、何をしているのかコードを理解しやすく、不具合が起きにくいことが利点です。

しかし、その他の処理(例えば、関数コールや、addressの0bit目をチェックする処理など)も処理時間がかかるので、余計な時間待っていることになり、実際のOnの時間は8Tよりも伸びてしまいます。

余計な時間の積み重なりでタイミングが少しづつ遅れてしまいます。微々たる時間なのですが、こういう時間も気になってしまうので、別のアプローチを考えることにしました。

タイミングを重視するケース(タイマを使用する)

それはタイマを使い、タイマイベントに同期してOn/Offを切り替えるようにすることです。具体的に実際のコードを以下に示します。

// 送信用カウンタ
// On/Offを続ける回数をカウントダウンするために使う。
static uint8_t IRT_counter;

/**
 * TMR0の割込みハンドラ
 *
 * 400us毎にコールされる。
 *
 */
void IRT_HandlerForTMR0(void)
{
    // IRT_Counterをカウントダウンし、0になったらIRT_NextSignalに従って、赤外線出力を変更する。
    // IRT_NextSignalはIRT_SendRun()で送信処理状況に応じて設定されている。
    if (--IRT_counter == 0)
    {
        if (IRT_NextSignal.IRT_OUT)
        {
            IRT_On();
        }
        else
        {
            IRT_Off();
        }

        // IRT_counterを設定する。
        IRT_counter = IRT_NextSignal.TURN;

        // IRT_NextSignalをクリアする。
        // クリアしないと、次の信号が再設定されなくなる。
        ClearNextSignal();
    }
}

IRT_HandlerForTMR0()はTMR0割込み時にコールされる関数です。TMR0は1T=400usec毎に割込みが入るように初期設定しておきます。そうすれば、この関数も1Tに1回コールされます。

関数の中身の説明をします。

IRT_NextSignalはstatic変数です。そしてIRT_HandlerForTMR0()の外(TMR0割込みではないタイミング)でセットされています。例えば、下のようにセットされているとしましょう。

IRT_NextSignal.TURN = 8;
IRT_NextSignal.IRT_OUT = 1;

そうすると、2つ目のif文(17行目)でIRT_NextSignal.IRT_OUTをチェックしOn/Offを変更しています。上の設定だとIRT_On()になります。

その後、IRT_counterにIRT_NextSignal.TURNをセットします。上の設定だと8です。

そして、IRT_counterは最初のif文(15行目)でカウントダウンされます。そして0になるとif文の中に入り、再びIRT_NextSignalに従ってOn/OffやIRT_Counterの設定を行います。

つまり、最初の設定だと「Onを8T」送信したあと、次のOn/OffはIRT_counterの再設定をすることになります。その後、ClearNextSignal()でNextSignalをクリアします。

例えば、main関数など、TMR0割込み以外の処理で下のようなコードになっていたとすれば、IRT_NexstSignalに従って、次々に送信されていくことになります。

void main(){
    // 初期設定などを行う。詳細は省略する。
    while(1){
        IRT_SendRun();
    }
}

void IRT_SendRun(void)
{
    if ( IRT_NextSignalがクリアされていない ){
        return;
    }
    IRT_NextSignal = 次の信号設定;
}

「次の信号設定」は、送信のタイミング(リーダー部の送信しているのか、アドレス部のNビット目を送信するのか、など)に応じて変更します。そうしたmain側処理と割込み処理が組み合わさって、下のような動作をすることになります。

IRT_NextSignalをセットする処理はTMR0割込みの外で行うため、単純ケースのときの「addressの0bit目をチェックする処理」のような時間が待ち時間に加算されません。

もちろんIRT_HandlerForTMR0()をコールしたり、if文をチェックしたりする処理時間はかかります。しかしTMR0割込みを基準にして考えればIRT_On()などの処理にたどり着くまでにかかる時間は毎回同じなので、単純ケースのときのようなタイミング遅れは発生しなくなります。

逆にいえば、IRT_On()などにたどり着くまでの処理時間が送信データによって変わってしまうと、このようなTMR0割込みを使って送信する意味(利点)がなくなります。似たようなプログラムを作る場合には注意しなくてはなりません。

IRT_NextSignal

コンパイル後(アセンブラレベル)でのプログラムサイズを減らすために、IRT_NextSignalについては少し工夫をしました。

次のコードのように宣言しています。

/**
 * ビット毎のアクセスや、上位/下位4bit毎のアクセスが必要なデータのためのunion型
 */
typedef union
{
    // 1bit毎にアクセスするための構造体
    struct
    {
        uint8_t B0 : 1; // 0ビット目
        uint8_t B1 : 1; // 1ビット目
        uint8_t B2 : 1; // 2ビット目
        uint8_t B3 : 1; // 3ビット目
        uint8_t B4 : 1; // 4ビット目
        uint8_t B5 : 1; // 5ビット目
        uint8_t B6 : 1; // 6ビット目
        uint8_t B7 : 1; // 7ビット目
    };
    // 4bit毎にアクセスするための構造体
    struct
    {
        uint8_t L4 : 4; // 下位4ビット
        uint8_t H4 : 4; // 上位4ビット
    };
    // TMR0割り込みで使用するための構造体
    struct
    {
        // 赤外線通信で出力するターン数
        // TURN数にセットする値は、現状は最大でも8なので、6bitあればあふれることはない。
        uint8_t TURN : 6;
        uint8_t IRT_OUT : 1; // 赤外線通信で出力する信号値 1 or 0
        uint8_t VALID : 1;   // データが有効か否か 1なら有効、0なら無効
    };
    // 8bitでアクセスするためのuint8_t
    uint8_t A8; // 8ビット整数値
} IRT_DATA_t;

// 次の出力する信号値
static IRT_DATA_t IRT_NextSignal;

上記のようにビットフィールドを使って1byteに収まるようにしています。

ON/OFFを表すには1bitで十分です。そのためIRT_OUTは1bitです。

また、今回の仕様上、ターン数の最大は8(リーダー部で「Onを8T」のとき)なので4bitあれば十分です。今回はビットフィールドに余裕があったのでTURNは6bitにしています。

VALIDは、1ならIRT_NextSignalの値が設定されていて、0ならクリアされていると判定するためのビットです。

単純なstruct型ではなく union型にしているのは全ビット(8bit)を同時にアクセスするためです。このときはA8にアクセスします。

PICのCコンパイラ(XC8)は複数ビットのビットフィールドを扱うのが苦手なようです。PRO版を使うと少しは改善されるのかもしれませんが、とにかく無料版は苦手と言ってよいでしょう。可能な限り8bitでアクセスするほうが効率が良いです。

そこで、下のようなマクロを用意してアクセスすることにしました。

/**
 * IRT_NextSignalをセットするマクロ
 * IRT_NextSignalは、
 *  0-5bit目がターン数
 *  6bit目が On/Off
 *   7bit目が データが無効か否か
 * を表す。
 * ターン数の6bit値を 0b00tttttt とするなら、
 * Onのときは0b11tttttt、Offのときは0b10ttttttとなればよい。
 * つまり、
 * Onのときは、7-6bit目を11にするために、turn数と0xC0のORを取る。
 * Offのときは、7-6bit目を10にするために、turn数と0x3FのANDを取り、0x80とORを取る。
 * 
 *      IRT_NextSignal.TURN = turn;
 *      IRT_NextSignal.IRT_OUT = 1;
 *      IRT_NextSignal.VALID = 1
 * の様にコーディングすると、コンパイル後のアセンブラコードが長くなるので、
 * それを避けるためにこのマクロを用意した。
 */
#define SetNextSignalOn(turn)                \
    {                                        \
        IRT_NextSignal.A8 = (0xC0 | (turn)); \
    }
#define SetNextSignalOff(turn)                        \
    {                                                 \
        IRT_NextSignal.A8 = (0x80 | (0x3F & (turn))); \
    }

/**
 * IRT_NextSignalをクリア(データ無効)にする。
 * さらに、VALIDを0にするだけでなく、全データを0クリアする。
 */
#define ClearNextSignal()      \
    {                          \
        IRT_NextSignal.A8 = 0; \
    }
/**
 * IRT_NextSignalがクリアされているか否か
 */
#define IsClearNextSignal() (!IRT_NextSignal.VALID)

コメント中にも書いていますが、TURN、IRT_OUT、VALIDをそれぞれセットするようなソースコードにすると、コンパイル後のアセンブラコードが長くなってしまいます。

全ての送信(通信フォーマットの終了部までの送信)が終わると、IRT_NextSignalはずっとクリア状態になります(次のデータ送信が始まるまで。)その間に、赤外線はずっとOffでなければなりません。そのために、クリア(ClearNextSiganal())時は、VALIDを0にするだけでなく、IRT_OUTも0にしなければなりません。

このときTURNはどの値でも構わないのですが、すべてを0クリアすることにしました。これもコンパイル後のアセンブラを意識していて、単にIRT_NextSignal.A8 = 0; とするほうが簡単なのです。

コメント