The goal is to read an analog voltage input using the microcontroller's ADC and convert it into a digital voltage value displayed over serial communication.
To implement a simple voltmeter, the microcontroller needs to:
Voltage Input Connection
Note: The input voltage must be within the microcontroller’s GPIO voltage range to avoid damaging the microcontroller ADC pin.
Here, we will use a potentiometer as a varying voltage source.
We can use any of the potentiometers with values between 1kΩ and 10kΩ.

So, by selecting a potentiometer as a voltage source, we can implement the task.
Below are the solutions to the given task using different microcontrollers
We’re using an STM32 NUCLEO-F103RB board, which runs at a 3.3V logic level.
Circuit Diagram

Project Setup in STM32CubeIDE
SystemClock_Config).HAL_Init() → Initializes HAL and system tick.SystemClock_Config() → Configures system clock (HSI + PLL).MX_GPIO_Init() → Initializes GPIO ports.MX_USART2_UART_Init() → Configures UART2.MX_ADC1_Init() → Configures ADC1 for analog input.ADC Initialization (MX_ADC1_Init):
static void MX_ADC1_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
/** Common config
*/
hadc1.Instance = ADC1;
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
/** Configure Regular Channel
*/
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_13CYCLES_5;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
}This configures ADC1 for single-channel operation with right-aligned 12-bit results.
UART2 Initialization (MX_USART2_UART_Init):
static void MX_USART2_UART_Init(void)
{
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
}This configures USART2 for communication at a 115200 baud rate with 8 data bits, no parity, and 1 stop bit, supporting both transmit and receive.
Header includes, Private defines, and Private Variables:
The provided code defines cleaner code organization:
#include <string.h>
// ADC configuration
#define ADC_MAX_VALUE 4095 // 12-bit ADC maximum value
#define VOLTAGE_MAX_VALUE 3.3f // Maximum voltage corresponding to ADC max value
#define ADC_TIMEOUT_MS 20 // ADC conversion timeout in milliseconds
#define SERIAL_DELAY_MS 2000 // Delay between serial transmissions
// ADC calibration values
#define LOW_RANGE_CALIB 0.050f // Calibration offset for values < 1900
#define HIGH_RANGE_CALIB 0.040f // Calibration offset for values < 4095
#define LOW_RANGE_THRESHOLD 1900 // Threshold for low range calibration
// Buffer for string conversions (optimized size)
static char conversion_buffer[16];Utility Functions:
a) Convert float to string with specified decimal places
/**
* @brief Converts a float to a string with specified decimal places
* @param val: The float value to convert
* @param buffer: Pointer to the output buffer
* @param decimals: Number of decimal places to show
* @retval None
*/
static void floatToStr(float val, char* buffer, uint8_t decimals) {
int32_t whole = (int32_t)val;
float fraction = val - whole;
// Handle negative numbers
if (val < 0) {
fraction *= -1;
}
// Convert whole part
int32_t n = whole;
uint8_t i = 0;
char temp[10]; // Temporary buffer for digit reversal
if (n == 0) {
buffer[i++] = '0';
} else {
if (n < 0) {
buffer[i++] = '-';
n = -n;
}
uint8_t j = 0;
while (n > 0) {
temp[j++] = (n % 10) + '0';
n /= 10;
}
while (j > 0) {
buffer[i++] = temp[--j];
}
}
// Decimal point
buffer[i++] = '.';
// Fraction part
for (uint8_t d = 0; d < decimals; d++) {
fraction *= 10;
uint8_t digit = (uint8_t)fraction;
buffer[i++] = digit + '0';
fraction -= digit;
}
buffer[i] = '\0'; // Null-terminate
}b) Print string over UART
/**
* @brief Prints a string to the serial monitor
* @param msg: Pointer to the null-terminated string to send
* @retval None
*/
static void printOnSerialMonitor(const char *msg) {
HAL_UART_Transmit(&huart2, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
}Main Loop - Reading and Displaying Voltage
while (1) {
// Start ADC conversion
HAL_ADC_Start(&hadc1);
// Wait for ADC conversion to complete
HAL_ADC_PollForConversion(&hadc1, ADC_TIMEOUT_MS);
// Read the converted ADC value (12-bit resolution)
uint16_t adcValue = HAL_ADC_GetValue(&hadc1);
// Calculate voltage from ADC value
float voltageValue = (VOLTAGE_MAX_VALUE * adcValue) / ADC_MAX_VALUE;
// Apply calibration offsets based on ADC range
if (adcValue != 0) {
if (adcValue < LOW_RANGE_THRESHOLD) {
voltageValue += LOW_RANGE_CALIB;
} else if (adcValue < ADC_MAX_VALUE) {
voltageValue += HIGH_RANGE_CALIB;
}
}
// Convert and display voltage value
floatToStr(voltageValue, conversion_buffer, 3);
printOnSerialMonitor("Voltage value: ");
printOnSerialMonitor(conversion_buffer);
printOnSerialMonitor(" \r\n");
HAL_Delay(SERIAL_DELAY_MS); // Delay between readings
}HAL_ADC_Start) to read the analog voltage from a sensor/potentiometer.HAL_ADC_PollForConversion).0–4095).0–3.3V).LOW_RANGE_THRESHOLD).floatToStr).printOnSerialMonitor).SERIAL_DELAY_MS) to stabilize readings and prevent serial flooding.The complete STM32CubeIDE project (including .ioc configuration, main.c, and HAL files) is available here:
📥 Download Project
We are using the ESP32 DevKitC v4 development board and programming it using the Arduino IDE
If we analyze the task carefully,
The circuit is connected as shown in the diagram below.
Circuit Diagram

