341 lines
11 KiB
Python
341 lines
11 KiB
Python
# device_pcad_config.py
|
|
# Process-Step Oriented 2D Doping Configuration File
|
|
# This file coexists with device_config.py and keeps the original doping path intact.
|
|
|
|
import os
|
|
import sys
|
|
import numpy as np
|
|
import math
|
|
|
|
# All units in cm (1 um = 1e-4 cm)
|
|
um = 1e-4
|
|
|
|
# Import geometric and simulation parameters from the original device_config
|
|
from device_config import (
|
|
W_DEVICE, H_SI, T_OX, H_MOLD, W_SIDE_MOLD, W_SIM,
|
|
VIA_WIDTH, VIA_P11_X, VIA_P13_X,
|
|
MT1_FP1_X1, MT1_FP1_X2, MT1_FP2_X1, MT1_FP2_X2,
|
|
SIM_NAME
|
|
)
|
|
|
|
# Define mask openings (Horizontal coordinate ranges, positive X-axis. Mirroring is handled dynamically)
|
|
masks = {
|
|
'p_well_mask': [(75.0 * um, 100.0 * um)],
|
|
'p12_mask': [(120.0 * um, 130.0 * um)],
|
|
'p13_mask': [(150.0 * um, 255.0 * um)],
|
|
'nplus_mask': [(164.0 * um, 185.0 * um)],
|
|
'mring_mask': [(340.0 * um, W_DEVICE)],
|
|
}
|
|
|
|
# --- Process Database ---
|
|
# Implant Range & Straggle in Silicon (Energy in keV, Rp and dRp in microns)
|
|
IMPLANT_DB = {
|
|
'Boron': {
|
|
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
|
|
'Rp': np.array([0.04, 0.07, 0.10, 0.16, 0.24, 0.30, 0.42, 0.55]),
|
|
'dRp': np.array([0.015, 0.025, 0.035, 0.050, 0.063, 0.070, 0.085, 0.100])
|
|
},
|
|
'Phosphorus': {
|
|
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
|
|
'Rp': np.array([0.015, 0.028, 0.040, 0.065, 0.100, 0.120, 0.180, 0.240]),
|
|
'dRp': np.array([0.007, 0.012, 0.016, 0.025, 0.038, 0.045, 0.060, 0.075])
|
|
},
|
|
'Arsenic': {
|
|
'energy': np.array([10.0, 20.0, 30.0, 50.0, 80.0, 100.0, 150.0, 200.0]),
|
|
'Rp': np.array([0.010, 0.018, 0.025, 0.038, 0.058, 0.070, 0.100, 0.130]),
|
|
'dRp': np.array([0.004, 0.007, 0.009, 0.014, 0.020, 0.025, 0.035, 0.045])
|
|
}
|
|
}
|
|
|
|
# Diffusion Arrhenius parameters in Silicon (D0 in cm^2/s, Ea in eV)
|
|
DIFFUSION_DB = {
|
|
'Boron': {'D0': 1.0, 'Ea': 3.46},
|
|
'Phosphorus': {'D0': 10.5, 'Ea': 3.69},
|
|
'Arsenic': {'D0': 0.32, 'Ea': 3.56}
|
|
}
|
|
|
|
def get_implant_params(dopant, energy_kev):
|
|
"""Interpolate Rp and dRp from database for a given energy in keV. Returns values in cm."""
|
|
db = IMPLANT_DB.get(dopant, IMPLANT_DB['Boron'])
|
|
e = np.clip(energy_kev, db['energy'][0], db['energy'][-1])
|
|
Rp_um = np.interp(e, db['energy'], db['Rp'])
|
|
dRp_um = np.interp(e, db['energy'], db['dRp'])
|
|
return Rp_um * 1e-4, dRp_um * 1e-4
|
|
|
|
def get_diffusion_coefficient(dopant, temp_c):
|
|
"""Calculate the diffusion coefficient D in cm^2/s at temperature temp_c in Celsius."""
|
|
db = DIFFUSION_DB.get(dopant, DIFFUSION_DB['Boron'])
|
|
T_k = temp_c + 273.15
|
|
k_B = 8.617333262145e-5 # eV/K
|
|
D0 = db['D0']
|
|
Ea = db['Ea']
|
|
return D0 * np.exp(-Ea / (k_B * T_k))
|
|
|
|
# Define process steps for the 2D doping profile
|
|
process_steps = [
|
|
# Step 0: Substrate Wafer
|
|
{
|
|
'enabled': True,
|
|
'name': 'Substrate',
|
|
'method': 'Substrate',
|
|
'type': 'n',
|
|
'doping': 1.0e14, # N_SUB
|
|
},
|
|
# Step 1: P11 Well Implant & Diffusion
|
|
{
|
|
'enabled': True,
|
|
'name': 'P11_Well',
|
|
'method': 'Implant_and_Diffuse',
|
|
'type': 'p',
|
|
'dopant': 'Boron',
|
|
'energy': 80.0, # keV
|
|
'dose': 1.77245385091e12, # cm^-2
|
|
'mask': 'p_well_mask',
|
|
'temp': 1150.0, # Celsius
|
|
'time': 465.54227, # minutes
|
|
'lateral_ratio': 0.8,
|
|
},
|
|
# Step 2: P12 Well Implant & Diffusion
|
|
{
|
|
'enabled': True,
|
|
'name': 'P12_Well',
|
|
'method': 'Implant_and_Diffuse',
|
|
'type': 'p',
|
|
'dopant': 'Boron',
|
|
'energy': 80.0,
|
|
'dose': 6.6467019409e11,
|
|
'mask': 'p12_mask',
|
|
'temp': 1150.0,
|
|
'time': 465.54227,
|
|
'lateral_ratio': 0.8,
|
|
},
|
|
# Step 3: P13 Well Implant & Diffusion
|
|
{
|
|
'enabled': True,
|
|
'name': 'P13_Well',
|
|
'method': 'Implant_and_Diffuse',
|
|
'type': 'p',
|
|
'dopant': 'Boron',
|
|
'energy': 80.0,
|
|
'dose': 1.77245385091e12,
|
|
'mask': 'p13_mask',
|
|
'temp': 1150.0,
|
|
'time': 465.54227,
|
|
'lateral_ratio': 0.8,
|
|
},
|
|
# Step 4: N+ Source Active Region
|
|
{
|
|
'enabled': True,
|
|
'name': 'NPlus_Active',
|
|
'method': 'Implant_and_Diffuse',
|
|
'type': 'n',
|
|
'dopant': 'Phosphorus',
|
|
'energy': 50.0,
|
|
'dose': 2.65868077636e11,
|
|
'mask': 'nplus_mask',
|
|
'temp': 1000.0,
|
|
'time': 34.108586,
|
|
'lateral_ratio': 0.666666666667,
|
|
},
|
|
# Step 5: MRING Guard Ring Active Region
|
|
{
|
|
'enabled': True,
|
|
'name': 'MRing_Active',
|
|
'method': 'Implant_and_Diffuse',
|
|
'type': 'n',
|
|
'dopant': 'Phosphorus',
|
|
'energy': 50.0,
|
|
'dose': 2.65868077636e11,
|
|
'mask': 'mring_mask',
|
|
'temp': 1000.0,
|
|
'time': 34.108586,
|
|
'lateral_ratio': 0.666666666667,
|
|
}
|
|
]
|
|
|
|
def resolve_steps():
|
|
"""
|
|
Resolves the physical process steps into analytical model parameters
|
|
(peak, vdiff, hdiff, x_ranges) for profile evaluation.
|
|
"""
|
|
resolved = []
|
|
|
|
# 1. Resolve substrate first
|
|
sub_step = [s for s in process_steps if s['method'] == 'Substrate'][0]
|
|
if sub_step.get('enabled', True):
|
|
resolved.append({
|
|
'name': sub_step.get('name', 'Substrate'),
|
|
'method': 'Substrate',
|
|
'type': sub_step['type'],
|
|
'doping': sub_step['doping']
|
|
})
|
|
|
|
# 2. Resolve implant/diffusion steps
|
|
for step in process_steps:
|
|
if step['method'] == 'Substrate' or not step.get('enabled', True):
|
|
continue
|
|
|
|
name = step.get('name')
|
|
type_ = step['type']
|
|
dopant = step['dopant']
|
|
method = step['method']
|
|
|
|
# Look up mask coordinate ranges
|
|
mask_name = step['mask']
|
|
x_ranges = masks.get(mask_name, [])
|
|
|
|
# Get implant properties
|
|
energy = step.get('energy', 80.0)
|
|
dose = step.get('dose', 1.0e12)
|
|
Rp, dRp = get_implant_params(dopant, energy)
|
|
|
|
# Get thermal diffusion properties
|
|
temp = step.get('temp', 1000.0)
|
|
time_min = step.get('time', 30.0)
|
|
|
|
D = get_diffusion_coefficient(dopant, temp)
|
|
time_sec = time_min * 60.0
|
|
Dt = D * time_sec
|
|
|
|
# Standard deviation incorporating both implant straggle and thermal budget diffusion
|
|
vdiff = math.sqrt(2.0 * (dRp ** 2) + 4.0 * Dt)
|
|
|
|
# Horizontal standard deviation based on lateral ratio
|
|
lateral_ratio = step.get('lateral_ratio', 0.8)
|
|
hdiff = vdiff * lateral_ratio
|
|
|
|
# Calculate peak concentration matching the integrated dose for erfc profile:
|
|
# peak = dose / (vdiff * (sqrt(pi) / 2))
|
|
peak = dose / (vdiff * (math.sqrt(math.pi) / 2.0))
|
|
|
|
resolved.append({
|
|
'name': name,
|
|
'method': 'Implant_and_Diffuse',
|
|
'type': type_,
|
|
'peak': peak,
|
|
'vdiff': vdiff,
|
|
'hdiff': hdiff,
|
|
'x_ranges': x_ranges
|
|
})
|
|
|
|
return resolved
|
|
|
|
def apply_pcad_doping_2d(device, region="Silicon"):
|
|
"""
|
|
Parses the process steps list and registers the corresponding
|
|
2D analytical doping models in DEVSIM.
|
|
"""
|
|
import devsim
|
|
|
|
donor_terms = []
|
|
acceptor_terms = []
|
|
|
|
steps = resolve_steps()
|
|
|
|
# 1. Setup Substrate baseline first
|
|
sub_step = [s for s in steps if s['method'] == 'Substrate'][0]
|
|
sub_doping = sub_step['doping']
|
|
sub_type = sub_step['type']
|
|
|
|
devsim.node_model(device=device, region=region, name="nD_sub", equation=f"{sub_doping}")
|
|
|
|
if sub_type == 'n':
|
|
donor_terms.append("nD_sub")
|
|
else:
|
|
acceptor_terms.append("nD_sub")
|
|
|
|
# 2. Iterate and build expressions for Implant/Diffusion steps
|
|
idx = 1
|
|
for step in steps:
|
|
if step['method'] == 'Substrate':
|
|
continue
|
|
|
|
name = step['name']
|
|
type_ = step['type']
|
|
peak = step['peak']
|
|
x_ranges = step['x_ranges']
|
|
vdiff = step['vdiff']
|
|
hdiff = step['hdiff']
|
|
|
|
# Construct analytical 2D erfc expression for each window range, including mirroring
|
|
expr_terms = []
|
|
for x1, x2 in x_ranges:
|
|
# Right side (positive X)
|
|
expr_terms.append(f"erfc(y / {vdiff}) * 0.5 * (erf((x - ({x1})) / {hdiff}) - erf((x - ({x2})) / {hdiff}))")
|
|
# Left side (negative X, mirrored: x1 -> -x1 and x2 -> -x2, so window is [-x2, -x1])
|
|
expr_terms.append(f"erfc(y / {vdiff}) * 0.5 * (erf((x - ({-x2})) / {hdiff}) - erf((x - ({-x1})) / {hdiff}))")
|
|
|
|
# Combine terms and multiply by peak concentration
|
|
combined_expr = f"{peak} * (" + " + ".join(expr_terms) + ")"
|
|
|
|
model_name = f"nA_{name}" if type_ == 'p' else f"nD_{name}"
|
|
devsim.node_model(device=device, region=region, name=model_name, equation=combined_expr)
|
|
|
|
if type_ == 'n':
|
|
donor_terms.append(model_name)
|
|
else:
|
|
acceptor_terms.append(model_name)
|
|
|
|
idx += 1
|
|
|
|
# 3. Combine separate models into global Donors and Acceptors fields in DEVSIM
|
|
donor_equation = " + ".join(donor_terms)
|
|
acceptor_equation = "1e10 + " + " + ".join(acceptor_terms)
|
|
|
|
devsim.node_model(device=device, region=region, name="Donors", equation=donor_equation)
|
|
devsim.node_model(device=device, region=region, name="Acceptors", equation=acceptor_equation)
|
|
devsim.node_model(device=device, region=region, name="NetDoping", equation="Donors - Acceptors")
|
|
devsim.node_model(device=device, region=region, name="LogNetDoping", equation="asinh(NetDoping / 2.0) / log(10.0)")
|
|
|
|
def get_pcad_doping_val_2d(x, y):
|
|
"""
|
|
Evaluates the 2D process-step doping profile analytically on numpy arrays x and y.
|
|
"""
|
|
import numpy as np
|
|
import math
|
|
|
|
erf_vec = np.vectorize(math.erf)
|
|
erfc_vec = np.vectorize(math.erfc)
|
|
|
|
donors = np.zeros_like(x, dtype=float)
|
|
acceptors = np.zeros_like(x, dtype=float)
|
|
|
|
steps = resolve_steps()
|
|
|
|
# 1. Setup Substrate baseline first
|
|
sub_step = [s for s in steps if s['method'] == 'Substrate'][0]
|
|
sub_doping = sub_step['doping']
|
|
sub_type = sub_step['type']
|
|
if sub_type == 'n':
|
|
donors += sub_doping
|
|
else:
|
|
acceptors += sub_doping
|
|
|
|
# 2. Iterate and build expressions for Implant/Diffusion steps
|
|
for step in steps:
|
|
if step['method'] == 'Substrate':
|
|
continue
|
|
|
|
type_ = step['type']
|
|
peak = step['peak']
|
|
x_ranges = step['x_ranges']
|
|
vdiff = step['vdiff']
|
|
hdiff = step['hdiff']
|
|
|
|
prof = np.zeros_like(x, dtype=float)
|
|
for x1, x2 in x_ranges:
|
|
# Right side
|
|
prof += erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - x1) / hdiff) - erf_vec((x - x2) / hdiff))
|
|
# Left side (mirrored)
|
|
prof += erfc_vec(y / vdiff) * 0.5 * (erf_vec((x - (-x2)) / hdiff) - erf_vec((x - (-x1)) / hdiff))
|
|
|
|
if type_ == 'n':
|
|
donors += peak * prof
|
|
else:
|
|
acceptors += peak * prof
|
|
|
|
# Acceptor base offset 1e10
|
|
acceptors += 1e10
|
|
|
|
return donors - acceptors
|