SoC嵌入式软件架构设计

ARM 129浏览

内存是SoC(System on Chip,片上系统)集成设计的重要模块,是SoC中成本比重较大的部分。内存管理的软硬件设计是SoC软件架构设计的重要一环,架构设计师必须要在成本和效率中取得平衡,做到在节省内存的同时保证整个系统的性能。系统内存需求评估是对嵌入式软件架构师的最基本要求,同时也是其最重要的技能之一。一般在SoC项目立项的时候,架构师就要完成系统内存需求评估。

    下面以一个多媒体电子解决方案中的SoC设计为原型,说明大致的评估流程:

    1. 根据产品规格,对各个应用场景进行功能和性能分解

    产品规格一般会描述应用功能场景和性能。架构师要对各个场景进行功能和性能分解,分析各个场景在内存使用上的关系。包括:

    1)列出所有的应用场景,明确各个应用的生命周期,在什么时候开始,什么时候结束。

    2)系统是否要同时支持多个应用(多进程),例如听歌曲的时候要浏览图片,这意味中两个应用是同时利用内存,不能进行应用内存分时复用;

    3)系统是否要同时支持多种介质,例如同时访问卡设备和闪存设备,一般在单进程时都只是访问单种存储设备,除非是实现数据复制,但在多进程的时候,不同的进程访问不同的存储设备也很正常,同时访问不同的存储设备意味着两种驱动是同时使用内存。

    4)系统是否要同时支持多种文件系统。不同的存储设备可能部署不同的文件系统,其同样存在2)中的问题。

    5)明确系统支持的编解码格式,其表现为算法内存需求。不同的编解码格式对内存的要求不同的,同样的算法时,不同的速率也导致不同的内存需求。

    6)系统性能要求,例如LCD刷屏,有大块framebuffer自然会有更好的性能。 


    2. 对系统软件进行分层,明确每一层模块的组成     

    1)系统分启动、驱动、操作系统、文件系统、中间件(算法、UI)、应用框架、应用等层次,一般的消费类电子产品,如多媒体设备、游戏机等产品系统都会分成多个层次。每个层次又会有多个模块组成,如驱动分字符设备驱动和块设备驱动,一般按键属于字符设备,存储设备一般属于块设备;存储设备里可能支持nand flash、SD-MMC card、Uhost等;文件系统又有FAT32、exfat等等;应用层当然会包括很多个应用程序。

    2)明确每个应用实现所需要的软件层次。有些应用可能要很多层,如音乐,从应用到应用框架(UI+按键)、API、中间件(解码)、操作系统、驱动等层次,而设置应用是不需要解码中间件的。

        3. 明确每个软件层次中内存分时复用的模块,找出最大内存需求的模块

如《节省内存的软件设计技巧》这篇文章提到,应用、驱动、中间件、数据段都存在着复用的需求。要在2)中的各个软件层次中区分各个不同的组成模块,明确各个模块是否能够进行分时复用。在复用的情况下,找出最大内存需求的模块,如nand
flash驱动要比card驱动复杂,那nand flash驱动的内存需求自然要高;而音乐应用自然要比设置或者FM等应用要复杂,其内存需求自然也要更多。

    4.对最大内存需求模块的代码进行分析,大致明确其常驻内存代码和分块(bank)管理的代码。

    常驻代码段一般是调用频繁的、性能要求高的代码段,如中断管理、消息管理等。一般应用中大量的代码是可以按需加载执行的,如音乐的音效管理和音量设置这些功能代码并不要求很高的执行性能,其是可以分时加载到内存执行,能够达到内存分时复用的目的。

    5.确定各个软件层次的常驻代码空间和分时复用内存的空间

    在成本的要求下尽可能减少常驻代码空间,会导致代码执行性能的降低,因为bank代码执行前要先进行加载,一般是从nand flash或者card中读取;在成本的要求下我们也想尽可能减少bank代码复用的内存空间,同样会导致bank代码切换频繁而降低性能,因此也不能一味地减少内存,而是仔细分析各个子模块的功能和性能对内存的大致要求。例如两个子模块的函数实现是8k和4k,那我们可以考虑2K的复用空间,即前者分成4个bank,后者2个bank,是否能够达到性能;如果复用空间设置为4K,效率会高一些,但成本会增加;如果设置为1k,那前者就会有8个bank,切换次数过多。

    6.明确可以固化的代码空间

    应用的常驻代码是不能固化的,因为不同的应用都需要常驻代码,即其是变化的,而像操作系统的中断管理、时间管理、任务调度管理等代码一般是不变的,其可以固化到ROM中,这样能达到节省内存的目的。

     7.考虑其他特殊的需求

    通过6,我们可以大致得到整个系统的内存需求。这时要考虑一些特殊的场景的内存需求,看之前制定的内存是否能够满足这个场景。例如启动阶段的内存需求分布,OS引导初始化时的内存要求等。这些并不是产品的规格,同样是架构设计师要考虑的。

     一般会对6中得到的内存再次评估,以进行细微的调整。

