PICによるRC戦車制御2

「戦車制御機能」の続きです。

モーターのソフトウェアPWM

戦車のモーターはソフトウェアPWMで動かします。そのためのコードを作成していきましょう。

これはいろいろと説明するよりも、コードを見た方が早いと思うので、先にコードを示します。

TMR2イベント処理

/**
 * モーターを約40Hzの周波数でPWM制御するために使うカウンタ
 * このカウンタはカウントダウンする形で使用する。
 */
static uint8_t MTR_counter100u;

/**
 * TMR2の割込み発生時の処理
 *
 * 約100us毎にコールされる。
 */
void MTR_HandlerForTMR2(void)
{
    // 100usカウンタをカウントダウンする。
    MTR_counter100u--;

    // モーターの制御状態が変わったイベントを発生する。
    EventRise(Event_MTR_ChangeAnyStatus);
}

MTR_HandlerForTMR2()は、名前の通りTMR2イベントが発生したときにコールされるハンドラ関数です。(参考:「PICによる赤外線通信8 外から発生するイベント」に載せているコードの、TMR2イベントに対するアクションのところを見てください。)

そのため100usecに1回の頻度で、変数MTR_Counter100uはカウントダウンし、イベントEvent_MTR_ChangeAnyStatusが発生します。

変数MTR_Counter100uはソフトウェアPWMのためのカウンタです。別途指定された変数MTR_XXXとこの変数を比べて、「MTR_Counter100u<<MTR_XXXなら1、MTR_Counter100u≧MTR_XXXなら0」を出力端子から出力すればソフトウェアPWMが実現できます。

MTR_Counter100uが256回カウントダウンすると1周して同じ値になるので、PWMの周期としては、\[1/(100[usec]×256)=1/(25600[usec])≒39[Hz]\]になります。

ただしソフトウェアPWM処理まではこの関数では行いません。そのかわりにイベントEvent_MTR_ChangeAnyStatusを発生しています。その理由はTMR2イベントでは最低限の処理に抑えたいからです。そのイベントのアクションとしてソフトウェアPWM処理を行います。

タイミング的に最も重要なのは赤外線受信処理です。パルスOn/Offの時間計測が遅れてしまうとデータを受信できなくなります。これは致命的な影響です。TMR2イベントのアクションとして処理時間を掛けないように注意する必要があります。

逆にモーター制御の処理が少しぐらい遅れても、致命的な影響は有りません。「Duty比が50%になるように設定していたのが少し遅れて50.5%になった」という程度の影響しかありません。そのためイベント発生だけで済ませ、処理を後回しにしています。

イベントEvent_MTR_ChangeAnyStatusは、モーター制御のための状態変数が変化したという意味のイベントです。MTR_Counter100uの以外にも次の変数が変化したときにこのイベントが発生します。

/**
 * 右モーターのスピード
 */
static uint8_t MTR_RMOTOR_SPD;

/**
 * 右モーターの方向 true:前進 false:後進
 */
static __bit MTR_RMOTOR_FOR_OR_BACK;

/**
 * 左モーターのスピード
 */
static uint8_t MTR_LMOTOR_SPD;

/**
 * 左モーターの方向 true:前進 false:後進
 */
static __bit MTR_LMOTOR_FOR_OR_BACK;

なお、MTR_RMOTOR_SPDやMTR_LMOTOR_SPDは前述の説明で「MTR_XXX」としていた変数として使います。

出力値の更新

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

/**
 * モーター制御のための更新処理
 */
void MTR_Update(void)
{
    // MTR_LATCにセットする値を作成するための作業変数
    OUT_DATA_t control_value;
    control_value.UINT8 = 0;

    // この関数は、各種変数に応じて、LATCの更新する。
    // LATCにセットする値を control_valueで作成していき、
    // 最後にOUT_SetMotor()を使ってLATCを更新する。

    // 右モーターの制御信号を変更する
    if (MTR_counter100u < MTR_RMOTOR_SPD)
    {
        if (MTR_RMOTOR_FOR_OR_BACK)
        {
            control_value.RMTR_FOR = true;
        }
        else
        {
            control_value.RMTR_BACK = true;
        }
    }

    // 左モーターの制御信号を変更する
    if (MTR_counter100u < MTR_LMOTOR_SPD)
    {
        if (MTR_LMOTOR_FOR_OR_BACK)
        {
            control_value.LMTR_FOR = true;
        }
        else
        {
            control_value.LMTR_BACK = true;
        }
    }

    // セットする値が完成したので、出力する。
    OUT_SetMotor(control_value.UINT8);
}

