XC7K325T_NetFirm-4E40-C
XC7K325T_NetFirm-4E40-C
背景
闲鱼上看到一个7K325T板子,型号是NetFirm-4E40-C,带4个光模块座子、DDR接口和PCIE接口,可以用来作为开发板使用,于是买了一套回来准备逆向一下管脚。


逆向管脚
JTAG接口
观察板子,板子边缘有个双排插针J1,一般这个就是JTAG调试口了,使用万用表测试所有IO,发现有7个是接地,有1个是2.5V电源,有2个貌似是悬空的,另外4个和地之间有二极管特性,应该就是JTAG引脚了。由于板上没有丝印,这里就只能通过遍历的方法尝试,好在只有4个引脚,很快就能试出来。
| 管脚号 | 信号 | 管脚号 | 信号 |
|---|---|---|---|
| 1 | GND | 2 | 2.5V |
| 3 | GND | 4 | TMS |
| 5 | GND | 6 | TCK |
| 7 | GND | 8 | TDO |
| 9 | GND | 10 | TDI |
| 11 | GND | 12 | 悬空? |
| 13 | GND | 14 | 悬空? |
搭建Vivado工程
参考之前逆向FPGA板子的经验,在知道芯片型号是XC7K325TFFG900的前提下,搭建Vivado工程,把FPGA的所有500个用户IO使用AXI-GPIO IP配置成GPIO接口,再使用JTAG-AXI IP实现JTAG转AXI接口,将JTAG-AXI与AXI-GPIO IP通过AXI互联起来,这样就可以实现通过JTAG命令控制所有IO口输出高低电平或读取IO输入状态了。
由于现在还不知道时钟信号引脚,只能使用FPGA内部产生的时钟CFGMCLK,这是FPGA用来上电从FLASH加载程序的时钟,可以通过原语调用。有了时钟信号后通过计时产生复位信号,时钟和复位作为前面提到的JTAG-AXI和AXI-GPIO IP的AXI时钟和复位。
module gen_cfgclk(
output clk,
output reg resetn
// input done
);
wire cfgmclk;
// <-----Cut code below this line---->
// STARTUPE2: STARTUP Block
// Kintex-7
// Xilinx HDL Language Template, version 2018.3
STARTUPE2 #(
.PROG_USR("FALSE"), // Activate program event security feature. Requires encrypted bitstreams.
.SIM_CCLK_FREQ(0.0) // Set the Configuration Clock Frequency(ns) for simulation.
)
STARTUPE2_inst (
.CFGCLK(), // 1-bit output: Configuration main clock output
.CFGMCLK(cfgmclk), // 1-bit output: Configuration internal oscillator clock output
.EOS(), // 1-bit output: Active high output signal indicating the End Of Startup.
.PREQ(), // 1-bit output: PROGRAM request to fabric output
.CLK(1'b0), // 1-bit input: User start-up clock input
.GSR(1'b0), // 1-bit input: Global Set/Reset input (GSR cannot be used for the port name)
.GTS(1'b0), // 1-bit input: Global 3-state input (GTS cannot be used for the port name)
.KEYCLEARB(1'b1), // 1-bit input: Clear AES Decrypter Key input from Battery-Backed RAM (BBRAM)
// .PACK(), // 1-bit input: PROGRAM acknowledge input
.USRCCLKO(1'b0), // 1-bit input: User CCLK input
// For Zynq-7000 devices, this input must be tied to GND
.USRCCLKTS(1'b1), // 1-bit input: User CCLK 3-state enable input
// For Zynq-7000 devices, this input must be tied to VCC
.USRDONEO(1'B1), // 1-bit input: User DONE pin output control
.USRDONETS(1'b0) // 1-bit input: User DONE 3-state enable output
);
// End of STARTUPE2_inst instantiation
reg [31:0] rst_cnt;
always@(posedge clk) begin
if(rst_cnt<32'd50000000)begin
rst_cnt <= rst_cnt + 1;
resetn <= 1'b0;
end else begin
rst_cnt <= rst_cnt;
resetn <= 1'b1;
end
end
// <-----Cut code below this line---->
// BUFG: Global Clock Simple Buffer
// Kintex-7
// Xilinx HDL Language Template, version 2018.3
BUFG BUFG_inst (
.O(clk), // 1-bit output: Clock output
.I(cfgmclk) // 1-bit input: Clock input
);
// End of BUFG_inst instantiation
endmodule

一个AXI-GPIO可控制2组GPIO,每组32个共64个。500个用户IO一共需要例化8个AXI-GPIO IP,最后一组GPIO只有20位。在地址编辑器中为所有GPIO IP分配地址,这里让每个IP的基地址间隔一样,方便后续操作。

接下来需要约束管脚,分配每个GPIO到特定的管脚上,这一步具体哪个管脚分配到哪个GPIO上并不重要,在I/O Ports界面选中所有IO后通过Place I/O Ports in Area功能,让Vivado自己分配就可以了。因为板上带DDR3,这里IO电平就选LVCMOS15。

分配完管脚后直接综合布线,没问题的话就可以生成比特流了。
使用JTAG操作GPIO
根据上一步搭建的工程,所有GPIO现在都可以通过JTAG发送AXI读写命令操作AXI-GPIO内部的寄存器实现GPIO输入输出控制了。参考AXI-GPIO IP的使用手册,内部2组GPIO分别有方向控制接触器、输出状态寄存器和输入状态寄存器,通过操作这三个寄存器就可以控制每一个GPIO。将JTAG读写AXI-GPIO寄存器命令封装成tcl脚本如下:
set hw_axi_name "hw_axi_1"
set DUAL_CHANNAL 1
set GPIO_NUMBER 500
set GPIO_CHANNAL_NUMBER 32
set GPIO_ADDR_BASE 0x40000000
set GPIO_ADDR_SIZE 0x10000
set REG_DATA 0x0
set REG_TRI 0x4
set REG_DATA2 0x8
set REG_TRI2 0xc
## 寄存器写入 ##
proc WriteReg {address data} {
create_hw_axi_txn write_reg [get_hw_axis $::hw_axi_name] -type write -address [format %08x $address] -len 1 -data [format "%08x" $data] -force
run_hw_axi write_reg
delete_hw_axi_txn write_reg
# puts "write reg:\taddr:[format '0x%08x' $address]\tdata:[format '0x%08x' $data]"
}
## 寄存器读取 ##
proc ReadReg {address} {
create_hw_axi_txn read_reg [get_hw_axis $::hw_axi_name] -type read -address [format %08x $address] -len 1 -force
run_hw_axi read_reg
set read_data [lindex [report_hw_axi_txn read_reg] 1]
# puts "read reg:\taddr:[format '0x%08x' $address]\tdata:0x$read_data"
return [expr 0x$read_data]
}
# index GPIO编号,从0开始
# direction 方向,1为输入,0为输出
proc SetGpioDirection {index direction} {
set group_offset [expr {$index / $::GPIO_CHANNAL_NUMBER}]
set bank_offset [expr {$index % $::GPIO_CHANNAL_NUMBER}]
if {$::DUAL_CHANNAL== 0} {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $::GPIO_ADDR_SIZE * $group_offset}]
set addr_tri [expr {$gpio_bank_base + $::REG_TRI}]
} else {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $group_offset / 2 * $::GPIO_ADDR_SIZE}]
set addr_tri [expr {$gpio_bank_base + $::REG_TRI + $group_offset % 2 * 0x8}]
}
set value [ReadReg $addr_tri]
if {$direction == 0} {
set value [expr {$value&(~(0x1<<$bank_offset))}]
} else {
set value [expr {$value|(0x1<<$bank_offset)}]
}
# puts "set direction index:$index\taddr:[format '0x%x' $addr_tri]\tvalue:[format '0x%08x' $value]"
WriteReg $addr_tri $value
}
# index GPIO编号,从0开始
# direction 方向,1为输入,0为输出
proc SetGpioGroupDirection {group direction} {
if {$::DUAL_CHANNAL== 0} {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $::GPIO_ADDR_SIZE * $group}]
set addr_tri [expr {$gpio_bank_base + $::REG_TRI}]
} else {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $group / 2 * $::GPIO_ADDR_SIZE}]
set addr_tri [expr {$gpio_bank_base + $::REG_TRI + $group % 2 * 0x8}]
}
if {$direction == 0} {
if {$group == 2} {
set value 0x2
} else {
set value 0x0
}
} else {
set value 0xffffffff
}
# puts "set direction index:$index\taddr:[format '0x%x' $addr_tri]\tvalue:[format '0x%08x' $value]"
WriteReg $addr_tri $value
}
# index GPIO编号,从0开始
# data 输出值
proc SetGpioData {index data} {
if {$index == 65} {
#return
}
set group_offset [expr {$index / $::GPIO_CHANNAL_NUMBER}]
set bank_offset [expr {$index % $::GPIO_CHANNAL_NUMBER}]
if {$::DUAL_CHANNAL== 0} {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $::GPIO_ADDR_SIZE * $group_offset}]
set addr_data [expr {$gpio_bank_base + $::REG_DATA}]
} else {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $group_offset / 2 * $::GPIO_ADDR_SIZE}]
set addr_data [expr {$gpio_bank_base + $::REG_DATA + $group_offset % 2 * 0x8}]
}
set value [ReadReg $addr_data]
if {$data == 0} {
set value [expr {$value&(~(0x1<<$bank_offset))}]
} else {
set value [expr {$value|(0x1<<$bank_offset)}]
}
# puts "set gpio index:$index\taddr:[format '0x%x' $addr_data]\tvalue:[format '0x%08x' $value]"
WriteReg $addr_data $value
}
proc SetGpioGroupData {group data} {
if {$::DUAL_CHANNAL== 0} {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $::GPIO_ADDR_SIZE * $group}]
set addr_data [expr {$gpio_bank_base + $::REG_DATA}]
} else {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $group / 2 * $::GPIO_ADDR_SIZE}]
set addr_data [expr {$gpio_bank_base + $::REG_DATA + $group % 2 * 0x8}]
}
if {$data == 0} {
set value 0
} else {
set value 0xffffffff
}
# puts "set gpio index:$index\taddr:[format '0x%x' $addr_data]\tvalue:[format '0x%08x' $value]"
WriteReg $addr_data $value
}
# 读取GPIO电平
proc GetGpioData {index} {
set group_offset [expr {$index / $::GPIO_CHANNAL_NUMBER}]
set bank_offset [expr {$index % $::GPIO_CHANNAL_NUMBER}]
if {$::DUAL_CHANNAL== 0} {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $::GPIO_ADDR_SIZE * $group_offset}]
set addr_data [expr {$gpio_bank_base + $::REG_DATA}]
} else {
set gpio_bank_base [expr {$::GPIO_ADDR_BASE + $group_offset / 2 * $::GPIO_ADDR_SIZE}]
set addr_data [expr {$gpio_bank_base + $::REG_DATA + $group_offset % 2 * 0x8}]
}
set value [ReadReg $addr_data]
# puts "get gpio index:$index\taddr:[format '0x%x' $addr_data]\tvalue:[format '0x%08x' $value]"
return [expr {0x1&($value>>$bank_offset)}]
}
proc bl {start stop value} {
for {set i $start} {$i < $stop} {incr i} {
if {$i == 65} {
continue
}
SetGpioDirection $i 0
SetGpioData $i $value
SetGpioDirection $i 1
}
}
proc setPin {index value} {
SetGpioDirection $index 0
SetGpioData $index $value
SetGpioDirection $index 1
}
proc probe {} {
set oldValue [GetGpioData 156]
for {set i 0} {$i < 16} {incr i} {
SetGpioGroupDirection $i 0
SetGpioDirection 156 1
SetGpioGroupData $i 0
set value [GetGpioData 156]
if {$value == 0} {
SetGpioGroupData $i 1
set value [GetGpioData 156]
if {$value == 1} {
puts "find group $i"
set start [expr {$i*32}]
set stop [expr {$i*32+32}]
for {set j $start} {$j < $stop} {incr j} {
SetGpioData $j 0
set value [GetGpioData 156]
if {$value == 0} {
SetGpioData $j 1
set value [GetGpioData 156]
if {$value == 1} {
if {$j == 156} {
continue
} else {
puts "find: $j"
SetGpioGroupDirection $i 1
return
}
}
}
}
}
}
SetGpioGroupDirection $i 1
}
puts "not find"
}
脚本中使用GPIO编号来对应每一个GPIO,GPIO的编号由上一步Vivado工程中GPIO所在的AXI-GPIO IP序号和GPIO在32位GPIO内的序号组成,具体计算方法是:
GPIO编号 index = IP编号*32+IP内部编号
例如工程中AXI_GPIO4 IP的GPIO2通道内的第3个GPIO,其GPIO编号为:7*32+3=227。
在vivado中通过JTAG连接到FPGA后,在Tcl Console窗口使用source命令将脚本输入到vivado,这样就可以在vivado中调用脚本中的tcl命令了
source {脚本路径}
当需要控制GPIO输出高低电平时,就是要上面脚本中的SetGpioDirection设置方向为输出,使用SetGpioData设置输出状态。当需要读取GPIO状态时,就使用SetGpioDirection设置为输入,使用GetGpioData获取状态。
探测普通引脚
最简单的方式是通过脚本中的bl接口可以遍历控制GPIO输出一下高低电平。通过观察,板上有12个LED灯,通过遍历全部500个IO输出高电平或低电平,观察LED灯是否产生变化,若有变化再通过二分法缩小遍历范围,继续观察是否闪烁判断GPIO编号范围,再进一步缩小遍历范围...,最终获取到GPIO编号。获取到的GPIO编号再通过上面的计算公式反向得到GPIO所在的IP序号和IO在IP内部的编号,得到vivado工程中的信号名,再通过搭建工程时自动分配IO时形成的管脚约束文件,反查得到引脚号,这样就实现了一个引脚的探测。
在遍历过程中,发现每当控制GPIO56输出低电平时,脚本执行就会报错,貌似是触发FPGA重新配置了,导致加载的FPGA里面的bit没了,因此GPIO56确认是不能当做普通IO的,在脚本中专门对GPIO56进行了处理,跳过控制GPIO56。
由于FPGA的IO是可以分BANK供电的,不同的IO可能有不同的电压,前期可以多使用上面的方法探测几个IO,查到IO所在的BANK,使用万用表或示波器检测GPIO输出高电平时的电压,从而检测到每个BANK对应的供电电压。在这个板子上我检测了几个LED,都是2.5V电平,因此判断部分BANK为2.5V供电,DDR3内存条的IO根据DDR3标准,是1.5V电平。
这个板子上除了内存条信号比较多,其他需要检测的IO就是12个LED、板上晶振、PCIE复位信号这几个,这部分比较少,直接使用上面遍历的方法就探测完了。内存条的信号比较多,一组DDR3就有100多个引脚需要探测,为了提高效率可以使用脚本来辅助。先使用前面的方法检测一个引脚出来,再将这个引脚作为输入通道,手动将该引脚通过飞线连接到其他需要检测的引脚上,再使用脚本遍历控制其他引脚输出高低电平,检测输入引脚是否产生一致的变化,若能同步变化,则识别为对应GPIO编号。使用这个思路编写了脚本中的probe函数,当飞线连接到待探测的IO时,调用一次probe函数即可显示对应的管脚号。在上面脚本中我使用的156号GPIO作为已知管脚,用来检测待测IO的状态。
需要注意的是,因为这个方法需要将两个IO连接起来,一定要保证两个IO是相同电压的,因为这个板子上都是探测DDR3信号,都是1.5V供电,没有这个问题,但探测其他信号时就需要特别注意,不确定电压的情况下只能使用遍历的方法来探测。
| 引脚位置 | 引脚名称 | 电压 | 信号 | 备注 |
|---|---|---|---|---|
| AB27 | IO_L12P_T1_MRCC_13 | 2.5V | 100M差分时钟正端 | LVDS25电平 |
| AC27 | IO_L12N_T1_MRCC_13 | 2.5V | 100M差分时钟负端 | LVDS25电平 |
| T28 | IO_L11N_T1_SRCC_14 | 2.5V | Q35栅极 | SFP发送使能,高有效 |
| T26 | IO_L12P_T1_MRCC_14 | 2.5V | Q36栅极 | SFP发送使能,高有效 |
| R30 | IO_L9P_T1_DQS_14 | 2.5V | Q37 栅极 | SFP发送使能,高有效 |
| U30 | IO_L15N_T2_DQS_DOUT_CSO_B_14 | 2.5V | Q38 栅极 | SFP发送使能,高有效 |
| N27 | IO_L16P_T2_A28_15 | 2.5V | Z2 LED | 高电平点亮 |
| M27 | IO_L16N_T2_A27_15 | 2.5V | Z3 LED | 高电平点亮 |
| N29 | IO_L17P_T2_A26_15 | 2.5V | Z4 LED | 高电平点亮 |
| N30 | IO_L17N_T2_A25_15 | 2.5V | Z5 LED | 高电平点亮 |
| L21 | IO_L4P_T0_AD9P_15 | 2.5V | Z6 LED | 低电平点亮 |
| K24 | IO_L3N_T0_DQS_AD1N_15 | 2.5V | Z7 LED | 低电平点亮 |
| K21 | IO_L4N_T0_AD9N_15 | 2.5V | Z8 LED | 低电平点亮 |
| J21 | IO_L5P_T0_AD2P_15 | 2.5V | Z9 LED | 低电平点亮 |
| U27 | IO_L13P_T2_MRCC_14 | 2.5V | 拉低会异常 | |
| J28 | IO_L8N_T1_AD3N_15 | 2.5V | 右上LED | 高电平点亮 |
| K30 | IO_L9N_T1_DQS_AD11N_15 | 2.5V | 右下LED | 高电平点亮 |
| L30 | IO_L9P_T1_DQS_AD11P_15 | 2.5V | 左上LED | 高电平点亮 |
| K26 | IO_L10P_T1_AD4P_15 | 2.5V | 左下LED | 高电平点亮 |
| M20 | IO_L6P_T0_15 | 2.5V | PRESET_N | PCIE复位 |
| J29 | IO_L7P_T1_AD10P_15 | 2.5V | SCL | DDR I2C总线 |
| H29 | IO_L7N_T1_AD10N_15 | 2.5V | SDA | DDR I2C总线 |
探测GTX信号
板上使用的XC7K325TFFG900芯片具有4个quad共16路GTX接口,通过观察可以发现板上有8路作为了PCIE接口,4路作为光模块信号。GTX接口需要确定参考时钟和GTX lane引脚。使用Vivado IBERT IP分别配置4个quad的clk0和clk1作为参考时钟,由于不知道参考时钟频率,先选择参考时钟频率为125M,配置lane速率为1.25G,在配置ibert的系统时钟时,选择上一步探测到的100M时钟管脚AB27。
将编译好的ibert bit加载到K7,观察到只有使用quad117 clk0和clk1的quad才能锁定时钟,将对应的lane设置为内部环回,使用clk0作为参考时钟的lane的线速率显示为1.25G,使用clk1作为参考时钟的lane速率显示为1.563G,根据比例关系计算:1.563G/1.25G*125M=156.25MHz,因此判断quad 117 clk1参考时钟频率为156.25MHz,quad 117 clk0参考时钟频率为125MHz。
另外观察板上还有个622.08MHz的晶振,还没有发现这个时钟接到哪里了。前面探测ibert时钟的工程中选择的参考时钟频率是125M,怀疑是和622.08M相差太大导致本来有时钟也不能锁定。修改ibert参考时钟为500M,重新生成并加载到K7,观察到quad118 clk0时钟可以锁定,且GTX lane速率也能对应得上,因此确定quad118 clk0参考时钟频率为622.08MHz。
| 引脚名 | 时钟 |
|---|---|
| MGTREFCLK0[P/N]_117 | 125MHz参考时钟 |
| MGTREFCLK1[P/N]_117 | 156.25MHz参考时钟 |
| MGTREFCLK0[P/N]_118 | 622.08MHz参考时钟 |
接下来就是探测GTX lane引脚,需要在光模块座子上插入sfp光模块和光纤,通过通道环回关系可以确定管脚如下。
| SFP光模块位置 | GTX信号位置 | 引脚 |
|---|---|---|
| 最上面 | X0Y15 | MGTX[T/R]X[P/N]3_118 |
| 中上 | X0Y14 | MGTX[T/R]X[P/N]2_118 |
| 中下 | X0Y11 | MGTX[T/R]X[P/N]3_117 |
| 最下面 | X0Y10 | MGTX[T/R]X[P/N]2_117 |
PCIE接口为8X,剩下的就是quad115和quad116,手上没有PCIE槽位,没去确定时钟的引脚了,后续用到了再看,思路和上面ibert找时钟是一样的。根据xilinx PCIE的IP手册,PCIE 8X接口的LANE顺序推荐如下:
| 引脚名 | 信号名 |
|---|---|
| MGTX[T/R]X[P/N]3_116 | PCIE_LANE0 |
| MGTX[T/R]X[P/N]2_116 | PCIE_LANE1 |
| MGTX[T/R]X[P/N]1_116 | PCIE_LANE2 |
| MGTX[T/R]X[P/N]0_116 | PCIE_LANE3 |
| MGTX[T/R]X[P/N]3_115 | PCIE_LANE4 |
| MGTX[T/R]X[P/N]2_115 | PCIE_LANE5 |
| MGTX[T/R]X[P/N]1_115 | PCIE_LANE6 |
| MGTX[T/R]X[P/N]0_115 | PCIE_LANE7 |

浙公网安备 33010602011771号