发帖数

43

原创数

43

关注者

16

阅读数

9333

点赞数

2

张角

  • RISC-V鸿蒙开发板单片机简介3(项目连载10)

    我们今天继续RISC-V I2C接口的介绍。上篇文章,我们大体比较了新思科技的I2C接口与STM32F030XXI2C接口之间宏观上的区别。整体上,我们可以感觉新思科技的I2C接口使用起来相对来说比较麻烦。好在新思科技给出了最初版本的驱动程序设计,我们可以在他们的基础之上进行适当的改动,就可以把I2C通信的功能建立起来,同时在使用中可以加深对这些接口模块的理解。

    image.png

    上图的代码是程序中对I2C接口的初始化部分,和STM32的寄存器配置基本是一样的。主要是对寄存器的配置,通过在寄存器那里设定给定的值以后,I2C模块就可以按照指定的方式进行工作。

    我们先从架构上来分析一下,首先代码中的reg_i2c这个变量,是I2C相关寄存器的基地址。红色字体寄存器的名字其实表示的是相关寄存器的偏移地址,这里也是采用“基地址+偏移地址”的方式进行寻址的。

    上图中红框部分的描述,应该比较容易理解,相应的位配置好以后,写到DW_IC_CON(控制寄存器)里面去,就可以实现相应的功能了,比如选择FAST_SPEED模式、MASTER模式以及进行RESTART等等。

    大家可以看一下,对于不同的速率,I2C接口模块会配置不同的时钟信号值。从数据上就能够看出来Slow模式和Fast模式,单周期内占用的时钟个数是不一致的,SLOW模式使用的时钟个数更多,不管是HCNThigh counter)还是LCNTlow counter)。这样的配置也是相对来说容易理解的,信号传递的速度越高,时钟频率肯定就会越大,那么时钟的HIGHLOW部分肯定占据了更少的时间。从寄存器上来看,对于低速(SS)、快速(FS)和高速(HS)配置的时候,使用的是不同的寄存器,而不是在同样的寄存器中使用不同的位来配置。

    image.png

    image.png

    上面两个函数是计算时钟的公式,具体这些计算方法和参数的设置应该是新思科技结合自己的硬件特性给出的。我们不要过于深究。

    image.png

    相比之下,STM32F030xx这款芯片中,对于时钟的配置就要简单的多。Reference Mannual中,对于不同的速率,只需要在SCLSDA中配置相应的值就可以了,而且系统给出了推荐值,是不是简化了不少?不过它这样的表述,从另一方面来看,也掩盖了许多硬件实现的细节。这些细节,对于我们从硬件层面上更加深刻的去理解I2C协议,肯定比较有帮助。只不过我们实际使用的过程中,一般不需要了解得这么细致。当然,从本质上讲,两者实现的思路肯定是一样的。

    时钟信号配置完以后,下一步就是要对SDA和中断系统进行配置了。

    image.png

    大家可以看一下,IC_SDA_HOLD这个位的解释,就是SCL由高到低变化后,SDA这个位还需要保持多少时间。我们说I2C的协议里面是不是定义了,在SCL为高的时候,SDA的数据信号是不是不能变化的呀。怎么能够实现不能变化呢?是不是就是要把SDA的信号延时一部分时间再变化,是不是就可以了呀。这个寄存器就是实现这个功能的。

    image.png

    最后一个是关于SPKLEN的,这个是一个保护设计,为了防止电压或者电流冲击对信号造成影响的。我们按照默认的配置写就可以了。

    image.png

    image.png

    今天关于I2C初始化部分介绍,我们就先到这里。后续的内容,我们会在文章中持续更新。


    收藏 0 回复 0 浏览 316
  • RISC-V 开发板 Buck电源调试记录(项目连载3)

    基于RISCV单片机的鸿蒙开发板

    Buck电源部分调试记录

    1. 起因

    这个开发板前期demo板的焊接,我这边焊接了两块电路板,主要是方便对比调试。前期的焊接都还比较顺利。各个模块(包括电源,LED灯,温湿度检测以及单片机最小系统等)测试都可以正常工作。

    等到两个开发板都焊接到红外模块的时候,两个板子同时出现了问题,程序再也烧录不进去了。没有开发板可以进行程序调试,问题就严重了。

    图片12.jpg

    1: 红色部分为红外探头

    出现这个事情的原因,很有可能是因为红外这个模块是射频模块,功率比较大或者有射频干扰,导致电路板上其他模块的工作出现异常。

    只所以能够得到这个结论,是基于以下两个判断:①两个板子同时出现焊接问题的可能性很小,②红外模块本身原理图出现问题很小,我司其他类型的开发板已经验证过这个电路模块的可行性。

    关于对红外模块的干扰处理,我们后面会有专文进行剖析,本文只做简要介绍。

     

    出现这个问题之后,我们首先想到的就应该是把红外模块的外围电路去除,看看程序的烧录工作还能不能正常进行。

    图片13.jpg

    2 红框中的电路即为红外模块的外围电路

    可是拆除这些模块之后,烧录工作并不能正常进行了,两块手动焊接的开发都是这么一个情况。开发板中肯定有其他模块因为红外模块的焊接出现了损坏,那么到底是哪个模块出现了问题呢?整个开发板,那么多元器件,怎么才能够快速的锁定问题,是一件棘手的事情。

    1. 解决问题的思路及注意事项

    现在的情况是不知道哪个模块出现问题了,只能是进行地毯式的搜查。排查问题的顺序,最好是先从电源部分入手,依次往后看。先看看各个芯片工作的电压是不是正常。如果电压正常,那么就有可能是芯片本身已经被损坏了,就需要把芯片更换掉,再进行测试。如果电压不正常,那么就是电源坏掉了。

    不断重复这个过程(以芯片的工作电压为抓手),最终锁定出问题的地方。

    在这个过程中,芯片会被拆掉,重新焊接,对调试人员焊接元器件的能力是有比较高的要求的。这个要求主要有以下几点:①焊接过程本身对焊盘是用损伤的,多次的焊接很容易把焊盘搞掉。一旦焊盘被搞掉了,可能整个板子就会被废掉,前期所有的工作都白费了。所以烙铁头一定不能剐蹭焊盘。另外同时烙铁头的温度也不能太高,否则粘稠的锡也可能把焊盘带掉。②拆除和焊接芯片的工程中,很容易把芯片的外围器件移动掉,导致外围器件也需要重新焊接,使开发板调试是速度变慢。

    2. 解决问题的过程

    我们首先测量Buck电源能不能够正常工作,结果发现3V3buck输出电压是3.5V。输出电压和实际的设计值之间差别巨大。

    我们使用的Buck芯片是SY8077,这一款工作频率高达1.5MhzBuck芯片,电路工作时典型的拓扑结构如下图:

    图片14.jpg

    3 SY8077芯片工作原理图

    电路正常工作是FB引脚的反馈电压是0.6V,如下图所示。

    图片15.jpg

    4 SY8077 FB接口引脚电压

    但是实际测量时候,FB引脚的电压是0.65V。这就很奇怪了,是Buck芯片出问题了,还是Buck电路的外围芯片出问题了?

    我们可以做出如下假设:

    ①芯片本身被损坏了,导致输出电压不正常,并且FB引脚电压也不正常。

    ②反馈电阻坏掉了,导致FB引脚的电压输出不正常。

     

    但是更换一个新的SY8077 Buck芯片之后,输出依然是3.5V。同时替换下了新的反馈电阻,FB引脚的电压依然是0.65VBuck电路依然没有能够正常工作。难道SY8077更换上去后,只要上电立即就坏掉了?至少到目前是有这个可能的。

    那这是哪里出问题了呢?问题一下子又复杂化了。

    这个Buck电路的输出电压不正常,那输出电流是什么样子呢?我们可以查看一下输出电流的波形,进一步确定buck电路的工作状态。

    图片16.jpg


    3  Buck电路电流测试电路


    图片17.jpg

    4 电流探头

     

    我们把这个电感竖起来,用一根白色的线延长电感的线路。这样把电流探头套到这根线上,就可以查看流过电感的电流波形,从而确定这个电感是工作在什么状态,是断续模式、临界连续模式还是连续模式状态?

    可是把电流探头套到这根线上之后,实测电流波形如下:

    图片18.jpg

    5 输出电流及电压波形

    图中蓝色部分的波形是输出电流的波形,可以看到电流在上电之后,出现了一个波动,然后电流就归于0了。也就是说,5V3.3V的这个buck电路并没有电流输出。

    Buck电流的输出电压不正常,而且输出电流为0;同时SY8077这个芯片以及外围电路已经更换过,应该也是没有问题。那么影响这个buck电路不能正常工作的因素只能是负载了,3V3这个输出电压的负载如果有短路的话,是不是会导致SY8077出现这样的情况呢?如果是负载的原因,那是哪个负载呢?

    确实一下很难猜出是哪个负载导致的,这个时候有两个办法一个是一个个拆解,看看拆掉哪个负载后,buck电路可以正常工作了;另外一个办法是,先把所有的负载拆掉,然后一个一个往电路上添加,如果添加到某一个负载时,buck电路不能工作了,那说明就是这个负载出了问题。

    我们上面还留有了一个疑问,是不是新换的buck芯片SY8077上电之后就坏掉了?我们需要首先验证一下buck电路是不是可以正常工作。要验证这个buck电路是不是可以正常工作,只有先把所有的负载先拆除掉,只留一个阻性负载或者选择电子负载用作测试。

    结合上面的因素,我们选择先把所有3V3的负载先全部拆掉。拆掉之后,重新查看电感的电流波形,发现电流是有输出的。而且随着电子负载的加重,电感的工作模式实现了从断续模式到连续模式的转变。这个测试结果说明SY8077这个芯片并没有损坏,同时SY8077这个芯片在自我保护的时候,其输出电流是0FB脚的电压是0.65V。具体为什么会是这么一个机制,SY8077datasheet并没有说清楚。

    后面就是不断把3.3V的负载(各个芯片,比如温湿度传感器SH20、电平转换芯片、电平逻辑芯片等等,只要供电电压是3.3V的,都算是3V3电源电压的负载)往电路板上添加。添加的过程一定要注意,最好是使用风枪吹,这样对周边焊接元器件的影响是最小的,可以加快调试板子的速度。

    等到添加FT4222HQ这个SPIFlash的时候,发现Buck电路电流又变成0了,也就是说Buck电路不能正常工作了。经过漫长的分析与调试,终于找到被损坏的芯片了!!!

    更换上新的FT4222HQ之后,Buck电源的输出就正常了,自然程序的烧录也就正常了。


    收藏 0 回复 0 浏览 295
  • ESP8266物联网开发板设计3 ---晶振起振原理

    大家好,我是张飞实战电子张角老师!我们今天继续物联网开发板硬件电路设计相关的探讨。

    我们先来看晶振这块。首先,单片机是可以看成是一个高速数字电路的集合体,其中速度最快的部件就要算是中央处理器了,其余的外围部件,比如PWM模块、I2C模块、SPI模块等等速度相对慢一些。我们先看CPU,它自己需要进行计算,但是多长时间CPU及其相关寄存器的状态改变一次,必须有一个时钟信号进行配合。换句话说,晶振的频率影响或者决定了CPU的动作周期。从另外一个层面来看,各个部件之间要协同工作,这肯定是需要同步的。这个同步的动作,肯定就需要时钟的参与。

    仅仅从上面两个部分来看,单片机要想高效地完成编程者设定的任务,时钟信号是不可或缺的。目前单片机的时钟信号的来源主要有两类,一类是内部的RC振荡器,另一类是外部的晶振电路。内部的RC振荡器,频率相对来说比较低,一个主要的原因是它受温度的影响比较大。主要的原因就是RC振荡器的外围电路,其实还都是半导体器件组成,而偏偏半导体器件温飘特性相对来说就是比较大。那么也就是RC振荡电路的周期就不准了,温度高的时候和温度低的时候,周期不一样。所以有些朋友家里安装的电子时钟,相对来说,时间一长,就容易不准,其实很大一部分原因就是这个产品使用的芯片内部的RC振荡器。

    但是有一个器件,能够提供非常精准的震荡频率,那就是石英晶体。石英晶体是一种具有压电效应的器件,在石英晶体两个管脚添加上交变的电场时,它将会产生一定的机械变形,这种机械变形反过来又会产生交变电场。一般情况下,无论是机械震动的振幅,还是交变电场的振幅都非常小。但是,当交变电场达到某一个特定值的时候,振幅陡然增大,产生共振。这个频率,我们称之为石英晶体的谐振频率。

    那么我们单片机配套的石英晶体振荡器,具体是如何起振的呢?我们只有知晓了具体的起振过程,才有可能搞清楚设计上需要注意的地方。

    我们单片机上的晶振电路,本质上就是电容三点式震荡电路的改进版。那么我们要讲清楚单片机上的晶振电路,就要讲清楚以下几个问题。第一,什么是震荡电路,震荡电路起振需要满足什么条件。第二,电容三点式震荡电路怎么起振的,它怎么就满足了震荡条件了。第三,电容三点式振荡电路的改进方向是什么,石英晶体震荡器符合电容三点式振荡器的改进方向么?第四,我们单片机内部电容三点式震荡电路一般是怎么实现的,我们设计晶振电路的时候有哪些需要注意的地方。在讲解这些地方的时候,我们尽量减少公式的表达,更多的追求直观的理解,毕竟我们的目的不是去设计单片机的晶振电路。

    我们说正弦波震荡电路起振的条件,主要是两个条件,一个是必须要引入正反馈,也就是说反馈信号必须要能够替代输入信号;第二个要有外加的选频网络,进而用来确定震荡的频率。我们下面来看一下,常见的电容三点式震荡电路是什么样子的。

    如下图所示,图中的三极管T1是放大部分,对A点输入的信号进行了放大。同时三极管的基极和射极信号是反向的,这个是三极管本身的特性导致的。如果对这个不熟悉的话,大家可以下去看看三极管的知识。那么三极管在这里也就是提供了两个功能,一个是信号的幅值放大(当然放大的倍数可以通过调整RcRe等进行调节),另一个功能提供了信号的相位的反相(图中A点和B点信号)。

    图中黑色虚线中的部分,就是电容三点式振荡器的选频网络。发生共振的时候,电感L1和电容C2C3组成的LC网络,可以近似为阻抗无穷大,也就是说电感L1和电容C2C3电流是环路电流。大家知道电容的特性,是不是电流相位超前电压90度呀。我们把C2C3的中间点接地,C点电压超前B90度,D点电压又超前C90度,那么D点电压是不是超前B180度呀。D点电压超前B180度,那是不是说D点的波形和B点是反向的呀。D点的波形和B点是反向的,B点的波形和A点是反向的,那么D点的波形和A点是不是就是同相的。那么这里是不是既有了正反馈,又有了选频网络。那么是不是就有可能满足正弦波的震荡条件呀,只要这个电路参数得当,那么就会产生正弦波。

    image.png

    这个电路的反馈系数F等于C2/C3, 电压放大倍数A = β*RL/rbe,其中RL = Rc//(Ri/F^2)

    我们是正弦波要能够起振,是不是 AF的值要大于1呀。如果我们通过调整C2/C3的值,就会出现一个相悖的结论,调大了F,结果A却小了。那么AF的乘积变化的趋势反而不明朗了。实际实验中,C2/C3的值既不能太大,也不能太小,具体指只能靠测试来设定。电容三点式震荡电路,相对来说它的缺点也在于此,通过调节电容去调节频率的话,会影响起振条件,但是通过调节电感办法来调节震荡频率,实现上又会比较困难。

    通过LC谐振点的公式来看,如果我们要想提高震荡的频率,只能是不断减小C2C3以及电感L。但是C2C3减小到一定程度的时候,比如和电路的杂散电容一个级别的时候,我们就很难确定震荡的频率了,因为杂散电容的电容值几乎很难确定。那么我们有没有其他办法来解决这个问题呢?如果我们把C2C3电容的容值设定在远大于杂散电容,那么杂散电容对电路的影响是不是就可以忽略掉了,电路的震荡频率是不是就会相对来说非常稳定。那么问题来了,震荡的频率如何提高呢?具体的措施,就是在电感上串联一个小电容C,同时C<<C2, C<<C3, 这样震荡频率只和这个C有关,f = 1/(2πsqrt(LC))。电容C2C3这里只是起到分压作用而已。整体的设计,如下图所示。

    image.png

    分析完了电容三点式震荡电路,那么我们看一下晶振怎么参与进来。因为我们前面已经提到了,我们单片机电路的晶振模块本质上也是电容三点式震荡电路。要看清楚这个问题,我们必须要了解晶振的高频模型,看看晶振的物理特性到底是什么。

    image.png

    实际上,晶振的高频等效模型如上图所示。这里的L,可以等效为晶振的惯性,这个值大概是几mH到几十mH。电容C等效为晶振的弹性,这个电容比较小,容值大概在0.01pF0.1pFR等效为晶振的摩擦损耗,这个值大概在100R,当然理想情况下这个值是零。电容C0等效为晶振的静态电容,它的大小和晶振的几何尺寸和极间面积有关,一般是几到几十个pF。那么实际上,晶振是惯性和弹性的结合体。结合我们前面提到的电容三点式振荡电路,我们更希望利用的其实是晶振的惯性部分,也就是电感部分对吧。在晶振组成的震荡电路中,震荡的稳定性主要是靠晶振本身的高Q值来稳定的,不是靠我们上面提到的那个思路,而且我们也不需要在外部震荡处那么高的频率,比如100Mhz的震荡。一般情况下,我们使用的是12Mhz的无源晶振,单片机内部的更高频次的震荡信号,是通过分频器和锁相环来实现的。

    既然,我们使用的是晶振的惯性部分,那么我们就来分析一下,在什么频率下晶振会更多的呈现惯性,也就是感性。

    当晶振的LCR电路发生谐振的时候,这个回路呈现纯阻性,等效电阻为R。谐振频率fs=1/(2πsqrt(LC))。当f低于fs的时候,C0这里起到主导作用,晶振呈现容性。当f大于fs的时候,LCR这条支路呈现感性。LCR这条支路将和C0发生并联谐振,谐振的频率fp = fs*sqrt(1+C/C0),这个具体的公式大家可以下去推导一下,我们这里就不再做更深入的介绍了。大家可以看一下,因为C远远小于C0,所以fsfp是不是无限接近呀。那么也就是说晶振只有在很窄的频率范围内,才会呈现感性。

    回到我们前面提到的电容三点式震荡电路,晶振只在一个很窄的范围内呈现感性,而且受温度的影响比较小。那么是不是说用晶振组成的三点式震荡电路,它的频率会非常稳定呀。具体电路设计如下图所示。那这样,我们使用晶振搭建的电容三点式震荡电路就算完成了。

    image.png

    那么下一个任务就要回答,单片机中的晶振电路一般是如何实现的呢?我们以80C51来距离,它震荡电路的框图大概是下面这个样子。

    image.png

    image.png

    收藏 0 回复 0 浏览 284
  • 讲透有史以来广受欢迎的运算放大器μA741(6)---放大级及输出级

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

    image.pngimage.png

    我们来看一下R5这个电阻的作用,如果没有R5这个电阻,那么T5T6的基极到地的阻抗就是不可以调节的,那么T7Ib电流也就确定了。T7Ib电流确定了,那么T7基极的电压也就确定了。我们前面分析过,T7这个管子的存在主要是增强两个输入端的对称性,我们先假定两个输入端的回路是完全对称的。那么T5T6C极电压就是相等的。那么对于输出来说,我们是不是希望当Vin+等于Vin-的时候,输出电位是不是零呀。那是不是要求T5T6C极电压有一个确定的值呀,这个确定的值可以使得当Vin+ = Vin-的时候,输出电压为零。R5这个电阻的作用也就在于此,它可以调节T7基极的电压,也就是T6 集电极的电压(也就是T5集电极的电压,根据对称性得来),使得输出电位为零。

    我们下面看一下放大级,放大级采用的是达林顿管的结构。什么是达林顿管呢,总体上而言,就是两个N管或者P管串联,这样就可以增大放大倍数。这个我们前面也提到过,使用这种方式来提高输入级的内阻。我们从另外一个层面看问题,其实从维护输入桥臂对称性的角度上来讲,从F点输出的电流也不宜较大,那样会拉低F点的电位。因为电流越大,相当于F点到负电源的阻抗也就越低,那么很显然会把F点的电位拉低。那么也就是说,如果把F点看成一个电源的话,它也是希望后级负载的阻抗相对较高。其实这个需求也是必然的,因为F点的作为源的话,它本身的阻抗是也是比较高的,那么自然它就会要求它的负载的阻抗相对高一些。

    image.png

    上图中T10这个管子是一种特殊的三极管,有两个集电极。我们可以通过控制集电极面积的大小,进而控制流过它们的电流。T10这个管子是镜像Q5这个管子的电流的,也是一个电流镜,总的Ic电流也是Iref。这个原理,我们就不再赘述了。在uA741中,流过T9这个管子的电流是0.75Iref,前面我们算过Iref大概是0.73mA,那么流过T9Ic电流大约就是0.548mA

    我们来看一下uA741的输出级,它的输出级采用的是上NP的推挽结构,这样可以提高输出功率。这个推挽结构,大家应该比较熟悉了。R9R10这两个电阻主要是用来进行限流使用的,具体起作用的方式我们后面再讲。T10T11这两个管子是一个Vbe发生器,也来产生一个固定的压差,这个压差骑在T12Q7的基极之间,这样就可以防止推挽结构导致的交越失真。在uA741中,这个压差的值是1.2V,这个值大了会导致T12Q7的损耗比较高;小了,交越失真就会相对严重一些。

    image.png

    Q6这个管子可以看成一个电压抬升器,把T9这个管子集电极电压又抬升了0.7VQ6这个管子很明显是放大状态接法,同时把信号放大和功率输出隔离开,尽可能减少功率输出部分对信号放大的影响。从驱动能力的角度看,T9最好也是驱动一个高内阻的负载,这样对T9的影响是最小的。


    收藏 0 回复 0 浏览 258
  • 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
×
张角