C
C++
Arduino
マイコン
pic

マイコンデザインパターン

More than 1 year has passed since last update.

後輩くん向けメモ

複雑なifはステート化する

変更前

なんかセンサーで障害物を回避したいとか言ってやってるが、
奇々怪々な挙動をして、一体どのif文が反応しているかわからない...
優先順位もよくわからない

  • 条件と行いたいことが混ざっていて見通しが悪い
  • その場限りの条件が量産される
  • 自分で条件が把握しきれなくなっている
//一番上の条件が優先される
if(Sensor1 < 10 && Sensor2 < 10 && Sensor3 < 10)
{
  MotorMode(0);
  LMotorNextStep(10);
  RMotorNextStep(10);
}else if(Sensor1 < 10 && Sensor2 < 10)
{
  if(Sensor1 < 5)
  {
    MotorMode(0);
    LMotorNextStep(10);
    RMotorNextStep(20);
  }else{
    MotorMode(0);
    LMotorNextStep(20);
    RMotorNextStep(10);
  }
}else if(Sensor2 < 10 && Sensor3 < 10)
  if(Sensor3 < 5)
  {
    MotorMode(0);
    LMotorNextStep(10);
    RMotorNextStep(20);
  }else{
    MotorMode(0);
    LMotorNextStep(20);
    RMotorNextStep(10);
}else{
 ...


変更後

判定と行動を分ける。

  • 条件と、行いたい行動が分離して見通しが良くなる。
  • また、現在の状態を知るために1つの変数を見るだけで良くなる。
  • 前回の判定結果との差を使えるようになる。
#define MODE_AHEAD  0
#define MODE_LEFT   1
#define MODE_RIGHT  2
#define MODE_BACK   3

static int dir = MODE_AHEAD; //どれにも当てはまらなかった場合
static int old_dir = MODE_AHEAD; //前回の条件を記録する

//-----------判定---------------

if(Sensor1 < 10 && Sensor2 < 10 && Sensor3 < 10)
{
  dir = MODE_BACK;
}

if(Sensor1 < 10 && Sensor2 < 10)
{
  dir = MODE_LEFT;
}

if(Sensor1 < 5 && Sensor2 < 10)
{
  dir = MODE_RIGHT;
}

//直前の挙動を判定に使えるようになる
if(Sensor1 < 5 && Sensor2 < 10 && old_dir == MODE_BACK)
{
  dir = MODE_LEFT;
}


...

//一番最後に代入された条件が実行される
//→先に行った条件の緩い判定を、より厳しい判定で上書きできる

//-----------行動---------------

if(dir == MODE_AHED)
{
  MotorMode(2);
  LMotorNextStep(10);
  RMotorNextStep(10);
}

if(dir == MODE_BACK)
{
  MotorMode(0);
  LMotorNextStep(10);
  RMotorNextStep(10);
}

...

//-----------報告---------------
//変化点を報告できるようになる
if(dir != old_dir)
{
  printf("Mode: %d → %d\n",old_dir,dir);
}
old_dir = dir; //更新

変更後2

2017/09/10
頂いたコメントより。
指導の都合上、ifからの変形を意図していたためifのままでしたが、switch使ったほうがスッキリしますね。

break;

を忘れると大事故になるので注意

...

//-----------行動---------------

switch (dir) {
case MODE_AHED:
  MotorMode(2);
  LMotorNextStep(10);
  RMotorNextStep(10);
  break;
case MODE_BACK:
  MotorMode(0);
  LMotorNextStep(10);
  RMotorNextStep(10);
  break;

  ...

}

ループは小さく回す

関数が大回りしている。
結果グローバル変数祭りや、static変数祭りになりがちになる。

変更前

main.c
int mode = 0;

void setup()
{
...
}

void loop()
{
  //順序だってやる処理
  switch(mode)
  {
    case 0:
      mode0();
      break;
    case 1:
      mode1();
      break;
    case 2:
      mode2();
      break;
  }  
}
mode0.c
//暫く掛かる(かなりの回数のループを必要とする)処理
void mode0()
{
   ...
   if(...)
   {
     mode = 1;
   }
   ...
}

変更後

main.c
void setup()
{
...
}

void loop()
{
  //順序どおり書ける
  mode0();
  mode1();
  mode2();
}
mode0.c
//処理が終わるまでローカルスコープを抜けないため、
//ローカル変数を有効に使えるようになる。使い回しが効きやすくなる
void mode0()
{
  //ここで初期化処理ができるようになる
  int cnt = 0;

  //ループの内側だけ考えれば良くなる。
  while(1)
  {
    ...

    if(...)
      break;
  }

  //終了処理ができるようになる。
}

グローバル変数は使うファイルにまとめる

変更前

main.c
#include "global.c"
#include "mode0.c"

//modeやdeltaはここでしか使わない
void setup()
{
  mode=0;
  delta = 0.552;
}

void loop()
{
  if(mode == 0)
    mode0();
  if(...)
    mode++;
}
mode0.c
//somevarはここでしか使わない
void mode0()
{
  somevar+=2.2
  ...
}

global.c
int i; //←!?
int mode=0;
float delta = 0.552;
float somevar = 1.3;
//その他プログラム中全てのグローバル変数数十個がここに集結している
//なんでや

変更後

※そもそもグローバル変数使わないに越したことはないですが。

2017/09/10
コメントより、ファイルスコープに収めるためにstatic付与したほうが安全ということで修正。
こうすることで、別のファイルから別のファイルのグローバル変数をうっかり触ることがなくなります。

main.c
#include "mode0.c"

static int mode=0;
static float delta = 0.552;

void setup()
{
  mode=0;
  delta = 0.552;
}

void loop()
{
  if(mode == 0)
    mode0();
  if(...)
    mode++;
}
mode0.c
static float somevar = 1.3;
void mode0()
{
  somevar+=2.2
  ...
}

ピンには名前をつける

変更前

本人でもわからなくなります。

main.c
void setup()
{
  pinMode(0,OUTPUT);
  pinMode(1,OUTPUT);
  pinMode(2,OUTPUT);
  pinMode(3,OUTPUT);
  pinMode(4,INPUT);
  pinMode(5,OUTPUT);
  pinMode(6,OUTPUT);
}

void loop()
{
  digitalWrite(3,HIGH);

  for(int i=0;i<5;i++)
  {
    digitalWrite(2,HIGH);
    digitalWrite(5,HIGH);
    delay(1);  
    digitalWrite(2,LOW);
    digitalWrite(5,LOW);
    delay(1);  
  }

  if(digitalRead(4)==LOW)
  {
    digitalWrite(6,HIGH);
  }else{
    digitalWrite(6,LOW);
  }

  digitalWrite(3,LOW);
  delay(50);
}

変更後

定数で名前つけましょう。C言語風にdefine使ってもいいですが、
事故防止にはconstがおすすめです。

main.c
const int running_led = 3;
const int sensor_led = 6;
const int sensor_in = 4;
const int stepping_motor_L = 2;
const int stepping_motor_R = 5;
const int comm1 = 0;
const int comm2 = 1;

void setup()
{
  pinMode(comm1,OUTPUT);
  pinMode(comm2,OUTPUT);
  pinMode(stepping_motor_L,OUTPUT);
  pinMode(running_led,OUTPUT);
  pinMode(sensor_in,INPUT);
  pinMode(stepping_motor_R ,OUTPUT);
  pinMode(sensor_led,OUTPUT);
}

void loop()
{
  digitalWrite(running_led,HIGH);

  for(int i=0;i<5;i++)
  {
    digitalWrite(stepping_motor_L ,HIGH);
    digitalWrite(stepping_motor_R ,HIGH);
    delay(1);  
    digitalWrite(stepping_motor_L ,LOW);
    digitalWrite(stepping_motor_R ,LOW);
    delay(1);  
  }

  if(digitalRead(sensor_in)==LOW)
  {
    digitalWrite(sensor_led,HIGH);
  }else{
    digitalWrite(sensor_led,LOW);
  }

  digitalWrite(running_led,LOW);
  delay(50);
}

変更後2

2017/09/10
enumを使うと簡素になりますとご指摘を頂いたので。たしかにその通りです。
よりシンプルに書けて良いですね。
(恥ずかしながらこういったシンプルなenumの使い方を知りませんでした)

ただし、Arduino等ではconst intの列挙が一般的のようです。

main.c
enum {
  running_led = 3,
  sensor_led = 6,
  sensor_in = 4,
  stepping_motor_L = 2,
  stepping_motor_R = 5,
  comm1 = 0,
  comm2 = 1,
};

void setup()
{
  pinMode(comm1,OUTPUT);
  pinMode(comm2,OUTPUT);
  pinMode(stepping_motor_L,OUTPUT);
  pinMode(running_led,OUTPUT);
  pinMode(sensor_in,INPUT);
  pinMode(stepping_motor_R ,OUTPUT);
  pinMode(sensor_led,OUTPUT);
}

void loop()
{
  digitalWrite(running_led,HIGH);

  for(int i=0;i<5;i++)
  {
    digitalWrite(stepping_motor_L ,HIGH);
    digitalWrite(stepping_motor_R ,HIGH);
    delay(1);  
    digitalWrite(stepping_motor_L ,LOW);
    digitalWrite(stepping_motor_R ,LOW);
    delay(1);  
  }

  if(digitalRead(sensor_in)==LOW)
  {
    digitalWrite(sensor_led,HIGH);
  }else{
    digitalWrite(sensor_led,LOW);
  }

  digitalWrite(running_led,LOW);
  delay(50);
}

デバイスは抽象化する

変更前

確かに動くんですけど、後々仕様変更する時に死ぬ思いします。
モータードライバを変えるとか、速度を変えるとか。

if(dir == MODE_AHED)
{
  digitalWrite(stepping_motor_L_dir ,HIGH);
  digitalWrite(stepping_motor_R_dir ,LOW);
  digitalWrite(stepping_motor_L_boostmode ,LOW);
  digitalWrite(stepping_motor_R_boostmode ,LOW);
  for(int i=0;i<500;i++)
  {
    digitalWrite(stepping_motor_L ,HIGH);
    digitalWrite(stepping_motor_R ,HIGH);
    delay(1);  
    digitalWrite(stepping_motor_L ,LOW);
    digitalWrite(stepping_motor_R ,LOW);
    delay(1);  
  }
}

if(dir == MODE_BACK)
{
  digitalWrite(stepping_motor_L_dir ,HIGH);
  digitalWrite(stepping_motor_R_dir ,HIGH);
  digitalWrite(stepping_motor_L_boostmode ,LOW);
  digitalWrite(stepping_motor_R_boostmode ,LOW);
  for(int i=0;i<500;i++)
  {
    digitalWrite(stepping_motor_L ,HIGH);
    digitalWrite(stepping_motor_R ,HIGH);
    delay(1);  
    digitalWrite(stepping_motor_L ,LOW);
    digitalWrite(stepping_motor_R ,LOW);
    delay(1);  
  }
}

変更後

複雑な処理、何度もやる処理は、関数に隠しましょう。
可能であれば、見ただけで「なにをしているのか」が分かる名前や単位に分解できると、コメントがなくても理解しやすいプログラムになります。

if(dir == MODE_AHED)
{
  MotorMode(2);//動作を分解し、関数化しておく。関数の関数の関数とかもよくある。
  LMotorNextStep(10);
  RMotorNextStep(10);
}

if(dir == MODE_BACK)
{
  MotorMode(0);
  LMotorNextStep(10);
  RMotorNextStep(10);
}

変更後2

抽象化しておくと、PCのC言語環境などでシミュレーションするのも楽になるし、コードの量も減る。

if(dir == MODE_AHED)
{
  robo_move(move_ahed,10);
}

if(dir == MODE_BACK)
{
  robo_move(move_back,10);
}

長期間存在する変数を直接触らない&複数のことを1箇所でやろうとしない

モーターの動いた距離を計算したいらしいといった場合。

変更前

  • 距離変数を直接足したり引いたりしている
  • そのうち妙な操作をし始めたり、別の場所で参照を始めるだろう
  • 式中の定数に名前を振っていない
  • 直接の目的外のことを同じ関数に書いている(行動と移動距離の測計算)
walk.c
...

//-----------行動---------------

switch (dir) {
case MODE_AHED:
  robo_move(move_ahed,10);
  //変数を直接操作している
  step_l += 10;
  step_r += 10;
  break;
case MODE_BACK:
  robo_move(move_back,10);
  //もちろん何箇所もある
  step_l -= 10;
  step_r -= 10;
  break;

  ...

}
  ...

  //-----------距離計算---------------
  //変数を直接操作している&定数に名前がない&邪魔
  len_l  = (step_l/600)*16.53;
  len_r = (step_r/600)*16.53;
  delta_theta = atan(...,...)
  delta_x = len_x*cos(...);
  delta_y = len_x*sin(...);

  //ほしいのはこれら
  x += delta_x; 
  y += delta_y;
  t += delta_theta;

  step_l = 0;
  step_r = 0;
  ...

変更後

  • 分離した
  • 変数には直接触らず、操作用の関数を通して触る
  • 計算式や、記録したい情報の形式の変更時、デバッグのメッセージを仕込むなどと言ったときに便利になる
walk.c
...

//-----------行動---------------

switch (dir) {
case MODE_AHED:
  robo_move(move_ahed,10);
  //抽象化した
  add_step(10,10);
  break;
case MODE_BACK:
  robo_move(move_back,10);
  add_step(-10,-10);
  break;

  ...

}
  ...
  //読み出しもシンプルになる
  float x,y,t;
  get_step_len(&x,&y,&t);
  ...

walkCalc.c
//移動と直接関係ないのでファイルごと分離できる

const float wheel_size = 16.53;
const float cycle_step = 600;

static int step_l=0;
static int step_r=0;

static float move_x=0;
static float move_y=0;
static float move_theta=0;

void reset_move()
{
  move_x = 0;
  move_y = 0;
  move_theta = 0;
}

void reset_step()
{
  step_l = 0;
  step_r = 0;
}

void add_step(int l,int r)
{
  step_l += 10;
  step_r += 10;
}

void get_step_len(float *x,float *y,float *t)
{
  len_l  = (step_l/cycle_step)*wheel_size;
  len_r = (step_r/cycle_step)*wheel_size;

  delta_theta = atan(...,...)
  delta_x = len_x*cos(...);
  delta_y = len_x*sin(...);
  move_x += delta_x;
  move_y += delta_y;
  move_theta += delta_theta;

  *x = move_x;
  *y = move_y;
  *t = move_theta;

  reset_step();
}

変更後2

クラス化

walk.c
walkCalc log;
...

//-----------行動---------------

switch (dir) {
case MODE_AHED:
  robo_move(move_ahed,10);
  log.add_step(10,10);
  break;
case MODE_BACK:
  robo_move(move_back,10);
  log.add_step(-10,-10);
  break;

  ...

}
  ...
  //読み出しもシンプルになる
  float x,y,t;
  log.get_step_len(x,y,t);
  ...

walkCalc.c
class walkCalc
{
private:
  const float wheel_size = 16.53;
  const float cycle_step = 600;

  int step_l=0;
  int step_r=0;

  float move_x=0;
  float move_y=0;
  float move_theta=0;

public:
  void reset_move()
  {
    move_x = 0;
    move_y = 0;
    move_theta = 0;
  }

  void reset_step()
  {
    step_l = 0;
    step_r = 0;
  }

  void add_step(int l,int r)
  {
    step_l += 10;
    step_r += 10;
  }

  //参照渡し
  void get_step_len(float &x,float &y,float &t)
  {
    len_l  = (step_l/cycle_step)*wheel_size;
    len_r = (step_r/cycle_step)*wheel_size;

    delta_theta = atan(...,...)
    delta_x = len_x*cos(...);
    delta_y = len_x*sin(...);
    move_x += delta_x;
    move_y += delta_y;
    move_theta += delta_theta;

    x = move_x;
    y = move_y;
    t = move_theta;

    reset_step();
  }
}

参照渡しについては以下を参照

C++ 値渡し、ポインタ渡し、参照渡しを使い分けよう
http://qiita.com/agate_pris/items/05948b7d33f3e88b8967

以後、適時追加します。