如何魔改Xilinx Vivado 的MIG IP核

本文介绍了如何修改Xilinx Vivado MIG IP核以适应特定硬件需求,详细阐述了从源码提取、MicroBlaze处理、引脚约束修改到DDR4控制器重构的全过程,旨在解决科研中遇到的DDR4控制器适配问题。通过这个过程,最终实现了在FCCM发表论文的研究成果。
摘要由CSDN通过智能技术生成

出于科研需求,需要修改DDR4控制器的物理层(PHY Layer)。DDR4控制器代码虽然好找,但是不一定能适配手上的ZCU104;从头开始写一个DDR4控制器工程量太大了,于是决定魔改一下Xilinx官方的MIG IP(v2.2 for Ultrascale+)核。

首先,官方的MIG并没有被lock,是可以看见源码的,也不构成侵权行为,官方论坛甚至也给出了一个修改的方法(https://forums.xilinx.com/t5/Memory-Interfaces-and-NoC/Editing-MIG-IP/td-p/902104)。但是这种修改方法不太适合大规模修改,尤其是在使用ZYNQ时,由于IP本身出于block diagram内,这方法不太适用,因此,需要寻找更彻底的修改方法,及完全重新封装这个IP的源码,毕竟它的源码是完全公开的。

既然源码是公开的,那岂不是先生成一个官方IP再把源码抠出来不就完事了?于是在做出一个官方可用版本后,我把所有源码(除了一些wrapper和simulation用的文件)全部加到了一个新工程里。它的文件结构大致可以用以下表格描述:

  • ddr4_0 //根目录
    • bd_0 // MicroBlaze的block design,主要目的可能是为了封装进elf
      • don't care files
      • bd_45eb.bd
      • bd_45eb_ooc.xdc
    • ip_0
      • don't care files
      • ddr4_microblaze_mcs.xci
    • ip_1
      • par //don't care
      • rtl
        • clocking //生成RXTX_BITSLICE的时钟(PLLE4)的模块
        • iob //生成io buffers的模块
        • ip_top //假顶层,是一个ip_wrapper,实际上没有用
        • map //逻辑向物理的映射文件和用于calibration地址映射文件
        • phy //真正的顶层文件和其下的一个小顶层,主要负责连接iob和RXTX_BITSLICE,并处理对上层的接口
        • xiphy_files //配置RXTX_BITSLICE的文件
      • don't care files
    • par
      • ddr4_0.xdc
    • rtl
      • axi //将AXI FULL向DDR4 PHY层输出的裸接口过渡
      • axi_ctrl //将AXI FULL向DDR4 PHY层输出的裸接口过渡;是一个选择关系,实际上这个文件夹中所有文件都没被用着,都是用的AXI FULL
      • cal //处理calibration以及控制权管理(是calibration控制接口还是用户读写)的一些模块
      • clocking //MMCM,生成DDR4主时钟,用户时钟,MicroBlaze (MB)时钟
      • controller //顾名思义,读写控制器
      • ip_top //顶层
      • ui //User Interface,连接并转化AXI和裸接口
    • sw
      • calibration_0
        • Debug
          • calibration_ddr.elf //装到MB里的calibration程序
    • tb //don't care
    • ddr4_0.xci
    • ddr4_0.xml
    • ddr4_0.xdc 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

其实到这里已经能发现问题了,这不是一个简单的代码,而是一个包含了子IP的复杂工程。其中甚至还有MicroBlaze这样的软核,必然需要一个在综合时关联elf文件(现在已经不允许将elf打包进ip了,只能通过打包block design的方法把elf一起封装进去,详见 https://www.xilinx.com/support/answers/67083.html)。所以这个方法可行性就比较低了,所以可能需要把MicroBlaze核心从代码中提取出来。首先来看看它的模块图(pg150, figure 3-5):

基本就是上电后MB控制复位和校准,然后交给用户。所以只需要把图中 "cal_riu"模块拎出来就好。但是其中MicroBlaze mcs是一个block design,所以我们要重新封装一下。方法是创建一个新的工程,直接加入现在ddr4_0/bd_0/bd_45eb.bd。这时候就能打开block design了。但是由于这个IP被xilinx锁住了,所以无法编辑。为了确保elf能正确加入,直接在tcl窗口中运行:

write_bd_tcl <your_folder>/microblaze_mcs.tcl

之后删除刚才加入的bd_45eb.bd,再创建一个新的blcok design,之后在tcl窗口中运行:

source <your_folder>/microblaze_mcs.tcl

这时,xilinx会生成一个和之前一模一样的block design。之后,用上文提到过的方法(https://www.xilinx.com/support/answers/67083.html)将 ddr4_0/sw/calibration_0/Debug/calibrate_ddr.elf导入,并将当前block design封装成一个独立IP备用。阅读"cal_riu"的源码(ddr4_cal_riu.sv):

module design_1_ddr4_0_0_ddr4_cal_riu(
...

design_1_ddr4_0_0_microblaze_mcs  mcs0 (
      .Clk                (riu_clk),
      .Reset              (reset_ub_riuclk),
      .IO_addr_strobe     (io_addr_strobe_ub),
      .IO_read_strobe     (),
      .IO_write_strobe    (io_write_strobe_ub),
      .IO_address         (io_address_ub),
      .IO_byte_enable     (),
      .IO_write_data      (io_write_data_ub),
      .IO_read_data       (io_read_data_ub),
      .IO_ready           (io_ready_ub),
      .TRACE_pc           (Trace_PC)
    );
...
endmodule

所以其实没几根线需要引出来,但是需要从这一层一口气拉倒top。同时注意输入和输出,不能搞反。此时,ddr4_0/bd_0和ddr4_0/ip_0中的文件已经不用管了。接下来,只需要打包其他文件即可。创建一个新的工程,导入其余所有sv, vh 和 有内容的xdc 文件。注意虽然ip_1中也是一个ip文件,但是可以直接忽略,只把源文件加入,千万不能把.xci一并加入,否则会被识别成ip,就无法修改了。这时候,按照之前说的把例化的mb删除,连接到外面。打包IP,创建新工程,导入之前打包的mb IP 和刚打包的控制器IP,按照原方式连接好,理论上来说已经可以正常工作了。这时候整个IP核看起来像这样

添加好zynq_mpsoc等等其他IP后,写好引脚约束文件,开始综合和实现(所有与ddr4相关的IO口的电平约束都在之前的文件中写好了,只需要约束位置即可)。

然后最骚的地方来了,貌似新版Vivado (我用的2019)的MIG IP核的配置页面里是不能指定引脚的约束的,也就是说当前MIG的代码其实并不能直接对应你的硬件。Vivado官方的指定做法是在配置好时钟之类的属性后,编辑引脚约束。然后,Vivado 会自动修改MIG中的一些文件从而使它适配你的硬件。由于Xilinx的代码是以HP IO上每个Byte为单位的(参见ug571),每个信号都和一个IO Byte绑定,在魔改后,Vivado不会修真代码,就会和约束产生冲突。举个例子,代码里可能默认ADDR[3]和ADDR[2]写在了一个IO Byte上,而在PCB上他们实际上在两个Byte上,冲突就产生了,导致我们无法通过implementation。那么想要解决这个问题,我们需要根据我们的硬件去修改MIG里面的某些代码,从而使代码与硬件不产生冲突。但是其实很好找,因为仔细想想既然都参数化了,一定是在一些头文件里,于是我找到了三个名字很显眼的文件:

  • design_1_ddr4_0_0_phy_ddrMapDDR4.vh
  • design_1_ddr4_0_0_phy_iobMapDDR4.vh
  • design_1_ddr4_0_0_phy_riuMap.vh

都叫Map了还不够明显吗。。。一个一个看,先看iobMap

其中是两组端口定义,分别是mcal_rd_vref_value和iob_pin

,.mcal_rd_vref_value (
{

    mcal_rd_vref_value[55:49],
    mcal_rd_vref_value[48:42],
    mcal_rd_vref_value[41:35],
    mcal_rd_vref_value[34:28],
    mcal_rd_vref_value[27:21],
    mcal_rd_vref_value[20:14],
    7'b0,
    7'b0,
    mcal_rd_vref_value[13:7],
    mcal_rd_vref_value[6:0]
}
)

其中很明显是一个7*8的v_ref向量,和两个组0。结合DDR的接口定义和ug571中HP IO的特性,我们可以猜到其中8组Vref显然对应了64位DDR上每8bit一组的DQ Bus;而0则是因为Command 总线只有输出,自然就不在乎输入的Vref了。那结合ZCU104的原理图我们用了3个Byte完成CMD的输出,数据的书序也不是顺序,所以我把他们对应改成了:

.mcal_rd_vref_value (
{
    mcal_rd_vref_value[34:28],
    mcal_rd_vref_value[41:35],
    mcal_rd_vref_value[48:42],
    mcal_rd_vref_value[55:49],
    mcal_rd_vref_value[13:7],
    mcal_rd_vref_value[6:0],
    mcal_rd_vref_value[20:14],
    mcal_rd_vref_value[27:21],
    7'b0,
    7'b0,
    7'b0
}

iob_pin 明显就是要根据原理图来重新填,在这里给出我的修改(只适用于ZCU104)

.iob_pin (
{
//byte 10, 15/14
ddr4_nc[24],//18/11
ddr4_dq[32],//17/10
ddr4_dq[33],//16/F
ddr4_dq[34],//15/E
ddr4_dq[35],//14/D
ddr4_dqs_c[4],//13/C
ddr4_dqs_t[4],//12/B
ddr4_dq[36],//17/10
ddr4_dq[37],//16/F
ddr4_dq[38],//15/E
ddr4_dq[39],//14/D
ddr4_nc[23],//13/C
ddr4_dm_dbi_n[4],//12/B
//byte 9, 13/12
ddr4_nc[22],//18/11
ddr4_dq[40],//17/10
ddr4_dq[41],//16/F
ddr4_dq[42],//15/E
ddr4_dq[43],//14/D
ddr4_dqs_c[5],//13/C
ddr4_dqs_t[5],//12/B
ddr4_dq[44],//17/10
ddr4_dq[45],//16/F
ddr4_dq[46],//15/E
ddr4_dq[47],//14/D
ddr4_nc[21],//13/C
ddr4_dm_dbi_n[5],//12/B
//byte 8, 11/10
ddr4_nc[20],//18/11
ddr4_dq[48],//17/10
ddr4_dq[49],//16/F
ddr4_dq[50],//15/E
ddr4_dq[51],//14/D
ddr4_dqs_c[6],//13/C
ddr4_dqs_t[6],//12/B
ddr4_dq[52],//17/10
ddr4_dq[53],//16/F
ddr4_dq[54],//15/E
ddr4_dq[55],//14/D
ddr4_nc[19],//13/C
ddr4_dm_dbi_n[6],//12/B
//byte 7, F/E
ddr4_nc[18],//18/11
ddr4_dq[56],//17/10
ddr4_dq[57],//16/F
ddr4_dq[58],//15/E
ddr4_dq[59],//14/D
ddr4_dqs_c[7],//13/C
ddr4_dqs_t[7],//12/B
ddr4_dq[60],//17/10
ddr4_dq[61],//16/F
ddr4_dq[62],//15/E
ddr4_dq[63],//14/D
ddr4_nc[17],//13/C
ddr4_dm_dbi_n[7],//12/B
//byte 6, D/C
ddr4_nc[16],//18/11
ddr4_dq[8],//17/10
ddr4_dq[9],//16/F
ddr4_dq[10],//15/E
ddr4_dq[11],//14/D
ddr4_dqs_c[1],//13/C
ddr4_dqs_t[1],//12/B
ddr4_dq[12],//17/10
ddr4_dq[13],//16/F
ddr4_dq[14],//15/E
ddr4_dq[15],//14/D
ddr4_nc[15],//13/C
ddr4_dm_dbi_n[1],//12/B
//byte 5, B/A
ddr4_nc[14],//18/11
ddr4_dq[0],//17/10
ddr4_dq[1],//16/F
ddr4_dq[2],//15/E
ddr4_dq[3],//14/D
ddr4_dqs_c[0],//13/C
ddr4_dqs_t[0],//12/B
ddr4_dq[4],//17/10
ddr4_dq[5],//16/F
ddr4_dq[6],//15/E
ddr4_dq[7],//14/D
ddr4_nc[13],//13/C
ddr4_dm_dbi_n[0],//12/B
//byte 4, 9/8
ddr4_nc[12],//18/11
ddr4_dq[16],//17/10
ddr4_dq[17],//16/F
ddr4_dq[18],//15/E
ddr4_dq[19],//14/D
ddr4_dqs_c[2],//13/C
ddr4_dqs_t[2],//12/B
ddr4_dq[20],//17/10
ddr4_dq[21],//16/F
ddr4_dq[22],//15/E
ddr4_dq[23],//14/D
ddr4_nc[11],//13/C
ddr4_dm_dbi_n[2],//12/B
//byte 3, 7/6
ddr4_nc[10],//18/11
ddr4_dq[24],//17/10
ddr4_dq[25],//16/F
ddr4_dq[26],//15/E
ddr4_dq[27],//14/D
ddr4_dqs_c[3],//13/C
ddr4_dqs_t[3],//12/B
ddr4_dq[28],//17/10
ddr4_dq[29],//16/F
ddr4_dq[30],//15/E
ddr4_dq[31],//14/D
ddr4_nc[9],//13/C
ddr4_dm_dbi_n[3],//12/B
//byte 2, 5/4
ddr4_nc[8],//18/11
ddr4_nc[7],//17/10
ddr4_cke[0],//16/F
ddr4_nc[28],//15/E
ddr4_adr[15],//14/D
ddr4_cs_n[0],//13/C
ddr4_adr[14],//12/B
ddr4_nc[27],//17/10
ddr4_nc[26],//16/F
ddr4_bg[0],//15/E
ddr4_act_n,//14/D
ddr4_odt[0],//13/C
ddr4_adr[16],//12/B
//byte 1, 3/2
ddr4_adr[0],//18/11
ddr4_adr[1],//17/10
ddr4_adr[2],//16/F
ddr4_adr[3],//15/E
ddr4_adr[4],//14/D
ddr4_adr[5],//13/C
ddr4_adr[6],//12/B
ddr4_adr[7],//17/10
ddr4_nc[25],//16/F
ddr4_ck_c[0],//15/E
ddr4_ck_t[0],//14/D
ddr4_nc[6],//13/C
not_used,//12/B
//byte 0, 1/0
ddr4_nc[4],		//18/11
ddr4_nc[3],		//17/10
ddr4_nc[2],		//16/F
ddr4_adr[8],	//15/E
ddr4_adr[9],	//14/D
ddr4_adr[10],	//13/C
ddr4_adr[11],	//12/B
ddr4_adr[12],	//17/10
ddr4_adr[13],	//16/F
ddr4_ba[0],		//15/E
ddr4_ba[1],		//14/D
ddr4_nc[1],		//13/C
ddr4_nc[0]		//12/B
}	
)

我们可以结合原理图看一下,这里就看CMD这一块

由于CKE1和ODT1没用到,所以直接写成ddr4_nc里的线。这个ddr4_nc就是专门定义出来丢垃圾的,后来没有被用到,只是用来对齐位。聪明的小伙伴应该一看就知道是咋对应的了。

改完这个,我们看看ddrMap,这里面是很多组端口映射。应该是将IO的线网名字对应到DDR4控制逻辑的线网。由于DDR4速度很高,所以Xilinx使用了每一个IO口都有的RXTX_Bitslice。其中包含了一个1-8串并转换器。换句话说,任何每一个IO口到这里对应的位宽都乘以了8。Xilinx的定义方式是按照每个IO Byte上的对应位来对齐。一个IO Byte上有13位,所以就有13组rd,wr,fifo_rden等等。这个时候填写的方式是按照之前我们iopin里Byte的顺序,找到每Byte中同一位,*8 后填入相应一组中,比如clb2phy_wr_dq11这一组,就是输出的每个Byte中的第11(或者12,从1数的话)位,*8后组合。那可以查一下iomap,从下往上一次是无输出,ADR[1],无输出,dq[24], dq[16],dq[0],dq[8],dq[56],dq[48],dq[40],dq[32],注意数据位要乘8作为起始位(串并转换导致),然后向上数八位,于是我们可以写出

.clb2phy_wr_dq11 (
    { 
        mcal_DQOut[263:256],
        mcal_DQOut[327:320],
        mcal_DQOut[391:384],
        mcal_DQOut[455:448],
        mcal_DQOut[71:64],
        mcal_DQOut[7:0],
        mcal_DQOut[135:128],
        mcal_DQOut[199:192],
        8'bx,
        mcal_ADR[15:8],
        8'bx
    } 

由于是修改,其实是重排序,不要修改变量名就不会出问题。在这里贴出完全修改玩的代码,同样只针对ZCU104


,.phy2clb_rd_dq0 (
    { 
        mcal_DMIn_n[39:32],
        mcal_DMIn_n[47:40],
        mcal_DMIn_n[55:48],
        mcal_DMIn_n[63:56],
        mcal_DMIn_n[15:8],
        mcal_DMIn_n[
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值