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
Note: The current task requires a highly accurate ADC. However, the ESP32’s built-in ADC is non-linear and shows poor accuracy near the voltage edges (close to 0 V and Vref). Due to this limitation, we are not implementing this task using ESP32.
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 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