RTL-SDR FIR filters

A question came up in the stream1090 thread about whether the FIR filters in the RTL-SDR dongle change with the sample rate. It turns out there are two cascaded FIR filters in the RTL-SDR dongle. The first filter operates at 28.8 MHz sample rate and has a first null at 2.2 MHz. A freq response plot of that filter was shown by @obj. The second filter does depend on the sample rate. This is all explained in that link I previously provided:

See the section labeled “Filtering” in that link.

The author of that paper shows what happens when you select a sample rate of around 2 MHz and also what happens at a very low sample rate of 256 kHz. The results for 2 MHz look good but the 256 kHz sample rate has extra responses at multiples of 1 MHz.

Another question is whether the first filter at 28.8 MHz sample rate could be optimized for the ADS-B waveform. This would be similar to the filter optimization that @mgrone is doing on stream1090. The optimization would be more difficult to do here since these FIR filters are in hardware rather than software.

1 Like

Oh interesting, good find. The apparent repetition of the fixed filter looks like aliasing, and implies that the 2832 is doing something approximately like:

ADC samples at xtal
→ decimate to Fs*4 using user-provided FIR running at xtal frequency.
→ decimate to Fs (i.e. fixed decimation factor of 4), using a hardwired FIR running at Fs*4 with a cutoff around Fs/2

Here’s another link to information on the RTL-SDR digital filtering.

I hesitated in linking this, as it is somewhat confusing. However there is block diagram in there that shows the decimation in two steps: probably by 3 and then 4 to get to 2.4 MHz from 28.8 MHz. Confirming what you said above.

