发帖数

43

原创数

43

关注者

16

阅读数

9329

点赞数

2

张角

  • ESP8266物联网开发板设计1

        大家好,我是张飞实战电子张角老师!

    通过这个电源,我们就可以得到12V的电压了,然后我们可以通过线性电源,比如7805把这个电压降压到5V,然后在再通过AMS11175V电压降压到3.3V。这种方案适用于产品开发中,我们甚至可以自己开发一块反激电源放在我们的产品的板子上。因为实际的环境中,可能只有AC 220的电源。对于一些极端的情况,比如AC 220V不能够达到的地方,有的物联网产品甚至只能是太阳能供电,当然这个电源是另外一个方案了。

    一般情况下开发板则不同,只是大家学习的工具,对吧。那么供电部分的方案,自然也是不同的。比如我们就可以从我们的笔记本或者台式机上的USB口上获取电源。这样我们就利用了电脑自己的电源模块,是吧。AC降压的部分是电脑自己完成的,我们这里的方案是站在它的基础上的。USB电源的输出电压是5V,一般情况下,这个5V电源的输出能力可以达到500mA左右,可见它的功率还是蛮大的。

    上面我们只是分析了大体的情况,那么具体到我们这个开发板中,这些大体的方案合适么?我们下面来具体分析一下。

    首先我们来看一下,我们的这个模组的供电要求,从下图可以看出这个模块要稳定工作,供电电流要大于500mA。但从这一个数据来看,我们就没有办法仅仅使用一个USB来供电,那样单片机有可能就不可以正常工作,我们这里说得是有可能呀,也不一定不能工作。但是从产品设计的角度来看,如果只用一个USB接口来供电,这样的设计是不稳妥的。

    我们今天使用ESP8266这款芯片设计一块物联网开发板,使用的模组是ESP-12F,这款模组我们在上篇文章已经给大家介绍过,生产厂家是深圳是安信可科技有限公司。我们这款开发板,只是使用这个模组内部的单片机ESP8266,实现的功能来说也相对较为简单,换句话说,也就是没有外挂单片机。

    那么总体上要实现什么功能呢?

    第一个要实现的功能是温湿度的采集,这个是物联网场景中最为常见的。远程而且在线的数据采集功能,能够为系统决策提供实时的决策依据。我们这里主要是以温湿度的采集以及往云端上传为例,给大家演示一下物联网开发板的流程是什么样子的。

    第二个要实现的功能是远程控制继电器的开关。我们从物联网的数据终端获得了数据之后,自然需要根据数据做出决策。那么这个决策执行,一个最简单的示例就是继电器的开通和关断。我们可以通过继电器的开通和关断,来决定相应的模块是不是需要运行。

    ESP-12F这个模组要想能够正常工作,首先就是要搭建一个单片机的最小系统。这个最小系统包括如下模块,比如电源模块,程序烧录模块,时钟模块,Reset模块,boot选择模块等等。只有这些模块工作正常了,ESP-12F内部的单片机才有可能工作起来。

    我们下面分别来看一下,这些小模块用什么样的方案实现比较合适。

    第一个是程序烧录模块,我们这里使用的是USB转串口,芯片是CH340G,这个是南京沁恒公司出品的,属于非常常用的芯片,性能稳定价格相对便宜。具体的电路设计也相对来说比较简单。大体的电路设计如下:

    image.png

    我们这里只是参考示意图,具体电路实现,我们后面再分析。

    第二个是时钟模块,我们这个ESP-12F里面应该已经包含了时钟震荡电路,我们不用再外接震荡电路模块。从模组管脚功能的描述上看,ESP-12F也没有外部晶振的接口,那么我们可以断定这个模组已经自己搞定了震荡器部分,要么是单片机内部的RC震荡器,要么是模组封装的石英震荡器。后面有机会,我们再对这块进行分析。

    image.png

    image.png

    Reset模块和BOOT模块按照芯片给出的说明来就可以了。RESET这个地方一般是低复位,意思也就是说这个管脚在低电平的时候,芯片内部执行复位操作。BOOT引脚,从上图也能看出,IO0为低的时候,单片机处于下载模式。如果要让单片机处于运行模式,这个引脚必须拉高或者悬空。那么我们进行电路设计的时候,按照芯片的说明操作就可以。

    再一个就是电源模块,很显然ESP-12F这个模组要能够正常工作,它一定是需要电源的。那么这个电源电压是多少呢?我们来看一下ESP-12Fdatasheet

    image.png

    这张表里面,我们是不是可以看出来这个模组的供电电压是不是3.3V呀,Io口的最大电流驱动能力是12mA对吧。VilVih表示输入低和高的电平值,从datasheet上可以看出来输入为低的时候,最低值是-0.3V,最大值是0.25倍的Vio;输入为高的时候,最低值是0.75倍的Vio,最大值是3.6VVol的最大值是0.1倍的Vio,也就是说作为输出且输出为低的时候,它的最大值0.1倍的VioVoh的最小值是0.8Vio,这句话的意思是,IO口作为输出,且输出为高的时候,它的最小值是0.8倍的Vio。了解了这些参数,可以对我们进行外围电路的设计起到指导作用。

    那么这个3.3V的电压从哪里来最合适呢?我们这里有两条思路,一条思路是从AC 220V上通过反激电源降压到12V,然后再从12V降压到3.3V;另外一条思路是利用USB接线的5V电压,直接变换成3.3V。第一种方案,我们首先要买一个12V的电源适配器,如下图所示。

    image.pngimage.png

    通过这个电源,我们就可以得到12V的电压了,然后我们可以通过线性电源,比如7805把这个电压降压到5V,然后在再通过AMS11175V电压降压到3.3V。这种方案适用于产品开发中,我们甚至可以自己开发一块反激电源放在我们的产品的板子上。因为实际的环境中,可能只有AC 220的电源。对于一些极端的情况,比如AC 220V不能够达到的地方,有的物联网产品甚至只能是太阳能供电,当然这个电源是另外一个方案了。

    一般情况下开发板则不同,只是大家学习的工具,对吧。那么供电部分的方案,自然也是不同的。比如我们就可以从我们的笔记本或者台式机上的USB口上获取电源。这样我们就利用了电脑自己的电源模块,是吧。AC降压的部分是电脑自己完成的,我们这里的方案是站在它的基础上的。USB电源的输出电压是5V,一般情况下,这个5V电源的输出能力可以达到500mA左右,可见它的功率还是蛮大的。

    上面我们只是分析了大体的情况,那么具体到我们这个开发板中,这些大体的方案合适么?我们下面来具体分析一下。

    首先我们来看一下,我们的这个模组的供电要求,从下图可以看出这个模块要稳定工作,供电电流要大于500mA。但从这一个数据来看,我们就没有办法仅仅使用一个USB来供电,那样单片机有可能就不可以正常工作,我们这里说得是有可能呀,也不一定不能工作。但是从产品设计的角度来看,如果只用一个USB接口来供电,这样的设计是不稳妥的。

    image.png

    另外一个耗电较大的是继电器,我们这里的继电器选定的是SRA系列的,线圈的开通电压,我们可以选择12V。那么我们来看一下,这个继电器的线圈需要多大的电流。

    image.png

    从上图中可以看出来,这个电流要50mA,是吧。

    我们下面来看一下CH340G的功耗情况。从下表可以看出来,它的功耗最大值是在20mA左右,这个器件待机状态的时候,功耗更低,只有0.2mA左右。

    image.png

    SHT20(温湿度检测芯片)和其他一些LED等的模块加起来,功耗应该不会超过10mA。这样算下来,这个开发板需要的总的供电能力,差不多在600mA左右。如果再留有一点余量的话,那么我们选用的反激电源适配器,它的输出能力差不多要在1A左右。

    从上面情况来看,USB供电是肯定不合适的。我们这个开发板,不适合USB供电,只能是使用适配器从AC 220V上引电下来。

    有了12V电源以后,我们第一步要做的事情,就是把12V电压变成5V的,常用的器件就是LM7805。我们来看一下LM7805的输出能力,从下图中可以看出来,它差不多有1A的输出能力,是吧。这个能力是可以满足模组功耗需求的。但是我们可以计算一下它自身的功率。 P = 12V  5V* 500mA  = 3.5W,也就是说仅仅带载ESP-12F这个板子,就需要3.5W的功率。这个对LM7805的封装来说是不可以承受的。即使加上散热片,也难保散热片不烫手,这个功率太大了。那怎么办呢?我们这里只能选择能效比更好的电源方案,比如开关电源。我们可以选择一个buck降压方案。这个buck电路,可以直接从12V降压到3.3V,也可以从12V先降压到5V,然后再通过LDO模块把电压从5V降低到3.3V。这两个方案,我们可以都用一下,看一看哪个更合适。为什么一般情况下,单片机前端电源一般采用LDO(比如AMS1117等)呢?主要是LDO输出的电压精度比较高。还有一个,如果Buck电源坏掉了,buck上的高压信号,一般也不会直接伤到单片机,中间还有一个LDO电源起到了隔离作用。另外,Buck电路输出的电压文波率相对来说也比较大,一般在2%-5%左右,这个波动对单片机的工作也会有影响。但是呢,现在的单片机一般也是宽电压输入,就像我们这个ESP-12F,它的输入电压可以从3.0V-3.6VBuck的文波率按理说应该是可以满足要求的。我们这里可以把两个方案都试一试,通过跳线帽或者0欧姆电阻的方式选择供电电源。一个是测试一个LDO电源(AMS1117)的发热量怎么样,再一个看看buck电源能不能满足单片机的需求。

    image.png

    那么我们这里还需要一个5V3.3V的电压变换,我们这里也选择最为常用的AMS1117。这个片子上面的功耗也是蛮大的,P = 5V  3.3V* 500mA = 0.85W。我们需要对这个片子的散热做一些特殊的处理,比如在地上需要大面积敷铜或者加散热片。我们这里AMS1117选择SOT223的封装,等板子搞好之后,实测一下温升能有多少。buck芯片我们采样SY8120 DCDC的片子,具体buck电路的设计,我们有机会展开来讲。

    讲完了单片机的最小系统,我们看一看器件封装的一些问题。

    对于各个器件原理图和PCB的封装,站在开发板这个需求程度上,我们可以直接采用立创上给出来的。但是如果实际做产品的时候,元器件pcb的封装,我们就要格外注意了。pcb的封装要和实际生产工艺匹配起来,这样才能最大程度上降低生产成本,进而能够降低产品总的成本。比如封装不能过大,否则可能就会有立碑现象;再比如,如果封装大小不合适,虚焊的可能性就会加大,进而产品的质量可能就会受到影响。做产品的问题,我们暂且不说,我们先来看一下,如何直接利用立创商城提供的封装。第一步,我们要先把这些封装导出来,我们这里以DHT11为例(目前这样使用有效,后续网站可能会改版,大家留意)。

    比如我们这里首先选择直接使用厂家提供的封装,点击立即使用以后,就会出现schdocpcbdoc两个类型的文件。我们后面把可以把它导出来,变成AD软件支持的格式。这里说一下,我这里习惯使用AD软件,可能和一些同学的习惯不太一样。如果大家直接使用立创的画图软件,也就不需要导出来了。这个地方大家酌情使用。

    image.png

    image.png

    image.png

    这些文件导出来之后,我们就可以在AD软件里面生成相应的lib文件了,以便于我们在画原理图和PCB图的时候使用。

    今天关于这个开发板的分享,就先到这里。我们下篇文章会继续探讨相关原理图模块的设计。

     

    ①基于ESP8266STM32物联网开发板设计

    https://blog.csdn.net/weixin_42107954/article/details/97494269

    ②物联网开发板-ESP8266   https://lceda.cn/jixin003/iot_board_esp12

    ESP8266教程-技小新 https://www.yuque.com/lingyao/jing/ob5wia

    ESP-12F 规格书

    https://item.szlcsc.com/84052.html

    https://atta.szlcsc.com/upload/public/pdf/source/20210219/C82891_ABD84A460F4F056A2757267CCCA4B508.pdf

    SRA系列继电器说明书

    https://item.szlcsc.com/61221.html

    https://atta.szlcsc.com/upload/public/pdf/source/20180525/C60169_619C103F264B8308190CF1E1CA37960C.pdf

    CH340C datasheet https://item.szlcsc.com/85852.html

    LM7805 datasheet https://item.szlcsc.com/520584.html

     


    收藏 0 回复 0 浏览 178
  • 物联网通信技术总结与分析

       大家好,我是张飞实战电子张角老师!

       现在是一个万物互联的时代,既然要实现万物互联,各个物联网设备之间的通信就是一个不可回避的问题。我们常用的单片机的接口比如spii2cuart等等,都属于有线连接,而且寻址和传输协议也相对来说比较简单,这也就决定了它们的主要适用范围主要是板级通信,传输速度不会太远。有线连接中,能够实现较远距离传输的,就是网络接口。借助互联网基础设施中的路由器、交换机以及光纤等转接和传输设备,基于复杂的TCP/IP协议栈,人类构建了庞大的互联网体系,并在此体系上衍生出了各种各样的应用,极大地提供了人们获取信息的速度。

    但是,随着越来越多的设备都需要连接在网络上,进一步提高设备的在线能力,只靠有线通信已经越来越不能满足需求。所以无线通信技术蓬勃发展起来,这里面比较常见的通信协议,包括wifi,蓝牙,zigbee2G3G4G5G等等。这些无线通信协议,都有着自己的技术优势和劣势,以及时代性。目前来看,除去运营商覆盖的网络或者说移动通信网络(2G3G4G5G等)外,最为常用的就是wifi和蓝牙通信技术,它们可以被认为是近距离通信(或者说固定无线通信)的解决方案中的佼佼者,比如我们的手机上一般都会配有wifi和蓝牙通信模块。其中wifi通信,因其天然具有连接互联网能力,所以应用范围更为广泛。

    总体上来说,Wifi和蓝牙通信技术各有各的优势,wifi通信传输的数据量比较大,但是功耗高;蓝牙通信技术,数据传输速度相对要慢一些,但是功耗低。

    另外,Wifi覆盖的范围相对蓝牙来说也更为广泛,覆盖半径可以达到300m,在家庭和办公室使用肯定是可以的。随着wifi技术的发展,有些wifi网络甚至可以覆盖整个办公大楼。蓝牙则不同,它的覆盖范围往往只有十几米。在频段上,蓝牙一直工作在2.45Ghz这个频段;但是Wifi目前有两个频段范,一个是2.4G频段,一个是5G频段。从上面的描述可以看出来,wifi要实现这些功能,具备强悍的穿墙能力肯定是必须的了,但是他们的工作频段是却是一样的,那么一个重要的区别就是他们的功率不一样了:Wifi的功率相对来说非常大,所以能够穿墙。如果从功耗方面来讲,设计10m有效传输距离的蓝牙模块,其功耗只有2.5mW左右,指望它实现穿墙功能,也是难为它了。

    但是蓝牙的超低功耗对于其在电池供电的场景中的使用,提供了非常具有竞争力的优势,所以很多智能门锁主要的通信手段是蓝牙通信。因为Wifi通信功耗太高,一般不太适合电池供电的场景中。

    我们今天着力分析物联网应用中的wifi模组。那么我们这里遇到的第一个问题,就是为什么我们这里提到的是模组,而不是模块呢?wifi模组和芯片有什么样的区别呢?单纯从名字上来看,wifi模组包含的东西就要比芯片要多。多出来的东西主要是什么呢?一般来说,主要是RF射频模块,存储芯片以及Wifi协议栈的实现。RF射频模块的开发,对信号质量,信号完整性都要求比较高,当然其开发难度也比较大,需要的设备也比较多。这个对小批量产品或者需要快速出货的产品,是一个极高的成本。另外,对一些技术能力较弱的公司,也是一个门槛。因此,市场上就出现了专门生产模组的公司,他们利用自己的技术实力把wifi芯片+存储芯片+射频模块等封装在一起,形成一个统一的解决方案。然后再通过通过商业营销,通过大规模的出货,去平摊他们的研发成本,并从中赚到一定的利润。这对物联网的开发者以及他们自己来说,都是一个双赢的方案。如果某款产品的出货量确实非常大,得到了市场的验证以后,这个时候它的开发者有能力也有动力去选择自己去开发wifi模块,去进一步降低成本或者作出更具特色的解决方案。实际上,从商业的角度讲,一般情况下使用通用的wifi模组可能成本更低,因为它的出货量更大。再一个,这些通用的WiFi模组,在实际应用中可能已经经历过各种各样实际场景的检验,进而模组的厂家可能会有不断的产品迭代,所以它的稳定性应该更为可靠。当然这些都是建立在选对wifi模组的基础之上的,如果选错了产品,可能就会适得其反。

    那么我们该如何使用wifi模组呢?这里有两种方式,一种是直接使用wifi模组内部的单片机进行控制及数据采集功能的实现,不过模组内部的单片机一般情况下,性能相对来说没有那么强大而且引脚数量也受限,所以这种方案只能用在功能简单的场景中。再一个,是我们自己根据实际功能的需求,选择匹配的单片机;wifi模组呢,只是作为一个透传的工具而已。具体怎样使用,可以根据自己的实际情况来确定。

    不管实现方式有何不同,wifi模组的使用者一般都只能使用AT命令来操作模组里面集成的wifi模块。当开发者参照模组说明,使用AT指令操作wifi模组时,wifi模组就可以按照命令进行正确的工作,比如联网什么的。

    那么我们刚才提到的透传是什么意思呢?一般情况下,我们的主控单片机和wifi模组之间使用的是UART通信,主控单片机通过UART接口发送AT指令给我们的wifiwifi模组收到指令之后,就按照AT指令的内容进行工作了。因此,从表面上看,我们的wifi模组好像不存在一样,就像我们使用uart接口在进行联网操作。这种场景下,wifi模组可以理解为一个透传模块。因为这种使用方式,我们需要使用两个单片机,相对来说成本比较高一些。但是因为我们要实现相对复杂的功能,如果wifi模组内部的单片机实现不了我们想要的东西,这个也是一个没有办法的选择。

    这里我们拿市场上比较火(使用相对来说较为简单,或者说可能出货量比较大)的一块wifi芯片ESP8266来举例,它自己的内核本身就是一款32位的MCU,内核是Tensilica L1o,采样的是cortex-M3的架构,主频高达135Mhz,有一个32位的乘法器,一个定时器,15个中断。对于简单的IOT产品,ESP8266完全可以兼任MCU主控的工作,这样可以进一步降低成本。

    image.png

    基于这款芯片的模组,淘宝上卖得比较火的比如深圳安信可的ESP-12F等,就是在这款芯片的基础上额外封装了外置存储芯片和RF模块等等。对于较为简单的IOT应用,我们当然是可以直接使用它内部的单片机来实现相应的功能。

    image.png

                       image.png

    从图上可以看出这款模组使用的是板载天线,也就是上图中板子边缘上的金色的线条部分。经过相关资料查询,它的WiFi传输距离可以达到80米,SPI Flash的存储空间是32Mbits,总共有9IO口。更为详细的参数表格,大家可以参考下图。image.png

    image.png

    收藏 0 回复 0 浏览 93
  • STM32 C语言预处理深入解析

    大家好,我是张飞实战电子张角老师! 我们今天对C语言的预处理指令做一个总结其实C语言的预处理指令主要有三大类文件包含宏定义和条件编译我们首先要明白什么是预处理第二个C语言编译之前为什么需要预处理或者说这些预处理的添加能够给C语言的编程带来什么好处?再一个,我们就展开讲解一下C语言常用的三种预处理指令,看看他们如何使用以及有各自有什么优缺点。

    所谓预编译或者说预处理,就是在编译之前进行的一些操作,这其中比较主要的可能就是展开操作。那么C语言为什么要添加这些预编译指令呢?从设计者的角度上来看,这些指令肯定会对C语言的编程带来效率的提升,C语言才会保留这些关键字。因为整体上各个预处理指令之间的关联性比较小,我们这里没有办法给他们的优点和缺点做一个统一的定论,这里只能是展开来讲一个一个仔细讲解。

    第一个我们我来看一下,头文件指令#include”。那么在回答这个文件之前,我们要先讨论一下头文件存在的必要性,或者说为什么C语言要分为头文件和源文件。为什么C语言不能像C#Java那样,不需要头文件么?

    第一个我们从C语言发展的历史来看或者说从共享代码对存储空间的使用量上来看,C语言头文件的必要性。之所以C语言一定需要头文件,一个最本质的原因,是因为C编译器编译出来的二进制码(.o, .obj, .lib, .dll)不包括自我描述的符号信息,那么要复用这种可执行代码的话,需要另外的文件。C#Java的可执行代码可以自带元数据信息,但是这也意味着运行时的内存需求的增加,毕竟这种自我描述的数据对最终用户来说是无用的。当然在C#Java被发明的时候,存储空间以及很便宜了。但是C被发明的时候,64K的内存是400多美金。一个程序经常几十个模块,运行的程序为某给模块去浪费几十K是不可能的,价格太高了。那么头文件也就应运而生了,可执行代码的元数据信息是在头文件中保存的,头文件可以充当这些模块复用的桥梁,那么进而就可以降低可执行模块复用时对存储空间的需求。

    第二,我们从编程器的角度来看一下。C时代的时候编译器比较简单,是固定的编译和链接两个过程,编译一次只处理一个文件,进行预处理之后,头文件会插入到这一个文件里,不同源代码文件的处理时独立的,这样如果头文件里面定义了一个函数的实现,编译的时候所有引用这个头文件的源码文件,生成的obj里都会有这个符号。而链接是通用的链接程序,从汇编时代就用的工具,没有什么高级功能,同一个符号链接时出现两次是会报错的。但是,我们又说了,每个文件的编译是独立的,所以如果实现不在当前源文件里面,调用的时候编译器就不知道这个函数的类型和签名,没法生成调用代码,所以必须在调用之前先声明一遍。如果不把声明写在头文件里面,就必须在每个用到这个函数的源文件里都声明一遍,很不方便,所以综合之后的解决方案就是实现写源码文件里面,声明写头文件里面。

    最后,我们从知识产权保护的角度上来分析一下。接口和实现分立之后,使用者只需要关注接口就行了,按照接口的说明使用就好了,不用关注具体是如何实现的。那么作为开发者呢,则可以对实现部分进行加密,那样是不是就可以保护一定的商业秘密了。具体到头文件来说,我们在头文件中进行的函数声明,其实就有这个作用。

    也就是说基于以上各种原因,头文件对于C语言来说,是一个确定的存在。那么就肯定要有一个关键字来处理头文件,那这个关键字就是“#include”。在程序正式编译之前,预处理程序会首先展开头文件中的函数以及变量声明。然后程序在编译的时候,对于没有在本文件中实现的变量或者函数,就会做好标记。然后在链接阶段,就会根据类型、名称信息,去其他可执行文件中去找在该文件中没有实现的同时声明过的变量或者函数,如果找到了,而且不重复,那么程序的编译就能够通过。如果没找到,或者找到两个,那么编译就不会成功。

    具体到#include的用法,这里还有区别。#include <xx.h>,这种尖括号写法,预编译程序只会到系统指定的目录中去找这些头文件,看看他们是否存在。#include xx.h这种写法则不是,这种双引号类型的写法,预编译程序会首先到工程目录去找相应的头文件,如果找不到,预处理程序会再到系统指定的目录里面去找。那也就是说,如果是自己写的头文件,我们一定需要使用#include xx.h的写法;如果是系统头文件,两种写法都行,但是#include <xx.h>这种写法,效率应该会更高一些。

    讲完了文件包含,我们看一下宏定义的预处理指令#define。这个指令大家应该都比较常见。使用宏定义,可以带来不少优点,比如可以避免意义相对模糊的数字出现,#define PI 3.1415926,那么我们就可以在程序中直接使用PI来代替那么一长串的数字。再一个,这个PI会在预编译的时候,直接进行值替代,并不需要为这些常量提供存储空间,那么就进一步提高了程序的执行效率。但是这个也会有隐患的,编译器这里只是进行展开,并不进行类型检查,与此对比的就是const的类型的变量,使用这个变量就有可能避免潜在的错误。再一个,像下面这个,#define FILE_PATH E:folder11.txt如果目录较长的话使用起来是不是非常不方便呀我们就可以直接使用FILE_PATH这个字符串去去代替较长的目录,这样是不是就方便了许多。

    虽然#define,给我们带来了许多便利,但是使用它的时候,一定要小心再小心,否则编程的时候,就比较容易出错。

    #define PCHAR char*

    PCHAR p1,p2;

    char** a = &p1;

    char** b = &p2

    像这段代码,其实就有问题。这里p2其实是char类型,并不是char*类型。对于这种类型的错误,可以使用typedef的方式来解决。

    我们再举一个例子,#define MUL(A,B) A*B而在使用的时候,这样的调用:int a=1,b=2,c=3,d=0;d=MUL(a+b,c)经过编译时候展开,就变成了d=a+b*c而不是我们所希望的d=(a+b)*c其解决办法也很简单,就是给每个分量,都加上括号,就可以避免此类问题,在宏定义的时候,如此定义:#define MUL(A,B) ((A)*(B))不过有些时候加了括号也没有办法避免这些问题

    但是使用#define来定义函数,确实可以减少系统开销,提高运行效率。为什么会这样呢?因为在C语言中,发生函数调用的时候,需要保留调用函数的现场,子函数执行完毕以后还有回复函数调用的现场,这都需要一定的时间。如果子函数执行的任务比较多,这点时间是可以忽略的,但是如果子函数的功能比较少,比如只是一个加法的操作,那这部分转换操作的开销就太大了。使用带参数的宏定义就不会出现这样的问题,因为他是在预处理阶段就进行了宏展开,在执行的时候不需要进行转换,即在当地执行。宏定义可以完成简单的操作,但是复杂的操作还是要借助函数调用来实现。另外,宏定义的代码如果比较长,预编译的时候,所有引用的部分的代码都需要展开,那么目标代码的空间就会相对较大。所以,我们还是要根据实际情况来决定是否使用宏定义。

    实际上,针对宏定义函数的一些缺点,C++引入了inline函数这个关键字。inline函数,可以像宏定义的函数一样,直接展开,而不会发生函数调用的开销。但是呢,inline函数,本质上就是函数,会有参数以及返回执行类型的检查,使用的时候,相对要安全很多。

    讲完了宏定义,我们就到了条件编译的部分。所谓条件编译,就是满足条件的时候,就编译,不满足条件的时候,就不编译。条件编译一个很重要的作用,就是可以提高程序的可移植性,我们可以根据不同的平台选择编译不同的程序。条件编译一般有三种格式,这里总结如下。

     

    第一种:

    #ifdef 标示符 (注意:标示符一般用#define命令定义)
    程序段1
    #else
    程序段2
    #endif

        第二种:

    #ifndef 标示符 (注意:标示符一般用#define命令定义)
    程序段1
    #else
    程序段2
    #endif

        第三种:

    #if 表达式 (注意:标示符一般用#define命令定义,如果表达式为真则编译程序1
    程序段1
    #else
    程序段2
    #endif

    大家在实际使用的时候,参考这三种类型进行使用,就可以了。

    我们今天对预编译指令的总结,就先到这里。下一篇文章,我们就谈谈其他的预编译指令。


    参考资料:

    1. 为什么C/C++要分为头文件和源文件 https://www.zhihu.com/question/280665935

    2. C语言预编译的缺点 https://blog.csdn.net/weixin_39632379/article/details/117079935

    3. C语言预处理详解 https://blog.csdn.net/czc1997/article/details/81079498

    4. #define宏定义的优点和缺点 https://blog.csdn.net/u013910522/article/details/22672057

    5. C语言提供的三种预处理命令 https://blog.csdn.net/xiongzebao/article/details/45586743

     


    收藏 1 回复 0 浏览 323
  • STM32 C语言“函数”深入剖析

    大家好,我是张飞实战电子张角老师!我们今天对C语言函数的概念进行相关的探讨。探讨的思路还是基本按照前面几篇文章的思路来进行,也就是说需要依次回答:是什么,为什么和怎么用的问题。具体到单片机C语言的函数,我们首先要明确函数是一个什么东东?C语言为什么要使用函数?这个问题主要是相对于汇编语言来说的,大家知道汇编语言就没有函数。函数是如何定义和声明的?后面的,就是函数在实际使用过程中相关的问题,比如函数和变量的定义以及声明有什么不同?函数与函数之间如何进行交互,比如参数传递以及函数如何返回计算结果?最后一部分,函数设计可能是C语言程序设计中的关键一环,我这里会总结一些函数设计相关的一些技巧,分享给大家,进一步减少程序设计过程中出错的可能性,提高编程的效率。

    我们首先来看,函数是什么呢?首先,函数有一个名字,这个名字大多数情况下,描述了它的基本功能。然后有的函数有参数,有的函数没有参数;有的函数有返回值,有的函数没有返回值。函数的参数,可以理解为函数进行运算时的加工原料;函数的返回值可以理解为函数计算结果。括号中的东西(函数体),可以理解为函数对数据的加工过程,或者理解为计算过程。

    image.png

       从上面的描述来看,函数应该是一个功能的封装。一般我们的C语言程序,是由许许多多封装起来的函数组成的。但是大家如果写过汇编语言的程序,可能就会有感觉,其实汇编语言并没有函数的概念,汇编语言程序就是一系列指令的罗列。那我们不禁要问,为什么C语言要引入函数的概念呢?函数概念的引入是需要解决什么问题?

    image.png

       大家看一下,函数的引入是不是相当于把一个复杂程序的功能实现了拆解呀,具体拆解的颗粒度因人而异,因程序而已。这个拆解的思路,是不是完美体现了分而治之的策略。在编写程序的过程中,有些常用的功能很有可能屡次被调用,我们把这些常用的功能封装成一个一个函数。程序写到这里的时候,只需要调用这个函数就可以了,不用再重新把代码写一遍,这样是不是就可以较大程度的较少代码量呀。

    实际上,我们调用的各种库函数,可以说是在编程中一些最常用的功能的集合。这些写好的、已经经过验证的、较为基本的函数模块,可以极大提升程序的开发效率。这个就相当于,建房子时,有好多基本的结构可以拿来直接使用,比如直接拿来一个柱子、一间房子、一个卫生间等等,不需要自己再使用最基本的砖块从头开始搭建。这样建房子(编程序)的效率是不是会猛增呀。

    再一个,没有这个函数的封装,如果程序较大、功能较复杂,我们面对的是不是一个超长的代码呀,那读起来是不是就比较费劲。这个就有点类似古代的文言文不分段落、没有标点符号一样,读起来是不是就比较吃力。那么可以说函数这个概念的引入,可以把一些较长的程序切分成了很多个小模块,那是不是极大地增强了程序的可读性。程序的可读性强,那是不是就从侧面降低了程序的开发和维护成本呀,不用消耗那么多的智力资源就能完成任务。

    前面,我们从函数功能的重用度或者说代码量的大小,以及函数对程序的可读性增强两个方面阐述了函数存在的必要性。那么C语言毕竟是人类发明的,肯定要从方便人去进行程序开发的角度来进行语言架构的设计,那么自然C语言就会引入函数的概念。从本质上讲,函数的引入就是为了降低程序开发的难度(这个是相对汇编语言而言的),提升程序开发的效率,方便人用更少的时间去做更多的事情。

    再一个,我们看一下,函数与函数之间的组装配合,实现了复杂的程序功能,这个过程是不是像极了人类社会中各个组织之间的相互配合。那么我们在进行程序开发的时候,函数的功能拆分怎么样才是相对比较合理呢?我觉得有两个指标,一个是函数之间调用的深度不要太深,太深的调用会影响程序的可读性。对于一个程序来说,不要分太多层,最好尽可能扁平化;第二个,函数与函数之间,功能一定要独立起来或者说功能解耦一定要尽可能彻底,同一层的函数与函数之间的功能没有相互依赖的关系。从图形上来说,这个可能就是一个树形的结构,第一个数不要太高,太高了人爬上去比较困难;第二个分支与分支之间不能相互影响,必须是各自隔离开,这样维护起来才更方便。从这个角度上,看对一个复杂的程序功能进行有效的拆解的过程,有点像行军打仗时,对军队的管理:不同的部分有不同的功能,各个部分之间有联系但不能是依赖(要能够独立行动),每个部分不能太大否则不好管理(尾大不掉),层级不要太多否则容易政令不通。

    讲完了函数是什么,以及C语言为什么要使用函数,那么就到了下面一个部分,如何声明及定义函数。函数的定义和声明,有点像全局变量,或者说函数都有全局属性,它的作用域是全局的。既然是全局的,那肯定要有声明和定义,一般情况下声明是放在xx.h”头文件里面,定义则是放在“xx.c”文件里面。当然,如何一个函数只是在局部使用,并不对外提供服务,那么可以在这个函数名字面前加上关键字“static”。另外,在这个局部特定的文件里面,这个static函数放在了调用它的函数前面,那么这个函数同样也不需要在进行声明了。编译器会自动找到这个函数,并在链接的时候,自动链接。

    既然默认的情况下,函数和全局变量都有全局属性,那么它们在声明和定义上有什么区别呢?主要的区别就在这个关键字extern”上,全局变量声明的时候,一定要加上extern关键字;但是函数声明的时候,则不需要extern关键字(或者说这个可以省略)。至于为什么有这样的区别,可能就是因为函数这种“数据类型”过于复杂,它的定义和声明之间差别极大,定义有函数体存在,对吧,声明却没有。但是对于变量来说,声明和定义是不是没有什么区别呀,比如定义“int a”,声明也用“int a”,两个是不是重复的呀,所以对于变量来说,声明的时候一定要加上“extern”。这样,其实我们对extern这个关键字,也做了一定的总结。

    函数的名字,本身就是一个地址,但从这一点上来看,它有点像数组,和结构体等什么的不太一样。那为什么会这样呢?函数的名字为什么要是一个地址呢?这个可能就和单片机执行程序的思路一样,从某一个地址开始开始执行,那么自然函数的名字就是一个地址更加有利于编译系统对程序进行编译。那么既然函数的名字是一个地址,我们的变量类型里面又有一个指针类型变量,那么他们两个肯定会发生关联,那也就是说一个指针变量里面存储了函数的地址值。这个指针,我们称之为函数指针,全称应该是函数类型的指针,用以区别于整数类型的指针、浮点数类型的指针等等。函数类型的指针,它的声明自然是与众不同的,声明的时候就得按照函数的形式来搞,这个是遵循int* p之类的关于指针声明的法则的。与之相关的一个,比较容易引起混淆的概念,是指针函数。其实如果想区分开来,也是蛮简单的,就是一个返回指针数据类型的函数。这里只是函数指针和指针函数放在一起容易混淆而已。

    讲完了函数的声明和定义,我们来看一下第三个问题,函数与函数之间怎么怎么进行交互的。这个交互包括两部分,一个调用方给被调用方数据传递,另一个是被调用方返回值给调用方。调用方给被调用方传递数据,其实大家理解起来也比较简单,就是通过函数的参数进行的。函数的参数需要什么类型的数据,调用方要按照约定传过去。被调用方给调用方传递数据,方式就多了。第一种方式,可以通过返回值的方式,把计算结果返回给调用方;第二种方式,则比较隐晦,不是通过计算结果来实现计算结果返回。具体实现的方式,调用方给被调用方传递参数的时候,传递的是地址;被调用方,通过修改这个地址指向的值来进行计算结果的返回。这个地方其实就是值传递和地址传递的区别,值传递的时候,没有办法实现计算结果这样返回。

    最后这一部分,我们来讲一讲函数设计的一般技巧,这样可以让我们在程序设计的时候,更少犯错,进一步提高程序编写的效率。

    第一个,原则上尽量少使用全局变量。每个源文件负责本身文件的全局变量,同时提供一组对外函数,方便其他函数使用该对函数来访问这个变量,比如SetValue”、“GetValue”等等,不要直接读写全局变量。尤其是在多线程编程的时候,必须要使用这种方式,并且要对读/写操作加锁。

    第二,和这个类似,函数也尽量少使用static类型的变量,这种变量有记忆功能。有记忆功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”,这种函数既不利于理解也不利于维护。

    第三,我们要避免函数有太多参数,尽量把函数参数控制在4个或者4个以内。过多的函数参数,可能会导致函数的使用成本太高,比如容易把参数的顺序搞错。

    第四,函数体的规模尽可能小,比如控制在80行以内或者说一页屏幕要能够看完。这个规则也可以说我们进行功能拆解的时候,要遵守的。当然,特殊的函数除外呀。

    第五,我们要在函数体的入口如,对参数的有效性进行检查,尤其是指针参数。

    第六,函数的返回值,一定不能是临时变量的地址。这个地址是在栈空间里面的,函数执行结束后,这个地址就是无效的了。那么自然这个地址,不能用作函数的返回值。

    第七,如果是地址传递,则尽量在指针前面使用const”关键词修饰,防止指针被函数内存的计算误修改。当然特殊需求除外。

    关于C语言函数相关的知识,我就和大家先探讨到这里。


    参考资料:

    C语言中为什么要引入函数的概念http://blog.sina.com.cn/s/blog_6fd2803b0100y9fl.html

    C语言函数 https://www.cnblogs.com/wucongzhou/p/12498949.html

    函数指针与指针函数 https://www.cnblogs.com/nevel/p/6370264.html

    定义与声明、头文件和extern总结 https://www.cnblogs.com/liushui-sky/p/7693537.html

    C语言深度解剖,陈正冲,北京航空航天大学出版社


    收藏 1 回复 0 浏览 235
  • STM32 C语言变量的定义和初始化

    我们今天探讨C语言变量的定义和初始化。那么我们首先要明确三个问题。第一,我们要明白什么是变量,或者为什么C语言一定要有变量;第二个在C语言中如何去表达这些变量,或者说C语言都有什么类型的变量如何定义这些变量;第三,变量为什么要初始化,以及如何初始化。

    第一个问题,关于变量,一个最通俗的理解就是变化的量。本来外在的物质世界就是在不断变化的,不是有句话么:“唯一不变的就是变化”。C语言作为描述客观世界变化的一种语言,首先就是要有能够对外界事物变化状态量化的工具,那么这个工具就是变量。数字世界,首先就是量化,这个是一切后续工作的基础。

    我们下面拿几个常用的变量类型进行说明,比如char型变量,它主要是用来应对0  255之间变化的事物的,比如字符什么的。比如float,浮点型的变量,它主要是量化客观世界中模拟量的事物,比如汽车的速度、太阳光的强度等等;再比如int型的变量,它的描述范围就比char型大得多了,它主要是应对整数变化的客观事物的,比如学生的个数、苹果的个数等等。

    那么实际上,我们说C语言的变量远不止这些简单的数据类型,是吧。我们还有数组,结构体,还有指针、栈、链表等等。每种数据类型的出现都是为了解决一个量化的问题,比如指针,它主要是定位量化计算机中内存寻址问题;比如结构体,它的定位主要是用来描述复杂事物的,就比如汽车,它不仅有行驶的速度,还有轮子的个数,椅子的个数等等;再比如栈,它的主要作用是解决任务切换以及函数调用时,程序现场的保护问题。

    那么也就是说每一个变量类型或者量化工具类型的出现,都是有原因的,都是为了解决实际问题的。当我们从这个视角去看这些变量类型和必要性的时候,我们的理解就会深刻很多。举个例子,比如面向对象数据类型的产生,就是把方法或者函数集成到了一个类型中,这样就可以更为准确的去描述客观事物,比如一个狗狗,它不仅有一条大尾巴,还可以跑得非常快,大尾巴是数据,会跑且跑得快是方法。面向对象的语言比如C++或者Java,就把这些变量和方法封装起来,形成一个新的更为综合的量化工具,那就是对象。

    image.png

    image.png

    站在C语言的基础上,往上看是C++语言等面向对象的,但是如果往下看,比如到了汇编级别,就是另外一番场景。我们知道汇编语言是最接近机器的语言,对于某一类型的单片机,它一般有几十条特定中汇编指令。但是我们说汇编语言是没有数据类型的,它操作的只有二进制类型的数据,并没有对这些数据进行按照其属性进行分类。没有根据属性对数据进行分类,其实也就是说没有对量化工具进行分类,那么人类的大脑就要耗费更多的能量去理解汇编程序,人的大脑本身都是很懒的,能省能量肯定是想办法节省能量。从这个角度上的看,汇编语言更像是机器语言。

    image.png

    但是我们说电脑本身就是机器,不同汇编语言的指令集才能真正反映各个芯片架构的不同,指令集不同可能对应的电路也是不同的。任何高级语言最后还是要在特定的机器上运行的,那也就是说这些高级语言最后还是要翻译成特定的汇编语言。这个翻译的工作就是编译器要做的事情。另外,软件的开发还要有量好的代码编辑环境以及调试环境(比如支持单步调试,实时查看寄存器及存储单元的数据),所以一款新的单片机是不是好用会有多方面的因素影响的,也不能只看指令集的执行效率。

    第二个问题,C语言都有什么类型的变量呢?我们可以用一张表来大概描述一下,下面这张表对C语言的数据类型进行了相对完整的总结。大家可以看一下,整体的数据类型被划分为四类:基本类型,构造类型(组合类型),指针类型还有空类型。基本的数据类型肯定是根本,C语言在级别数据类型的基础上构造出更为复杂的数据类型,用于描述相对复杂的事物,比如结构体等等。那么C语言就是使用这些相对抽象的基本类型,去量化和描述纷繁复杂的外部世界的。我们在前面已经提到了绝大多数数据类型产生的原因,这里就不再赘述了。如果有还不理解的同学,可以自己上网去查一查资料。

    image.png

    从下面这幅图可以看出,不同的基础数据类型器长度是不一样的,而且同一种数据类型在不同的机器和编译器编译下其数据长度也是不一样的。不同的基础数据类型,所占据是二进制数据位数发生不同,这个是可以理解的。对于相对简单的事物,比如字符,本来就不需要使用那么长的位数来表达,这个是基本诉求。再一个,比如对于char类型这种需要较少位数就可以表达的可以量化的事物,如果使用int这种长度的数据去表达,本身也是对计算机存储资源的浪费。基于上述两个原因,才出现了不同的数据类型有不同的长度的现象。我们在实际编程的时候,从设计的角度上来看,肯定是选择使用最少的存储位数来量化自己要描述的事物,这样占用的资源才能是最少。当程序代码行数非常多的时候,这种差异就会相对非常明显了。

    image.png

        下面,我们来探讨第三个问题,变量为什么要初始化以及如何初始化。我们首先解释一下为什么单片机数据最好要初始化。众所周知,变量是存储在RAM中,掉电后即丢失,上电后默认全为0。那么这样的话没赋初值的变量值全为0,这也应该是大家认为理所当然的。但是实际上并不是这样,有些类型的单片机,当单片机复位的时候(包括硬件复位即按下复位按钮,看门狗复位,以及其它软件程序复位),单片机只是重新跳回到main函数开始执行,而并没有清空RAM!所以,那些只是定义而没有赋初值的变量(尤其是全局变量)依然会使用复位前留下来的值!那么这样程序运行可能就会出现异常的结果尤其是指针变量。数据有一个初始的值,整个程序也就有了一个初始状态,初始状态确定了,如果程序设计得没有问题,那么就可以按照既定的规则跑下去。如果程序错误发生在初始位置上,那就太可惜了。大家在编程的时候,一定要注意这个现象。

    image.png

    那么下面我们看一下,如何对变量进行初始化。不同的变量类型,初始化的方式肯定是不一样的。首先对于基础的数据类型,可以直接初始化成想要的值即可。那么对于数组、结构体等类型,初始化的方法就具体问题具体分析,各具特色了。我们下面举例子进行说明。

    一维数组:

    int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

    int a[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

    int a[10] = {0};

    int a[] = {1, 2, 3, 4, 5};

    二维数组:

    int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};

    int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

    int a[3][4] = {{1}, {5}, {9}};

    /*

        1 0 0 0

        5 0 0 0

        9 0 0 0

    */

    int a[3][4] = {{1}, {0, 6}, {0, 0, 11}};

    /*

        1 0 0 0

        0 6 0 0

        0 0 11 0

    */

    int a[3][4] = {{1}, {5, 6}};

    /*

        1 0 0 0

        5 6 0 0

        0 0 0 0

    */

    int a[][4] = {{0, 0, 3}, {}, {0, 10}};

    /*

        0 0 3 0

        0 0 0 0

        0 10 0 0

    */

    字符数组:

    char c[10] = {'I', ' ', 'a', 'm', ' ', 'h', 'a', 'p', 'p', 'y'};

    char c[2][3] = {{'y', 'o', 'u'}, {'a', 'r', 'e'}};

     

    char c[] = {"I am happy"};

    char c[] = "I am happy";    // 可以省略花括号

    char c[] = {'I', ' ', 'a', 'm', ' ', 'h', 'a', 'p', 'p', 'y', ''}; // 与上面等价


    char *p;

    p = "I love China"; // 正确

     

    char c[14];

    c = "I love China"; // 错误

    c[14] = "I love China"; // 错误

     

    结构体:

    struct {

    char name[20];

    int age;

    }student1, student2;

    //匿名结构体

    struct Student {

    char name[20];

    int age;

    }student1, student2;

    //声明结构体

     

     

    struct Student {

    char name[20];

    int age;

    }student1 = {"xiaoming", 20};

    struct Student student1={.age=12}; // C99可以只对age进行初始化,其他变量初始化成零

     

    联合体:

    union Data {

    int i;

    char ch;

    float f;

    }a = {1, 'a', 1.5}; // 错误,不能同时初a.ch = 'A'; // 正确

    对共用体赋值要指明赋值对象,如

     

    a.f = 1.5;  // 正确

    a.i = 40;   // 正确

    a = 1;      // 错误,没有指明赋值对象始化3

    union Data a = {16}; // 正确

    union Data a = {.ch='j'}; // 正确 C99新增

     

    枚举:

    第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1

    声明

    enum Weekday {sun, mon, tue, wed, thu, fri, sat};

    定义

    enum Weekday workday, weedkend;

    赋值

    enum Weekday {sun=7, mon=1, tue, wed, thu, fri, sat};

    // sun=7, mon=1, tue=2, wed=3, thu=4, fri=5, sat=6

     

    把上述三个问题到搞清楚后,我们的这篇文章到此就结束了。

     

    参考资料

    C语言数据类型总结 https://blog.csdn.net/xingjiarong/article/details/46942649

    C++继承 https://www.runoob.com/cplusplus/cpp-inheritance.html

    ③单片机C语言探究--为什么变量最好要赋初值 https://blog.csdn.net/weixin_34342207/article/details/92999746

    C语言-定义与初始化总结 https://blog.csdn.net/syzdev/article/details/103532435


    收藏 0 回复 0 浏览 547
×
张角