Tracer l'impédance microstrip vs largeur avec UliEngineering

Les graphiques suivants offrent un moyen simple de visualiser la relation entre la largeur du microstrip et l’impédance pour différentes hauteurs et épaisseurs de substrat.

Voir Prepreg and core thickness for different standard stackups pour plus d’informations sur les épaisseurs des stackups PCB standards et les valeurs de $\epsilon_r$.

Microstrip Impedance.svg

Une autre façon d’examiner les mêmes données consiste à tracer l’épaisseur du diélectrique vs la largeur de la piste pour des valeurs d’impédance prédéfinies :

Microstrip Width mm.svg

ou en mil :

Microstrip Width Mil.svg

Exemple de base

Ce script Python montre comment calculer l’impédance microstrip pour un ensemble unique de paramètres à l’aide de la bibliothèque UliEngineering.

example_microstrip_width_calculation.py
#!/usr/bin/env python3
from UliEngineering.Electronics.Microstrip import microstrip_width
from UliEngineering.EngineerIO import format_value
from UliEngineering.EngineerIO.Length import convert_length_to_unit

# Calculer la largeur pour un microstrip 50 Ω avec des arguments chaîne incluant les unités
w = microstrip_width("50 Ω", h="150 um", t="35 um", e_r="4.4")
# Convertir le résultat (mètres) en mils et afficher
w_mil = convert_length_to_unit(w, "m", "mil")
print(format_value(w_mil, "mil"))

Sortie du script d’exemple

output.txt
10.2 mil

Code source du tracé

Ce script est utilisé pour produire le tracé impédance vs largeur affiché ci-dessus.

plot_microstrip_impedance_vs_width.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter, AutoMinorLocator

# Imports UliEngineering
from UliEngineering.Electronics.Microstrip import microstrip_impedance
from UliEngineering.EngineerIO import format_value
from UliEngineering.EngineerIO.Length import normalize_length

# Constantes
e_r = 4.4
thicknesses = ["100um", "150um", "200um", "1550um"]
# Épaisseur de cuivre (35um par défaut car non spécifié mais requis pour le calcul)
t_copper = "35um"

# Axe des largeurs : 1mil à 10mm linéaire
w_min = normalize_length("1mil")
w_max = normalize_length("10mm")
# Pour un axe x logarithmique, nous avons besoin de largeurs espacées logarithmiquement ; utiliser np.logspace
widths = np.logspace(np.log10(w_min), np.log10(w_max), 1000)

# Tracé
plt.style.use("ggplot")
fig, ax = plt.subplots(figsize=(10, 12))

# Générer les couleurs
colors = plt.cm.viridis(np.linspace(0, 1, len(thicknesses)))

for h_str, c in zip(thicknesses, colors):
    # Calculer l'impédance pour chaque largeur
    # microstrip_impedance utilise des fonctions math donc ne supporte pas les tableaux numpy directement
    zs = [microstrip_impedance(w, h=h_str, t=t_copper, e_r=e_r) for w in widths]
    
    ax.plot(widths, zs, color=c, lw=2, label=f"h={h_str}")

# Ajouter une légende
legend = ax.legend(loc='upper right', fontsize='small')

ax.set_xscale("log")
ax.set_xlabel("Largeur (échelle log)")
ax.set_ylabel("Impédance")
ax.set_title(rf"Impédance microstrip vs largeur ($\epsilon_r={e_r}$)")
ax.grid(True, which="both", ls="--")

# Formateurs d'axes utilisant format_value d'EngineerIO
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: format_value(x, "m")))
ax.yaxis.set_major_formatter(FuncFormatter(lambda y, pos: format_value(y, "Ω")))

# Activer les graduations mineures et ajouter des étiquettes de graduation mineures sur l'axe Y
ax.minorticks_on()
# Pour une échelle linéaire, AutoMinorLocator est généralement suffisant, ou laisser matplotlib gérer.
# Le modèle utilisait LogLocator, mais nous sommes en linéaire.
ax.yaxis.set_minor_locator(AutoMinorLocator())
ax.yaxis.set_minor_formatter(FuncFormatter(lambda y, pos: format_value(y, "Ω")))

# Style des étiquettes de graduation mineures : 30 % plus petites et gris à 70 %
maj_ylabels = ax.yaxis.get_ticklabels(which='major')
if len(maj_ylabels) > 0:
    base_size = maj_ylabels[0].get_size()
else:
    base_size = plt.rcParams.get('ytick.labelsize', plt.rcParams.get('font.size', 10))
ax.tick_params(axis='y', which='minor', labelsize=base_size * 0.7, labelcolor='0.7', colors='0.7')

# Définir également les graduations mineures de l'axe X et les styliser avec des étiquettes plus petites et pivotées
from matplotlib.ticker import LogLocator
ax.xaxis.set_minor_locator(LogLocator(base=10.0, subs=(2, 3, 4, 5, 6, 7, 8, 9)))
ax.xaxis.set_minor_formatter(FuncFormatter(lambda x, pos: format_value(x, "m")))

