由于项目需求,希望将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实现(先不管写入功能): image.png

潘多拉的魔盒就此打开,我先用芯来的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菜单找到这个例化模块)。 image.png

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实现。 image.png

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功能,非常容易替换。 image.png

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设置可以参考截图。

标签: E203

添加新评论