FM Radio using RDA5807M and MicroPython ======================================= .. image:: images/fm_radio_thumbnail.png :target: https://youtu.be/bj8MgL6k2tU Quick Links ----------- If you want to give it a go, you can find the code here: `FM Radio Code `_ RDA5807M -------- The RDA5807M is a highly integrated single-chip FM stereo radio tuner. The chip can be connected to a microcontroller using the `I2C `_ interface to control it. It includes everything you need to add an FM radio capability to a microcontroller project with only a couple of extra components. It can even directly drive a 32 ohm load (e.g. headphones) without using an external amplifier. There is a plentiful supply of very cheap modules based on this chip that make it easy to design an FM radio or to add an FM radio to another project. Apart from the chip itself, they include a crystal oscillator and a couple of capacitors, but not much else. The modules aren't a standard pitch, ideally, they would be soldered to a suitable PCB footprint, but with a bit of care, you can solder small wires to the pads. The pads are quite delicate and can easily fall off, so it is important to relieve any strain on the wires. The interface is very simple, it just needs power, an I2C connection, an antenna input and audio output to be connected. .. image:: images/fm_radio_rda5807m_module.png Breadboard Version ------------------ To put the RDA5807M module through its paces I built a simple "breadboard" FM receiver using a Pi-Pico. It doesn't have a dedicated user interface, I'm using a USB-serial connection to control the tuning and the volume. .. image:: images/fm_radio_breadboard.png Parts List ~~~~~~~~~~ Most parts are commonly available and could be purchased from multiple sources, but I have included links to some example components. + 1x `RDA5807M fm radio module `_ + 1x `Raspberry Pi Pico `_ + 1x 3.5mm stereo headphone socket + 2x 10uF electrolytic capacitor + 1x Battery holder 2xAA or 2xAAA MicroPython Library ~~~~~~~~~~~~~~~~~~~ This time, I thought it would be fun to build the project using MicroPython. The RDA5807M is doing all the work, so there's no need to squeeze every ounce of performance out of the device, so why not enjoy the power and simplicity of a Python programming environment? I wrote a simple Python library to control the RDA5807M using MicroPython's built-in I2C library. The Pi-Pico has hardware support for I2C if you use the dedicated pins. This is the preferred option for I2C control, but it is possible to use pretty much any pin using the software I2C implementation in MicroPython. The interface to both hardware and software I2C is the same, so the RDA5807 library works just as well with either. Antenna ~~~~~~~ For the antenna, I just connected 750 mm of wire to the antenna pin of the module to form a simple whip. Headphones ~~~~~~~~~~ The module is capable of directly driving a 32-ohm load, so we can drive a pair of headphones without any additional amplification. I just added a couple of 10uF DC blocking capacitors and a 3.5mm stereo socket. PSU Noise ~~~~~~~~~ The RDA5807 does need a low-noise PSU. The Pi-Pico uses a switched-mode voltage regulator to drive the 3.3v rail. The regulator automatically switches to a power-saving mode when it is lightly loaded. Unfortunately, the regulator has a great deal of ripple when operating in this mode. It is possible to override the power-saving feature of the regulator by setting the GPIO23 output high. It is worth doing this in any application that might be sensitive to PSU noise. Unfortunately, even after disabling the power-saving mode the 3.3v output from the Pi-Pico still had too much noise for the RDA5807M and it wouldn't receive any stations. For simplicity, I used 2 AA batteries to provide a low-noise power supply to the module. This worked great and the radio worked well. USB Serial Example ~~~~~~~~~~~~~~~~~~ I knocked up some simple example code to test out the radio using the USB serial port to control the device. .. code:: Python import sys import time import rda5807 from machine import Pin, I2C #disable power save mode to reduce regulator noise psu_mode = Pin(23, Pin.OUT) psu_mode.value(1) # configure radio module radio_i2c=I2C(0, sda=Pin(4), scl=Pin(5), freq=400000) radio = rda5807.Radio(radio_i2c) time.sleep_ms(1000) volume = 1 mute = False frequency = 88.0 radio.set_volume(volume) radio.set_frequency_MHz(frequency) radio.mute(mute) print("Pi Pico RDA5807 FM Radio Example") print("press ? for help") while(1): command = sys.stdin.read(1) if command == "?": print("Commands") print("========") print("") print("? - help (this message)") print(", - seek down") print(". - seek up") print("- - volume down") print("= - volume up") print("") if command == ".": print("seeking...") radio.seek_up() frequency = radio.get_frequency_MHz() print(frequency, "MHz") elif command == ",": print("seeking...") radio.seek_down() frequency = radio.get_frequency_MHz() print(frequency, "MHz") elif command == "=": if mute == True: mute = False radio.mute(mute) elif volume < 15: volume += 1 radio.set_volume(volume) elif command == "-": if volume > 0: volume -= 1 radio.set_volume(volume) elif mute == False: mute = True radio.mute(mute) time.sleep_ms(100) Installing Thonny and Micropython ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Thonny is one of the easiest tools to get started with MicroPython development on the Pi-Pico. Details about how to download and install Thonny can be found `here `_. Once Thonny has been installed, installing Micropython on the Pi-Pico is a simple operation. .. image:: images/install_micropython.png .. image:: images/install_micropython2.png Trying the serial example ~~~~~~~~~~~~~~~~~~~~~~~~~ By default, micropython will load and run the module called `main.py` in the root folder. Loading the example is simply a matter of transferring `serial_example.py` to the Pi-Pico, renaming it to `main.py`. You also need to transfer the `rda5807.py`. You can do this using Thonny, which also provides the ability to edit, run and debug the script or drop into the MicroPython interpreter. For convenience, I also added a simple script to transfer the necessary Python files to the Pi-Pico. It makes use of the `pyboard.py `_ utility to transfer the files to the Pi-Pico's flash-based file system. You may need to edit the serial port settings in the `send_serial_exampl_to_pico` script to match the serial port names on your system. .. code:: bash $cd 101Things/19_fm_radio/ $. send_serial_example_to_pico Once the Python modules are loaded, the example code is automatically loaded when the Pi-Pico boots. You can connect to the USB serial port using the screen utility, or another serial terminal application. The serial example uses simple commands to tune a station or adjust the volume. .. code:: bash $screen /dev/ttyACM0 Commands ======== ? - help (this message) , - seek down . - seek up - - volume down = - volume up seeking... 104.0 MHz seeking... 106.7 MHz seeking... 102.8 MHz Standalone Version ------------------ Now we have the radio functionality, we can think about building a standalone radio. There is lots of room for customisation, there is plenty of scope to experiment with the user interface, to make a portable device or perhaps a retro-style radio. Perhaps this could be combined into another project, adding FM radio functionality to an MP3 player, or an internet radio? .. image:: images/fm_radio_standalone.jpg I decided to go for a simple, portable, battery-powered device. I retrofitted the functionality to my usual Swiss-Army-PCB encloser which I used in the `guitar multi-effects unit `_ and the `power and SWR meter `_. I wanted something that could run on batteries, with a small audio amplifier and speaker. .. image:: images/fm_radio_schematic.svg Parts List ~~~~~~~~~~ Most parts are commonly available and could be purchased from multiple sources, but I have included links to some example components. + 1x `RDA5807M fm radio module `_ + 1x `Raspberry Pi Pico `_ + 1x `128x64 OLED Display `_ + 1x `3.5mm stereo headphone socket `_ + 3x `TPA2012D `_ + 5x 10uF ceramic capacitor X7R 0805 + 3x 1uF ceramic capacitor X7R 0805 + 1x 100nF ceramic capacitor X7R 0805 + 1x `ferrite bead 0805 `_ + 1x resistor 10R 0805 + 2x resistor 1K 0805 + 2x resistor 100K 0805 + 1x `SPDT slide switch `_ + 1x `SMA connector `_ + 1x battery holder 2xAA or 2xAAA + 1x speaker 4-ohm/8-ohm + 4x `tactile switch 6mm `_ PSU Noise ~~~~~~~~~ When I first built the radio, I powered both the RDA5807M and the Pi-Pico directly from 2xAA batteries. This worked quite well, but there were a couple of downsides to this approach. The first issue was that noise from the pico reduced the performance of the FM receiver so that some of the weaker stations could no longer be received. The second was that the voltage range of the RDA5807 is quite narrow from 2.7v to 3.3v. 2xNiMH cells should produce 2.4v when charged, reducing to 2.0v when discharged, in practice the device seemed to work with 2 AA NiMH batteries until they were almost completely discharged, but it would be nice to have the flexibility to use 2 or 3 NiMH or alkaline cells. This would need a voltage range from 2V to 4.5V. Another nice option would be to use a lithium polymer cell which might output 4.2V when charged and 3.4V when discharged. The Pi-Pico has a nice built-in switching regulator that can run on 1.8V to 5.5V, and efficiently produces a 3.3V output. If we could filter the 3.3v supply well enough to power the RDA5807M, this configuration would allow any of these battery combinations, while keeping power loss to a minimum. Some better supply filtering was needed. I tried this circuit using a ferrite, resistor and capacitors. .. image:: images/fm_radio_psu_filter.png Adding the filter to the 3.3V output from the Pi-Pico did the trick. When the RDA5807M was able to receive even the weaker stations using the filtered supply. User Interface ~~~~~~~~~~~~~~ I was looking for something compact, low-power and inexpensive so I stuck with the usual SSD1306 OLED display. Fortunately, MicroPython has inbuild support for this type of display which helps to keep the development simple. I could have shared the I2C bus between the RDA5807M and the SSD1306, but I'm not doing much else with the Pi-Pico and I have plenty of spare IO so I used a separate bus. A few tactile push buttons are all that is needed to control the device. MicroPython provides a flash-based file system so it is quite easy to store the settings by writing them to a file. This is certainly a lot easier than writing to Flash using the C/C++ SDK. .. code:: python def load_settings(): """ Load settings from a file in Flash on power-up """ #default settings if no file present settings = { "volume" : 0, "mute" : False, "frequency_MHz" : 88 } try: with open("settings.txt") as input_file: for line in input_file: key, value = line.split(":") key = key.strip() value = value.strip() settings[key] = value except OSError: pass return settings def save_settings(settings): """ Save settings to a file when a change is made """ with open("settings.txt", 'w') as output_file: for key, value in settings.items(): line = "%s:%s\n"%(key, value) output_file.write(line) Battery Monitor ~~~~~~~~~~~~~~~ In the Pi-Pico, VSYS (the battery input in our case) is permanently connected to one of the 5 ADC channels via a potential divider. This makes it very easy to track the battery voltage. I used a very simple smoothing filter to remove measurement noise. .. code:: python #The ADC pin needs to be configured at power-up adcpin = machine.Pin(29, machine.Pin.IN) ... #read battery voltage analogIn = ADC(3) batt_voltage = analogIn.read_u16() * 3.0 * 3.3 / 65536 average_batt_voltage = (0.6 * average_batt_voltage) + (0.4 * batt_voltage) Once we know the battery voltage, we can convert this into a battery level that can be drawn on the display. The battery level scale is from 0 to 10 with zero representing a flat battery and 10 representing a full one. The voltage of a full and flat battery depends on the battery chemistry. .. code:: python # Values could be adjusted for different chemistry # based on 2 AA cell batt_voltage_max = 3.0 batt_voltage_min = 2.0 # based on 3 AA cell #batt_voltage_max = 4.5 #batt_voltage_min = 3.0 # based on 1 3.7/4.2v Lithium Polymer cell #batt_voltage_max = 4.2 #batt_voltage_min = 3.4 if average_batt_voltage > batt_voltage_max: batt_level = 10 elif average_batt_voltage < batt_voltage_min: batt_level = 0 else: batt_level = (average_batt_voltage - batt_voltage_min) / (batt_voltage_max - batt_voltage_min) batt_level = round(batt_level * 10.0) Signal Strength Indicator ~~~~~~~~~~~~~~~~~~~~~~~~~ The RDA5807M provides an RSSI monitor indicating the signal strength. The rda7805.get_signal_strength() method returns a signal strength using a logarithmic scale from 0 to 7. The example code represents this as a signal strength icon which it draws on the screen. .. code:: python def draw_signal_strength(display, radio): """ Draw a signal strength icon on screen. Queries radio's RSSI. """ x = 110 y = 0 display.line(x+2, y+0, x+2, y+8, 1) display.line(x+0, y+0, x+2, y+4, 1) display.line(x+4, y+0, x+2, y+4, 1) level = radio.get_signal_strength() for i in range(level): display.line(x+4+(2*i), y+8, x+4+(2*i), y+8-i, 1) RDS ~~~ The RDA5807M provides the ability to receive and decode RDS data. The `datasheet `_ for the device does seem to lack detail in this area. The RDS protocol supplies data in four blocks, the RDA5807M provides each of these blocks in a separate 16 bit registers RDSA/B/C/D. The protocol includes both data and parity bits which allows for bit errors to be either detected and corrected or just detected depending on how many bit errors are present. The RDA5807M does the error correction for us giving us only the data part of each block. The number of bit errors in blocks A and B can be determined by reading BLERA/B. As far as I can tell there is no way to tell how many bit errors are present in blocks C and D, so it doesn't seem to be possible to completely avoid using corrupted data. Experimentally, it seems that reading from RDSA/B/C/D is an atomic operation. So long as the registers are read in address order, the blocks always seem to relate to the same message. Also, it seems like polling the device for new messages every 10ms is more than sufficient to ensure that we don't miss any of the received messages. I included a simple RDS decoder, which only decodes `station name`, `station text` and `time` messages which are displayed on the screen. Class-D Amplifier ~~~~~~~~~~~~~~~~~ Although the RDA5807M can drive headphones directly, it is nice to have a bit more power to drive a speaker. The LM386 might be the traditional choice, but I opted for the `TPA2012D2 `_. This class-D stereo amplifier is very efficient and can supply as much as 2.1W per channel into 4-ohm speakers. It is cheap and requires only a couple of supporting components, so really is ideal for a battery-powered application like this. One potential downside of the TPA2012D2 is that the WQFN package isn't particularly beginner-friendly. Fortunately, several ready-made modules such as `this one `_ are available. .. image:: images/fm_radio_amplifier.png The IC has 2 inputs G0 and G1 that set the gain of the amplifier. In this application we don't need much gain so I wired both these pins to ground giving the lowest possible (6dB) gain. I opted for a tiny 8-ohm speaker, these tiny speakers sound pretty terrible unless they are sealed in an air-tight enclosure. Some kind of 3D-printed surround would have been ideal, but I went for a lower-tech solution and just used a bottle cap secured with adhesive. Although not Hi-Fi quality, once sealed in an enclosure I was quite happy with the quality. You can get miniature speakers that have a built-in enclosure. I would have taken this route but didn't have the space in the enclosure. A larger speaker (in a suitable enclosure) would give even better (and louder) audio, and would be worth considering if you have the space. Antenna ~~~~~~~ The FM receiver is quite sensitive, and I found that the wire whip antenna was able to receive quite a few local stations. In the standalone receiver, I used an SMA connector for the antenna, this allows a compact telescopic antenna to be used for portable use while allowing a better external antenna to be used if necessary. It is possible to use the headphone cable as an antenna if suitable filtering is used, and the RDA5807 datasheet gives details on how to achieve this. Splash Screen ~~~~~~~~~~~~~ For a bit of fun, I thought it might be interesting to play try displaying images on the OLED display. The MicroPython library for this display is based on the `FrameBuffer` class which provides drawing primitives but doesn't have a way of loading an image from a file. The closest thing is the `blit` method that allows a frame buffer to be drawn on top of another. A `FrameBuffer` can also be passed a `bytearray` on construction, and it is simple enough to read this from a file. .. code:: python def draw_image(display, filename): """ Load an image from flash into a framebuf object and display on screen """ width = 128 height = 64 with open(filename, "br") as inf: data = inf.read(width * height // 8) fbuf = framebuf.FrameBuffer(bytearray(data), width, height, framebuf.MONO_HLSB) display.blit(fbuf, 0, 0, 0) display.show() To limit the amount of processing that needs to be done in Python, I pre-process the image into a `bytearray` of the correct format using a Python script on a PC. .. code:: python import imageio import sys input_file = sys.argv[1] output_file = sys.argv[2] im = imageio.imread(input_file) h, w, c = im.shape bytevals = [] for y in range(h): for i in range(w//8): byteval = 0 for j in range(8): x = (i*8)+j byteval <<= 1 if im[y][x][0] > 0: byteval |= 1 bytevals.append(byteval) with open(output_file, "wb") as outf: outf.write(bytes(bytevals)) The script uses the `imageio` library so can convert from most common image types. The image does need to have the correct height (in this case 128 pixels wide by 64 pixels high). The pixels in the image should be either black or white. .. image:: images/fm_radio_splash_screen.png :align: center .. code:: bash $ python image2fbuf.py radio.png radio.fbuf .. image:: images/fm_radio_splash_screen2.jpg :align: center Installing the SSD1306 library ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Assuming you have already installed Thonny and MicroPython, it is easy to install the SSD1306 library. .. image:: images/install_ssd1306_lib.png .. image:: images/install_ssd1306_lib2.png Loading the Python Project ~~~~~~~~~~~~~~~~~~~~~~~~~~ I have written another `pyboard.py `_ based script `send_oled_example_to_pico` to download all the necessary files. This time the script sends the `FrameBuffer` image file along with the Python modules. .. image:: images/fm_radio_install_python_project.png Testing ~~~~~~~ That's it! A working FM radio receiver, using an RD5807M and a Pi-Pico programmed in MicroPython. The process was fairly simple, supplying the RDA5807 with a clean supply was probably the trickiest part of the project. .. image:: images/fm_radio_standalone.jpg :target: https://youtu.be/uNHXsJeDlwY If you want to give it a go, you can find the code here: `FM Radio Code `_ Useful Links ------------ `RDA5807M datasheet `_ `C/C++SDK library for RDA5807 `_ `TPA2012D2 datasheet `_.