maj_xlabels = ax.xaxis.get_ticklabels(which='major')
if len(maj_xlabels) > 0:
    base_x_size = maj_xlabels[0].get_size()
else:
    base_x_size = plt.rcParams.get('xtick.labelsize', plt.rcParams.get('font.size', 10))
ax.tick_params(axis='x', which='minor', labelsize=base_x_size * 0.7, labelcolor='0.7', colors='0.7')

for tl in ax.get_xminorticklabels():
    tl.set_rotation(90)
for tl in ax.get_xmajorticklabels():
    tl.set_rotation(90)

fig.tight_layout()
plt.show()
plt.savefig("Microstrip-Impedance.svg")

D’autre part, le script suivant produit un tracé de l’épaisseur du diélectrique vs pour des valeurs d’impédance prédéfinies (le deuxième et le troisième tracé affichés ci-dessus, le troisième étant généré avec --y-unit mil).

plot_microstrip_width_vs_thickness.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
import argparse
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter, LogLocator

# UliEngineering imports
from UliEngineering.Electronics.Microstrip import microstrip_width
from UliEngineering.EngineerIO import format_value
from UliEngineering.EngineerIO.Length import normalize_length, convert_length_to_unit

# Ligne de commande : choisir les unités de l'axe Y (mètres par défaut, ou "mil")
parser = argparse.ArgumentParser(description="Tracer la largeur microstrip vs l'épaisseur du diélectrique")
parser.add_argument("--y-unit", choices=("m", "mil"), default="m",
                    help="Unités pour l'axe Y : 'm' pour mètres (par défaut) ou 'mil' pour mils")
args = parser.parse_args()
y_unit = args.y_unit

# Constantes
e_r = 4.4
impedances = [50, 75, 100]
# Épaisseur de cuivre
t_copper = "35um"

# Axe des épaisseurs : 70um à 2.0mm (échantillonnage logarithmique pour des valeurs bien espacées sur un axe x logarithmique)
h_min = normalize_length("70um")
h_max = normalize_length("2mm")
thicknesses = np.logspace(np.log10(h_min), np.log10(h_max), num=1000)

# Tracé
plt.style.use("ggplot")
fig, ax = plt.subplots(figsize=(10, 8))

# Générer les couleurs avec viridis pour des couleurs plus esthétiques et uniformes sur le plan perceptuel
colors = plt.cm.viridis(np.linspace(0, 1, len(impedances)))

# Calculer les traces de largeur et les collecter pour déterminer les limites Y si nécessaire
all_ws = []
for z0, c in zip(impedances, colors):
    ws = [microstrip_width(Z0=z0, h=h, t=t_copper, e_r=e_r) for h in thicknesses]
    ax.plot(thicknesses, ws, color=c, lw=2, label=f"Z0={z0} Ω")
    all_ws.append(np.array(ws))

# Ajouter une légende
legend = ax.legend(loc='upper left', fontsize='medium')

ax.set_xlabel("Épaisseur du diélectrique")
ax.set_ylabel(f"Largeur de la piste ({'m' if y_unit == 'm' else 'mil'})")
ax.set_title(rf"Largeur microstrip vs épaisseur du diélectrique ($\epsilon_r={e_r}$, $t_{{Cu}}={t_copper}$)")
ax.grid(True, which="both", ls="--")

# Mise à l'échelle logarithmique sur les deux axes
ax.set_xscale("log")
ax.set_yscale("log")

# Imposer les limites X de 70 µm à 2.0 mm exactement
ax.set_xlim(h_min, h_max)

# Configurer les locateurs majeurs et mineurs pour les échelles logarithmiques
major_x_locator = LogLocator(base=10.0)
minor_x_locator = LogLocator(base=10.0, subs=(2, 3, 4, 5, 6, 7, 8, 9))
major_y_locator = LogLocator(base=10.0)
minor_y_locator = LogLocator(base=10.0, subs=(2, 3, 4, 5, 6, 7, 8, 9))
ax.xaxis.set_major_locator(major_x_locator)
ax.xaxis.set_minor_locator(minor_x_locator)
ax.yaxis.set_major_locator(major_y_locator)
ax.yaxis.set_minor_locator(minor_y_locator)

# Formateurs d'axes utilisant format_value d'EngineerIO
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: format_value(x, "m")))

# Le formateur de l'axe Y dépend de l'unité demandée.
if y_unit == "m":
    ax.yaxis.set_major_formatter(FuncFormatter(lambda y, pos: format_value(y, "m")))
    ax.yaxis.set_minor_formatter(FuncFormatter(lambda y, pos: format_value(y, "m")))