程序的大部分代码都可以在必要的时候才加载到内存去执行,运行完后可以被直接丢弃或者被其他代码覆盖。我们PC上同时跑着很多的应用程序,每个应用程序使用的虚拟地址空间几乎可以整个线性地址空间(除了部分留给操作系统或者预留它用),可以认为每个应用程序都独占了整个虚拟地址空间(字长是32的CPU是4G的虚拟地址空间),但我们的物理内存只是1G或者2G。即多个应用程序在同时竞争使用这块物理内存,其必然会导致某个时刻只存在程序的某个片段在执行,也即是所有程序代码和数据分时复用物理内存空间—这就是内存管理单元(MMU)工作核心作用所在。

       处理器系列的芯片(如X86、ARM7以上、MIPS)一般都会有MMU,跟操作系统一块实现虚拟内存管理,MMU也是Linux、Wince等操作系统的硬件要求。而控制器系统的芯片(面向低端控制领域,ARM1,2,MIPS M系列,80251等)一般都没有MMU,或者其只有单一的线性映射机制。

       本文要谈的是控制器领域SoC的内存管理单元的硬件设计,其重要的理念同样是代码和数据分时复用物理内存空间,在保障系统功能和性能的基础上最大限度地节省物理内存的目的。相关的文章包括:SoC软件架构设计之一:系统内存需求评估节省内存的软件设计技巧

 

一、内存管理单元(MMU)的工作机制

在阐述控制器领域的内存管理之前,还是要先介绍处理器领域的虚拟内存管理机制,前者很大程度上是对后者核心机制精髓的借鉴。实现虚拟内存管理有几个模块是协调工作的:CPU、MMU、操作系统、物理内存,如图示(假设该芯片系列没有cache):


       我们根据上图来分析一下CPU访问内存的过程,假设寻址是0x10000008,一页大小为4K(12比特)。则虚拟地址会分成两个部分:页映射部分(20bit,0x10000)+页内偏移(12bit, 0x8)。CPU通过总线把地址信号(0x10000008)送给MMU,MMU会把该地址的页映射部分(20bit)拿到TLB中匹配。

TLB是什么东西?Translation Lookaside Buffer,网上有称为“翻译后备缓冲器”。这个翻译都不知道它干什么。它的作用就是页表的缓冲,我喜欢叫它为页表cache。其结构图如下:


       可以想象,TLB就是索引地址数组,数组的每个元素就是一个索引结构,包含虚拟页地址和物理页地址。其在芯片内部表现为寄存器形式,一般寄存器都是32位,实际上TLB中的页地址也是32位寄存器,只不过索引比较时是比较前20bit,后12bit其实也是有用的,例如可以设置某个bit是表示常驻的,即该索引是永远有效的,不能更换,这种场景一般是为适合一些性能要求特别高的编解码算法而设计的。非常驻内存的一般在某个时刻(如TLB填满时访问一个新的页地址)就会发生置换。

1) 假如0x10000008的前20bit在TLB中第M个索引中命中,这时就表示该虚拟页在物理内存中已经给它分配好对应的物理内存,页表中也已经做好记录。至于虚拟地址对应的代码页是否从外存储(flash,card,硬盘)的程序中加载到内存中还需要要另外的标记,怎么标记呢?就是利用上面所讲的TLB低12位的某一bit(我们称为K)来标识,1标识代码数据已经加载到内存,0表示还没加载到内存。假如是1,那就会用M中的物理地址作为高20bit,以页内偏移0x8作为低12bit,形成一个物理地址,送到内存去访问。此时该次访问就会完成。

