出于科研需求,需要修改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用的文件)全部加到了一个新工程里。它的文件结构大致可以用以下表格描述:
|
其实到这里已经能发现问题了,这不是一个简单的代码,而是一个包含了子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[