忍者ブログ

Fグループ電子工作講座

秋月電子SH7125ボードで始めるマイコン開発

関数化(1)

関数化を行ったところでプログラムの動作自体は何も変わらない
「関数化」という作業が増えるだけで成果物に変化は無い
これだけ聞くと意味の無い作業の様に思える。
しかし、この作業により後々で作業効率に大きな影響を及ぼす。

「楽をするためにはどんな努力も惜しまない」
今回はそんな話。

プログラム数学では「関数」の意味が若干違います。
プログラムの場合、「関数=一連の作業をまとめた物」という説明が良いかと思います。
関数の例
y=function(x);
入力するx引数、出力される値を戻り値と呼ぶ。
関数に対して引数と戻り値は必ず設定されている訳ではなく、関数の設定によってあったり無かったりする。

タイプ1:値を入れると値が戻ってくるもの
ある値に対してそれに関連した値を自動計算したい場合に使う。
例:y=sin(x); //典型的な関数の例 数学で言う関数と同じ使い方。
例:theta=atan2(y,x); //引数が複数ある場合の例

タイプ2:関数に入力しないけど値だけ戻ってくるもの
システム内部の値を知りたい場合に使う事が多い。
例:start = clock(); //現在の時刻を取得する。入力する値なんて無い。

タイプ3値を入れて処理をさせるもの
ある値に対して何かしらの処理を行いたい場合に使う。
値が戻ってくることを期待しない場合。
例:printf("Hello World\n"); //値を入れると画面に文字が表示される。戻ってくる値を使う事は稀。
※x=printf("Hello World\n");を実行すると表示した文字数12がxに格納される。

タイプ4:値を入れず何か処理をさせるもの
事前に決めた流れ作業を実行したい場合に使う。
例:getchar(); //キー入力を待機。
※本来getcharはキー入力を待機し、入力されたキーを返す関数なので戻り値が設定されています。



<関数化が必要な例>

たとえば三角関数が標準で使えず、自分で計算しないといけない場合。
マクローリン展開によるsinの近似計算が以下だったとする
  double x;
  double s;
  double e;
  double d;
  double y;
  int k;

  x=0.1;//入力
  s=x;
  e=x;
  double eps = 1e-32;

  x = fmod(x, 2 * π);
  for(k = 2; k <= 200; k = k + 2){
    d = s;
    e = -e * x;
    e = e * x;
    e = e / k;
    e = e / (k + 1);
    s = s + e;
    double work = e;
    if(work < 0){
      work *= -1;
    }
    if(work < eps){
      y = s;
      break;
    }
  }
  if(k>=200){
    y=1;
  }

関数化せずにsin(0.1)*sin(0.2)*sin(0.3)を計算しようと思うと
上記計算式を3つ並べる事になる。
・パッと見で何をしたいのか分からない。
・行数が増えて非常に見辛い。
・同じ計算方法について同じことを何回も書かなければいけない。
・動作確認済みの箇所を簡単に変更可能な場所に置いておくと
 間違えて書き換えてしまう危険がある。

パッと見で何をしたいのか分からない

正直、これが3つ並んでいたとして「あぁ、sinを3回計算してるな~」と
思う人はいないでしょう。
内部でどの様に処理をしてようが
sin(0.1)*sin(0.2)*sin(0.3)
と表記した方が何をしたいか全体の流れが分かりやすいと思います。

行数が増えて非常に見辛い。

同じデータが並ぶとどこが何の処理にそうとうするのか分からなくなります。
行数が増えると少しスクロールを触っただけですごいスピードでスクロールしてしまいます。
また、沢山の行の中から編集したい場所を探し出すのに手間取ります。

同じ計算方法について同じことを何回も書かなければいけない

プリンを作るときに作り方を毎回指示しないといけない様な物。
一回やり方教えたんだから後は同じ要領でやってよ。
これが関数。

間違えて書き換えてしまう危険がある

一度動作確認が終わったら、その場所は今後編集する必要は無いはず。
メイン処理の中に混ざっていると間違えて編集してしまう可能性がある。
動作確認が終わったものはメインの処理から隔離してしまうのが無難です。




<sw1.cの中身を関数化しておく>

