An Intensive Tutorial on FMCW Radar Fundamentals

From the chirp equation to range–Doppler–angle maps, with derivations and runnable Python

How to use this tutorial

This tutorial builds a complete picture of a basic Frequency-Modulated Continuous-Wave (FMCW) radar, one layer at a time. Each part follows the same rhythm: physical intuition first, then the mathematics derived step by step, then Python code that simulates the effect so you can see the equation working. The code cells are written for a Jupyter notebook and only need numpy, scipy and matplotlib. Run them in order, because later cells reuse parameters and functions defined earlier.

The roadmap is:

  1. Radar basics and why FMCW
  2. The chirp signal
  3. Mixing and the intermediate-frequency (IF) signal
  4. Range measurement, resolution and maximum range
  5. Velocity measurement with chirp-to-chirp phase
  6. The range–Doppler map (2-D FFT)
  7. Angle of arrival and MIMO virtual arrays
  8. Windowing and sidelobes
  9. Detection with CFAR
  10. Noise, SNR and processing gain
  11. A complete design example
  12. Exercises with answers

Notation used throughout: cc is the speed of light (3×1083\times10^8 m/s), fcf_c the start (carrier) frequency, λ=c/fc\lambda = c/f_c the wavelength, BB the sweep bandwidth, TcT_c the chirp duration, S=B/TcS = B/T_c the chirp slope, FsF_s the ADC sample rate, NsN_s the samples per chirp, NcN_c the chirps per frame, and NrxN_{rx} the number of receive channels.

# Setup: imports, constants and plotting style used by every later cell
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import windows

c = 3e8                      # speed of light [m/s]
rng = np.random.default_rng(seed=7)

plt.rcParams.update({
    "figure.figsize": (9, 4),
    "figure.dpi": 110,
    "axes.grid": True,
    "grid.alpha": 0.3,
    "axes.spines.top": False,
    "axes.spines.right": False,
    "font.size": 10,
})

1. Radar basics and why FMCW

A radar transmits an electromagnetic wave and listens for echoes. Everything it measures comes from comparing the echo to what was sent:

1.1 The pulsed-radar problem

A simple pulsed radar measures τ\tau directly with a short pulse of width TpT_p. Its range resolution is

ΔRpulse=cTp2. \Delta R_{\text{pulse}} = \frac{c\,T_p}{2}.

For 15 cm resolution the pulse must be 1 ns long. A short pulse carries very little energy unless its peak power is enormous, and timing nanosecond events needs very fast electronics. That is expensive.

1.2 The FMCW solution

FMCW radar transmits continuously and sweeps its frequency linearly over a bandwidth BB. Resolution is set by the bandwidth, not the pulse length:

ΔR=c2B. \Delta R = \frac{c}{2B}.

So a 4 GHz sweep gives 3.75 cm resolution, while the radar runs at low peak power for the whole sweep. Better still, the receiver mixes the echo with the transmitted signal, turning a tiny time delay into a low frequency (kHz to a few MHz) that a cheap ADC can sample. That combination is why FMCW dominates automotive radar (76–81 GHz), industrial level sensing, gesture sensing (60 GHz) and drone altimeters.

1.3 The radar range equation

The received power from a target of radar cross-section (RCS) σ\sigma is

Pr=PtGtGrλ2σ(4π)3R4L, P_r = \frac{P_t\,G_t\,G_r\,\lambda^2\,\sigma}{(4\pi)^3\,R^4\,L},

where PtP_t is transmit power, GtG_t and GrG_r are antenna gains and LL lumps the system losses. The R4R^4 term is the key fact: doubling the range costs 12 dB of signal. Section 10 shows how FMCW processing gain wins that back.

1.4 Block diagram

A basic FMCW radar has five stages:

  1. Synthesizer generates the chirp (a linear frequency ramp).
  2. Transmit antenna radiates it.
  3. Receive antenna collects the delayed echo.
  4. Mixer multiplies the received signal with a copy of the transmitted one, producing the IF (beat) signal.
  5. Low-pass filter and ADC sample the IF signal for digital processing (FFTs, detection, tracking).
# Radar parameters used throughout this tutorial (a 77 GHz automotive-style radar)
fc  = 77e9          # start frequency [Hz]
B   = 1e9           # sweep bandwidth [Hz]  -> 15 cm resolution
Tc  = 40e-6         # chirp duration (ramp time) [s]
S   = B / Tc        # chirp slope [Hz/s]
Fs  = 10e6          # ADC sample rate [Hz] (complex / IQ sampling)
Ns  = 256           # samples per chirp
Nc  = 128           # chirps per frame
lam = c / fc        # wavelength [m]

print(f"Wavelength        λ   = {lam*1e3:.3f} mm")
print(f"Chirp slope       S   = {S/1e12:.1f} MHz/µs")
print(f"Range resolution  ΔR  = {c/(2*B)*100:.1f} cm")
print(f"Max range (IQ)    Rmax= {Fs*c/(2*S):.1f} m")
print(f"Max velocity      vmax= ±{lam/(4*Tc):.2f} m/s")
print(f"Velocity res.     Δv  = {lam/(2*Nc*Tc):.3f} m/s")
Wavelength        λ   = 3.896 mm
Chirp slope       S   = 25.0 MHz/µs
Range resolution  ΔR  = 15.0 cm
Max range (IQ)    Rmax= 60.0 m
Max velocity      vmax= ±24.35 m/s
Velocity res.     Δv  = 0.380 m/s

2. The chirp signal

2.1 Instantaneous frequency

During one chirp, 0≤t<Tc0 \le t < T_c, the transmitted frequency rises linearly:

fTX(t)=fc+St,S=BTc. f_{TX}(t) = f_c + S\,t, \qquad S = \frac{B}{T_c}.

2.2 Phase and the transmitted waveform

Phase is the integral of angular frequency:

ϕTX(t)=2π∫0tfTX(u)du=2π(fct+S2t2). \phi_{TX}(t) = 2\pi\int_0^t f_{TX}(u)\,du = 2\pi\left(f_c\,t + \frac{S}{2}t^2\right).

The transmitted signal, with amplitude ATA_T, is therefore

xTX(t)=ATcos(2πfct+πSt2). x_{TX}(t) = A_T\cos\!\left(2\pi f_c t + \pi S t^2\right).

The quadratic term πSt2\pi S t^2 is what makes it a chirp: the phase accelerates, so the frequency climbs.

2.3 Frames

Chirps are repeated NcN_c times to form a frame. Within a frame there are two time axes, and keeping them separate is the most useful mental model in FMCW processing:

Real systems add idle time between chirps, so the chirp repetition period TrepT_{rep} is slightly longer than the ramp. This tutorial takes Trep=TcT_{rep} = T_c for clarity; in real designs replace TcT_c with TrepT_{rep} in every velocity formula.

# Visualise a chirp: (a) frequency vs time for several chirps, (b) a slow toy chirp waveform
fig, ax = plt.subplots(1, 2, figsize=(11, 3.8))

t_frame = np.linspace(0, 4*Tc, 2000)
f_inst = fc + S * (t_frame % Tc)
ax[0].plot(t_frame*1e6, f_inst/1e9, lw=2)
ax[0].set(xlabel="Time [µs]", ylabel="Frequency [GHz]",
          title="Instantaneous frequency of a chirp sequence")

# Real 77 GHz chirps oscillate too fast to plot, so use a toy chirp: 1 → 20 Hz in 1 s
t_toy = np.linspace(0, 1, 4000)
f0, S_toy = 1.0, 19.0
ax[1].plot(t_toy, np.cos(2*np.pi*(f0*t_toy + 0.5*S_toy*t_toy**2)), lw=1)
ax[1].set(xlabel="Time [s]", ylabel="Amplitude",
          title="Toy chirp waveform: cos(2π(f₀t + St²/2))")
plt.tight_layout(); plt.show()

3. Mixing and the IF signal

This is the heart of FMCW. Read the derivation slowly; every later result follows from it.

3.1 The received signal

A target at range RR returns a delayed, attenuated copy of the chirp, with round-trip delay

τ=2Rc. \tau = \frac{2R}{c}.

xRX(t)=ARcos(2πfc(t−τ)+πS(t−τ)2). x_{RX}(t) = A_R\cos\!\left(2\pi f_c (t-\tau) + \pi S (t-\tau)^2\right).

3.2 The mixer

A mixer multiplies its two inputs. Using cosacosb=12[cos(a−b)+cos(a+b)]\cos a\cos b = \tfrac{1}{2}[\cos(a-b) + \cos(a+b)], the product contains a difference-frequency term and a sum term near 2fc2f_c. The low-pass filter removes the sum, leaving the IF signal:

xIF(t)=12ATARcos(ϕTX(t)−ϕRX(t)). x_{IF}(t) = \tfrac{1}{2}A_T A_R \cos\!\big(\phi_{TX}(t) - \phi_{RX}(t)\big).

3.3 Deriving the IF phase

Expand the phase difference term by term:

ϕTX(t)−ϕRX(t)=2πfct+πSt2−[2πfc(t−τ)+πS(t2−2tτ+τ2)] \phi_{TX}(t) - \phi_{RX}(t) = 2\pi f_c t + \pi S t^2 - \left[2\pi f_c (t-\tau) + \pi S (t^2 - 2t\tau + \tau^2)\right]

The 2πfct2\pi f_c t and πSt2\pi S t^2 terms cancel, leaving

ϕTX(t)−ϕRX(t)=2πfcτ+2πSτt−πSτ2. \phi_{TX}(t) - \phi_{RX}(t) = 2\pi f_c \tau + 2\pi S \tau\, t - \pi S \tau^2 .

So the IF signal is a pure tone:

xIF(t)=Acos(2πfbt+ϕ0), x_{IF}(t) = A\cos\!\left(2\pi f_b\, t + \phi_0\right),

with beat frequency and initial phase

fb=Sτ=2SRc,ϕ0=2πfcτ−πSτ2≈4πRλ. f_b = S\tau = \frac{2 S R}{c}, \qquad \phi_0 = 2\pi f_c \tau - \pi S \tau^2 \approx \frac{4\pi R}{\lambda}.

The term πSτ2\pi S\tau^2 is the residual video phase. For R=100R=100 m, S=25S = 25 MHz/µs it is about 0.035 rad, negligible for most purposes.

3.4 Two numbers, two jobs

These two results drive everything else:

  1. The IF frequency fbf_b is proportional to range. A frequency analysis (FFT) of one chirp gives a range profile.
  2. The IF phase ϕ0=4πR/λ\phi_0 = 4\pi R/\lambda is extremely sensitive. A change of just λ/2≈2\lambda/2 \approx 2 mm in range rotates the phase by a full 2π2\pi, which is the basis of velocity measurement.

3.5 Complex (IQ) IF signal

Most modern radar chips use an IQ (quadrature) mixer that produces the complex signal

xIF(t)=Aej(2πfbt+ϕ0). x_{IF}(t) = A\,e^{\,j(2\pi f_b t + \phi_0)}.

A complex tone has a single spectral line at +fb+f_b, rather than the pair ±fb\pm f_b a real cosine has, so the usable unambiguous bandwidth doubles. The simulations below use complex IF signals.

3.6 The sampled IF signal for a moving target

Allow the target to move, R(t)=R0+vtR(t) = R_0 + v\,t, and evaluate the IF signal at fast-time sample nn (t=n/Fst = n/F_s) of chirp mm. Dropping the small terms, the IF signal becomes the fundamental FMCW data model:

x[n,m]=Aexp{j2π(2SR0c⋅nFs+2vλmTc+2R0λ)}. x[n, m] = A\,\exp\!\left\{ j2\pi\left( \frac{2 S R_0}{c}\cdot\frac{n}{F_s} \;+\; \frac{2 v}{\lambda}\, m T_c \;+\; \frac{2R_0}{\lambda} \right) \right\}.

Read it as a 2-D complex sinusoid: its frequency along nn encodes range, and its frequency along mm encodes velocity. Sections 4 to 6 simply estimate those two frequencies.

def simulate_if(targets, n_rx=1, d_over_lam=0.5, noise_std=0.0, exact=True):
    """
    Simulate the complex (IQ) IF data cube x[n, m, k] for point targets.
      n: fast-time sample, m: chirp index, k: receive (virtual) channel.
    targets: list of dicts with keys R [m], v [m/s], theta [deg], amp.
    exact=True uses the full round-trip delay per sample (includes range
    migration and range-Doppler coupling); False uses the simplified model.
    """
    n = np.arange(Ns)[:, None, None]
    m = np.arange(Nc)[None, :, None]
    k = np.arange(n_rx)[None, None, :]
    t_fast = n / Fs
    cube = np.zeros((Ns, Nc, n_rx), dtype=complex)
    for tg in targets:
        R0, v = tg["R"], tg["v"]
        th = np.deg2rad(tg.get("theta", 0.0))
        amp = tg.get("amp", 1.0)
        if exact:
            t_abs = m*Tc + t_fast                       # absolute time
            tau = 2*(R0 + v*t_abs) / c                  # round-trip delay
            phase = 2*np.pi*(fc*tau + S*tau*t_fast - 0.5*S*tau**2)
        else:
            phase = 2*np.pi*(2*S*R0/c*t_fast + 2*v/lam*m*Tc + 2*R0/lam)
        phase = phase + 2*np.pi*d_over_lam*k*np.sin(th)  # spatial (angle) phase
        cube += amp*np.exp(1j*phase)
    noise = noise_std/np.sqrt(2)*(rng.standard_normal(cube.shape)
                                  + 1j*rng.standard_normal(cube.shape))
    return cube + noise

