RTL2832U and rtl-sdr on an STM32 microcontroller, Part 1
This is a series dedicated to describing the development of software-defined-radio applications on a microcontroller, utilizing the ubiquitous RTL2832-based DVB-T dongle and librtlsdr. See justification and all of the [source code for this project on GitHub][github].
In this entry, I explain how the [Realtek RTL2832U DVT-B dongle][rtl2832u] is integrated with the microcontroller processor target, culminating in a successful enumeration of the device and demonstration of a few radio features. These tasks are accomplished via the integration of various codebases and porting a host-based USB library (libusb) to the new processor target. To begin, I create a bare-metal firmware image consisting of the the minimum components necessary to accomplish this task: a board support library to control the dev board hardware, the [librtlsdr][librtlsdr] library, minimally-ported [libusb][libusb] library and some application glue logic. In doing so, I’ll use a strategy of building-up from known baselines; Particularly useful for embedded projects where even non-software related issues can be a factor.
The development environment consists of the test rig hardware, build toolchain and various software and hardware debug tools. The test rig is comprised of an [ST STM32F7-Discovery][stm32f7-discovery] processor development board, the Realtek radio dongle and a VHF antenna. All software is cross-compiled with the ARM GNU toolchain, using a series of makefiles to automate and organize the process, and the target is manipulated using OpenOCD as a GDB server. Many other tools and techniques are employed throughout this project, such as static analysis, trace tools, ARM semihosting, symbolic debugging, and a USB traffic analyzer- each of which I will attempt to further describe along the way.
Enumerating a high-speed USB device with the target processor operating as host #
In this configuration, the microcontroller (MCU) acts as the USB host, servicing the radio dongle, which means the MCU is scheduling all of the traffic and then notifying the application software upon completion of certain events (ie, completion of data transfer). The very first thing we want to prove, is simply that the MCU can transfer any amount of data successfully, validating our hardware, physical connections and some of the USB driver code provided by the processor manufacturer. The data to verify is the USB enumeration process (which basically is how the host becomes aware of the attached device and figures out what its capabilities are). This transfer always occurs on the USB endpoint 0, using control transfer types. These are very simple frame transfers that exchange identification data such as the vendor identification and product identification codes (VID/PID), as well as, more verbose strings describing the model name, (ie “Realtek RTL2832U DVT-B Adapter”, or something like that, hardcoded in the device).
Before we fire up our demonstration application, it would be best to know what we’re looking for, and have a way to verify what is actually happening ‘over the wire’, in case things don’t go as planned. For this, we turn the to [Total Phase Beagle USB Protocol Analyzer][beagle]. The Beagle is placed between the target host and the RTL dongle, with a third interface to a workstation for analysis of the real-time traffic. If you are embarking on any serious USB development work, I highly recommend this tool! The Beagle verifies that the device is receiving power via the VBUS from the host and also captures all of the bus traffic to ensure the transactions occur correctly, as depicted in the diagram below (for all of the gory detail, check out the USB spec, or [USB In a NutShell][usb-nutshell] for more in-depth, yet very readable, descriptions of how USB packets are defined).
After capturing several initialization sessions, it should become immediately clear which parts, if any, are failing. Below are the exact steps I have taken to verify a successful device enumeration on the STM32 host.
Step 1: Setup a development environment #
- Find and build reference code which runs the processor in a USB Host mode. Development starting from a known-working USB-Host application on the target processor will probably help. In this case, I chose the Cortex-M7 based STM32F7 development kit (primarily, because it ships with a High Speed USB interface out of the box). The reference I started with can be found in the STM32CubeF7 package (STM32Cube_FW_F7_V1.4.0/STM32746G-Discovery/…/MSC_Standalone).
- In the reference application code, be sure to turn on USB middleware debug output, found in [usbh_conf.h][usbh_conf.h], by setting the macro
USBH_DEBUG_LEVEL
to 3. - Enable semihosting output in OpenOCD. This redirection of writes to
stdout
will be how the USB descriptor data is verified. - Setup trace capability and enable in OpenOCD. Configuring and using the ITM is well worth the effort if execution time profiling is valuable to you. By sending the processor DWT:CYCCOUNT register out the high-speed ITM module, I am able to zero in on very difficult bugs much faster than I would with only the semihosting capability.
Step 2: Bring in librtlsdr and [get it to build][librtlsdr-mods] for the embedded target #
- Include [libusb.h][libusb.h] as-is, so the API is defined for features used within the librtlsdr.c file.
- Statically define the rtlsdr_dev instance in librtlsdr.c to avoid any dynamic allocation.
- Replace
usleep()
withHAL_Delay()
. - Remove some dead code.
Step 3: Implement libusb routines, as necessary #
- Fill in libusb functions to call STM32 USB library functions. Notably,
libusb_control_transfer
(to enable the enumeration process and control of the radio features) andlibusb_bulk_transfer
(for raw radio data recovery). Refer to the STM32Cube USB device library for details on the USB middleware API.
For the control transfer implementation, I used in the libusb function arguments to modify the USBH_HandleTypeDef
structure, fed to the USBH_CtlReq()
function.
int libusb_control_transfer(...) {
hUSBHost.Control.setup.b.bmRequestType = request_type;
hUSBHost.Control.setup.b.bRequest = bRequest;
hUSBHost.Control.setup.b.wValue.w = wValue;
hUSBHost.Control.setup.b.wIndex.w = wIndex;
hUSBHost.Control.setup.b.wLength.w = wLength;
do {
status = USBH_CtlReq(&hUSBHost, data, wLength);
} while (status == 1);
...
The libusb bulk transfer function is redirected to the USBH_BulkReceiveData()
ST middleware routine.
int libusb_bulk_transfer(...) {
USBH_BulkReceiveData(&hUSBHost,
data, // rx buffer
length, // data count to rx
pipe);
...
Step 4: Write a simple application to prove the concept #
The demo firmware image is extremely simple. After all normal initialization steps have been completed, simply create the inbound data pipe for the control frames (on inbound endpoint 0x81
), then attempt to open the control pipe.
InPipe = USBH_AllocPipe(&hUSBHost, USB_PIPE_NUMBER);
USBH_StatusTypeDef status = USBH_OpenPipe(&hUSBHost,
InPipe,
USB_PIPE_NUMBER,
hUSBHost.device.address,
hUSBHost.device.speed,
USB_EP_TYPE_BULK,
USBH_MAX_DATA_BUFFER);
Success #
As seen in the terminal output below, the application is able to retrieve some descriptor strings that seem very promising. The manufacturer and full product identification strings are evidence of successful data transfer via the USB control pipe.
Now that the device enumerates and is communicating successfully with the MCU, perhaps some of the application functions will work? The following dongle setup procedure is taken from some of the example applications in the librtlsdr codebase, with [librtlsdr.c modified gently][librtlsdr-mods]:
// open device
rtlsdr_open(&dev, 0);
// Set the sample rate
verbose_set_sample_rate(dev, adc_samp_rate);
// Set the frequency
verbose_set_frequency(dev, tuner_frequency);
// set auto gain
verbose_auto_gain(dev);
// set ppm error
verbose_ppm_set(dev, tuner_ppm_error);
// Reset endpoint
verbose_reset_buffer(dev);
Finally, read from device:
rtlsdr_read_sync(dev, dest_buffer, usb_pkt_bytes, dev_id);
After each execution of that last line, I am able to examine the the dest_buffer memory location and verify that data is changing. This hints that the bulk data transfer operation is also possibly working. Now, hopefully there is an FM radio station in there somewhere…but I’ll save demodulation for another post.
Next steps #
- Attempt to receive radio data from the dongle and figure out if it’s correct. For instance, up to this point, I have no idea if the buffers are complete, overflowing, dropping packets, or if even the tuner is configured correctly. All I have now is a binary blob- hopefully there is a good radio signal in there! This is maybe the most difficult part.
- Setup some reliable way of playing raw audio output, in hopes of one day verifying the demodulated radio signal.
[librtlsdr-mods]: https://github.com/thenatefisher/stm32_rtlsdr_fm_radio_receiver/commits/master/source/resources/librtlsdr/librtlsdr.c
[usbh_conf.h]: https://raw.githubusercontent.com/thenatefisher/stm32_rtlsdr_fm_radio_receiver/e5f3b2e28a548f009d1aecbbc5f2b8981e782e4b/source/application/usb/usbh_conf.h
[trace.c]: https://raw.githubusercontent.com/thenatefisher/stm32_rtlsdr_fm_radio_receiver/e5f3b2e28a548f009d1aecbbc5f2b8981e782e4b/source/application/trace.c
[libusb_port.c]: https://raw.githubusercontent.com/thenatefisher/stm32_rtlsdr_fm_radio_receiver/e5f3b2e28a548f009d1aecbbc5f2b8981e782e4b/source/application/usb/libusb_port.c
[libusb.h]: https://raw.githubusercontent.com/thenatefisher/stm32_rtlsdr_fm_radio_receiver/b1979ca35e1bcaea85e5425ae7a55932607fb969/source/application/usb/libusb.h
[beagle]: http://www.totalphase.com/products/beagle-usb480/
[github]: https://github.com/thenatefisher/stm32_rtlsdr_fm_radio_receiver
[librtlsdr]: http://sdr.osmocom.org/trac/wiki/rtl-sdr
[libusb]: http://libusb.info/
[stm32f7-discovery]: http://www.st.com/content/st_com/en/products/evaluation-tools/product-evaluation-tools/mcu-eval-tools/stm32-mcu-eval-tools/stm32-mcu-discovery-kits/32f746gdiscovery.html
[usb-nutshell]: http://www.beyondlogic.org/usbnutshell/usb4.shtml
[rtl2832u]: http://www.realtek.com.tw/products/productsView.aspx?Langid=1&PFid=35&Level=4&Conn=3&ProdID=257