2) 假如K是0,那意味着代码数据尚未加载到内存,这时MMU会向中断管理模块输出信号,触发一个中断进行内核态,由操作系统负责将对应的代码页加载到内存。并修改对应页表项的K比特和TLB对应项的K比特为1.

3) 假如0x10000008的前20bit在TLB所有索引中都没有命中,则MMU也会向中断管理模块输出一个信号触发中断进入内核态,由操作系统将0x10000008右移12位(即除以4K)到页表中去取得对应的物理页值,假如物理页值非0有效,说明代码已经加载到内存了,这时将页表项的值填入到某一个空闲的TLB项中;假如物理页值为0,说明尚未给这个虚拟页分配实际的物理内存空间,这时会给它分配实际的物理内存,并写好页表的对应项(这时K是0),最后将这索引项写入TLB的其中一条。

2)和3)其实都是在中断内核态中完成的,为什么不一块做了呢?主要是因为一次中断不应该做太多事情,以加大中断延时,影响系统性能。当然如果有芯片将两者做成一个中断也是可以理解的。我们再来看看页表的结构。页表当然也可以按TLB那样做成索引数组,但是这样有两个不好的地方:

1)页表是要映射所有的虚拟页面的,其维护在内存中也需要不小的空间。页大小是4K时,那映射全部就是4G/4K=1M条索引,每条索引4*2=8个字节,就是8M内存。

2)假如按TLB那种结构,那匹配索引的过程就是一个for循环匹配电路,效率很低,要知道我们做这个都是在中断态完成的。

所以一般的页表都是设计成一维数组,即以整个线性虚拟地址空间按页为单位依次作为数组的下标,即页表的第一个字(4字节)就映射虚拟地址空间的最低4K,第二个字映射虚拟地址最低的第二个4K,以此类推,页表的第N个字就映射虚拟地址空间的第N个4K空间,即(N-1)*4K~4KN的地址空间。这样页表的大小就是1M*4=4M字节,而且匹配索引的时候只是一个偏移计算,非常快。

 

承前启后,在引出第二部分之前先明确两个概念:

1. Bank表示代码分块的意思,类似于上面提到的页的概念。

2.不同代码分时复用内存:不同代码即意味着不同的虚拟地址对应的代码,(程序链接后的地址都是虚拟地址),内存即物理内存,即一定大小的不同虚拟地址的代码在不同的时刻都跑在同一块一定大小的物理内存空间上。每一块不同的代码块即是不同的代码Bank。 

 

二、控制器领域SoC内存管理单元的硬件设计

       这里专指没有内存管理单元的SoC设计,一般为了降低成本,在性能足够时,如果16位或者24位字长CPU能够解决问题,一般都不会去选32位字长的CPU,除非是计算性能考虑,或者32位CPU的license更便宜(一般很少见)。只要能够达到高效地进行内存管理,实现物理内存分时复用的目的,那都可以称为是成功或者有效的。在介绍真正的内存管理单元硬件设计之前,我们先简单介绍一种利用工具链来实现内存分时复用的机制,然后再结合MMU和这个工具链实现的分块处理方法去设计我们新的内存管理单元,包括其硬件工作机制和软件设计和关键机制。


        由于以下内容涉及到在审专利,经过考虑,暂时将以下内容隐藏,适当时候再公开,抱歉!

×××××××

后记补充

         在集成没有MMU的CPU时,SoC要实现内存管理,需要另外设计一个内存管理模块,实现MMU的核心功能,即代码分页(块)映射的功能,而且需要简化设计以达到最高的效率,同时代码分块需要直接地体现在链接脚本上。为了追求效率,编译链接后的可执行性文件还会被离线解析组织成一个更简化的执行文件,把不需要的段都删除,并将分块代码按逻辑顺序放好,以便于操作系统在必要时更快地加载。当然,操作系统的代码内存管理也需要配合内存管理硬件电路,并能够解析重新打包后的执行程序文件。因此内存管理的实现是需要架构师从软件和硬件上全面考虑,尽可能地在实现核心功能的基础上简化电路和设计,涉及的模块包括:硬件机制设计、物理内存分配、代码分块原则、linker脚本定义、打包执行文件、操作系统定制等等。后续阐述的架构设计将会包括以上内容。

