大家好,我是无际。
今天给大家讲一下芯片/模块厂家写SDK必须会使用的一种技术:回调函数。
回调函数这个知识点其实并不是很难,难是难在网上很多讲解回调函数的都说的太学术化了化了,一点也不亲民。
很多人即使知道怎么写回调函数也根本就搞不懂它们在实际产品中也有什么用,什么时候用。
所以这节课呢我们会以程序架构的需求为出发点,讲解回调函数是怎么满足它这个需求的。
为了方便大家理解,这篇内容也对应有一篇文章,大家可以找无际单片机编程获取。
一、通过这节课程你能掌握以下知识:
二、程序架构的核心理念和需求
很多人可能会说一个好的程序架构啊,就是代码很紧凑、执行效率也很高。
其实这个说的很片面,不完全对,这只能说明你程序算法写的好,但架构不一定做的好。
即然是架构,那自然是以从”大局”为重,思维不能局限于当下的产品功能,还要考虑到以后功能的增加和裁剪,那么对于单片机开发来说,我认为一个好的程序架构至少要达到以下要求:
硬件层和应用层的程序代码分开,相互之间的控制和通讯使用接口,而且不会共享的全局变量或数组。
这里呢,我就这个要求,别小看这一个要求,因为这个要求里面蕴藏着很多学问的,比如用专业称为可移植性、可扩展性。
那么我们来想象一下我们通常写单片机代码的方式啊,在51的时候基本一个.c文件解决,包括寄存器配置啊,产品功能啊。

这种就是没有架构的程序,然后我们进化到STM32这个单片机以后,程序大了,慢慢也会在工程文件里加几个文件夹目录把硬件层和应用层代码分开了。
于是我们会把一些不同的外设功能,比如Led、按键、串口等外设功能代码分别写在不同的.c文件里,然后统一用函数接口去调用它。

比方说控制一个LED灯亮,直接在led.c文件里写一个驱动led灯状态的函数然后给外部调用就好了。

那我们我们看这种Led的控制函数确实也是满足程序架构的需求的,硬件层和应用层代码分开,应用层用硬件层提供的接口来控制,而且又不会有硬件层和应用层共享的全部变量或数组。像这种是不是很简单?
那么不知道你们有没有碰到另外一种情况,就是应用程序需要采集硬件层的数据,比如串口接收数据,按键采集、ADC值采集。
这种硬件层的数据怎么通知应用层来拿,或者怎么主动给它?
我们以往最简单粗暴的方式是不是就是用一个全局变量,比方说硬件层串口接收到数据来了,那么我们把数据丢到数组里,然后把接收完成全局变量标志位置1。
比方说全局变量名为RcvFlag,然后应用层程序会轮询判断RcvFlag==1?是的话就开始把数组里的数据取出来解析。
很多人就会说了,你看我用这种方法照样能实现功能啊,为什么还要学习别的架构。
这样做当然可以实现功能,但是会存在移植性很差的问题。
比如说你们老板让你把这个串口的硬件层封装起来给客户用,但不能让客户看到你实现的源代码,只提供接口(函数名)给对方用。
那么这时候难道你要告诉客户先判断哪个变量为1,然后再取哪个数组的数据这么LOW的做法吗?
那么如果是懂行的客户一定会怀疑你们公司的技术实力是不是小学生水平。
那怎样做才会既方便又专业呢? 这里我们就需要用到回调函数啦。
三、回调函数的作用
那么在讲回调函数之前呢,对于函数调用呢我一般分为2种类型:
1.输出型
不知道大家有没有用过C语言自带的一些库函数,比如说sizeof()获取数据长度的函数,memcpy()是内存拷贝函数,我们调用这个函数之后呢就能完成相应的功能。
还有我们基于单片机的一些程序函数,比方说控制LED点亮熄灭、继电器吸合断开、LCD驱动等等。
那么这些呢,我一般称为输出型的函数。
输出型函数我们是主导的角色,我们知道什么时候该调用它。
2.输入型
输入型呢,也称为的是响应式的函数。
什么叫响应式的函数呢?
比方说接收串口的数据,我们不知道什么数据什么时候来。
再比方说,我们按键检测的函数,我们不知道什么时候会按下按键,那么这些就要定义成响应式函数来实现,而响应式函数就可以用回调函数来实现。
所以通过这两个种类型的分析啊,我们就可以知道,回调函数基本是用在输入型的处理中。
比方说串口数据接收,那么数据是输入到单片机里面的,单片机是处于从机角色。
按键检测,按键状态是输入到单片机里的。
再比方说ADC值采集,ADC值也是输入到单片机里的。
那么它们输入的时间节点都是未知的,这些就能够用回调函数来处理。
具体怎么处理后面我们会用代码来给大家举例。
回调函数还有一个作用就是为了封装代码。
比如说做芯片或者模组的厂家,我们拿典型的STM32来举例,像外部中断、定时器、串口等中断函数都是属于回调函数,这种函数的目的是把采集到的数据传递给用户,或者说应用层。
所以回调函数的核心作用是:
1.把数据从一个.c文件传递到另一个.c文件,而不用全局变量共享数据这么LOW的方法。
2.对于这种数据传递方式,回调函数更利于代码的封装。
四、掌握回调函数的程序编写
前面说了很多概念性的东西,可能大家也比较难理解,回调函数最终呢是靠函数指针来实现的。
那么我这里通过一些模拟按键的例子来演示下怎么回通过调函数来处理它们。
下面是我们的c-free工程,用这个来模拟方便点:

