
1. Introduction
In this project, you’ll build a digital voltmeter capable of measuring 0V to 5V DC using the built-in ADC (Analog-to-Digital Converter) of the PIC16F877A microcontroller. The measured voltage is displayed on a 16×2 LCD screen in real-time.
Why this project?
Understanding how to measure voltages with a microcontroller is fundamental to many applications like battery level monitoring, sensor data acquisition, and general diagnostics.
2. Learning Objectives
- Interface analog inputs and use the ADC module in PIC16F877A.
- Calculate and display voltage values on an LCD screen.
- Understand ADC resolution and scaling.
- Practice with 4-bit LCD communication protocol.
3. Required Tools and Components
- PIC16F877A microcontroller
- 16×2 LCD (HD44780 compatible)
- Variable voltage source or potentiometer (for voltage testing)
- 10K potentiometer (for LCD contrast control)
- Jumper wires, breadboard, and power supply (5V regulated)
- MPLAB X IDE with XC8 compiler
- PIC programmer (e.g., PICkit 3 or 4)
4. Background Knowledge
ADC Basics
The ADC in PIC16F877A is 10-bit, meaning it can convert an analog voltage into a digital value between 0 and 1023. With a reference voltage of 5V, each step equals ~4.88 mV (5V / 1024). To calculate voltage from ADC value:
iniCopyEditVoltage = (ADC_Value * 5.0) / 1024
LCD Interfacing
The LCD operates in 4-bit mode to save I/O pins. It requires control pins (RS, E) and data pins (D4–D7) along with power and contrast control.
5. Wiring Instructions (Descriptive)
- Connect analog voltage source (or potentiometer wiper) to RA0 (Pin 2).
- Connect LCD pins RS, E, D4–D7 to RB0, RB1, RB2–RB5 respectively.
- Use a 10K potentiometer across Vss–Vdd of LCD, with wiper to Vo for contrast.
- Power both the PIC and LCD with 5V regulated.
- Tie R/W of LCD to GND to keep it in write mode.
6. Full Source Code with Comments (XC8)
// Digital Voltmeter Code for PIC16F877A
cCopyEdit#include <xc.h>
#define _XTAL_FREQ 8000000 // 8 MHz crystal
// Configuration bits
#pragma config FOSC = HS
#pragma config WDTE = OFF
#pragma config PWRTE = ON
#pragma config BOREN = ON
#pragma config LVP = OFF
#pragma config CPD = OFF, CP = OFF
// LCD Control Pins
#define RS RB0
#define E RB1
#define D4 RB2
#define D5 RB3
#define D6 RB4
#define D7 RB5
// Function declarations
void ADC_Init();
unsigned int ADC_Read(unsigned char channel);
void LCD_Init();
void LCD_Command(unsigned char cmd);
void LCD_Char(unsigned char data);
void LCD_String(const char *str);
void LCD_SetCursor(unsigned char row, unsigned char column);
void LCD_Clear();
void main() {
unsigned int adc_result;
float voltage;
char buffer[10];
TRISA = 0xFF; // Set PORTA as input
TRISB = 0x00; // Set PORTB as output for LCD
ADC_Init();
LCD_Init();
LCD_Clear();
LCD_SetCursor(1, 1);
LCD_String("Voltage: ");
while (1) {
adc_result = ADC_Read(0); // Read analog value from channel 0 (RA0)
voltage = (adc_result * 5.0) / 1024.0; // Convert to voltage
sprintf(buffer, "%.2f V", voltage);
LCD_SetCursor(2, 1);
LCD_String(" "); // Clear previous reading
LCD_SetCursor(2, 1);
LCD_String(buffer);
__delay_ms(500);
}
}
// Initialize ADC Module
void ADC_Init() {
ADCON1 = 0x80; // Right justified result, Fosc/32
ADCON0 = 0x41; // ADC ON, Channel 0 selected
}
// Read from ADC
unsigned int ADC_Read(unsigned char channel) {
ADCON0 &= 0xC5; // Clear existing channel selection
ADCON0 |= (channel << 3); // Select desired channel
__delay_ms(2); // Acquisition delay
GO_nDONE = 1; // Start conversion
while (GO_nDONE); // Wait until done
return ((ADRESH << 8) + ADRESL); // Combine 10-bit result
}
// LCD Functions
void LCD_Init() {
LCD_Command(0x02); // Initialize 4-bit mode
LCD_Command(0x28); // 2 line, 5x7 matrix
LCD_Command(0x0C); // Display ON, cursor OFF
LCD_Command(0x06); // Increment cursor
LCD_Command(0x01); // Clear display
}
void LCD_Command(unsigned char cmd) {
RS = 0;
D4 = (cmd >> 4) & 1;
D5 = (cmd >> 5) & 1;
D6 = (cmd >> 6) & 1;
D7 = (cmd >> 7) & 1;
E = 1; __delay_ms(1); E = 0;
D4 = cmd & 1;
D5 = (cmd >> 1) & 1;
D6 = (cmd >> 2) & 1;
D7 = (cmd >> 3) & 1;
E = 1; __delay_ms(1); E = 0;
__delay_ms(2);
}
void LCD_Char(unsigned char data) {
RS = 1;
D4 = (data >> 4) & 1;
D5 = (data >> 5) & 1;
D6 = (data >> 6) & 1;
D7 = (data >> 7) & 1;
E = 1; __delay_ms(1); E = 0;
D4 = data & 1;
D5 = (data >> 1) & 1;
D6 = (data >> 2) & 1;
D7 = (data >> 3) & 1;
E = 1; __delay_ms(1); E = 0;
__delay_ms(2);
}
void LCD_String(const char *str) {
while (*str) {
LCD_Char(*str++);
}
}
void LCD_SetCursor(unsigned char row, unsigned char column) {
unsigned char position = (row == 1) ? 0x80 + (column - 1) : 0xC0 + (column - 1);
LCD_Command(position);
}
void LCD_Clear() {
LCD_Command(0x01);
}
7. Troubleshooting Tips
- LCD not responding: Verify connections, adjust contrast potentiometer, and ensure correct delays are used in code.
- Voltage readings incorrect: Double-check the voltage calculation formula, especially ADC scaling factors.
- No ADC value change: Confirm voltage at RA0 using a multimeter; ensure analog source is within 0–5V.
- Compile errors: Re-check configuration bits and ensure ADCON1/ADCON0 are correctly initialized.
8. Project Extensions
- Expand measurement range using a voltage divider (e.g., for 0–12V).
- Log voltage readings to EEPROM or send them over UART.
- Add over-voltage warning with buzzer alert.
- Display readings on a graphical LCD or OLED display.