-
On matched filtering in the presence of carrier frequency offset
If \(\{x_k\}_{k=0}^{N-1}\) is a complex waveform and this waveform is received in AWGN, that is, the sequence \(y_k = A x_k + n_k\) is received, where \(A\) is an unknown complex amplitude and \(n_k\) are independent circularly symmetric normal random variables with standard deviation one, it is well known that the linear filter \(\{a_k\}_{k=0}^{N-1}\) that maximizes the detection SNR of \(T = \sum_{k=0}^{N-1} a_k y_k\), which is defined as \(|\mathbb{E}[T]|^2/V(T)\), is given by the matched filter \(a_k = \overline{x_k}\).
A natural question is what happens if the waveform is received with an unknown carrier frequency offset \(f\). That is, if \(y_k = A e^{2\pi i f k} x_k + n_k\). The typical solution is to use a bank of matched filters which are spaced by \(\Delta\) in the frequency domain and cover the expected carrier frequency offset range. This means that we use filters \(a_{k,m} = e^{-2\pi i \Delta m k} \overline{x_k}\), where the integers \(m\) are such that the grid of frequencies \(\Delta m\) covers the required frequency range. Here the hope is that if \(\Delta\) is small enough, then choosing the integer \(m\) such that \(\Delta m\) is closest to \(f\), we have that\[T = \sum_{k=0}^{N-1} e^{-2\pi i \Delta m k} \overline{x_k}y_k = A \sum_{k=0}^{N-1} e^{2 \pi i (f-\Delta m) k} |x_k|^2 + \sum_{k=0}^{N-1} e^{-2 \pi i \Delta m k} \overline{x_k} n_k.\]If \(\Delta m-f\) is small, this expression will have an SNR similar to the SNR for \(f = 0\).
However it is possible to do better. We can modify the matched filter to improve the SNR when there is frequency error. Since we choose the filter \(m\) from the bank such that \(\Delta m\) is closest to \(f\), we can assume that \(|f| \leq \Delta / 2\) and that we choose bank \(m = 0\). Now we can pose an optimization problem that involves the frequency error \(f\). Different kinds of optimization problems will lead to different solutions. A natural quantity to maximize is the average SNR with respect to \(f\), assuming that \(f\) is uniformly distributed in \([-\frac{\Delta}{2},\frac{\Delta}{2}]\). This is\[\frac{1}{\Delta}\int_{-\frac{\Delta}{2}}^{\frac{\Delta}{2}} \frac{|\mathbb{E}[T]|^2}{V(T)}\, df.\]Let us solve the optimization problem.
Similarly to the case of the matched filter with no frequency error, we have\[\mathbb{E}[T] = A \sum_{k=0}^{N-1} e^{2 \pi i f k} a_k x_k,\]so\[V(T) = \mathbb{E}[|T-\mathbb{E}[T]|^2] = \mathbb{E}\left[\left|\sum_{k=0}^{N-1} a_k n_k\right|^2\right] = \sum_{k = 0}^{N-1} |a_k|^2.\]Therefore, the optimization problem is equivalent to finding a vector \(\{a_k\}_{k=0}^{N-1}\) with unit norm (\(\sum_{k=0}^{N-1} |a_k|^2 = 1\)) that maximizes the expression\[\frac{1}{\Delta}\int_{-\frac{\Delta}{2}}^{\frac{\Delta}{2}}\left|\sum_{k=0}^{N-1} e^{2\pi i f k} a_k x_k\right|^2\, df.\]This expression can be rewritten as\[\frac{1}{\Delta}\sum_{k=0}^{N-1}\sum_{l=0}^{N-1}\overline{a_l x_l}a_k x_k \int_{-\frac{\Delta}{2}}^{\frac{\Delta}{2}} e^{2\pi i f (k-l)}\, df = \sum_{k=0}^{N-1}\sum_{l=0}^{N-1}\overline{a_l x_l}a_k x_k \operatorname{sinc} (\Delta (k-l)),\]where \(\operatorname{sinc}(x) = \sin(\pi x)/(\pi x)\).
Write \(x_k = e^{i\theta_k}|x_k|\) and denote by \(G\) the matrix with entries \(G_{kl} = |x_k||x_l|\operatorname{sinc} (\Delta (k-l))\). This matrix is the Gram matrix of the vectors \(\varphi_k(f) = e^{2 \pi i f k}|x_k|\) in \(L^2[-\frac{\Delta}{2}, \frac{\Delta}{2}]\) (with the Lebesgue measure multiplied by the normalization factor \(\frac{1}{\Delta}\)), so it is positive semidefinite (and it is positive definite if \(\Delta > 0\) and \(x_k \neq 0\) for all \(k = 0, \ldots, N-1\)). Let \(u = \{u_k\}_{k=0}^{N-1}\) be any unit norm eigenvector corresponding to the largest eigenvalue of \(G\). The solution to the optimization problem is then given by \(a_k = e^{-i\theta_k} u_k\).
The case in which \(|x_k| = 1\) for all \(k = 0, \ldots, N-1\) is particularly important, because it corresponds to constant-envelope modulations, and it is also easier to treat than the general case. This case includes the case of detecting a CW carrier, which corresponds to \(x_k = 1\) for all \(k\). In this case, the Gram matrix \(G\) is simply a matrix of sinc functions \(G_{kl} = \operatorname{sinc} (\Delta (k-l))\). This matrix has been studied by Slepian in 1978, because it appears in the spectral concentration problem, which is equivalent to the optimization problem I’m presenting here. In the spectral concentration problem we consider a unit norm sequence \(\{a_k\}_{k=0}^{N-1}\) and its Fourier transform\[U(f) = \sum_{k=0}^{N-1} e^{-2\pi i f k}a_k.\]The energy that \(\{a_k\}\) has in the frequency band \([-\frac{\Delta}{2}, \frac{\Delta}{2}]\) is\[\int_{-\frac{\Delta}{2}}^{\frac{\Delta}{2}} |U(f)|^2\,df.\]The problem asks which unit norm sequences \(\{a_k\}\) maximize the energy in this frequency band. It is clear that this is equivalent to a particular case of the optimization problem that we are considering (the case \(x_k = 1\) for all \(k\)).
The eigenvectors of the matrix \(G\) given by \(G_{kl} = \operatorname{sinc} (\Delta (k-l))\) can be computed numerically, but the problem is ill-conditioned for large \(N\), because many eigenvalues cluster around the largest eigenvalue. There is a trick to compute the eigenvectors without running into issues. In our case, we can use that trick to compute the eigenvector corresponding to the largest eigenvalue. The trick starts by noticing that the tridiagonal symmetric matrix \(R\) defined by\[\begin{split}R_{k,k} &= \cos(\pi\Delta) \left(k-\frac{N-1}{2}\right)^2,\ k = 0,\ldots, N-1\\R_{k,k+1} = R_{k+1,k} &= \frac{1}{2}(k+1)(N-1-k),\qquad k = 0,\ldots,N-2\end{split}\]commutes with \(G\) (this is shown by some straightforward calculations). This implies that the eigenspaces of \(G\) and \(R\) are the same. All the eigenvalues of a tridiagonal matrix with non-zero entries in its upper diagonal are simple. This is because the first component of an eigenvector for a given eigenvalue determines the remaining components (this is a very simple calculation). Moreover, it is also possible to prove that if \(v_0, v_1, \ldots, v_{N-1}\) are the eigenvectors of \(R\) with corresponding eigenvalues \(\lambda_0 > \lambda_1 > \cdots > \lambda_{N-1}\), then \(v_0, v_1, \ldots, v_{N-1}\) (which must be eigenvectors of \(G\)) have eigenvalues \(\mu_0 > \mu_1 > \cdots > \mu_{N-1}\) as eigenvectors of \(G\). However I haven’t been able to find a reference that proves this in a straightforward way, though presumably this follows from all the calculations in Slepian’s paper. This means that the eigenvector \(u\) that we need can be computed as \(v_0\), the eigenvector corresponding to the largest eigenvalue \(\lambda_0\) of \(R\).
There are efficient algorithms to compute eigenvalues and eigenvectors for tridiagonal symmetric matrices. For instance, SciPy has an eigh_tridiagonal function. Additionally it is possible to use these algorithms to compute only a selection of some of the eigenvalues and eigenvectors. This is precisely what we need, because in our case we only want the eigenvector \(v_0\) corresponding to the largest eigenvalue \(\lambda_0\) of \(R\).
The eigenvectors \(v_0, v_1, \ldots, v_{N-1}\) are called discrete prolate spheroidal sequences (DPSS) and they also play a role in other similar spectral concentration problems and in multitaper spectral analysis. They can be calculated with the dpss window function in SciPy. The first DPSS, also known as Slepian window, and \(v_0\) in our eigenvector notation, is the vector that solves our optimization problem in the constant-envelope case. That is, when \(|x_k| = 1\) for all \(k = 0, \ldots, N-1\). The takeaway of this post is:
For a constant-envelope waveform, average SNR performance in the presence of carrier frequency offset improves if the matched filter is multiplied by a DPSS window.
In the general case in which the waveform is not constant-envelope, the trick of using a tridiagonal matrix does not work, and we can also have ill-conditioning when trying to compute the eigenvectors numerically (for instance by using the eigh SciPy function).
It is convenient to put the above theoretical results in perspective and show what they really imply for the usual practical applications. In most applications \(\Delta \lesssim 1/N\). For instance, when an FFT is used to detect a CW carrier, we have \(\Delta = 1/N\), because in this case \(\Delta\) is equal to the FFT bin spacing. Other matched filter applications that use an FFT similarly set \(\Delta\) in relation to the FFT bin spacing.
The following plot shows, for a constant-envelope waveform of \(N\) samples, what is the average SNR loss when there is a carrier frequency offset \(f\) uniformly distributed on \([-\frac{\Delta}{2},\frac{\Delta}{2}]\), for both the matched filter (solid lines) and for a matched filter weighted with the DPSS window (dashed lines). The values of \(\Delta = 1/(4N)\), \(1/(2N)\), \(1/N\), \(2/N\), \(4/N\) are shown. We see that there is almost no dependence on \(N\), which is to be expected because this problem can be approximated well by an analogous continuous-time problem. We also see that for the smallest values of \(\Delta\) the loss of the matched filter is already quite small and the DPSS weighting gives almost no improvement.