Let us write the code for this voltmeter setup. It is very straightforward :
#define adc_pin 34
void setup() {
Serial.begin(115200);
analogReadResolution(12); // 0–4095
analogSetAttenuation(ADC_11db); // allows ~0–3.3V input range
// or analogSetPinAttenuation(pin, ADC_11db); for specific pin
}
void loop() {
int raw = analogRead(adc_pin);
float voltage = (raw / 4095.0) * 3.3;
Serial.print(" Voltage : ");
Serial.println(voltage, 3);
delay(2000);
}We will be using GPIO 34 as the analog input pin for voltage measurement.
1. analogRead(adc_pin)
Reads the analog voltage on GPIO 34 and returns a 12-bit ADC value
Range: 0 → 4095 (corresponding to 0V → 3.3V)
2. analogSetAttenuation(ADC_11db)
Enables the ESP32 ADC to measure voltages up to ~3.
3. float voltage = (raw / 4095.0) * 3.3
Converts the ADC value into actual voltage
4. Serial.print / Serial.println
Displays the measured voltage on the Serial Monitor.
As we know, the ADC gives a linear response proportional to the input. But in the ESP32, we have observed that it gives a non-linear response.
The responsive curve is shown in the chart below.

Due to this nonlinear behavior, when increasing the voltage from 0 to 3.3 V, the ADC keeps reading 0 until about 0.138 V — this is the dead zone.
When decreasing the voltage from 3.3 V to 0, the ADC stays at 4095 until about 3.21 V — this is the saturation zone.
Only between ~0.138 V and ~3.21 V does the ADC respond normally.
To calculate the accuracy error, we use:
Error(%) = ( |Vactual - Vmeasured | / Vmeasured ) x 100
Based on the measured behavior of the ESP32 ADC:
Linear Region (0.12 V → 3.12 V)
Dead Zone (0 V → 0.12 V)
Saturation Zone (3.12 V → 3.30 V)
We are using the Arduino UNO development board and programming it using the Arduino IDE.
If we analyze the task carefully,
The circuit is connected as shown in the diagram below.

Let us write the code for this voltmeter setup. It is very straightforward :
// Pin for analog input
const int analogPin = A0;
void setup() {
Serial.begin(9600);
}
void loop() {
// Read the raw ADC value (10-bit resolution: 0 to 1023)
int adcValue = analogRead(analogPin);
// Convert the ADC value to voltage
// 5V reference, voltage = (adcValue * 5.0) / 1023
float voltage = adcValue * (5.0 / 1023.0);
// Print the ADC value and corresponding voltage
Serial.print("ADC Value: ");
Serial.print(adcValue);
Serial.print(" | Voltage: ");
Serial.print(voltage);
Serial.println(" V");
// Wait for a short time before taking another reading
delay(500); // 500ms delay
}
analogRead(analogPin): reads ADC value.float voltage = adcValue * (5.0 / 1023.0) : converts ADC_value to voltage.Serial.print: print ADC_value and Voltage.The circuit connection is as follows

The output of the voltmeter readings is as follows
