Software defined radio with Raspberry Pi (Part2)

slb   February 10, 2019   Comments Off on Software defined radio with Raspberry Pi (Part2)

In my previous post, I described how I stumbled into RTL-SDR and connecting it to a Raspberry Pi.  It wasn’t working well for me due to network lag, hiccups, and sample rate limitations.  I didn’t want to give up, and the code looked like it could be improved.   This post describes some of the changes and the results.

The goal

The goals I had were to:

  • Move the RTL-SDR dongle to a raspberry pi connection in a closet, far away from my desktop with the GQRX software & GUI.
  • Utilize as much of the bandwidth of the RTL-SDR dongle as possible.
  • Hopefully enable all of this over wifi, because an ethernet cable limits the placement of the antenna/pi, and the PiZeroW doesn’t even have an ethernet interface.

What I changed

From the source, rtl_tcp.c uses two threads.  One receives a call-back from the RTL-SDR library any time there is new data from the USB dongle.  The other thread is supposed to take any new data and write it to the TCP socket so the kernel can transmit this data over the network to GQRX (or whatever client).

I decided to rip out the semaphore between the TCP and USB threads — remove all locking.  I also removed the linked list structure and allocation / deallocation entirely, so that none of this code would have to call malloc() or free() in the performance path. .  I replaced these with a single statically allocated circular buffer.  Both threads would interact with the data through a non-blocking polled-mode interface.   The USB-reader thread writes data into the circular buffer at head, and the TCP worker reads/transmits data from the same circular buffer at tail.   Neither thread touches the same variables, they only each update head or tail themselves.

The USB Worker:

 if ((ringbuf_head+len) < (unsigned int)ringbuf_sz)
 {
      memcpy(((unsigned char*)(ringbuf+ringbuf_head)), buf, len);
 }
 else
 {
      memcpy(((unsigned char*)ringbuf+ringbuf_head), buf, ringbuf_sz-ringbuf_head);
      memcpy((unsigned char*)ringbuf, buf+(ringbuf_sz-ringbuf_head), len-(ringbuf_sz-ringbuf_head));
 }
 ringbuf_head = (ringbuf_head + len) % ringbuf_sz;

The TCP Worker:

 unsigned int sendchunk;
 if (ringbuf_tail < ringbuf_head)
     sendchunk = ringbuf_head - ringbuf_tail;
 else
     sendchunk = ringbuf_sz - ringbuf_tail;
 bytessent = send(s, (unsigned char*)(ringbuf+ringbuf_tail), sendchunk, 0);
 bytesleft -= bytessent;
 ringbuf_tail = (ringbuf_tail + bytessent) % ringbuf_sz;

With these changes, both threads are free to run as fast as they can.  Ideally, the network can keep up with the USB data stream coming in.  So the TCP worker thread should be reading data just behind the USB thread for the lowest latency.   If there is a ‘hiccup’ and the TCP thread doesn’t get to run, or blocks for some reason, then the amount of data not yet transmitted in the buffer will grow.

In the worst case, so much data is in the buffer that the USB worker thread runs into the “tail” and cannot write any new data.  I decided in this case to trim the buffer, and move the tail ahead.  This would result in the oldest data being lost.  But this is real-time information and I figured it was better to stay current than introduce a lag that could never go away.

The results

The performance improvement was tremendous!   Ditching ethernet, the Raspberry Pi 3B+ was able to do the maximum bandwidth of the RTL-SDR at 3.2Million samples per second, even over wifi.  This is a sustained 53Mbps over wifi, not bad.   Even at this maximum value allowed by GQRX, the lag is never more than one data chunk from the USB.  If there’s an occasional blip on raspberry pi that pre-empts the TCP sender, it quickly recovers and never gets more than 2-3 chunks behind.   Changing channels is now instantaneous even at the highest bitrate.  And switching to Wifi means that I can simply stick the Raspberry pi to the antenna assembly and place it anywhere for the best reception!

I also tried it with a Raspberry Pi Zero W.  I was immediately able to jump around frequencies with very little (1/4 second) lag at 1.9Million samples per second.  It was very stable, with about 4-6 data chunks in the circular buffer at any time.   I was not however able to extend beyond 1.9Msps.  When I did this, the Raspberry Pi Zero W CPU was overloaded and couldn’t keep up with the data rate.  But at a reasonable data rate, the lag is always zero.

Also, the “ll+ now (##)” messages are gone :). I replaced those with a 30-second update on the overall bitrate and the number of bytes that are inflight between the USB dongle and the TCP socket.

I did do some “stress testing” and reduced wifi reception or overloaded the Raspberry Pi Zero W.  Listening to FM radio was still “OK”, but every 10 seconds or so there was a “jump” when it would truncate the buffer.  The lag was at worst about 3 seconds with this design.  In the previous design, the lag would sometimes extend to 15+ seconds, which was so long you weren’t sure any change took effect or not.

Summary:

  • Raspberry Pi 3B+
    • Before: Could only support up to 1.92Msps over wifi with 1-2 seconds of lag.  Ethernet could do 2.4Msps.
    • After: Able to set the sample rate to the dongle maximum of 3.2Msps over wifi with instant channel changes.
  • Raspberry Pi Zero W
    • Before: Couldn’t get above 0.92 Msps, and the lag was often 5-10 seconds.  Not stable.
    • After: Able to achieve 1.92Msps with very good lag (feels like about a quarter second).

I really enjoyed jumping into “software defined radio on the cheap” and can’t wait to try out other extensions or applications that can use this data.  I’ve heard SDR# is a really good application, albeit closed source.  I just need to find a windows machine.  Given the improvement and the other people and blogs citing these issues, I will probably submit a PR soon with the changes in case it helps anyone else.

Update: I submitted a PR.  https://github.com/osmocom/rtl-sdr/pull/6