1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Koushiroによる WRO / EV3rt / SPIKE-RT などなどのAdvent Calendar 2023

Day 21

#10 関数を使おう【もっと!後輩たちのためのEV3rt講座】

Posted at

目次

タイトル 内容
1 はじめに 今回やることの説明
2 関数とは(おさらい) 関数のキホン(#4の復習)
3 関数の「データ型」 戻り値や引き数に指定する型について
4 関数の定義・実行 関数の定義と実行の仕方
5 関数のプロトタイプ宣言 関数のプロトタイプ宣言についての説明
6 まとめ 今回のまとめ

1. はじめに

前回までで、EV3のモータやセンサ、インテリジェントブロックといったプログラム可能なものを動かす方法は説明が完了しました。
今回からは、より効率的にプログラムを作成したり、より発展的な動きをさせるための方法を紹介していきたいと思います。

まず今回は、改めて 「関数の使い方」 について解説していきます。
既に#4 C言語のキホン➀ 6章にて、関数の大まかな概要は説明していますが、より具体的に解説し、実際に独自の関数を作成・使用するところまでいきたいと思います。
又、説明にあたり、EV3ソフトウェアの 「マイブロック機能」 と比較しながら説明していきます。

マイブロック.png

2. 関数とは(おさらい)

上述のとおり、#4にて既に関数の解説は行っていますが、ここでも改めてその概要を説明しておきます。
覚えていて必要なければここは読み飛ばしてください。

関数を一言で説明すれば、「ある値を入れた時に、決められた法則に基づいて値を返してくれる機能」です。

イラストで表すとこんな感じ👇

func.gif

上の図に表すように、関数にいくつかの値を入れます。
この値のことを 「引き数」(ひきすう) と言います。

引き数を受け取った関数は、決まった法則に基づいて処理を行います。

その結果得られた値を 「戻り値」 として返します。

例えば、以下のような「足し算を行う関数」があるとします。

int sum(int a, int b){
  return a + b;
}

関数の名前がsumで、引き数はabの二つです。
意味としては「引き数」として受け取ったabの値を足し合わせて、「戻り値」として返すというものです。

よって、以下のように関数を実行すると…

int c = sum(2,3);

変数cには5が入るということになります。

ここで、abの前、さらにsumの前に int という文字がありますが、これらはデータの「型」というものでした。
関数の「引き数」や「戻り値」には、必ずデータ型を付ける必要があります。

上記の関数で言えば、「引き数」として整数値であるabを受け付けて、a+bの結果を整数値で返す、という意味になります。

ここで、マイブロックと照らし合わせてみると、以下のような関係になります。

関数とマイブロック.png

3. 関数の「データ型」

前章で、関数の「引き数」や「戻り値」には「データ型」があるとお話しましたが、もう少し詳しく説明したいと思います。

戻り値の「データ型」

戻り値のデータ型は、関数名の前に記します。
上記のsum関数の例ではint型を使用しましたが、当然どんな型を使っても大丈夫です。

例として、前回紹介した電圧値の取得関数を使って、残りの電池残量が何パーセントなのか表示する関数を作ってみましょう。

double remain_battery(){
    int current_v = ev3_battery_voltage_mV();
    double base_v = 9000.0;
    return current_v / base_v * 100;
}

上のコードでは、定格である9Vに対し、現在の電圧値がどれくらいの割合なのかを、double型で返しています。
このように、戻り値の型は自由に決めることが出来ます。

さらにロボットプログラミングにおいては、特定の決まった動きをさせたいだけの場合、計算はしていないので「戻り値はいらない」という状況もあるかと思います。
そのような場合、「戻り値は何も無い」 ことを示す void 型を使います。

こちらも、例を示してみましょう。

LEDが赤色に点滅するコードを作成し、異常発生時に光らせることが出来るよう関数として登録したいとします。

void warning(){
    int i;
    for(i=0; i<10; i++){
        ev3_led_set_color(LED_RED);
        dly_tsk(100*1000);
        ev3_led_set_color(LED_OFF);
        dly_tsk(100*1000);
    }
}

for文によりLEDが点滅する関数をwarningとして作成しました。
この関数に「戻り値」は必要ないので、関数名warningの前の型には、voidと指定しています。

このようにして、関数の型を適宜決めていくことになります。

引き数について

ここでお気づきの方もいるでしょうが、関数に与えるデータである「引き数」も、省略することが可能です。
上記のremain_battery関数にしても、warning関数にしても、カッコの中身が何もないことが分かるかと思います。
このように、「引き数」も必要が無ければ無理に与えることは無く、そのまま空白にしておけば良いのです。

一方で、一つの関数により多くの引き数を与えることも可能です。
一番最初のsum関数では、引き数はabの二つでしたが、cdを追加して引き数を三つ、四つと増やしても大丈夫です。

int sum(int a, int b, int c, int d){
  return a + b + c + d;
}

ただし、多くしすぎるとそれはそれで関数が見にくく、扱いづらくなる傾向にあります。
よって、多くても5個程度に留めるようにしておきましょう。
多くなってしまう場合は、機能を絞るか、より細かい単位でアルゴリズムを構成してみると良いかと思います。

4. 関数の定義・実行

関数の基本事項を一通りおさえたところで、実際に関数を定義して、実行してみましょう。

今回は、「指定された色を検出するまでライントレースする」というシチュエーションで考えてみましょう。
ひとまず、今まで通りmain_task内に、上記ミッションをクリアするプログラムを書いてみます。

app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

void main_task(intptr_t unused)
{
    ev3_motor_config(EV3_PORT_A, LARGE_MOTOR);
    ev3_motor_config(EV3_PORT_D, LARGE_MOTOR);
    ev3_sensor_config(EV3_PORT_1, COLOR_SENSOR);

    int power = 30;
    int border = 50;
    double gain = 0.5;

    while(ev3_color_sensor_get_color(EV3_PORT_1) != COLOR_RED){
        int ref = ev3_color_sensor_get_reflect(EV3_PORT_1);
        int power_A = power + (border - ref) * gain;
        int power_D = power - (border - ref) * gain;
        ev3_motor_set_power(EV3_PORT_A, power_A);
        ev3_motor_set_power(EV3_PORT_D, power_D);
        tslp_tsk(1);
    }

    ev3_motor_stop(EV3_PORT_A, true);
    ev3_motor_stop(EV3_PORT_D, true);
}

このうち、11~25行目が関数にしたい部分です。
この部分を切り取り、関数としてmain_taskの上に定義します。

app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

// 関数の定義
void linetrace(int power, int border, double gain, colorid_t color){
    while(ev3_color_sensor_get_color(EV3_PORT_1) != color){
        int ref = ev3_color_sensor_get_reflect(EV3_PORT_1);
        int power_A = power + (border - ref) * gain;
        int power_D = power - (border - ref) * gain;
        ev3_motor_set_power(EV3_PORT_A, power_A);
        ev3_motor_set_power(EV3_PORT_D, power_D);
        tslp_tsk(1);
    }

    ev3_motor_stop(EV3_PORT_A, true);
    ev3_motor_stop(EV3_PORT_D, true);
}

void main_task(intptr_t unused)
{
    ev3_motor_config(EV3_PORT_A, LARGE_MOTOR);
    ev3_motor_config(EV3_PORT_D, LARGE_MOTOR);
    ev3_sensor_config(EV3_PORT_1, COLOR_SENSOR);

    linetrace(30, 50, 0.5, COLOR_RED); // 関数の呼び出し
}

include文の直後に、linetraceという関数名でライントレースする関数を定義しました。

引き数として、「ポート」「閾値」「比例ゲイン」「検知色」を用意しています。
このうち「検知色」の部分に注目すると、データ型としてcolorid_tを指定しています。
これは#7 カラーセンサを使おう 3章で紹介した、カラーセンサ検知色の列挙型です。
前の章でもお伝えしましたが、このように引き数にはintdoubleといったものに限らず、列挙型のような型でも指定することが出来ます。

話を戻して、与えられた四つの引き数は、パワーの算出に用いられ、モータを動かす処理を行います。
ループの条件式にて、引き数で指定した「検知色」ではない時にループするとなっているので、逆に言えば指定した色を検知したタイミングで、ループを抜け、モータを停止した後関数は終了します。

もともとmain_taskに書かれていたところには、関数を呼び出すコードが書かれています。

これにより、「関数の定義」および「呼び出し」が出来ました。

繰り返し呼び出す

さて、関数の最大のメリット、それは「同じ処理を繰り返し行うことに強い点」です。
ロボット競技においては、ライントレースはしょっちゅう行われます。
その度に、十数行に渡るコードを書き直すのは非常に面倒ですし、可読性を下げることにつながります。

しかし、一度関数として定義しておけば、あとは呼び出すコードを書くだけで、同じ動きをさせることが出来るのです。
先ほどのlinetrace関数を何度か呼び出してみましょう。

app.c
void main_task(intptr_t unused)
{
    ev3_motor_config(EV3_PORT_A, LARGE_MOTOR);
    ev3_motor_config(EV3_PORT_D, LARGE_MOTOR);
    ev3_sensor_config(EV3_PORT_1, COLOR_SENSOR);

    linetrace(30, 50, 0.5, COLOR_RED); // 関数の呼び出し

    // 直進
    ev3_motor_rotate(EV3_PORT_A, 360, 50, false);
    ev3_motor_rotate(EV3_PORT_D, 360, 50, true);

    linetrace(30, 50, 0.5, COLOR_YELLOW); // 関数の呼び出し

    // 直進
    ev3_motor_rotate(EV3_PORT_A, 360, 50, false);
    ev3_motor_rotate(EV3_PORT_D, 360, 50, true);

    linetrace(30, 50, 0.5, COLOR_BLUE); // 関数の呼び出し
}

(include文、関数の宣言は省略しています。)

この例では、初め「赤色を検知するまでライントレース」を行い、少し直進した後、「黄色を検知するまでライントレース」を行い、さらに少し直進した後、「青色を検知するまでライントレース」を行っています。

実に3回も関数を呼び出しており、ベタ打ち(それぞれ打ち直し)した際に比べて二十数行の削減につながっています。
そして何より、プログラムをパッと見たとの分かりやすさが段違いに違います。
又、この例では「検知色」を毎回変えていますが、このように引き数の設定次第で関数の動きに幅を持たせることも可能です。
アルゴリズムの組み立て方次第で、効率的かつ読みやすいプログラムが作れるので、試行錯誤してみましょう。

5. 関数のプロトタイプ宣言

関数定義の順序

先ほどの関数の定義の例について、改めて見てみましょう。

app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

// 関数の定義
void linetrace(int power, int border, double gain, colorid_t color){
    while(ev3_color_sensor_get_color(EV3_PORT_1) != color){
        int ref = ev3_color_sensor_get_reflect(EV3_PORT_1);
        int power_A = power + (border - ref) * gain;
        int power_D = power - (border - ref) * gain;
        ev3_motor_set_power(EV3_PORT_A, power_A);
        ev3_motor_set_power(EV3_PORT_D, power_D);
        tslp_tsk(1);
    }

    ev3_motor_stop(EV3_PORT_A, true);
    ev3_motor_stop(EV3_PORT_D, true);
}

void main_task(intptr_t unused)
{
    ev3_motor_config(EV3_PORT_A, LARGE_MOTOR);
    ev3_motor_config(EV3_PORT_D, LARGE_MOTOR);
    ev3_sensor_config(EV3_PORT_1, COLOR_SENSOR);

    linetrace(30, 50, 0.5, COLOR_RED); // 関数の呼び出し
}

先ほど私は、関数をmain_taskの上に定義するように、と記しました。
これには大事な理由があります。

プログラムファイルを実行ファイルに変換する「コンパイラ」は、原則的にプログラムファイルを上から順に読んでいきます。
もし関数をmain_taskの上に書いておらず、急にlinetrace(30, 50, 0.5, COLOR_RED);などとmain_task内に書かれてしまうと、

コンパイラ「linetrace君?? いや、君どこの誰??」

と、なってしまうわけです。
よって、main_taskの上に関数を定義しなければならないのです。

(※最近のコンパイラは非常に優秀なので、main_taskより下に書いてもコンパイルしてくれることが多くありますが、動作の不安定化に繋がるので極力控えましょう。)

関数が増えたら…

ここで、今回の例はlinetrace関数のみを定義しているのでまだ大丈夫ですが、開発を進めるうえでどんどん関数が増えていったとしたら…
想像してみてください。app.cファイルを開くと、大量の独自関数が定義され、なかなかmain_taskにたどり着けない…

そんなことが、ロボットのプログラミングでは往々にして起こります。

これではかえってプログラムが読みにくくなっており、本末転倒です。

そこで、コンパイラに対して、

私「後で『linetrace』ってやつが来るから、覚えといてな!」

という宣言をする機能があります。
これを、 「関数のプロトタイプ宣言」 と言います。

この5章では、関数のプロトタイプ宣言をする方法を示していきます。

プロトタイプ宣言の仕方

プロトタイプ宣言の仕方は、「関数の1行目だけを先に書いておく」、ただそれだけです。
例を示した方が分かりやすいかと思いますので、先ほどのlinetrace関数を例にとってみてみましょう。

app.c
#include "ev3api.h"
#include "app.h"
#include <stdio.h>

// 関数のプロトタイプ宣言
void linetrace(int power, int border, double gain, colorid_t color);

void main_task(intptr_t unused)
{
    ev3_motor_config(EV3_PORT_A, LARGE_MOTOR);
    ev3_motor_config(EV3_PORT_D, LARGE_MOTOR);
    ev3_sensor_config(EV3_PORT_1, COLOR_SENSOR);

    linetrace(30, 50, 0.5, COLOR_RED); // 関数の呼び出し
}

// 関数の定義
void linetrace(int power, int border, double gain, colorid_t color){
    while(ev3_color_sensor_get_color(EV3_PORT_1) != color){
        int ref = ev3_color_sensor_get_reflect(EV3_PORT_1);
        int power_A = power + (border - ref) * gain;
        int power_D = power - (border - ref) * gain;
        ev3_motor_set_power(EV3_PORT_A, power_A);
        ev3_motor_set_power(EV3_PORT_D, power_D);
        tslp_tsk(1);
    }

    ev3_motor_stop(EV3_PORT_A, true);
    ev3_motor_stop(EV3_PORT_D, true);
}

include分の下に追加されたのが、「関数のプロトタイプ宣言」です。
これにより、

「後でこういう引き数・戻り値を持ったlinetraceっていう名前の関数が来るぞ~」

と教えておくことが出来るのです。
これを受け取ったコンパイラは、

コンパイラ「了解、覚えておくね~」

となり、main_task内を読み進めてくれます。
最後にlinetrace関数の定義部分が現れたら、

コンパイラ「あ、君がさっきのlinetrace君ね」

と、関数定義の本体部分を認識し、しっかりコンパイルしてくれるのです。

この関数プロトタイプ宣言を行うことで、可読性を担保しながら関数を定義、実行することが出来ます。是非有効活用してください。

6. まとめ

今回は関数の使い方を詳しいところまで解説していきました。
本当は、作成した関数を別のファイルに記述したりすると、より綺麗に書けるのですが、それはまた別の回に紹介したいと思います。
皆さんも関数を有効活用して、効率的かつ可読性のあるプログラムを書いてみてください。

次回はEV3rt講座最大の山場、「タスクと周期ハンドラ」のお話です。
少し難しい概念が登場しますが、頑張って学習していきましょう。

前回: #9 インテリジェントブロックの機能を使おう
次回: 執筆中

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?