LNA Example

Let’s design a LNA using Infineon’s BFU520 transistor. First we need to import scikit-rf and a bunch of other utilities:

import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = [10, 10]

import skrf as rf
from skrf.media import DistributedCircuit

f = rf.Frequency(0.4, 2, 101, 'GHz')
tem = DistributedCircuit(f, z0=50)
# import the scattering parameters/noise data for the transistor
bjt = rf.Network('BFU520_05V0_010mA_NF_SP.s2p').interpolate(f)
2-Port Network: 'BFU520_05V0_010mA_NF_SP',  0.4-2.0 GHz, 101 pts, z0=[50.+0.j 50.+0.j]

Let’s plot the smith chart for it:


Now let’s calculate the source and load stability curves. I’m slightly misusing the Network type to plot the curves; normally the curves you pass in to Network should be a function of frequency, but it also works to draw these circles as long as you don’t try to use any other functions on them

sqabs = lambda x: np.square(np.absolute(x))

delta = bjt.s11.s*bjt.s22.s - bjt.s12.s*bjt.s21.s
rl = np.absolute((bjt.s12.s * bjt.s21.s)/(sqabs(bjt.s22.s) - sqabs(delta)))
cl = np.conj(bjt.s22.s - delta*np.conj(bjt.s11.s))/(sqabs(bjt.s22.s) - sqabs(delta))

rs = np.absolute((bjt.s12.s * bjt.s21.s)/(sqabs(bjt.s11.s) - sqabs(delta)))
cs = np.conj(bjt.s11.s - delta*np.conj(bjt.s22.s))/(sqabs(bjt.s11.s) - sqabs(delta))

def calc_circle(c, r):
    theta = np.linspace(0, 2*np.pi, 1000)
    return c + r*np.exp(1.0j*theta)
for i, f in enumerate(bjt.f):
    # decimate it a little
    if i % 100 != 0:
    n = rf.Network(name=str(f/1.e+9), s=calc_circle(cs[i][0, 0], rs[i][0, 0]))
for i, f in enumerate(bjt.f):
    # decimate it a little
    if i % 100 != 0:
    n = rf.Network(name=str(f/1.e+9), s=calc_circle(cl[i][0, 0], rl[i][0, 0]))

So we can see that we need to avoid inductive loads near short circuit in the input matching network and high impedance inductive loads on the output.

Let’s draw some constant noise circles. First we grab the noise parameters for our target frequency from the network model:

idx_915mhz = rf.util.find_nearest_index(bjt.f, 915.e+6)

# we need the normalized equivalent noise and optimum source coefficient to calculate the constant noise circles
rn = bjt.rn[idx_915mhz]/50
gamma_opt = bjt.g_opt[idx_915mhz]
fmin = bjt.nfmin[idx_915mhz]

for nf_added in [0, 0.1, 0.2, 0.5]:
    nf = 10**(nf_added/10) * fmin

    N = (nf - fmin)*abs(1+gamma_opt)**2/(4*rn)
    c_n = gamma_opt/(1+N)
    r_n = 1/(1-N)*np.sqrt(N**2 + N*(1-abs(gamma_opt)**2))

    n = rf.Network(name=str(nf_added), s=calc_circle(c_n, r_n))

print("the optimum source reflection coefficient is ", gamma_opt)
the optimum source reflection coefficient is  (-0.08373015564356305+0.028299611051606704j)

So we can see from the chart that just leaving the input at 50 ohms gets us under 0.1 dB of extra noise, which seems pretty good. I’m actually not sure that these actually correspond to the noise figure level increments I have listed up there, but the circles should at least correspond to increasing noise figures

So let’s leave the input at 50 ohms and figure out how to match the output network to maximize gain and stability. Let’s see what matching the load impedance with an unmatched input gives us:

gamma_s = 0.0

gamma_l = np.conj(bjt.s22.s - bjt.s21.s*gamma_s*bjt.s12.s/(1-bjt.s11.s*gamma_s))
gamma_l = gamma_l[idx_915mhz, 0, 0]
is_gamma_l_stable = np.absolute(gamma_l - cl[idx_915mhz]) > rl[idx_915mhz]

gamma_l, is_gamma_l_stable
(np.complex128(0.24318880200879378+0.3424018477949188j), array([[ True]]))

This looks like it may be kind of close to the load instability circles, so it might make sense to pick a load point with less gain for more stability, or to pick a different source impedance with more noise.

But for now let’s just build a matching network for this and see how it performs:

def calc_matching_network_vals(z1, z2):
    flipped = np.real(z1) < np.real(z2)
    if flipped:
        z2, z1 = z1, z2

    # cancel out the imaginary parts of both input and output impedances
    z1_par = 0.0
    if abs(np.imag(z1)) > 1e-6:
        # parallel something to cancel out the imaginary part of
        # z1's impedance
        z1_par = 1/(-1j*np.imag(1/z1))
        z1 = 1/(1./z1 + 1/z1_par)
    z2_ser = 0.0
    if abs(np.imag(z2)) > 1e-6:
        z2_ser = -1j*np.imag(z2)
        z2 = z2 + z2_ser

    Q = np.sqrt((np.real(z1) - np.real(z2))/np.real(z2))
    x1 = -1.j * np.real(z1)/Q
    x2 = 1.j * np.real(z2)*Q

    x1_tot = 1/(1/z1_par + 1/x1)
    x2_tot = z2_ser + x2
    if flipped:
        return x2_tot, x1_tot
        return x1_tot, x2_tot

z_l = rf.s2z(np.array([[[gamma_l]]]))[0,0,0]
# note that we're matching against the conjugate;
# this is because we want to see z_l from the BJT side
# if we plugged in z the matching network would make
# the 50 ohms look like np.conj(z) to match against it, so
# we use np.conj(z_l) so that it'll look like z_l from the BJT's side
z_par, z_ser = calc_matching_network_vals(np.conj(z_l), 50)
z_l, z_par, z_ser

Let’s calculate what the component values are:

c_par = np.real(1/(2j*np.pi*915e+6*z_par))
l_ser = np.real(z_ser/(2j*np.pi*915e+6))

print(c_par, l_ser)
3.065863450236221e-13 8.778535101731343e-09

The capacitance is kind of low but the inductance seems reasonable. Let’s test it out:

output_network = tem.shunt_capacitor(c_par) ** tem.inductor(l_ser)

amplifier = bjt ** output_network


That looks pretty reasonable; let’s take a look at the S21 to see what we got:


So about 18 dB gain; let’s see what our noise figure is:


So 0.96 dB NF, which is reasonably close to the BJT tombstone optimal NF of 0.95 dB

[ ]: