PICによる赤外線通信4

送信プログラム作成の続きです。

送信フェイズの検討

前回までのコードで状況に応じてIRT_NextSignalにセットする値を変えることで、赤外線通信ができるようになっています。「状況に応じて」を整理するために、送信処理をいくつかのフェイズに分割して考えます。

フェイズ名説明IRT_NextSignalにセットする値
STOP送信していない状態。初期値クリア
STARTデータ送信開始。クリア
LEADER_ONリーダー部のOnの部分を送信する。On 8T
LEADER_OFFリーダー部のOffの部分を送信する。Off 4T
ADDRESSアドレス部の4bitデータを送信するアドレスに応じて変化
DATAデータ部の8bitデータを送信するデータに応じて変化
CHECKSUMチェックサム部の4bitデータを送信するチェックサム値に応じて変化
ENDER終了部のOnを送信するOn 1T
送信フェイズ一覧

これらのフェイズはenum値で定義して、送信フェイズとして保持することにします。

/**
 * 送信フェイズを示すenum型
 */
typedef enum
{
    IRT_PHASE_STOP,       // 送信停止
    IRT_PHASE_START,      // 送信開始
    IRT_PHASE_LEADER_ON,  // LeaderのOn 8Tを 送信
    IRT_PHASE_LEADER_OFF, // LeaderのOff 4Tを 送信
    IRT_PHASE_ADDRESS,    // Address部を送信
    IRT_PHASE_DATA,       // Data部を送信
    IRT_PHASE_CHECKSUM,   // CheckSum部を送信
    IRT_PHASE_ENDER,      // 終了部を送信
} IRT_PHASE_t;

// 送信フェイズ
static IRT_PHASE_t IRT_phase;

