top of page
Writer's pictureRamesh G

FM Radio GUI Interface using ESP32 and a 3.5-inch TFT touchscreen display (ILI9488) with LVGL

To build an FM radio GUI interface using ESP32 and a 3.5-inch TFT touchscreen display (ILI9488) with LVGL (Light and Versatile Graphics Library) and Bodmer's TFT_eSPI Arduino library, we will create two distinct screens:

  1. FM Frequency and Volume Control Screen: This screen allows you to tune into FM frequencies and adjust the volume.

  2. Audio Equalizer Screen: This the screen offers control over the frequency range (0 to 20 kHz) and the balance between left and right audio channels

The FM radio module isn't included in this guide, but the GUI setup, control logic, and screen transitions will be outlined. This guide assumes you're using the TFT_eSPI library to interface with the TFT display and LVGL to manage the graphical interface.



Components Required

ESP-32 Module (38Pin)

3.5 inch TFT ILI9488 SPI Interface Module 480x320 with Touch Screen Display

Jumper Wires



Circuit Diagram

3.5inch TFT display

TFT refer blog:


In the VCC pin, you can either use 5V or 3.3V depending upon if your J1 connection is open or closed,

J1 = open=5V; J1=close=3.3V.














LVGL refer:


Hardware Requirements

  1. ESP32 Board: Any variant should work, but make sure it has enough GPIO pins.

  2. TFT LCD Touchscreen: Common models include ILI9488

  3. Connecting Wires: Jumper wires for connections.

  4. Breadboard (optional): For easier connections.




Software Requirements

  1. Arduino IDE: Make sure you have the latest version.

    Select “File>Preferences>settings>Additional Boards Manager URLs” to fill the link.

    Here are the steps to install the ESP32 board in Arduino IDE: 


    1. Open the Arduino IDE  

    2. Select File and then Preferences 

    3. Find the Additional Board Manager URLs field and activate the text box 

    4. Add the URL to the field 

    5. Click OK to save the changes 

       

     https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json,

    https://arduino.esp8266.com/stable/package_esp8266com_index.json,

    https://espressif.github.io/arduino-esp32/package_esp32_index.json




  2. LVGL Library: Available through the Library Manager in the Arduino IDE.


  3. Touch Driver Library: Depending on your touchscreen (e.g., TFT_eSPI)

    Configuring LVGL

    1. LVGL Configuration: You may need to configure LVGL for your specific display and touch screen. Create a lv_conf.h file or modify the existing one in the LVGL library folder:

      • Set LVGL_DISPLAY_HOR_RES and LVGL_DISPLAY_VER_RES to match your display's resolution.

      • Configure the display and touch input settings based on your library (e.g., TFT_eSPI settings).


    2. TFT_eSPI Configuration:

      A feature rich Arduino IDE compatible graphics and fonts library for 32-bit processors. The library is targeted at 32-bit processors, it has been performance optimised for RP2040, STM32, ESP8266 and ESP32 types, other 32-bit processors may be used but will use the slower generic Arduino interface calls. The library can be loaded using the Arduino IDE's Library Manager. Direct Memory Access (DMA) can be used with the ESP32, RP2040 and STM32 processors with SPI interface displays to improve rendering performance. DMA with a parallel interface (8 and 16-bit) is only supported with the RP2040.

       Go to the TFT_eSPI library folder and open the User_Setup.h file to configure the pins according to your wiring.

    3. Wiring the TFT Display

TFT Pin  ESP32 Pin

VCC       3.3V

GND      GND

CS          GPIO 5

RESET   GPIO EN

DC/RS  GPIO 17

SDI(MOSI)          GPIO 23

SCK       GPIO 18

LED       3.3V (or PWM pin for brightness)


Wiring the Touchscreen (e.g., TFT_espi)

Touch Pin           ESP32 Pin

T_CS     GPIO 16

T_IRQ    GPIO 21

T_DOUT              GPIO 19

T_DIN   GPIO 23

