当前位置: 首页 > 科技观察

一篇学习回调函数的文章

时间:2023-03-11 20:41:41 科技观察

函数指针学习回调函数其实就是函数指针的应用。函数指针在上篇文章《??指针与函数??》有详细讲解。这里我就不展开详细的解释了。上一篇文章中的功能我会重新贴出来。指针示例代码:#includevoidMyFun1(intx);voidMyFun2(intx);voidMyFun3(intx);typedefvoid(*FunType)(int);/*②。定义一个函数指针类型FunType,与①函数类型一致*/voidCallMyFun(FunTypefp,intx);intmain(intargc,char*argv[]){CallMyFun(MyFun1,10);/*⑤.CallMyFun函数分别调用三个不同的函数*/CallMyFun(MyFun2,20);CallMyFun(MyFun3,30);}voidCallMyFun(FunTypefp,intx)/*③.参数fp的类型是FunType。*/{fp(x);/*④。执行通过fp的指针传入的函数。注意fp指向的函数有一个参数。*/voidMyFun1(intx)/*①.这是一个只有一个参数的函数,下面两个函数也是一样的。*/{printf("MyFun1:%d\n",x);}voidMyFun2(intx){printf("MyFun2:%d\n",x);}voidMyFun3(intx){printf("MyFun3:%d\n",x);}运行结果如下:为什么需要回调函数?先说软件分层问题。软件分层的一般原则是:上层可以直接调用下层的功能,但下层不能直接调用上层的功能。这句话说起来简单,在现实中往往下层要依次调用上层函数。比如你复制一个文件,在接口层调用一个复制文件的函数。接口层是上层,复制文件功能是下层,上层调用下层,这是水到渠成的事。但是如果要在复制文件的同时更新进度条,问题就来了。一方面,只有复制文件功能知道复制的进度,但无法更新界面的进度条。另一方面,界面知道如何更新进度条,但不知道复制的进度。该怎么办?通常的做法是在接口上给拷贝文件函数设置一个回调函数,拷贝文件函数会在合适的时候调用这个回调函数通知接口更新状态。上面主要提到了大规模软件分层的概念,作为嵌入式开发程序员,尤其是在单片机开发中,由于与硬件结合紧密,需要快速响应,所以大部分软件结构都是面向流程开发,回调函数频率不变。不高。但是在软件中使用回调函数可以使软件更加模块化。上图展示了回调函数的作用。上面提到的软件分层。在嵌入式代码中,我们一般将与硬件交互的代码称为硬件层,将业务逻辑代码称为应用层代码。对于优秀的嵌入式代码,一般要求硬件层和应用层代码分离。一般的回调函数代码结构如下:typedefvoid(*ReceiveFarmDataFun)();staticCallbackReceive_tHandlerCompleted;/*用于注册回调函数的函数函数*/voidCallbackRegister(CallbackFunc_tcallback_func){HandlerCompleted=callback_func;}串口串口在嵌入式应用中,串口通信是一种非常经典和常用的外围设备。举个简单的例子,接收到的串口数据帧头是@,帧尾是*。中间数据不能出现@和*。一般来说,代码会这样写。/*串口中断函数*/uint8_treceive_flg=0;uint8_treceive_data[100];uint8_tUSART1_data=0;uint8_tUSART1_data_len=0;uint8_tUSART1_receive_sta=0;voidUSART1_IRQHandler(void){uint8_tdataFL_tmp;){data_tmp=USART_ReceiveData(USART1);if((data_tmp=='*')&&(USART1_receive_sta==1)){receive_flg=1;USART1_receive_sta=0;接收数据[USART1_data_len++]=data_tmp;}if(receive_flg==0){if(data_tmp=='@'){USART1_receive_sta=1;USART1_data_len=0;}if(USART1_receive_sta)receive_data[USART1_data_len++]=data_tmp;如果(USART1_data_len>(100-1)){receive_flg=0;USART1_receive_sta=0;}}USART_ClearFlag(USART1,USART_FLAG_RXNE);}}/*应用层代码,简化->在main函数*/voidmain(){/*省略其他代码*/while(1){if(receive_flg==1)//通过检查receive_data判断函数是否收到{/*通过receive_data数组处理数据*/receive_flg=0;}}}这样实现功能是没有问题的。在我接触的很多项目中确实是类似的结构,但是还有一个移植性不好的情况。也就是说,如果你接到一个请求,要求封装硬件层供客户使用,不让客户看到源代码,并以“保护通信协议”为目的,打包成一个库,那么你需要告诉客户那你需要判断receive_flg变量,然后读取receive_data数组的内容???我不得不说你可以这样做,但大多数公司不会这样做。这时候可以使用一个回调函数来解决这个问题。/*打开给客户的头文件*//*包括--------------------------------------------------------------*/#includetypedefvoid(*ReceiveFarmDataFun)(uint8_t*buff,uint32_tbufferlen);externvoidCallbackRegister(CallbackFunc_tcallback_func);/*封装的函数*/staticCallbackReceive_tHandlerCompleted;voidCallbackRegister(CallbackFunc_tcallback_func){HandlerCompleted=callback_func;}uint8_treceive_flg=0;uint8_treceive_data[100];uint8_data=tUSART1uint8_tUSART1_data_len=0;uint8_tUSART1_receive_sta=0;voidUSART1_IRQHandler(void){uint8_tdata_tmp;如果(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)){data_tmp=USART_ReceiveData(USART1);if((data_tmp=='*')&&(USART1_receive_sta==1)){receive_flg=1;USART1_receive_sta=0;处理程序完成(接收数据,USART1_data_len);}if(receive_flg==0){if(data_tmp=='@'){USART1_receive_sta=1;USART1_data_len=0;}if(USART1_receive_sta)receive_data[USART1_data_len++]=data_tmp;如果(USART1_data_len>(100-1)){receive_flg=0;USART1_receive_sta=0;}}USART_ClearFlag(USART1,USART_FLAG_RXNE);:typedefvoid(*ReceiveFarmDataFun)(uint8_t*buff,uint32_tbufferlen);externvoidCallbackRegister(CallbackFunc_tcallback_func);客户可以编写如下代码:voiduartdatadeal(uint8_t*buff,uint32_tbufferlen){/*buff指针存放串口数据,bufferlen存放数据长度*//*客户应用层代码*/}voidmain(){/*省略其他代码*/CallbackRegister(uartdatadeal);while(1){}}这样就可以解决上面的问题了,客户只要注册串口接收函数,当接收到有效数据的时候,就可以跳转到用户的代码,可以封装自己的硬件层看,有些嵌入式的大佬可能会意识到这里有些问题,这样写代码,数据处理的功能就相当于在中断中,这是不合理的。是的,有这个问题,所以给客户的库文件一定要说明这一点,让客户自己选择,如果客户不想在中断中执行,可以按照我们原来的逻辑来写,如下:voiduartdatadeal(uint8_t*buff,uint32_tbufferlen){/*buff指针存放串口数据,bufferlen存放数据长度*/receive_flg=1;}voidmain(){/*省略其他代码*/CallbackRegister(uartdatadeal);while(1){if(receive_flg==1){/*处理数据*/receive_flg=0;}}}事实上,芯片/模块厂商在写SDK的时候也经常这样做,一些大型的开源库也采用这种方式,比如典型的lwip库。后记看到这里的同学可能会觉得这完全是在“脱裤子放屁”,属于“炫技”,没用。诚然,在很多应用中,尤其是一些单片机项目中,代码量并不大。使用receive_flg这样的全局变量来控制,代码结构也很清晰。并且项目不需要打包库给客户,一个单片机软件开发工程师就可以看懂整个项目的代码,不需要这种“秀操作”。关于回调函数,我的态度是:回调函数可以让我们的代码更高效,更容易维护,降低耦合。明智地使用它们很重要,否则过度使用回调(函数指针)会使代码难以排除故障和调试。