そして、IRT_Phaseをチェックして「状況に応じてIRT_NextSignalにセット」することができます。下のようなコードにすればよいでしょう。

    switch (IRT_phase)
    {
    case IRT_PHASE_STOP: // 停止中
        // 大前提として、IRT_NextSignalがクリアされている時に、
        // 「送信状況に応じて」IRT_NextSignalをセットするので、ここでクリアする必要はない。
        break;
    case IRT_PHASE_START: // 送信開始
        IRT_phase = IRT_PHASE_LEADER_ON;
        break;
    case IRT_PHASE_LEADER_ON: // Leader部のOnを送信
        // Leader部の On 8T を送信開始
        SetNextSignalOn(8);
        // Leader部のOffを送信に遷移する。
        IRT_phase = IRT_PHASE_LEADER_OFF;
        break;
// 以降は省略

各フェイズのうち、ADDRESS、DATA、CHECKSUMの3フェイズはデータに応じてIRT_NextSignalにセットする値が変化します。ADDRESSフェイズの場合、以下のようになります。

ADDRESSのサブフェイズ説明IRT_NextSignalにセットする値
BIT0_ON0bit目のOnを送信するOn 1T
BIT0_OFF0bit目のOffを送信するOff 1T または Off 3T
BIT1_ON1bit目のOnを送信するOn 1T
BIT1_OFF1bit目のOffを送信するOff 1T または Off 3T
BIT2_ON2bit目のOnを送信するOn 1T
BIT2_OFF2bit目のOffを送信するOff 1T または Off 3T
BIT3_ON3bit目のOnを送信するOn 1T
BIT3_OFF3bit目のOffを送信するOff 1T または Off 3T
ADDRESSフェイズのサブフェイズ

DATAフェイズ、CHECKSUMフェイズも同様のサブフェイズがあります。これらサブフェイズも同様にenum定義するのは、効率が悪いでしょう。前述のswitch文が巨大になってしまいます。

また、3つのフェイズは全て複数のビットデータを送信するフェイズです。「最初にOnを1T、bitの値に応じてOffを1Tまたは3T」の送信を繰り返すので、処理手順を共通化したほうが良いはずです。

ビットデータ送信処理

そこで、ビットデータを送信するための関数を作成しました。

// IRT_SendBuffer()で使用する作業バッファ
static uint8_t IRT_buffer;

// IRT_SendBuffer()で使用するカウンタ
static uint8_t IRT_bitcounter;

/**
 * IRT_SendBuffer()を使うための設定処理
 *
 * @param data:uint8_t 送信したいデータ
 * @param bit:uint8_t dataの送信したいビット数。1から8の値を指定する。
 *
 * IRT_InitBuffer(xxxx,4) とすれば、xxxxの下位4bit分を送信する。
 */
#define IRT_InitBuffer(data, bit) \
    {                             \
        IRT_buffer = (data);      \
        IRT_bitcounter = (bit)*2; \
    }

/**
 * Address部やData部などの送信中に IRT_SendRun() からコールされる。
 *
 * IRT_InitBuffer()でセットしたデータを0bit目から順に送信していく。
 *
 */
static void IRT_SendBuffer(void)
{
    // 1bitのデータを送信するには、 Onを1Tの間送信した後、Offを1または3Tの送信する必要がある。
    // つまり N bitのデータを送信するには On/Offの切り替えがN*2回発生する。
    // そこで、IRT_InitBuffer()で送信データとビット数をセットするときに、
    // IRT_bitcounterにはビット数の2倍の値をセットしておく。
    // そして、IRT_bitcounterをカウントダウンしていき、
    // 奇数ならOnを送信し、偶数ならOffを送信する。
    IRT_bitcounter--;
    if (IRT_bitcounter & 0x01)
    {
        // 1Tの間Onにする。
        SetNextSignalOn(1);
    }
    else
    {
        // IRT_bufferの0bit目が1なら3Tの間Offにし、0なら1Tの間Offにする。
        if (IRT_buffer & 0x01)
        {
            SetNextSignalOff(3);
        }
        else
        {
            SetNextSignalOff(1);
        }
        // 次のbitのデータ送信のために、IRT_bufferをシフトする。
        IRT_buffer >>= 1;
    }
}


/**
 * IRT_SendBuffer()による送信処理が忙しい(何かのデータを送信中)
 * か否かを判定する。
 * 「送信処理が終わっている」なら IRT_bitcounterが 0 になることを利用してチェックしている。
 *
 */
#define IRT_SendBufferIsBusy() (IRT_bitcounter != 0)

ビットデータに対してIRT_NextSignalをセットするのは、IRT_SendBuffer()です。この関数の中で、変数IRT_bufferとIRT_bitcounterの値から現在の送信状態を判断し、IRT_NextSignalに値をセットしていきます。

変数IRT_bufferは送信データを一時的に保持します。この変数は送信処理が進むと右シフトされていき、やがて0になってしまいます。送信処理のための作業バッファとして使います。

変数IRT_bitcounterは現在の送信状態を保持します。この変数は送信処理が進むとカウントダウンし、やがて0になってしまいます。

ビットデータの送信では、1bit分のデータを送信するときは最初に「Onを1T」を送信し、次に「Offを1T or 3T」送信します。これらを送信したいビット数繰り返すので、IRT_NextSignalをセットする回数は「ビット数×2」になります。そこでビット数の2倍をIRT_bitcounterをセットしておきます。そしてIRT_SendBuffer()の中でカウントダウンすれば、残りの回数や今の状況を判断し、IRT_NextSignalにセットする値を決定することができます。

IRT_SendBuffer()を使う前に、2つの変数をセットしておかなくてはなりません。そのためにマクロ関数IRT_InitBuffer()を使います。例えば、「アドレス値(4bit)として1を送信する」というケースでは、IRT_InitBuffer(1,4)とします。そうすると、IRT_buffer = 1, IRT_bitcounter = 8 がセットされます。

IRT_SendBuffer()では最初にIRT_bitcounterをカウントダウンします。その結果が奇数のときは、「Onを1T」のタイミングだと判断しています。偶数のときは「Offを1T or 3T」のタイミングだと判断しています。1Tと3Tのどちらなのかは、送信するビットデータによって変わるので、IRT_bufferの0bit目(IRT_buffer & 0x01)で判定しています。そして、次回の判定のために、IRT_bufferを右シフトしています。

IRT_InitBuffer()でセットした情報がすべて送信し終わっていれば、IRT_bitcounterが0になります。マクロ関数IRT_SendBufferIsBusy()はその判定行うためのに用意しています。

赤外線送信処理のメイン部分

赤外線送信処理のメイン部分IRT_SendRun()のコードを以下に示します。この関数は、状況に応じて、IRT_NextSignalをセットすることが目的です。

// 送信アドレス
static IRT_DATA_t IRT_address;

// 送信データ
static IRT_DATA_t IRT_data;

/**
 * 赤外線通信の送信処理を行う。
 */
void IRT_SendRun(void)
{
    // 実際に赤外線送信の制御(信号のOn/Offや送信ターン数の管理)する処理は、
    // IRT_HandlerForTMR0() で行っている。
    // その IRT_HandlerForTMR0()では、 IRT_NextSignal に従って赤外線送信を行なう。
    //
    // この関数では 処理状況(どのようなデータを送信しているのか)にあわせて、
    // IRT_NextSignal をセットする処理を行っている。

    // NextSignalがクリアされていないときは、スキップする。
    if (!IsClearNextSignal())
    {
        return;
    }

    // IRT_SendBuffer()を使って送信する処理中なら、その続きを行う。
    if (IRT_SendBufferIsBusy())
    {
        IRT_SendBuffer();
        // IRT_SendBuffer()の中で NextSignal がセットされるので、以降の処理はスキップ。
        return;
    }

    switch (IRT_phase)
    {
    case IRT_PHASE_STOP: // 停止中
        break;
    case IRT_PHASE_START: // 送信開始
        // IRT_counterがカウントダウンして0になることで、送信のための
        // さまざまな処理が実施される。( IRT_HandlerForTMR0() の処理を参照。)
        // しかしこの時点ではIRT_counterの値が不定であり、
        // 大きな値になっていると無駄な待ち時間が発生する。
        // 処理がすぐに始まるように(カウントダウンですぐに0になるように)、
        // ここで1をセットしておく。
        // (なお、0をセットするとカウントダウンで255になってしまうので、適切ではない。)
        IRT_counter = 1;
        // Leader部のOnを送信に遷移する。
        IRT_phase = IRT_PHASE_LEADER_ON;
        break;
    case IRT_PHASE_LEADER_ON: // Leader部のOnを送信
        // Leader部の On 8T を送信開始
        SetNextSignalOn(8);
        // Leader部のOffを送信に遷移する。
        IRT_phase = IRT_PHASE_LEADER_OFF;
        break;
    case IRT_PHASE_LEADER_OFF: // Leader部のOffを送信
        // Leader部の Off 4T を送信開始
        SetNextSignalOff(4);
        // Address部を送信に遷移する。
        IRT_phase = IRT_PHASE_ADDRESS;
        break;
    case IRT_PHASE_ADDRESS: // Address部を送信
        // Address部を送信する。
        IRT_InitBuffer(IRT_address.A8, 4);
        // Data部を送信に遷移する。
        IRT_phase = IRT_PHASE_DATA;
        break;
    case IRT_PHASE_DATA: // Data部を送信
        // Data部を送信する。
        IRT_InitBuffer(IRT_data.A8, 8);
        // CheckSum部を送信に遷移する。
        IRT_phase = IRT_PHASE_CHECKSUM;
        break;
    case IRT_PHASE_CHECKSUM: // CheckSum部を送信
        // CheckSum部を送信する
        // CheckSum部は Address(4bit) + dataの下位4bit + dataの上位4bitの合計の下位4bit
        // 厳密に書けば、IRT_address.L4 + IRT_data.L4 + IRT_data.H4 になるが、
        // 最終的に3つの値の合計値の上位4bitは無視されるので、下のコードでも正しく動作する。
        // そして下のコードの方が、アセンブルリストが短くなる。
        IRT_InitBuffer((IRT_address.A8 + IRT_data.A8 + IRT_data.H4), 4);
        // 終了部の送信に遷移する。
        IRT_phase = IRT_PHASE_ENDER;
        break;
    case IRT_PHASE_ENDER: // 終了部を送信
        // On 1Tを送信開始
        SetNextSignalOn(1);
        // 全ての送信が終わったので、停止状態にする。
        IRT_phase = IRT_PHASE_STOP;
        break;
    default:
        break;
    }
}

/**
 * アドレスをセットする
 *
 * @param address:uint8_t 送信アドレス。ただし、下位4bitまで有効
 */
void IRT_SetAddress(uint8_t address)
{
    IRT_address.A8 = address;
}

/**
 * データを送信する
 *
 * @param data:uint8_t 送信データ
 */
void IRT_SendData(uint8_t data)
{
    IRT_data.A8 = data;
    IRT_phase = IRT_PHASE_START;
}

IRT_SendRun()はmain()関数の処理ループから何度もコールされることが前提となっています。そのため、アドレスやデータなどの送信情報を関数の引数として渡すのではなく、あらかじめ、IRT_addressとIRT_dataにセットしておきます。

まず、IRT_NextSignalがクリアでなければ次の値をセットする必要がないので、即座に関数を抜けます。

そして、IRT_SendBuffer()を使ってIRT_NextSignalをセットする処理の途中なら、そちらでIRT_SendBuffer()をコールし、関数を抜けます。

どちらでもない場合は、フェイズに応じてIRT_NextSignalをセットするか、あるいは、IRT_SendBuffer()を使うための設定を行います。

IRT_SetAddress()はアドレスをセットする関数です。IRT_addressをセットしているだけです。

同様にIRT_SendData()は送信データをセットする関数ですが、IRT_dataをセットすると同時に、IRT_PhaseをSTARTにしています。こうすることで、送信フェイズが変化し始め、IRT_SendRun()コール時にIRT_NextSignalがセットされるようになります。

IRT_SendRun()をコールするmain()処理側は下のようなコードになるイメージです。

void main(void)
{
    // 各種初期設定(コードは省略するが、以下のようなことを行う。)
    //   PICの初期設定
    //   TMR0の初期設定(割込みに対してIRT_HandlerForTMR0()がコールされるように設定)

    // 赤外線通信処理部を初期化する。(IRT_Phaseなどの変数を初期値にする)
    IRT_Initialize();
    // アドレスをセットする。
    IRT_SetAddress(ADDRESS);

    while (1)
    {
        // 赤外線通信の処理を行なう。
        IRT_SendRun();

        if (!IRT_IsBusy())
        {
            // リモコンのレバーやスイッチから送信データを決定する。
            uint8_t sendData = GetSendData();
            IRT_SendData(sendData);
        }
    }

コードは省略しますが、IRT_IsBusy()は送信処理が行われいてるか否かを判定する関数です。前のデータが送信されている途中で次のデータの送信処理が始まると、全体の動きがおかしくなってしまいます。送信がすべて終わってから次のデータ送信を開始する必要があります。

ただし、上のコードですと、休むことなく赤外線通信を行ってしまいます。TMR0か別のタイマを使って、例えば「250msecに1回の頻度で送信処理を行う。」という処理を追加して、ある程度間隔を空けて送信したほうが良いでしょう。

NECフォーマットへの応用

今回のコードは独自フォーマット用に作っていますが、NECフォーマットを送信するように変更することは難しくないでしょう。変更点は下記の通りです。

  • 1Tの長さが違うので、TMR0割込みの間隔を変える。
  • リーダー部のOnとOffのターン数を変える。
  • アドレス(4bit)ではなく、カスタマーコード(16bit)を送信する。フェイズを1つ追加し、カスタマーコード上位ビットフェイズと下位ビットフェイズにすれば、8bitを2つ送信するだけなので、IRT_SendBuffer()をそのまま使うことができる。
  • チェックサム(4bit)の代わりにデータの反転ビット(8bit)を送信する。

コメント