MCU-based LED-VU Meter

From time to time, I see people trying to build an audio VU meter. In the analog era, that’s typically done with a voltage divider + a series of comparators; or using chips like LM3914/3915.
Those chips are harder and harder to find, or you may need more resolution, or a different output profile. What to do?
Modern MCUs offer an easy solution.

  1. their build-in ADC (10-bit or more) is more than enough for this type of applications;
  2. their fast speed allows easy multiplexing so a few pins can control lots of output;
  3. their PWM capabilities also allow them to drive analog coil meters.

In this post, we will explore #1 and #2 and leave #3 to those of you who want to explore more on their own.
MCU-based LED-VU Meter
Picking a MCU:
For the purposes of this application, we need to pick a MCU with built-in adc, and sufficient pins. Because we are using multiplexing, we also want to control the timing via a timer interrupt. So a mcu with the following features is needed:

  1. built-in adc;
  2. port operations;
  3. timer;
  4. interrupts.

That’s pretty much any modern MCU, :).
When I started coding for this project, I had a PIC16F684 in my mind, and I thought to pick 6 output pins (“segments”) over 4 multiplexing lines (“digit”, or “channel”) for one adc input, making it a VU-meter driving up to 24 leds. But to make the code more versatile, I decided to write one that supports 6 16 segments, 2 – 4 digits so it is good for a total of 1264 LEDs, over 1 or 2 adc channels.
I actually didn’t find a PIC16F684 in my part box so instead I wrote it for a PIC16F690 – but I only used a few pins and used a random number generator so simulate the adc for demo purposes.
Basic Principles of Operations:
Essentially, we are going to take the adc input – in this case, 10-bit adc, and convert it to a 12-bit logarithmic value to be displayed on the LED. For flexibility, I want to support both bar patterns and dot patterns.

The code is written so that each output pin is individually configurable:

//hardware configuration
#define NUM_OF_SEGS 6 //number of segments. 

#define SEG0_PORT PORTC
#define SEG0_DDR TRISC
#define SEG0 (1<<0)

#define SEG1_PORT PORTC
#define SEG1_DDR TRISC
#define SEG1 (1<<1)

#define SEG2_PORT PORTC
#define SEG2_DDR TRISC
#define SEG2 (1<<2)

#define SEG3_PORT PORTB
#define SEG3_DDR TRISB
#define SEG3 (1<<4)

#define SEG4_PORT PORTB
#define SEG4_DDR TRISB
#define SEG4 (1<<5)

#define SEG5_PORT PORTB
#define SEG5_DDR TRISB
#define SEG5 (1<<6)

#define SEG6_PORT PORTB
#define SEG6_DDR TRISB
//#define SEG6 (1<<6)

#define SEG7_PORT PORTB
#define SEG7_DDR TRISB
//#define SEG7 (1<<7) //uncomment if not used

#define SEG8_PORT PORTB
#define SEG8_DDR TRISB
//#define SEG8 (1<<0) //uncomment if not used

#define SEG9_PORT PORTB
#define SEG9_DDR TRISB
//#define SEG9 (1<<0) //uncomment if not used

#define SEG10_PORT PORTB
#define SEG10_DDR TRISB
//#define SEG10 (1<<0) //uncomment if not used

#define SEG11_PORT PORTB
#define SEG11_DDR TRISB
//#define SEG11 (1<<0) //uncomment if not used

#define SEG12_PORT PORTB
#define SEG12_DDR TRISB
//#define SEG12 (1<<0) //uncomment if not used

#define SEG13_PORT PORTB
#define SEG13_DDR TRISB
//#define SEG13 (1<<0) //uncomment if not used

#define SEG14_PORT PORTB
#define SEG14_DDR TRISB
//#define SEG14 (1<<0) //uncomment if not used

#define SEG15_PORT PORTB
#define SEG15_DDR TRISB
//#define SEG15 (1<<0) //uncomment if not used

#define CH0_PORT PORTC
#define CH0_DDR TRISC
#define CH0 (1<<7)

#define CH1_PORT PORTB
#define CH1_DDR TRISB
#define CH1 (1<<7)

#define CH2_PORT PORTA
#define CH2_DDR TRISA
//#define CH2 (1<<4) //uncomment if not used

#define CH3_PORT PORTA
#define CH3_DDR TRISA
//#define CH3 (1<<5) //uncomment if not used

//end hardware configuration

The output is written so it can be active high or active low:

//global defines
#define LEDVU_DLY() NOP4() //guard against RMW

//active high for vu pins
#define SEG_ON(port, pin) do {IO_SET(port, pin); LEDVU_DLY();} while (0)
#define SEG_OFF( port, pin) do {IO_CLR(port, pin); LEDVU_DLY();} while (0)
//active low for vu pins
//#define SEG_ON(port, pin) do {IO_CLR(port, pin); LEDVU_DLY();} while (0)
//#define SEG_OFF( port, pin) do {IO_SET(port, pin); LEDVU_DLY();} while (0)

//macros to control output channels
//active low for dig pins - for small current applications
#define CH_ON(port, pin) do {IO_CLR(port, pin); LEDVU_DLY();} while (0)
#define CH_OFF(port, pin) do {IO_SET(port, pin); LEDVU_DLY();} while (0)
//active high for dig pins - better to control more leds through a npn/n-ch, high current applications
//#define CH_ON(port, pin) do {IO_SET(port, pin); LEDVU_DLY();} while (0)
//#define CH_OFF(port, pin) do {IO_CLR(port, pin); LEDVU_DLY();} while (0)

Read More: MCU-based LED-VU Meter

Leave a Comment

Your email address will not be published. Required fields are marked *