Since the behaviour changes very little with \(N\), it is better to study what happens for a fixed \(N\) (here \(N = 8192\)), in terms of \(\Delta N\). This is what the plot below shows.

Another good takeaway from this plot is:
The average SNR loss of matched filtering with a frequency bin separation equal to the reciprocal of the filter length (for instance when using an FFT to detect a CW carrier) is around 1 dB, regardless of whether DPSS weighting is used.
The following figure shows the difference between the two traces in the figure above. This indicates how much improvement is obtained by applying DPSS weighting to the matched filter. For \(\Delta = 1/N\) the improvement is 0.05 dB, which explains why this is rarely done in practice. However for \(\Delta = 2/N\) the improvement is 0.36 dB, so DPSS weighting can be a useful technique if we need to implement a system that is in this situation for whatever reason. Note that interestingly the improvement decreases for \(\Delta > 2/N\).

Finally, the following plot shows the DPSS windows. As expected, when \(\Delta\) decreases they tend to a rectangular window, and as \(\Delta\) increases the window tapers off more.

These plots have been made in this Jupyter notebook.
Here I have treated the optimization of the average SNR assuming a uniform distribution of \(f \in [-\frac{\Delta}{2},\frac{\Delta}{2}]\). An equally interesting and useful optimization criterion is to maximize the worst-case SNR. That is, find the sequence \(\{a_k\}\) with unit norm that maximizes\[\min_{|f| \leq \frac{\Delta}{2}}\frac{|\mathbb{E}[T]|^2}{V(T)} = \min_{|f| \leq \frac{\Delta}{2}} \left|\sum_{k=0}^{N-1} e^{2\pi i f k} a_k x_k\right|^2.\]The techniques that are applicable in this problem are related to the Remez exchange used for optimal FIR design, but since the solution is quite involved, this might be a topic for another time.
-
X-engine correlator on a Ryzen NPU
Back in May I wrote a post explaining the architecture of AMD NPUs and demonstrating a simple kernel that reaches close to the peak TOPS allowed by the hardware resources. That program was very simple and there was no data movement between the NPU compute tiles and the rest of the system. After writing that post, I wanted to write a more complex program to understand better the NPU architecture and the software stack, and what the challenges are when working on real-world projects.
I decided to implement an X-engine correlator, such as the ones commonly used in radio astronomy. This is a problem that I’m personally interested in, and it is basically a big matrix multiplication, with some small caveats that I will explain below. As the main goal of NPUs is to be used for matrix multiplication in machine learning inference, I thought “how hard can this be?”. After a few months of working on and off on this project, I now have something ready to show.
The design has actually been quite challenging. There are many nuances I will get into below, and many dead-end paths I have taken until I discovered that when using some particular approaches performance would be killed for one reason or another, so I had to backtrack multiple times. I should also mention that while it is usually very clear what is happening at low level (which was one of the main messages of my previous post), the lack of flexibility of the hardware is often quite limiting, in particular regarding the programmability of the DMA engines, so it is quite challenging to design approaches that actually work well in the hardware.
This post is a full presentation of my X-engine implementation.
-
Tianwen-1 safe mode telemetry
On 2026-08-02, Peter Gülzow, AMSAT-DL‘s president, noticed that Tianwen-1 was transmitting very low rate telemetry, in contrast to the usual 16 kbaud telemetry. This is typical of a safe mode configuration, since it allows decoding even when low gain antennas are used and the attitude is unfavourable. Peter made a couple of short IQ recordings. Mars was setting in Bochum, at an elevation of 4º, when the recordings were done, so he could not record for longer. After this event, Tianwen-1 has been operating again nominally with 16 kbaud telemetry, so we do not know the details of what happened. In this post I will analyse the safe mode telemetry signal.
-
ORI’s lunar descent CTF
Open Research Institute has recently published the solutions for a Lunar Descent CTF that they ran at BSides San Diego 2026. The CTF revolves around the Ka-band radio altimeter used by the Chandrayaan-3 lunar lander. The CTF includes a simulation of the radio altimeter and the goal is to discover why the lander is crashing in this simulation and fix the problem. The CTF is in the Github repository OpenResearchInstitute/lunar-descent-ctf, which includes both the CTF and the solutions (in a
spoilersdirectory). This seemed like an interesting topic, and in the past I have enjoyed a lot other CTFs that were organized by Michelle Thompson, so I decided to clone the repo, delete thespoilersdirectory, and start playing. In this post I comment on the CTF and my solution, so read no further if you don’t want to see spoilers. -
Tianwen-2 low data rate telemetry
Thomas Telkamp has shared with me an IQ recording of the Tianwen-2 telemetry downlink made with the Bochum 20 metre antenna on June 8. This is part of an ongoing effort led by Peter Gülzow, AMSAT-DL‘s president, for closely monitoring Tianwen-2’s operations. All the material I have used in previous posts about Tianwen-2 has come from these activities.
This recording was made just before Tianwen-2 did a manoeuvre (recall that the orbital insertion at asteroid Kamo’oalewa was reported to be on June 7). During this recording Tianwen-2 was transmitting telemetry at a slower rate than the usual 16384 baud. This makes sense, given the fact that the attitude during the manoeuvre would be unfavourable. In this short post I decode this recording.
The only two configuration differences between the regular 16 kbaud telemetry and this lower data rate telemetry is that the baudrate is reduced to 4096 baud, and the frame size is reduced to 220 bytes. This means that a single Reed-Solomon codeword from the shortened (252, 220) code is used, instead of four interleaved Reed-Solomon codewords from the full (255, 223) code. The over-the-air frame duration is still one second.
The corresponding modifications to the GNU Radio decoder are simple. This plot shows the decoder running on the recording. The SNR is excellent.

GNU Radio decoder processing the Tianwen-2 low data rate recording The contents of the telemetry frames are the same as the regular 16384 baud telemetry. There are very few differences. One difference is that the last two bytes of the AOS insert zone, which are always
0x300bin 16384 baud telemetry, are always0x0001in this 4096 baud telemetry. I don’t know what these bytes mean, and this difference doesn’t give me a clue either.Almost all the same APIDs as in the regular telemetry are present, although their transmission rate is reduced due to the lower data rate. Comparing, I see that only APIDs 1553 and 1554 are missing in the low rate telemetry.

The GNU Radio decoder used in this post is here, the Jupyter notebook is here, and the binary file containing the decoded frames is here.
-
PLL coefficients for rate-only feedback
A couple years ago I wrote a post about the paper Controlled-Root Formulation for Digital Phase-Locked Loops, by Stephens and Thomas. In this paper, the authors study digital PLLs by studying the transfer function in discrete time, rather than doing any continuous-time approximations or equivalences. They give values for the loop coefficients that are needed to achieve a certain noise bandwidth for some standard loop root placements (the supercritical damped response and the standard underdamped response). In general, the loop coefficients are obtained numerically. In my post, I show that in the case of a loop of order 2 with supercritical response the loop coefficients can be calculated explicitly in terms of the loop bandwidth by using the solution of a cubic equation. However, in that post, I treated only the phase/phase-rate feedback case. In this post I cover the rate-only feedback case.
-
An update about Tianwen-2 telemetry
Yesterday I posted about my decoding of some recordings of the X-band telemetry of Tianwen-2 done by the Dwingeloo radio telescope. Today I have some small updates.
First of all, I have figured out the format of the AOS insert zone. In the previous post I mentioned that the AOS insert zone contains 8 bytes that are mostly static, except for one byte that seems to be a frame counter. I suspected that the AOS insert zone would contain timestamps, which was the case with Tianwen-1, but this didn’t seem to be the case with Tianwen-2. However, today I have found that the 8-byte insert zone contains a 6-byte timestamp in little endian format that counts the number of \(2^{-16}\) second ticks since the epoch, which is 2019-12-31 16:00:00 UTC (or 2020-01-01 00:00:00 Beijing time). The remaining two bytes have the constant value
0x300b. I don’t know what these two bytes are, since they don’t seem to be a CCSDS time code P-field.There were two things about this timestamp field that were confusing me: the fact that it is little-endian, since CCSDS and the telemetry data in the Space Packet payloads is always big-endian, and the fact that these AOS frames take exactly one second to transmit. This means that the change in the timestamp in each frame is just an increment in the byte corresponding to seconds, which carries over to the next bytes on overflows, plus a very slow drift in the least significant byte caused by the relative drift of the symbol rate clock and the spacecraft clock. Only now that I’ve seen how this field evolves during longer periods of time, I have been able to figure its format.
Using an epoch in Beijing time instead of UTC is common in Chinese spacecraft. For instance, Tianwen-1 uses 2016-01-01 00:00:00 Beijing time as its epoch.
The second update is that AMSAT-DL has been tracking Tianwen-2 with their 20 m dish in Bochum and decoding the telemetry signal in real time with SatDump. They have shared the decoded AOS frames with me, and I have run them through my Jupyter notebook. They have collected a good amount of data: a few hours on May 26 and a full track of about 10 hours on May 27. This data is what has given me the clues to figure out how the timestamps work. The following plot shows the APIDs received over time. This indicates that there are no APIDs that are only active occasionally.

