Introduction
In this article, we will explore together how to transform your ESP8266 into an ESP8266 FM radio, i.e. a frequency modulation receiver controlled by an infrared remote control and equipped with a TFT display. Are you ready to dive into the world of FM radio with a touch of technological magic?
The project is based on an ESP8266 managing a Si4703 module for tuning FM radio stations. A distinctive feature of this system is the easy interaction, made possible thanks to an infrared remote control connected to the ESP8266. This remote control allows you to issue commands conveniently, making the radio listening experience extremely customizable and convenient.
The integrated TFT display provides real-time detailed information on the station being listened to, including data such as frequency, RDS information, channel and various radio settings, such as mute/unmute, stereo/mono and bass booster. The clear and intuitive view on the display makes it easy to understand and adjust all desired settings.
One of the main features is the ability to manually scan frequencies forwards/backwards, offering complete control over the search for the desired station. Additionally, the system allows the storage of up to 9 favorite radio stations in the ESP8266’s EEPROM memory, making access quick and easy.
Volume control, mute mode, stereo/mono switching and bass booster option are further features that allow you to tailor listening to your personal preferences. These options are operated via the remote control.
For listening, the user can choose between using earphones for a personal experience or connecting the system to an external audio system equipped with LM386 modules and speakers. This versatility allows you to adapt listening to different needs and circumstances.
In summary, this system represents a complete and customizable project for radio enthusiasts, offering a wide range of features and a simple user interface for a tailor-made radio listening experience.
As usual, to create the sketch we will use the excellent PlatformIO.
The Si4703 module
The Si4703 module represents the technological heart of this radio project, giving the system the advanced ability to tune FM stations. Featuring high-quality and precision features, the Si4703 integrates a highly sensitive FM radio receiver, allowing radio signals to be received with extreme clarity. Its compact and reliable design makes it ideal for integration into DIY projects like this, providing a solid foundation for radio frequency tuning.
One of the distinctive features of the Si4703 is its ability to manage the RDS (Radio Data System), providing additional information on the station being listened to. This information includes details such as station name, program type and more, enriching the user’s radio experience. RDS compatibility adds a level of interactivity and information to the system, making listening to your favorite stations more engaging.
Intelligent frequency management and the ability to manually scan through stations give the user total control, allowing for complete customization of listening. Additionally, the ability to store favorite stations in the ESP8266’s EEPROM memory further expands quick access options, ensuring your favorite stations are always at your fingertips.
The Si4703, with its reliable design and advanced features, proves to be an essential component for anyone looking to create a high-quality, customized radio system. Its versatility and precision help make this project an extraordinary radio listening experience and highly adaptable to user preferences.
This device is directly connected with the ESP8266 and communicates with it via the I2C protocol.
The I2C bus
The I2C (Inter-Integrated Circuit) bus is the bus through which the Si4703 module communicates with the ESP8266. It is a popular serial communication protocol used to connect various devices and sensors to microcontrollers, microprocessors and other embedded devices. Here are some key points to consider:
- Two-wire communication: the I2C bus uses only two wires for communication: one for data transmission (SDA, Serial Data) and one for clock synchronization (SCL, Serial Clock). This simplicity makes it ideal for connecting multiple devices on a single bus.
- Master and Slave: in I2C communication, there is a master device called “master” and one or more secondary devices called “slave”. The master initiates and controls the communication, while the slaves respond to the master’s requests.
- Addressing: each slave device has a unique address on the I2C bus. This allows the master to select the device it wishes to communicate with. Multiple devices with the same address cannot coexist on the same bus.
- Half-Duplex: the I2C bus supports half-duplex communication, which means that data can only flow in one direction at a time. However, it is possible to reverse the direction of communication during transmission.
- Variable speed: the I2C bus supports a variety of communication speeds, allowing you to tailor the speed to specific application needs.
- Widely used: the I2C bus is widely used in a wide range of devices, including sensors, memory devices, displays, microcontrollers and many more. It is a common choice in IoT devices and embedded designs.
- Pull-Up resistors: to ensure reliable communication, the I2C bus requires the use of pull-up resistors on the SDA and SCL outlets. These resistors help stabilize the signal and prevent noise.
- High-level protocol: the I2C bus is a high-level protocol that simplifies communication between devices, allowing developers to focus on application logic rather than managing communication.
In summary, the I2C bus is a reliable and flexible communication protocol that offers a convenient way to connect devices and sensors to a microcontroller or microprocessor. Its breadth of use and ease of implementation make it a popular choice in embedded electronics and IoT.
The TFT display
The 1.8-inch TFT display takes on a crucial role within this radio project, providing a technologically advanced visual platform. Featuring crisp resolution and vibrant colours, the display offers a rich, detailed viewing experience, ideal for viewing complex information such as radio frequencies, RDS data and system settings.
Its TFT (Thin Film Transistor) technology ensures fast pixel response and effective management of brightness levels, ensuring optimal visual clarity in a variety of environmental conditions. The backlight ensures clear viewing even in unfavorable light conditions.
The compact physical size of the display, with a diagonal of 1.8 inches, contributes to a lightweight and discreet construction, facilitating integration into the overall aesthetics of the project. The intuitive user interface is enriched by high definition graphics, making it easy to navigate between the different screens and manage listening options.
In the context of radio design, the TFT display serves as a visual control element, allowing users to customize and monitor the radio experience in real time. Its seamless integration with the overall system gives a touch of sophistication and technological functionality to the entire project.
We have already used the TFT display in articles Measuring oxygen saturation and heart rate with the ESP8266 and MAX30101 Sensor: complete guide and Weather station project with ESP8266 and TFT display: real-time monitoring of weather forecasts
The LM386 module
This module is based on the LM386 integrated circuit, a low frequency power amplifier, capable of delivering a couple of W into a 4/8 Ω load.
Operating on a single 4V to 12V power supply, this amplifier IC is ideal for small-scale audio projects such as portable radios, audio interfaces and DIY projects. Some key features include:
- Adjustable gain: the LM386 offers the ability to adjust gain through the use of external components. This feature allows customization of the amplification level based on the needs of the project.
- Low power consumption: thanks to its low power consumption, the LM386 is suitable for battery-powered applications, contributing to the energy efficiency of devices.
- Simple design: the LM386 simplifies the design process with a limited number of external components required for its operation. This aspect makes it particularly suitable for DIY projects and for those who are approaching audio circuit design for the first time.
The module we will use in this project contains an LM386 and a trimmer and is a pre-assembled audio amplifier, designed to simplify the integration of audio amplifiers into electronic projects. Features include:
- Integrated LM386: the module mounts the LM386 as the main component, providing a wide range of amplification possibilities for low-power audio signals.
- Adjustable gain: the trimmer on the module allows the gain of the LM386 to be adjusted. This adjustment can be tailored to specific project needs, allowing precise control over the power of the amplified signal.
- Ease of use: thanks to the pre-assembled module, users can easily integrate audio amplification functionality into their projects without having to design the circuit from scratch.
- Power Supply flexibility: the module is designed to operate with a wide range of supply voltages, contributing to its versatility.
This module is particularly suitable for DIY audio projects, portable speakers, and other applications where low-power audio signals need to be amplified.
The infrared remote control
Infrared remote control is a remote control device that uses infrared signals to communicate with electronic devices. Consisting of a transmitter and buttons for selecting functions, the infrared remote control is widely used in consumer electronic devices. Below are some of its main features and components:
- Transmitter LED: the remote control is equipped with an infrared LED as a transmitter. When you press a button, the LED emits an infrared signal containing coded information about the selected command.
- Control buttons: the remote control has a series of buttons that correspond to different functions of the controlled device. Each button sends a specific signal to the device, activating the respective function.
- Control circuit: inside the remote control, a control circuit manages the operating logic. It interprets the signals coming from the buttons and translates them into infrared signals sent through the LED.
The infrared receiver is the component located on the device intended to receive and interpret the infrared signals transmitted by the remote control. Below are its main features:
- Infrared photodiode: the key component of the receiver is the infrared photodiode, which is sensitive to light in the infrared range. When the infrared signal hits the photodiode, it generates an electric current proportional to the intensity of the light received.
- Signal demodulation: after the photodiode receives the signal, the receiver demodulates the infrared signal and the result is a digital signal that represents the command sent by the remote control.
- Command decoding: the control software inside the device that will process the received signal (in our case the ESP8266) decodes the received signal, associating it with a specific function or command. Based on the command received, the device will activate the corresponding function.
In summary, the infrared remote control and its receiver constitute a system that allows remote control of electronic devices, using infrared signals to communicate efficiently and wirelessly.
What components do we need for our ESP8266 FM Radio?
The list of components is not particularly long:
- a breadboard to connect NodeMCU ESP8266 to other components
- some DuPont wires (male – male, male – female, female – female)
- a TFT 1.8″ display
- a Si4703 module
- OPTIONAL: two amplifier modules with LM386
- a stereo headset with 3.5mm male jack
- OPTIONAL: two 4/8 Ω speakers and at least 2/3 W
- an infrared remote control with relative receiver
- and, of course, one NodeMCU ESP8266 !
Let’s now see the photos of the various modules used in the project.
Here is the TFT display used in this project:

The next two photos show us the Si4703 module:


Generally the connector is supplied separately so, if you need to solder it, I recommend you first take a look at article Yet another tutorial on how to solder to see how to do it.
It is convenient to solder the connector on the silk-screen side so that when the module is mounted on the breadboard the writing is below and the female jack is above.
The pins we must consider are 3.3V, GND, SDIO, SCLK, not RST (i.e. with the hyphen at the top) and we will connect them in this way
Si4703 | ESP8266 |
3.3V | 3V3 |
GND | GND |
SDIO | D2 |
SCLK | D1 |
not RST (with the hyphen above) | D8 |
Let’s now see the remote control used in this project:

This is a fairly common remote control in Arduino projects.
Let’s see the corresponding receiver.

The IR receiver consists of a module (left) on which the IR receiver diode (right) must be mounted on the bottom connector. The top connector instead will be connected to the power supply and to a GPIO of the ESP8266.
In the next two photos we will see the mounted receiver module.

The top connector is used to power the module and to connect it to the ESP8266:
IR receiver (top connector) | ESP8266 |
VCC | 3V3 |
GND | GND |
IN | D6 |
The IR receiver diode is mounted on the bottom connector so that its window is oriented forward (as in the next photo):

Let’s now see the amplifier module with LM386:

The top connector is the power and signal input. The VDD terminal must be connected to the Vin terminal of the ESP8266 (which is at 5V), the GND terminals to the GND of the circuit while the IN terminal represents the signal input of one of the two channels (right or left).
The lower connector is connected to the speaker: the GND terminal to the black wire (or marked with “-“) of the speaker, the OUT terminal to the red wire (or marked with “+”).
In order to create a pair of amplified “speakers” in this way I used an old pair of non-working stereo earphones with 3.5mm jack. I cut off the earphones and stripped the sheaths of both wires connecting them. Inside each sheath there are two enamelled wires (usually colored differently): one is connected to the ground terminal of the jack, the other to one of the two remaining terminals of the jack (representing the left channel and the right channel) . The wires are enamelled so they do not short circuit each other. It is therefore necessary to remove a section of enamel for each with the simple flame of a lighter. The flame eliminates the enamelling and makes the wire tin-proof. Then, peel the plastic sheath of each channel, separate the wires inside by color (they are colored differently), remove the enamel for each one with the lighter flame and solder them with the soldering iron. This operation must be done for both channels. At this point you will have a wire carrying the ground and the left channel and a wire carrying the ground and the right channel. Connect one pair to one of the two modules in the GND and IN terminals of the upper connector in the photo, you will do the same thing with the other pair of wires on the other module.
Alternatively you can use a pair of amplified computer speakers or you can also listen to the radio through a simple stereo headset.
Let’s now see a set of “speakers” created to listen to the radio:

When you start using the radio, you will have to adjust the trimmers on the two modules in order to dose the signal and ensure that the sound comes out clean and not distorted.
Finally we see the radio complete and in operation:

Project implementation
The wiring diagram
Before creating the actual circuit let’s take a look at the pinout of the board:

Let’s also see the display pinout:

For our project we will only consider the pins on the left side (with yellow connector on this photo).
Typically this display is already sold with the connector mounted but if yours doesn’t have it and you need to solder it, I advise you to first take a look at article Yet another tutorial on how to solder to learn how to make a perfect solder.
Before moving on to the wiring diagram you will need to carry out a small operation. The display can work at both 5V and 3.3V but since the digital pins of the ESP8266 do not tolerate voltages higher than 3.3V we will have to prepare the display to work with this voltage.
To do this we will simply have to short circuit jumper J1 i.e. the one shown in the following photo:

As you can see, it’s a fairly simple operation: a small drop of tin is enough to short-circuit the two pads indicated in red.
Let’s now see the electrical diagram of the project, created as usual with Fritzing:

For greater convenience I will report below the connection table between display and ESP8266:
TFT Display | ESP8266 |
1 | D4 |
2 | D3 |
3 | D0 |
4 | D7 |
5 | D5 |
6 | 3V3 |
7 | 3V3 |
8 | GND |
The sketch
Let’s create the PlatformIO project
We have already seen the procedure for creating a PlatformIO project in the article How to create a project for NodeMCU ESP8266 with PlatformIO.
Do not install the libraries indicated in the article but install the following libraries:
The Adafruit GFX Library by Adafruit library:

The Adafruit ST7735 and ST7789 Library by Adafruit library:

The DIYables_IRcontroller by DIYables.io library:

The Radio by Matthias Hertel library:

Now edit the platformio.ini file to add these two lines:
monitor_speed = 115200
upload_speed = 921600
so that it looks like this:
[env:nodemcuv2]
platform = espressif8266
board = nodemcuv2
monitor_speed = 115200
upload_speed = 921600
framework = arduino
lib_deps = mathertel/Radio@^3.0.1
diyables/DIYables_IRcontroller@^1.0.0
adafruit/Adafruit GFX Library@^1.11.9
adafruit/Adafruit ST7735 and ST7789 Library@^1.10.3
Obviously you can download the project from the following link:
Replace the main.cpp file of the project you created with the one present in the zip file.
To create the following sketch I based myself on two examples:
- the first example is taken from the http://www.mathertel.de/Arduino/RadioLibrary.aspx site and concerns the management of the radio module. In the starting example the radio is controlled with commands sent via Serial Monitor
- the second example is taken from the https://newbiely.com/tutorials/esp8266/esp8266-ir-remote-control site and is used to decode the commands sent by the remote control and received by the IR receiver
Now let’s see how the sketch works.
First of all you need to make a change to the radio module library. In the file .pio/libdeps/nodemcuv2/DIYables_IRcontroller/src/DIYables_IRcontroller.cpp, in the function DIYables_IRcontroller::begin() you need to modify the line:
IrReceiver.begin(_pin, ENABLE_LED_FEEDBACK);
to:
IrReceiver.begin(_pin, DISABLE_LED_FEEDBACK);
In practice the ENABLE_LED_FEEDBACK parameter becomes DISABLE_LED_FEEDBACK. This modification is necessary to operate the IR receiver and the TFT display at the same time.
Initially, the libraries necessary for managing the EEPROM, the I2C bus, the TFT display, the radio module and the IR receiver are included. Furthermore, the GPIOs for the display are defined and the tft object that manages the display is created:
#include <EEPROM.h>
#include <Wire.h>
#include <Adafruit_GFX.h> // include Adafruit graphics library
#include <Adafruit_ST7735.h> // include Adafruit ST7735 TFT library
// ST7735 TFT module connections
#define TFT_RST D4 // TFT RST pin is connected to NodeMCU pin D4 (GPIO2)
#define TFT_CS D3 // TFT CS pin is connected to NodeMCU pin D3 (GPIO0)
#define TFT_DC D0 // TFT DC pin is connected to NodeMCU pin D0 (GPIO16)
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
#include <radio.h>
// all possible radio chips included.
#include <RDA5807M.h>
#include <RDA5807FP.h>
#include <SI4703.h>
#include <SI4705.h>
#include <SI47xx.h>
#include <TEA5767.h>
#include <RDSParser.h>
#include <DIYables_IRcontroller.h> // DIYables_IRcontroller library
The GPIO for receiving commands from the IR receiver is then defined and the irController object that will decode these commands is defined:
#define IR_RECEIVER_PIN D6 // The ESP8266 pin D6 connected to IR controller
DIYables_IRcontroller_21 irController(IR_RECEIVER_PIN, 200); // debounce time is 200ms
An array of EEPROM addresses is then defined:
String currentChannelAddressEE[9] = {"2", "7", "12", "17", "22", "27", "32", "37", "42"};
These addresses are 9 and are intended to store the radio presets. We’ll talk about this feature in more detail later.
Variables are then defined which are used to store some states of certain functions (mute/unmute, stereo/mono, equalization and so on) and the string which will contain the command to be executed (cmdToExecute).
String channelToMemorize = "0";
String channelToReproduce = "";
String currentChannelAddressEEString = "";
int initialAddress1 = 0;
int indexArray = 0;
int lastCH = 0;
bool mute = false;
bool lastMute = false;
bool stereo = true;
bool lastStereo = true;
bool equal = false;
bool lastEqual = false;
unsigned long lastTimeRanCycle;
unsigned long measureDelayCycle = 500; // 0.5s
String cmdToExecute = "";
Subsequently, some GPIOs are defined that depend on the architecture (in our case ESP8266):
// ===== SI4703 specific pin wiring =====
#define ENABLE_SI4703
#ifdef ENABLE_SI4703
#if defined(ARDUINO_ARCH_AVR)
#define RESET_PIN 2
#define MODE_PIN A4 // same as SDA
#elif defined(ESP8266)
#define RESET_PIN D8 //
#define MODE_PIN D2 // same as SDA
#elif defined(ESP32)
#define RESET_PIN 4
#define MODE_PIN 21 // same as SDA
#endif
#endif
At this point the saveCurrentStation variable is defined which will be used in the storage operation of a radio station:
bool saveCurrentStation = false;
The objects radio (which manages the Si4703 module) and rds which receives additional information from the channel (in our case the name of the radio station, if present) are defined. Then follows the definition of some support variables:
SI4703 radio;
RDSParser rds;
String lastName = "";
int16_t kbValue;
bool lowLevelDebug = false;
The DisplayFrequency function shows the current frequency on Serial Monitor (it is mostly used for debugging):
void DisplayFrequency()
{
char s[12];
radio.formatFrequency(s, sizeof(s));
Serial.print("FREQ:");
Serial.println(s);
} // DisplayFrequency()
The DisplayServiceName function is used to print/update the name of the radio station on the display (extracted from the RDS data received from the radio module) if present:
void DisplayServiceName(const char *name)
{
Serial.print("RDS:");
Serial.println(name);
tft.setCursor(0, 22);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
tft.println(lastName);
tft.setCursor(0, 22);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
String n = String(name);
n.trim();
tft.println(n);
lastName = n;
} // DisplayServiceName()
The RDS_process function processes RDS data:
void RDS_process(uint16_t block1, uint16_t block2, uint16_t block3, uint16_t block4)
{
rds.processData(block1, block2, block3, block4);
}
The runSerialCommand function manages the commands received and decoded by the IR receiver:
void runSerialCommand(char cmd, int16_t value)
{
// ----- control the volume and audio output -----
if (cmd == '+')
{
// increase volume
int v = radio.getVolume();
radio.setVolume(++v);
}
else if (cmd == '-')
{
// decrease volume
int v = radio.getVolume();
if (v > 0)
radio.setVolume(--v);
}
else if (cmd == 'u')
{
// toggle mute mode
radio.setMute(!radio.getMute());
}
// toggle stereo mode
else if (cmd == 's')
{
radio.setMono(!radio.getMono());
}
// toggle bass boost
else if (cmd == 'b')
{
radio.setBassBoost(!radio.getBassBoost());
}
// ----- control the frequency -----
else if (cmd == 'f')
{
radio.setFrequency(value);
}
else if (cmd == '.')
{
radio.seekUp(false);
}
else if (cmd == ':')
{
radio.seekUp(true);
}
else if (cmd == ',')
{
radio.seekDown(false);
}
else if (cmd == ';')
{
radio.seekDown(true);
}
else if (cmd == '!')
{
// not in help
RADIO_FREQ f = radio.getFrequency();
if (value == 0)
{
radio.term();
}
else if (value == 1)
{
radio.initWire(Wire);
radio.setBandFrequency(RADIO_BAND_FM, f);
radio.setVolume(10);
}
}
else if (cmd == 'i')
{
// info
char s[12];
radio.formatFrequency(s, sizeof(s));
Serial.print("Station:");
Serial.println(s);
Serial.print("Radio:");
radio.debugRadioInfo();
Serial.print("Audio:");
radio.debugAudioInfo();
}
else if (cmd == 'x')
{
radio.debugStatus(); // print chip specific data.
}
else if (cmd == '*')
{
lowLevelDebug = !lowLevelDebug;
radio._wireDebug(lowLevelDebug);
}
} // runSerialCommand()
In particular:
- if it receives “+” or “-” it increases or decreases the volume
- if it receives “u” it switches from MUTE to UNMUTE and vice versa
- if it receives “s” it switches from STEREO to MONO and vice versa
- if it receives “b” it activates or deactivates the BASS BOOST function
- if it receives the “f” command it selects the frequency contained in the value variable
- if it receives “.” commands or “:” continues the search (scan) for a frequency occupied by a radio station
- if it receives the commands “,” or “;” goes back in search (scan) of a frequency occupied by a radio station
- the commands “!”, “i”, “x” and “*” are not used
The testfillrects function draws an animation based on rectangles when the device is turned on (for purely decorative purposes):
void testfillrects(uint16_t color1, uint16_t color2) {
tft.fillScreen(ST7735_BLACK);
for (int16_t x=tft.width()-1; x > 6; x-=6) {
tft.fillRect(tft.width()/2 -x/2, tft.height()/2 -x/2 , x, x, color1);
tft.drawRect(tft.width()/2 -x/2, tft.height()/2 -x/2 , x, x, color2);
}
}
The selectChannel function selects the requested station (requestChannel) previously stored in the EEPROM:
void selectChannel(int requestChannel) {
Serial.println(String(requestChannel));
indexArray = requestChannel - 1;
if(saveCurrentStation) {
channelToMemorize = String(requestChannel);
} else {
cmdToExecute = "f";
channelToReproduce = ""; // currentChannelAddressEE[9] = {"2", "7", "12", "17", "22", "27", "32", "37", "42"};
currentChannelAddressEEString = currentChannelAddressEE[indexArray];
initialAddress1 = currentChannelAddressEEString.toInt();
for(int i = 0; i < 5; i++) {
channelToReproduce = channelToReproduce + char(EEPROM.read(initialAddress1 + i));
}
if(String(channelToReproduce.charAt(0)) == "0") {
channelToReproduce.remove(0, 1);
}
Serial.println(channelToReproduce);
}
}
We now meet the setup function:
void setup()
{
delay(3000);
tft.initR(INITR_BLACKTAB);
tft.fillScreen(ST7735_BLACK);
testfillrects(ST7735_YELLOW, ST7735_MAGENTA);
tft.fillScreen(ST7735_BLACK);
tft.setCursor(0, 0);
tft.setRotation(1);
tft.setTextColor(ST7735_RED);
tft.setTextSize(2);
tft.println("Starting in progress: wait a moment");
delay(3000);
tft.fillScreen(ST7735_BLACK);
tft.setCursor(0, 80);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("STEREO");
irController.begin();
delay(1000);
// open the Serial port
Serial.begin(115200);
// delay(3000);
Serial.println("SerialRadio...");
EEPROM.begin(512); //Initialize EEPROM
#if defined(RESET_PIN)
// This is required for SI4703 chips:
radio.setup(RADIO_RESETPIN, RESET_PIN);
radio.setup(RADIO_MODEPIN, MODE_PIN);
#endif
// Enable information to the Serial port
radio.debugEnable(true);
radio._wireDebug(lowLevelDebug);
// Set FM Options for Europe
radio.setup(RADIO_FMSPACING, RADIO_FMSPACING_100); // for EUROPE
radio.setup(RADIO_DEEMPHASIS, RADIO_DEEMPHASIS_50); // for EUROPE
// Initialize the Radio
if (!radio.initWire(Wire))
{
Serial.println("no radio chip found.");
delay(4000);
while (1)
{
};
};
radio.setBandFrequency(RADIO_BAND_FM, 8930);
radio.setMono(false);
radio.setMute(false);
radio.setVolume(radio.getMaxVolume() / 2);
// setup the information chain for RDS data.
radio.attachReceiveRDS(RDS_process);
rds.attachServiceNameCallback(DisplayServiceName);
} // Setup
This function initializes the display, IR receiver controller, serial port, EEPROM and Si4703 module.
The loop function starts with this block:
if (millis() > lastTimeRanCycle + measureDelayCycle)
{
f = radio.getFrequency();
if (f != lastFrequency)
{
// print current tuned frequency
tft.setCursor(0, 0);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
String fToDelete = "F: " + String(lastFrequency/100) + "." + String(lastFrequency%100) + " MHz";
tft.println(fToDelete);
tft.setCursor(0, 0);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
String s = "F: " + String(f/100) + "." + String(f%100) + " MHz";
tft.println(s);
lastFrequency = f;
}
if (indexArray != lastCH)
{
tft.setCursor(0, 43);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
tft.println("CH: " + String(lastCH + 1));
tft.setCursor(0, 43);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("CH: " + String(indexArray + 1));
lastCH = indexArray;
}
if (mute != lastMute)
{
if(mute) {
tft.setCursor(0, 62);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("MUTE");
} else {
tft.setCursor(0, 62);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
tft.print("MUTE");
}
lastMute = mute;
}
if (stereo != lastStereo)
{
if(stereo) {
tft.setCursor(0, 80);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
tft.println("MONO");
tft.setCursor(0, 80);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("STEREO");
} else {
tft.setCursor(0, 80);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
tft.println("STEREO");
tft.setCursor(0, 80);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("MONO");
}
lastStereo = stereo;
}
if (equal != lastEqual)
{
if(equal) {
tft.setCursor(0, 99);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("BASS BOOST");
} else {
tft.setCursor(0, 99);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
tft.print("BASS BOOST");
}
lastEqual = equal;
}
lastTimeRanCycle = millis();
}
This block, every measureDelayCycle ms (in our case 500 ms), updates the data on the display if there are variations (frequency, channel, mute/unmute status, stereo/mono status, status with and without equalization ( BASS BOOST)).
The loop function continues with the following block:
cmdToExecute = "";
Key21 key = irController.getKey();
if (key != Key21::NONE)
{
switch (key)
{
case Key21::KEY_CH_MINUS:
Serial.println("CH-");
Serial.println(indexArray);
if(indexArray > 0) {
indexArray--;
cmdToExecute = "f";
channelToReproduce = ""; // currentChannelAddressEE[9] = {"2", "7", "12", "17", "22", "27", "32", "37", "42"};
currentChannelAddressEEString = currentChannelAddressEE[indexArray];
initialAddress1 = currentChannelAddressEEString.toInt();
for(int i = 0; i < 5; i++) {
channelToReproduce = channelToReproduce + char(EEPROM.read(initialAddress1 + i));
}
if(String(channelToReproduce.charAt(0)) == "0") {
channelToReproduce.remove(0, 1);
}
Serial.println(channelToReproduce);
}
break;
case Key21::KEY_CH:
Serial.println("CH");
saveCurrentStation = true;
break;
case Key21::KEY_CH_PLUS:
Serial.println("CH+");
Serial.println(indexArray);
if(indexArray < 8) {
indexArray++;
cmdToExecute = "f";
channelToReproduce = ""; // currentChannelAddressEE[9] = {"2", "7", "12", "17", "22", "27", "32", "37", "42"};
currentChannelAddressEEString = currentChannelAddressEE[indexArray];
initialAddress1 = currentChannelAddressEEString.toInt();
for(int i = 0; i < 5; i++) {
channelToReproduce = channelToReproduce + char(EEPROM.read(initialAddress1 + i));
}
if(String(channelToReproduce.charAt(0)) == "0") {
channelToReproduce.remove(0, 1);
}
Serial.println(channelToReproduce);
}
break;
case Key21::KEY_PREV:
Serial.println("<<");
cmdToExecute = ",";
tft.setCursor(0, 43);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
tft.println("CH: " + String(lastCH + 1));
break;
case Key21::KEY_NEXT:
Serial.println(">>");
cmdToExecute = ".";
tft.setCursor(0, 43);
tft.setRotation(1);
tft.setTextColor(ST7735_BLACK);
tft.setTextSize(2);
tft.println("CH: " + String(lastCH + 1));
break;
case Key21::KEY_PLAY_PAUSE:
Serial.println(">||");
Serial.println(saveCurrentStation);
if(saveCurrentStation && channelToMemorize != "0") {
Serial.println("save frequency: ");
char freq[12];
radio.formatFrequency(freq, sizeof(freq));
String stationDisplay = "";
String station = String(freq);
station.trim();
stationDisplay = station;
station.replace(".", "");
station.replace(" MHz", "");
if(station.length() == 4) {
station = "0" + station;
}
Serial.print(station);
Serial.print(" on position ");
Serial.print(channelToMemorize);
Serial.println();
tft.fillScreen(ST7735_BLACK);
tft.setCursor(0, 0);
tft.setRotation(1);
tft.setTextColor(ST7735_GREEN);
tft.setTextSize(2);
tft.println("Station " + stationDisplay + " saved on position " + channelToMemorize);
delay (3000);
tft.fillScreen(ST7735_BLACK);
tft.setCursor(0, 0);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
String s = "F: " + stationDisplay;
tft.println(s);
tft.setCursor(0, 43);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("CH: " + String(channelToMemorize));
if(mute) {
tft.setCursor(0, 62);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("MUTE");
}
if(stereo) {
tft.setCursor(0, 80);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("STEREO");
} else {
tft.setCursor(0, 80);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("MONO");
}
if(equal) {
tft.setCursor(0, 99);
tft.setRotation(1);
tft.setTextColor(ST7735_CYAN);
tft.setTextSize(2);
tft.println("BASS BOOST");
}
Serial.println(stereo);
Serial.println(mute);
Serial.println(equal);
// String currentChannelAddressEE[9] = {"2", "7", "12", "17", "22", "27", "32", "37", "42"};
int channelIndex = channelToMemorize.toInt() - 1;
int initialAddress = currentChannelAddressEE[channelIndex].toInt();
for(int i = 0; i < station.length(); i++) {
EEPROM.write(initialAddress + i, station[i]);
}
EEPROM.commit();
} else {
Serial.println("no channel saved");
}
saveCurrentStation = false;
channelToMemorize = "0";
break;
case Key21::KEY_VOL_MINUS:
Serial.println("–");
cmdToExecute = "-";
break;
case Key21::KEY_VOL_PLUS:
Serial.println("+");
cmdToExecute = "+";
break;
case Key21::KEY_EQ:
Serial.println("EQ");
cmdToExecute = "b";
equal = !equal;
break;
case Key21::KEY_100_PLUS:
Serial.println("100+");
cmdToExecute = "u";
mute = !mute;
break;
case Key21::KEY_200_PLUS:
Serial.println("200+");
cmdToExecute = "s";
stereo = !stereo;
break;
case Key21::KEY_0:
Serial.println("0");
break;
case Key21::KEY_1:
selectChannel(1);
break;
case Key21::KEY_2:
selectChannel(2);
break;
case Key21::KEY_3:
selectChannel(3);
break;
case Key21::KEY_4:
selectChannel(4);
break;
case Key21::KEY_5:
selectChannel(5);
break;
case Key21::KEY_6:
selectChannel(6);
break;
case Key21::KEY_7:
selectChannel(7);
break;
case Key21::KEY_8:
selectChannel(8);
break;
case Key21::KEY_9:
selectChannel(9);
break;
default:
Serial.println("WARNING: undefined key:");
break;
}
}
This block decodes the commands coming from the IR receiver (and therefore from the remote control) and carries out the corresponding functions.
- if the command is “CH+” or “CH-” the receiver scrolls forward or backward between the stations stored in the EEPROM (up or down the channel)
- if the command is “CH” the receiver prepares for memorizing the channel on the EEPROM (we will see the operation in more detail later)
- if the command is “<<” or “>>” the receiver performs a free scan backwards or forwards and stops on the first frequency with a signal of sufficient amplitude
- if the command is “>||” the receiver stores the current frequency in a location in the EEPROM (and therefore on a channel). We will see the operation in more detail later
- if the command is “+” or “-” the volume will be increased or decreased
- if the command is “EQ” the BASS BOOST function will be activated or deactivated
- if the command is “100+” the mute function is activated/deactivated
- if the command is “200+” the STEREO or MONO mode is set
- if the command is a digit between “1” and “9” the station previously stored in that position will be played
Finally we meet the last commands:
kbValue = channelToReproduce.toInt();
runSerialCommand(cmdToExecute.charAt(0), kbValue);
// check for RDS data
radio.checkRDS();
The first obtains the frequency to be reproduced, the second executes the command received (with any frequency value), the third periodically checks the RDS data.
Instructions for Use
When turned on, the receiver prepares to receive the default frequency of 89.30 MHz.
With the following keys you can go forward or backward with the frequency scan:

Suppose you arrive at a frequency you like (for example 93.80 MHz) and you want to memorize it on position 1 (the “1” button on the remote control). With the receiver tuned to this frequency, first press the “CH” button:

then press the “1” key:

finally press the button:

The 93.80 MHz frequency will be stored in the EEPROM corresponding to the “1” key and a confirmation message will appear on the display.
Continue scanning the frequencies and memorize the “2” key and so on up to 9. You can memorize 9 stations (on keys 1 to 9).
You can also overwrite the frequency stored on a key. For example, if you accidentally stored the frequency 93.80 MHz on the “1” key but instead wanted to save the frequency 104.70 MHz, you must use the frequency scan keys ( |◅◅ and ▻▻| ) until you reach the frequency 104.70 MHz and then repeat the memorization procedure (CH key, 1 key, ▻|| key).
Let’s open a small parenthesis on how the mechanism works.
We saw previously that an array of addresses has been defined on the EEPROM:
String currentChannelAddressEE[9] = {"2", "7", "12", "17", "22", "27", "32", "37", "42"};
The “1” key on the remote control corresponds to the address “2” on the EEPROM, the “2” key on the remote control corresponds to the “7” address on the EEPROM and so on up to the “9” key which corresponds to the address “42” ” on the EEPROM.
You will notice that each number (apart from the first) is 5 places away from the previous one. This is because the frequency values are converted to always occupy exactly 5 positions. For example, the frequency 98.60 MHz is stored as 09860 (5 places) while the frequency 102.30 MHz is stored as 10230 (5 places).
So if I store the frequency 98.60 MHz on key 5 and the frequency 102.30 MHz on key 8, in the end I will have the value 09860 stored at address 22 on the EEPROM and the value 10230 at address 37.
When key 5 is pressed to listen to the station stored in that position, the value 09860 is converted to 9860 and sent to the I2C bus to tell the radio module to tune to that frequency and then converted to 98.60 and sent to the display to be displayed .
If the 8 key is pressed instead, the value 10230 is sent directly to the I2C bus to tell the radio module to tune to that frequency and then converted into 102.30 and sent to the display to be displayed.
The keys

and

allow us to scroll through the stored channels.
If you want to better understand how the EEPROM works I invite you to read the article How to use the EEPROM memory on the NodeMCU ESP8266
PLEASE NOTE: when the ESP8266 is turned on for the first time, the addresses “2”, “7”, “12”, “17”, “22”, “27”, “32”, “37”, “42” will be occupied by numbers and random letters (depending on the state the EEPROM is in at that moment) therefore not corresponding to any station. It is therefore advisable to immediately proceed with memorizing the stations on keys from 1 to 9 to overwrite these random values with actually existing radio stations in order to avoid unexpected behavior or possible blocks of the radio module which is unable to interpret these strings (this check has not been included for simplicity but you could try implementing it yourself).
The keys

adjust the volume.
The button

switches from the MUTE state (radio mute) to the UNMUTE state (radio not muted) and vice versa.
The button

switches between STEREO mode and MONO mode and vice versa.
The button

activates/deactivates the BASS BOOST mode.
The button

is not associated with any function. You could try adding a function to this button.
And finally a nice video of the operation of our EPS8266 FM radio
Newsletter
If you want to be informed about the release of new articles, subscribe to the newsletter. Before subscribing to the newsletter read the page Privacy Policy (UE)
If you want to unsubscribe from the newsletter, click on the link that you will find in the newsletter email.