最初にOUT_DATA_t型の変数control_valueの全ビットを0で初期化します。(OUT_DATA_t型は前回説明しているのでそちらを参照。)

そして、「MTR_Counter100u<MTR_RMOTOR_SPDなら1、MTR_Counter100u≧MTR_RMOTOR_SPDなら0」を右モーターを制御するビットにセットします。

ただし、モーターの回転方向によってセットするビットが変わるので、MTR_RMOTOR_FOR_OR_BACKをチェックして切り替えています。

これで、MTR_RMOTOR_SPDをどこかでセットしておけば、この値に応じてPWM制御で右モーターを制御していることになります。PWMのDuty比はMTR_RMOTOR_SPD/256です。MTR_RMOTOR_SPDが0なら右モーターの2つのビットは常に0になりモーターは停止します。100%にすることはできませんが、電源電圧の関係で100%にすることがないため問題は有りません。

PICを動かすために電源は3V以上が必要なため、戦車の電源は電池3本以上になります。使用するモーターは一般的な模型用モーター(130モーター)なので、Duty比100%だと定格電圧を大幅に超えてしまいます。

そして、同様の処理を左モーターを制御するビットに対しても行います。

こうしてモーターを制御する4つのビットがcontrol_valueにセットできたので、それを引数としてOUT_SetMotor()をコールしています。(OUT_SetMotorは前回説明しています。)

ここまで作成したプログラムにより、前回述べた「左右のモーターの回転方向と回転速度に応じて、ソフトウェアPWMでモーターを制御する処理」までが実現できました。

モーター制御で残っている作業は、「赤外線受信処理で受け取った4ビットのデータから左右のモーターの回転方向や回転速度に変換する処理」です。

受信データの変換処理

残った作業のコードを作成していきましょう。

受信データとモーターの動きを整理

まず、受信データ4ビットに対してモーターの動きを整理します。

受信データの3~2ビットが00のとき、戦車は単純に前進/後進します。

受信データ4ビット戦車の動き左モーター右モーター
0000停止STOPSTOP
0001前進FOR1FOR1
0010前進(高速)FOR2FOR2
0011更新BACKBACK

STOPはモーター停止です。FOR1とFOR2はモーターを正転ですが、FOR2はFOR1速く回転させます。BACKはモーターを逆転です。回転スピードはFOR1と同じにします。

同様の表を受信データの残りのビットに対しても作成すると次のようになります。

受信データ4ビット戦車の動き左モーター右モーター
0100右に超信地旋回FOR1BACK
0101前進+車体を右FOR1STOP
0110大周りに右旋回FOR2FOR1
0111後進+車体を右STOPBACK
1000左に超信地旋回BACKFOR1
1001前進+車体を左STOPFOR1
1010大周りに左旋回FOR1FOR2
1011後進+車体を左BACKSTOP

3~2ビット目が11になるのはエラー値です。

結局のところ受信データは12種類しかないため要素数12の配列に左モーターと右モーターの値を格納した変換テーブルを用意すれば、変換は容易にできます。

変換テーブル

変換テーブルを次のように作成しました。

#define MTR_SPEED(left, right) (MTR_L##left | MTR_R##right)
#define MTR_RSTOP 0        // 右モーターSTOP
#define MTR_RFOR1 (1 << 0) // 右モーターFOR1
#define MTR_RFOR2 (1 << 1) // 右モーターFOR2
#define MTR_RBACK (1 << 2) // 右モーターBACK
#define MTR_LSTOP 0        // 左モーターSTOP
#define MTR_LFOR1 (1 << 3) // 左モーターFOR1
#define MTR_LFOR2 (1 << 4) // 左モーターFOR2
#define MTR_LBACK (1 << 5) // 左モーターBACK

