Vector measurements with the HackRF, 2

13.05.2021 20:10

Over the past year I've been slowly building up a small, one-port vector network analyzer. The last improvement I made to it was replacing the rtl-sdr receiver with the HackRF One. This increased the frequency range, but the dynamic range of the measurement was still quite low at higher frequencies. I suspected that a significant source of noise in the system was phase noise. In this post I describe some measurements I performed to get a better idea of what is going on in regard to phase. I also wanted to have a base reference to compare with when I change things in the future. This way I will be able to see whether I improved the instrument or made it worse.

My small, home-made vector network analyzer, upgraded with a HackRF.

First thing I measured was the apparent phase noise of the stimulus signal in the digital baseband. I manually set my instrument so that the signal coming from the ERASynth Micro synthesizer was routed directly to the HackRF receiver. I then recorded an IQ signal trace from the HackRF and calculated the apparent phase noise of the sine wave. The ERASynth Micro output frequency was set to 1 GHz.

Apparent phase noise of the stimulus signal in the digital baseband.

This is the resulting plot of the phase noise versus frequency offset. It is based on the FFT of the recorded digital baseband signal with 128k points and the Hann window. I verified that the measured noise level is above the spectral leakage due to FFT windowing (for Hann window the leakage falls off by 60 dB per decade). For reference I also plotted the phase noise specification of the ERASynth Micro from its datasheet. That would be the ideal result if HackRF was perfect and didn't contribute any additional noise. In reality, HackRF's internal oscillator is probably much noisier than the ERASynth Micro.

Currently the ERASynth Micro and HackRF are both running free from their own internal oscillators. They are not synchronized to a common reference, hence this graph is the combination of all sorts of effects in both devices: phase noise in both oscillators, jitter from various phase locked loops and probably other effects as well. The noise shown on the plot is not present in any real analog signal anywhere. It shows up on the digital data that comes out of the HackRF's ADC. Since that is the input to all further processing it's the thing I'm most interested in.

Trace phase noise when measuring the open standard.

Another thing I was interested in was the final noise level in the vector measurement. This is the trace phase noise that's usually specified for commercial vector network analyzers in degrees root-mean-square. It's the effective error on the phase coordinate that shows up in the final measurement result, after all the processing has been done. To estimate this for my system I did a zero-span vector measurement of the open calibration standard. I recorded 200 points at 1 GHz. The plot above shows the result on a linear-scale polar plot. The estimated error of the measurement was 1.39 degrees RMS.

Its apparent from the plot that my measurements are smeared more along the phase than the amplitude axis. This is where my initial assumption came from that the phase noise is currently more problematic in my system than inaccuracies in measuring the amplitude of the signal. Just for comparison I looked up some datasheets for commercial network analyzers. It seems a typical value for this would be in the range of 0.1 to 0.01 degrees RMS. Not that I ever expect to reach that level of accuracy with my home-brew instrument, but it's interesting to see how it compares.

Next step for this project is definitely to try to run the HackRF from the 10 MHz TCXO in ERASynth Micro and see how much this improves the metrics I described above. After some research it seems that I need to be careful with how I approach this. HackRF needs a 3.3V CMOS digital signal as a reference while Ref out on ERASynth Micro is a sine wave. I need to design a board that will convert the waveform, however a sloppy conversion can introduce additional jitter. I've been looking at some previous work published by the amazing Osmocom project and I will likely take their osmo-clock-gen and/or osmo-clock-conv designs as a starting point.

Posted by Tomaž | Categories: Analog | Comments »

Measuring interrupt response times, part 3

01.05.2021 18:17

Around five years ago I performed some measurements of interrupt response times in a Raspberry Pi Zero and an Arduino. My goal was to get some rough estimates of what kind of real-time performance you can expect from these systems. I was not interested in pushing them to their limits. I wanted to compare the most straightforward approaches - code you would find in documentation or in examples that pop up on top of web searches. This year the Raspberry Pi Pico was released and it promises to become just as popular. It brings some interesting new features that I wanted to explore, like MicroPython and the programmable I/O (PIO). I thought it would be interesting to repeat my old measurements and see how well it compares to the other two systems.

I only briefly summarize my previous results here. Read my original blog post for a longer introduction, description of the test setup and more in-depth discussion of the first batch of measurements. In the follow up post I also dug a little deeper into the reasons behind some of the more unusual results I got with Arduino and Raspberry Pi Zero.

Raspberry Pi Pico connected to the test setup.

For the purpose of this test, the interrupt response time is the time the system takes to change a state of an output GPIO pin in response to the change in an input GPIO pin. In real applications there is usually some kind of processing involved, so this value represents only the best-case scenario of how fast the software can respond to external events.

This response time was measured using a signal generator and an oscilloscope. A square wave generated by the signal generator was connected to the input pin. The two-channel oscilloscope was connected to both the input pin and the output pin. It was setup to measure the interval between the two state changes. The measurement was automated and repeated 500 times for each setup. Exact settings used are noted here.

To perform the test with the RP2040 processor on the Raspberry Pi Pico I installed a MicroPython firmware, as described in the Getting Started guide. I tried two implementations: A pure Python implementation was using the machine.Pin built-in class to configure a Python function as an interrupt handler. The PIO implementation used the rp2.asm_pio decorator to program the PIO state machine from Python code (see Section 3.9 in the Python SDK manual). After the state machine was programmed, the input was handled purely inside the PIO and the Python interpreter was not involved. You can find exact code I used in the GitHub repo.

Here is how the new measurements with the RP2040 compare with Arduino and the Raspberry Pi Zero:

Histogram of interrupt response time measurements.

The MicroPython implementation on the RP2040 (yellow) has the average response time of around 60 μs. This is around 3.5 times faster than using a CPython implementation on the Zero (cyan) which averages at around 210 μs. It is also more consistent, with less spread between minimum and maximum response times. A surprising result at the first glance, since Zero has a much more capable CPU running at up to 1000 MHz while the ARM core in the Pico only runs at 125 MHz.

The difference is very likely due to all the Linux kernel housekeeping and context switching that happens before the interrupt is propagated from the hardware to the Python process. MicroPython, while quite complex, is still a lightweight interpreter compared to the full CPython on the Zero. This is consistent with the fact that a C implementation that runs in the kernel on the Zero (blue) is much faster than MicroPython on the RP2040.

The following figure zooms in on the left end of the histogram:

Zoomed view of the left end of the response time histogram.

Here you can see that the PIO implementation is amazingly fast compared to all previously tested configurations. With the average response time of 0.043 μs it beats both the polling and the interrupt-driven C++ implementation on the Arduino by two orders of magnitude.

This comparison is a bit unfair though. The specialized PIO state machines on the RP2040 are indeed very fast, with only 8 ns per instruction and an instruction set that is optimized for responding to input events. However, the amount of processing you can do with them is very limited compared to all other approaches I've tested. Each PIO can only process 32 instructions. Most real-life applications beyond interfacing with a simple bus protocol will need a round-trip to MicroPython. This puts the response time back into the hundred-microsecond range.

Still, investigating PIO performance is interesting. Here is another level of zoom to show only the distribution of response times by the PIO implementation:

Histogram of response times for the RP2040 PIO implementation.

The response times should be in the range of 4 to 5 instruction cycles - 2 cycles for the input synchronizer (see 3.5.6.3 in the RP2040 Datasheet), between 1 or 2 cycles for WAIT and 1 cycle for SET. I did not use any clock dividers and used the default 125 MHz system clock, so each instruction takes 8 ns. This gives the range of response times between 32 to 40 ns.

I measured between 38 and 48 ns. Very likely this is a measurement error. Unfortunately my signal generator has a rise-time of around 10 ns. This means that in the nanosecond range the transition between low and high logic level is not well defined and this introduces an error into my measurement. I verified by other means that one PIO instruction indeed takes exactly 8 ns in my setup. It is also possible that I missed something and there is an additional PIO cycle (or two) needed somewhere before the response propagates to the GPIO pin.

On the oscilloscope screenshot below, the blue trace is the stimulus signal from the signal generator and the yellow trace is the response generated by the PIO on the output pin. You can see that the rise times are not insignificant compared to the measured time interval.

Signals on the input and output pins on the RP2040.

In the end this was an interesting exercise. I was surprised by the performance of MicroPython on the Raspberry Pi Pico and how quick the development setup is. I honestly expected Python code to run slower and I was again reminded that my intuition can be wrong sometimes. Unfortunately I didn't have time to setup the C SDK to also try out a native implementation of the same test on the RP2040. Perhaps some other day.

Programmable I/O is certainly the most interesting part of the RP2040. It took me a while to understand the unusual instruction set and how the FIFO buffers work. I like how the integration of the assembler into MicroPython makes it easily accessible for experimentation. I was impressed by the performance and quick response times. On the other hand, I was also surprised by how limited PIOs are in terms of the program size and the choice of instructions. I was expecting something similar to PRUs on the Sitara SoC. PIOs seem indeed very specialized devices for interfacing with digital buses and can't do much more in terms of algorithmic complexity.

Posted by Tomaž | Categories: Digital | Comments »