PICによるRC戦車制御3

「戦車制御機能」部分が作成できたので、残りの機能を作っていきます。

受信と制御の連絡機能

赤外線受信処理では、データの受信が完了したときにEvent_IRR_DataIsReceivedイベントを発生します。(詳細は「PICによる赤外線通信7 赤外線受信処理のメイン部分」のコードと説明を見てください。)

このイベントが発生したら、データに応じてモーターやLEDを制御します。

Event_IRR_DataIsReceivedイベントに対するアクションであるActionDataReceived()のコードを以下に示します。

#define ADDRESS 1 // 受信アドレス

/**
 * 受信データをモーターやLEDの制御に渡す処理
 */
void ActionDataReceived(void)
{
    // 受信アドレスをチェックする
    if (IRR_GetAddress() == ADDRESS)
    {
        // 赤外線受信信号の8bit値
        RECEIVEDATA_t ReceiveData;

        // 受信データを受け取る。
        ReceiveData.value = IRR_GetData();

        // モーターを制御する。
        MTR_SetControlParameter(ReceiveData.MOTOR);

        // LEDを制御する。
        OUT_SetLed(ReceiveData.LED);

        // NRTカウンタをリセット
        NRT_Reset();
    }

    // 次のデータの受信処理を開始する
    IRR_ReceiveStart();
}

最初に受信したアドレス値(4ビット値)をチェックします。

IRR_GetAddress()でアドレス値を受け取り、送信側で設定しているアドレス値と比較しています。

アドレス値は(送信側のアドレス値と同じ値を)#define ADDRESSで定義しています。

アドレスが一致しないときは、受信データを無視します。モーターやLEDの制御は行いません。

アドレス値が一致したときは、IRR_GetData()で受信データを受け取り、ReceiveDataにセットします。RECEIVEDATA_t型は受信する8ビットデータに対応したデータ型です。次のように定義してあります。

/**
 * 赤外線受信信号の8bit値
 */
typedef union
{
    uint8_t value; // 8bitとして扱うとき
    struct
    {
        uint8_t MOTOR : 4; // MTR_SetControlParameter()に渡す値
        uint8_t LED : 1;   // LED点灯のOn/Off
        uint8_t : 3;       // 未使用
    };
} RECEIVEDATA_t;

そして、ReceiveData.MOTORをモーター制御のための関数MTR_SetControlParameter()に渡します。この関数は前回作成した関数です。そして、この関数をコールすることによって、左右のモーターの回転方向、回転速度が設定され、ソフトウェアPWMでモーターを制御できるようになります。

以上でモーター制御に関しては「受信と制御の連絡」ができました。

また、ReceiveData.LEDをLED制御のための関数OUT_SetLed()に渡します。(こちらの関数は「PICによるRC戦車制御1 LEDの制御値を出力する」で説明しています。)

これで、LED制御に関しても「受信と制御の連絡」ができました。

次にNRTカウンタをリセットします。これもまだ説明していません。後ほど説明します。

そして関数の最後に、次のデータを受信するための処理を開始します。IRR_ReceiveStart()をコールすることで、赤外線通信のリーダー部を待つ状態になります。(詳細は、赤外線受信開始処理を参照してください。)

停止機能

動作仕様

停止機能は、一定時間赤外線を受信しなくなったときに、モーターを停止させる機能です。

プログラムを作成する前にもう少し仕様を決めることにします。具体的には次のようにしました。

  • 赤外線受信処理が最後にデータを受け取ったときを基準(時刻0)として考える。
  • 時刻0から1秒経つと左右のモーターを停止する。
  • 時刻0から6秒経つとLEDを消灯する。

送信側では、約250msecに1回の頻度で赤外線を送信しています。そのため、4回分である1秒を「一定時間」にしました。

LEDの消灯は当初は考えていませんでした。しかし戦車が動かなくなってもずっと点灯し続けているのも変だと感じ、後から付け加えた仕様です。時間についても後述のNRTカウンタで計測可能な時間でかつキリの良い時間が6秒だったので、6秒を選んでいます。

NRTカウンタ

停止機能のために時刻0を基準にして1秒と6秒の時間が経過したことを計測する機能が必要です。

そのための機能がNRTカウンタです。NRTは「No Recive Time」の略で、赤外線を受信していない時間という意味です。

そしてNRTカウンタを赤外線受信が完了したタイミングで0にリセットしておきます。(具体的には、前述のActionDataReceived()のコードのNRT_Reset()がこの処理を行っています。)

そしてNRTカウンタが1秒と6秒を超えたときにそれぞれイベントを発生させます。

そしてイベントへのアクションとして、モーターを停止したり、LEDを停止したりします。

TMR2イベント処理

1秒以上の時間を計測する場合TMR1を使うというのが(ネットで検索して見つかる作例などでは)一般的ですが、NRTカウンタもTMR2割込みを利用して時間をカウントすることにしました。具体的なコードを以下に示します。

