上篇文章(智能手表音乐播放功耗的优化)讲了怎么优化音乐场景下的功耗,其中第二点是优化memory的布局。那么在哪里优化memory的布局呢?就是在本文要讲的链接脚本(ld)文件里。作为audio DSP 软件工程师,ld文件要能看懂和会修改。作为程序来说,先编译后链接。编译得到目标文件,链接就是把这些目标文件合成一个输出文件。链接过程都由链接脚本控制,链接脚本主要用于规定如何把输入文件内的section放入输出文件内, 并控制输出文件内各部分在程序地址空间内的布局。链接脚本有其语法,把语法搞懂了,就能看懂和会修改ld文件。本文就通过具体的例子来解析ld文件,使其通俗易懂。
链接脚本语法中有很多关键字,如OUTPUT等。本文要举的例子中都是一些基本的。如在工作中遇到没见过的,可以去网上搜,搞清楚意思,并能应用就可以了。我把例子按前后顺序分成了5张图,下面具体来看。
图1是ld文件的开头部分。
图 1
蓝框1处是关键字OUTPUT,格式为OUTPUT(filename),表示执行ld文件后的输出文件是什么。在CEVA DSP上,输出是elf文件。Elf文件是可执行链接文件,是执行链接的产物(编译后的产物是*.o),elf文件可以在CEVA的IDE集成开发环境上运行,但是不能在芯片上运行。要想在芯片上运行,需要将其转化为bin。至于怎么转,后面再讲。蓝框2处是关键字ENTRY,格式为ENTRY(symbol),是将symbol设为软件的入口地址,即程序执行的第一条指令。图1中symbol为__cxd_inttbl_start ,在代码中就相当于一个全局变量,表示软件从__cxd_inttbl_start开始运行。__cxd_inttbl_start在crt0.c文件里用到。Crt表示c runtime,0表示最开始。后面准备写一篇adsp boot的文章,到时再具体讲crt0.c。
图2是ld文件的第二部分。
图 2
蓝框3定义了各块Memory的起始地址和大小等各种变量,是给蓝框4中的关键字MEMORY用的。ADSP可用的memory包括内部的ITCM、DTCM以及外部的DDR、SRAM以及ROM等(ASIC设计时就可以让ADSP访问外部的DDR、SRAM、ROM)。例子中定义了DDR中可用的起始地址是0x29000000,可用大小是0x40000(这是与AP协商的结果)。也定义了ITCM/DTCM的起始地址均是0x0,大小都是128k。还定义了ROM的大小是128k,起始地址是0xc0860000,其中96k用来放code,32k用来放data。蓝框4中用关键字MEMORY描述了各块memory的起始地址和大小,其中用INTERNAL_CODE表示ITCM,INTERNAL_DATA表示DTCM。MEMORY的语法如下:

NAME表示memory名称,ORIGIN表示起始地址,LENGTH表示大小,ATTR表示属性(w表示可写,r表示可读, x表示可执行)。
图3/4/5就是各个section的具体内容。先从图3看起。
图 3
蓝框5是关键字SECTIONS,格式为SECTIONS { },大括号内是各个具体的section。蓝框6/7/8是三个section,均放在DDR(DDR_MEM)上。先看蓝框6。Section SLOW_I_RAM 从名字看出里面放的是指令(instruction, 即I),也就是code。关键字ALIGN表示字节对齐。从图中看出这一section开始处要4k(0x1000)对齐。要4k对齐的原因是这一section放的是code,为了加快访问速度,要设置cache属性为cacheable,cache要求4k对齐。. += __rom_check_shift中.是定位符,表示当前位置的地址。.开始表示section SLOW_I_RAM的起始地址,后来又自加了__rom_check_shift个字节。举个例子,假设section SLOW_I_RAM的起始地址是0x29000000,__rom_check_shift等于4。开始时.就表示地址0x29000000,加4后.就表示地址0x29000004。__rom_check_shift是用于做ROM的,做完ROM后这个值要变为0(关于怎么做ROM,可以见文章在audio DSP中如何做软件固化)。关键字PROVIDE用来定义一个符号,可以理解为定义了一个全局变量,这个全局变量可以在代码里用。关键字ABSOLUTE表示取绝对值。PROVIDE(__DDR_CODE_CACHED_START = ABSOLUTE(.))就表示定义个一个全局变量__DDR_CODE_CACHED_START,用它来指定要cached的code的起始地址,这个起始地址是用定位符获取的。*(IPC_SLOW_CODE)等表示这个section上放哪些具体的代码模块。放完code 段后又加了个PROVIDE(__DDR_CODE_CACHED_END = ABSOLUTE(.)),表示要cached的code的结束地址。这两个全局变量均用在配置memory的cache属性里,这一段放code,要配成cacheable。再来看蓝框7。从名字CONST_D_RAM看出是放常量的,属于data。为了加快访问速度,依旧要用配置成cacheable,所以用PROVIDE(__DDR_DATA_CACHED_START = ABSOLUTE(.))来指定要cache的起始地址,然后后面放数据段。最后看蓝框8,依旧是放data,不过是未初始化的,运行时不需要载入memory,所以用了关键字NOLOAD。关键字NOLOAD表示运行时不用载入memory。最后用PROVIDE(__DDR_DATA_CACHED_END = ABSOLUTE(.))来指定要cached的结束地址。蓝框7和蓝框8都是放data,只不过一个放的是常量等,一个放的是未初始化的。这两段都要配置成cacheable,所以cache的起始地址是蓝框7的开始处,结束地址是蓝框8的结束处。
再来看图4,它是关于ROM的。ROM是事先做好的,ld文件里有这两个section有两个目的。一是让它参与链接,得到ROM里函数和数据的地址,去与事先做好的比较,看是否改变。如果改变了,说明有问题,需要去解决,直到得到的地址与事先做好的完全一样。二是得到需要cache的起始和结束地址。ROM也算片外,为了加快速度,也需要用cache,包括code和data的。
图 4
图4展示了ROM里code(L2_I_ROM,蓝框9)和data(L2_D_ROM,蓝框10)段的内容。依旧要设置要cache的起始地址和结束地址。这里就不细讲了。
最后看图5,是关于ITCM和DTCM的。ITCM的放code,DTCM的分成两部分:常量和已初始化的以及bss上的。放bss上的包括未初始化的data以及各个任务用的栈等。这一部分不需要载入memory, 所以用了NOLOAD。
图 5
图3/4/5分别代表放在DDR/ROM/内部memory上。典型场景的code和data尽量放在内部memory上,非典型场景的code和data最好放在外部DDR上。
前文说了,链接的输出是elf文件,不能在芯片上运行。要想在芯片上运行,需要写个应用程序将其转化为bin,bin里包括运行时要load进memory的section。Ld文件里这么多section,哪些要放进bin里?就是上面那些section里没有用NOLOAD关键字的。ROM由于事先已经做好,两个section也不需要放进bin。总结就是DDR放code的section和放常量和已初始化数据的section以及ITCM上的section和DTCM上放常量和已初始化数据的section这4部分要放进bin里。boot时把DDR的section内容放到DDR上,把ITCM的内容拷进ITCM里,把DTCM的内容拷进DTCM里,系统就能正常运行了。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步