XC7K325T_NetFirm-4E40-C

XC7K325T_NetFirm-4E40-C

背景

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

image-20260428070129126

image-20260428070352337

逆向管脚

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	

image-20260428070826340

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

image-20260428071450216

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

image-20260428071732639

分配完管脚后直接综合布线,没问题的话就可以生成比特流了。

使用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
posted @ 2026-04-29 08:06  LM358  阅读(105)  评论(0)    收藏  举报