# One stationary target at 20 m: IF should be a tone at f_b = 2SR/c
R_demo = 20.0
fb_theory = 2*S*R_demo/c
x = simulate_if([{"R": R_demo, "v": 0.0}])[:, 0, 0]

t_us = np.arange(Ns)/Fs*1e6
plt.plot(t_us, x.real, label="I (real)")
plt.plot(t_us, x.imag, label="Q (imag)", alpha=0.7)
plt.xlabel("Fast time [µs]"); plt.ylabel("Amplitude")
plt.title(f"IF signal of a target at {R_demo} m  (theory f_b = {fb_theory/1e6:.3f} MHz)")
plt.legend(); plt.show()

4. Range measurement

4.1 Range from beat frequency

Inverting fb=2SR/cf_b = 2SR/c gives

R=cfb2S. R = \frac{c\,f_b}{2S}.

Several targets produce a sum of tones, one per target. Taking the FFT of the NsN_s samples of one chirp, the range FFT, gives a peak for each target. FFT bin kk corresponds to frequency kFs/Nsk F_s/N_s, so its range is

Rk=c2S⋅kFsNs. R_k = \frac{c}{2S}\cdot\frac{k\,F_s}{N_s}.

4.2 Range resolution

Two tones observed for a time TcT_c can be separated only if their frequencies differ by at least 1/Tc1/T_c (the Fourier resolution limit):

Δfb≥1Tc. \Delta f_b \ge \frac{1}{T_c}.

Converting to range with ΔR=cΔfb/(2S)\Delta R = c\,\Delta f_b / (2S) and S=B/TcS = B/T_c:

ΔR=c2S⋅1Tc=c2B. \Delta R = \frac{c}{2S}\cdot\frac{1}{T_c} = \frac{c}{2B}.

Range resolution depends only on the swept bandwidth. Strictly, BB here is the bandwidth swept while the ADC is sampling, Beff=SNs/FsB_{\text{eff}} = S\,N_s/F_s, which is often slightly less than the full ramp.

4.3 Maximum range

The largest beat frequency the receiver can capture is limited by the ADC (and the IF filter):

