PCIe FPGA Data Aquisition Board

PCIe FPGA Data Aquisition Board#

The topic in this notebook is the analysis of a high-speed data acquisition PCIe board based around a large FPGA (Field Programmable Gate Array). A number of high-speed ADCs, each with its own power delivery system, is connected to the FPGA with JESD204B links. Data is pre-processed in the FPGA before being sent across the PCIe bus. The board can only draw power from the PCIe connector (no extra ATX connectors), and the task at hand is to determine how many ADC channels can be added to the board within the power limits of the PCIe specification (75W).

from sysloss.components import *
from sysloss.utils import *
from sysloss.system import System
import pandas as pd

System definition#

Each ADC channel will use the following resources:

  • Two JESD204B lanes to the FPGA

  • About 7% of FPGA logic for pre-processing

  • Four power rails: 1.8V and 3x 1.2V (converted from 12V):

ADC power

The FPGA has 32 transceivers, 8 are used for PCIe while the rest are dedicated to JESD204 lanes. This sets the upper limit for the number of ADC channels to 12. The FPGA itself has 4 power rails: 1.8V (AUX), 1.2V (AVTT), 0.9V (AVCC) and 0.85V (VCCINT). Except for the high-speed transceivers, there are practically no I/O used, so I/O bank power is left out of the analysis.

Power consumption of the ADC is collected from the data sheet, and FPGA power is estimated using the FPGA power tools. FPGA power consumption is summarized in the table below:

Voltage

ADC (per channel) Power (W)

System control & PCIe Power (W)

Static FPGA power (W)

VCCINT

1.58

1.254

1.67

AVCC

0.051

0.45

0.57

AVTT

0.22

0.76

0.031

VAUX

1.35

FPGA and ADC’s will be powered from the 12V input, while board monitoring and test signal generators will be powered from the 3.3V input.

Tip

Use limits on components like Converters (input voltage range, output current) and LinRegs (output voltage and current) to get warnings if component voltages or currents are out of spec.

Buck converter efficiency is defined as interpolation data. The subsystems are defined in functions for easy manipulation of key system parameters and system architecture.

Tip

When a PCB design involves high currents, the PCB trace resistance should be taken into account. sysLoss has a function trace_res() for calculating the PCB trace resistance in the utils package.

eff_2v3 = {"vi":[12], "io":[1e-3, 1, 2, 3, 4, 5], "eff":[[0.45, 0.95, 0.94, 0.925, 0.9, 0.88]]}
eff_1v7 = {"vi":[12], "io":[1e-3, 1, 2, 3, 4, 5], "eff":[[0.33, 0.92, 0.92, 0.91, 0.9, 0.875]]}
eff_0v85 = {"vi":[12], "io":[1, 2, 5, 10, 20, 30], "eff":[[0.48, 0.7, 0.86, 0.9, 0.87, 0.84]]} 
eff_1v8 = {"vi":[12], "io":[.1, .25, .5, 1.0, 1.5, 2.0], "eff":[[0.63, 0.82, 0.86, 0.9, 0.9, 0.885]]}
eff_0v9 = {"vi":[12], "io":[.1, .25, .5, 1.0, 1.5, 2.0], "eff":[[0.54, 0.75, 0.82, 0.85, 0.84, 0.83]]}
eff_1v2 = {"vi":[12], "io":[.01, .1, .5, 1.0, 2.0, 4.0], "eff":[[0.72, 0.78, 0.84, 0.88, 0.87, 0.8]]}

