SoC知多少
SoC是System on Chip的缩写,直译是“芯片级系统”,通常简称“片上系统”。因为涉及到“Chip”,SoC身上也会体现出“集成电路”与“芯片”之间的联系和区别,其相关内容包括集成电路的设计、系统集成、芯片设计、生产、封装、测试等等。跟“芯片”的定义类似,嵌入式SoC更强调的是一个整体,在嵌入式应用领域,给它的定义为:由多个具有特定功能的集成电路组合在一个模块上形成的系统或产品,其中包含完整的硬件系统及其承载的嵌入式软件。
内存是资源紧缺型系统SoC(System on Chip,片上系统)集成芯片设计的重要模块,是SoC中成本比重较大的部分。内存管理的软硬件设计是SoC软件架构设计的重要一环,架构设计师必须要在成本和效率中取得平衡,做到在节省内存的同时保证整个系统的性能。系统内存需求评估是对嵌入式软件架构师的最基本要求,同时也是其最重要的技能之一。一般在SoC项目立项的时候,架构师就要完成系统内存需求评估。
嵌入式SoC有两个显著的特点:一是核心硬件设计难度大;二是软件比重大,需要进行软硬件协同设计。举个例子,比如城市相比农村的优势很明显:配套齐全、交通便利、效率高。
嵌入式SoC也有类似特点:在单个模块上集成了更多配套的电路,节省了集成电路的面积,也就节省了成本,相当于城市的能源利用率提高了;片上互联相当于城市的快速道路,高速、低耗,原来分布在电路板上的各器件之间的信息传输,集中到同一个模块中,相当于本来要坐长途汽车才能到达的地方,现在已经挪到城里来了,坐一趟地铁或BRT就到了,这样明显速度快了很多;城市的第三产业发达,更具有竞争力,而嵌入式SoC上的软件则相当于城市的服务业务,不单硬件好,软件也要好;同样一套硬件,今天可以用来做某件事,明天又可以用来做另一件事,类似于城市中整个社会的资源配置和调度、利用率方面的提高。
可见嵌入式SoC在性能、成本、功耗、可靠性,以及生命周期与适用范围各方面都有明显的优势。
嵌入式SoC设计思想
在嵌入式系统综合设计技术中,系统设计往往非常复杂,一般需要学习和了解非常多的技术相关知识,且需求、设计、修改一变则设计重来,牵一发而动全身。
模块化设计是嵌入式SoC的基础设计思想,嵌入式SoC技术设计应用电子系统的基本设计思想就是实现全系统的模块化设计。用户只须根据需要选择并更换各部分模块和嵌入结构,就能实现所需要的目标,而不必花时间熟悉特定的电路开发技术。模块化设计的优点就是系统能更接近理想系统,更容易实现设计目标。
嵌入式SoC设计重用技术
嵌入式系统应用越来越复杂化,且底层硬件驱动开发关系到整个系统的稳定性,如果从最底层的寄存器操作开始,一步一步地构建整个开发平台,必须投入大量的资金、人员和时间才有可能保证系统的可靠性,因此复杂的嵌入式产品推荐不要一切从头开始,要将设计建立在较高的层次之上。将更多的模块技术复用,只有这样,才能较快地完成设计,保证设计成功,快速满足市场需求。
嵌入式SoC设计再利用是建立在核心模块(CORE)基础上的,它是将己经验证好的复杂嵌入式软硬件系统,以便后续的设计利用。嵌入式SoC通常由两部分组成,一部分称为硬件模块化,具有复杂的,高性能的嵌入式处理的最小系统和特定功能,并被稳定性验证过,可被新设计作为特定的功能模块直接调用。
另外一部分是固件(firm core),是在固件的基础上开发的,如图5 1所示,开发工程师告别操作寄存器的开发模式,不需要了解ARM硬件功能,只需调用底层硬件驱动程序、OS、GUI、FAT文件管理系统、TCP/IP协议栈、CAN-bus高层协议......等固件的API函数,即可快速地开发出一个稳定、可靠的产品,这就是嵌入式SoC设计重用技术所要实现的目标。
嵌入式SoC流程
下面以低端多媒体电子(如bombox、点读学习机、录音笔等)解决方案中的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中得到的内存再次评估,以进行细微的调整。
内存管理单元的软、硬件协同设计
软硬件整合一直是SOC芯片设计的核心技术,由系统软件架构师和芯片系统设计人员共同评估、设计SOC的各个模块,以性能、成本、软件编程灵活性、软件扩展性等作为考量依据,决定模块哪些流程可以软件硬化,哪些环节可以硬件软化。
程序的大部分代码都可以在必要的时候才加载到内存去执行,运行完后可以被直接丢弃或者被其他代码覆盖。我们PC上同时跑着很多的应用程序,每个应用程序使用的虚拟地址空间几乎可以整个线性地址空间(除了部分留给操作系统或者预留它用),可以认为每个应用程序都独占了整个虚拟地址空间(字长是32的CPU是4G的虚拟地址空间),但我们的物理内存只是1G或者2G。即多个应用程序在同时竞争使用这块物理内存,其必然会导致某个时刻只存在程序的某个片段在执行,也即是所有 程序代码和数据分时复用物理内存空间 —这就是内存管理单元(MMU)工作核心作用所在。
处理器系列的芯片(如X86、ARM7以上、MIPS)一般都会有MMU,跟操作系统一块实现虚拟内存管理,MMU也是Linux、Wince等操作系统的硬件要求。而控制器系统的芯片(面向低端控制领域,ARM1,2,MIPS M系列,80251等)一般都没有MMU,或者其只有单一的线性映射机制。
一、内存管理单元(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更便宜(一般很少见)。只要能够达到高效地进行内存管理,实现物理内存分时复用的目的,那都可以称为是成功或者有效的。所以在介绍真正的内存管理单元硬件设计之前,我们先简单介绍一种利用工具链来实现内存分时复用的机制。
1、工具链实现的CODE BANKING机制
CODE BANKING机制是由Keil C针对16位8051系列单片机退出的内存管理解决方案,其目的是扩展访问内存地址空间。16位单片机的可访问空间是64K字节,假如程序和数据大于64K(系统和应用稍为复杂点就很有可能啦)怎么办?Keil C推出的解决思路是利用P1口作为地址线的扩展去访问,最大支持2M空间,即32个64K,需要5个P1口线。其译码(即P1口线的高低电平选择)是由keil C的特别编译器在编译时自动产生的。当然,也需要在代码和链接脚本中主动说明某个函数是bank代码,否则所有函数在调用之前都插入一段译码代码,那程序都不用跑了。而且其也有公共代码区,例如中断处理,操作系统常驻段等等,在每个64k中都同样有一份。
我们会问,上面讲的CODE BANKING机制只是扩大可访问的内存地址空间,跟内存分时复用似乎没有关系。其实我们是可以利用这个机制来实现不同代码分时复用的。
如果P1的某条扩展地址线并没有接到实际的物理内存上,即该线无论输出1还是0,这个地址访问的物理内存其实是一样的。例如有个地址是0x10008,假如P1.0这根线没接到内存,那第17个bit的1其实是没有产生译码作用的,即0x10008访问的是0x00008的内存。也即是两个不同的地址其实对应的是同样的物理空间。所以我们可以将不同的函数编译链接到不同的虚拟地址,但其最终会被加载到同样的物理内存中去执行。这样就实现了不同代码的内存分时复用。
不过这种方法也有一定的缺陷:
1) 最多只有 32个Bank,不能再多了。一般多媒体消费产品系统都难以满足。
2) 应该开发要经常关注调用过程,要经常关注是否调了 Bank代码,调试起来也比较麻烦,16位地址还要结合P1口才能确定真正的物理地址。
当然,CODE BANKING机制的初衷是为了扩大可访问的内存地址空间,而不是给我们这样用的。
2、软、硬件协同设计
终于要进入我们的正题了。其实有了上面的介绍,下面的阐述你会很容易领会。我们会模仿真正的MMU去进行我们的内存管理单元硬件设计,并且考虑相关的细节。
我们要清楚,利用控制器领域的CPU(例如251,MIPS M系列等)进行SoC设计,一般都会高度集成,K级别的内存都会使用SRAM技术集成到SoC中。SoC内存管理单元结构图如下:
将这个图和MMU的结构示意图比较,可以发现有以下不同:
1) SRAM 中没有页表。
2) Bank 号寄存器组可以理解成跟TLB的索引数组
3) Bank 内存管理电路跟MMU的管理电路的核心电路应该说是一致的,当然会非常简单,其都是实现一个循环匹配的过程,实际上就是一个选择器电路。
可以说,Bank内存管理电路就是MMU单元的简化版,只是内存中不再存在页表。我们来看看它的工作机制,看在没有页表的情况下如何做到映射。这是由操作系统和应用程序、Bank内存管理单元一块完成的,架构师的责任!这一定会是SOC软硬件协同设计的一个教科书式经典案例!
1) SRAM 内存中划分常驻代码数据区域和若干个Bank区域。有同时竞争内存使用的程序模块就使用同一个Bank空间,如两个应用层(音乐和FM)程序约定分时使用某个Bank,而两个存储驱动(如card驱动和flash驱动)约定分时使用另外一个Bank空间。我们把使用某个Bank空间的所有虚拟Bank号称为一个Bank组。所以系统中会存储多个Bank组,而每个Bank组使用的实际物理地址空间是之前约定好的。
2) 一个虚拟地址分两个部分,一个是 Bank号,另一个是Bank内的偏移。假如CPU字长是24bit,设定Bank号的比特数是8,则虚拟Bank大小就是16bit,64K。物理地址也分两个部分,一个是Bank组号(对应物理内存块),另一个是Bank内偏移。
3) 实际物理 Bank大小是小于虚拟地址Bank大小的,可以是1k,2k等等,低16bit的地址都是bank内偏移地址,虚拟和物理内存是一一对应的。物理bank大小由操作系统来约定,应用程序编写模块函数时遵守即可。
4) 在整个虚拟地址空间中划分不同的 Bank组,就需要设定每一组Bank对应的Bank号的范围。假设有8个Bank组,那每个Bank组的Bank号数目就是256/8=32个。
5) Bank 号寄存器组和上面设定的Bank组是一一对应的,某个时刻每个Bank组只有一个Bank号写入到对应的Bank号寄存器中。
6) 通过以上约定和分析,可以知道,对于一个虚拟地址,是可以知道它是属于哪个 Bank组的,而通过Bank组和Bank内偏移是可以确定它在内存中的确切地址,也意味着它实现MMU中的页表的功能。如下图:
假设某个地址,我们来想象一下内存管理单元的工作过程:
1) 将该地址的高 N比特和Bank号寄存器组进行匹配,如果匹配命中,证明该地址所在的Bank代码已经在内存中,内存管理单元直接转换为物理地址进行访问。
2) 假设不命中,证明对应的 Bank代码没有在内存中,这时会触发一个异常进入中断,这时操作系统会判断该地址所属哪个Bank组,找出其所对应的文件代码,将其从外存储设备中读到内存,并更换对应的Bank号寄存器。这时返回后再次访问时就不会再次发生中断,而是命中读取。
3) 提到触发异常,而我们的 Bank内存管理电路是自己设计的,并不是CPU原有的模块,怎么触发异常呢?一种方法是在不命中时由内存管理单元返回一条未知指令,而每种CPU都有一种未知指令异常,这时CPU执行这条指令就会发生异常,进入中断。而因为这条指令是人为增加的,执行后PC值也增加了,在中断管理程序中要将之前保存的PC调整到未知指令执行之前的位置。
另外,架构师还应该按以上机制定义的规则定制好链接脚本,以便应用和驱动人员进行编程时不需要关心底层内存管理的实现。底层对上层透明才是优秀的设计!
内存空间规划分配
一、嵌入式系统软件分层
系统软件层次包括:启动、驱动、操作系统、文件系统、libc、中间件、应用框架、应用等层次。
驱动、文件系统和操作系统的时间管理、中断管理等接口一般都是通过API来进行调用;
libc和中间件、应用框架在系统中的处理可能以API的形式进行调用,也可以直接作为静态库与应用直接进行链接。
libc和中间件、应用框架作为静态库时,会减少API的占用空间(API往往是常驻空间,没理由调用API时还要从外存储中将API的代码加载到内存,这样效率太低),省去API层也可以提高调用速度,但会增加库函数的代码空间。如果库函数链接时可以运行在Bank内存中,由于Bank内存可以复用,增加的代码空间可以忽略,从这一点来看其又是一个优点。如判断某个文件是哪种解码格式时,其可以作为中间件来实现,并链接到应用的Bank空间,因为这是音乐解码前的预处理,可以和解码时刻的控制流复用同一块Bank空间。
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示意图:
四、程序内存空间分配
根据软件分层和程序段综合考虑,一般在物理内存的基础上先进行分层划分内存区域,再进行各层程序的段内存划分。有以下原则:
各层的常驻段(代码和数据)应该紧凑分配,而各层的Bank空间与常驻空间分块,也应该紧凑分配。
Bank空间的起始地址应该与扇区单位对齐,可取得最好的加载代码性能。
先把常见的场景的内存分配好,再考虑特殊场景的需求,看看特殊场景能否复用普通场景的内存空间。
buffer的划分也要考虑场景的复用,否则太浪费。如解码的buffer可以在未解码的时候用作预处理时的媒体文件有效性判断的buffer。
有时两组Bank空间可以合并起来当作另一个场景的一组Bank空间来使用。如解码时的软件分层比较多,涉及到应用中间件和算法中间件,而文件浏览应用则没有这么多层次,可以将两个中间件的Bank合并起来当一组Bank来使用。
一个模块的代码不应该跨越两个物理内存块,否则访问性能会降低。
尽可能提高内存利用率,避免内存碎片。
内存分配的细节要以公共链接文件出现,并用有意义的名称来定义各段的起始地址和长度,除架构设计师外,其他人不允许修改该文件。
API设计方法
一、基本原理
驱动在系统中会按模块进行分类,例如KEY驱动、LCD驱动、文件系统、card驱动、I2C驱动等等;每个模块又有多个接口,例如LCD驱动有光标定位、画点、画直线等,而文件系统有fread、fwrite、fseek、fopen等接口。以下举例将以文件系统的fopen为例。
二、设计和实现方法
1. 驱动接口声明:extern FILE * fopen(const char * path,const char * mode),位于fs.h中
2. 驱动接口定义:FILE * open(const char * path,const char * mode){...},位于fs.c中。
3. 驱动接口API:
fopen :
li v1, FILE_OPEN;
syscall; 位于api.S中,是汇编代码。这里以mips为例。
4. 驱动接口函数指针数组:
struct file_operations fs_fops { open,read,write,seek};
5. 文件系统被加载时,会将文件系统的接口函数指针数组fops注册到系统的API管理数组中。
6. 系统对驱动进行分类管理,其有一个记录各个驱动接口函数指针数组基址的全局数组,各个驱动事先按顺序进行约定,如数组的第一个元素就是按键驱动的key_fops,而第二个就是LCD驱动的lcd_fops,以此类推。某个驱动被加载时,驱动会把对应的fops通过API管理的接口记录到该数组的对应位置。
这个约定一般会在api.h中,如#define KEY 0 //表示key驱动是约定在数组的第一个位置, #define FS 2//表示FS是约定在数组的第三个位置。
7. FILE_OPEN定义:
#define FILE_OPEN (FS
在fs.h中,表示fopen是文件系统提供的第一个接口,这个常量包含两部分信息,一是文件系统在API管理中的索引,二是该接口在自己驱动接口中的索引。
三、调用过程
应用调用时,path和mode等形参会压入栈或者寄存器(MIPS是栈和寄存器(a1,a2,a3,a4)都同样存放,而ARM是优先寄存器,不够再转到栈中存放),然后进入fopen的API,其将FILE_OPEN常量赋值给v1,通过syscall陷入到异常,进行内核态,这时即可以进入到API管理中,API根据FILE_OPEN提供的两部分信息可以迅速找到open的地址,异常返回时即跳到open的地址执行,此即进行实际的接口调用。整个过程完成。
四、API的传参效率
API的传递会包括两部分内容,一是接口本身的参数,如fopen的文件名和操作方式,二是API的索引号,如FOPEN。考虑到系统陷入后的管理最终会跳转调用到目标的接口,应该将接口本身的参数放在API接口的前面(并且尽量用寄存器存放完,因此参数不宜超过4个),而索引FOPEN放在最后(这里的mips汇编把这个参数放在返回值寄存器v1)。这样陷入后的管理就不需要对参数进行调整。这是汇编和C语言混合编程需要考虑的问题。
在资源紧缺型嵌入式系统中节省内存的软件设计技巧
资源紧缺型嵌入式系统一般主控都是单片机控制器,内存资源都是K字节级别。资源紧缺型的电子产品都是成本敏感的,而成本跟内存的容量是成正比的,尤其在SOC芯片设计时是固件开发需要重点关注的。量产前要确定片上RAM的大小,而且是在满足功能需求的情况下越小越好。节省内存的常规做法,一般是通过高效利用内存来达到目的,此外还有其他的重要的软件设计技巧,这自然要考究开发人员的系统软件设计能力和编程开发的技能了。
这里以低端多媒体电子产品为例,其一般会自行定制精简版操作系统,驱动、中间件和应用等模块都有,所谓麻雀虽小,五脏俱全。
一、内存分时复用
分时复用即对代码进行分块(Bank)管理。它的设计需求来源于:
1. 很多电子产品并不是像现在的安卓手机一样同时跑多个应用,顶多就听歌时浏览图片而已,非智能手机也是如此。但我们也会看到电子产品里面有有很多的应用,如设置、电子书、电话本、录音啊等等。因此,不同时运行的应用占用同一块内存空间理所当然。
2. 驱动空间。有很多的驱动并不同时使用,如听FM时是FM驱动,听歌又是使用解码器,所以很多驱动也是可以服用同一块空间的。
3. 中间件的复用。如UI、硬件驱动的再次封装使用等等,其由对应的应用直接调用,一般也存在复用的需求。
4. 数据段的复用。应用和驱动都有数据,同样有复用的场景需求。
理论上驱动和代码也可以服用空间的,但需要考虑的细节太多,而且这样做扩展性不好,所以应用一般是不会跟驱动复用空间的。一般较为粗糙地将软件系统分为以下几个部分:启动、驱动、操作系统、中间件、应用等层次。启动为一次性执行,不需太多考虑复用的空间。操作系统一般有常驻内存的需求,如中断管理、时间管理、调度管理、模块代码管理、虚拟文件系统等等,当然操作系统的一部分功能并不需要常驻内存,主要是一些调用频率较小的一些接口,如驱动装卸载、应用初始化等模块。不需常驻内存的一些接口实现也可以跟驱动复用空间。
二、内存分块的大小
考虑系统架构师的功力。块设大了浪费,小了会导致代码切换频繁效率低下。既然都是RAM,有时数据可以跟代码段放到同一个块中,而没有必要另加一块数据块。当然这些细节需要综合评估并加以详细设计。在成本敏感的电子产品中,这些技巧需要努力挖掘发挥。
三、ROM代替RAM
这只是从成本的角度去节省内存资源,有些代码需要常驻内容,但其内容并不会随着版本的更新而发生变化,如上节所讲操作系统的调度管理等,可以考虑将这些代码固化到ROM中。理论上操作系统大部分需要常驻内存的代码都可以固化。RAM和ROM同样大小的成本比大概是6:1,因此使用ROM也能大幅降低成本。
四、系统移植裁剪
这是操作系统设计人员务必要考虑的。每个产品都有独有的功能,而底层操作系统具有普适性,在资源紧缺型系统中,砍掉不必要的模块是非常明智的。
五、重构可执行程序
我们所阐述的系统一般都是封闭系统,只要能高效地实现功能,我们可以任意改动系统中所有的代码。例如对于可执行的ELF文件,操作系统如果按标准的流程要解析完ELF文件再加载,但不仅需要很多的内存资料,而且也效率低下。ELF有关加载和执行最重要的就是.CODE、.DATA、.CONST、.BSS等段信息,我们完全可以离线抽取出来生成一个新的简单的定制文件格式,操作系统只需解析这个简单的文件就可以了。这样做不仅节省内存,也能节省外存储空间,更能实现高效加载的目的。
六、编程技巧
这个需要平时的积累。例如,在变量的排列方面,我们都知道编译器会考虑对齐。明显,以下第一种定义需要的内存比第二种要大。
1)char a;int b; char c;
2)char a; char c; int b;
七、算法设计
好的算法一般会是轻巧的,效率高的。
八、编译优化
编译时选择优化级别高的,这样生成代码大小有有大规模的减小。
九、指令编译模式
如arm里面选thumb指令,mips选择mips16e,这是由体系结构所决定的,体系结构也是为了考虑节省代码空间资源而设计了16位的指令模式,而这些CPU的字长往往是32位。这种方式能减少30%左右的代码量。
十、栈空间规划
每个线程都会有自己的栈,而每个线程的栈都应该根据其线程的调用深度来具体设定,像UCOS就有一个栈使用率的任务,我们不妨借用这种思路来看看某个线程最终的栈深度。
设定独立的中断栈,可以避免每个任务栈都要给中断预留栈空间。
扁平的函数调用方法用栈一般要比纵向的函数调用小。嵌入式开发有时为了效率和资源,不应该把代码分块分得太细,函数一大摞,既增加代码量和栈,也降低运行效率。
十一、定制链接脚本,合理规划内存空间
例如,我们规划空间时往往代码段和数据段分开,但实际的代码段可能又用不完,这时就可以把一部分变量定位到代码段之后。这样能够尽可能地利用完内存,而不留“碎片”。
来源:网络
领取专属 10元无门槛券
私享最新 技术干货