节省内存的嵌入式软件设计技巧

     现在新买的安卓千元机都是2G内存的了,我们还要绞尽脑汁地省内存?是的,那是高端处理器的特色,咱们这里讲的是资源紧缺型的嵌入式系统设计方法。一般主控是单片机控制器的电子产品的成本跟内存的关系可是成正比的,尤其在SOC芯片设计时是固件开发需要重点关注的。大量量产前要确定内置SRAM的大小,而且是在满足功能需求的情况下越小越好。这就需要考究软件系统的设计和编程开发的技能了。这里仅就我个人的工作经验来总结,涉及的是音视频多媒体电子产品,类似系统一般都会自行定制操作系统,驱动、中间件和应用等模块都有,所谓麻雀虽小,五脏俱全。

        一、内存块分时复用

         分时复用即对代码进行分块(Bank)管理。它的设计需求来源于:

       1. 很多电子产品并不是像现在的安卓手机一样同时跑多个应用,顶多就听歌时浏览图片而已,非智能手机也是如此。但我们也会看到电子产品里面有有很多的应用,如设置、电子书、电话本、录音啊等等。因此,不同时运行的应用占用同一块内存空间理所当然。

       2. 驱动空间。有很多的驱动并不同时使用,如听FM时是FM驱动,听歌又是使用解码器,所以很多驱动也是可以服用同一块空间的。

       3. 中间件的复用。如UI、硬件驱动的再次封装使用等等,其由对应的应用直接调用,一般也存在复用的需求。

       4. 数据段的复用。应用和驱动都有数据,同样有复用的场景需求。

       理论上驱动和代码也可以服用空间的,但需要考虑的细节太多,而且这样做扩展性不好,所以应用一般是不会跟驱动复用空间的。一般较为粗糙地将软件系统分为以下几个部分:启动、驱动、操作系统、中间件、应用等层次。启动为一次性执行,不需太多考虑复用的空间。操作系统一般有常驻内存的需求,如中断管理、时间管理、调度管理、模块代码管理、虚拟文件系统等等,当然操作系统的一部分功能并不需要常驻内存,主要是一些调用频率较小的一些接口,如驱动装卸载、应用初始化等模块。不需常驻内存的一些接口实现也可以跟驱动复用空间。     

         咱们不妨比较一下高端 处理器的内存管理单元的功能,内存管理单元实现内存管理有两个部分,包括硬件TLB模块和软件的页表,硬件TLB是自动将虚拟地址的高N位匹配成物理内存的高N位,匹配是根据页表(TLB是页表的cache)进行。可以认为页表是虚拟-物理映射的索引表。高端处理器一般所带的内存都是M级别以上,往往是SDRAM,而不是内置 SRAM了。一般也会用支持多进程的操作系统,即同时支持多个应用在跑。而这多个应用能够使用的虚拟地址空间和物理地址空间都是整个空间(可能会划掉一部分用于操作系统,linux就是这样),也就是其在整个地址空间中进行分时复用。 而我们上面所讲的代码分块管理只是参考了MMU的设计思想,其分块是在一定的空间中进行的,而且应用和驱动的分块空间是分开的。 

       二、代码分块的技巧

       第一点是分块管理的需求和大致的原则,但是如何分块,块大小的设计极为考虑系统架构师的功力。块设大了浪费,小了会导致代码切换频繁效率低下。既然都是RAM,有时数据可以跟代码段放到同一个块中,而没有必要另加一块数据块。当然这些细节需要综合评估并加以详细设计。在成本敏感的电子产品中,这些技巧需要努力挖掘发挥。

       三、ROM代替RAM

       这只是从成本的角度去节省内存资源,有些代码需要常驻内容,但其内容并不会随着版本的更新而发生变化,如上节所讲操作系统的调度管理等,可以考虑将这些代码固化到ROM中。理论上操作系统大部分需要常驻内存的代码都可以固化。RAM和ROM同样大小的成本比大概是6:1,因此使用ROM也能大幅降低成本。

       四、系统移植时砍掉不需要的模块

       这是操作系统设计人员务必要考虑的。每个产品都有独有的功能,而底层操作系统具有普适性,在资源紧缺型系统中,砍掉不必要的模块是非常明智的。

       五、操作系统定制

       也可以称为改进操作系统,我们所阐述的系统一般都是封闭系统,只要能高效地实现功能,我们可以任意改动系统中所有的代码。例如对于可执行的ELF文件,操作系统如果按标准的流程要解析完ELF文件再加载,但不仅需要很多的内存资料,而且也效率低下。ELF有关加载和执行最重要的就是.CODE、.DATA、.CONST、.BSS等段信息,我们完全可以离线抽取出来生成一个新的简单的定制文件格式,操作系统只需解析这个简单的文件就可以了。这样做不仅节省内存,也能节省外存储空间。

       六、 编程技巧

       这个需要平时的积累。例如,在变量的排列方面,我们都知道编译会考虑对齐。

       char a;int b; char c;这样定义变量的次序需要的内存是比 char a; char c; int b;要大的。

      七、算法设计

       好的算法一般会是轻巧的,效率高的。

      八、代码编译优化

       编译时选择优化级别高的,这样生成代码大小有有大规模的减小。

      九、编译指令模式

      如arm里面选thumb指令,mips选择mips16e,这是由体系结构所决定的,体系结构也是为了考虑节省代码空间资源而设计了16位的指令模式,而这些CPU的字长往往是32位。这种方式能减少30%左右的代码量。

  十、 栈空间的规划

         每个线程都会有自己的栈,而每个线程的栈都应该根据其线程的调用深度来具体设定,像UCOS就有一个栈使用率的任务,我们不妨借用这种思路来看看某个线程最终的栈深度。

        设定独立的中断栈,可以避免每个任务栈都要给中断预留栈空间。

        扁平的函数调用方法用栈一般要比纵向的函数调用小。嵌入式开发有时为了效率和资源,不应该把代码分块分得太细,函数一大摞,既增加代码量和栈,也降低运行效率。

       十一、合理规划内存空间,调整好链接文件,尽可能做到已有物理空间的高度利用。

       例如,我们规划空间时往往代码段和数据段分开,但实际的代码段可能又用不完,这时就可以把一部分变量定位到代码段之后。

      十二、善于利用链接的段属性

       利如uboot的命令格式,每个命令格式都是一个数据结构cmd_tbl_s,有名称、执行函数、帮助提示信息等等。考虑到命令的管理,我们自然会想到结构数组,但是数组的大小怎么设置呢?设置大了浪费,设置刚刚够,那增加一个命令又得改大小。uboot巧妙地运用了链接的段属性,只要是命令就加上section (".u_boot_cmd"),那所有命令自然就放到这个段里面了,需要查询命令就遍历这个section就可以了。linux里面大量应用了链接段属性技术。

 

posted @ 2014-09-30 10:53  吴跃前  阅读(1656)  评论(0编辑  收藏  举报