def adc_subsystem(sys, channel, src):
    # assume a star connection to 12V using 8cm long, 50mils wide traces on a 1oz/ft^2 copper layer (return current on ground plane).
    rs = trace_res(w1_mm=50*MILS2MM, w2_mm=50*MILS2MM, l_mm=80, t_mm=1*OZ2MM)
    idx = "[{}]".format(channel+1)
    sys.add_comp(src, comp=RLoss("ADC"+idx+" PCB trace", rs=rs))
    sys.add_comp("ADC"+idx+" PCB trace", comp=Converter("ADC"+idx+" buck 2.3V", vo=2.3, eff=eff_2v3))
    sys.add_comp("ADC"+idx+" buck 2.3V", comp=RLoss("ADC"+idx+" ferrit1", rs=0.087))
    sys.add_comp("ADC"+idx+" ferrit1", comp=LinReg("ADC"+idx+" LDO 1.8V", vo=1.8, ig=0.5e-6, vdrop=0.15, limits={"vo":[1.8, 1.8]}))
    sys.add_comp("ADC"+idx+" LDO 1.8V", comp=ILoad("ADC"+idx+" AVVD18", ii=0.5))
    sys.add_comp("ADC"+idx+" PCB trace", comp=Converter("ADC"+idx+" buck 1.7V", vo=1.7, eff=eff_1v7))
    sys.add_comp("ADC"+idx+" buck 1.7V", comp=RLoss("ADC"+idx+" ferrit2", rs=0.103))
    sys.add_comp("ADC"+idx+" ferrit2", comp=LinReg("ADC"+idx+" LDO[1] 1.2V", vo=1.2, ig=0.23e-6, vdrop=0.15, limits={"vo":[1.2, 1.2]}))
    sys.add_comp("ADC"+idx+" LDO[1] 1.2V", comp=ILoad("ADC"+idx+" AVVD12", ii=0.74))
    sys.add_comp("ADC"+idx+" ferrit2", comp=LinReg("ADC"+idx+" LDO[2] 1.2V", vo=1.2, ig=0.23e-6, vdrop=0.15, limits={"vo":[1.2, 1.2]}))
    sys.add_comp("ADC"+idx+" LDO[2] 1.2V", comp=ILoad("ADC"+idx+" CLKVDD", ii=0.086))
    sys.add_comp("ADC"+idx+" ferrit2", comp=LinReg("ADC"+idx+" LDO[3] 1.2V", vo=1.2, ig=0.23e-6, vdrop=0.15, limits={"vo":[1.2, 1.2]}))
    sys.add_comp("ADC"+idx+" LDO[3] 1.2V", comp=ILoad("ADC"+idx+" DVDD", ii=1.41))
    return sys

def fpga_subsystem(sys, channels, src):
    # assume connection to 12V using a 5cm long, 40mils wide trace on a 1oz/ft^2 copper layer (return current on ground plane).
    rs = trace_res(w1_mm=40*MILS2MM, w2_mm=40*MILS2MM, l_mm=50, t_mm=1*OZ2MM)
    sys.add_comp(src, comp=RLoss("FPGA PCB trace", rs=rs))
    sys.add_comp("FPGA PCB trace", comp=Converter("FPGA VCCINT", vo=0.85, eff=eff_0v85, limits={"io":[0.0, 30.0]}))
    sys.add_comp("FPGA VCCINT", comp=PLoad("FPGA INT static", pwr=1.67))
    sys.add_comp("FPGA VCCINT", comp=PLoad("FPGA INT dynamic", pwr=1.25+channels*1.58))
    # VCCAUX
    sys.add_comp("FPGA PCB trace", comp=Converter("FPGA VCCAUX", vo=1.8, eff=eff_1v8, limits={"io":[0.0, 2.0]}))
    sys.add_comp("FPGA VCCAUX", comp=PLoad("FPGA AUX static", pwr=1.35))
    # AVCC
    sys.add_comp("FPGA PCB trace", comp=Converter("FPGA AVCC", vo=0.9, eff=eff_0v9, limits={"io":[0.0, 2.0]}))
    sys.add_comp("FPGA AVCC", comp=PLoad("FPGA AVCC static", pwr=0.57))
    sys.add_comp("FPGA AVCC", comp=PLoad("FPGA AVCC dynamic", pwr=0.45+channels*0.051))
    # AVTT
    sys.add_comp("FPGA PCB trace", comp=Converter("FPGA AVTT", vo=1.2, eff=eff_1v2, limits={"io":[0.0, 4.0]}))
    sys.add_comp("FPGA AVTT", comp=PLoad("FPGA AVTT static", pwr=0.031))
    sys.add_comp("FPGA AVTT", comp=PLoad("FPGA AVTT dynamic", pwr=0.76+channels*0.22))
    sys.add_comp("FPGA PCB trace", comp=ILoad("Board fan", ii=0.06))
    return sys

def PCIe_system(channels):
    # power inputs 12V and 3.3V with current limits set to PCIe spec.
    sys = System("PCIe FPGA board", source=Source("12V", vo=12.0, limits={"io":[0.0, 5.5]}))
    sys.add_source(Source("3.3V", vo=3.3, limits={"io":[0.0, 3.0]}))
    # 3.3V subsystem
    sys.add_comp("3.3V", comp=Converter("Buck 2.5V", vo=2.5, eff=eff_2v3))
    sys.add_comp("Buck 2.5V", comp=PLoad("Board monitor", pwr=0.35))
    sys.add_comp("Buck 2.5V", comp=PLoad("Signal generators", pwr=1.55))
    # FPGA subsystem
    sys = fpga_subsystem(sys, channels, "12V")
    # ADC channels
    for i in range(channels):
        sys = adc_subsystem(sys, i, "12V")
    return sys