関数化するのは3か所

①初期化に関する一連の動作

ここは引数無し戻り値なしであらかじめ定めた処理を実行するだけ。
メイン処理からIOの初期化に関する部分を抜き出して1つの関数に放り込んでおく。
タイプ④の入力無し出力無し

関数の例
void hardware_setup(void){
}

あとはメイン関数の先頭でこの関数を実行するだけ。
これで間違ってPE02を出力にしてしまう事もない。

関数の使い方
hardware_setup();

②LEDの出力

PE.DRL.BIT.B0なんて書かれてもこれが入力なのか出力なのかすら分からない。
これがLED赤だと確認した後はポートEのBit0ではなく、LED_Rとして扱いたい。
PE.DRL.BIT.B1についても同様
タイプ③の入力あり出力無し
※関数としての出力(戻り値)は無いが、IOポートから0/5Vが出る。

関数の例
void set_LED_R(int on){
  if(on!=0)
    PE.DRL.BIT.B0=1;
  else
    PE.DRL.BIT.B0=0;
}
void set_LED_G(int on){
  if(on!=0)
    PE.DRL.BIT.B1=1;
  else
    PE.DRL.BIT.B1=0;
}

関数の使い方
赤LEDを点けたかったら
set_LED_R(1);
赤LEDを消したかったら
set_LED_R(0);

③SW1の入力

これもPE.DRL.BIT.B2って書かれても何の事か分からない。
タイプ②の入力無しの出力あり

関数の例
int get_sw1(void){
 return PE.DRL.BIT.B2;
}

関数の使いかた
x=get_sw1();


これを踏まえてメイン関数を修正すると
main(){
  hardware_setup(); //初期化
  set_LED_R(0); //LED赤消灯
  set_LED_G(0); //LED緑消灯
  while(1){
    if(get_sw1){    //スイッチがONの時
      set_LED_R(1); //LED赤点灯
      set_LED_G(0); //LED緑消灯
    }else{          //スイッチがONじゃない時(OFF)
      set_LED_R(0); //LED赤消灯
      set_LED_G(1); //LED緑点灯
    }
  }
}

これでプログラムの流れが分かりやすくなる。

プロトタイプ宣言

C言語ではある関数を使う場合、「その関数を使う場所より上に関数を作らなければならない」という規則があります。
この規則にそのまま従うと

細かい作業用関数1(){
  作業内容
  作業内容
  作業内容
  作業内容
  作業内容
}

細かい作業用関数2(){
  作業内容
  作業内容
  作業内容
  作業内容
  作業内容
}

細かい作業用関数3(){
  作業内容
  作業内容
  作業内容
  作業内容
  作業内容
}





メイン関数():本来行いたい処理

となります。
正直中身の処理はどうでもいい細かい作業がずらずらと並んだ後、一番最後に本来やりたい作業が出てくるという事態になります。
これでは見たくない(一度設定すれば見る必要のない)を見てからメインの関数を見ることになり、メインの処理を見つけ出すのに時間がかかります。

C言語ではこれを回避するためにプロトタイプ宣言という機能が用意されています。
プロトタイプ宣言はこういう関数を使いますよという予告です。
予告さえしておけば実際に処理を行う関数の実体はどこに書いても良くなります。

プロトタイプ宣言を使った場合

//プロトタイプ宣言(使う関数の予告)
細かい作業用関数1();
細かい作業用関数2();
細かい作業用関数3();

メイン関数(){
}

//関数の実体
細かい作業用関数1(){
  作業内容
  作業内容
  作業内容
  作業内容
  作業内容
}




という順番に変えることができます。
これですぐメイン関数にたどり着くことができるようになります。

関数化したファイルを置いておきます。
関数化済sw1.c


マイコン端子の特殊機能を使いたい場合はかなりの量の設定値を書く必要がります。
これを1つのファイルに全て書くとプロトタイプ宣言が大量に発生して見づらくなります。
これを解消する方法がありますがまた今度。
PR

コメント

プロフィール

HN:
ぼんどF博士
性別:
男性

最新コメント

[02/26 とし]
[02/25 ぼんどF博士]
[02/25 とし]
[10/25 ぼんどF博士]
[10/18 ss400]