Receiving RDS with the RTL-SDR

Update 12/2016: Redsea has seen a lot of development since this post was written; see Redsea 0.7, a lightweight RDS decoder.

redsea is a command-line RDS decoder. I originally wrote it as a script to decode RDS from demultiplexed FM stereo sound. Later I've experimented with other ways to read the bits, and the latest addition is to support the RTL-SDR television receiver via the rtl_fm tool.

Redsea is on GitHub. It has minimal dependencies (perl core modules, C standard library, rtl-sdr command-line tools) and has been tested to work on OSX and Linux with good enough FM reception. All test results, ideas, and pull requests are welcome.

What it says

The program prints out decoded RDS groups, one group per line. Each group will contain a PI code identifying the station plus varying other data, depending on the group type. The below picture explains the types of data you'll probably most often encounter.

[Image: Screenshot of textual output from redsea, with some parts explained.]

A more verbose output can be enabled with the -l option (it contains the same information though). The -t option prefixes all groups with an ISO timestamp.

How it works

The DSP side of my program, named rtl_redsea, is written in C99. It's a synchronous DBPSK receiver that first bandpass filters ① the multiplex signal. A PLL locks onto the 19 kHz stereo pilot tone; its third harmonic (57 kHz) is used to regenerate the RDS subcarrier. Dividing it by 16 also gives us the 1187.5 Hz clock frequency. Phase offsets of these derived signals are adjusted separately.

[Image: Oscillograms illustrating how the RDS subcarrier is gradually processed in redsea and finally reduced to a series of 1's and 0's.]

The local 57 kHz carrier is synchronized so that the constellation lines up on the real axis, so we can work on the real part only ②. Biphase symbols are multiplied by the square-wave clock and integrated ③ over a clock period, and then dumped into a delta decoder ④, which outputs the binary data as bit strings into stdout ⑤.

Signal quality is estimated a couple of times per second by counting the number of "suspicious" integrated biphase symbols, i.e. symbols with halves of opposite signs. The symbols are being sampled with a 180° phase shift as well, and we can switch to that stream if it seems to produce better results.

This low-throughput binary string data is then handled by redsea.pl via a pipe. Synchronization and error detection/correction happens there, as well as decoding. Group data is then displayed on the terminal, in semi-human-readable form.

Future

My ultimate goal is to have a tool useful for FM DX, i.e. pretty good noise resistance.