T_CLK   GPIO 18

4. UI Library: User Interface (e.g., UI)




FM Frequency and Volume Control Screen:

Frequency Tuning: Use the touch-enabled knob with indicator to tune into FM radio stations.

Volume Control: Adjust the volume using the touch-controlled with knob with indicator to set the desired value.


Audio Equalizer Screen Design

Frequency Bands (0 Hz to 20 kHz)

Common bands


Bass: 20 Hz - 250 Hz (affects low-end sound)

Midrange: 250 Hz - 2 kHz (affects vocals, instruments)

Treble: 2 kHz - 20 kHz (affects high-end sound)

Slider Controls: Each band might have a vertical slider, where you can increase or decrease the volume of each frequency band.


Balance Control

Left and Right Audio Channels: A horizontal slider or knob that adjusts the balance between the left and right audio channels (stereo channels).

Moving the slider to the left would emphasize the left speaker or headphone.

Moving it to the right would emphasize the right speaker or headphone.

The center point indicates a balanced audio output, where both channels are equally loud.



Arduino Code

Here’s a basic example to initialize the display and create a simple button using LVGL:


######################################################################

//Arduino-TFT_eSPI board-template main routine. There's a TFT_eSPI create+flush driver already in LVGL-9.1 but we create our own here for more control (like e.g. 16-bit color swap).


#include "FS.h"

#include <SPI.h>


#include <lvgl.h>

#include <TFT_eSPI.h>

#include <ui.h>


/*Don't forget to set Sketchbook location in File/Preferences to the path of your UI project (the parent foder of this INO file)*/


/*Change to your screen resolution*/

static const uint16_t screenWidth  = 480;

static const uint16_t screenHeight = 320;


enum { SCREENBUFFER_SIZE_PIXELS = screenWidth * screenHeight / 10 };

static lv_color_t buf [SCREENBUFFER_SIZE_PIXELS];


TFT_eSPI tft = TFT_eSPI( screenWidth, screenHeight ); /* TFT instance */

#define CALIBRATION_FILE "/calibrationData"


#if LV_USE_LOG != 0

/* Serial debugging */

void my_print(const char * buf)

{

    Serial.printf(buf);

    Serial.flush();

}


/* Display flushing */

void my_disp_flush (lv_display_t disp, const lv_area_t area, uint8_t *pixelmap)

{

    uint32_t w = ( area->x2 - area->x1 + 1 );

    uint32_t h = ( area->y2 - area->y1 + 1 );


    if (LV_COLOR_16_SWAP) {

        size_t len = lv_area_get_size( area );

        lv_draw_sw_rgb565_swap( pixelmap, len );

    }


    tft.startWrite();

    tft.setAddrWindow( area->x1, area->y1, w, h );

    tft.pushColors( (uint16_t*) pixelmap, w * h, true );

    tft.endWrite();


    lv_disp_flush_ready( disp );

}


/*Read the touchpad*/

void my_touchpad_read (lv_indev_t  indev_driver, lv_indev_data_t  data)

{

    uint16_t touchX = 0, touchY = 0;


    bool touched = tft.getTouch( &touchX, &touchY, 600 );


    if (!touched)

    {

        data->state = LV_INDEV_STATE_REL;

    }

    else

    {

        data->state = LV_INDEV_STATE_PR;


        /*Set the coordinates*/

        data->point.x = touchX;

        data->point.y = touchY;


        Serial.print( "Data x " );

        Serial.println( touchX );


        Serial.print( "Data y " );

        Serial.println( touchY );

    }

}


/*Set tick routine needed for LVGL internal timings*/

static uint32_t my_tick_get_cb (void) { return millis(); }



void setup ()

