LoginSignup
35
36

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-09-09

後輩くん向けメモ

複雑な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

以後、適時追加します。

35
36
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
36