Even though now we have a much longer time span of data, the qualitative behaviour of the telemetry is still the same as I mentioned in the last post. The Jupyter notebook where I analyse the frames received by Bochum is here.
-
Decoding Tianwen-2
Tianwen-2 is a Chinese mission that will return samples from the Earth quasi-satellite asteroid 469219 Kamoʻoalewa and rendezvous with the 311P/PanSTARRS comet. It was launched on 28 May 2025 from the Xichang Satellite Launch Center. It is planned to perform its orbital insertion at Kamoʻoalewa on 7 June 2026, and study the asteroid until 24 April 2027. Since ephemerides for this mission are not publicly available, it has been difficult for amateur observers to track it so far, but now it is close enough to Kamoʻoalewa to find it by pointing around the asteroid.
On Monday, CAMRAS used the 25 meter Dwingeloo radio telescope to receive and record the X-band telemetry signal from Tianwen-2, publishing the SigMF recordings in their data archive. They reported that the spacecraft was 1.1 degrees away from the asteroid. In this post I will decode and analyse the telemetry using these recordings.
-
Getting peak TOPS on a Ryzen AI 7 350 NPU
I have a Framework Laptop 13 that has a Ryzen AI 7 350 CPU that includes an NPU. I have started playing with this NPU to understand how to develop software for it. While NPUs are mainly intended as accelerators for inference of ML models, they are fundamentally hardware accelerators for matrix multiplication and other similar linear algebra operations, so they are also useful for signal processing and other compute applications, which is why I am interested in them. Another reason why I am interested in this NPU is that, as I will explain below, it is very similar to the AIE-ML v2 AI engine in Versal FPGA SoCs, so this laptop is a great platform to learn how to use this AI engine.
NPUs use the concept of TOPS (tera operations per second) as a high-level marketing figure of their capabilities. An operation is generally understood as an addition or multiplication for
int8data types, since the amount of parallelization that can be achieved depends on the datatype width. The NPU on the Ryzen AI 7 350 is marketed as a 50 TOPS NPU. The main goal of this post is to understand where this number comes from, in terms of hardware execution units and capabilities, understand under which conditions it can be reached, and write a small application that reaches this TOPS value.I think this is a good way of gaining in-depth understanding about a compute architecture. Most typical real world use cases are going to be slower than this, because the algorithms will have bottlenecks that result in hardware underutilization. By understanding how the hardware needs to be used to reach peak performance, we have a better idea of the gaps of these algorithms and also how to rewrite the algorithms to reduce the gap if possible. In a post last year about NEON kernels on the ARM Cortex-A53 I worked in a similar way, by choosing a simple kernel to accelerate and by comparing performance benchmarks with the peak performance allowed by the hardware.
-
Decoding the NB-IoT downlink
Recently I have been posting about V16 beacons, which are car emergency warning beacons that have been introduced this year in Spain, and which use the LTE NB-IoT cellular network to transmit their geolocation data to the traffic authority network when they are switched on. As part of experimenting with these beacons, I made recording of the downlink and uplink NB-IoT signals while the beacon was sending data to the network. My hope was to be able to decode these signals and extract the two-way traffic that shows how the beacon attaches to the LTE network and sends its data. I already decoded all the uplink transmission in a previous post. In this post I will decode the corresponding recording of the downlink channel.
However, as I already suspected when I was decoding the uplink recording, due to how I physically set up the experiment to avoid saturating the SDR receiver with the beacon transmissions, it turns out that the beacon was talking to an NB-IoT cell that is relatively weak in the downlink recording. More specifically, the antenna for the SDR receiver was set up near a window in the north side of the house, while the beacon was placed on the window sill on the south side of the house. The SDR receiver sees strong downlink signals from cell 145, which is located northeast of the house and is the cell to which the beacon connected in a previous experiment I did with the beacon placed in the north window. However, in this experiment with the beacon on the south window, the beacon connected to cell 261, which is southwest of the house. The signal from this cell is weaker in the downlink recording and is frequently overwhelmed by the signals from cell 145 and other strong cells. So I have had partial success decoding the transmissions that the network sent to the beacon.
This post is mainly about the NB-IoT downlink in general. At the end I focus on the downlink transmissions to the V16 beacon that I have been able to decode. It is a rather long post, because I cover all the main physical channels and signals of the NB-IoT downlink. I show how the NPSS and NSSS primary and secondary synchronization signals and the NRS reference signals work, how to decode the MIB-NB in the NPBCH, how to decode the SIB1-NB and SI messages carrying other SIB-NBs, how to decode NPDCCH transmissions in the Type1 common search space, which corresponds to paging, as well as decoding the corresponding NPDSCH transmissions carrying paging messages, how to do blind decoding of NPDCCH transmissions in the Type2 common search space and UE-specific search space, which correspond to uplink grants and downlink scheduling, and decode the corresponding NPDSCH transmissions that send data to the V16 beacon.
The recording used in this post is published in the dataset Recording of the NB-IoT downlink of a V16 beacon in Zenodo.
5g 10ghz artemis1 astronomy astrophotography ATA ccsds ce5 contests digital modes doppler dslwp dsp eshail2 fec filters freedv frequency gmat gnss gnuradio gomx hermeslite hf jt kits lilacsat limesdr linrad lte microwaves mods noise ofdm orbital dynamics outernet polarization radar radioastronomy rust satellites sdr signal generators tianwen vhf & uhf