ESP32 Analog VU Meter – Smooth Needle, Real Audio Response (DIY Build)
In this project, I built a fully functional analog-style VU meter using an CrowPanel 3.5"-HMI ESP32 display, and LVGL.
Devices & Components
4
Resistor 100k _1206
2
Resistor 1k ohm
2
10uF Capacitor
2
Resistor 220 ohm
2
LED(Light Emitting Diode) White,Green,Red,Yellow,Blue Color
2
CrowPanel 3.5"-HMI ESP32 Display 480x320
Hardware & Tools
1
Soldering Iron Kit
Software & Tools
Arduino IDE
Project description
Code
Code
cpp
...
1// by mircemk April, 2026 2 3#include <Arduino.h> 4#include <lvgl.h> 5#include <TFT_eSPI.h> 6#include "ui.h" 7 8#define SCREEN_WIDTH 480 9#define SCREEN_HEIGHT 320 10 11#define AUDIO_PIN 35 12 13// -------------------------------------------------- 14// Peak LED settings 15// -------------------------------------------------- 16#define PEAK_LED_PIN 21 17#define PEAK_HOLD_MS 120 18#define PEAK_ANGLE_THRESHOLD 270 19 20// -------------------------------------------------- 21// Backlight PWM settings 22// -------------------------------------------------- 23#define TFT_BL 27 24#define BL_CHANNEL 0 25#define BL_FREQ 5000 26#define BL_RESOLUTION 8 // 0-255 27#define BL_LEVEL 155 // <-- ova doteraj go za sekoj displej posebno 28 29// -------------------------------------------------- 30// TFT + LVGL 31// -------------------------------------------------- 32TFT_eSPI tft = TFT_eSPI(); 33 34static lv_disp_draw_buf_t draw_buf; 35static lv_color_t *buf1 = nullptr; 36static lv_color_t *buf2 = nullptr; 37 38// -------------------------------------------------- 39// VU meter variables 40// -------------------------------------------------- 41float smoothed_val = 0.01f; 42 43int adcMin = 120; 44int adcMax = 1000; 45 46int16_t meterAngleMin = -400; 47int16_t meterAngleMax = 400; 48 49int16_t current_angle = 0; 50uint32_t last_update = 0; 51uint32_t peakTimer = 0; 52 53// -------------------------------------------------- 54// LVGL flush 55// -------------------------------------------------- 56void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) 57{ 58 uint32_t w = (area->x2 - area->x1 + 1); 59 uint32_t h = (area->y2 - area->y1 + 1); 60 61 tft.startWrite(); 62 tft.setAddrWindow(area->x1, area->y1, w, h); 63 tft.pushColors((uint16_t *)&color_p->full, w * h, true); 64 tft.endWrite(); 65 66 lv_disp_flush_ready(disp); 67} 68 69// -------------------------------------------------- 70// Intro screen 71// -------------------------------------------------- 72void showIntroText() 73{ 74 tft.fillScreen(TFT_BLACK); 75 tft.setTextDatum(MC_DATUM); 76 tft.setTextColor(TFT_WHITE, TFT_BLACK); 77 78 tft.setTextFont(4); 79 tft.drawCentreString("VU - Meter", SCREEN_WIDTH / 2, 95, 4); 80 tft.drawCentreString("by", SCREEN_WIDTH / 2, 145, 4); 81 tft.drawCentreString("mircemk", SCREEN_WIDTH / 2, 190, 4); 82 delay(1800); 83} 84 85// -------------------------------------------------- 86// Read audio 87// -------------------------------------------------- 88int readVuLevel() 89{ 90 const int samples = 16; 91 int minVal = 1700; 92 int maxVal = 0; 93 94 for (int i = 0; i < samples; i++) { 95 int v = analogRead(AUDIO_PIN); 96 if (v < minVal) minVal = v; 97 if (v > maxVal) maxVal = v; 98 } 99 100 int peakToPeak = maxVal - minVal; 101 102 const float attackCoeff = 0.20f; 103 const float releaseCoeff = 0.20f; 104 105 if (peakToPeak > smoothed_val) { 106 smoothed_val = peakToPeak * attackCoeff + smoothed_val * (1.0f - attackCoeff); 107 } else { 108 smoothed_val = peakToPeak * releaseCoeff + smoothed_val * (1.0f - releaseCoeff); 109 } 110 111 return (int)smoothed_val; 112} 113 114// -------------------------------------------------- 115// Setup needle 116// -------------------------------------------------- 117void setupNeedle() 118{ 119 if (ui_Image2 == NULL) return; 120 121 const int needlePivotX = 6; 122 const int needlePivotY = 300; 123 const int needleX = 0; 124 const int needleY = -10; 125 126 lv_img_set_pivot(ui_Image2, needlePivotX, needlePivotY); 127 lv_obj_set_pos(ui_Image2, needleX, needleY); 128 lv_img_set_angle(ui_Image2, 0); 129 130 Serial.printf("needle x=%d y=%d\n", lv_obj_get_x(ui_Image2), lv_obj_get_y(ui_Image2)); 131 Serial.printf("parent w=%d h=%d\n", 132 lv_obj_get_width(lv_obj_get_parent(ui_Image2)), 133 lv_obj_get_height(lv_obj_get_parent(ui_Image2))); 134} 135 136// -------------------------------------------------- 137// Arduino setup 138// -------------------------------------------------- 139void setup() 140{ 141 Serial.begin(115200); 142 delay(300); 143 144 analogReadResolution(12); 145 pinMode(AUDIO_PIN, INPUT); 146 147 pinMode(PEAK_LED_PIN, OUTPUT); 148 digitalWrite(PEAK_LED_PIN, LOW); // LED OFF 149 150 151 152 tft.begin(); 153 tft.setRotation(1); 154 tft.fillScreen(TFT_BLACK); 155 156 // Backlight PWM 157 ledcSetup(BL_CHANNEL, BL_FREQ, BL_RESOLUTION); 158 ledcAttachPin(TFT_BL, BL_CHANNEL); 159 ledcWrite(BL_CHANNEL, BL_LEVEL); 160 161 showIntroText(); 162 163 lv_init(); 164 165 const uint32_t buf_pixels = SCREEN_WIDTH * 100; 166 buf1 = (lv_color_t *)heap_caps_malloc(buf_pixels * sizeof(lv_color_t), MALLOC_CAP_DMA); 167 buf2 = (lv_color_t *)heap_caps_malloc(buf_pixels * sizeof(lv_color_t), MALLOC_CAP_DMA); 168 169 if (!buf1 || !buf2) { 170 Serial.println("LVGL buffer allocation failed!"); 171 while (1) delay(100); 172 } 173 174 lv_disp_draw_buf_init(&draw_buf, buf1, buf2, buf_pixels); 175 176 static lv_disp_drv_t disp_drv; 177 lv_disp_drv_init(&disp_drv); 178 disp_drv.hor_res = SCREEN_WIDTH; 179 disp_drv.ver_res = SCREEN_HEIGHT; 180 disp_drv.flush_cb = my_disp_flush; 181 disp_drv.draw_buf = &draw_buf; 182 lv_disp_drv_register(&disp_drv); 183 184 ui_init(); 185 setupNeedle(); 186 187 Serial.println("UI initialized."); 188} 189 190// -------------------------------------------------- 191// LOOP 192// -------------------------------------------------- 193void loop() 194{ 195 lv_timer_handler(); 196 197 if (millis() - last_update >= 20) { 198 last_update = millis(); 199 200 int vuLevel = readVuLevel(); 201 202 int16_t target_angle = map(vuLevel, adcMin, adcMax, meterAngleMin, meterAngleMax); 203 204 if (target_angle > meterAngleMax) target_angle = meterAngleMax; 205 if (target_angle < meterAngleMin) target_angle = meterAngleMin; 206 207 lv_img_set_angle(ui_Image2, target_angle); 208 current_angle = target_angle; 209 210 // Peak LED spored realnata pozicija na iglata 211 if (current_angle >= PEAK_ANGLE_THRESHOLD) { 212 digitalWrite(PEAK_LED_PIN, HIGH); // LED ON 213 peakTimer = millis(); 214 } 215 else if (millis() - peakTimer > PEAK_HOLD_MS) { 216 digitalWrite(PEAK_LED_PIN, LOW); // LED OFF 217 } 218 } 219}
Downloadable files
Libraries
...
Libraries.zip
Code_VU_Final full
...
Code_VU_Final.zip
Documentation
Schematic
...
Schematic.jpg

Comments
Only logged in users can leave comments