其他
一位嵌入式工程师,硬核单片机编程思想
击上方“果果小师弟”,选择“置顶/星标公众号”干货福利,第一时间送达!摘要:没有思想的裸程序就如一副人体骨架,有个人形,但没有人样,骨骼之间的关节都是靠胶水或拉线连接起来的,生硬而呆板。如果给骨架包上皮肉,加上灵魂,我们就会惊叹:啊!这是帅哥,这是美女!因为骨架活了。一、裸编程是什么?先声明一个概念,裸编程,指的是在裸机上编写程序,裸机,在单片机领域就是指带着硬件的单片机控制系统,不要想歪咯。在裸机上编程,就犹如在一片荒地上开垦,任何一锄头下去,都会碰到硬生生的石头,要说做这有什么味?拓荒者追求的是来年的绿洲。而我们这些开垦裸机的所谓的工程师们追求的是什么?我们当然追求的是完成一个任务。我们一般都自称是高级知识分子,那么我们在拓荒的过程中应该想些什么?当然不是想着如何把任务完成,而应该首先想着我们在想些什么。绕了是不?绕了就对了,这一绕就绕出了思想。思想是一个简单的人在一个复杂的环境里做任何事情的统帅,它影响着一个拓荒者人生的每一个细节,当然也包括裸编程本身。当一个人拿着锄头,一锄又一锄,汗滴脚下土的时候,我们能知道他们在想什么吗?当然这不好说,如果自己去锄就知道了。但是大抵也差不多,随便举几个吧:这太阳他娘的怎么这么毒?这石头他娘的咋这么多?这地种什么最好?这还有多少天能搞完?这样干太慢了,要是有台机械搞多好。当然这只是一部分,任何人可以想出很多想法来。那么当我们在裸机上拓荒的时候,我们该想些什么?也许我们一般的想法是:先把一个简单的功能做了,先把一个重要的功能做了,今天终于把这个功能调试好了明天可以做下一个功能了,这个为什么不是我想象的那样的结果?真是莫名其妙!也等等一下吧。如果拿来一个任务,搭好测试平台就开始做程序,想着一个功能一个功能的凑完,然后就自我陶醉着成功的喜悦,那这样做程序,基本就叫做没思想。有思想的做程序,是不能一下去就堆积源码的,因为那样只会让一堆生硬的数字怯生生的挤在一起,不管他们有没有多余,有没有矛盾。所以写源码之前,是要想想如何写的。也许很多人在写之前都想过类似的问题,比如把任务模块化后再组织程序。但是这样的想法只是任务上的事情,而并不是裸编程时的思想,裸编程的思想,应该是在组织任务模块过程中及编写裸程序时影响源码组织的指导思想,它直接决定着源码的质量。一个数据结构,一个模块形成,一个单片机的指令,一个硬指令的运行机制,一个口线的驱动方式,一个中断的顺序,一个跳变的延迟,一个代码的位置,一个逻辑的组织,一个模块与模块之间的生(运行时的状态)死(不运行时的状态)关系等等,都是裸程序思想的组成部分。这似乎很琐碎,但是裸程序原本就如此,它不同于上位机程序,有一个强大完善的操作系统支持。单片机里不可能植入操作系统,那样做就变味了,可不要有人跳出来说,某某某单片机就有操作系统了。裸程序就应该是建立在赤裸裸的硬件基础上的程序,只有有用的功能才有代码,裸程序的质量也许经常在应用中感觉不出来,也许你做和他做都能实现功能,但是好的裸程序有良好的可扩充性、可维护性,系统具有高稳定性和高性能。而追求这种高品位的技术境界,就必须要有好的思想来指导。是不是看着有些迷糊?别说看得迷糊,我说都说迷糊了,总的来说,就是把一个优秀的灵魂,植入你的源码中,让你的源码具有一个优良的思想。二、裸编程具体做法前文说到裸编程要有思想,也许还不够具体,接下来就是要具体说裸编程的思想的具体做法。没有思想的裸程序就如一副人体骨架,有个人形,但没有人样,骨骼之间的关节都是靠胶水或拉线连接起来的,生硬而呆板。如果给骨架包上皮肉,加上灵魂,我们就会惊叹:啊!这是帅哥,这是美女!因为骨架活了。裸程序也一样,如果按传统的思维方式说这样就足够了,那么裸程序就形如骨架,通常只是一些功能的粗糙堆砌,也只会叫后人看了说这程序垃圾,而后人再做也未必能跳出这个圈子,那么后后人看了又叫这程序垃圾,如此下去,代代相传,传了什么?传了一个总被叫垃圾的东西:无思想的裸程序。我做了程序好多年,也思考了编程好多年,不断的经验积累告诉我:写好的程序不是如何去完成代码,而是如何去组织代码。上位机中面向对象的编程思想,就是一个非常可取的思想。面向对象的编程思想在上位机中是有一个非常丰富的开发包和功能强大的操作系统支持的,裸编程如何引入这样的思想呢?也许很多人会觉得不可能。其实,没有什么是不可能的。再复杂的思想,最终都会归结到汇编,归结到裸程序,我们的单片机程序,正是一种裸程序。只是在单片机编程时和微机编程时我们站在开发平台上的高度不一样,而已!对这个高度的理解,也许很多人很困惑,因为我们平时很少注意它们,那么这里我就举个其他的例子来说明,尽管和裸编程好象不很相关,但是这个例子里的高度概念十分清晰。我们知道网络传输标准层次有七层:应用层、表示层、会话层、传输层、网络层、链路层、物理层,这么多层做什么用?也许理解这样分层的概念也十分辛苦,但是理解这样分层的思想,就容易多了,而且这也是我们硬件工程师们最应该借鉴的思想,让我们的硬件设计更具有标准性和前瞻性。这个七层的思想从根本上讲就是将一个网络传输产品细化,让不同的制造商选择一个适合自己的层次开发自己的产品,层次不一样,他们所选择的开发基础和开发内容就不一样,高一层开发者继承低层开发者的成果,从而节省社会资源,提高社会生产力。对这个指导思想我就不赘述了,各位自己去理解,这里要说的是,微机上的面向对象编程思想就是如同在应用层上实现的思想,而裸程序的面向对象思想则如同在链路层上实现的思想,他下面没有软件开发包,只有物理构架。但是在应用层上实现的思想,最终都要翻译到物理构架上。看懂了上面的例子,就一定明白,裸程序的面向对象思想,是可以实现的,只是难度要大得多,理解要难得多。但是这不要紧,这正是软件水平的表现,你喜欢技术,又何惧之?其实也不会难到哪里去,只是把做事情的方式稍微改变一下而已。传统上我们都喜欢用功能来划分模块,细分任务,面向对象思想不这样。面向对象思想则是先从一个任务中找出对象,在对象中搀杂些模块等来实现功能的。这就是两种风格截然不同的地方。比如我们要让我们的单片机把显示信息输出到显示器,那么传统的分析方法是信息格式化、格式化数据送显示器显示,似乎这样也就足够了,不同的显示器用不同的送显示程序或者程序段,配置不同的变量,能共的共起来,不能共的分开。但是面向对象的思想不是这样做的,而是首先把显示器当作一个对象,该对象具有一些功能和一些变量属性,不同的显示器在对象中使用相同的代码标识,如函数指针(C语言中),这样对于任何一个不同的显示器,在调用时都使用同样的代码。也许有人说,传统的做法这样也可以做呀,为什么要弄得罗里吧唆的呢?其实不然,使用了正确的思想的好处在前头已经说了好多了,如果还模糊就上去再看一次。说了那么多理论,现在就说些具体的做法吧。以KeilC为编译环境来说说一个对象具体组织的一些做法。首先是找出对象,如显示器,这就是一个典型的对象。其次是分析一个活对象所应具有的基本特征,即属性与动作。显示器的属性如:类型代号、亮度、对比度、显存等,动作如:初始化、内容刷新和显示、开启和关闭、内容闪烁等花样显示等。这样分也比较容易理解,下面是对于代码的组织上,要注意对象的独立性与完整性,首先把显示器对象单独放在一个文档上,属于对象特有的变量与对象的定义放在一起,要区分公有变量与私有变量的定义方式,对于私有变量要考虑临时变量与永久变量的安排,这些安排都是对变量生命期的严格确定,这样可以节省内存,避免混乱。如某一个函数要使用一个变量,函数在调用完了就退出了,而有一个变量只有它使用,却要保存每一次调用函数所产生的结果,这样的变量怎么定义呢?很多人会直接定义一个全局变量,但是一个好的做法是把这个变量定义成该函数的局部变量,但是定义成静态的,那么这样这个变量对其他代码就是透明的,完全不可能会被误修改,而且代码分类性好,便于将来的维护。用函数指针来统一不同类型的显示器不同的处理方式,也是一个很好的处理办法,那样可以让具体处理方式千差万别的显示器都能用一个统一的对象,但是函数指针要慎重使用。好了,说长了我就头晕,不说了,思想的精华,不必有一样的形态,不同的人会有不同的理解,我只希望能给大家的程序生涯抛砖引玉,我就觉得很有成就感了。三、准备工作本文在此引用一个例子。在引入例子之前,我们要做一些准备工作,然后一步一步地走向例子里去。就以前面帖子提到过的显示器控制为例。显示器就是一个对象。无论它是功能多么复杂的显示器,或者功能多么简单的显示器,对于需要显示信息的调用者来说,都并不重要,也就是说对于需要使用显示器的主体来讲,他只管显示信息,不管显示器的千差万别,只要显示器提供了某功能,它可以调用就行,调用前当然要遵守显示器的数据传递规则,但是不必考虑不同的显示器所产生的传递规则的差异。也就是说,对于调用者来说,永远不会希望有多条规则来让自己的调用代码变复杂。因此,我们首先需要构造一个相对独立的代码段,也就是显示器对象,以下都以KeilC作为裸程序的编译环境。正如很多人说的,KeilC并不是OOP语言,那怎么做?正是因为我们认为KeilC不能做,所以我才把这种思想拿出来与大家探讨,让我们的程序变得更精彩,更有技术含量。形成一个独立代码段,最好的办法就是在主工程目录下建立一个子目录,如DISPLAY,然后再在DISPLAY目录下建立一个文档,如DISPLAY.H,然后把DISPLAYDISPLAY.H文档#include到一个恰当的位置上,这样,一个独立的面向对象的代码段就初步形成了,以后维护代码的时候,你永远不要考虑调用者是什么感受,只要维护这个文档,就可以保证你对显示器的不断更新了。很多人也许会说,这算什么OOP?大家先别着急,这才是个开始,下面才是组织代码的具体过程。对于一个显示器,我们必须要有显示要求,我们才会去定制它,如果连使用要求都提不出来,就不要去让人为你做显示器。所以我们首先要明确我们要的显示器必须要做什么。由于是单片机控制的显示器,我们不能想象成微机显示器那样,一个大的显存,可以显示多少页,显示多少色,满屏满屏的传递数据,如果这样想了,就是犯了盲目例比的错误,说明对问题没研究透。对于单片机控制的显示器,我们考虑能显示单个字符、单行显示,就基本足够了。所以我们可以定义下列两个对象功能:dispShowAChar();//显示一个字符dispShowALine();//显示一行字符由于是单片机的裸系统,所以我们作为一个软件设计者,我们一定要清楚,我们所面对的显示器,经常是没有CPU的,所以我们一定要明白,我们这两个函数,实质上都做些什么。很显然,这两个函数是不能长期占有CPU的,否则我们的程序将什么都不能做,专去显示了,成了显示器的一个处理芯片,所以这两个函数运行完后是肯定要退出来的,而显示不能中断呀,所以必须要有一个代码段一直存在于活动代码中而且不能影响其他的功能。做过上位机程序的人应该能看出来,这段代码就是线程。裸编程中我们也用这个概念。我们的显示器对象正需要一个一直活动的线程,来完成单片机系统对显示功能的解释和执行,因此dispShowAChar()和dispShowALine()实质上是不能直接去做显示工作的,它俩最合适的工作,就是去按指定的格式去设置显示内容,这样我们在使用的时候就不必在这两个函数里设置复杂的代码和嵌套调用关系,因为那样一定会浪费很多的代码,调用多了也会让单片机运行效率降低,硬件资源消耗增加,严重的可能会造成堆栈溢出最后还不晓得为什么。让我们也为这个活动线程也先命个名吧:dispMainThread();//按指定的要求执行显示功能//指定的要求包括颜色信息、闪烁、游动等等程序分析下去,引出的概念也就会越来越多,这里所说的多线程概念以后有机会再说,单片机里的多线程也是一个复杂罗嗦的处理问题,现在介绍还为时过早。只是我感觉一不小心又说长了,具体下文继续展开。四、展开思想对于对象能力的定义,我们一般可以从重要的入手,然后慢慢地展开,把所需要的其他能力逐渐归纳为函数,从而把面向对象的思想发展下去。上文我们提到了三个函数是怎么来的,还没有涉及到函数的任何实质,那么本帖就探讨一下这三个函数的实质性规划与设计。有了功能要求,我们就要实现它,在裸程序中,实现它的一个首要任务,就是要进行数据传递方式的设计。很显然我们必须要有一个显示区域,来存放我们所要显示的内容,以及显示内容的显示属性,我们还要规划这个显示区域到底要显示多少多少字符或者是点阵。但是由于我们事先并不知道我们的显示设备一次会提供多少显示量,所以我们可以把显示区域的内存,也就是显存,定义得大一点,以至任何一款符合设计要求的显示器都能得到满足,这样的做法在裸编程中其实还是比较实用的,因为裸编程中我们很少去申请动态的空间,程序设计完,所有的变量位置皆已确定,行就行,不行编译就过不去,所以我们可以通常选择一些内存资源比较丰富的新款单片机。但是这样的做法也有一个弊端,比如当我们预先估计不足而导致数据空间不够的时候,我们就得从头来改这个显存的大小,从而导致整个显示程序都要相应的产生一些变动,这还不是最糟糕的,最糟糕的是当一款新的显示器因为新的功能需求而导致数据结构需要发生变化的时候,我们就崩溃了,前期的工作可能改动就非常大,甚至于都要重新过一遍,也就是重写重调,这么痛苦的事情,我是最讨厌的了。所以我们要尽量避免这类事情发生,这里对面向对象的思想,就颇为需求了。这个时候,我们就要引入一个新的概念,那就是对象的儿子,子对象。前面讨论的,其实都只是一个抽象的对象,没有任何具体的样子,而只是笼统的规划了所有的显示器必须具有什么能力,而对于每一个具体的显示器来说,还没有任何具体的设计,在这里,每一个具体的显示器,就是显示器对象的子对象,他们形态各异,但是都必须能完成规定的功能。以传统的OOP语言理论来说,这里就产生了一个继承的关系,但是在裸程序思想里,我并不赞成引入这个概念,因为传统的OOP语言里的继承,纯粹是一个语法上的逻辑关系,继承关系明确,而裸程序中的这个思想,并没有任何语法支持,继承关系就非常微弱了,还不如说是归类与概括。但无论是什么关系,我还是不想就这种一目了然的关系弄个新名词来,让看的人费解。既然引入了子对象,我们能看出这种做法有什么实际意义吗?也许有经验的资深程序员能看出来。我们在做父对象数据设计的时候,我们并不规定具体的数据格式和显存大小,而是一股脑儿地全推给子对象自己去搞,父对象什么都不管。哈哈!这样做事情真是很简单吧?不是我的事情我不管,不要说我偷懒,因为站在父对象的角度讲,这是最明智的做法,因为管不了,所以不管。到这里也许就会产生更多的疑问了,一个对象什么都不管,那作为调用者怎么使用这个对象呢?你想用它,它什么都不管,这怎么行呀?别着急,父对象不管的,是那些具体的事情,抽象的事情,还是管的,要不然它就没有理由存在了。你抱怨了,说明你在思考,既然思考了,就把思考的问题提出来,提出来的,就是我们设计父对象的依据。提问题,我想这比搞程序要简单得多,比如:显示器能显示多少乘多少的字符?颜色是多色还是单色?显示模式是否支持预定的方式(如移动、闪烁等)?工作模式是图像还是字符?等等,这里附加说明一下,对于显示模式,我们这里都以字符显示为例,既然是面向对象的思想,相信扩充出图像显示模式,还是很容易的事情。有问题出来了,我们就继续为它添加代码好了。dispGetMaxCol();//取一行最多有多少列dispGetMaxRow();//取显示器一共有多少行dispGetMaxColors();//取显示器最多有多少色dispSetShowMode();//设置显示的方式,对于不支持的显示方式就自动转为正常显示dispSetWorkMode();//设置工作模式,如果没有的模式就返回0,支持的就返回1
2022年1月15日