In this article, we will go through the basic steps of the up- and downconversion of a baseband signal to the passband signal. In most digital signal processing devices, any signal processing is performed in the baseband, i.e. where the signals are centered around the DC frequency. These baseband signals are mainly complex-valued. However, only real-valued signals can be sent with real-world physical devices. The process of upconversion thus has two purposes:

- Convert the complex-valued baseband signal to a real-valued signal which can be transmitted over an antenna, cable or similar.
- Adapt the transmit signal such that it uses a specific frequency band of the physical channel. This way, multiple signals can be transmitted independently on different frequency bands.

At the receiver, the received signal is then downconverted to baseband such that the subsequent processing can be done in complex-valued baseband domain.

A generic digital transceiver system with digital baseband processing, analog to digital conversion and up- and downconversion is presented in the figure below:

```
```

```
Fs = int(6e4) # the sampling frequency we use for the discrete simulation of analog signals
fc = int(3e3) # 3kHz carrier frequency
Ts = 1e-3 # 1 ms symbol spacing, i.e. the baseband samples are Ts seconds apart.
BN = 1/(2*Ts ) # the Nyquist bandwidth of the baseband signal.
ups = int(Ts*Fs) # number of samples per symbol in the "analog" domain
N = 10 # number of transmitted baseband samples
```

*Feel free to change the filter or rolloff for your own experiments.* Since the normal RRC filter is infinitely long in time domain, we need to truncate it to a length of $2t_0$ and make it causal by shifting it by $t_0$ to the right:

```
# the RRC filter should span 3 baseband samples to the left and to the right.
# Hence, it introduces a delay of 3Ts seconds.
t0 = 3*Ts
# Calculate the filter coefficients (N=number of samples in filter)
_, rrc = commpy.filters.rrcosfilter(N=int(2*t0*Fs), alpha=1,Ts=Ts, Fs=Fs)
t_rrc = np.arange(len(rrc)) / Fs # the time points that correspond to the filter values
plt.plot(t_rrc/Ts, rrc)
```

In step T1) we generate some random baseband data. The baseband of the digital transmission system can be complex-valued, so we create complex baseband samples $d[n]$.

```
# Step T1)
constellation = np.array([1+1j, 1-1j, -1+1j, -1-1j]) # the possible values in the baseband
dk = np.random.choice(constellation, size=(N)) # randomly choose some samples
t_symbols = Ts * np.arange(N) # time instants of the baseband samples
# Plot the samples
plt.subplot(121)
plt.stem(t_symbols/Ts, dk.real);
plt.subplot(122)
plt.stem(t_symbols/Ts, dk.imag);
```

```
# Step T2)
x = np.zeros(ups*N, dtype='complex')
x[::ups] = dk # every ups samples, the value of dn is inserted into the sequence
t_x = np.arange(len(x))/Fs
plt.figure(figsize=(8,3))
plt.subplot(121)
plt.plot(t_x/Ts, x.real);
plt.subplot(122)
plt.plot(t_x/Ts, x.imag);
```

In step T3), the weighted Dirac comb function is filtered with the pulse shaping filter $g(t)$. The outcome is the baseband signal $u(t)$, given by $$u(t)=g(t)*x(t)=\sum_{k=-\infty}^{\infty}d[k]g(t-kT_s).$$

In the code below, we calculate the filtering by using `np.convolve`

. For the plotting of the signal, we also plot the corresponding weighted Dirac comb in the figures. However, since the filtering introduces a delay of $t_0$ seconds, we also delay $x(t)$ by $t_0$ to match it to the transmitted signal:

```
# Step T3)
u = np.convolve(x, rrc)
t_u = np.arange(len(u))/Fs
plt.subplot(121)
plt.plot((t_x+t0)/Ts, x.real, label='$x(t)$') # artificial extra delay for the baseband samples
plt.plot(t_u/Ts, u.real, label='$u(t)$')
plt.subplot(122)
plt.plot((t_x+t0)/Ts, x.imag)
plt.plot(t_u/Ts, u.imag)
```

As we see, the baseband signal roughly matches the transmitted baseband samples. Since we use an RRC filter, the signal is not ISI-free and hence the baseband signal does not exactly go through the values of the baseband samples. We will use a matched filter at the receiver to get the correct samples out of this signal.

In the next step T4), the complex baseband signal $u(t)$ is split into real and imaginary part. The real and imaginary part are also named in-phase (I) and quadratur (Q) components.

```
# Step T4)
i = u.real
q = u.imag
```

