Step-by-Step Guide: Build an ESP8266 FM Radio with Infrared Remote Control and TFT Display

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Variable speed: the I2C bus supports a variety of communication speeds, allowing you to tailor the speed to specific application needs.
  6. 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.
  7. 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.
  8. 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 1.8" TFT display
The 1.8″ TFT display

The next two photos show us the Si4703 module:

The Si4703 module seen from the front with the connector to be soldered
The Si4703 module seen from the front with the connector to be soldered

The Si4703 module seen from behind with the connector to be soldered
The Si4703 module seen from behind with the connector to be soldered

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

Si4703ESP8266
3.3V3V3
GNDGND
SDIOD2
SCLKD1
not RST (with the hyphen above)D8
Connection table of the Si4703 module

Let’s now see the remote control used in this project:

Infrared (IR) remote control
Infrared (IR) remote control

This is a fairly common remote control in Arduino projects.

Let’s see the corresponding receiver.

The disassembled receiver
The disassembled 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 mounted IR receiver module seen from above
The mounted IR receiver module seen from above

The top connector is used to power the module and to connect it to the ESP8266:

IR receiver (top connector)ESP8266
VCC3V3
GNDGND
IND6
IR receiver module connections

The IR receiver diode is mounted on the bottom connector so that its window is oriented forward (as in the next photo):

The mounted IR receiver module seen from the front
The mounted IR receiver module seen from the front

Let’s now see the amplifier module with LM386:

Amplifier module with LM386
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:

Powered speakers
Powered speakers

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:

The complete and operational radio
The complete and operational radio

Project implementation

The wiring diagram

Before creating the actual circuit let’s take a look at the pinout of the board:

ESP8266 NodeMCU pinout
ESP8266 NodeMCU pinout

Let’s also see the display pinout:

1.8" TFT display pinout
1.8″ TFT 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:

Jumper J1 to be shorted to operate the TFT display at 3.3V
Jumper J1 to be shorted to operate the TFT display at 3.3V

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:

Complete wiring diagram of the ESP8266 FM radio
Complete wiring diagram of the ESP8266 FM radio

For greater convenience I will report below the connection table between display and ESP8266:

TFT Display ESP8266
1D4
2D3
3D0
4D7
5D5
63V3
73V3
8GND
TFT display connections

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:

Install the Adafruit GFX Library
Install the Adafruit GFX Library

The Adafruit ST7735 and ST7789 Library by Adafruit library:

Install the Adafruit ST7735 and ST7789 Library
Install the Adafruit ST7735 and ST7789 Library

The DIYables_IRcontroller by DIYables.io library:

Install the DIYables_IRcontroller library by DIYables.io
Install the DIYables_IRcontroller library by DIYables.io

The Radio by Matthias Hertel library:

Install the Radio by Matthias Hertel library
Install 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:

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:

Frequency scan buttons forward/backward

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:

Station memorization procedure start button

then press the “1” key:

Button 1

finally press the button:

Station store 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

Button to scroll back stored channels

and

Button for scrolling forward stored channels

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

Volume down or up keys

adjust the volume.

The button

MUTE/UNMUTE button

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

The button

STEREO/MONO button

switches between STEREO mode and MONO mode and vice versa.

The button

Button activates/deactivates the BASS BOOST mode

activates/deactivates the BASS BOOST mode.

The button

0 key

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.

Enter your name
Enter your email
Scroll to Top