const uint8_t MTR_SPEED_TABLE[12] = {
    MTR_SPEED(STOP, STOP),
    MTR_SPEED(FOR1, FOR1),
    MTR_SPEED(FOR2, FOR2),
    MTR_SPEED(BACK, BACK),

    MTR_SPEED(FOR1, BACK),
    MTR_SPEED(FOR1, STOP),
    MTR_SPEED(FOR2, FOR1),
    MTR_SPEED(STOP, BACK),

    MTR_SPEED(BACK, FOR1),
    MTR_SPEED(STOP, FOR1),
    MTR_SPEED(FOR1, FOR2),
    MTR_SPEED(BACK, STOP),
};

左モーターと右モーターをまとめて1byteの変数として保持します。そして、1byteの5~3ビット目を左モーター、2~0ビット目を右モーターに割り当てます。MTR_RSTOPからMTR_LBACKまでのdefineマクロは、その割り当てを定義しています。

こうすることで、(MTR_LFOR1 | MTR_RFOR1)は、「左右のモーター両方とも前進」という意味になります。

そして上のコードのように配列MTR_SPEED_TABLE[]を定義すれば、受信データがdataに対する左右のモーターの状態を、MTR_SPEED_TABLE[data]で変換できます。

受信データからモーター制御値への変換

/**
 * モーター制御のパラメーター
 */
static uint8_t MTR_ControlParameter;

/**
 * モーターを制御するパラメーターをセットする。
 *
 * @param[in] parameter
 *
 * parameterの下位4bitを用いてモーターを制御する。
 * 上位4bitは無視する。
 *
 * @param parameterの1-0ビット
 * 速度(0:停止, 1:前進(遅い), 2:前進(早), 3:後進)
 *
 * @param parameterの3-2ビット
 * 旋回(0: 直進, 1: 車体を右に向ける, 2: 車体を左に向ける, 3: error)
 *
 */
void MTR_SetControlParameter(uint8_t parameter)
{
    // parameterは 2進数で0000~1011までの値しか有効ではない
    // (2-3ビットが11はエラー)
    // そのため 12以上になった場合は、無視する。

    if (parameter >= 12)
    {
        return;
    }

    MTR_ControlParameter = parameter;
    // 12未満の場合は、モーター制御パラメーターが変化したイベントを発生する。
    EventRise(Event_MRT_ControlParameter_Changed);
}

MTR_SetControlParameter()は、モーター制御の入り口になる関数です。

引数parameterは受信データの4ビットが入ることを想定していますそのため12以上になるとき、つまり3~2ビット目が11になるときは、それ以上処理を続けずにreturnします。

そして、parameterが0~11のときのみMTR_ControlParameterにセットし、イベントEvent_MRT_ControlParameter_Changedを発生させます。

前述の変換テーブルを使ってモーターを制御する処理は、イベントEvent_MRT_ControlParameter_Changedへのアクションとして行います。

ここでイベント発生しているのは、処理時間の都合によるものです。最初の設計では、この後で説明するMTR_SetControlParameter_PostProcess()の処理もこの関数の中で行っていました。

しかし、テーブルを参照してモーター制御値を変更するのは処理時間がかかります。(配列参照するとポインタ計算が入るので結構時間がかかる。)さらに、このMTR_SetControlParameter()を呼び出す側も、そこそこ時間のかかる処理でした。

そのためどこかで(イベント発生という形で)処理を切る必要があり、入力データをチェックするというのは1つの区切りとして良いタイミングだったため、ここでイベントを発生しています。

// FOR1やBACKのときの速度(PWMのDuty比:45%)
#define MTR_FOR1_SPEED (256 * 45 / 100)
// FOR2のときの速度(PWMのDuty比: 90%)
#define MTR_FOR2_SPEED (256 * 90 / 100)

/**
 * MTR_SetControlParameter()の後処理を行う関数
 *
 * MRT_ControlParameter_Changed イベントによってコールされる。
 * MTR_SetControlParameter()の処理内容(アセンブル後の処理命令数)が多かったので、
 * 小分けに別関数にしている。
 *
 * MTR_ControlParameterに応じて、モーターの回転方向や速度を決定する。
 *
 */