The interesting part of up-conversion happens in step T5). Here, the real and imaginary part of the signal are multiplied by cosine and sine, respectively:

$$\begin{align} i_{up}(t)&=i(t)\cos(2\pi f_c t)\\ q_{up}(t)&=-q(t)\sin(2\pi f_c t) \end{align}.$$```
# Step T5)
iup = i * np.cos(2*np.pi*t_u*fc)
qup = q * -np.sin(2*np.pi*t_u*fc)
```

```
# define a function to calculate the spectrum of a signal
fftLen = 4*len(u) # perform 4-times zeropadding to get smoother spectrum
spectrum = lambda x: np.fft.fftshift(np.fft.fft(x, fftLen)) / Fs * (len(u))
# Calculate the spectrum of the signals
f_u = np.linspace(-Fs/2, Fs/2, fftLen)
I = spectrum(i); Iup = spectrum(iup)
Q = spectrum(q); Qup = spectrum(qup)
# Plot the time-domain signals
plt.subplot(221)
plt.plot(t_u/Ts, iup, label='$i_{up}(t)$')
plt.plot(t_u/Ts, i, 'r', label='$i(t)$')
plt.subplot(222)
plt.plot(t_u/Ts, qup, label='$q_{up}(t)$')
plt.plot(t_u/Ts, q, 'r', label='$q(t)$')
plt.subplot(223)
plt.plot(f_u, abs(I), 'r')
plt.plot(f_u, abs(Iup), 'b')
plt.subplot(224)
plt.plot(f_u, abs(Q), 'r')
plt.plot(f_u, abs(Qup), 'b')
```

```
# Step T6)
s = iup + qup
```

```
S = spectrum(s)
U = spectrum(u)
plt.subplot(121)
plt.plot(t_u/Ts, s)
plt.subplot(122)
plt.plot(f_u, abs(U), 'r', label='$|U(f)|$')
plt.plot(f_u, abs(S), 'b', label='$|S(f)|$')
```

From the time-domain signal we cannot really see anything. But, looking at the spectrum we can see the following:

- The blue spectrum $S(f)$ is symmetric (to $f=0$). Hence, it corresponds to a real signal. This is clear, since the signal which is sent to the antenna must be real-valued.
- The red spectrum $U(f)$ is not symmetric, since it corresponds to a complex-valued baseband signal. However, note that the right half of $S(f)$ equals $U(f)$ (up to a scaling factor). The left half of $S(f)$ equals $U(-f)$, i.e. the mirrored baseband spectrum. By this trick, the complex-valued baseband signal $u(t)$ is converted to a real-valued bandpass signal $s(t)$ which carries the same information.

At the receiver, in step R1) the signal $s(t)$ is first multiplied by a sine and a cosine to get a down-converted I and Q component, given by $$\begin{align} i_{down}(t) &= s(t)\cos(2\pi f_c t)\\ q_{down}(t) &= -s(t)\sin(2\pi f_c t) \end{align}.$$

```
# Step R1)
idown = s * np.cos(2*np.pi*-fc*t_u)
qdown = s * -np.sin(2*np.pi*fc*t_u)
```

Let us again look at the spectrum of both downconverted signals:

```
Idown = spectrum(idown)
Qdown = spectrum(qdown)
plt.subplot(121)
plt.plot(f_u, Idown.real, label=r'$\Re\{I_(f)\}$', color='r')
plt.plot(f_u, S.real, label='$\Re\{S(f)\}$', color='b')
plt.subplot(122)
plt.plot(f_u, Qdown.real, label=r'$\Re\{Q_(f)\}$', color='r')
plt.plot(f_u, S.imag, label=r'$\Im\{S(f)\}$', color='b')
```

First of all, we see that the downconverted signal has componenents around $f=0$ and some images around $f=2f_c$. Without going deeply into the maths, we can intuitively explain this: The blue signal $s(t)$ is multiplied by a cosine in time domain. This operation equals a convolution in the frequency domain, and the frequency domain expression of a cosine is given by

$$\mathcal{F}\{\cos(2\pi f_c t)\}=\frac{1}{2}(\delta(f-f_c)+\delta(f+f_c)).$$Hence, we can find the following relation: $$\begin{align} i_{down}(t)&=s(t)\cos(2\pi f_c t)\\ I_{down}(f)&=S(f)*\frac{1}{2}(\delta(f-f_c)+\delta(f+f_c))=\frac{1}{2}(S(f-f_c)+S(f+f_c)). \end{align} $$

