Arduino学习:Callback回调函数的理解

在学Arduino的过程中,回调函数(Callback)的概念让我迷糊了好一阵。在理解后把这个理解过程记录下来。

网上搜索了很久,很多文章没有看的太明白,后续问了专业编程的同事,并且找到了一个网上的描述的很清晰文章,感觉终于理解过来了。

网上文章出处:https://www.gammon.com.au/callbacks 以下是翻译+理解过程:

我们在写一些的Arduino程序时,经常会有需求:希望在某种条件发生的情况下,执行这个情况对应的操作。一般我们采用条件语句switch case 来完成就可以,比如:

int act = 2; // 2 is an example

switch (action)

  {

  case 0: doAction0 (); break;

  case 1: doAction1 (); break;

  case 2: doAction2 (); break;

  case 3: doAction3 (); break;

  case 4: doAction4 (); break;

  } // end of switch

这样写也没问题,但是比较冗长。我们也可以这样写:

action=3;

doAction(action);

这样执行我们需要把action作为参数传递到一个广义的doAction函数的内部,doAction需要定义类似的条件语句并描述好原来doAction1-4的所有算法。其实和Switch case 方法没有什么区别,只是换了一个地方。

函数指针的使用:

可以定义指向函数类型的指针型变量。首先用typedef先来声明你计划调用的函数的类型是很有用的,比如我们定义调用的函数是不带参数和不带返回值的类型的:

typedef void (*GeneralFunction) ();

接下来我们可以定义一些这类的函数如:

void doAction1 () {

Serial.println (“动作1”);

}

void doAction2 () {

Serial.println (“动作2”);

}

然后我们可以用定义好的指针型变量直接指向不同的函数:

GeneralFunction Foo;

Foo = doAction1;

Foo(); // 切记不要忘了括号!

这样Foo()执行的,其实就是doAction1()函数,我们可以更改变量Foo的值来改变指向的函数;

以上的函数类型是很特定的,那么怎么调用带有参数和返回值的函数呢?

如果要用指针型变量指向这类函数,需要这样去做typedef的定义:

typedef int (*GeneralArithmeticFunction)  (const int arg1, const int arg2);

这样定义了一个带两个整形参数并返回一个整形结果的函数指针。

然后我们定义几个这中类型的函数:

int Add (const int arg1, const int arg2) {

return arg1 + arg2;

} // 加法运算

int Subtract (const int arg1, const int arg2) {

return arg1 - arg2;

} // 减法运算

int Multiply (const int arg1, const int arg2) {

return arg1 * arg2;

} // 乘法运算

int Divide (const int arg1, const int arg2) {

return arg1 / arg2;

} // 除法运算

然后我们可以定义这类函数的指针变量通过赋值去指向不同运算:

GeneralArithmeticFunction fAdd = Add;

GeneralArithmeticFunction fSubtract = Subtract;

GeneralArithmeticFunction fDivide = Divide;

GeneralArithmeticFunction fMultiply = Multiply;

  //  use the function pointers

  Serial.println (fAdd (40, 2)); # 输出42

  Serial.println (fSubtract (40, 2)); # 输出38

  Serial.println (fDivide (40, 2)); # 输出20

  Serial.println (fMultiply (40, 2)); # 输出80

到这里,我们已经做到了灵活的通过指针型变量去指向不同的函数。它是回调函数的基础,但不是回调函数的完全体现。现在我们尝试一下:

通常我们的“主”代码将调用库(或其他)里的函数,向该函数传递指向“回调”函数的指针,然后该函数将在适当的时间调用。

typedef void (*GeneralMessageFunction) ();

void sayHello ()

    Serial.println ("Hello!"); 

} // end of sayHello

void sayGoodbye ()

    Serial.println ("Goodbye!"); 

} // end of sayGoodbye

void checkPin (const int pin, GeneralMessageFunction response); // prototype

void checkPin (const int pin, GeneralMessageFunction response) 

    if (digitalRead (pin) == LOW) { 

        response (); // call the callback function 

        delay (500); // debounce 

    } 

} // end of checkPin 

void setup ()

    Serial.begin (115200); 

    Serial.println (); 

    pinMode (8, INPUT_PULLUP); 

    pinMode (9, INPUT_PULLUP); 

} // end of setup

void loop () {

    checkPin (8, sayHello); 

    checkPin (9, sayGoodbye);

}  // end of loop

我们在checkPin函数的定义里增加了一个函数指针的参数,当pin的值符合拉低条件时,该指针便指向一个类型的函数。在实际应用checkPin函数时,为该指针赋一个值(实参),使其指向特定的函数并执行。也可以这么理解,checkPin是这样工作的:如果指定的pin值被拉低,那么checkPin函数就去调用另一个函数去执行。这个被调用的函数,它是checkPin函数中的一个实参。我们可以说,checkPin函数实现了Callback的功能。

