软件环境:vivado 2018.1 硬件平台:XC7K325
接上篇文章最后的问题,microblaze的lwip工程从qspi启动需要7、8分钟,时间过长的问题排查感觉可以从以下几个方面入手
1.首先,工程是可以正常使用,正常网络通讯的,这样的话,工程通常可以认为是没有问题的,那么,提高或者降低系统的时钟频率,是否对启动时间有影响,如果时间过长的问题是因为时钟频率不够,那么提高或降低系统时钟频率,这个启动时间应该会相应的增加或者减少。
2.开机启动时候的打印,众所周知,这个printf的打印是非常耗时的,如果启动时间过长是因为大部分时间都花在了printf的打印上,那么屏蔽掉相关的打印函数,接下来的启动时间就会非常短,相反,如果其他部分耗时比打印需要的时间长,那么屏蔽掉打印相关的调用之后,启动的时间应该是不变的。
3.我一直比较在意烧写flash时候专门勾的这个convert ELF to bootloadable SREC format and program,将ELF应用程序转换为能够boot的SREC格式文件,为什么ELF应用不能直接搬进DDR运行,还非要通过SREC来boot,这个SREC什么来头,会不会有问题。
通过上面三个问题,我逐一进行测试之后发现
1.我成倍的增加或者降低系统时钟频率,启动时间确实会近似成倍的增加或者减少,说明这里系统时钟是有影响的,但也比较尴尬,举个例子,系统时钟100 MHz的时候,启动时间需要8分钟,系统时钟倍频到200 MHz,启动时间4分钟左右,你说这有效吗,这有效,但是4分钟还是长啊,你能继续倍到400 MHz吗?对不起,你不能,你能板子也不能,通过时钟改善启动问题,只能说治标不治本,因为时钟不能无限制的放大。而且,100~200 MHz的时钟很正常啊,就这都还要花这么长时间启动,只能怀疑是不是有其他地方,耗费了太多的时钟周期。
2.通过在bootloader.c这个文件中,屏蔽VERBOSE,启动时候的调试信息就不会打印出来了,但是屏蔽了以后,虽说是不打印了,但是启动时间还是那么多,依旧没有减少。所以就像我说的,肯定有地方耗费的时间比打印时间多的多,所以屏蔽掉打印以后,对启动时间才会丝毫没影响。
3.我尝试不勾选烧写flash时候的那个将ELF转换为SREC的选框,重新烧程序之后,连启动都启动不了,丝毫没反应,所以确实像选框说的,直接烧进去的ELF,是识别不了的。那没办法,只能一点一点扒代码看了。
int main()
{
int Status;
uint8_t ret;
#ifdef VERBOSE
print ("\r\nSREC SPI Bootloader\r\n");
#endif
/*
* Initialize the SPI driver so that it's ready to use,
* specify the device ID that is generated in xparameters.h.
*/
Status = XSpi_Initialize(&Spi, SPI_DEVICE_ID);
if(Status != XST_SUCCESS) {
return XST_FAILURE;
}
/*
* Start the SPI driver so that interrupts and the device are enabled.
*/
XSpi_Start(&Spi);
XSpi_IntrGlobalDisable(&Spi);
/*
* Initialize the Serial Flash Library.
*/
Status = XIsf_Initialize(&Isf, &Spi, ISF_SPI_SELECT, IsfWriteBuffer);
if(Status != XST_SUCCESS) {
return XST_FAILURE;
}
init_stdout();
#ifdef VERBOSE
print ("Loading SREC image from flash @ address: ");
putnum (FLASH_IMAGE_BASEADDR);
print ("\r\n");
#endif
flbuf = (uint8_t*)FLASH_IMAGE_BASEADDR;
ret = load_exec ();
/* If we reach here, we are in error */
#ifdef VERBOSE
if (ret > LD_SREC_LINE_ERROR) {
print ("ERROR in SREC line: ");
putnum (srec_line);
print (errors[ret]);
} else {
print ("ERROR: ");
print (errors[ret]);
}
#endif
return ret;
}
精简过以后,有用的也就是下面几个函数。
Status = XSpi_Initialize(&Spi, SPI_DEVICE_ID);
XSpi_Start(&Spi);
XSpi_IntrGlobalDisable(&Spi);
Status = XIsf_Initialize(&Isf, &Spi, ISF_SPI_SELECT, IsfWriteBuffer);
init_stdout();
flbuf = (uint8_t*)FLASH_IMAGE_BASEADDR;
ret = load_exec ();
打眼一看函数名就知道,前几个基本上就是SPI初始化相关的函数,可以不用管,init_stdout()呢?
void
init_stdout()
{
/* if we have a uart 16550, then that needs to be initialized */
#ifdef STDOUT_IS_16550
XUartNs550_SetBaud(STDOUT_BASEADDR, XPAR_XUARTNS550_CLOCK_HZ, 9600);
XUartNs550_SetLineControlReg(STDOUT_BASEADDR, XUN_LCR_8_DATA_BITS);
#endif
}
嗯~基本上么得屌用。串口相关的。那能够怀疑的基本也就只有最后的load_exec()了,这个是干嘛的。
static uint8_t load_exec ()
{
uint8_t ret;
void (*laddr)();
int8_t done = 0;
srinfo.sr_data = sr_data_buf;
while (!done) {
if ((ret = flash_get_srec_line (sr_buf)) != 0)
return ret;
if ((ret = decode_srec_line (sr_buf, &srinfo)) != 0)
return ret;
#ifdef VERBOSE
display_progress (srec_line);
#endif
switch (srinfo.type) {
case SREC_TYPE_0:
break;
case SREC_TYPE_1:
case SREC_TYPE_2:
case SREC_TYPE_3:
memcpy ((void*)srinfo.addr, (void*)srinfo.sr_data, srinfo.dlen);
break;
case SREC_TYPE_5:
break;
case SREC_TYPE_7:
case SREC_TYPE_8:
case SREC_TYPE_9:
laddr = (void (*)())srinfo.addr;
done = 1;
ret = 0;
break;
}
}
#ifdef VERBOSE
print ("\r\nExecuting program starting at address: ");
putnum ((uint32_t)laddr);
print ("\r\n");
#endif
(*laddr)();
/* We will be dead at this point */
return 0;
}
我就说么!原来在这里,能看出来,通过done的标志,不停的在读flash里烧进去的srec格式的数据,然后将数据转码,再将转码后的数据,通过memcpy拷到DDR里去跑,整个搬运的过程就是在这里完成的,又是读,又是转码,还要memcpy内存拷贝,不论哪一个都是非常耗时的操作,还三个放一起用个大while括起来,这时间再不长就怪了。
SREC、SREC、SREC又是个什么。
这个格式以ASCII格式存储二进制信息。(这个格式已经有40年的历史,最初是为8位Motorola 6800微处理器而开发的)。一个完整的MOTOROLA S-Record格式数据包含如下区域:
<type> <length> <address> <data> <checksum>
<type>:标示记录的类型,占1 byte。范围“S0”~"S9",没有"S6"
“S0” -- 记录描述信息
“S1”, “S2”, “S3” -- 记录存储的数据。区别在于地址(address)的长度不同,S1为2 byte,S2为3 byte,以及S3为4 byte。
“S5” -- 包含了“S1”, “S2”, “S3”的信息。
“S7”, “S8”, “S9” -- 确定程序的开始地址。区别也在于地址(address)的长度不同,S9为2 byte,S8为3 byte,以及S7为4 byte。
<length>:标示了数据的长度,是 <address>, <data> 和<checksum>这三个字段的byte的个数。该字段占据1 byte。
<address>:标示了数据写入的起始地址。该字段的长度取决于<type>的取值。
<data>:标示了存储的数据。该字段占据的byte个数可以这样计算:<length>的值 - <address>字段的长度(取值为2、3、4) - 1(<checksum>字段的长度)
<checksum>:标示校验位,占据1-byte。该数据可以由<address>和<data>的数据累加然后每bit取反获得。
这么看下来确实是太麻烦了,将你的应用程序数据内容,写入flash的时候,一条一条的按照SREC格式写进去,上电启动时候再一条一条的读出来,再解码,最后复制到内存。看样子问题应该就是出在这里了,有没有解决办法,有。
那就是烧写flash的时候,不将ELF转为SREC,这样也就避免了不断的转码和拷贝浪费的时间,那既然不转SREC,势必要直接使用ELF文件,自然就要对ELF文件格式有所了解,而且,SDK自带的SREC SPI的bootloader工程肯定也就不能直接使用了,需要做一个自己的可以直接从ELF中提取程序段搬到DDR的bootloader,难度还是有的,没办法,鱼与熊掌,想启动快,有些东西还是避免不了的。
ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。一个典型的ELF文件大致由如下结构组成。
文件头(ELF Header)
---------------------------
程序头表(Program Header Table)
代码段(.text)
数据段(.data)
bss段(.bss)
段表字符串表(.shstrtab)
---------------------------
段表(Section Header Table)
符号表(.symtab)
字符串表(.strtab)
重定位表(.rel.text)
重定位表(.rel.data)
ELF文件头用于记录一个ELF文件的信息(多少位?能够运行的CPU平台是什么?程序的入口点在哪里),最开头是16个字节的ident, 其中包含用以表示ELF文件的字符,以及其他一些与机器无关的信息。开头的4个字节值固定不变,为0x7f和ELF三个字符。
#define EI_NIDENT (16)
typedef struct
{
unsigned char ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half type; /* Object file type */
Elf32_Half machine; /* Architecture */
Elf32_Word version; /* Object file version */
Elf32_Addr entry; /* Entry point virtual address */
Elf32_Off phoff; /* Program header table file offset */
Elf32_Off shoff; /* Section header table file offset */
Elf32_Word flags; /* Processor-specific flags */
Elf32_Half ehsize; /* ELF header size in bytes */
Elf32_Half phentsize; /* Program header table entry size */
Elf32_Half phnum; /* Program header table entry count */
Elf32_Half shentsize; /* Section header table entry size */
Elf32_Half shnum; /* Section header table entry count */
Elf32_Half shstrndx; /* Section header string table index */
} Elf32_Ehdr;
程序头表记录了每个Segment的相关信息,比如类型、对应文件的偏移、大小、属性等,程序头表包含多个程序头表项,程序头表描述的对象称为“Segment”,Segment描述的是ELF文件加载后的数据块,段(section)描述的是ELF文件加载前的数据块。
/* Program segment header. */
typedef struct
{
Elf32_Word type; /* Segment type */
Elf32_Off offset; /* Segment file offset Segment对应的内容在文件的偏移*/
Elf32_Addr vaddr; /* Segment virtual address Segment在内存中的线性地址*/
Elf32_Addr paddr; /* Segment physical address */
Elf32_Word filesz; /* Segment size in file */
Elf32_Word memsz; /* Segment size in memory */
Elf32_Word flags; /* Segment flags */
Elf32_Word align; /* Segment alignment */
} Elf32_Phdr;
#define PT_NULL 0 /* Program header table entry unused */
#define PT_LOAD 1 /* Loadable program segment */
#define PT_DYNAMIC 2 /* Dynamic linking information */
#define PT_INTERP 3 /* Program interpreter */
#define PT_NOTE 4 /* Auxiliary information */
#define PT_SHLIB 5 /* Reserved */
#define PT_PHDR 6 /* Entry for header table itself */
#define PT_TLS 7 /* Thread-local storage segment */
#define PT_NUM 8 /* Number of defined types */
我们往DDR里搬程序,主要关注着两个结构体表头就够了,接下来是搬程序的主要代码段。
首先当然是验证ELF文件头是不是0x7f E L F这4个字符。
ret = spi_flash_read(&Spi, ELF_IMAGE_BASEADDR, ReadBuffer, sizeof(hdr));
if (ret == XST_SUCCESS) {
memcpy(&hdr, ReadBuffer + SPI_VALID_DATA_OFFSET, sizeof(hdr));
}
if (hdr.ident[0] != 0x7f ||hdr.ident[1] != 'E' ||hdr.ident[2] != 'L' ||hdr.ident[3] != 'F') {
print("Invalid ELF header");
return -1;
}
验证没问题以后,根据程序头表的数目,将程序段依次从flash拷到DDR。
for (i = 0; i < hdr.phnum; i++) {
ret = spi_flash_read(&Spi, ELF_IMAGE_BASEADDR + hdr.phoff + i * sizeof(phdr), ReadBuffer, sizeof(phdr));
if (ret == XST_SUCCESS) {
memcpy(&phdr, ReadBuffer + SPI_VALID_DATA_OFFSET, sizeof(phdr));
if (phdr.type == PT_LOAD) {
for (addr = 0; addr < phdr.filesz; addr += EFFECTIVE_READ_BUFFER_SIZE) {
if (addr + EFFECTIVE_READ_BUFFER_SIZE > phdr.filesz) {
ret = spi_flash_read(&Spi, ELF_IMAGE_BASEADDR + phdr.offset + addr,ReadBuffer, phdr.filesz - addr);
} else {
ret = spi_flash_read(&Spi, ELF_IMAGE_BASEADDR + phdr.offset + addr,ReadBuffer, EFFECTIVE_READ_BUFFER_SIZE);
}
if (ret == XST_SUCCESS) {
if (addr + EFFECTIVE_READ_BUFFER_SIZE > phdr.filesz) {
memcpy((void*)phdr.paddr + addr, ReadBuffer + SPI_VALID_DATA_OFFSET, phdr.filesz - addr);
} else {
memcpy((void*)phdr.paddr + addr, ReadBuffer + SPI_VALID_DATA_OFFSET, EFFECTIVE_READ_BUFFER_SIZE);
}
} else {
print("Failed to read ELF program segment");
return -1;
}
}
if (phdr.memsz > phdr.filesz) {
memset((void*)(phdr.paddr + phdr.filesz), 0, phdr.memsz - phdr.filesz);
}
}
} else {
print("Failed to read ELF program header");
return -1;
}
}
最后,别忘了跳到程序入口。
((void (*)())hdr.entry)();
再烧写一次应用程序的ELF,这次记得不要再勾选第一个将ELF转换为SREC的选框了。
烧写完毕后,重新上电,不吹牛逼,应该也就是个3、5秒,启动了。