最近在学Xilinx公司的开发流程,正巧学到了MicroBlaze处理器,由于使用的这款VC707开发板没有像ZYNQ系列集成了ARM的硬核,所以在处理一些复杂的逻辑的时候还是要借助软核来处理。但是在做MicroBlaze开发的时候遇到了一点问题,导致我花了很多天的时间去思考和测试。
主要问题是,当我在使用MicroBlaze处理器的时候,我希望能够实现上电之后从flash加载程序,并且执行。
但是查到的资料多都是使用片上的RAM资源来做RAM,然后在生成FPGA的bit文件的时候将代码嵌进去。当上电加载的时候,程序就在RAM中,这样自然可以直接运行。但是片上RAM寸土寸金,我程序大一点岂不是成本巨大?于是就有另外的方法了,那就是用一块小的片上RAM,给里边装上SREC Bootloader,实现上电了之后将程序加载到外部的SDRAM中,这个方法虽然折中了许多,但是给我的一个直接印象就是:
不美!
因为这一块小的片上RAM,它完成了Boot任务之后,我究竟该不该使用它呢?用的话容量太小也没有什么用处,不用的话,那么这些资源完全属于浪费,能挪到别的地方该多好?所以这块RAM实属鸡肋。
所以这几天我便研究了一下如何在不使用这块片上RAM的情况下完成上电自启。
首先分析原理:当不使用片上RAM的时候,也就是说我的代码一开始是放在flash当中的,所以我需要一段程序将flash中的程序复制进SDRAM里,因为flash是NAND Flash所以不能直接将flash当RAM使用,而SREC flash loader虽然也是完成这个任务,不过观察其代码,由于它使用了data空间,所以在SREC loader程序运行前还是需要将其本身加载到RAM中去。
所以详细的解决方案是:先编写一段程序将SREC flash loader复制到SDRAM中,再使用SREC flash loader加载用户程序。
先看硬件工程设计:
先将MicroBlaze上的LMB关掉,并且启用AXI指令总线。将指令总线和数据总线同时接到DDR Memory和External Flash上。
分配地址空间,这里我将DDR RAM分配在0x0至0x3FFFFFFF之间,将Flash分配在0x60000000之后。
由于我使用的是VC707评估板,其上搭载的是Virtex-7 XC7VX485T-2FFG1761C这块FPGA,其配置bit文件的大小为20273588字节即0x13559B4字节,所以从0x60000000到0x613559B4这段地址空间是不能使用的,所以我将程序分配在0x61800000之后。因此调整MicroBlaze的reset地址:
在例化的时候,可能会碰到一条Critical Warning,这里可以不用理会,这是由于没有使用片上RAM引起的。
接下来进行软件开发,首先编译生成SREC Bootloader:
将blconfig.h中flash映像地址改成如图所示。这个地址便是之后用户程序存储点。
将链接脚本中的配置更改为:
主要注意修改DDR的Base Address,由于Bootloader在加载用户程序的时候我们不希望其将自己破坏,所以放在内存中高位的位置。如果提示空间不足的话,可以考虑降低一点基地址或者减小一点堆栈空间。
编译生成elf文件后,还不能直接拿来使用,需要将其转换为内存映像,才能写入flash。
在SDK的Xilinx->Launch Shell中打开命令行,进入包含编译好elf文件的目录执行下面的命令生成内存映像文件:
data2mem -bd FILNAME.elf -d -o m FILNAME.mem
用UltraEdit打开文件,可以看到各个不同部分的内存数据已经全部被导出来了。
将0x3FFFE000后面这一段数据复制出来(其余区段部分为中断和复位跳转,可以删掉),转换成二进制流文件,这里我自己编写了一个简陋的小工具来完成这个操作(可执行程序和源码见下面的压缩包)。
mem2bit -i FILNAME -o FILNAME
这样便生成了SREC Bootloader的内存映像文件。
但是光是这样也是不够的,还需要一段程序将其复制进入内存中,否则其对data段数据的写入操作会导致程序出错。
所以需要自己编写一段程序来完成这个操作。打开记事本编写并保存以下这段汇编代码,并更改其中的地址量适配自己的系统。
.global _start _start: addi r1, r0, 0x00001FF0 #r1 is flash data size addi r2, r0, 0x61900000 #r2 is flash pointer addi r3, r0, 0x3FFFE000 #r3 is mem pointer addi r4, r0, -4*5 #r4 is loop back instruction counter lwi r5, r2, 0 #load a 32-bit(4byte) data from flash swi r5, r3, 0 #store the data to DDR memory addi r2, r2, 4 #increase flash pointer by 4 addi r3, r3, 4 #increase memory pointer by 4 addi r1, r1, -4 #decrease flash data counter bne r1, r4 #check if flash data is all copied brai 0x3FFFE000 #jump to srec bootloader place
继续在Shell中输入命令将其转成内存映像文件:
mb-as FILNAME -o FILNAME.o -mlittle-endian mb-ld FILNAME.o -Ttext 0x0 -o FILNAME.elf -oformat elf32-microblaze data2mem -bd FILNAME.elf -d -o m FILNAME.mem mem2bit -i FILNAME -o FILNAME
到此为止我分别得到了两个boot文件,分别是自己编写的boot0.boot,另一个是SRECBoootloader的boot.boot
接下来使用SDK中的flash工具将其烧写进相应的位置:
这样整个bootloader部分就做好了,接下来将应用程序烧写进0x61A00000中去便可以启动了。当然在编译应用程序前要注意修改程序的复位和中断点,由于最开始设置了复位地址,所以xilinx工具会自动生成在flash中的复位点,这里要注意。
将程序编译好了之后写入flash相应位置,需要设置为SREC可加载格式。
接下来将FPGA配置文件写入flash,便可以实现上电自启了。效果如图:
至此MicroBlaze不使用片上RAM自启动便做好了。
补充:在写完文章后,开发的过程中又碰到问题。是由于MicroBlaze的中断、异常的地址没有找到单独更改的方法,在手册中它们是以复位地址为基础加的(这一点就和Nios II有点差距了,灵活性明显不如Nios II),所以在做boot0的时候,需要将中断异常等进行重定向。所以将汇编部分的代码更改为:
.section .vectors.reset,"ax" .global _start _start: brai _main #reset jump .section .vectors.sw_exception,"ax" _vectors_usrvec: brai 0x00000008 #user vector jump .section .vectors.interrupt,"ax" _vectors_int: brai 0x00000010 #interrupt jump .section .vectors.break,"ax" _vectors_break: brai 0x00000018 #break jump .section .vectors.hw_exception,"ax" _vectors_exception: brai 0x00000020 #hardware exception jump .section .text _main: addi r1, r0, 0x00001FF0 #r1 is flash data size addi r2, r0, 0x61900000 #r2 is flash pointer addi r3, r0, 0x3FFFE000 #r3 is mem pointer addi r4, r0, -4*5 #r4 is loop back instruction counter lwi r5, r2, 0 #load a 32-bit(4byte) data from flash swi r5, r3, 0 #store the data to DDR memory addi r2, r2, 4 #increase flash pointer by 4 addi r3, r3, 4 #increase memory pointer by 4 addi r1, r1, -4 #decrease flash data counter bne r1, r4 #check if flash data is all copied brai 0x3FFFE000 #jump to srec bootloader place
同时在链接时应该使用-T参数加上链接脚本:
ENTRY(_start) SECTIONS { .vectors.reset 0x61800000 : { *(.vectors.reset) } .vectors.sw_exception 0x61800008 : { *(.vectors.sw_exception) } .vectors.interrupt 0x61800010 : { *(.vectors.interrupt) } .vectors.break 0x61800018 : { *(.vectors.break) } .vectors.hw_exception 0x61800020 : { *(.vectors.hw_exception) } .text 0x61800050 : { *(.text) } }
其余步骤均与原文相同。
(完)
附:mem2bit源码:
// mem2bit.cpp : 定义控制台应用程序的入口点。 // #include #include int main(int argc, char *argv[]) { char *infile_name, *outfile_name; bool flag_infile, flag_outfile; std::fstream infile; std::fstream outfile; int byte_buf; flag_infile = false; flag_outfile = false; for (size_t i = 0; i < argc; i++) { if (strcmp(argv[i], "-i") == 0) { infile_name = argv[i + 1]; flag_infile = true; continue; } if (strcmp(argv[i], "-o") == 0) { outfile_name = argv[i + 1]; flag_outfile = true; continue; } } if (!(flag_infile & flag_outfile)) { std::cout << "Useage: mem2bit -i -o \n"; return 1; } infile.open(infile_name, std::ios::in); if (!infile.is_open()) { std::cout < < "Open" << infile_name << "Error!\n"; return 1; } outfile.open(outfile_name, std::ios::out | std::ios::binary); if (!infile.is_open()) { infile.close(); std::cout << "Open" << outfile_name << "Error!\n"; return 1; } while (!infile.eof()) { infile >> std::hex >> byte_buf; outfile.write((char *)&byte_buf, 1); } std::cout < < "Generate done!\n"; infile.close(); outfile.close(); return 0; }