It’s been a long time since I last played with this, and never in an ADS-B context. The best performance was always with settings at less than 2.048MHz, and if memory serves, the best setting was around 1.024MHz (or 1.0MHz, one of the RTL-SDR options in SDR#).

As for additional decimation, in software, I never noticed any benefit.

Ran into a snag when I attempted to modify the FIR filter coefficients in librtlsdr.c . It turns out that when you recompile librtlsdr you won’t see the modified version. That’s because when dump1090-fa was installed, it also installed librtlsdr and that’s the version that is used. Don’t know how to resolve this. Anyone run into a similar situation? TIA

The LD_LIBRARY_PATH environment variable is probably what you want here (any directory you set there is prepended to the shared library search list)

You can check how shared libraries are being resolved at runtime by running ldd `which dump1090-fa`

Thanks. So according to AI, I would use LD_LIBRARY_PATH as follows:

3. Set it for a Single Command 
The most isolated and recommended way to use LD_LIBRARY_PATH is to set it on the same line as the command you are running. This ensures the setting applies only to that specific command and not subsequent commands in the same session.
LD_LIBRARY_PATH=/path/to/your/custom/libs ./your_program

So I tried
LD_LIBRARY_PATH=/home/pi/rtl-sdr/build/src/librtlsdr.so /usr/local/bin/rtl_test -s 3.4e6
However, when I tried that there was no change in how rtl_test runs. As a matter of fact I could use any path and the results were the same. Am I misunderstanding what AI is saying on this?

Edit: Ok you have to follow exactly what AI says and use the ./ form to run the program. So CD into rtl-sdr/build/src/ and run:

> LD_LIBRARY_PATH=/home/pi/rtl-sdr/build/src/librtlsdr.so ./rtl_test -s 3.4e6

That worked! Thanks again @obj

I think you have something else going on here. The binary in build/src likely has a sopath set to use the version of the library in the build tree, which is why it grabs the right version when run from there (this is, IIRC, a thing that cmake does, specifically to make it easier during development). When the binary is installed, that sopath setting gets removed and it goes back to using the system search path, which is why the installed copy in /usr/local/bin gets the wrong version. dump1090-fa is likely the same, using the system search path.

LD_LIBRARY_PATH needs to be a directory containing the library, not the full path to the library, which is probably why it didn’t work right for you. Try setting LD_LIBRARY_PATH=/home/pi/rtl-sdr/build/src/

Yes, that works.
However, I also found out I can just use ./rtl_test (from the folder containing the rtl_test program and.so file of course) without using LD_LIBRARY_PATH and that also works – that is, the correct library is used.

In summery you can do:
LD_LIBRARY_PATH=/home/pi/rtl-sdr/build/src/ rtl_test
from anywhere. Or you can do: ./rtl_test from /rtl-sdr/build/src/ folder.

I would like to change the FIR filter coefficients (taps) in the RTL-SDR dongle. I could recompile each time to make a filter change in librtlsdr.c. However a much faster way would be to add a command line argument to rtl_sdr to load a file with filter taps, This would require modifying rtl_sdr.c librtlsdr.c and the header file rtl-sdr.h.

For me, making the above changes would take a fair amount of time. But maybe someone has already done this. I looked at RTL-SDR GitHub and there are 351 forks. Looking through the list of forks, it’s very difficult to figure out what was changed in any given fork. Is there a way to determine this without opening each fork?
Thanks.

Have you asked copilot something like this? “Can i patch librtlsdr to expose rtlsdr_set_fir() publicly?” You might have to insist a little.
Can you check if you can open this link to the conversation?

  • link removed

Edit: Nevermind, not working. You have to ask copilot yourself.
This is the part you need to expose to the public interface:

I tried chatGPT, Described what I wanted to do. It came up with some code that was pretty superficial. It told me how to change the command line interface (that’s the easy part). Then how to do the FIR filtering. (That wasn’t asked for, because that’s done in hardware). It didn’t know about librtlsdr.c and how it would be used to load the filter coefficients. I guess, how could it know about that. That would require knowledge about all of the source code for rtl-sdr. So that’s what I meant about it having only superficial knowledge about what had to be done.

So i cannot say anything about chatgpt.

Copilot does know about librtlsdr. There is however another problem. It also knows plenty about me and what i am doing. It keeps the context across conversations. So this was the result of a short side question (sorry for the screenshots)

This is how it started. You need to keep annoying the copilot. It was claiming that you cannot change things bla bla.

I consider this a not too bad for an answer.

Edit: However, you need to find your way with these “helpers”. They will lie to you in your face and present it in a way to make you believe it. Do not go with the flow of the AI. My most common words when talking to copilot are “No”, “Wait”, “Wrong” “Simply not true”, “Incorrect”, “We do this differently”. One other thing you have to remember: You need to build up context. You are not asking some random person “I want to change the internal FIR taps of the RTL chip in librtlsdr”. For librtlsdr and copilot, you can assume that copilot knows about it, but you have to explain your goal.

Actually, that last reply is not completely correct either. The filter is 32-taps symmetrical. So you only need to supply 16-taps. The taps are duplicated and reversed in order when they are loaded into the chip. The part about 8 and 12 bits is correct.

I don’t know how it would design a filter optimized for ADS-B. It doesn’t know the output sample rate being used.

To be clear, here is a block diagram of the RTL2832U:

We are talking about the taps for the filters labeled LPF in the figure. The ones running at 28.8 MHz sample rate. If we are setting an output sample rate of 2.4 MHz, the Resampler reduces the rate by 3 and the Fix FIR and decimator reduces the rate by 4, for a composite of 12. Not sure how AI can design a filter optimized for ADS-B when it doesn’t know about the filters in the Resampler and fixed decimator.

1 Like

Already too much for me. What is leaving the Resampler? What is then done in which order in Fix FIR & Decimation?

Is this setting the taps in Fix FIR?

The sample rate out of the Resampler is 4x the output sample rate that you set. So if the output sample rate is 2.4 MHz, the output of the Resampler is 9.6 MHz sample rate. The Fix FIR and decimation is just another lowpass FIR filter followed by a decimation of 4 (to the final output sample rate). This filter (or, I should say filters, I and Q legs) cannot be changed.

The only filter taps that can be changed are the ones in the blocks labeled LPF in the figure --the filters running at 28.8 MHz sample rate. Remember this is all in hardware in the RTL2832U chip.

1 Like

Got it. So LPF is basically doing right now what stream1090 does with the airspy input. Run two LPF’s independently on the I and Q branch.

1 Like

The method I planned to use to compare two sets of FIR filters is to run two RTL-SDR dongles simultaneously with the two sets of filters and collect I/Q data in files. Then run the two sets of data through dump1090-fa using the --stats option.That is: dump1090-fa --stats --ifile output.iq.

The setup is: half-wave vertical dipole antenna, uputronics filtered LNA, XRDS-RF 2-way splitter, two RTL-SDR dongles, Rpi 4. The antenna is inside the house on first floor – ie not very good, and therefore a small plane count. But that can be improved later.

First I figured I would run a sanity check with the same filters in the two dongles. The stats should be nearly equal. However, I was in for a surprise. The script for running the test is just:

rtl_sdr -d 00000978 -f 1090e6 -s 2.4e6 -n 240000000 -g 49 output1.iq &

rtl_sdr -d 00001090 -f 1090e6 -s 2.4e6 -n 240000000 -g 49 output0.iq

Notice the & at the end of the first line – both of those lines run at the same time. The I/Q data collection time is 100 secs in the above test. After collecting the data in output0.iq and output1.iq files, run these files through dump1090-fa as above.

I found that there could be differences in the stats up to 10% or more depending on the RTL-SDR modules used. Furthermore, the differences stayed with the RTL-SDR module, that is they are not random differences. Also it appears that older modules were worse than newer ones.

The following shows the stats outputs from dump1090-fa

Statistics: Sun Feb  8 21:48:29 2026 PST - Sun Feb  8 21:48:34 2026 PST
Local receiver:
     240000000 samples processed
             0 samples dropped
             0 Mode A/C messages received
       1883334 Mode-S message preambles received
         7647542 with bad message format or invalid CRC
          790533 with unrecognized ICAO address
            1186 accepted with correct CRC
  -37.4 dBFS noise power
  -22.9 dBFS mean signal power
  -12.7 dBFS peak signal power
      0 messages with signal power above -3dBFS
Decoder:
      1186 total usable messages
         292 DF0 messages
         120 DF4 messages
          10 DF5 messages
         347 DF11 messages
           3 DF16 messages
         412 DF17 messages
           2 DF20 messages
Statistics: Sun Feb  8 21:50:38 2026 PST - Sun Feb  8 21:50:43 2026 PST
Local receiver:
     240000000 samples processed
             0 samples dropped
             0 Mode A/C messages received
       1878925 Mode-S message preambles received
         7630601 with bad message format or invalid CRC
          788867 with unrecognized ICAO address
            1093 accepted with correct CRC
  -37.4 dBFS noise power
  -23.0 dBFS mean signal power
  -13.0 dBFS peak signal power
      0 messages with signal power above -3dBFS
Decoder:
      1093 total usable messages
         267 DF0 messages
         100 DF4 messages
           7 DF5 messages
         311 DF11 messages
           3 DF16 messages
         403 DF17 messages
           1 DF20 messages
           1 DF21 messages

1093 vs 1186 usable messages – almost a 9% difference.
Notice the signal levels are about the same, so that can’t explain the difference.
This data was from two fairly old RTL-SDR modules.(About 3 years old).

This kind of variability between RTL-SDR modules really surprised me. Possibly there’s some flaw in my testing methodology. If there is, please point it out.

Yea i had the same experience a few months ago. I have 3 noolec v5. They are not performing the same way. I went through all configurations with two of them. Cables x dongles x antennas x machines.

Btw: i am running currently the filter optimizer for RTL-SDR dongles. I was a bit of an idiot before. Your diagram got me thinking about why the filter stuff was not working for rtlsdr based dongles.

Where i messed up: librtlsdr will provide you with proper iq pairs. This is different from airspy where you need to fix the raw output. Applying these to the output of librtlsdr, that is, fixing something that do not need fixing, breaks it.

I introduced a simpler pipeline that only does

Low-pass I 
     |
     |--> magnitude --> upsampler
     |
Low-pass Q
1 Like