{

  uint16_t calibrationData[5];

  uint8_t calDataOK = 0;

    Serial.begin( 115200 ); /* prepare for possible serial debug */


    String LVGL_Arduino = "Hello Arduino! ";

    LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();


    Serial.println( LVGL_Arduino );

    Serial.println( "I am LVGL_Arduino" );


    lv_init();


#if LV_USE_LOG != 0

    lv_log_register_print_cb( my_print ); /* register print function for debugging */


    tft.begin();          /* TFT init */

    tft.setRotation( 3 ); /* Landscape orientation, flipped */


    static lv_disp_t* disp;

    disp = lv_display_create( screenWidth, screenHeight );

    lv_display_set_buffers( disp, buf, NULL, SCREENBUFFER_SIZE_PIXELS * sizeof(lv_color_t), LV_DISPLAY_RENDER_MODE_PARTIAL );

    lv_display_set_flush_cb( disp, my_disp_flush );


    static lv_indev_t* indev;

    indev = lv_indev_create();

    lv_indev_set_type( indev, LV_INDEV_TYPE_POINTER );

    lv_indev_set_read_cb( indev, my_touchpad_read );


    lv_tick_set_cb( my_tick_get_cb );


    ui_init();


    Serial.println( "Setup done" );

      // check file system

  if (!SPIFFS.begin()) {

    Serial.println("formatting file system");


    SPIFFS.format();

    SPIFFS.begin();

  }


  // check if calibration file exists

  if (SPIFFS.exists(CALIBRATION_FILE)) {

    File f = SPIFFS.open(CALIBRATION_FILE, "r");

    if (f) {

      if (f.readBytes((char *)calibrationData, 14) == 14)

        calDataOK = 1;

      f.close();

    }

  }

  if (calDataOK) {

    // calibration data valid

    tft.setTouch(calibrationData);

  } else {

    // data not valid. recalibrate

    tft.calibrateTouch(calibrationData, TFT_WHITE, TFT_RED, 15);

    // store data

    File f = SPIFFS.open(CALIBRATION_FILE, "w");

    if (f) {

      f.write((const unsigned char *)calibrationData, 14);

      f.close();

    }

  }

}


void loop ()

{

    lv_timer_handler(); /* let the GUI do its work */

    delay(5);


    lv_label_set_text_fmt(ui_Label_volume, "%" LV_PRId32 "%%", lv_arc_get_value(ui_Arc_volume));

    /*Rotate the label to the current position of the arc*/

    lv_arc_rotate_obj_to_angle(ui_Arc_volume, ui_Label_volume, 0);

   

    int knob1R = map(lv_arc_get_value(ui_Arc_volume), 0, 100, 400, 3200);

    lv_img_set_angle(ui_knob1, knob1R);


    lv_label_set_text_fmt(ui_Label_tuner, "%" LV_PRId32 "%", lv_arc_get_value(ui_Arc_tuner));

    int knob2R = map(lv_arc_get_value(ui_Arc_tuner), 880, 1080, 350, 3200);

    lv_img_set_angle(ui_knob2, knob2R);

    //int DialR = map(lv_arc_get_value(ui_Arc_tuner), 880, 1080, 0, 700);

    //lv_image_set_rotation(ui_Dial, DialR);

    int DialR = map(lv_arc_get_value(ui_Arc_tuner), 880, 1080, 350, -355);

    lv_obj_set_x(ui_Dial, DialR);

 }

######################################################################



Final Steps

  1. Upload the code to your ESP32 using the Arduino IDE.

  2. Open the Serial Monitor to check for any debug messages.

  3. Interact with the touchscreen to see your UI in action!


Explanation:

  • FM Radio Control: The FM radio's frequency and volume can be controlled using the TFT Control. These are updated in the LVGL GUI in real-time.

  • LVGL Interface: The interface includes a frequency display, and knobs to tune the radio up/down and adjust the volume.


Tips

  • Adjust the pin assignments based on your wiring.

  • Explore LVGL's extensive documentation for more UI elements and functionalities.

  • Use LVGL's built-in tools to design and customize your interfaces.

This should give you a solid start on using LVGL with an ESP32 and a TFT LCD touchscreen. Happy coding!


Demo:



Recent Posts

See All
bottom of page