void MTR_SetControlParameter_PostProcess(void)
{
    // 変換テーブルを使って、paramterを制御値に変換する
    uint8_t speed_table = MTR_SPEED_TABLE[MTR_ControlParameter];

    // 右モーター:回転方向
    MTR_RMOTOR_FOR_OR_BACK = !(speed_table & MTR_RBACK);
    // 左モーター:回転方向
    MTR_LMOTOR_FOR_OR_BACK = !(speed_table & MTR_LBACK);

    // 右モーター速度
    if (speed_table & MTR_RFOR2)
    {
        MTR_RMOTOR_SPD = MTR_FOR2_SPEED;
    }
    else if (speed_table & (MTR_RFOR1 | MTR_RBACK))
    {
        MTR_RMOTOR_SPD = MTR_FOR1_SPEED;
    }
    else
    {
        MTR_RMOTOR_SPD = 0;
    }

    // 左モーター:速度
    if (speed_table & MTR_LFOR2)
    {
        MTR_LMOTOR_SPD = MTR_FOR2_SPEED;
    }
    else if (speed_table & (MTR_LFOR1 | MTR_LBACK))
    {
        MTR_LMOTOR_SPD = MTR_FOR1_SPEED;
    }
    else
    {
        MTR_LMOTOR_SPD = 0;
    }

    // モーターの制御状態が変わったイベントを発生する。
    EventRise(Event_MTR_ChangeAnyStatus);
}

MTR_SetControlParameter_PostProcess()はイベントEvent_MRT_ControlParameter_Changedへのアクションとして実行される関数です。その名前の通りMTR_SetControlParameter()の処理の続きを行います。

MTR_ControlParameterには、受信してモーター制御値として0~11の値が入っているので、それを変換テーブルのインデックスにしてspeed_tableをセットします。

そしてMTR_RBACKを使ってspeed_tableのビットをチェックすることで、右のモーターがBACKか否かを判定し、MTR_RMOTOR_FOR_OR_BACKをセットします。左モーターも同様です。これで左右のモーターの回転方向をセットすることができました。

さらに、ビットチェックにより、回転スピードを判定できるので、それに応じてMTR_RMOTOR_SPD(PWMのDuty比)をセットします。上のコードではFOR1とBACKのときにDuty比45%、FOR2のときに90%になるようにセットしています。

そして、モーター関連の変数が変化したので、イベントEvent_MTR_ChangeAnyStatusを発生しています。

配列MTR_SPEED_TABLEを作成するときに、右モーターに対する情報を3ビットの情報(2~0ビット)として保持しています。(左モーターも同様)

モーターの情報としては、STOP、FOR1、FOR2、BACKの4状態しかないので、端に情報を保持するだけなら2ビットで十分であり、下のように#defineを定義することもできます。

// 左右モーターの情報を2ビットに納めたいとき、
#define MTR_RSTOP 0  // 右モーターSTOP 0000
#define MTR_RFOR1 1  // 右モーターFOR1 0001
#define MTR_RFOR2 2  // 右モーターFOR2 0010
#define MTR_RBACK 3  // 右モーターBACK 0011
#define MTR_LSTOP 0  // 左モーターSTOP 0000
#define MTR_LFOR1 4  // 左モーターFOR1 0100
#define MTR_LFOR2 8  // 左モーターFOR2 1000
#define MTR_LBACK 12 // 左モーターBACK 1100

しかしこの場合は、MTR_SetControlParameter_PostProcess()の中の計算式や条件式のいくつかが、ビットチェックでは行えなくなります。例えば、次のようになります。

// 右モーター:回転方向
MTR_RMOTOR_FOR_OR_BACK = ((speed_table & 0x03 ) == MTR_RBACK);

計算式が複雑になれば命令が増え、それだけ処理時間がかかります。こうしたことも加味してビットチェックしやすいデータにしています。

モーター制御の処理フロー

モーター制御に対する全体的な処理フローとして、次のように動作します。

イベント発生チェック
  +→ TMR2割込みイベントに対するアクション
  |     + MTR_Counter100u が変化
  |     + Event_MTR_ChangeAnyStatusイベントを発生する。
  +→ 赤外線通信でデータを受信したイベントに対するアクション
  |     + MTR_SetControlParameter()をコールする。
  |     + 関数の入力パラメータからMTR_ControlParameterをセット
  |     + Event_MRT_ControlParameter_Changedを発生。
  +→ Event_MRT_ControlParameter_Changedイベントに対するアクション
  |     + MTR_RMOTOR_SPDなどのモーター関連の変数が変化する。
  |     + Event_MTR_ChangeAnyStatusイベントを発生する。
  +→ Event_MTR_ChangeAnyStatusイベントに対するアクション
        + モーター関連の変数を使ってモーターをPWM制御する。

コメント