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位数据。
posted @ 2016-11-18 10:35  亦梦云烟  阅读(2636)  评论(0编辑  收藏  举报