This means, the red spectrum is the sum of shifting the blue spectrum by $f_c$ to the right to the left. Since $S(f)$ is concentrated around $f=\pm f_c$, we first get a component around $f=0$, but also images at $f=2f_c$.

Since we are only interested in the central part of the signal, the images at $2f_c$ need to be eliminated. To this end, we apply a low-pass filter, which is called the *image rejection filter* by obvious reasons.

Let us design such a filter. Normally, the cutoff should be closely chosen to the bandwidth of the following AD converter in order to eliminate as much noise as possible. However, here we take a more pragmatic approach and design a filter that just rejects the images at $2f_c$. Also note that the filter, to be causal, necessarily introduces some extra delay $\tau_{LP}$ to the signal.

```
cutoff = 5*BN # arbitrary design parameters
lowpass_order = 51
lowpass_delay = (lowpass_order // 2)/Fs # a lowpass of order N delays the signal by N/2 samples (see plot)
# design the filter
lowpass = scipy.signal.firwin(lowpass_order, cutoff/(Fs/2))
# calculate frequency response of filter
t_lp = np.arange(len(lowpass))/Fs
f_lp = np.linspace(-Fs/2, Fs/2, 2048, endpoint=False)
H = np.fft.fftshift(np.fft.fft(lowpass, 2048))
plt.subplot(121)
plt.plot(t_lp/Ts, lowpass)
plt.gca().annotate(r'$\tau_{LP}$', xy=(lowpass_delay/Ts,0.08), xytext=(lowpass_delay/Ts+0.3, 0.08), arrowprops=dict(arrowstyle='->'))
plt.subplot(122)
plt.plot(f_lp, 20*np.log10(abs(H)))
```

As we see, the filter will introduce a delay of $\tau_{LP}$ due to its maximum at this time. Furthermore, according to the frequency response, it should reliably remove the images at $2f_c$.

So, in step R2) the images that stem from the downconversion are filtered out by means of the lowpass image rejection filter: $$\begin{align} i_{down,lp}(t)&=i_{down}(t)*LP(t)\\ q_{down,lp}(t)&=q_{down}(t)*LP(t)\\ \end{align}.$$

```
# Step R2)
idown_lp = scipy.signal.lfilter(lowpass, 1, idown)
qdown_lp = scipy.signal.lfilter(lowpass, 1, qdown)
```

Let us again have a look at the spectrum of these filtered signals:

```
Idown_lp = spectrum(idown_lp)
Qdown_lp = spectrum(qdown_lp)
plt.subplot(121)
plt.plot(f_u, abs(Idown), 'r', lw=2, label=r'$|I_{down}(f)|$')
plt.plot(f_u, abs(Idown_lp), 'g-', label=r'$|I_{down,lp}(f)|$')
plt.subplot(122)
plt.plot(f_u, abs(Qdown), 'r', lw=2, label=r'$|Q_{down}(f)|$')
plt.plot(f_u, abs(Qdown_lp), 'g', label=r'$|Q_{down,lp}(f)|$')
```

```
# Step R3)
v = idown_lp + 1j*qdown_lp
```

```
# Step R4)
y = np.convolve(v, rrc) / (sum(rrc**2)) * 2
```

Here, we introduce some energy normalization to get the correct amplitude of the signal at the receiver. Eventually, in step R5), we sample the analog baseband signal to get the actual baseband samples that were transmitted over the channel. Here, we need to take care of the overall transmission delay of the chain, which consists of

- $t_0$ delay at the transmitter due to filtering with $g(t)$ in the DA conversion,
- $\tau_{LP}$ delay at the receiver due to the image rejection filter,
- $t_0$ delay at the receiver due to matched filtering with $g(t)$ in the AD conversion.

Hence, the overall delay is given by $2t_0+\tau_{LP}$. So, to get the baseband samples, we sample the received baseband signal at the following positions:

$$\hat{d}[n] = v(nT_s+2t_0+\tau_{LP}).$$```
# Step R5)
delay = int((2*t0 + lowpass_delay)*Fs)
```

```
t_y = np.arange(len(y))/Fs
t_samples = t_y[delay::ups]
y_samples = y[delay::ups]
plt.subplot(221)
plt.plot(t_y/Ts, y.real)
plt.stem(t_samples/Ts, y_samples.real)
plt.subplot(222)
plt.plot(t_y/Ts, y.imag)
plt.stem(t_samples/Ts, y_samples.imag)
plt.subplot(223)
plt.stem(t_symbols/Ts, dk.real);
plt.subplot(224)
plt.stem(t_symbols/Ts, dk.imag);
```

Do you have questions or comments? Let's dicuss below!