从模块化编程的思想来看,整个工程分为2个部分,应用层main.c文件,硬件层key.c和key.h文件。
不管再怎么复杂的程序,我们都要先从main函数一步步往下挖,main函数代码如下。
int main(int argc, char *argv[])
{
KeyInit();
KeyScanCBSRegister(KeyScanHandle);
KeyPoll();
return 0;
}
KeyInit();是key.c文件的按键初始化函数
KeyScanCBSRegister(KeyScanHandle);是key.c的函数指针注册函数。
这个函数可能大家会有点蒙,请跟进我们的节奏,下面开始烧脑环节,也是写回调函数的必须步骤,
想理解这个回调函数注册函数,我们要先从硬件层(key.h)头文件的函数指针定义说起,具体看下图。

这里自定义了一个函数指针类型,带两个形参。
然后,我们在key.c这个文件里定义了一个函数指针变量。

重点来了,我们就是通过这个函数指针,指向应用层的函数地址(函数名)。
具体怎么实现指向呢?就是通过函数指针注册函数。

这个函数是在main函数里调用,使用这种注册函数的方式注册灵活性也很高,你想要在哪个.c文件使用按键功能就在哪里调用。

这里要注意,main.c这个文件要定义一个函数来接收硬件层(key.c)过来的数据。
这里定义也不是乱定义的,一定要和那个自定义函数指针类型返回值、形参一致。

然后把这个函数名字直接复制给KeyScanCBSRegister函数的形参就可以了。
这样调用后,我们key.c文件的pKeyScanCBS这个指针其实就是指向的KeyScanHandle函数。
也就是说执行pKeyScanCBS的时候,就是执行KeyScanHandle函数。
那具体检测按键的功能就是KeyPoll函数,这个在main函数里调用。

当检测到键盘有输入以后,最终会调用pKeyScanCBS。
最终执行的是main.c文件的KeyScanHandle函数。
所以,我们来看下输出结果。

如果还是有点模糊,下面我再给大家捋一捋编写和使用回调函数的流程:
Ok,这就是回调函数的使用。
如果还看不懂建议多看两遍。
下面请大家思考一下,这个程序虽然简单,但是不是架构还不错?应用层和硬件层完全独立?
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h