Arduino中的中断功能,就是典型的回调:

attachInterrupt (0,blink, CHANGE);

attachInterrupt函数定义如果中断引脚的值发生变化的情况下,就去调用blink函数。

接下来我们看回调函数相对复杂的应用,比如用在排序功能中。

const int COUNT = 10;

int someNumbers [COUNT] = { 7342, 54, 21, 42, 18, -5, 30, 998, 999, 3  };

// callback function for doing comparisons

template int myCompareFunction (const void * arg1, const void * arg2) {

    T * a = (T *) arg1;  // cast to pointers to T

    T * b = (T *) arg2;

    if (*a < *b) { return -1;} // a less than b?

    if (*a > *b) {return 1; }// a greater than b?

    return 0; // must be equal

  }  // end of myCompareFunction

void setup () {

    // sort using custom compare function

    qsort (someNumbers, COUNT, sizeof (int), myCompareFunction);

    for (int i = 0; i < COUNT; i++) {

        Serial.println (someNumbers [i]); }

}  // end of setup

void loop () { }

qsort的compar参数是一个回调函数(关于qsort的函数介绍请网上搜索),它被设定为myCompareFunction<数据类型>,通过template 的模板定义,可以用函数去定义数据类型,不需要固定死。于是我们就可以在用qsort时没有数据类型的限制,例如替换成:

qsort (someNumbers, COUNT, sizeof (float), myCompareFunction<float>);

用它可以直接实现浮点数的排序而不需要更改回调函数的内容。

关于函数指针,我们还可以采用数组的形式去批量指向:

void doAction0 () { Serial.println (0); }

void doAction1 () { Serial.println (1); } 

void doAction2 () { Serial.println (2); }

void doAction3 () { Serial.println (3); }

void doAction4 () { Serial.println (4); }

typedef void (*GeneralFunction) ();// array of function pointersGeneralFunction doActionsArray [ ] = {

doAction0,

doAction1,

doAction2,

doAction3,

doAction4,

};

void setup ()  {  

    Serial.begin (115200);  

    Serial.println ();  

    int action = 3;  // 3 is an example 

    doActionsArray [action] (); //依旧不要忘记它作为一个函数指针,调取时需要代括号。

}  // end of setup

void loop () { }

如果你的函数指针数组里有很多函数,而不想占用有限的RAM资源,可以将指针数组存储到程序存储空间中。使用PROGMEM和对应的操作。

void doAction0 () { Serial.println (0); }

void doAction1 () { Serial.println (1); } 

void doAction2 () { Serial.println (2); }

typedef void (*GeneralFunction) ();// array of function pointers

const GeneralFunction doActionsArray [] PROGMEM =

    doAction0, 

    doAction1, 

    doAction2, };

    void setup () { 

   Serial.begin (115200); 

   Serial.println (); 

   int action = 2; // 2 is an example

    // get function address from program memory, call the function

    ((GeneralFunction) pgm_read_word (&doActionsArray [action])) ();

}  // end of setup

void loop () { }

最近的版本的Arduino IDE 和C++11语法上支持Lambda(未命名的)函数,不需要定义很多函数的名字,所以上述也可以写得更简化些:

void (*doActionsArray []) () = {

    [] { Serial.println (a); } ,

    [] { Serial.println (b); } ,

    [] { Serial.println (c); } ,

    [] { Serial.println (d); } ,

    [] { Serial.println (e); } ,

};

void setup ()  {

    int action = 2;  // 举例

    doActionsArray [action] ();

 }  // end of setup

void loop () { }

以上是阅读网上的文章进行的梳理。希望有助于理解。

实际应用中,可以拿Arduino中MQTT的示例来理解回调函数:

声明一个用来被回调的函数mqtt_callback:

void mqtt_callback(char *topic, byte *payload, unsigned int length) {

    Serial.print("Message arrived [");

    Serial.print(topic);

    Serial.print("] ");

    payload[length] = '\0';

    Serial.println((char *)payload);

}

在setup中调用这个函数,在收到订阅的消息时调用它:

mqttClient.setCallback(mqtt_callback);

在loop中让其能够不停的轮询:

mqttClient.loop();

以及在蓝牙例程中的接收信息用的回调:

//ble callback receive the data

class MyCallbacks: public BLECharacteristicCallbacks {

     void onWrite(BLECharacteristic *pCharacteristic) {

         std::string rxValue = pCharacteristic->getValue();

         int dataLen = rxValue.length();

         if ( dataLen > 0) {

            //Serial.print("Received Value: ");

            for (int i = 0; i < dataLen; i++){

            Serial.print(rxValue[i]);

            }

        }

    }

}

关于回调函数的理解如上,希望有所帮助。

你可能感兴趣的:(Arduino学习:Callback回调函数的理解)