浪潮加速卡作为XC7K480T FPGA开发板
浪潮加速卡改造为XC7K480T FPGA开发板
背景
众所周知,FPGA开发板都卖的比较贵,淘宝上一个资源少的可怜的FPGA做个开发板都要卖几百上千,买起来玩肯定是相当不划算的,因此我在小黄鱼上经常关注一些FPGA板子,想着淘一个二手板子,把板上的资源都摸清楚后作为开发板来玩。正好前不久刷到一个浪潮加速卡,也就是今天介绍的主角:YPCB-00338-1P1。板卡主芯片是Xilinx XC7K480TFFG1156,板上有PCIE、DDR、FLASH和LED灯几个外设可以玩,看外观JTAG应该也是引出来了的,芯片逻辑资源丰富,调试也方便,主要价格还便宜120包邮到家,因此就买了回来玩起。

板卡介绍
板子本身是PCIE卡的形式,对外是标准的PCIE X8接口,可以插在电脑主板上作为PCIE设备来调试开发,同时板上还有2组DDR3颗粒,每组分为9片256M*8bit的DDR颗粒,猜测是64bit数据加8bit ECC的组合,那么每组容量就是2GB,共4GB内存。前面板上具有4个LED灯,根据后面的引脚探测情况来看,有一颗应该是电源指示灯,另外3颗是受FPGA管脚控制的。查看板上各个芯片的丝印,整理了下几个比较关心的芯片型号和功能:
| 位号 | 型号 | 功能 |
|---|---|---|
| U3 | XC7K480TFFG1156 | |
| U4 | MT28GU512AAX1E-BPI-X16 | FPGA配置FLASH、BPI X16接口 |
| U5 U6 U7 U8 U9 U10 U11 U12 U13 | MT41K256M8HX-15E_D | 通道1 DDR3颗粒,256M*8bit |
| U1 U14 U15 U16 U17 U18 U19 U20 U21 | MT41K256M8HX-15E_D | 通道2 DDR3颗粒,256M*8bit |
| U23 | LM73 | I2C接口温度传感器 |
| U22、U26 | PCA9517A | I2C电平转换 |
| Y2 | 50MHz单端晶振 | |
| Y1 Y3 | 200MHz差分晶振 | |
| J1 | FPGA JTAG调试接口 | |
| SW1 | 按键 | 重加载按键?(按下FPGA会重新加载) |
| SW2 | 按键 | 未知 |
| U2 | TXS0108 | 1.8和3.3电平转换,功能未知 |


板卡本身为PCIE卡形式,需要通过PCIE接口进行供电,这里我买了个PCIE转接卡用来作为供电底座。

管脚探测
以上只是通过肉眼和示波器探测或猜测到的一些信息,具体确认FPGA的管脚分配情况还需要进一步的测试。由于网上没有这个板子的原理图,因此只能尝试自己探测管脚了。具体思路就是编写一个JTAG转GPIO的模块,将FPGA的所有用户IO让JTAG能控制,再通过JTAG控制管脚遍历输出高低电平,外围配合示波器测试想探测的引脚的信号变化,从而确认管脚信息。JTAG控制GPIO这部分可以使用VIVADO中现成的JTAG转AXI IP和AXI-GPIO IP来实现,JTAG可以通过TCL脚本实现自动遍历FPGA,唯一需要手动操作的就是用示波器检测管脚的电平变化,当信号变化时反过来看遍历到哪个GPIO在输出了就行。
Vivado工程搭建
根据上面的思路,使用Vivado2024快速搭建了一个测试模块,在BLOCK DESIGN中调用了JTAG转AXI和几个AXI-GPIO IP,整体设计如下:

XC7K480TFFG1156共有400个用户IO,因此使用了7个AXI GPIO模块,每个模块可以输出64路GPIO,将所有400个IO通过管脚约束文件约束到AXI GPIO IP的输出端,这样就可以通过JTAG读写寄存器来控制每个IO的输入输出状态了。
每个AXI GPIO模块都分配了一个基地址,根据AXI GPIO的手册中的寄存器描述,每个GPIO模块分为2组32通道的GPIO,每个通道由1个32位的方向寄存器和1个32位的数据寄存器来控制,每一位对于一个GPIO,方向寄存器写0是输出,写1是输入,数据寄存器读取是获取输入状态,写寄存器是设置输出状态。

使用JTAG进行管脚探测
上面的工程综合实现完成后,板卡就可以通过仿真器连接上FPGA加bit了

JTAG读写AXI寄存器可以通过脚本实现
set hw_axi_name "hw_axi_1"
set DUAL_CHANNAL 1
set GPIO_NUMBER 400
set GPIO_CHANNAL_NUMBER 32
set GPIO_ADDR_BASE 0x10000
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开始
# data 输出值
proc SetGpioData {index data} {
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
}
# 读取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)}]
}
上面的TCL脚本就实现了控制每一个GPIO的输入输出状态,再配合示波器检测管脚状态就可以实现检测管脚了。
每一个GPIO都用示波器去检测太过麻烦,在探测到第一个管脚后,还可以把这个管脚引一根线出来,作为输入检测管脚,再手动把这个管脚连接到其他待测管脚上去,使用tcl脚本依次控制其他管脚输出不同的电平,并自动检测输入管脚是否有变化,这样就可以自动确定输出管脚的编号了。
另一个问题是DDR内存颗粒为BGA封装,没法直接接触到信号管脚,因此想探测到DDR管脚,必须把DDR颗粒拆下来再检测。我使用热风枪和加热板,把所有DDR颗粒都拆下来,再使用前面描述的方法确定每个DDR管脚的分配,确认完后再焊回去,万幸没有出现掉焊盘或焊点短路的问题。
结语
使用以上方法,板上所有管脚就都可以确定出来了,确定管脚后,板上的DDR资源就可以用起来了,以后可以基于DDR跑MICRLBLAZE或DMA等IP核,又可以折腾起来了。这块板子唯一的缺点就是没有引出IO来,小黄鱼上另一款FPGA板子倒是引出了许多IO,但那个板子就要贵很多了。我觉得其实还好,我平时玩FPGA主要是在FPGA内部跑一些IP验证或测试一些自己设计的接口模块,对外IO的话一般是用来外接其他外设了,像ADC啥的,我这边还没有这种需求,因此综合来看这个板子便宜又有必要的外设,还是挺适合用来作为一款入门学习的开发板的。
逆向引脚资料和测试程序自取:https://pan.baidu.com/s/1xmLexNwvflCn5LhYS9_vhQ?pwd=8as6 提取码: 8as6

浙公网安备 33010602011771号