fb,max={Fscomplex (IQ) sampling,Fs/2real sampling,⇒Rmax=cfb,max2S. f_{b,\max} = \begin{cases} F_s & \text{complex (IQ) sampling},\\[2pt] F_s/2 & \text{real sampling}, \end{cases} \qquad\Rightarrow\qquad R_{\max} = \frac{c\,f_{b,\max}}{2S}.

For complex sampling, FsF_s covers [−Fs/2,Fs/2)[-F_s/2, F_s/2); with only positive (approaching-range) beat frequencies in practice, designs typically quote Rmax=Fsc/(2S)R_{\max} = F_s c/(2S) for IQ and Fsc/(4S)F_s c/(4S) for real ADCs. Note the trade-off: a steeper slope SS improves resolution at fixed TcT_c but shrinks RmaxR_{\max} for a given ADC.

4.4 Range accuracy versus resolution

Resolution is the ability to separate two targets. Accuracy is how precisely a single target’s range is measured, which can be much finer by interpolating between FFT bins or by using the phase. Do not confuse them.

# Range FFT with three targets, two of them closer than one resolution cell
dR = c/(2*B)
targets = [
    {"R": 12.0,       "v": 0, "amp": 1.0},
    {"R": 30.0,       "v": 0, "amp": 0.5},
    {"R": 30.0 + 0.6*dR, "v": 0, "amp": 0.5},   # only 0.6 ΔR apart -> unresolved
]
x = simulate_if(targets, noise_std=0.05)[:, 0, 0]

N_fft = 1024                                        # zero-padding for a smoother plot
X = np.fft.fft(x, n=N_fft) / Ns
f_axis = np.arange(N_fft) * Fs / N_fft
r_axis = c * f_axis / (2*S)

plt.plot(r_axis, 20*np.log10(np.abs(X) + 1e-12))
for tg in targets:
    plt.axvline(tg["R"], color="k", ls=":", lw=0.8)
plt.xlim(0, 60); plt.ylim(-60, 5)
plt.xlabel("Range [m]"); plt.ylabel("Magnitude [dB]")
plt.title(f"Range profile (ΔR = {dR*100:.0f} cm): the two targets near 30 m merge into one peak")
plt.show()

# Resolution depends only on bandwidth: same two targets (30 cm apart), different B
fig, ax = plt.subplots(figsize=(9, 4))
for B_try in [0.5e9, 1e9, 2e9]:
    S_try = B_try / Tc
    tt = np.arange(Ns)/Fs
    sig = sum(np.exp(1j*2*np.pi*2*S_try*R/c*tt) for R in [5.0, 5.3])
    spec = np.abs(np.fft.fft(sig, 4096))/Ns
    r = c*np.arange(4096)*Fs/4096/(2*S_try)
    ax.plot(r, spec, label=f"B = {B_try/1e9:.1f} GHz  →  ΔR = {c/(2*B_try)*100:.1f} cm")
ax.set_xlim(4, 6.5); ax.set_xlabel("Range [m]"); ax.set_ylabel("|X| (linear)")
ax.set_title("Two targets 30 cm apart: only wider sweeps resolve them")
ax.legend(); plt.show()

5. Velocity measurement

5.1 Why not just use the beat frequency?

A moving target shifts the beat frequency by the Doppler frequency fD=2v/λf_D = 2v/\lambda. At 77 GHz and 10 m/s, fD≈5f_D \approx 5 kHz, while one range bin spans 1/Tc=251/T_c = 25 kHz. The Doppler shift is far below the range resolution of a single chirp, so frequency alone cannot measure velocity. The phase can.

5.2 Phase difference between consecutive chirps

From Section 3, the IF phase of chirp mm is ϕm=4πRm/λ\phi_m = 4\pi R_m/\lambda. Between consecutive chirps the target moves vTcv T_c, so

Δϕ=4πvTcλ⇒v=λΔϕ4πTc. \Delta\phi = \frac{4\pi\, v\, T_c}{\lambda} \qquad\Longrightarrow\qquad v = \frac{\lambda\,\Delta\phi}{4\pi T_c}.

At 77 GHz, a target moving only 0.1 mm between chirps rotates the phase by about 18°: easily measurable.

5.3 Maximum unambiguous velocity

Phase is only known modulo 2π2\pi, so the measurement is unambiguous for |Δϕ|<π|\Delta\phi| < \pi:

4πvTcλ<π⇒vmax=λ4Tc. \frac{4\pi\, v\, T_c}{\lambda} < \pi \qquad\Longrightarrow\qquad v_{\max} = \frac{\lambda}{4\,T_c}.

Faster targets alias (wrap around) to a wrong velocity. Short chirps raise vmaxv_{\max}.

5.4 The Doppler FFT and velocity resolution

With NcN_c chirps, the phase rotates at a constant rate across the frame: the slow-time signal is itself a complex sinusoid with frequency fD=2v/λf_D = 2v/\lambda, sampled every TcT_c. An FFT across chirps (the Doppler FFT) resolves two velocities if their phase rates differ by at least 2π/Nc2\pi/N_c per chirp:

4πΔvTcλ≥2πNc⇒Δv=λ2NcTc=λ2Tf, \frac{4\pi\,\Delta v\, T_c}{\lambda} \ge \frac{2\pi}{N_c} \qquad\Longrightarrow\qquad \Delta v = \frac{\lambda}{2\,N_c\,T_c} = \frac{\lambda}{2\,T_f},

where Tf=NcTcT_f = N_c T_c is the frame (observation) time. Longer frames give finer velocity resolution, just as wider bandwidth gives finer range resolution.

5.5 Relation to the familiar Doppler formula

Equivalently, the Doppler FFT measures fD=2v/λf_D = 2v/\lambda with frequency resolution 1/Tf1/T_f, and Δv=λ/(2Tf)\Delta v = \lambda/(2T_f) follows directly from ΔfD=1/Tf\Delta f_D = 1/T_f.

# Phase across chirps for a single target: it rotates linearly at Δφ = 4π v Tc / λ
v_demo = 6.0
cube = simulate_if([{"R": 20.0, "v": v_demo}], exact=False)
k_peak = int(round(2*S*20.0/c / (Fs/Ns)))          # range bin of the target
range_fft = np.fft.fft(cube[:, :, 0], axis=0)
phase = np.unwrap(np.angle(range_fft[k_peak, :]))

dphi_theory = 4*np.pi*v_demo*Tc/lam
dphi_meas = np.mean(np.diff(phase))
print(f"Δφ theory   = {np.degrees(dphi_theory):.2f}° per chirp")
print(f"Δφ measured = {np.degrees(dphi_meas):.2f}° per chirp")
print(f"v estimate  = {lam*dphi_meas/(4*np.pi*Tc):.3f} m/s")

plt.plot(np.degrees(phase), ".-", ms=3)
plt.xlabel("Chirp index m"); plt.ylabel("Unwrapped phase [deg]")
plt.title("Phase at the target's range bin grows linearly with chirp index")
plt.show()
Δφ theory   = 44.35° per chirp
Δφ measured = 44.35° per chirp
v estimate  = 6.000 m/s

# Velocity aliasing: a target faster than v_max appears at the wrong velocity
vmax = lam/(4*Tc)
win = windows.hann(Nc)
fig, ax = plt.subplots(figsize=(9, 4))
for v_true in [10.0, 20.0, 30.0]:
    slow = np.exp(1j*4*np.pi*v_true*Tc/lam*np.arange(Nc))
    spec = np.fft.fftshift(np.fft.fft(slow*win))
    v_axis = np.fft.fftshift(np.fft.fftfreq(Nc, d=Tc)) * lam/2
    ax.plot(v_axis, 20*np.log10(np.abs(spec)/np.abs(spec).max() + 1e-9),
            label=f"true v = {v_true} m/s → peak at {v_axis[np.argmax(np.abs(spec))]:.1f} m/s")
ax.axvline(vmax, color="k", ls="--", lw=0.8); ax.axvline(-vmax, color="k", ls="--", lw=0.8)
ax.set_ylim(-60, 3); ax.set_xlabel("Velocity [m/s]"); ax.set_ylabel("dB")
ax.set_title(f"Doppler FFT, v_max = ±{vmax:.1f} m/s (dashed): 30 m/s wraps around")
ax.legend(fontsize=8); plt.show()

6. The range–Doppler map

6.1 The 2-D FFT

Arrange the samples of one frame in a matrix with fast time nn down the rows and slow time mm across the columns. Using the data model of Section 3.6, a 2-D DFT gives

X[k,l]=∑n=0Ns−1∑m=0Nc−1wr[n]wd[m]x[n,m]e−j2πknNse−j2πlmNc, X[k, l] = \sum_{n=0}^{N_s-1}\sum_{m=0}^{N_c-1} w_r[n]\,w_d[m]\; x[n,m]\; e^{-j2\pi \frac{kn}{N_s}}\, e^{-j2\pi \frac{lm}{N_c}},

where wrw_r and wdw_d are window functions (Section 8). Each target becomes a peak at range bin kk and Doppler bin ll:

R=c2S⋅kFsNs,v=λ2Tc⋅lNc,l∈[−Nc2,Nc2). R = \frac{c}{2S}\cdot\frac{k F_s}{N_s}, \qquad v = \frac{\lambda}{2 T_c}\cdot\frac{l}{N_c}, \quad l \in \left[-\tfrac{N_c}{2}, \tfrac{N_c}{2}\right).

In practice this is done as NcN_c range FFTs (one per chirp, often computed on the fly as chirps arrive) followed by NsN_s Doppler FFTs (one per range bin).

6.2 Range–Doppler coupling and range migration

The simplified model ignores two effects that the exact simulation includes:

A useful design check is therefore vmaxTf<ΔRv_{\max} T_f < \Delta R.

# Full range–Doppler map with four targets (exact signal model + noise)
targets = [
    {"R": 10.0, "v":  0.0, "amp": 1.0},    # stationary
    {"R": 18.0, "v":  5.0, "amp": 0.6},    # receding
    {"R": 25.0, "v": -8.0, "amp": 0.4},    # approaching
    {"R": 32.0, "v":  3.0, "amp": 0.3},
]
cube = simulate_if(targets, noise_std=0.3)[:, :, 0]

def range_doppler(data):
    w_r = windows.hann(Ns)[:, None]
    w_d = windows.hann(Nc)[None, :]
    rfft = np.fft.fft(data*w_r, axis=0)                       # range FFT
    rd = np.fft.fftshift(np.fft.fft(rfft*w_d, axis=1), axes=1)  # Doppler FFT
    return rd

rd = range_doppler(cube)
rd_db = 20*np.log10(np.abs(rd) + 1e-12)
rd_db -= rd_db.max()

range_axis = c*np.arange(Ns)*Fs/Ns/(2*S)
vel_axis = np.fft.fftshift(np.fft.fftfreq(Nc, d=Tc))*lam/2

plt.figure(figsize=(9, 5))
plt.imshow(rd_db, aspect="auto", origin="lower", cmap="viridis", vmin=-60, vmax=0,
           extent=[vel_axis[0], vel_axis[-1], range_axis[0], range_axis[-1]])
plt.colorbar(label="dB")
for tg in targets:
    plt.plot(tg["v"], tg["R"], "r+", ms=12, mew=1.5)
plt.ylim(0, 45); plt.xlabel("Velocity [m/s]"); plt.ylabel("Range [m]")
plt.title("Range–Doppler map (red + = true target positions)")
plt.show()

7. Angle of arrival

7.1 Phase across receive antennas

Place NrxN_{rx} receive antennas in a line with spacing dd. A plane wave arriving from angle θ\theta (measured from broadside) travels an extra distance dsinθd\sin\theta to each successive antenna, giving a phase step

ω=2πdsinθλ⇒θ=sin−1(λω2πd). \omega = \frac{2\pi\, d \sin\theta}{\lambda} \qquad\Longrightarrow\qquad \theta = \sin^{-1}\!\left(\frac{\lambda\,\omega}{2\pi d}\right).

The signal across antennas, xk=Aejkωx_k = A\,e^{jk\omega}, is again a complex sinusoid, so a third FFT, the angle FFT, across the antenna dimension estimates ω\omega.

7.2 Field of view

Unambiguous measurement requires |ω|<π|\omega| < \pi:

θmax=sin−1(λ2d). \theta_{\max} = \sin^{-1}\!\left(\frac{\lambda}{2d}\right).

With d=λ/2d = \lambda/2 the field of view is the full ±90∘\pm 90^\circ. Larger spacing creates grating lobes (angle aliasing).

7.3 Angular resolution

An NrxN_{rx}-point FFT resolves phase steps that differ by 2π/Nrx2\pi/N_{rx}. Differentiating ω\omega with respect to θ\theta, Δω≈(2πdcosθ/λ)Δθ\Delta\omega \approx (2\pi d\cos\theta/\lambda)\,\Delta\theta, gives

Δθ=λNrxdcosθ→d=λ/2,θ=0Δθ=2Nrxrad. \Delta\theta = \frac{\lambda}{N_{rx}\, d \cos\theta} \quad\xrightarrow{\;d=\lambda/2,\ \theta=0\;}\quad \Delta\theta = \frac{2}{N_{rx}}\ \text{rad}.

Resolution is best at broadside and degrades toward the edges as 1/cosθ1/\cos\theta. Eight elements give about 14∘14^\circ at boresight.

7.4 MIMO and the virtual array

Adding receivers is costly. MIMO radar uses NtxN_{tx} transmitters and NrxN_{rx} receivers, with the transmitters separated in time (time-division multiplexing, TDM) or by code. If transmitter ii sits at position 𝐩i\mathbf{p}_i and receiver kk at 𝐪k\mathbf{q}_k, the round-trip phase depends on 𝐩i+𝐪k\mathbf{p}_i + \mathbf{q}_k, so the pair behaves like a single virtual element at that sum. With receivers at {0,d,2d,3d}\{0, d, 2d, 3d\} and two transmitters spaced 4d4d apart:

{0,d,2d,3d}∪{4d,5d,6d,7d}=8-element virtual array. \{0, d, 2d, 3d\} \;\cup\; \{4d, 5d, 6d, 7d\} \;=\; \text{8-element virtual array}.

So NtxNrxN_{tx} N_{rx} virtual channels come from only Ntx+NrxN_{tx}+N_{rx} physical antennas. One caveat with TDM: a moving target’s phase advances between transmitter slots, adding a velocity-dependent error to the angle that must be compensated using the Doppler estimate.

7.5 The radar data cube

Combining all three dimensions, the full data model is

x[n,m,k]=Aexp{j2π(2SRcnFs+2vλmTc+dsinθλk)}, x[n, m, k] = A\,\exp\!\left\{j2\pi\left(\frac{2SR}{c}\frac{n}{F_s} + \frac{2v}{\lambda} m T_c + \frac{d\sin\theta}{\lambda} k\right)\right\},

and a 3-D FFT (range, Doppler, angle) maps each target to a point in range–velocity–angle space.

# Angle FFT on an 8-element virtual array (2 TX × 4 RX), two targets at the same range and speed
N_virt = 8
targets = [
    {"R": 15.0, "v": 2.0, "theta": -20.0, "amp": 1.0},
    {"R": 15.0, "v": 2.0, "theta":  15.0, "amp": 0.7},
]
cube = simulate_if(targets, n_rx=N_virt, noise_std=0.3)

# Range–Doppler per channel, then pick the strongest cell
rd_all = np.stack([range_doppler(cube[:, :, ch]) for ch in range(N_virt)], axis=-1)
power = np.sum(np.abs(rd_all)**2, axis=-1)
k_r, k_d = np.unravel_index(np.argmax(power), power.shape)
snapshot = rd_all[k_r, k_d, :]                  # 8 complex values across the array

N_ang = 256
ang_spec = np.fft.fftshift(np.fft.fft(snapshot*windows.taylor(N_virt, nbar=3, sll=25), N_ang))
omega = np.fft.fftshift(np.fft.fftfreq(N_ang))*2*np.pi
theta_axis = np.degrees(np.arcsin(omega/np.pi))   # d = λ/2

spec_db = 20*np.log10(np.abs(ang_spec)/np.abs(ang_spec).max())
plt.plot(theta_axis, spec_db)
for tg in targets:
    plt.axvline(tg["theta"], color="k", ls=":", lw=0.8)
plt.ylim(-40, 2); plt.xlabel("Angle θ [deg]"); plt.ylabel("dB")
plt.title(f"Angle spectrum at R={range_axis[k_r]:.1f} m, 8 virtual channels "
          f"(Δθ ≈ {np.degrees(2/N_virt):.0f}°)")
plt.show()

# Range–angle map (birds-eye view): sum over Doppler after the angle FFT
targets = [
    {"R": 10.0, "v": 1.0, "theta": -30.0, "amp": 1.0},
    {"R": 20.0, "v": -3.0, "theta":  0.0, "amp": 0.8},
    {"R": 28.0, "v": 4.0, "theta":  25.0, "amp": 0.7},
]
cube = simulate_if(targets, n_rx=N_virt, noise_std=0.3)
rd_all = np.stack([range_doppler(cube[:, :, ch]) for ch in range(N_virt)], axis=-1)
ra = np.fft.fftshift(np.fft.fft(rd_all*windows.hann(N_virt), n=64, axis=2), axes=2)
ra_map = np.sum(np.abs(ra)**2, axis=1)            # non-coherent sum over Doppler
ra_db = 10*np.log10(ra_map/ra_map.max())

omega = np.fft.fftshift(np.fft.fftfreq(64))*2*np.pi
th = np.arcsin(omega/np.pi)
Rg, Th = np.meshgrid(range_axis, th, indexing="ij")
X, Y = Rg*np.sin(Th), Rg*np.cos(Th)

plt.figure(figsize=(7, 6))
plt.pcolormesh(X, Y, ra_db, cmap="viridis", vmin=-35, vmax=0, shading="gouraud")
plt.colorbar(label="dB")
for tg in targets:
    t = np.deg2rad(tg["theta"])
    plt.plot(tg["R"]*np.sin(t), tg["R"]*np.cos(t), "r+", ms=12, mew=1.5)
plt.gca().set_facecolor(plt.cm.viridis(0.0))
plt.xlim(-25, 25); plt.ylim(0, 40); plt.gca().set_aspect("equal")
plt.xlabel("Cross-range x [m]"); plt.ylabel("Down-range y [m]")
plt.title("Range–angle map in Cartesian coordinates")
plt.show()

8. Windowing and sidelobes

8.1 Spectral leakage

An FFT of NN samples implicitly multiplies the signal by a rectangular window. The transform of a rectangle is the Dirichlet (periodic sinc) kernel

W(f)=sin(πfNTs)Nsin(πfTs)e−jπf(N−1)Ts, W(f) = \frac{\sin(\pi f N T_s)}{N\sin(\pi f T_s)}\,e^{-j\pi f (N-1) T_s},

whose first sidelobe is only −13.3-13.3 dB below the peak. A strong target’s sidelobes can therefore hide a weak target nearby, which is a serious problem when a truck and a pedestrian share a scene.

8.2 Window trade-offs

Tapering the data with a window w[n]w[n] lowers sidelobes at the cost of a wider main lobe (coarser effective resolution) and a small SNR loss:

Window Peak sidelobe Main-lobe width (−3 dB, bins) SNR loss
Rectangular −13.3 dB 0.89 0 dB
Hann −31.5 dB 1.44 1.76 dB
Hamming −42.7 dB 1.30 1.34 dB
Blackman −58.1 dB 1.68 2.37 dB
Chebyshev (−80 dB) −80 dB ~1.9 ~2.5 dB

The Hann window is a common default for range and Doppler; Chebyshev or Taylor windows are used when a specific sidelobe level is required.

# Strong target + weak target 3 bins away (40 dB weaker): rectangular vs Hann vs Blackman
tt = np.arange(Ns)/Fs
bin_hz = Fs/Ns
sig = np.exp(1j*2*np.pi*40.3*bin_hz*tt) + 0.01*np.exp(1j*2*np.pi*46.3*bin_hz*tt)

fig, ax = plt.subplots(figsize=(9, 4))
for name, w in [("Rectangular", np.ones(Ns)), ("Hann", windows.hann(Ns)),
                ("Blackman", windows.blackman(Ns))]:
    spec = np.abs(np.fft.fft(sig*w, 4096))
    ax.plot(np.arange(4096)*Ns/4096, 20*np.log10(spec/spec.max() + 1e-12), label=name)
ax.axvline(46.3, color="k", ls=":", lw=0.8)
ax.set_xlim(25, 60); ax.set_ylim(-90, 3)
ax.set_xlabel("FFT bin"); ax.set_ylabel("dB")
ax.set_title("A target 40 dB weaker (dotted line) is hidden by rectangular-window sidelobes")
ax.legend(); plt.show()

9. Detection with CFAR

9.1 Why a fixed threshold fails

After the FFTs, each cell holds signal plus noise and clutter. A fixed threshold either misses weak targets or floods the output with false alarms when the noise floor changes (rain, ground clutter, temperature drift). Constant False Alarm Rate (CFAR) detection estimates the local noise level around each cell and sets the threshold relative to it.

9.2 Cell-Averaging CFAR

For each cell under test (CUT), average the power of NN training cells on either side, skipping a few guard cells next to the CUT so target energy does not leak into the estimate:

P̂n=1N∑i=1N|Xi|2,detect if |XCUT|2>αP̂n. \hat{P}_n = \frac{1}{N}\sum_{i=1}^{N} |X_i|^2, \qquad \text{detect if } |X_{\text{CUT}}|^2 > \alpha\,\hat{P}_n .

For exponentially distributed noise power (complex Gaussian noise), the threshold factor that gives a desired false-alarm probability PfaP_{fa} is

α=N(Pfa−1/N−1). \alpha = N\left(P_{fa}^{-1/N} - 1\right).

For N=16N = 16 and Pfa=10−4P_{fa} = 10^{-4}, α≈12.5\alpha \approx 12.5 (11 dB). Variants such as OS-CFAR (ordered statistic) handle closely spaced targets and clutter edges better.

def ca_cfar_1d(power, n_train=12, n_guard=3, pfa=1e-4):
    """Cell-averaging CFAR on a 1-D power profile. Returns threshold and detection mask."""
    N = 2*n_train
    alpha = N*(pfa**(-1/N) - 1)
    L = len(power)
    thr = np.full(L, np.nan)
    for i in range(n_train + n_guard, L - n_train - n_guard):
        lead = power[i - n_guard - n_train : i - n_guard]
        lag  = power[i + n_guard + 1 : i + n_guard + 1 + n_train]
        thr[i] = alpha*np.mean(np.concatenate([lead, lag]))
    return thr, power > thr

# Range profile with a noise floor that rises with range (e.g. clutter), plus 4 targets
targets = [{"R": R, "v": 0, "amp": a} for R, a in [(8, 0.2), (15, 0.08), (27, 0.12), (40, 0.25)]]
x = simulate_if(targets, noise_std=0.15)[:, 0, 0]
x = x + np.linspace(0, 0.25, Ns)*(rng.standard_normal(Ns) + 1j*rng.standard_normal(Ns))
P = np.abs(np.fft.fft(x*windows.hann(Ns)))**2

thr, det = ca_cfar_1d(P)
plt.plot(range_axis, 10*np.log10(P), label="Range profile")
plt.plot(range_axis, 10*np.log10(thr), label="CA-CFAR threshold")
plt.plot(range_axis[det], 10*np.log10(P[det]), "rv", label="Detections")
plt.xlim(0, 50); plt.xlabel("Range [m]"); plt.ylabel("Power [dB]")
plt.title("CA-CFAR adapts its threshold to the rising noise floor")
plt.legend(); plt.show()
print("Detected ranges [m]:", np.round(range_axis[det], 2))

Detected ranges [m]: [ 7.73  7.97  8.2  15.   15.23 26.72 26.95 27.19 39.61 39.84 40.08]

Notice that each target triggers two or three adjacent cells, because the Hann window widens the main lobe. Real pipelines follow CFAR with peak grouping (keep only local maxima) or clustering, so that each physical target produces one detection.

10. Noise, SNR and processing gain

10.1 Thermal noise

Receiver noise power in bandwidth BnB_n is

Pn=kBT0BnF, P_n = k_B\,T_0\,B_n\,F,

where kB=1.38×10−23k_B = 1.38\times10^{-23} J/K, T0=290T_0 = 290 K and FF is the receiver noise figure. At room temperature kBT0≈−174k_B T_0 \approx -174 dBm/Hz.

10.2 Coherent integration gain

An NN-point FFT adds a sinusoid’s samples coherently (amplitude grows as NN, power as N2N^2) while noise adds incoherently (power grows as NN). The SNR gain is therefore NN, or 10log10N10\log_{10}N dB. Across range, Doppler and angle FFTs:

Gproc=10log10(NsNcNrx)dB. G_{\text{proc}} = 10\log_{10}\!\left(N_s\,N_c\,N_{rx}\right)\ \text{dB}.

For 256×128×8256 \times 128 \times 8 samples that is 54.2 dB. Equivalently, the effective noise bandwidth after processing is 1/Tf1/T_f per channel, so the SNR for one target is

SNR=PtGtGrλ2σNrxTf(4π)3R4kBT0FL. \text{SNR} = \frac{P_t\,G_t\,G_r\,\lambda^2\,\sigma\; N_{rx}\,T_f}{(4\pi)^3\,R^4\,k_B\,T_0\,F\,L}.

Solving for RR with a required detection SNR gives the maximum detection range:

Rdet=[PtGtGrλ2σNrxTf(4π)3kBT0FLSNRmin]1/4. R_{\text{det}} = \left[\frac{P_t\,G_t\,G_r\,\lambda^2\,\sigma\,N_{rx}\,T_f}{(4\pi)^3\,k_B\,T_0\,F\,L\;\text{SNR}_{\min}}\right]^{1/4}.

# Verify the coherent integration gain numerically for the range FFT
N = Ns
snr_in_db = -10
amp = 1.0
sigma = amp/np.sqrt(10**(snr_in_db/10))           # noise std for the chosen input SNR
trials = 200
gains = []
for _ in range(trials):
    tone = amp*np.exp(1j*2*np.pi*40*np.arange(N)/N)
    noise = sigma/np.sqrt(2)*(rng.standard_normal(N) + 1j*rng.standard_normal(N))
    Xs, Xn = np.fft.fft(tone), np.fft.fft(noise)
    gains.append(np.abs(Xs[40])**2 / np.mean(np.abs(Xn)**2))
snr_out = 10*np.log10(np.mean(gains))
print(f"Input SNR  = {snr_in_db} dB per sample")
print(f"Output SNR = {snr_out:.2f} dB  →  gain {snr_out - snr_in_db:.2f} dB "
      f"(theory 10·log10({N}) = {10*np.log10(N):.2f} dB)")

# Detection range from the radar equation
def detection_range(Pt_dbm=12, Gt_db=10, Gr_db=10, rcs=10, NF_db=12, L_db=6, snr_min_db=15,
                    n_rx=8, Tf=Nc*Tc, f=fc):
    kT = 1.38e-23*290
    lin = lambda db: 10**(db/10)
    Pt = lin(Pt_dbm)*1e-3
    lam_ = c/f
    num = Pt*lin(Gt_db)*lin(Gr_db)*lam_**2*rcs*n_rx*Tf
    den = (4*np.pi)**3*kT*lin(NF_db)*lin(L_db)*lin(snr_min_db)
    return (num/den)**0.25

for name, rcs in [("Car (10 m²)", 10), ("Pedestrian (0.5 m²)", 0.5), ("Bicycle (2 m²)", 2)]:
    print(f"{name:22s} detection range ≈ {detection_range(rcs=rcs):6.1f} m")
Input SNR  = -10 dB per sample
Output SNR = 14.09 dB  →  gain 24.09 dB (theory 10·log10(256) = 24.08 dB)
Car (10 m²)            detection range ≈  157.9 m
Pedestrian (0.5 m²)    detection range ≈   74.7 m
Bicycle (2 m²)         detection range ≈  105.6 m

11. A complete design example

11.1 Requirements

Design the chirp for a short-range automotive radar:

11.2 Step-by-step design

Step 1, bandwidth from resolution. B≥c2ΔR=3×1082×0.1=1.5GHz. B \ge \frac{c}{2\Delta R} = \frac{3\times10^8}{2\times0.1} = 1.5\ \text{GHz}.

Step 2, chirp time from maximum velocity. With λ=3.896\lambda = 3.896 mm, Tc≤λ4vmax=3.896×10−3100=39μs. T_c \le \frac{\lambda}{4 v_{\max}} = \frac{3.896\times10^{-3}}{100} = 39\ \mu\text{s}. Choose Tc=38μT_c = 38\ \mus (this is the repetition period, including idle time).

Step 3, slope and ADC rate from maximum range. If roughly 30 µs of each chirp is sampled, the slope is S=1.5GHz/30μs=50S = 1.5\ \text{GHz}/30\ \mu\text{s} = 50 MHz/µs. The beat frequency at 50 m is fb,max=2SRmaxc=2×50×1012×503×108=16.7MHz, f_{b,\max} = \frac{2 S R_{\max}}{c} = \frac{2 \times 50\times10^{12}\times50}{3\times10^8} = 16.7\ \text{MHz}, so an IQ ADC at Fs=20F_s = 20 MSps works. Samples per chirp: Ns=Fs×30μs=600N_s = F_s \times 30\ \mu\text{s} = 600, rounded up to a 1024-point FFT with zero padding (or choose 512 samples and a slightly shorter sampling window).

Step 4, number of chirps from velocity resolution. Nc≥λ2ΔvTc=3.896×10−32×0.2×38×10−6=256.3⇒Nc=256 (or 512). N_c \ge \frac{\lambda}{2\,\Delta v\, T_c} = \frac{3.896\times10^{-3}}{2\times0.2\times38\times10^{-6}} = 256.3 \;\Rightarrow\; N_c = 256 \text{ (or 512)}. With Nc=256N_c = 256 the resolution is 0.200 m/s, right at the limit; choose 512 if margin is needed.

Step 5, sanity checks. The frame time is Tf=256×38μs=9.7T_f = 256 \times 38\ \mu\text{s} = 9.7 ms. Range migration at vmaxv_{\max}: 25×9.7×10−3=0.2425 \times 9.7\times10^{-3} = 0.24 m, which spans about 2.4 range bins, so a fast target will smear. Options are to accept it, to shorten the frame, or to apply range-migration compensation (for example keystone processing).

The calculator below automates these trade-offs.

def fmcw_design(fc, B, Tc, Fs, Ns, Nc, n_rx=1, iq=True):
    """Return the key performance numbers for an FMCW chirp configuration."""
    lam_ = c/fc
    t_samp = Ns/Fs
    S_ = B/t_samp                         # slope over the sampled portion
    out = {
        "Wavelength [mm]":          lam_*1e3,
        "Slope [MHz/µs]":           S_/1e12,
        "Range resolution [cm]":    c/(2*B)*100,
        "Max range [m]":            (Fs if iq else Fs/2)*c/(2*S_),
        "Max velocity [m/s]":       lam_/(4*Tc),
        "Velocity resolution [m/s]": lam_/(2*Nc*Tc),
        "Frame time [ms]":          Nc*Tc*1e3,
        "Range migration at vmax [bins]": (lam_/(4*Tc))*Nc*Tc/(c/(2*B)),
        "Angular resolution [deg]": np.degrees(2/n_rx) if n_rx > 1 else np.nan,
        "Processing gain [dB]":     10*np.log10(Ns*Nc*n_rx),
    }
    return out

design = fmcw_design(fc=77e9, B=1.5e9, Tc=38e-6, Fs=20e6, Ns=600, Nc=256, n_rx=8)
for k_, v_ in design.items():
    print(f"{k_:34s} {v_:10.3f}")
Wavelength [mm]                         3.896
Slope [MHz/µs]                         50.000
Range resolution [cm]                  10.000
Max range [m]                          60.000
Max velocity [m/s]                     25.632
Velocity resolution [m/s]               0.200
Frame time [ms]                         9.728
Range migration at vmax [bins]          2.494
Angular resolution [deg]               14.324
Processing gain [dB]                   60.895

12. Exercises

Try these before reading the answers. Use c=3×108c = 3\times10^8 m/s.

  1. A radar sweeps 4 GHz in 50 µs. What is its range resolution and slope?
  2. With the radar in Q1 and an IQ ADC at 12.5 MSps, what is the maximum range?
  3. A target at 10 m is measured with the radar in Q1. What is the beat frequency?
  4. At 60 GHz with Tc=100T_c = 100 µs, what is the maximum unambiguous velocity?
  5. The same 60 GHz radar uses 64 chirps per frame. What is the velocity resolution?
  6. Between two chirps 40 µs apart, the phase at a 77 GHz target’s range bin changes by 30°. What is the target’s velocity?
  7. A 3 TX × 4 RX MIMO radar has λ/2\lambda/2 virtual spacing. What is its angular resolution at boresight and at 45°?
  8. Why does doubling the transmit power only increase detection range by about 19%?
  9. Modify the range–Doppler simulation so that one target moves at 30 m/s. Where does it appear, and why?
  10. In the CFAR code, set n_guard=0 and place two targets 2 bins apart. What happens to the weaker one?

Answers

  1. ΔR=c/2B=3.75\Delta R = c/2B = 3.75 cm. S=4GHz/50μs=80S = 4\ \text{GHz}/50\ \mu\text{s} = 80 MHz/µs.
  2. Rmax=Fsc/(2S)=12.5×106×3×108/(2×80×1012)=23.4R_{\max} = F_s c/(2S) = 12.5\times10^6 \times 3\times10^8 / (2\times80\times10^{12}) = 23.4 m.
  3. fb=2SR/c=2×80×1012×10/3×108=5.33f_b = 2SR/c = 2\times80\times10^{12}\times10/3\times10^8 = 5.33 MHz.
  4. λ=5\lambda = 5 mm, so vmax=λ/4Tc=5×10−3/400×10−6=12.5v_{\max} = \lambda/4T_c = 5\times10^{-3}/400\times10^{-6} = 12.5 m/s.
  5. Δv=λ/(2NcTc)=5×10−3/(2×64×100×10−6)=0.39\Delta v = \lambda/(2 N_c T_c) = 5\times10^{-3}/(2\times64\times100\times10^{-6}) = 0.39 m/s.
  6. v=λΔϕ/(4πTc)v = \lambda\Delta\phi/(4\pi T_c) with λ=3.896\lambda = 3.896 mm and Δϕ=π/6\Delta\phi = \pi/6: v=3.896×10−3×(π/6)/(4π×40×10−6)=4.06v = 3.896\times10^{-3}\times(\pi/6)/(4\pi\times40\times10^{-6}) = 4.06 m/s.
  7. 12 virtual elements: Δθ=2/12=0.167\Delta\theta = 2/12 = 0.167 rad =9.5∘= 9.5^\circ at boresight; 9.5∘/cos45∘=13.5∘9.5^\circ/\cos45^\circ = 13.5^\circ at 45°.
  8. Detection range scales as Pt1/4P_t^{1/4}, and 21/4=1.192^{1/4} = 1.19.
  9. vmax=24.35v_{\max} = 24.35 m/s, so 30 m/s wraps to 30−2×24.35=−18.730 - 2\times24.35 = -18.7 m/s. It appears as an approaching target. Remedies include shorter chirps, staggered chirp periods, or tracking.
  10. Without guard cells, the strong target’s main lobe falls in the training window of the weak target’s CUT, inflating the noise estimate so the weaker target may be missed (target masking). Guard cells or OS-CFAR fix this.

Summary of key equations

Quantity Equation Depends on
Chirp slope S=B/TcS = B/T_c Bandwidth, chirp time
Beat frequency fb=2SR/cf_b = 2SR/c Slope, range
IF phase ϕ0=4πR/λ\phi_0 = 4\pi R/\lambda Range (sub-mm sensitivity)
Range resolution ΔR=c/(2B)\Delta R = c/(2B) Bandwidth only
Maximum range (IQ) Rmax=Fsc/(2S)R_{\max} = F_s c/(2S) ADC rate, slope
Velocity v=λΔϕ/(4πTc)v = \lambda\Delta\phi/(4\pi T_c) Chirp-to-chirp phase
Maximum velocity vmax=λ/(4Tc)v_{\max} = \lambda/(4T_c) Chirp period
Velocity resolution Δv=λ/(2NcTc)\Delta v = \lambda/(2N_cT_c) Frame time
Angle θ=sin−1(λω/2πd)\theta = \sin^{-1}(\lambda\omega/2\pi d) Inter-antenna phase
Field of view θmax=sin−1(λ/2d)\theta_{\max} = \sin^{-1}(\lambda/2d) Antenna spacing
Angular resolution Δθ=λ/(Ndcosθ)\Delta\theta = \lambda/(N d\cos\theta) Number of (virtual) antennas
Processing gain 10log10(NsNcNrx)10\log_{10}(N_sN_cN_{rx}) Total samples per frame

Further reading