Hummingbirdv2 E203 移植之固化itcm
由于项目需求,希望将Hummingbird2 E203 SoC移植到Xilinx Zynq UltraScale+ MPSoC 上,核心板是XCU3EG。期间遇到一个大坑,debug足足用了7个小时,惨不忍睹。
一、Flash不够
芯来自己的MCU200T的开发板,有两个Flash:一个是FPGA Flash,用于保存mcs文件,上电时加载硬件电路,另一个是MCU Flash,用于保存软件二进制代码。芯来有自己的SDK做软件调试,软件代码编译成二进制文件后,有三种方式写入E203的指令存储器(itcm)做调试:
- ilm: 直接将代码写入指令存储器(itcm),速度最快,但掉电会消失
- flash:先保存到mcu flash,然后再拷贝到itcm上执行,掉电不消失
- flashxip:直接保存到mcu flash,并读flash运行,速度较慢
但MPSoC只在ARM核的PS端有下载用的FPGA Flash,没有MCU Flash,如果需要掉电保存软件代码,该怎么办呢?
二、修改itcm
E203的指令存储器是通过寄存器实现的,如果我们把它修改成只读存储器ROM,然后预先读入.coe文件,那么就可以和E203一起烧入flash,这样一上电就可以运行代码。
说干就干,那么用哪种IP呢?BRAM还是Distributed ROM? 这得通过代码仔细分析时序:
//读部分逻辑
reg [DW-1:0] mem_r [0:DP-1]; //DW=32,DP=8192
always @(posedge clk)
begin
if (ren) begin
addr_r <= addr;
end
end
assign dout_pre = mem_r[addr_r];
这点挺有意思,当可读时,先一拍保存地址addr_r,然后直接读出数据,而且不可读的时候,输出会保持,这点用rom实现起来挺方便,于是便决定用简单的rom实现(先不管写入功能):
潘多拉的魔盒就此打开,我先用芯来的SDK生成.verilog的代码二进制文件(可以参考这篇文章),然后以.coe文件的方式读入rom。有一点需要注意,.verilog文件还需要调整数据顺序,这里写了一个python脚本,实现转换:
#python hello.verilog
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-f','--file',dest='file',type=str,default='hummibird.verilog')
args = parser.parse_args()
file_data = "memory_initialization_radix=16;\nmemory_initialization_vector=\n"
total_len = 0
str_16 = []
with open(args.file,"r",encoding="utf-8") as f:
for line in f:
if(line[0]=="@"):
temp = eval("0x"+line.strip("\n")[1:])
for i in range(temp - total_len):
str_16.append("00")
total_len += 1
else:
temp = line.strip("\n").split(" ")
for i in range(len(temp)):
str_16.append(temp[i])
total_len +=1
for i in range(8-total_len % 8):
str_16.append("00")
row = int(total_len//8)+1
for i in range(row):
file_data += str_16[i*8+7]+ str_16[i*8+6]+str_16[i*8+5]+str_16[i*8+4]\
+str_16[i*8+3]+str_16[i*8+2]+str_16[i*8+1]+str_16[i*8+0]+",\n"
with open("out.coe","w",encoding= "utf-8") as f:
f.write(file_data)
三、恶梦debug
看似一切完美,指令存储器不需要写入功能,tb仿真应该一切正常。检测了开始的几个周期,确定指令确实正确写入,但之后却一直不输出结果。好在另一台电脑有备份,跑一波正确的版本,发现2.5us之后,指令输出就出错了,为之奈何?说实话,当时真没勇气搞定这个bug,下了很大决心才决定回溯信号。
从这个output找,到底那个input出了问题,模块之间不停套娃。在这个过程中,还发现一个vivado的彩蛋:vivado和modelsim在仿真时不会保存所有信号,如果要查看其他信号的变化,就需要重新仿真,这一点不如iverilog和vcs方便;但vivado有个彩蛋是,它保存了仿真最后时刻所有信号的信息,而且将鼠标停留在代码上的信号,会显示该信号的数值(如果不显示,可以在scope菜单找到这个例化模块)。
tip: 仿真到bug出现的时刻点,然后可以在代码中查看此刻各信号的数值,大大加快速度。
第一阶段回溯到了寄存器堆regfile某个地址数据没有对上,接着看它是什么时候发生错误反转,再仿真都这个时刻,最后回溯最终是那个输入发生错误。
经过漫长的回溯,最终找到了数据存储器,scope一看,进入例化模块是rom!原来itcm和dtcm用的是同一个module,这样itcm改成了rom,dtcm也就不能写入!抽丝剥茧,终于搞定。
只需要另外添加一个itcm的module,分类例化即可,问题解决。
四、能不能加入写功能?
itcm的写功能主要用于调试,借助其SDK,通过Jtag口将代码直接传入itcm,方便debug,否则每次修改代码都需要重新烧一遍板子,非常费时费力。
写逻辑也挺有意思,除了常见的写使能we,还有mask信号wem,来确定32bit里面,哪几位8bit需要修改。分析了一下时序,动了些小聪明,发现可以用Dualport RAM实现。
module sirv_sim_ram_itcm
#(parameter DP = 512,
parameter FORCE_X2ZERO = 0,
parameter DW = 32,
parameter MW = 4,
parameter AW = 32
)
(
input clk,
input [DW-1 :0] din,
input [AW-1 :0] addr,
input cs,
input we,
input [MW-1:0] wem,
output [DW-1:0] dout
);
reg [AW-1:0] addr_r;
wire [MW-1:0] wen;
wire ren;
wire [DW-1:0] dout_pre,spo,din_temp,wen_ext;
assign din_temp = (wen_ext & din) | (~wen_ext &spo); //下一时刻更新数据
assign wen = ({MW{cs & we}} & wem);
assign wen_ext={ {8{wen[7]}},{8{wen[6]}},{8{wen[5]}},{8{wen[4]}},{8{wen[3]}},{8{wen[2]}},{8{wen[1]}},{8{wen[0]}}};
dist_mem_gen_rewr u_rewr(
.a(addr), //写地址
.d(din_temp), //写数据
.dpra(addr_r), //读地址
.clk(clk),
.we(we), //读使能
.spo(spo), //写地址的当前数据,需要结合wen和din得到下一时刻的更新数据din_temp
.dpo(dout_pre) //读数据
);
assign ren = cs & (~we);
always @(posedge clk)
begin
if (ren) begin
addr_r <= addr;
end
end
endmodule
这样就不仅能烧录flash,而且运行时可以修改代码做测试,最后可以将测试好的代码重新烧写进flash,方便开发。
五、能不能用BRAM?
总感觉写功能的实现挺low,是否更快捷的方式?查看xilinx关于ram的说明文档发现bram自带wea功能,非常容易替换。
module sirv_sim_ram
#(parameter DP = 512,
parameter FORCE_X2ZERO = 0,
parameter DW = 32,
parameter MW = 4,
parameter AW = 32
)
(
input clk,
input [DW-1 :0] din,
input [AW-1 :0] addr,
input cs,
input we,
input [MW-1:0] wem,
output [DW-1:0] dout
);
reg [AW-1:0] addr_r;
wire [MW-1:0] wen;
wire ren;
wire [DW-1:0] dout_pre;
assign ren = cs & (~we);
assign wen = ({MW{cs & we}} & wem);
blk_mem_gen_itcm u_dtcm(
.clka(clk),
.addra(addr),
.dina(din),
.wea(wen),
.clkb(clk),
.addrb(addr),
.doutb(dout_pre),
.enb(ren)
);
endmodule
几个特别注意点:
- itcm的位宽是64bit,长度8192;dtcm的位宽是32bit,长度是16384,这一点在e203_define.v和config.v里面有说明,添加IP的时候要注意这两个不同点。
- Basic 勾选 Write Enable,Byte Size 选8bits
- Port A Options ,Enable Port Type 选择“Always Enabled”
- PortB Options,不要选 Primitives Output Register
- Summary 确认 PortB Latency为1 Cycles
最后附上这些模块的代码,区分了itcm和dtcm,上层例化的时候要注意,相应的ram ip设置可以参考截图。