第三:

上一节讲述了在没有MMU的CPU(如80251、MIPS M控制器系列、ARM cortex m系列)上实现虚拟内存管理的集成硬件设计方法,新设计的内存管理管理单元要实现虚拟内存管理还需要操作系统、代码分块(Bank)的支持,详见SoC嵌入式软件架构设计之二:没有MMU的CPU实现虚拟内存管理的设计方法这里要阐述Bank设计的一些原则。

    Bank设计是为了实现不同时刻运行的Bank(代码块)运行在同一块内存上,所以在运行之前操作系统需要将已存在内存的代码/数据进行缓存处理,并加载将要运行的Bank到该内存上。为了实现这个目的,需要明确以下要点:

     1.为了提高效率,我们认为代码是不会自修改的,即代码是只读的,则在Bank切换的时候可以直接将已经存在内存的Bank代码丢弃。我们只需要将当前已经存在内存的Bank代码的Bank号入栈即可,新加载的代码可以直接覆盖该块内存。不同的Bank有不同的虚拟地址,为什么可以放到同样的物理内存?其实是新设计的内存管理单元的电路决定的。参考前一节的文章(SoC嵌入式软件架构设计之二:没有MMU的CPU实现虚拟内存管理的设计方法)介绍,关键是同一个Bank组的不同虚拟地址信号对应的物理输出信号是一样的。

     2.程序调用后返回到一个Bank的某一行时同样需要加载该Bank代码,这时操作系统会将之前的Bank号出栈,并根据Bank号将对应的代码加载到该块内存。从1和2来看,调用Bank代码和返回一个Bank设计到Bank号的入栈和出栈,如果设计的Bank代码中的函数的虚拟运行地址带有明确的Bank号信息,那函数的调用和返回就是一个入栈和出栈过程,这样操作系统可以减少出入栈的工作,代码运行也更顺畅。

     3.Bank代码中的变量数据处理:

     1)全局变量。如果全局变量定义在公共区域,那Bank代码切换过程中不需对其进行处理。如果全局变量定义在Bank内存区域,则Bank切换时需要对这部分全局变量进行缓存处理。即在Bank号入栈之后,将Bank中的数据存到堆中,在Bank返回时除了从外存储设备加载对应的代码时,还要将其对应的数据从堆中恢复到Bank内存。为了加快数据的恢复,往往默认一个Bank数据空间的最大值,这样就不需要记录每个Bank的数据空间的大小。

     2)静态变量。跟全局变量一样。

     3)常量段。其是只读,跟代码一块处理。

     4)局部变量。局部变量是在栈中分配空间的,所以不需要进行缓存。

     5)buffer。假如该Buffer只是某个Bank调用,而该Bank除了代码还有剩余空间大于buffer大小,那将buffer设置在代码段之后,并定义一个指针局部变量,程序中直接指向该buffer的首地址。

     如果我们将Bank内的全局变量全部转为局部变量,那操作系统就不需要对数据进行缓存管理,就不需要堆空间。但是局部变量对应的栈空间就加大了。一个Bank可能有多个函数,而多个函数是可能会用到同样的全局变量的。但这种情况需要的全局变量往往不大,可以考虑都转为局部变量。如果不需要进行数据缓存,那系统管理将会非常简单。

     4.中断处理不能进行Bank切换。Bank切换需要进行读写外存储设备,会造成很大的延时,所以在中断里面不应该产生Bank切换。

     5.操作系统、驱动、应用各层次频繁调用的代码应设置为常驻代码,如果发生切换会损失效率。如果频繁调用的代码很固定,如操作系统的调度管理等代码可以固化到ROM中,以减少成本。

     6.Bank内存分块大小要适中,在保持切换性能的基础上选择较小的内存块。Bank块设置过小,就会导致Bank切换频繁,损失效率,Bank设置过大会造成内存浪费。

     7.Bank内存的起始地址应该对齐扇区(512字节),这样读外存储设备能够达到最好的性能。