else:
    # Formater les valeurs en mils sans préfixes SI à l'aide de la fonction utilitaire
    def format_mil_no_prefix(y, pos):
        mils = convert_length_to_unit(y, "m", "mil")
        if mils >= 100:
            s = f"{int(round(mils))} mil"
        elif mils >= 10:
            s = f"{mils:.1f} mil"
        elif mils >= 1:
            s = f"{mils:.2f} mil"
        else:
            s = f"{mils:.3f} mil"
        return s

    ax.yaxis.set_major_formatter(FuncFormatter(format_mil_no_prefix))
    ax.yaxis.set_minor_formatter(FuncFormatter(format_mil_no_prefix))

# Activer les graduations mineures
ax.minorticks_on()
ax.xaxis.set_minor_formatter(FuncFormatter(lambda x, pos: format_value(x, "m")))

# Calculer et définir les graduations X contraintes à la plage X demandée
major_locs = major_x_locator.tick_values(h_min, h_max)
minor_locs = minor_x_locator.tick_values(h_min, h_max)
major_locs = np.array([m for m in major_locs if m >= h_min and m <= h_max])
minor_locs = np.array([m for m in minor_locs if m >= h_min and m <= h_max])

# Ajouter une graduation mineure spéciale à X = 1.55 mm
special_tick = normalize_length("1.55mm")
if special_tick not in minor_locs:
    minor_locs = np.sort(np.concatenate((minor_locs, [special_tick])))

# Définir les graduations explicitement
ax.set_xticks(major_locs, minor=False)
ax.set_xticks(minor_locs, minor=True)

# Tracer également un guide vertical subtil à 1.55 mm pour mettre en évidence le point spécial
ax.axvline(special_tick, color='0.5', linestyle=':', linewidth=1)

# Calculer la plage Y à partir des données et définir les graduations appropriées en mode mil
if len(all_ws) > 0:
    y_all = np.concatenate(all_ws)
    y_all = y_all[y_all > 0]
    if len(y_all) > 0:
        y_min = np.min(y_all)
        y_max = np.max(y_all)
        # marge
        y_pad = (np.log10(y_max) - np.log10(y_min)) * 0.05 if y_max > y_min else 0.1
        ax.set_ylim(10 ** (np.log10(y_min) - y_pad), 10 ** (np.log10(y_max) + y_pad))

        if y_unit == 'mil':
            yl, yu = ax.get_ylim()
            yl_mil = convert_length_to_unit(yl, 'm', 'mil')
            yu_mil = convert_length_to_unit(yu, 'm', 'mil')
            # choisir les décades majeures en mils (10, 100, 1000, ...)
            dmin = int(np.floor(np.log10(max(yl_mil, 1e-12))))
            dmax = int(np.ceil(np.log10(max(yu_mil, 1e-12))))
            # S'assurer d'inclure 1 mil (10^0) comme graduation majeure si possible
            dstart = max(0, dmin)
            majors_mil = [10 ** d for d in range(dstart, dmax + 1)]
            majors_m = [convert_length_to_unit(m, 'mil', 'm') for m in majors_mil]
            minors_m = []
            for d in range(dstart, dmax + 1):
                for s in (2, 3, 4, 5, 6, 7, 8, 9):
                    v_mil = s * (10 ** d)
                    minors_m.append(convert_length_to_unit(v_mil, 'mil', 'm'))
            majors_m = np.array([m for m in majors_m if m >= yl and m <= yu])
            minors_m = np.array([m for m in minors_m if m >= yl and m <= yu])
            ax.set_yticks(majors_m, minor=False)
            ax.set_yticks(minors_m, minor=True)

# Pivoter les étiquettes si nécessaire
for tl in ax.get_xminorticklabels():
    tl.set_rotation(90)
for tl in ax.get_xmajorticklabels():
    tl.set_rotation(90)

# Style des étiquettes de graduation mineures : 30 % plus petites et gris à 70 % (les deux axes)
maj_ylabels = ax.yaxis.get_ticklabels(which='major')
if len(maj_ylabels) > 0:
    base_y_size = maj_ylabels[0].get_size()
else:
    base_y_size = plt.rcParams.get('ytick.labelsize', plt.rcParams.get('font.size', 10))
maj_xlabels = ax.xaxis.get_ticklabels(which='major')
if len(maj_xlabels) > 0:
    base_x_size = maj_xlabels[0].get_size()
else:
    base_x_size = plt.rcParams.get('xtick.labelsize', plt.rcParams.get('font.size', 10))
ax.tick_params(axis='y', which='minor', labelsize=base_y_size * 0.7, labelcolor='0.7', colors='0.7')
ax.tick_params(axis='x', which='minor', labelsize=base_x_size * 0.7, labelcolor='0.7', colors='0.7')

fig.tight_layout()
plt.savefig("Microstrip-Width.svg", dpi=300, bbox_inches='tight')
# N'afficher interactivement que lorsqu'on n'est pas dans un CI headless
try:
    plt.show()
except Exception:
    pass

Consultez les articles similaires par catégorie : Electronics, RF