Analysis#

We start by looking at the power tree and power consumption for a one channel board:

sys = PCIe_system(1)
sys.tree()
PCIe FPGA board
├── 3.3V
│   └── Buck 2.5V
│       ├── Signal generators
│       └── Board monitor
└── 12V
    ├── ADC[1] PCB trace
    │   ├── ADC[1] buck 1.7V
    │   │   └── ADC[1] ferrit2
    │   │       ├── ADC[1] LDO[3] 1.2V
    │   │       │   └── ADC[1] DVDD
    │   │       ├── ADC[1] LDO[2] 1.2V
    │   │       │   └── ADC[1] CLKVDD
    │   │       └── ADC[1] LDO[1] 1.2V
    │   │           └── ADC[1] AVVD12
    │   └── ADC[1] buck 2.3V
    │       └── ADC[1] ferrit1
    │           └── ADC[1] LDO 1.8V
    │               └── ADC[1] AVVD18
    └── FPGA PCB trace
        ├── Board fan
        ├── FPGA AVTT
        │   ├── FPGA AVTT dynamic
        │   └── FPGA AVTT static
        ├── FPGA AVCC
        │   ├── FPGA AVCC dynamic
        │   └── FPGA AVCC static
        ├── FPGA VCCAUX
        │   └── FPGA AUX static
        └── FPGA VCCINT
            ├── FPGA INT dynamic
            └── FPGA INT static
sys.solve()
Component Type Parent Domain Vin (V) Vout (V) Iin (A) Iout (A) Power (W) Loss (W) Efficiency (%) Warnings
0 3.3V SOURCE 3.3V 3.3 3.3 0.693784 0.693784 2.289488 0.0 100.0
1 Buck 2.5V CONVERTER 3.3V 3.3V 3.3 2.5 0.693784 0.76 2.289488 0.389488 82.987988
2 Signal generators LOAD Buck 2.5V 3.3V 2.5 0.0 0.62 0.0 1.55 0.0 100.0
3 Board monitor LOAD Buck 2.5V 3.3V 2.5 0.0 0.14 0.0 0.35 0.0 100.0
4 12V SOURCE 12V 12.0 12.0 1.309342 1.309342 15.712108 0.0 100.0
5 ADC[1] PCB trace SLOSS 12V 12V 12.0 11.984934 0.482757 0.482757 5.793085 0.007273 99.87445
6 ADC[1] buck 1.7V CONVERTER ADC[1] PCB trace 12V 11.984934 1.7 0.345631 2.236001 4.142367 0.341165 91.763999
7 ADC[1] ferrit2 SLOSS ADC[1] buck 1.7V 12V 1.7 1.469692 2.236001 2.236001 3.801201 0.514969 86.452466
8 ADC[1] LDO[3] 1.2V LINREG ADC[1] ferrit2 12V 1.469692 1.2 1.41 1.41 2.072266 0.380266 81.649751
9 ADC[1] DVDD LOAD ADC[1] LDO[3] 1.2V 12V 1.2 0.0 1.41 0.0 1.692 0.0 100.0
10 ADC[1] LDO[2] 1.2V LINREG ADC[1] ferrit2 12V 1.469692 1.2 0.086 0.086 0.126394 0.023194 81.649546
11 ADC[1] CLKVDD LOAD ADC[1] LDO[2] 1.2V 12V 1.2 0.0 0.086 0.0 0.1032 0.0 100.0
12 ADC[1] LDO[1] 1.2V LINREG ADC[1] ferrit2 12V 1.469692 1.2 0.74 0.74 1.087572 0.199572 81.649739
13 ADC[1] AVVD12 LOAD ADC[1] LDO[1] 1.2V 12V 1.2 0.0 0.74 0.0 0.888 0.0 100.0
14 ADC[1] buck 2.3V CONVERTER ADC[1] PCB trace 12V 11.984934 2.3 0.137126 0.5 1.643446 0.493445 69.975
15 ADC[1] ferrit1 SLOSS ADC[1] buck 2.3V 12V 2.3 2.2565 0.5 0.5 1.150001 0.02175 98.108694
16 ADC[1] LDO 1.8V LINREG ADC[1] ferrit1 12V 2.2565 1.8 0.5 0.5 1.128251 0.228251 79.769476
17 ADC[1] AVVD18 LOAD ADC[1] LDO 1.8V 12V 1.8 0.0 0.5 0.0 0.9 0.0 100.0
18 FPGA PCB trace SLOSS 12V 12V 12.0 11.979847 0.826586 0.826586 9.919032 0.016658 99.832055
19 Board fan LOAD FPGA PCB trace 12V 11.979847 0.0 0.06 0.0 0.718791 0.0 100.0
20 FPGA AVTT CONVERTER FPGA PCB trace 12V 11.979847 1.2 0.097293 0.8425 1.165552 0.154552 86.74
21 FPGA AVTT dynamic LOAD FPGA AVTT 12V 1.2 0.0 0.816667 0.0 0.98 0.0 100.0
22 FPGA AVTT static LOAD FPGA AVTT 12V 1.2 0.0 0.025833 0.0 0.031 0.0 100.0
23 FPGA AVCC CONVERTER FPGA PCB trace 12V 11.979847 0.9 0.105649 1.19 1.265658 0.194658 84.62
24 FPGA AVCC dynamic LOAD FPGA AVCC 12V 0.9 0.0 0.556667 0.0 0.501 0.0 100.0
25 FPGA AVCC static LOAD FPGA AVCC 12V 0.9 0.0 0.633333 0.0 0.57 0.0 100.0
26 FPGA VCCAUX CONVERTER FPGA PCB trace 12V 11.979847 1.8 0.128056 0.75 1.534091 0.184091 88.0
27 FPGA AUX static LOAD FPGA VCCAUX 12V 1.8 0.0 0.75 0.0 1.35 0.0 100.0
28 FPGA VCCINT CONVERTER FPGA PCB trace 12V 11.979847 0.85 0.435588 5.294118 5.218281 0.718281 86.235294
29 FPGA INT dynamic LOAD FPGA VCCINT 12V 0.85 0.0 3.329412 0.0 2.83 0.0 100.0
30 FPGA INT static LOAD FPGA VCCINT 12V 0.85 0.0 1.964706 0.0 1.67 0.0 100.0
31 Subsystem 3.3V 3.3 0.693784 2.289488 0.389488 82.987988
32 Subsystem 12V 12.0 1.309342 15.712108 3.478126 77.863401
33 System total 18.001596 3.867614 78.515159

