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位數據。