好久没来电源网发帖,学习电子技术的过程中积累了一点心得体会,总想直抒胸臆,可身边没几个同行,只好跑到电源网写点酸文,贻笑与大方之家。
既然是技术心得类的帖子,肯定存在理解不透彻的地方,肯定有自己没有关注到的知识点,还望大家海涵,及时斧正,谢谢。
先在此挖个坑,慢慢填吧,可能会虎头蛇尾,但肯定会不定期更新。
好久没来电源网发帖,学习电子技术的过程中积累了一点心得体会,总想直抒胸臆,可身边没几个同行,只好跑到电源网写点酸文,贻笑与大方之家。
既然是技术心得类的帖子,肯定存在理解不透彻的地方,肯定有自己没有关注到的知识点,还望大家海涵,及时斧正,谢谢。
先在此挖个坑,慢慢填吧,可能会虎头蛇尾,但肯定会不定期更新。
说心里话,很早就想写一点有关C语言的学习文章,但是怎么写,才能让大家相对愉快地学习,我也想了很久。为此我也翻了很多程序语言书籍,自认为积累了一点学习经验,分享给大家,下面转入正题。
讲解程序语言,教科书大多从琐碎的语法结构开始,这样的讲解方式直接把多数的学生拒之门外。回想我当年学习C语言的经历,只能说苦不堪言。起初C语言的书籍是我的枕边读物,一般晚上入睡前翻一两页,相当有助于睡眠。
我们能不能站得更高一点,尝试着采用先整体后局部的方法讲讲C语言呢?换句话说,我们能否先将程序语言的框架搭起来,然后再根据功能需求用各种语法一步一步地完善框架,最终使其成为完整的程序。
通俗点说,就是先建立骨架,再摸索着往骨架上填充血肉,接着尝试关联各个功能部件,最后大功告成。当然过程肯定不会一帆风顺,必然存在诸多的问题,通过不断地调试,最终问题都会解决,功能也会实现。
其实程序的运行不就和眼前的滚滚长江水一样吗?流动的长江水必然有其源头以及尽头。长江水在流经不同地貌时,会产生很多的支流,不同的支流又会分化出诸多小支流。支流中有些绕过一段河道后又重新汇入长江,有些则成了一潭死水。
如果从江水的源头与尽头看,我们可以粗略地认为长江是以直线的运动轨迹川流不息。看看长江的各个支流,我们可以说这是长江的流动分支。再瞧瞧江水绕过的各个湾湾码头,这不是在循环往复吗?
再看看程序的运行,不也是一样吗。整体上看,程序从上往下依次执行,再执行过程中根据不同条件分化出诸多的节点,有些节点执行一次即可,有些节点需要执行多次,有些节点则直接跳过。
至此,程序的框架终于清晰了,再结合书本专业知识,我们总算弄通了程序是怎么运行的。
程序是由顺序结构,分支结构以及循环结构组成的。如果对汇编语言有所了解或者熟悉C语言goto关键字的用法,我们甚至可以说程序语言只有一种结构,那就是跳转结构。再深入一点,若理解了指针的概念,同时懂得一点计算机的组成原理,我们满可以说,程序的本质就是通过合理地规划内存,最大限度发挥计算机内部各功能模块的性能。
上述放弃学习单片机的工程师离单片机入门仅仅差一小步,那就是思考,准确地说是带着问题去思考。
学习单片机的心态很重要,我是一个学习单片机失败了很多次的LOSER(我对电子技术没有多少兴趣,坚持至今是因为没有更好的谋生手段罢了,我比常人多了一点耐心,恒心),在此总结归纳一下我认为正确的或者说是恰当的学习单片机的方法及心态。
第一点是忘,忘掉过往已经掌握的开关电源的系统知识(电源网的大多数受众是电源工程师),对于射频工程师,PLC工程师,上位机软件工程类似。为什么有此一说呢,这是我尝试着学习单片机失败了多次后悟出来的。
我们一旦有门技术傍身后,总会习惯性地靠这门擅长的手艺谋生,慢慢就形成了依赖,专业术语称作思维舒适区。我们思想中总会有一个误区,认为过往掌握的技能有助于我们学习后面的新知识。不自觉的我们就把以前的思维模式带入到新的知识领域中。这就是最大的问题。
单片机与开关电源,变频器,射频电子,PLC,上位机软件编程等都是相对独立的技能,不要指望学会了一门手艺后,就能触类旁通其他的相关技能。这是我们开始学习单片机必须要有的思想觉悟。
不信,你可以问问身边精通单片机,或者PLC,乃至JAVA编程的同事懂不懂开关电源的设计,大概率是不懂的。会抄个反激电源的单片机工程师不算,他那点浅薄的对开关电源的认识不够看。
能够将开关电源和单片机区分开来学习,你已经成功了一大步。单片机和开关电源是两个不同的领域,虽然熟悉开关电源有助于我们理解单片机中涉及的硬件知识,但这远远不够。
学习单片机需要具有的第二个心态是不急,有耐心。市面上有一些教材号称一个礼拜掌握单片机,这样的教程针对的受众通常是有一定基础的,且资质相对较好的朋友。如果没有一定的知识储备,个人建议还是按部就班的学习。
知识是逐步缓慢积累的,快速上手的教程最大的副作用可能是营造了一种焦虑的环境,让很多初学者再学过好几遍仍找不着头绪时,开始了自我否定,最终放弃。在此我送给那些一直徘徊在单片机入门阶段的朋友一句话:“坚持就是在快要放弃时再忍耐忍耐。”
我个人定义单片机技术为一门谋生的手艺,甚至可以说是安身立命的手段,那肯定不太容易掌握,否则满大街的人都会玩,何谈养家糊口一说。
周围也有很多电子工程师精通单片机技术,那说明这门技术的学习掌握也没有难于上青天。只要努力,理顺思路,储备了足够的知识,找到适合自己的学习方法,肯定可以精通单片机技术。
学习单片机技术需要具备的第三个心态是恒心,或者说是韧性。想学好单片机必然会遇到各种各样稀奇古怪的问题,这时心态一定要足够坚韧,不能但凡遇到一点困难,一时半会解决不了就想着放弃。
在某个问题上卡住了,如果以当前的知识储备确实解决不了,那就放一放,继续往前走,千万不能自设囚笼,钻牛角尖把自己困在那里,这一点很重要。我自己属于典型的一根筋,遇到问题总想着立马解决,但身边又缺少能够及时给自己答疑解惑的朋友。在苦思冥想过一番仍不可得时,我就失去了继续往前探索学习的兴趣。等下一次再提起兴趣时,又得从头开始,周而复始,永远困在单片机入门学习的道路上。
在学习的过程中明知会遇到各种问题而依旧选择努力前行不失为勇者的表现,在遇到超出自己能力范围的问题而选择适时地放一放,可谓是智者的选择。
C语言中的内存被分割为四个区域,分别对应着代码区(code),数据区(data),栈区(stack),堆区(heap)。不同区域的功能特性也不尽相同。首先讲讲代码区(code).
单片机上电复位后,编译生成的HEX代码经由机器内部固化的微指***令加载进内存,实质上代码被引导至内存中的代码区,即code区。该段内存的特性之一是不可以修改,专业术语为只读。其次该区域内容是可共享的(即某段可执行代码能够访问它)。
C语言代码区可共享的目的是针对频繁调用的只读数据,使用此法可有效提高程序的访问效率。代码区设置为只读的意义在于防止程序运行过程中的意外而修改了某些指***令或数据,导致不可预料的错误发生。
51单片机代码区如何配置可共享数据呢?使用51单片机特有的C语言关键字code,即可实现。C51中该类变量常用于存放一些复杂函数的固定值数据,如各类真值表,三角函数值,反三角函数值,对数值,指数值等。
为什么要这样做呢?很多的8位机,16位机内部并没有浮点运算器,遇到复杂一些的运算基本都不能胜任,而用户仅仅关注最后的结果。我们在程序中预先给出运算结果,当涉及某一功能函数计算时,直接将代码区中对应的运算结果调用显示即可。此种方法既高效,又节省了大量机器资源。这种思想在单片机技术中被称作查表执行。
51单片机中code关键字修饰的数据,其特性为只读。从单片机内部资源的角度看,此类数据存储至程序空间ROM中(C语言中则为代码区)。这样可以大大节省单片机的RAM使用量,毕竟我们的单片机RAM空间比较小,而程序空间相对大的多。
数据区又可以细分为data区,bss区及常量区。其中data区里主要存放的是已经初始化的全局变量、静态变量。bss区主要存放未初始化的全局变量、静态变量。未初始化的数据在程序执行前会被编译器自动初始化为0或NULL。顾名思义常量区存放的是一些常量,如const关键字修饰的全局变量以及字符串常量等。
数据区中的数据相较于代码区中的数据,其不仅可读而且可写。可读表明该区域里的数据可被函数调用,可写表明该区域中的数据能够被改变。该区域数据的特点是只要内存不掉电,数据就能一直存活。单片机中的各功能寄存器也有此类似特性。只要配置好寄存器相关数据后,寄存器中的数据就能一直存在,除非掉电。有很多场合的应用需要保证寄存器中的数据不丢失,为防止外界电源断电,单片机系统会接入备用电池。如实时时钟,日历,低功耗模式等。
不同类型的变量在内存中的存活时间各不相同,这一特性专业术语称作变量的生命周期。其次可读数据变量在内存中能被哪些函数调用涉及到变量的调用范围,专业术语叫变量的作用域。
类人猿之所以能够进化为人类,我的理解是它们具备不断试错不断学习的能力。在进化的道路中每一次试错都伴随着一次成长过程。在缓慢的成长过程中,其心志得到不断的进化。当积累到一定程度,量变引起质变,类人猿终于成长为今天的你我。将人类的进化过程映射到计算机技术上,虽有些扯,但多少有些相通性。
某种意义上说,循环结构是特殊的选择结构。程序语言根据条件判断依次往下执行时,我们说这是典型的选择结构。当程序语言根据条件判断执行若干动作后,又回至条件判断的起点,重新进入条件判断的流程,依此往复执行…… 我们可以说这就是循环结构。
从上述角度解读,我们可以认为循环结构是特殊的选择结构。当然,如果循环结构的判断执行条件仅只执行一次,我们同样可以说选择结构是特殊的循环结构。换句话说,选择结构和循环结构之间可以相互转换。
不管是代码区还是数据区中的数据自始至终都驻扎在内存中,而内存规划的初衷是为了高效利用内存,及时回收释放多余的内存,使有限的内存有足够的空间支撑程序的全过程运行,显然仅仅靠代码区及数据区远远不够,遂引入堆和栈的概念。
堆区(heap),该区域可看作是一个存储数据的大容器,其容量远远大于后面要讲解的栈区,该区域中的数据不需要像栈区那样遵循先进后出的规则。
堆区主要用于动态内存分配。堆区在内存中位于数据区中的BSS区和栈区之间。该区域一般由程序员手动分配和释放,若程序员不释放,程序结束时由操作系统回收(该话题涉及到操作系统内存回收的话题,此处一笔带过,有机会分析嵌入式linux系统时讲解)。
C语言中堆区的大小可使用malloc函数进行分配,堆区的释放采用free函数实现。程序中malloc等内存分配函数的使用次数一定要和free函数相等,并一一配对使用。否则随着程序的运行会出现内存泄漏的问题。
栈区(stack),用于存储所有的局部变量,包括用户定义的auto型局部变量,函数的实参,返回值,嵌套函数的调用(递归函数)等。栈区和其他三个区域最大的不同点在于其采用的是先进后出的内存结构。局部变量在该区域由编译器在程序运行过程中实时加载和释放。所以局部变量的生存周期很短,需要时即申请,不用即销毁释放。
auto型局部变量经编译器自动放入栈中后,其生存周期及作用域只限于定义的功能函数内有效,当一个auto型局部变量超出其作用域时,自动从栈中弹出,用完后再由操作系统自动释放。
auto型局部变量的生存周期及作用域的特点牵出一个很有意思的话题,即函数实参的调用是否影响对应变量的数值,答案是不影响(后续C语言语法章节会详细讲解)。
另外一个注意事项是不要返回局部变量的地址,因为栈区开辟的地址空间由编译器自动释放,当auto型局部变量失效后,原先对应的地址空间也随之释放,返回的局部变量地址是一个随机数,而不是之前对应的局部变量的地址。
上述堆区和栈区的概念是针对C语言而言,对于单片机我们也经常提到堆栈的概念,但C语言的堆区栈区和单片机的堆栈还是有区别的。单片机的堆栈实质是C语言中栈的描述。通常单片机的内存资源相当有限,所以一般不分配堆区,而且很少用到标准库的malloc类函数,当然上升到操作系统层面就是另一说了。
单片机的堆栈也属于内存的一部分,所以它肯定有临时存储的功能,那单片机的堆栈主要用于哪些存储场合呢?如果说代码区和数据区中的数据是用于单片机自始至终的全程数据存储,那堆栈就是用于单片机运行过程中的中间数据处理。
通俗的说,代码区和数据区中存储的数据只要单片机不掉电,其中的数据就不会被释放。而堆栈中的数据是根据程序需要实时产生,实时释放,我们通常提到的内存回收与释放实质是针对堆栈中数据的回收释放操作。