from typing import Dict, Any
from matplotlib import pyplot as _plt
import numpy as _np
from pdkmaster.technology import primitive as _prm
from pdkmaster.design import circuit as _ckt
from c4m.pdk import sky130
prims = sky130.tech.primitives
We use own compiled version of ngspice and set NGPSICE_LIBRARY_PATH
accordingly. Reason is to use more recent version (>= 35) that speeds up the simulation wirh the ngspice decks. Also the provided .spiceinit
file in the running directory is needed to get this speed up.
import os
os.environ["NGSPICE_LIBRARY_PATH"] = "/home/verhaegs/software/mint20/stow/ngspice-36/lib/libngspice.so"
from PySpice.Unit import *
class Bandgap:
def __init__(self, *,
nmos: _prm.MOSFET, nmos_params: Dict[str, Any],
pmos: _prm.MOSFET, pmos_params: Dict[str, Any],
resistor: _prm.Resistor, r1_params: Dict[str, Any], r2_params: Dict[str, Any],
pnp: _prm.Bipolar, pnp_params: Dict[str, Any], pnp_ratio: int,
):
self.ckt = ckt = sky130.cktfab.new_circuit(name="bandgap")
n1 = ckt.instantiate(nmos, name="n1", **nmos_params)
n2 = ckt.instantiate(nmos, name="n2", **nmos_params)
ns = (n1, n2)
p1 = ckt.instantiate(pmos, name="p1", **pmos_params)
p2 = ckt.instantiate(
pmos, name="p2", **pmos_params)
p3 = ckt.instantiate(pmos, name="p3", **pmos_params)
ps = (p1, p2, p3)
r1 = ckt.instantiate(resistor, name="r1", **r1_params)
r2 = ckt.instantiate(resistor, name="r2bot", **r2_params)
pnp1 = ckt.instantiate(pnp, name="pnp1", **pnp_params)
pnp2s = tuple(
ckt.instantiate(pnp, name=f"pnp2[{n}]", **pnp_params)
for n in range(pnp_ratio)
)
pnp3s = tuple(
ckt.instantiate(pnp, name=f"pnp3[{n}]", **pnp_params)
for n in range(pnp_ratio)
)
pnps = (pnp1, *pnp2s, *pnp3s)
ckt.new_net(name="vdd", external=True, childports=(
*(p.ports.sourcedrain1 for p in ps),
*(p.ports.bulk for p in ps),
))
ckt.new_net(name="vss", external=True, childports=(
*(n.ports.bulk for n in ns),
*(pnp.ports.base for pnp in pnps),
*(pnp.ports.collector for pnp in pnps),
))
ckt.new_net(name="vref", external=True, childports=(
p3.ports.sourcedrain2, r2.ports.port2,
))
ckt.new_net(name="p_gate", external=False, childports=(
*(p.ports.gate for p in ps),
p2.ports.sourcedrain2, n2.ports.sourcedrain1,
))
ckt.new_net(name="n_gate", external=False, childports=(
*(n.ports.gate for n in ns),
p1.ports.sourcedrain2, n1.ports.sourcedrain1,
))
ckt.new_net(name="vq1", external=False, childports=(
n1.ports.sourcedrain2, pnp1.ports.emitter,
))
ckt.new_net(name="vq2r1", external=False, childports=(
n2.ports.sourcedrain2, r1.ports.port1,
))
ckt.new_net(name="vq2", external=False, childports=(
r1.ports.port2, *(pnp2.ports.emitter for pnp2 in pnp2s),
))
ckt.new_net(name="vq3", external=False, childports=(
r2.ports.port1,
*(pnp3.ports.emitter for pnp3 in pnp3s),
))
def temp_sweep(self, *,
vdd=1.8, corner, sweep=slice(-20, 81, 20),
abstol=1e-9, gmin=1e-9,
):
self.lasttb = tb = sky130.pyspicefab.new_pyspicecircuit(
corner=corner, top=self.ckt, title="bandgap test",
)
tb.V("supply", "vdd", "vss", vdd)
tb.C("out", "vref", "vss", 1e-12)
tb.V("gnd", "vss", tb.gnd, 0.0)
sckt = tb._subcircuits["bandgap"]
sckt.Mp1.drain.add_current_probe(sckt)
sckt.Mp2.drain.add_current_probe(sckt)
sckt.Mp3.drain.add_current_probe(sckt)
self.lastsim = sim = tb.simulator(temperature=u_Degree(25))
# Avoid convergence problems
sim.options(abstol=abstol, gmin=gmin)
return sim.dc(temp=slice(-20, 81, 20))
We use l of 2µm
def _block():
l = 2.0
ws = _np.arange(3.0, 30.5, 3)
vgs = slice(0, 3.31, 0.05)
ckt = sky130.cktfab.new_circuit(name="pmos_tb")
sb = ckt.new_net(name=f"sb", external=True)
for w in ws:
pmos = ckt.instantiate(prims.pfet_g5v0d10v5, name=f"pmos_w{w}", l=l, w=w)
ckt.new_net(name=f"nmos_w{w}_gd", external=True, childports=(
pmos.ports.sourcedrain2, pmos.ports.gate,
))
sb.childports += (pmos.ports.sourcedrain1, pmos.ports.bulk)
tb = sky130.pyspicefab.new_pyspicecircuit(
corner="io_tt", top=ckt, title="nmos_tb",
)
tb.V("gs", "sb", "gd", 1.8)
tb.V("gnd", "gd", tb.gnd, 0.0)
for w in ws:
# Current measurement
tb.V(f"w{w}", "gd", f"nmos_w{w}_gd", 0.0)
sim = tb.simulator(temperature=u_Degree(25))
dc = sim.dc(vgs=vgs)
_plt.figure(figsize=(16,8))
_plt.subplot(1, 2, 1)
for w in ws:
_plt.plot(dc.sweep, 1e6*_np.array(dc.branches[f"vw{w}"]))
_plt.axis((0.0, 3.3, -500, 0))
_plt.title(f"l={l}µm")
_plt.xlabel("Vgs=Vds [V]")
_plt.ylabel("Ids [µA]")
_plt.legend(tuple(f"w={w}um" for w in ws))
_plt.grid(True)
_plt.subplot(1, 2, 2)
for w in ws:
_plt.plot(dc.sweep, 1e6*_np.array(dc.branches[f"vw{w}"]))
_plt.axis((0.9, 1.4, -50, 0))
_plt.title(f"l={l}µm")
_plt.xlabel("Vgs=Vds [V]")
_plt.ylabel("Ids [µA]")
_plt.legend(tuple(f"w={w}um" for w in ws))
_plt.grid(True)
_block()
Unsupported Ngspice version 36
def _block():
l = 2.0
ws = _np.arange(3.0, 30.5, 3)
vgs = slice(0, 3.31, 0.05)
ckt = sky130.cktfab.new_circuit(name="pmos_tb")
sb = ckt.new_net(name=f"sb", external=True)
for w in ws:
pmos = ckt.instantiate(prims.pfet_g5v0d10v5, name=f"pmos_w{w}", l=l, w=w)
ckt.new_net(name=f"nmos_w{w}_gd", external=True, childports=(
pmos.ports.sourcedrain2, pmos.ports.gate,
))
sb.childports += (pmos.ports.sourcedrain1, pmos.ports.bulk)
tb = sky130.pyspicefab.new_pyspicecircuit(
corner="io_tt", top=ckt, title="nmos_tb",
)
tb.V("gs", "sb", "gd", 1.8)
tb.V("gnd", "gd", tb.gnd, 0.0)
for w in ws:
# Current measurement
tb.V(f"w{w}", "gd", f"nmos_w{w}_gd", 0.0)
sim = tb.simulator(temperature=u_Degree(25))
dc = sim.dc(vgs=vgs)
_plt.figure(figsize=(16,8))
_plt.subplot(1, 2, 1)
for w in ws:
_plt.plot(dc.sweep, 1e6*_np.array(dc.branches[f"vw{w}"]))
_plt.axis((0.0, 3.3, -500, 0))
_plt.title(f"l={l}µm")
_plt.xlabel("Vgs=Vds [V]")
_plt.ylabel("Ids [µA]")
_plt.legend(tuple(f"w={w}um" for w in ws))
_plt.grid(True)
_plt.subplot(1, 2, 2)
for w in ws:
_plt.plot(dc.sweep, 1e6*_np.array(dc.branches[f"vw{w}"]))
_plt.axis((0.9, 1.4, -50, 0))
_plt.title(f"l={l}µm")
_plt.xlabel("Vgs=Vds [V]")
_plt.ylabel("Ids [µA]")
_plt.legend(tuple(f"w={w}um" for w in ws))
_plt.grid(True)
_block()
This is a quick design for a working bandgap design. Better optimization for sensitivity to process and voltage variations are left for a follow-up exercise.
We use 9 bipolar transistor in the design in a common centroid design: 1 in the first branch; 4 in the second and third branch. This results in a ratio for the bipolars for 1:4
pnp_ratio = 4
We assume a supply voltage of 3.3V nominal and design to optimize the temperature sensitivity for this nominal voltage. If we assume a voltage over the bipolars of around 0.7V we can design the circuit for 20µA for a l of 2.0µm. As Vt for nmos is lower than for pmos to compute the w of the transistors we use Vds of 1.25V for nmos and 1.35V for pmos. This gives values for w of 3.7µm for nmos and 18µm for the pmos.
lp = ln = 2.0
wn = 3.7
wp = 18.0
def _block():
_plt.figure(figsize=(8,8))
res1_hs = _np.arange(9.0, 13.1, 0.5)
for res1_h in res1_hs:
res2_h = 9*res1_h
bandgap = Bandgap(
nmos=prims.nfet_g5v0d10v5, nmos_params={"l": ln, "w": wn},
pmos=prims.pfet_g5v0d10v5, pmos_params={"l": lp, "w": wp},
resistor=prims.poly_res, r1_params={"height": res1_h}, r2_params={"height": res2_h},
pnp=prims.pnp_05v5_w3u40l3u40, pnp_params={}, pnp_ratio=pnp_ratio,
)
try:
dc = bandgap.temp_sweep(vdd=3.3, corner=("io_tt", "diode_tt", "pnp_t"), gmin=2e-9)
except:
print(f"{res1_h:.1f} failed")
continue
_plt.plot(
dc.sweep, 1e6*_np.array(dc.branches["v.xtop.vmp2_drain"]),
label=f"res1_h={res1_h:.1f}µm",
)
_plt.axis((-20, 80, 15, 28))
_plt.title(f"res2_h=1*res1_h")
_plt.xlabel("temp [°C]")
_plt.ylabel("vref [V]")
_plt.legend()
_plt.grid(True)
_block()
We take a height of 11.5µm for resistor1 height
res1_h = 11.5
Now we size resistor 2 to minimize the temperature sensitivity of the output voltage
def _block():
_plt.figure(figsize=(16,8))
_plt.subplot(1, 2, 1)
print("crude")
res2_hs = _np.arange(120.0, 180.5, 5)
for res2_h in res2_hs:
bandgap = Bandgap(
nmos=prims.nfet_g5v0d10v5, nmos_params={"l": ln, "w": wn},
pmos=prims.pfet_g5v0d10v5, pmos_params={"l": lp, "w": wp},
resistor=prims.poly_res, r1_params={"height": res1_h}, r2_params={"height": res2_h},
pnp=prims.pnp_05v5_w3u40l3u40, pnp_params={}, pnp_ratio=pnp_ratio,
)
try:
dc = bandgap.temp_sweep(vdd=3.3, corner=("io_tt", "diode_tt", "pnp_t"))
except:
print(f"{res2_h:.1f} failed")
continue
_plt.plot(dc.sweep, dc.nodes["vref"], label=f"res2_h={res2_h:.1f}µm")
_plt.axis((-20, 80, 1.075, 1.175))
_plt.title(f"res1_h={res1_h}µm")
_plt.xlabel("temp [°C]")
_plt.ylabel("vref [V]")
_plt.legend(loc="lower left")
_plt.grid(True)
_plt.subplot(1, 2, 2)
print("fine")
res2_hs = _np.arange(138.0, 142.1, 0.5)
for res2_h in res2_hs:
bandgap = Bandgap(
nmos=prims.nfet_g5v0d10v5, nmos_params={"l": ln, "w": wn},
pmos=prims.pfet_g5v0d10v5, pmos_params={"l": lp, "w": wp},
resistor=prims.poly_res, r1_params={"height": res1_h}, r2_params={"height": res2_h},
pnp=prims.pnp_05v5_w3u40l3u40, pnp_params={}, pnp_ratio=pnp_ratio,
)
dc = bandgap.temp_sweep(vdd=3.3, corner=("io_tt", "diode_tt", "pnp_t"))
_plt.plot(dc.sweep, dc.nodes["vref"], label=f"res2_h={res2_h:.1f}µm")
_plt.axis((-20, 80, 1.133, 1.145))
_plt.title(f"res1_h={res1_h}µm")
_plt.xlabel("temp [°C]")
_plt.ylabel("vref [V]")
_plt.legend(loc="lower left")
_plt.grid(True)
_block()
crude fine
For height of resistor 1 11.5µm we use height of 140µm for resistor 2. This corresponds with 7 fingers of 20µm.
res2_h = 140.0
Transistor sizing:
l | w | |
---|---|---|
nmos | 2.0µm | 3.7µm |
pmos | 2.0µm | 18µm |
Resistor sizing:
width | height | |
---|---|---|
R1 | 0.33µm | 11.5µm |
R2 | 0.33µm | 140µm |
bandgap = Bandgap(
nmos=prims.nfet_g5v0d10v5, nmos_params={"l": ln, "w": wn},
pmos=prims.pfet_g5v0d10v5, pmos_params={"l": lp, "w": wp},
resistor=prims.poly_res, r1_params={"height": res1_h}, r2_params={"height": res2_h},
pnp=prims.pnp_05v5_w3u40l3u40, pnp_params={}, pnp_ratio=pnp_ratio,
)
def _block():
_plt.figure(figsize=(10,8))
for io_corner in {"io_ff", "io_ss"}:
for pnp_corner in {"pnp_f", "pnp_s"}:
corner = (io_corner, pnp_corner, "diode_tt")
label = f"{io_corner};{pnp_corner}"
print(label)
try:
dc = bandgap.temp_sweep(vdd=3.3, corner=corner)
except:
print("failed")
continue
_plt.plot(dc.sweep, dc.vref, label=label)
_plt.legend()
_plt.grid(True)
_plt.axis((20, 80, 1.11, 1.17))
_plt.xlabel("temp [°C]")
_plt.ylabel("vref [V]")
_block()
io_ff;pnp_s io_ff;pnp_f io_ss;pnp_s io_ss;pnp_f
Below 60mV variation is seen over the different process corners.
def _block():
_plt.figure(figsize=(10,8))
for vdd in _np.arange(3.6, 2.39, -0.15):
label = f"vdd={vdd:.2f}V"
print(label)
try:
dc = bandgap.temp_sweep(vdd=vdd, corner=("io_tt", "diode_tt", "pnp_t"))
except:
print("failed")
continue
_plt.plot(dc.sweep, dc.vref, label=label)
_plt.legend()
_plt.grid(True)
_plt.axis((20, 80, 0.9, 1.2))
_plt.xlabel("temp [°C]")
_plt.ylabel("vref [V]")
_block()
vdd=3.60V vdd=3.45V vdd=3.30V vdd=3.15V vdd=3.00V vdd=2.85V vdd=2.70V vdd=2.55V vdd=2.40V
For the variation of the supply voltage of 3.0V to 3.6V an output voltage variation of around 120mV is seen on the output.