本文继续阐述基于低端控制器CPU的SoC固件架构设计。第一节 SoC嵌入式软件架构设计之一:系统内存需求评估 讲述了系统内存需求的评估。这一节讲述内存空间的具体规划分配。CPU有两种体系结构:哈佛结构和冯诺依曼结构。哈佛结构是一种将程序指令存储和数据存储分开的存储器结构,如80251,代码空间与数据空间完全分开,独立编址;冯诺依曼结构是一种将程序指令存储器和数据存储器合并在一起的存储器结构,如MIPS,ARM等,其代码和数据空间是统一编址。这里就以冯诺依曼体系结构为例。 

     一、嵌入式系统软件分层

     系统软件层次包括:启动、驱动、操作系统、文件系统、libc、中间件、应用框架、应用等层次。

     1)驱动、文件系统和操作系统的时间管理、中断管理等接口一般都是通过API来进行调用;

     2)libc和中间件、应用框架在系统中的处理可能以API的形式进行调用,也可以直接作为静态库与应用直接进行链接。

     3)libc和中间件、应用框架作为静态库时,会减少API的占用空间(API往往是常驻空间,没理由调用API时还要从外存储中将API的代码加载到内存,这样效率太低),省去API层也可以提高调用速度,但会增加库函数的代码空间。如果库函数链接时可以运行在Bank内存中,由于Bank内存可以复用,增加的代码空间可以忽略,从这一点来看其又是一个优点。如判断某个文件是哪种解码格式时,其可以作为中间件来实现,并链接到应用的Bank空间,因为这是音乐解码前的预处理,可以和解码时刻的控制流复用同一块Bank空间。

     4)libc和中间件、应用框架以API形式来调用时,会产生API的常驻内存空间需求,在内存中也只存在一份真正的代码,供所有模块共同调用,而且应用开发者无需关心接口实现,也不允许开发者去修改。

     各个模块应根据实际情况来决定其供上层调用的形式。

     代码分页(块,Bank)设计请参考: SoC嵌入式软件架构设计之二:没有MMU的CPU实现虚拟内存管理的设计方法 和 SoC嵌入式软件架构设计之三:代码分块(Bank)设计原则

     二、程序段组成

     这里程序段是指可执行文件中出现的段名,如.CODE、.DATA、.BSS等默认段名和其他自定义的段名。GNU工具链,各种编译输出段名称是可以在链接脚本中指定的,当然在编写代码时也可以指定函数或者代码的编译输出段名称,如在定义一个数据变量时添加一个属性__attribute__((section("bank_data")))时,该数据变量将会被重定位在bank_data段。下图是具有Bank代码段的程序与可执行文件段名的对应关系图:


             三、SoC内置内存规划

         一般地,如果SOC中内置SRAM超过32K,数字工程师也会将内置内存进行分块,一是为了减少电路延时,二是为了让内存得到更有效率的利用。如某块内存在某个时刻是作为代码使用,有时也可能作为数据使用(如果是哈佛结构,那就要切换内存的选址译码电路,从代码空间转到数据空间),有时也可能用作特别的解码buffer使用,而有些解码的缓存是以24bit作为单位,如果所有内存都作为一块来设计,显然是满足不了这样的需求的。下图是常见的SRAM示意图:


                 四、程序内存空间分配

            根据软件分层和程序段综合考虑,一般在物理内存的基础上先进行分层划分内存区域,再进行各层程序的段内存划分。有以下原则:

            1)各层的常驻段(代码和数据)应该紧凑分配,而各层的Bank空间与常驻空间分块,也应该紧凑分配。

            2)Bank空间的起始地址应该与扇区单位对齐,可取得最好的加载代码性能。

            3)先把常见的场景的内存分配好,再考虑特殊场景的需求,看看特殊场景能否复用普通场景的内存空间。

            4)buffer的划分也要考虑场景的复用,否则太浪费。如解码的buffer可以在未解码的时候用作预处理时的媒体文件有效性判断的buffer。

            5)有时两组Bank空间可以合并起来当作另一个场景的一组Bank空间来使用。如解码时的软件分层比较多,涉及到应用中间件和算法中间件,而文件浏览应用则没有这么多层次,可以将两个中间件的Bank合并起来当一组Bank来使用。

            6)一个模块的代码不应该跨越两个物理内存块,否则访问性能会降低。

            7)尽可能提高内存利用率,避免内存碎片。

            8)内存分配的细节要以公共链接文件出现,并用有意义的名称来定义各段的起始地址和长度,除架构设计师外,其他人不允许修改该文件。

            下图是一个系统的局部分配,程序内存空间分配大致如此:Rcode是常驻段,Bank是复用内存的代码块。

低端控制器对执行效率要求很高,成本敏感,因而SoC内置SRAM是紧缺资源。代码分块管理就是为了充分利用内存,提高内存的复用效率而提出的一种设计方法。代码分块管理不仅涉及到硬件,同样对操作系统和应用、驱动的设计都有要求,这些模块共同努力以使执行效率达到最高。

          本节讲述代码分块(Bank)管理思想下可执行文件的重构,即对程序编译后的可执行文件进行重新组织、打包,以在加载阶段获得最高的执行效率,减少内存占用。要使执行效率高,意味着可执行文件的格式尽可能简单,解析执行文件的流程简单,相应地,解析过程代码量少,即能够减少内存的占用。

          keil产生的Hex或者Bin,其实也是keil对axf调试文件进行抽取,重新打包生成。Hex和Bin可以通过烧写器下载到flash。GCC工具链会生成可执行格式ELF,也可以离线对ELF格式内容进行抽取,重新打包生产Hex格式,并烧写到flash中。本次讲述的是可执行文件存储在外存储设备(card,nand flash,硬盘)中,在运行前由操作系统将其加载到内存再执行这种情况。

          要对工具链生成的可执行文件(如ELF)进行重构,自然需要熟悉该执行文件的格式,如ELF,里面会包括文件头,SECTION节,符号段,代码段,数据段,还有调试信息等等。而程序的运行往往只需要其中较少的段信息。可执行文件就是要得到必要的段的信息和数据,将不必要的通通删除,并且根据代码分块管理的特点进行自定义格式的编制。这里不再对ELF等标准执行文件进行分析(以后可能另写文章),只描述重构后的目标格式,看看什么段信息和数据是操作系统必要要知道和获取的。

            1. 文件头

            1)格式标识符,如应用,驱动等

            2)版本号

            3)常驻代码段在文件中偏移

            4)常驻代码长度

            5)常驻代码段在内存中的起始地址            

            6)常驻数据(.DATA)段在文件中偏移

            7)常驻数据(.DATA)长度

            8)常驻数据段(.DATA)在内存中的起始地址

            9).bss段长度

            10).bss段在内存中的起始地址

            11)程序入口地址ENTRY:第一行代码的地址,不是main哦,是运行时库的一行代码地址

            12)reserved,对齐到扇区或者1024字节。如果是驱动类型,则会增加两个字段:驱动入口地址和驱动退出地址。             

             2. 代码块头

             1)第一个Bank组的头,按顺序是第一个Bank的块信息、第二个Bank的块信息、。。。第N个Bank的块信息。一个Bank组最大含有N个Bank,不够的填0.

             2)第二个Bank组的头,按顺序是第一个Bank的块信息、第二个Bank的块信息、。。。第N个Bank的块信息。一个Bank组最大含有N个Bank,不够的填0.

             .....

             有几个Bank组由文件头的格式标识符来判定,由架构师事先约定。

             每个Bank块的块信息包括:bank块数据(代码和数据)在文件中的偏移,Bank长度,BAnk对应的内存地址。


    3.常驻代码段数据,对齐扇区

    4.常驻数据段数据,对齐扇区

    5.第一组Bank的数据,对齐扇区,只记录真实的Bank数据,一个Bank组不够N个bank时不需补0.

               6. 第二组Bank的数据,对齐扇区,只记录真实的Bank数据,一个Bank组不够N个bank时不需补0.

               7. 最后一个组的Bank数据。


         加载时解析该文件头,获得常驻代码段和数据段的信息,将两者加载到内存,对Bss段清0,然后读取到程序的ENTRY入口,跳到该地址执行即可,简单高效!

         Bank切换时,根据Bank组号和Bank号即快速定位到Bank的段信息地址,读出该Bank在文件中的偏移和Bank长度,进而读取并加载到复用的内存空间。    

现在新买的安卓千元机都是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%左右的代码量。

     十、有待你的评论,希望能看到更多的总结建议,谢谢!

增加两点:

1. 栈空间的规划

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

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

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

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

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

  • 百分百原创,资深研发工程师分享----嵌入式、Linux、物联网、Android开发技术,欢迎扫码关注!