43 comments:

  1. You do know about the existence of GnuRadio, right?

    Anyhow, your still awesome :)

    ReplyDelete
    Replies
    1. Yes, I know about the existence of GNU Radio.

      Delete
  2. @Anonymous: "GnuRadio is a really great program easily worth a thousand bucks. But it was designed to run on $500 computers with $500 SDR hardware. Where as this is made for $20 SDRs plugged into $20 computers." http://kmkeen.com/rtl-demod-guide/

    ReplyDelete
    Replies
    1. GNU Radio works fine with rtl-sdr hardware. The GNU Radio conference gave them to attendees last year to help people get started with GNU Radio.

      Delete
  3. It was good post/project before the stunning gfx. However, your waveform picture are always a treat; I had to come back. Thanks for writing.

    ReplyDelete
    Replies
    1. Yeah, the waveform graphic is beautiful and very clear! I second this :)

      Delete
  4. very good! I search a project for auto-log (for example in file text or csv) all picode and ps in fm band by scannering 87.50-108.00.
    Cris.

    ReplyDelete
  5. Hello, i really like the content of your blog. One post on your blog is more interesting than 90% of the web content.
    I haven't (yet) received my rtl-sdr. If one day you have free time , can you post a raw sample of FM + RDS at baseband ?
    I know it's big, I do not want you if you can not. I would expect my rtl- sdr at the end of the month.
    Sorry for bad english, google trad is my unique friend.

    Thanks for your posts.

    ReplyDelete
  6. There ought to be an open-source version of RDS...

    ReplyDelete
  7. Hi, I just tested redsea with a R820T TCXO dongle using
    perl readsea.pl -g 28 97.0M. The R820T is recognized,
    but I don't see any decoding nor error messages.
    If I plug just the same device to a Win7 running SDR# with gain 28
    I see RDS decoding for 97.0 instantly. My hardware is not
    the latest. Do you have any suggestions?

    ReplyDelete
    Replies
    1. The noise tolerance could be better. Is this a mono station?

      Delete
    2. Hi, I tested two clear and strong FM stations in Germany.
      Both are stereo, both are negative for decoding
      I tested the whole range of gains. I tested this with LNA and without.
      I am running Ubuntu 14.04 LTS 32 bit and using a 7 year old Lenovo T60.
      Other RTLSDR applications, such as ACARS, ADSB, GQRX are running well.

      Delete
    3. Hi, after a bunch of software updates redsea now runs beautifully on the same system. I am impressed . Well done.

      Delete
  8. Marcos Sanchez17 April, 2015 00:57

    Hi, Oona, nice article and blog!

    I'd like to get one of those RTL receivers, but there are a lot of different models and I'm somewhat confused. Could you give me some advice? I just want it to be linux compatible and with good enough reception.

    Thanks a lot!

    ReplyDelete
    Replies
    1. There are some lists online, for instance on r/RTLSDR.

      Delete
  9. Hello, can you explain the recent changes (and why did you make them) to the pll loop filter ?

    Thanks for your posts.

    ReplyDelete
    Replies
    1. My method of loop filter design is currently "design by herp derp" - I don't have any design tools, I just keep trying nearly every possible option. Almost. -F 9 seems to be a better filter than -F 0.

      Delete
    2. Thank you, i was just curious about global pll behaviour when tuning filter loop characteristics.

      Delete
  10. I am running Ubuntu 14.04 LTS 64 bit. Other RTLSDR applications, such as ADS-B, GQRX are running well. But redsea return no RDS data. Any idea?

    ReplyDelete
  11. With all this recent meteor shower activity, what's your thoughts on using rtlsdr in conjunction with redsea to pick up RDS from meteor scatter? My thoughts are, as standard, rtl sdr dongles might not be sensitive enough, and there might not be enough time for your software to "lock on" to the signal.

    ReplyDelete
    Replies
    1. On top of that, the noise resistance of redsea is pretty bad.

      Delete
  12. Very nice project! I've tested it on atom mini itx mainboard with ubuntu 10.04 and it works nicely :) It would be very cool to "marry" your code with this one: http://comments.gmane.org/gmane.comp.mobile.osmocom.sdr/299 . It's a rtl_fm modification which enables fm stereo reception. I've tested it and it works. Then we can have lightweight fm stereo+RDS receiver. Keep the good work, you're amazing :)

    ReplyDelete
  13. Great project. I just tested it on a little netbook (dell mini 1018) with a rtl-sdr usb dongle... On debian 8.2.
    It works great.
    I can understand and see how RDS works... my next step is to get TMC; but it seems more difficult... (I also saw your article about that... but did not test anything about TMC)
    Thanks

    ReplyDelete
  14. A few hints for those trying to figure out the code: It's worth mentioning that the code no longer matches the description above - in particular, the pilot tone stuff has been removed. Also, the subcarrier filter has been changed and no longer matches the mkfilter command line given in the code - the order is now 2 not 10.

    ReplyDelete
  15. Hi, thank you for the nice post. I am not a DSP expert. Nevertheless, I have a few comments on your PLL. In your C-code you calculate the phase error:
    d_phi_sc = 2*LP_PLL(bb0*bb1)
    with bb0 = LP(sample[i]*cos(sc_phi)) approx. cos(d_phi)
    and bb1 = LP(sample[i]*sin(sc_phi)) approx. -sin(d_phi)
    Here sc_phi is the phase of your PLL signal and LP and LP_PLL are your two low pass filters. I assume the signal as sample[i] = cos(phi[i]) which gives you after mixing (=multiplication) + filtering the sin or cos of the phase difference d_phi. With this your d_phi_sc should converge towards 2*0.5*sin(2*d_phi). I think you assume d_phi is small and use sin(2*d_phi) ~ 2*d_phi. But this is wrong as long as your PLL is not locking. I did a simulation of the signal which confirms this. Additionally, on a real signal I see it sometimes locking and sometimes not. Without having tested it I would suggest to use:
    d_phi_sc = atan2(-bb1,bb0)
    which gives you the true phase error in all quadrants. Additionally, your small locking bandwidth and the large phase shift introduced by your filters (especially your 2.4kHz filter) could be improved. Maybe only one filter before the atan2 might be needed? Greetings, Andreas

    ReplyDelete
    Replies
    1. Thanks, very useful insight! I struggle(d) with PLL filters and the ones used here are not based on any thorough planning, to be honest. I'm writing a new version of redsea so I will take this into account.

      Delete
  16. Hello Oona.
    I have a problem. I am using ubuntu 14.04 and I think that your redsea decoder could solved that without 2 much complicating with GNU radio: I would like to scan fm airband and from couple of captured stations I want to parse only radiotext group (one completed line per station), and then simultaneously by some database such as couchDB (apache) or maybe would be better RDBMS(Relational Database Management System) to store radiotext. And I have no idea how to start with even one single simple entry of radiotext being stored into database. Any kind of advice would bring me a lot of happiness, since I have very little of programming skill.

    And also, for trimming only one line of the radio stream I was wondering if i could help myself -- with following if statement:
    if (rt_.isComplete()){
    printf(",\"radiotext\":\"%s\"",rt_.getLastCompleteStringTrimmed().c_str());} -- but i don't know where to put it and if does it make any sence or how to modify the script to do that.

    Thank you for your time

    ReplyDelete
    Replies
    1. This Linux command would extract only the first fully received RadioText field at 89.2 MHz (it uses jq):

      ./rtl-rx.sh -f 89.2M | grep "\"radiotext\"" | jq '.radiotext' | head -n 1

      Delete
    2. Oh, okay. Thank you. But now I see the one problem. I am trying to decode 2B and 0B groups also, some of the radios uses B also as you know I suppose. So where should I change the source code to solved this problem? I mean I get the 2B groups but without decoded text, but I am having bad radio reception at where I am using it - right now. Does redsea decode B groups also? I am confused, sorry

      Delete
    3. I think the problem is at reception power(gain), but I might be wrong. I just check the code and there is definitely decoding for B groups also involved, but where I see the problem is that when I check the exact radio station with the 'gqrx' radio app which has also rds decoder It gets all the demand information also as fast as other radio that has 2A groups instead(which i get also immediatelly). But the reception power is weaker on the radio station (using 2B groups - in a way that varies from -3dBFS to the -11dBFS, it has some frequencies nearby, mybe thats the problem but it gets ful of values of rds at the end)

      So from the redsea side I get tuned (to avoid DC spike) a little different frequency then gqrx(using hardware frequency which is a little lower then redsea is set (only +103k). So from the redsea i get only pi code in the issued radio station (with groups of Bs). No matter how much time i wait. The radio station with groups of As
      i get the all information almost immediately. And now I am confused as you also i suppose. I write too much and complicate. Forgive me. I just like app pretty much, not gonna lie, very simple and useful for my purpose. So I am asking to help me please.

      Hope i was enough exact with my explanation to understand me, other wise please let me put this problem in another way that you will definitely understand.

      thank you once again Oona

      Delete
    4. Yes, redsea is sensitive to noise. This is a known issue, and any ideas are welcome: https://github.com/windytan/redsea/issues/30. More about the recent versions in this blog post.

      By the way, I've never actually received a station that uses 2B groups. If you do receive a good signal at some point it'd be interesting to see a minute or two's worth of output using the -x flag.

      Delete
  17. thank you for fast reply. So your code doesn't decode 2B and 0B groups? (which is the same as 2A and 0A) Can I change that? Because this 2B uses for radiotext and i need that very bad, but the problem might be, because the fragment of '2B' is 4B long not 2B as group of '2A'. But it migh be the problems with the sampling mybe also? Once i remember that at gnu radio receiver didn't show me the rds information (at least radiotext) only the pi code like in your example - if i encrased the sample rate bigger then 1.2MS/s. So might be with the problem also here I don't know. But as I said I get rds information with gqrx app which is similar to yours only that i can't decode 2B group which is radiotext. And yes 2B and 0B uses to transmit radiotext and ps as well: http://www.nrscstandards.org/nrsc/NRSCFiles/RUWG/RUWG%20archive/PS%20specification%20-%20updated%20RDS%20Standard.pdf
    http://www.nrscstandards.org/DocumentArchive/NRSC-4%201998.pdf

    So how can I help myself, any idea very please?
    Thank you
    Regards

    ReplyDelete
  18. if you just get me a advice how to change source code or maybe just a script would be very helpful. Now I have some backup programming skill by another person who is helping me right now, with this case of yours. Which is very useful, i just need the decode also 0B, and 2B groups. Sorry for being annoying, but it is very important to me. And once again, thank you for this very easy to use and useful program

    ReplyDelete
    Replies
    1. Please do this:
      1. Tune to a frequency that transmits 0B and 2B groups.
      2. Start redsea with the -x switch.
      3. Let it run for a minute or two.
      4. Copy and paste the output to pastebin.
      5. Write the pastebin link here, in the comments section.

      This way I have some example data to work on. I don't have any test data on 0B and 2B groups, as I told above. 0B and 2B groups are not very common, and I don't have any test data to work on. Redsea already has support for 0B and 2B groups, but if it seems not be working, I need some test data.

      Delete
    2. https://bpaste.net/show/abe9b29ddd11 <-what redsea gets in hex;
      https://bpaste.net/show/a6350652a69d <-decoded with json

      0B should be decoded as "KRKA" and 2B should be decoded as "PRIJETNO POSLUSANJE RADIA KRKA" (and by the way as I said it always the same name for radiotext if that matters anything).
      Please chech this for saving some time mybe-> already part of decoded hex PS + RT to strings https://bpaste.net/show/5879f0dbc044

      Delete
    3. Thanks, very valuable data. I've fixed this issue now; the current version on GitHub correctly decodes group 2B radiotext.

      Delete
    4. Oh my Lord, Oona I love you. I will be in touch. I think I have something for you in exchange - not related with programming though :) Cya

      Enjoy

      Delete
    5. Yesterday I was so ethusiastic I haven't checked the ps at all, when I saw the radiotext, so later I just realized I was thinking: "Oh shoosh I really need this important data of station name, so I got scared and thinking about it, if you had so much work to work on all that source code (which I can imagine) or did you forgot". Today I woke up and after the breakfast I just wanted to bother you with my annoyance, and just check out your site and I've seen that you just updated "25minutes ago" and I just switch state of mind from the being too concerned into ecstasy again hehe :) Thank you again Oona, It means a lot to me. It will serve very efficiently. I'll keep you post it. You made my day again :) Thank you, once again.

      Regards

      Delete
    6. So glad you've found it useful :) No need to be scared, I like writing code.

      Delete
  19. and by the way, what do you mean exactly with "-x flag" which one to be exact?

    ReplyDelete
    Replies
    1. Start the program with this command:

      rtl_fm -M fm -l 0 -A std -p 0 -s 171k -g 40 -F 9 -f 87.9M | ./src/redsea -x

      Remember to replace 87.9M with the frequency that transmits 0B and 2B groups.

      Delete

The comments section is pre-moderated; it will take some time for the comment to show up.

You might want to check out the FAQ first.