软件环境:vivado 2018.1        硬件平台:XC7K325 


接上篇文章最后的问题,microblaze的lwip工程从qspi启动需要7、8分钟,时间过长的问题排查感觉可以从以下几个方面入手

1.首先,工程是可以正常使用,正常网络通讯的,这样的话,工程通常可以认为是没有问题的,那么,提高或者降低系统的时钟频率,是否对启动时间有影响,如果时间过长的问题是因为时钟频率不够,那么提高或降低系统时钟频率,这个启动时间应该会相应的增加或者减少。

2.开机启动时候的打印,众所周知,这个printf的打印是非常耗时的,如果启动时间过长是因为大部分时间都花在了printf的打印上,那么屏蔽掉相关的打印函数,接下来的启动时间就会非常短,相反,如果其他部分耗时比打印需要的时间长,那么屏蔽掉打印相关的调用之后,启动的时间应该是不变的。

RK3566 烧写ANDROID_#define

3.我一直比较在意烧写flash时候专门勾的这个convert ELF to bootloadable SREC format and program,将ELF应用程序转换为能够boot的SREC格式文件,为什么ELF应用不能直接搬进DDR运行,还非要通过SREC来boot,这个SREC什么来头,会不会有问题。

RK3566 烧写ANDROID_RK3566 烧写ANDROID_02


 通过上面三个问题,我逐一进行测试之后发现

1.我成倍的增加或者降低系统时钟频率,启动时间确实会近似成倍的增加或者减少,说明这里系统时钟是有影响的,但也比较尴尬,举个例子,系统时钟100 MHz的时候,启动时间需要8分钟,系统时钟倍频到200 MHz,启动时间4分钟左右,你说这有效吗,这有效,但是4分钟还是长啊,你能继续倍到400 MHz吗?对不起,你不能,你能板子也不能,通过时钟改善启动问题,只能说治标不治本,因为时钟不能无限制的放大。而且,100~200 MHz的时钟很正常啊,就这都还要花这么长时间启动,只能怀疑是不是有其他地方,耗费了太多的时钟周期。

2.通过在bootloader.c这个文件中,屏蔽VERBOSE,启动时候的调试信息就不会打印出来了,但是屏蔽了以后,虽说是不打印了,但是启动时间还是那么多,依旧没有减少。所以就像我说的,肯定有地方耗费的时间比打印时间多的多,所以屏蔽掉打印以后,对启动时间才会丝毫没影响。

RK3566 烧写ANDROID_RK3566 烧写ANDROID_03

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>

RK3566 烧写ANDROID_单片机_04

  <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的选框了。

RK3566 烧写ANDROID_单片机_05

烧写完毕后,重新上电,不吹牛逼,应该也就是个3、5秒,启动了。

RK3566 烧写ANDROID_数据_06