FMCW Radar 201 - Maths
This section will cover a bottom up approach to derive some key equations used throughout FMCW radar.
You can open this workbook in Google Colab to experiment with it, by clicking on:
Transmitting a chirp
A chirp is defined by a linearly increasing frequency.
Where:
\(f_{0}\) is the frequency at the begining of the chirp
s is the slope of frequency increase over time during the chirp
t is the time referenced to the beginning of the chirp.
It is beyond the scope of this writeup to demonstrate how phase and instantaneous frequency are related or the conditions of validty of the relations. We will just consider proven and valid that:
From which we can derive, that the phase of the transmitted FMCW radar is:
Definitions
\(\Phi\) is the full phase (including time or distance variables)
\(\phi\), \(\phi_0\), \(\phi_1\) are the phase offsets
\(\psi\) are the number of cycles (i.e. \(\frac{\phi}{2 \pi}\) )
So the full transmitted signal is
Receiving a chirp
The received signal is the transmitted signal being delayed by a constant time $ :nbsphinx-math:`Delta `$, which translates in phase of the RX signal is time delayed version of TX:
Where:
d being the distance to the scatterer (so travelled twice by the radar wave)
\(\Delta\) is the time of flight (i.e. \(\frac{2 \cdot d}{c}\))
So the full received signal is:
Mixer RX and TX leading to IF
The mixer multiplies RX and TX signals
replacing $ Y_{TX}(t) $ and $ Y_{RX}(t) $ by their respective equations :
We are now going to leverage Trigonometric relations to establish how the IF signal is actually how relatively lower frequency which is why it can be sampled by ADC converters.
Trigonometric relations
Trigonometric relations establish relationship between the product of 2 cosines, with the sum of 2 related cosines in the following relation:
which in our case translates into:
The following cells will leverage simpy to prove those equalities, but one can already see that the IF signal is the sum of a low frequency content \(\Phi_T-\Phi_R\) and high-frequency content \(\Phi_T+\Phi_R\). The high-frequency content is implicitely removed in many textbooks. In reality a low pass filter will ensure no aliasing from the R+T signal gets into the IF.
The output of the low pass filter after the MIXER is called the Intermediate Frequency, often abreviated IF signal
Eq. 3
Which defining:
\(\Psi_0 = - \Delta f_{0} + \frac{\Delta f_{c}}{2}\)
\(f_c = \Delta \cdot s\)
Becomes
Changes in distance
Assuming that there is a small change in the distance $ d $, it yields a small change in the time of flight $ \delta`$which from $:nbsphinx-math:Delta $ becomes :math:Delta + delta`
Then the new number of cycle \(\Psi_1\) is
(Eq. 5)
So the change in number of cycles between the two signals becomes
From this point on, we make assumptions about the relative frequency of the carrier and the IF.
If we notice that \(f_0 \approx 6 \cdot 10^{10} Hz\) and since the IF is below 100 MHz (just to simplify) \(f_c \lt 10^8 Hz\) we can neglect the second term. It is beyond the scope of this write-up to explore the boundary conditions of validity of this simplification
Implications for velocity or AoA
Illustration to come
Given that
or
for range the change of time of flight is proportional to twice the change of distance (\(\Delta = \frac{2 \cdot D}{c}\))
for AoA the change of time of flight is proportional to the change of distance (\(\Delta = \frac{D}{c}\))
Since
for range the number of cycles is then
or in phase shift (\(\Phi=2 \cdot \pi \cdot \Psi\))
which leads for speed measurement:
Angle of Arrival (AoA)
For AoA, the number of cycle count changes by half:
or in phase shift
with x, the distance between two antennas and theta the angle from boresight
or
Discrete Fourier Transform (DFT) to the rescue
Since the frequency of the intermediate frequency (IF signal) is related to distance, measuring the frequency of the IF signal is a straightforward way to estimate the distance to the scatterer.
Given X the Discrete Fourier Transform (DFT) of \(Y_{IF}\)
The tone is found by looking for the peak of the magnitude of the DFT by
For speed (doppler) and angle of arrival (AoA), it is important to note that the DFT transform has a closed form:
Side reminder: another way to look at it is to remember that the Fourier transform or a time delayed signal is the frequency shifted Fourier transform as follows:
so if the phase delay is to change over time (for range) or over angles (for AoA), the second FFT would yield either the speed or angles (to be developped).
Second DFT
Now if we assume that the phase of peak \(k^{th}\) changes over time (say we sampled at regular time \(Y_IF\) and the peak is always at the same location \(k_p\) so that
or in discrete time with T the time interval between each chirps
Then the DFT of X will have a peak at the closest position to f1
with
then
Third and fourth DFT
By similarities if the phase of the $ m_p $ value of Z was to change across sampling we could run a third DFT to identify the slope of the variation.
Most frequent usage of the third DFT is in the following flow:
Range DFT (often a FFT)
Dopple DFT (ibid)
AoA DFT (often azimuth)
At which point depending on the antenna pattern a 4th DFT could be done for elevation (if the array is dense)
If the array is not dense a different cube might be generate
At this point it becomes useful to introduce the concept of radar cube though a misnomer as radar tensor or radar hypercube would be more appropriate since there are 4 dimensions in the data:
range
doppler
azimuth
elevation
SYMPY verifications
The following section, goes over the different equations step by step leveraging sympy to verify the absence of typos
[ ]:
# Install a pip package in the current Jupyter kernel
import sys
from os.path import abspath, basename, join, pardir
import datetime
# hack to handle if running from git cloned folder or stand alone (like Google Colab)
cw = basename(abspath(join(".")))
dp = abspath(join(".",pardir))
if cw=="docs" and basename(dp) == "mmWrt":
# running from cloned folder
print("running from git folder, using local path (latest) mmWrt code", dp)
sys.path.insert(0, dp)
else:
print("possibly running from Google Colabs, doing required installs")
!pip install antlr4-python3-runtime==4.11
!pip install pdflatex
!sudo apt-get install texlive-latex-recommended
!sudo apt install texlive-latex-extra
!sudo apt install dvipng
from sympy.parsing.latex import parse_latex
from IPython.display import Image, display
from sympy import preview
from sympy import init_printing, latex, Symbol
import matplotlib.image as mpimg
from matplotlib import pyplot as plt
# f_0: the fundamental frequency of the chirp being transmitted
f_0 = Symbol(r'f_0') # the chirp fundamental frequency
f_c = Symbol(r'f_c') # the IF frequency
# s the slope of the chirp
s = Symbol(r"s")
# the time referenced to the begining of the chirp
t = Symbol(r"t")
Delta = Symbol(r"\Delta") # initial time of flight
delta = Symbol(r"\delta") # change in time of flight
Phi_0 = Symbol(r'\Phi_0')
Psi_0 = Symbol(r'\Psi_0')
nu = Symbol(r"\nu")
eq_n = 0 # equation numbering
running from git folder, using local path (latest) mmWrt code c:\git\mmWrt
FMCW equation
Chirping
A chirp is defined by a linearly increasing frequency.
Where:
$ f_{0} $ is the frequency at the begining of the chirp
s is the slope of frequency increase over time during the chirp
t is the time referenced to the beginning of the chirp.
Given that the instant frequency is the derivative of the phase, the phase of transmitted signal is defined by:
Definitions
$ :nbsphinx-math:`Phi `$ is the full phase (including time or distance variables)
$ \phi`$, :math:phi_0`, \(\phi_1\) are the residual phase in TX signal
$ :nbsphinx-math:`psi `$ is the residual phase in the IF signal
[ ]:
# sympy quirk seems f_0 needs to be first defined as f o then formally subsituted
phi_TX_t = parse_latex(r"2 \pi \cdot ( \mathit{f o} \cdot t + \frac{s}{2} \cdot t^2 ) + \mathit{Phi}_0")
phi_TX_t = phi_TX_t.subs("f o", f_0)
phi_TX_t = phi_TX_t.subs("Phi",Phi_0)
print("latex of phi_TX_t:")
eq_n += 1
print(f"{latex(phi_TX_t)} (Eq {eq_n})")
print("which gets rendered as:")
phi_TX_t
latex of phi_TX_t:
\Phi_{0} + 2 \pi \left(f_{0} t + t^{2} \frac{s}{2}\right) (Eq 1)
which gets rendered as:
So the full transmitted signal is
Received signal
phase of the RX signal is time delayed version of TX:
which can be re-written as (the convention to write \(\left(- \Delta + t\right)\) as opposed to \(\left(t - \Delta \right)\) is chosen to make comparison with simpy display of equation.
Where:
d being the distance to the scatterer (so travelled twice by the radar wave)
$ \Delta `$ is the time of flight (i.e. :math:frac{2 cdot d}{c}`)
$ Y_{RX}(t) = A_R \cdot `:nbsphinx-math:cos{phi_{RX}(t)}` $ is the physical signal received by the antennas.
[ ]:
phi_RX_t = phi_TX_t.subs("t", t-Delta)
eq_n += 1
print(f"{latex(phi_RX_t)} (Eq. {eq_n})")
phi_RX_t
\Phi_{0} + 2 \pi \left(f_{0} \left(- \Delta + t\right) + \frac{s \left(- \Delta + t\right)^{2}}{2}\right) (Eq. 2)
Mixer and IF
The mixer multiplies RX and TX signals, the output is called the Intermediate Frequency, often abreviated IF signal
replacing $ Y_{TX}(t) $ and $ Y_{RX}(t) $ by their respective equations :
We are now going to leverage Trigonometric relations to establish how the IF signal is actually how relatively lower frequency which is why it can be sampled by ADC converters.
Trigonometric relations
Trigonometric relations establish relationship between the product of 2 cosines, with the sum of 2 related cosines in the following relation:
which in our case translates into:
The following cells will leverage simpy to prove those equalities, but one can already see that the IF signal is the sum of a low frequency content \(\Phi_T-\Phi_R\) and high-frequency content \(\Phi_T+\Phi_R\). The high-frequency content is implicitely removed in many textbooks. In reality a low pass filter will ensure no aliasing from the R+T signal gets into the IF.
[ ]:
# Y_T the transmitted signal
Y_T = parse_latex(r"A_T \cdot \cos(\mathit{PhiT})")
# Y_R the received signal
Y_R = parse_latex(r"A_R \cdot \cos(\mathit{PhiR})")
# Y_IF the IF signal (the low frequency content at the output of the mixer)
Y_L = parse_latex(r"\frac{A_T \cdot A_R}{2} \cdot \cos(\mathit{PhiT}-\mathit{PhiR})")
# Y_H: the high frequency content at the output of the mixer
Y_H = parse_latex(r"\frac{A_T \cdot A_R}{2} \cdot \cos(\mathit{PhiT}+\mathit{PhiR})")
# Phi_T the phase of Y_T
PhiT = Symbol("\Phi_T")
# Ph_R the phase of Y_R
PhiR = Symbol("\Phi_R")
Y_R = Y_R.subs("PhiR",PhiR)
Y_T = Y_T.subs("PhiT",PhiT)
Y_L_symbolic = Y_L.subs("PhiR",PhiR)
Y_L_symbolic = Y_L_symbolic.subs("PhiT",PhiT)
Y_H_symbolic = Y_H.subs("PhiR",PhiR)
Y_H_symbolic = Y_H_symbolic.subs("PhiT",PhiT)
# Y_MIX the signal at the output of the mixer before the low pass filter
Y_MIX_simbolic = Y_R * Y_T
print("Y_MIX symbollically is:")
Y_MIX_simbolic
Y_MIX symbollically is:
Now we display this leveraging the trigonometric relations to highlight, high-frequency content and low-frequency content
[ ]:
Y_L_symbolic+Y_H_symbolic
Now we verify that both are the same
[ ]:
assert Y_MIX_simbolic.equals(Y_H_symbolic+Y_L_symbolic)
Mixer output
Mixer output is a 2 tone:
Y_H: high-frequency
Y_L: low-frequency (aka IF: intermediate frequency)
[ ]:
phi_RX_t + phi_TX_t
develop and simplify
[ ]:
# phi_2 is the phase of the `high frequency tone`
# here we write it explicitly to simplify the symbolic writting
# we define \mathit{Delta A} for convenience as to also remember it seems
# \mathic can accept spaces
# \mathic does not seem to accept numbers
# \mathic does not seem to accept lower case for first letter ?!?
phi_2_t = parse_latex(r"2 \pi * (2 \cdot \mathit{f o} \cdot t - \mathit{f o} \cdot \mathit{Delta A} + \frac{s}{2} \cdot \mathit{Delta A}^2 - s * t * \mathit{Delta A} +s*t^2) + 2 * \mathit{Phi}")
phi_2_t = phi_2_t.subs("Phi",Phi_0)
phi_2_t = phi_2_t.subs("Delta A",Delta)
phi_2_t = phi_2_t.subs("f o",f_0)
phi_2_t
we verify that the developped form and initial formulas are the same
[ ]:
# now we verify the two expressions match
phi_1_t = phi_RX_t+phi_TX_t
assert phi_2_t.equals(phi1_t)
Display the high frequency tone explicit formula
[ ]:
# Y_H_t is the `high frequency` component after the mixer
# we replace the symbol `PhiT` by the explicit formula
# then replace `PhiR` by its respective formula
Y_H_t = Y_H.subs("PhiT",phi_TX_t)
Y_H_t = Y_H_t.subs("PhiR",phi_RX_t)
# now we can display the full explicit equation of the high frequency tone
Y_H_t
Focus on the IF (low frequency component)
We derive here the lower frequency tone exact formula
[ ]:
phi_RX_t - phi_TX_t
[ ]:
# now we symbolically define the equation of the low frequency tone of the IF signal
Y_L_t = Y_L.subs("PhiR",phi_RX_t)
Y_L_t = Y_L_t.subs("PhiT",phi_TX_t)
# phi_l is the phase of the `low frequency tone` aka IF tone
# reminder $Delta A$ is required because of `quirks` in parse_latex and/or mathit handling of numbers for symbols
phi_l_t = parse_latex(r"2 \pi * (- \mathit{f o} \cdot \mathit{Delta A} + \frac{s}{2} \cdot \mathit{Delta A}^2 - s * t * \mathit{Delta A} )")
phi_l_t = phi_l_t.subs("Phi",Phi_0)
phi_l_t = phi_l_t.subs("Delta A",Delta)
phi_l_t = phi_l_t.subs("f o",f_0)
phi_l_t
Eq 3
phase of the IF
[ ]:
eq_n += 1
print(f"{latex(phi_l_t)} (Eq {eq_n})")
2 \pi \left(\frac{\Delta^{2} s}{2} - \Delta f_{0} - \Delta s t\right) (Eq 3)
[ ]:
# verify that the two phase definitions match:
assert phi_l_t.equals(phi_RX_t-phi_TX_t)
[ ]:
Y_L_t
Having verified the equation, we now rewrite it for simpler display
we introduce:
*\(f_c\) as the frequency of the received tone in IF (as a function of the slope and the time of flight)
\(\psi_0\) the residual phase in the IF
Where, from above we kept the following:
\(\Delta\) is the time of flight (i.e. \(\frac{2 \cdot d}{c}\))
[ ]:
fb_explicit = parse_latex(r"\mathit{Delta A} \cdot s")
fb_explicit = fb_explicit.subs("Delta A",Delta)
fc_symbol = Symbol("f_c")
# now we replace fb=Delta*s with fc
phi_l_new = phi_l_t.subs(fb_explicit,fc_symbol)
# and we display the new simplified form of the phase of the IF signal
print("phase of IF signal:")
phi_l_new
phase of IF signal:
we now define \(\psi_0\) as the residual phase in IF
[ ]:
psi_0 = Symbol("\psi_0")
psi0_latex = parse_latex(r"\mathit{Delta A} \cdot \mathit{fc}/2 - \mathit{Delta A} \cdot \mathit{f o}")
# define psi0_e as the explicit form of the IF phase
# psi0_e = psi0_i.subs("Phi zero",Phi_0)
psi0_e = psi0_latex.subs("Delta A",Delta)
psi0_e = psi0_e.subs("fc",fc_implicit)
psi0_e = psi0_e.subs("f o",f_0)
psi_0
equals
[ ]:
psi0_e
[ ]:
eq_n += 1
print(f"{latex(psi0_e)} (Eq. {eq_n})")
- \Delta f_{0} + \frac{\Delta f_{c}}{2} (Eq. 4)
[ ]:
# replace in the IF phase the constant offset by psi_0
phi_l_new = phi_l_new.subs(psi0,psi_0)
phi_l_new
we now verify that the simplified IF signal is the same as phi_l_t
[ ]:
phi_l_t
cases where the phase varies
While the underlying assumption in Fast Chirp FMCW (FC-FMCW), is that the distance is not changing during a chirp, the distance may change from chirp to chirp
Where:
$ :nbsphinx-math:`delta `$ is the change in time of flight
[ ]:
# seems that Delta B or delta A are not valid so setting delta as \mathit{D A}
Dd_latex = parse_latex(r"\mathit{Delta A} + \mathit{D A}")
Dd = Dd_latex.subs("Delta A",Delta)
Dd = Dd.subs("D A",delta)
Dd
[ ]:
psi1_e = psi0_e.subs(Delta, Dd)
psi1_e
[ ]:
eq_n += 1
print(f"{latex(psi1_e)} (Eq. {eq_n})")
- f_{0} \left(\Delta + \delta\right) + \frac{f_{c} \left(\Delta + \delta\right)}{2} (Eq. 5)
[ ]:
psi1_e-psi0_e
[ ]:
Delta_Psi_latex = parse_latex(r"\frac{\mathit{D A} \cdot \mathit{f c}}{2} - \mathit{D A} \cdot \mathit{f o}")
Delta_Psi = Delta_Psi_latex.subs("D A",delta)
Delta_Psi = Delta_Psi.subs("f c",f_c)
Delta_Psi = Delta_Psi.subs("f o",f_0)
Delta_Psi
verify they are both the same
[ ]:
eq_n += 1
print(f"{latex(Delta_Psi)} (Eq. {eq_n})")
- \delta f_{0} + \frac{\delta f_{c}}{2} (Eq. 6)
[ ]:
psi1_e-psi0_e
[ ]:
Delta_Psi
[ ]:
assert Delta_Psi.equals(psi1_e-psi0_e)
here we need to consider that f0 ~60e9 and fc ~1e7 MHz
BACK-UP section
debugging symbols in a sympy equations
Delta_Psi.free_symbols
Syntax Examples with Sympy
[ ]:
P = Symbol(r"P")
phi_TX_full = parse_latex(r"2 \pi \cdot ( f_0 \cdot t + \frac{s}{2} \cdot t^2 ) + P")
Y_T = parse_latex(r"A_T \cdot \cos(P)")
print("Phi T:", latex(P))
print("phi full:",latex(phi_TX_full))
Y_T1 = Y_T.subs(P,phi_TX_full)
print("Y1", latex(Y_T1))
print("works")
Phi T: P
phi full: P + 2 \pi \left(f_{0} t + t^{2} \frac{s}{2}\right)
Y1 A_{T} \cos{\left(P + 2 \pi \left(f_{0} t + t^{2} \frac{s}{2}\right) \right)}
works
[ ]:
P0 = Symbol(r"Phi")
x = Symbol("x")
phi_TX_full0 = parse_latex(r"2 \pi \cdot ( f_0 \cdot t + \frac{s}{2} \cdot t^2 ) + P")
Y_T = parse_latex(r"A_T \cdot \mathit{Phi}")
print(f"P0: {P0}")
print(f"phi full: {Y_T}")
Y_T0 = Y_T.subs(P0,x)
print("SUBS:", latex(Y_T0))
print("works")
P0: Phi
phi full: A_{T}*Phi
SUBS: A_{T} x
works
[ ]:
x = Symbol("x")
phi_TX_full0 = parse_latex(r"2 \pi \cdot ( f_0 \cdot t + \frac{s}{2} \cdot t^2 ) + P")
Y_T = parse_latex(r"A_T \cdot \mathit{Phi}")
print(f"P0: {P0}")
print(f"Y_T: {Y_T}")
Y_T0 = Y_T.subs("Phi",x)
print("SUBS:", latex(Y_T0))
print("works")
P0: Phi
Y_T: A_{T}*Phi
SUBS: A_{T} x
works
[ ]:
P5 = Symbol(r"\Psi_{5}")
# P5 = Symbol("P5") # works
Y = parse_latex(r"A_T \cdot \mathit{psi-5}") # does not work
Y = parse_latex(r"A_T \cdot \mathit{psi 5}") # does not work
Y = parse_latex(r"A_T \cdot \mathit{psi5}") # does not work
Y = parse_latex(r"A_T \cdot \mathit{psiv}") # works
# Y = parse_latex(r"A_T \cdot \mathit{psi v}") # works
print("Y",Y)
Y_T1 = Y.subs("psiv",P5)
print("SUBS:", latex(Y_T1))
P5
Y A_{T}*psiv
SUBS: A_{T} \Psi_{5}
[ ]:
[ ]: