DSP程序nandflash固化(三)——COFF文件解析与AIS脚本制作
CCS生成的可执行文件是COFF或ELF文件,通常使用COFF文件作为下载文件。本节将对COFF文件做一定的介绍,并通过C语言数据结构读取COFF文件并将其解析成内存映像,然后根据内存数据制作AIS脚本文件。
程序中的代码和数据在COFF文件中是以段的形式组织。烧写COFF文件由几种方法,其中一种是TI提供的hex6x工具,该工具可以将CCS生成的.out文件转换成hex文件,再手工去除无用的信息,然后合并文件,最后将hex文件转换成bin文件,bin文件可以直接烧写到flash中,这种方法复杂,操作麻烦,而且需要安装多种工具,其中的perl脚本工具更是非常难用,另一种方法则是使用TI提供的FLASHburn软件,通过AISGen工具可以生成AIS脚本文件,但缺点是AISGen工具只提供DSK评估板中使用到的芯片对应的配置,对于其他芯片则无法使用,本文提供一种直接翻译COFF文件,制作AIS文件的方法,可以方便的进行修改配置,并且代码全部开源给大家参考。之所以开源还是因为笔者在研究DSP固化从NANDFLASH启动的过程中,经历了太多波折,网上的参考资料太少,大多讲解不完全或者只有提供文字说明,没有具体的代码和操作步骤,为此笔者将这个过程分享出来,减少新手所走的弯路。
首先我们需要了解COFF文件的组成结构,默认情况下,COFF文件包含3段:.text可执行代码段,.data已初始化数据, .bss未初始化数据保留空间,在烧写过程中只烧写.text段和.data数据段。这里需要啰嗦的是,一个CCS工程中,并不是只有.text段和.data以及.bss段,除了这些段还有其他的段,如.cinit段,.switch段,还有用户自定义的段,这里提到的.text和.data只是对代码和数据的概括,从意义上来说.cinit也属于.text段,但在COFF文件中.text段和.cinit等这些段不在用一个段中,但他们都需要下载到FLASH中,这一点需要读者明白。
下面来分析COFF文件的结构,COFF文件从上到下依次是:文件头,可选文件头,段头信息表,段头信息表对应的数据段,重定位信息,行号入口表,符号表,字符串表,如下图所示:
文件头:描述了整个文件的基本信息,包含段的总数目,时间戳,符号表位置等,从文件的0偏移处开始,C语言描述的结构如下:
/* COFF file header struct (22Bytes) */ typedef struct _FILE_HEADER { unsigned short fileVersionID; // Version ID unsigned short fileSecNum; // Number of section headers unsigned int fileTime; // Time and date stamp unsigned int filePointer; // File pointer unsigned int fileEntry; // Number of entries in the symbol table unsigned short fileByte; // Number of bytes in the optional header unsigned short fileFlag; // 可选文件头长度,如果有则为28否则为0 unsigned short fileTargetID; // Target ID,若为0x0099,目标文件可以在C6x平台上运行 } FILE_HEADER, * PFILE_HEADER;
这里只介绍本次需要用到的关键字段及其含义,下文的结构描述亦是如此。
fileSecNum: 段的总数,COF文件中包含的段落数,在解析过程中需要用到,循环读取段落的次数。
fileFlag: 可选文件头长度,如果有可选文件头则为28,否则为0,C6000一般都有,所以在程序中认为有不做判断。
fileTargetID: 若为0x0099则可在C6x上运行,一般的只需看看是不是就行了,在不确定解析文件是否正确的情况下,看看这个字段的值,如果是0x0099则可以认为文件头解析没有错。
可选文件头:紧跟文件头,22字节处开始,包含代码大小,已初始化数据大小和未初始化数据大小,最终要的包含了程序入口地址,C语言描述如下:
// COFF file optional file header(28Bytes) typedef struct _OPTIONAL_FILE_HEADER { unsigned short optionMagic; // Magic number unsigned short optionVersion; // Version stamp unsigned int optionExecut; // Size of executable code unsigned int optionInit; // Size of initialized data unsigned int optionUninit; // Size of uninitialized data unsigned short EntryPointLo; // Entry point unsigned short EntryPointHi; unsigned int optionBeginCode; // Beginning address of executable code unsigned int optionBeginData; // Beginning address of executable data }OPTIONAL_FILE_HEADER;
optionExecut: 可执行代码的大小,单位字节;
optionInit: 已初始化数据大小;
EntryPointLo: 程序入口地址的低16位,EntryPointHi:程序入口地址的高16位。
到这里细心的读者肯定会问,为什么这里不定义unsigned int EntryPoint,而是要分开定义两个16位长度的地址,在各大百科上,关于COFF文件结构的介绍都是定义成unsigned long型数据,但在实验过程中笔者发现无论定义成unsigned int还是unsigned long,读出的数据都不是一个合理的数据,起初以为是字节没有对齐导致的,在结构体定义前后加入#pragma pack(4)指令仍然出错,但同样是unsigned int类型的代码数据大小就没有问题(读者可以字节在解析的时候计算optionExecut和.out文件大小对比,差不多大小说明解析无误),至今笔者没有查到原因何在,庆幸的是16位合并的方法处理得到的结果是正确的,如果读者知道这其中的原因,希望读者留言解释,感激不尽!
段头:紧跟在可选段(如果有的话),长度固定为48B,所有的段头连续存储,C语言描述如下:
//section header(48Bytes) typedef struct _SECTION_HEADER { char secCharacter[8];// Section name unsigned short secPhyAddrLo; // Section's physical address unsigned short secPhyAddrHi; unsigned int secVirAddr; // Section's virtual address unsigned short secSizeLo; // Section size (Byte) unsigned short secSizeHi; unsigned short secRawLo; // File pointer to raw data(段数据指针) unsigned short secRawHi; unsigned int secPointerRelo; // File pointer to relocation entries段重定位表指针 unsigned int secReserved1; // Reserved 行号表指针 unsigned int secNumRelo; // Number of relocation entries重定位表长度 unsigned int secReserved2; // Reserved行号表长度 unsigned short secFlagLo; // 段标识 unsigned short secFlagHi; unsigned short secReserved3; // Reserved保留字段 unsigned short secMemory; // Memory page number内存页号 } SECTION_HEADER;
secCharacter[8]: 段名,如.text, .bss等,当用户自定义的段名长度超过8字节时以字符串符号表的指针表示;
secPhyAddrLo,secPhyAddrHi: 段的物理地址,即载入内存时的地址,section load指令的一个参数(前一节讲过);
secSizeLo, secSizeHi: 段大小;
secRawLo, secRawHi: 段数据偏移,该段数据在.out文件中的偏移量;
secFlagLo, secFlagHi: 段标识,非常重要,区分是否写入AIS脚本的唯一判决条件。
这里需要注意的是Flag的值所表示的含义:
Flag值 | 说明 |
0x0020(0x100000) | STYP_TEXT正文段标识 |
0x0040(0x1000000) | STYP_DATA数据段标识,有些保存已初始化数据 |
0x0080(0x10000000) | STYP_BSS保存的是未初始化的数据 |
那么在程序判断中应该如何判断是否需要写入AIS文件呢?其实上面的FLAG是字段标识,可以认为在FLAG的第6位和第七位分别标示代码段和数据段,用户定义的段在相应的位也要根据需要置1或置0,因此可以判断第六位或第七位只要有一个是1则需要下载到FLASH中,也就是需要将这样的段转换成AIS脚本。
下面分析主要的代码:
首先读取文件的段落头和可选段落,
File_header = (FILE_HEADER *) &updateBuffer[0x0];
Opt_File_header = (OPTIONAL_FILE_HEADER *) &updateBuffer[0x16];
在AIS起始位置开始写入AIS魔术字和内部函数调用:
#define WRITEAIS(data) *(writePTR++)=data WRITEAIS(0x41504954); //magic number WRITEAIS(0x00000000); //Place holder reserved for number of pages over which image spans WRITEAIS(0x00000000); //Place holder for block where image starts WRITEAIS(0x00000000); //Place holder for page where image starts WRITEAIS(0x5853590D); //function load WRITEAIS(0x00030000); //set pll function WRITEAIS(0x00000015); //PLLM = 0x15 WRITEAIS(0x00000000); //PLLDIV1 = 0 WRITEAIS(0x00000000); //internal oscillator WRITEAIS(0x5853590D); WRITEAIS(0x00050001); //SET EMIF WRITEAIS(0x00840328); //AB1CR WRITEAIS(0x00840328); //AB2CR WRITEAIS(0x00000101); //AB3CR WRITEAIS(0x00000001); //AB4CR WRITEAIS(0x00000001); //NANDFCR WRITEAIS(0x5853590D); WRITEAIS(0x00090002); //SET DDR WRITEAIS(0x00000030); //0 DDR PLLM WRITEAIS(0x00000001); //1 DDR CLK WRITEAIS(0x00000000); //2 0X00000000 WRITEAIS(0x00000000); //3 DDR clock WRITEAIS(0x50006405); //4 DDRPHYCR WRITEAIS(0x00138822); //5 SDBCR WRITEAIS(0x22923209); //6 SDTIMR WRITEAIS(0x0012C722); //7 SDTIMR2 WRITEAIS(0x000004EF); //8 SDRCR //WRITEAIS(0x58535904); //disable CRC WRITEAIS(0x58535903); //ENABLE CRC
接着处理每一个段,顺序读取段落头,根据段落头中的信息读取段落内容,写入AIS:
// process each sector sectionOffset = 0x32; TOTAL_BYTE = 0; for(i = 0; i < File_header->fileSecNum; i++) { Sec_header = (SECTION_HEADER *) &updateBuffer[sectionOffset+i*sizeof(SECTION_HEADER)]; Section_flag = TO32(Sec_header->secFlagLo, Sec_header->secFlagHi); Section_size = TO32(Sec_header->secSizeLo, Sec_header->secSizeHi); if((Section_flag & 0x60) && Section_size) { // 数据 Section_addr = TO32(Sec_header->secRawLo, Sec_header->secRawHi); Section_size = TO32(Sec_header->secSizeLo, Sec_header->secSizeHi); boot_addr = TO32(Sec_header->secPhyAddrLo, Sec_header->secPhyAddrHi); WRITEAIS(0x58535901); WRITEAIS(boot_addr); WRITEAIS(Section_size); memcpy ((void*)writePTR, (void*)(updateBuffer+Section_addr), Section_size); if(Section_size % 4) { memset (((char*)writePTR)+Section_size, 0, 4 - Section_size % 4); } writePTR += ((Section_size+3)/4); TOTAL_SECT+=1; TOTAL_BYTE+=Section_size; } }
最后写入结束跳转指令:
boot_addr = TO32(Opt_File_header->EntryPointLo, Opt_File_header->EntryPointHi); WRITEAIS(0x58535906); WRITEAIS(boot_addr); WRITEAIS(TOTAL_SECT); WRITEAIS(TOTAL_BYTE);
到此还没有完,在魔术字之后又三个保留参数,第一个是所有写入的页数目,第二个是开始搜索的块号,第三个是开始搜索的页号,还记得前面讲过的AIS需要存放在哪里吗?没错,必须存放在第1块之后,所以这里第二个参数写入1.但是如果第一块是坏块怎么办呢?顺着往后延续,为此需要判断一下:
writepage = (AISsize/NAND_PAGESIZE+1)/NAND_PAGES_PER_BLOCK + 1; bad_blocks = 0; writeblock = 1;//最少为第1块 while ( (bad_blocks < 20) && bad_block_list[bad_blocks] != 0xFFFFFFFF && (writeblock + writepage > bad_block_list[bad_blocks]) ) { writeblock = bad_block_list[bad_blocks]; bad_blocks ++; } ((int *)AISbuffer)[1] = (AISsize/NAND_PAGESIZE+1); ((int *)AISbuffer)[2] = writeblock; ((int *)AISbuffer)[3] = 0;
至此,.out文件转换为AIS脚本的工作已经全部完成,接下来就只需将AIS脚本写入到NANDFLASH中即可。
本节重点:
- COFF文件结构,由三种类型的段类型组成,需要解析每种段头的字段内容;
- 原始数据存放在所有段落头的后面,必须为32位字宽写入AIS;
- 魔术字后面要写入搜索起始的块号和页号,而且块号必须大于0;
- bootloader可以自动从1往后搜索,直到魔术字开始执行AIS脚本指令,在搜索过程中遇到坏块可以自动跳过,但程序在写入时要考虑坏块的情况;
- 跳转结束指令要指定跳转地址,这个地址在.out中不是连续存储的四字节整型数据,需要分别解析高16位和低16位,最终合并成一个32位数据。