Note

When the system has more than one voltage source, a new column Domain appears in the results table. The name of the source (voltage domain) of each component is listed here.

Next, we check power consumption for ADC count between 1 and 12:

res = []
for cnt in range(1,13):
    psys = PCIe_system(channels=cnt)
    res += [psys.solve(tags={"ADCs":cnt})]
df = pd.concat(res, ignore_index=True)
df[df.Component == "System total"][["Component", "ADCs", "Power (W)", "Loss (W)", "Efficiency (%)", "Warnings"]].style.hide(axis='index')
Component ADCs Power (W) Loss (W) Efficiency (%) Warnings
System total 1 18.001596 3.867614 78.515159
System total 2 25.810332 6.242406 75.814316
System total 3 33.583143 8.581267 74.447696
System total 4 41.413241 10.977422 73.492965
System total 5 49.372805 13.503060 72.650815
System total 6 57.358484 16.054815 72.009694
System total 7 65.370851 18.633197 71.496168
System total 8 73.421068 21.249490 71.058048 Yes
System total 9 81.506463 23.900964 70.675989 Yes
System total 10 89.623232 26.583817 70.338253 Yes
System total 11 97.772038 29.298710 70.033651 Yes
System total 12 105.953564 32.046327 69.754366 Yes

We see that 7 ADC channels are the most we can power. From 8 channels and up we get warnings. Even though the 8-channel case has a system total power of 73W which is less than 75W PCIe specifications, the current drawn from the 12V supply is too high. Let’s look at the 12V input current (the PCIe specification says max 5.5A):

df[df.Component == "12V"][["Component", "ADCs", "Iout (A)", "Power (W)", "Warnings"]].style.hide(axis='index')
Component ADCs Iout (A) Power (W) Warnings
12V 1 1.309342 15.712108
12V 2 1.960070 23.520844
12V 3 2.607805 31.293655
12V 4 3.260313 39.123753
12V 5 3.923610 47.083317
12V 6 4.589083 55.068996
12V 7 5.256780 63.081363
12V 8 5.927632 71.131580 io
12V 9 6.601415 79.216975 io
12V 10 7.277812 87.333744 io
12V 11 7.956879 95.482551 io
12V 12 8.638673 103.664076 io

