在学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<typename T> 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<int>);
for (int i = 0; i < COUNT; i++) {
Serial.println (someNumbers [i]); }
} // end of setup
void loop () { }
qsort的compar参数是一个回调函数(关于qsort的函数介绍请网上搜索),它被设定为myCompareFunction<数据类型>,通过template <typename T>的模板定义,可以用函数去定义数据类型,不需要固定死。于是我们就可以在用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]);
}
}
}
}
关于回调函数的理解如上,希望有所帮助。