Tutorial: Microstrip Patch Antenna Design
This tutorial covers designing and simulating microstrip patch antennas with EdgeFEM. You'll learn to:
- Design a patch antenna for a target frequency
- Configure feed mechanisms (probe and edge feed)
- Compute input impedance and return loss
- Generate radiation patterns
- Analyze bandwidth and directivity
Background
Microstrip patch antennas are popular for their low profile, ease of fabrication, and integration with PCBs. A rectangular patch antenna resonates when its length is approximately half a wavelength in the dielectric medium.
Design equations:
- Effective dielectric constant: \(\varepsilon_{eff} = \frac{\varepsilon_r + 1}{2} + \frac{\varepsilon_r - 1}{2}\left(1 + 12\frac{h}{W}\right)^{-1/2}\)
- Fringing extension: \(\Delta L = 0.412h \frac{(\varepsilon_{eff} + 0.3)(W/h + 0.264)}{(\varepsilon_{eff} - 0.258)(W/h + 0.8)}\)
- Resonant frequency: \(f_r = \frac{c}{2(L + 2\Delta L)\sqrt{\varepsilon_{eff}}}\)
Step 1: Design for Target Frequency
Let's design a 2.4 GHz patch antenna on FR-4 substrate:
from edgefem.designs import PatchAntennaDesign
import numpy as np
# Target frequency
f_target = 2.4e9 # 2.4 GHz (WiFi band)
# Substrate parameters (FR-4)
eps_r = 4.4 # Relative permittivity
tan_d = 0.02 # Loss tangent
h = 1.6e-3 # Thickness: 1.6 mm (standard)
# Initial patch dimensions (will be refined)
# Rule of thumb: L ≈ c / (2 * f * sqrt(eps_eff))
c0 = 299792458.0
eps_eff_approx = (eps_r + 1) / 2
L_approx = c0 / (2 * f_target * np.sqrt(eps_eff_approx))
W_approx = 1.3 * L_approx # Width slightly larger for better efficiency
print(f"Initial estimate: L = {L_approx*1000:.1f} mm, W = {W_approx*1000:.1f} mm")
Create the patch antenna:
patch = PatchAntennaDesign(
patch_length=29e-3, # 29 mm (resonant dimension)
patch_width=38e-3, # 38 mm
substrate_height=h,
substrate_eps_r=eps_r,
substrate_tan_d=tan_d,
)
# EdgeFEM estimates resonant frequency
print(f"Estimated resonant frequency: {patch.estimated_resonant_frequency/1e9:.3f} GHz")
Step 2: Configure Feed
Option A: Coaxial Probe Feed
The probe feed connects the ground plane to the patch through the substrate:
# Probe at optimal position for ~50 ohm match
# Position is relative to patch center
patch.set_probe_feed(
x_offset=0, # Centered in width
y_offset=-5e-3, # 5mm from center toward edge
probe_radius=0.5e-3 # 0.5mm radius probe
)
Feed position affects input impedance: - At edge: highest impedance (~200-400 Ω) - At center: zero impedance (null point) - Intermediate: use for 50 Ω match
Option B: Edge Feed with Inset
For microstrip feed line integration:
# Alternative: inset edge feed
patch_edge = PatchAntennaDesign(
patch_length=29e-3,
patch_width=38e-3,
substrate_height=h,
substrate_eps_r=eps_r,
)
patch_edge.set_edge_feed(
inset_length=8e-3, # 8mm inset for impedance matching
line_width=3e-3, # 3mm feed line (≈50Ω on FR-4)
edge='y_neg' # Feed from -Y edge
)
Step 3: Generate Mesh
# Generate mesh (15 elements per wavelength recommended for antennas)
patch.generate_mesh(density=15)
print(f"Mesh generated: {len(patch.mesh.elements)} elements")
Save mesh for inspection:
patch.generate_mesh(density=15, output_path="patch_antenna")
# Creates: patch_antenna.geo, patch_antenna.msh
Step 4: Input Impedance Analysis
# Compute input impedance at design frequency
Z_in = patch.input_impedance(2.4e9)
print(f"\nInput impedance at 2.4 GHz:")
print(f" Z_in = {Z_in.real:.1f} + j{Z_in.imag:.1f} Ω")
# Reflection coefficient (assuming 50Ω system)
Z0 = 50
Gamma = (Z_in - Z0) / (Z_in + Z0)
print(f"\nReflection coefficient:")
print(f" |Γ| = {abs(Gamma):.4f}")
print(f" Return loss = {20*np.log10(abs(Gamma)+1e-10):.1f} dB")
# VSWR
vswr = (1 + abs(Gamma)) / (1 - abs(Gamma))
print(f" VSWR = {vswr:.2f}")
Step 5: Return Loss Sweep
Find the resonant frequency and bandwidth:
# Frequency sweep around design frequency
freqs, Z_in_array, S11_array = patch.frequency_sweep(
f_start=2.0e9,
f_stop=2.8e9,
n_points=81,
z0=50.0,
verbose=True
)
# Find minimum return loss (resonant frequency)
rl_db = 20 * np.log10(np.abs(S11_array) + 1e-10)
idx_min = np.argmin(rl_db)
f_resonant = freqs[idx_min]
print(f"\nResonant frequency: {f_resonant/1e9:.3f} GHz")
print(f"Minimum return loss: {rl_db[idx_min]:.1f} dB")
Plot the results:
import matplotlib.pyplot as plt
import edgefem.plots as vp
# Return loss plot
vp.plot_return_loss(freqs, S11_array,
title="Patch Antenna Return Loss",
save="patch_return_loss.png")
Custom impedance plot:
fig, axes = plt.subplots(2, 1, figsize=(10, 8))
# Return loss
axes[0].plot(freqs/1e9, rl_db, 'b-', linewidth=2)
axes[0].axhline(y=-10, color='r', linestyle='--', label='-10 dB')
axes[0].set_xlabel('Frequency (GHz)')
axes[0].set_ylabel('Return Loss (dB)')
axes[0].set_title('Patch Antenna Return Loss')
axes[0].legend()
axes[0].grid(True)
axes[0].set_ylim(-30, 0)
# Input impedance
axes[1].plot(freqs/1e9, Z_in_array.real, 'b-', label='Re(Z)', linewidth=2)
axes[1].plot(freqs/1e9, Z_in_array.imag, 'r--', label='Im(Z)', linewidth=2)
axes[1].axhline(y=50, color='g', linestyle=':', label='50Ω')
axes[1].set_xlabel('Frequency (GHz)')
axes[1].set_ylabel('Impedance (Ω)')
axes[1].set_title('Input Impedance')
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
plt.savefig('patch_impedance.png', dpi=150)
plt.show()
Step 6: Bandwidth Analysis
# Compute bandwidth for VSWR < 2 (return loss < -10 dB)
f_low, f_high, bw_percent = patch.bandwidth(
vswr_threshold=2.0,
z0=50.0
)
print(f"\nBandwidth (VSWR < 2.0):")
print(f" Lower frequency: {f_low/1e9:.3f} GHz")
print(f" Upper frequency: {f_high/1e9:.3f} GHz")
print(f" Bandwidth: {bw_percent:.1f}%")
print(f" Bandwidth: {(f_high - f_low)/1e6:.1f} MHz")
Step 7: Radiation Pattern
Compute the 3D far-field pattern:
# Compute radiation pattern at resonance
pattern = patch.radiation_pattern(f_resonant, n_theta=91, n_phi=73)
# Compute directivity
D = patch.directivity(f_resonant)
D_dBi = 10 * np.log10(D)
print(f"\nRadiation characteristics at {f_resonant/1e9:.3f} GHz:")
print(f" Directivity: {D_dBi:.1f} dBi")
Plot pattern cuts:
# E-plane and H-plane cuts
vp.plot_pattern_3d_cuts(
pattern,
title=f"Patch Antenna Pattern ({f_resonant/1e9:.2f} GHz)",
min_db=-30,
save="patch_pattern_cuts.png"
)
Interactive 3D pattern (requires plotly):
try:
vp.plot_pattern_3d(
pattern,
title="Patch Antenna 3D Pattern",
interactive=True,
save="patch_pattern_3d.html"
)
print("Interactive 3D pattern saved to patch_pattern_3d.html")
except ImportError:
print("Install plotly for 3D interactive: pip install plotly")
Step 8: Export Results
Touchstone Format
# Export S11 to Touchstone
patch.export_touchstone("patch_antenna", freqs, S11_array)
print("Saved: patch_antenna.s1p")
Pattern Data
# Export pattern to CSV
patch.export_pattern_csv("patch_pattern.csv", f_resonant)
print("Saved: patch_pattern.csv")
Design Optimization
To adjust the design for better matching:
# Parametric study: vary feed position
feed_positions = np.linspace(-10e-3, -3e-3, 8)
results = []
for y_pos in feed_positions:
# Create new patch with different feed position
p = PatchAntennaDesign(
patch_length=29e-3,
patch_width=38e-3,
substrate_height=1.6e-3,
substrate_eps_r=4.4,
)
p.set_probe_feed(x_offset=0, y_offset=y_pos)
p.generate_mesh(density=12)
Z_in = p.input_impedance(2.4e9)
rl = p.return_loss(2.4e9)
results.append({
'y_pos': y_pos * 1000, # mm
'R': Z_in.real,
'X': Z_in.imag,
'RL': rl
})
print(f"y = {y_pos*1000:.1f} mm: Z = {Z_in.real:.0f}+j{Z_in.imag:.0f} Ω, RL = {rl:.1f} dB")
# Find best match
best = min(results, key=lambda r: abs(r['R'] - 50) + abs(r['X']))
print(f"\nBest match at y = {best['y_pos']:.1f} mm: Z = {best['R']:.0f}+j{best['X']:.0f} Ω")
Design Guidelines
Substrate Selection
| Substrate | εr | tan δ | Use Case |
|---|---|---|---|
| FR-4 | 4.4 | 0.02 | Low cost, WiFi |
| Rogers RO4003C | 3.55 | 0.0027 | RF/microwave |
| Rogers RT/duroid 5880 | 2.2 | 0.0009 | High frequency |
| Alumina | 9.8 | 0.0001 | mmWave |
Dimensions
| Parameter | Typical Range | Effect |
|---|---|---|
| Length L | λ_eff/2 | Sets resonant frequency |
| Width W | 1.2-1.5 × L | Affects bandwidth, efficiency |
| Height h | 0.003-0.05 λ₀ | Higher = wider BW, more loss |
Feed Position
| Feed Type | Impedance Range | Advantage |
|---|---|---|
| Probe at edge | 200-400 Ω | Simple |
| Probe offset | 20-100 Ω | Direct 50Ω match |
| Inset edge | 20-100 Ω | Planar, PCB compatible |
| Aperture coupled | Wide range | Better BW, isolation |
Troubleshooting
Resonance too high/low
- Too high: Increase patch length
- Too low: Decrease patch length
- Rule: Δf/f ≈ -ΔL/L
Can't achieve 50Ω match
- Adjust feed position (probe) or inset depth (edge feed)
- Consider different substrate thickness
- Use matching network
Narrow bandwidth
- Increase substrate thickness
- Use lower εr substrate
- Consider stacked patch design
Low directivity
- Increase patch width
- Check ground plane size (should be > λ/2 beyond patch)
Complete Script
"""
Complete Patch Antenna Design Script
"""
from edgefem.designs import PatchAntennaDesign
import edgefem.plots as vp
import numpy as np
import matplotlib.pyplot as plt
# Design parameters
f_design = 2.4e9 # Target frequency
eps_r = 4.4 # FR-4
h = 1.6e-3 # 1.6mm substrate
# Create antenna
patch = PatchAntennaDesign(
patch_length=29e-3,
patch_width=38e-3,
substrate_height=h,
substrate_eps_r=eps_r,
substrate_tan_d=0.02,
)
print(f"Estimated resonance: {patch.estimated_resonant_frequency/1e9:.3f} GHz")
# Configure probe feed
patch.set_probe_feed(x_offset=0, y_offset=-5e-3)
# Generate mesh
patch.generate_mesh(density=15)
print(f"Mesh: {len(patch.mesh.elements)} elements")
# Frequency sweep
freqs, Z_in, S11 = patch.frequency_sweep(2.0e9, 2.8e9, n_points=81, verbose=True)
# Find resonance
rl_db = 20 * np.log10(np.abs(S11) + 1e-10)
f_res = freqs[np.argmin(rl_db)]
print(f"\nResonant frequency: {f_res/1e9:.3f} GHz")
# Bandwidth
f_lo, f_hi, bw = patch.bandwidth(vswr_threshold=2.0)
print(f"Bandwidth (VSWR<2): {bw:.1f}% ({(f_hi-f_lo)/1e6:.0f} MHz)")
# Pattern and directivity
pattern = patch.radiation_pattern(f_res)
D = patch.directivity(f_res)
print(f"Directivity: {10*np.log10(D):.1f} dBi")
# Plots
vp.plot_return_loss(freqs, S11, title="2.4 GHz Patch", save="patch_rl.png")
vp.plot_pattern_3d_cuts(pattern, title="Pattern Cuts", save="patch_cuts.png")
# Export
patch.export_touchstone("patch_2p4ghz", freqs, S11)
print("\nDesign complete!")
Next Steps
- Unit Cell Tutorial - Periodic structures
- Waveguide Tutorial - S-parameter analysis
- Designs API - Full class reference