With 8 channels, the 12V current is 5.91A, above the 5.5A limit in the PCIe specification.

System optimization#

We can guess that the marketing department is not going to be happy to sell a 7-channel board - what can we do to get up to 8 channels? Looking at the analysis result above, the 3.3V supply is not fully utilized. So, we can try to power one extra channel from 3.3V. The ADC power circuit provides 2.3V as the highest voltage, so this will work fine. Converter efficiency will be better in fact operating from 3.3V than from 12V. The converter efficiency parameter needs an update for 3.3V input, and the system generating function assigns the first ADC channel to the 3.3V supply.

# expand ADC buck converter parameters for 3.3V input
eff_2v3 = {"vi":[3.3, 12], "io":[1e-3, 1, 2, 3, 4, 5], "eff":[[0.63, 0.955, 0.96, 0.94, 0.92, 0.9],[0.45, 0.95, 0.94, 0.925, 0.9, 0.88]]}
eff_1v7 = {"vi":[3.3, 12], "io":[1e-3, 1, 2, 3, 4, 5], "eff":[[0.59, 0.94, 0.95, 0.92, 0.91, 0.89],[0.33, 0.92, 0.92, 0.91, 0.9, 0.875]]}

# redefine system function to allocate one ADC to 3.3V supply
def PCIe_system(channels):
    # power inputs 12V and 3.3V with current limits set to PCIe spec.
    sys = System("PCIe FPGA board", source=Source("12V", vo=12.0, limits={"io":[0.0, 5.5]}))
    sys.add_source(Source("3.3V", vo=3.3, limits={"io":[0.0, 3.0]}))
    # 3.3V subsystem
    sys.add_comp("3.3V", comp=Converter("Buck 2.5V", vo=2.5, eff=eff_2v3))
    sys.add_comp("Buck 2.5V", comp=PLoad("Board monitor", pwr=0.35))
    sys.add_comp("Buck 2.5V", comp=PLoad("Signal generators", pwr=1.55))
    # FPGA subsystem
    sys = fpga_subsystem(sys, channels, "12V")
    # ADC channels
    for ch in range(channels):
        if ch == 0:
            sys = adc_subsystem(sys, ch, "3.3V")
        else:
            sys = adc_subsystem(sys, ch, "12V")
    return sys
psys2 = PCIe_system(8)
psys2.solve()
Component Type Parent Domain Vin (V) Vout (V) Iin (A) Iout (A) Power (W) Loss (W) Efficiency (%) Warnings
0 3.3V SOURCE 3.3V 3.3 3.3 2.344953 2.344953 7.738344 0.0 100.0
1 ADC[1] PCB trace SLOSS 3.3V 3.3V 3.3 3.247308 1.688386 1.688386 5.571675 0.088964 98.403286
2 ADC[1] buck 1.7V CONVERTER ADC[1] PCB trace 3.3V 3.247308 1.7 1.241431 2.236001 4.031308 0.230107 94.291998
3 ADC[1] ferrit2 SLOSS ADC[1] buck 1.7V 3.3V 1.7 1.469692 2.236001 2.236001 3.801201 0.514969 86.452466
4 ADC[1] LDO[3] 1.2V LINREG ADC[1] ferrit2 3.3V 1.469692 1.2 1.41 1.41 2.072266 0.380266 81.649751
... ... ... ... ... ... ... ... ... ... ... ... ...
120 FPGA INT dynamic LOAD FPGA VCCINT 12V 0.85 0.0 16.341176 0.0 13.89 0.0 100.0
121 FPGA INT static LOAD FPGA VCCINT 12V 0.85 0.0 1.964706 0.0 1.67 0.0 100.0
122 Subsystem 3.3V 3.3 2.344953 7.738344 2.255145 70.857529
123 Subsystem 12V 12.0 5.444726 65.336707 18.648328 71.458114
124 System total 73.075051 20.903473 71.394515

125 rows × 12 columns

The 8-channel case is now within spec.

Summary#

This notebook demonstrates how to define a complex system by splitting it up in subsystems and defining each subsystem separately. This enables easy exploration of different power architectures. We have also seen how key component parameters can be monitored by setting component limits.