浪潮加速卡作为XC7K480T FPGA开发板

浪潮加速卡改造为XC7K480T FPGA开发板

背景

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

c5198a361cf451a11857013efbf8aec

板卡介绍

板子本身是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电平转换,功能未知

3e3fa41e8121d7dcdd2c1743a4252d8

5d73bd034e53ef47bd80b1f33e7319b

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

55f02d952bbd7f96db98cfaee12337e

管脚探测

以上只是通过肉眼和示波器探测或猜测到的一些信息,具体确认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,整体设计如下:

image-20241108025618652

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是输入,数据寄存器读取是获取输入状态,写寄存器是设置输出状态。

image-20241108025922816

使用JTAG进行管脚探测

上面的工程综合实现完成后,板卡就可以通过仿真器连接上FPGA加bit了

image-20241108030514400

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

posted @ 2024-11-08 03:29  LM358  阅读(4293)  评论(22)    收藏  举报