/**
 * NRT用のカウンタ
 *
 * 16ビット使用する。
 */
static union
{
    uint16_t All; // 16bit
    struct
    {
        uint8_t L8; // 下位8bit
        uint8_t H8; // 上位8bit
    };
} NRT_Counter;

/**
 * NRTカウンタをリセットする。
 */
void NRT_Reset(void)
{
    NRT_Counter.All = 0;
}

/**
 * NRTカウンタのための、TMR2割り込みハンドラ
 *
 * TMR2割り込み時にカウントアップする。
 */
void NRT_HandlerForTMR2(void)
{
    NRT_Counter.All++;

    // NRT_Counterが変化したイベントを発生する
    EventRise(Event_NRT_CounterChanged);
}

NRT_CounterがNRTカウンタです。ソースの定義通り16ビットのカウンタです。

カウンタを8ビットにすると、\[256×100usec=25600usec=25.6msec\]までの時間しかカウントできません。最低でも1秒をカウントするには8ビットでは不足です。16ビットなら約6.5秒までカウントできます。(動作仕様でも説明しましたが、NRTカウンタが6.5秒までカウントできるようになったので、LEDの消灯の時間を6秒にしました。)

NRT_Reset()はNRTカウンタを0にリセットします。停止機能のために時刻0を生み出します。前述のActionDataReceived()の中で使っています。

NRT_HandlerForTMR2()は名前の通りTMR2イベントが発生したときにコールされるハンドラ関数です。

行なっている処理は、NRTカウンタをカウントアップして、Event_NRT_CounterChangedイベントを発生するだけです。

時間経過のチェック

Event_NRT_CounterChangedイベントに対するアクションとして、以下の関数を実行します。

/**
 * NRTカウンタをチェックし、イベントを発生する。
 */
void NRT_Check(void)
{
    // NRT_CounterはTMR2割り込み=100usec毎にカウントアップするので、
    // X秒とNRT_Counterを比較するなら、 NRT_Counter.All == 10000 * X とすればよい。
    // しかし、そこまで正確な時間を求める必要はないので、
    // 上位8bitだけで時間経過を判定することにする。

    // NRT_Counter.All == 10000 * X
    // -> NRT_Counter.All / 256 == 10000 * X / 256
    // -> NRT_Counter.H8 == 10000 * X / 256

    // 10000 * 6 / 256 にすると、intとして計算されるので、10000Uとすること。
    // そうしないと(10000*6)が最大値を超えたというワーニングがコンパイル時に出てしまう。

    if (NRT_Counter.H8 == 10000U / 256)
    {
        // NRTカウンタが1秒経ったイベントを発生する。
        EventRise(Event_NRT_1SecPassed);
    }
    else if (NRT_Counter.H8 == (10000U * 6 / 256))
    {
        // NRTカウンタが6秒経ったイベントを発生する。
        EventRise(Event_NRT_6SecPassed);
    }
}

この関数は、NRTカウンタを使って時間経過をチェックし、イベントを発生します。

ソースコード中のコメントにも書いてあるように、厳密に1秒(=10000usec)をチェックするのではなく、NRTカウンタの上位8ビットだけをチェックして大雑把な判定を行っています。これは16ビット値をif文で比較するよりも8ビット値の比較の方が命令数が少なく、処理時間が短くなるからです。

今回のNRTカウンタの目的を考えると、厳密な1秒を比較する必要がありません。そもそもPICの内部クロックの動作周波数にも若干のずれがあるため、TMR2だけで厳密な1秒を測定できません。そのため大雑把な比較によって判定することを選択しました。

そして比較により1秒経ったならEvent_NRT_1SecPassedイベントを発生します。同様に6秒経ったならEvent_NRT_6SecPassedイベントを発生します。

Event_NRT_1SecPassedイベントに対するアクションとしては、左右のモーターを停止させます。そのためには、次の処理を行えばよいです。

MTR_SetControlParameter(0);

また、Event_NRT_6SecPassedイベントに対するアクションとして、LEDのOffにします。そのためには、次の処理を行えばよいです。

OUT_SetLed(false);

NRTカウンタに関連するイベントのアクションや、Event_IRR_DataIsReceivedイベントの実際のコードは、すでに「PICによる赤外線通信8 イベントアクション」にコードがありますので、そちらも参照してください。

なお、Event_NRT_1SecPassedイベントに対するアクションで、MTR_SetControlParameter(0)といきなり引数0を指定するのは、「マジックナンバー」になってしまってよくありません。

実際のコードでは、次のようにしています。

    EventAction(Event_NRT_1SecPassed, {
        // NRTカウンタが1秒経ったイベント
        // モーターを止める
        MTR_Stop();
    });

MTR_Stop()はマクロ関数です。次のように定義しています。

/**
 * モーターを停止する
 */
#define MTR_Stop()  MTR_SetControlParameter(0)

コメント