/*
* Práctica 3: ADC - Construcción de un Goniómetro
* Microcontrolador: ESP32
* Sensor: Potenciómetro 100kΩ
* Descripción: Sistema completo de medición de ángulos con captura de datos,
* calibración y análisis experimental
*
* Funcionalidades:
* - Medición continua de ángulos
* - Captura y promediado de 100 muestras por posición
* - Cálculo de errores experimentales
* - Exportación de datos para análisis en Excel
* - Calibración automática
* - LED indicador y pulsador de control
*/
// ============ CONFIGURACIÓN DE PINES ============
const int POT_PIN = 4; // Pin ADC donde se conecta el potenciómetro (GPIO4)
const int LED_PIN = 2; // Pin del LED indicador
const int BUTTON_PIN = 22; // Pin del pulsador para captura de datos
// ============ CONSTANTES DE CALIBRACIÓN ============
// Para un potenciómetro típico de 100kΩ con rotación de 270°
const int MIN_ADC = 0; // Valor ADC mínimo (cuando el potenciómetro está al mínimo)
const int MAX_ADC = 4095; // Valor ADC máximo (ESP32 tiene resolución de 12 bits)
const float MIN_ANGLE = 0.0; // Ángulo mínimo en grados
const float MAX_ANGLE = 270.0; // Ángulo máximo en grados (típico para potenciómetros)
const float VREF = 3.3; // Voltaje de referencia del ADC
const int SAMPLES_COUNT = 100; // Número de muestras para promediar
const int MAX_DATA_POINTS = 50; // Máximo número de puntos de datos experimentales
// ============ VARIABLES GLOBALES ============
int sensorValue = 0; // Valor leído del ADC
float angle = 0.0; // Ángulo calculado
float voltage = 0.0; // Voltaje en el pin ADC
// Variables para captura de datos experimentales
struct DataPoint {
float expectedAngle; // Ángulo esperado (teórico)
float measuredADC; // Valor ADC promediado
float measuredVoltage; // Voltaje promediado
float measuredAngle; // Ángulo calculado
float error; // Error en grados
};
DataPoint experimentalData[MAX_DATA_POINTS];
int dataPointCount = 0; // Contador de puntos de datos capturados
// Variables de control
bool captureMode = false;
bool buttonPressed = false;
bool modeSelected = false; // Indica si el usuario ya seleccionó un modo
unsigned long lastButtonTime = 0;
const unsigned long DEBOUNCE_TIME = 200; // Tiempo de debounce en ms
// ============ CONFIGURACIÓN INICIAL ============
void setup() {
// Inicializar comunicación serie
Serial.begin(115200);
delay(1000); // Esperar a que se estabilice
// Configurar los pines
pinMode(POT_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT); // GPIO 22 conexión a GND
// Configurar resolución del ADC (12 bits = 0-4095)
analogReadResolution(12);
// Mostrar menú inicial
showStartupMenu();
delay(500);
}
/**
* Muestra el menú de inicio con opciones disponibles
*/
void showStartupMenu() {
Serial.println("\n========================================");
Serial.println(" GONIÓMETRO DIGITAL CON ESP32");
Serial.println(" Sistema de Medición de Ángulos");
Serial.println("========================================\n");
Serial.println("MODOS DISPONIBLES:");
Serial.println("1. Lectura continua");
Serial.println("2. Calibración manual");
Serial.println("3. Captura de datos experimentales");
Serial.println("4. Exportar datos capturados");
Serial.println("5. Mostrar estadísticas");
Serial.println("\nIngrese su opción:\n");
}
// ============ BUCLE PRINCIPAL ============
void loop() {
// Detectar presión de pulsador
checkButton();
// Leer entrada del usuario por serial
handleSerialInput();
// Solo ejecutar operación si el usuario ha seleccionado un modo
if (modeSelected) {
// Modo de lectura continua
if (!captureMode) {
// Leer valor del ADC
sensorValue = analogRead(POT_PIN);
// Convertir valor ADC a voltaje (ESP32 tiene Vref = 3.3V)
voltage = (sensorValue / 4095.0) * VREF;
// Mapear valor ADC a ángulo
angle = mapADCtoAngle(sensorValue);
// Mostrar datos en el monitor serie
displayData();
// Controlar LED indicador
digitalWrite(LED_PIN, HIGH);
delay(100);
digitalWrite(LED_PIN, LOW);
delay(250);
}
}
}
/**
* Verifica si el pulsador fue presionado
*/
void checkButton() {
if (digitalRead(BUTTON_PIN) == LOW && !buttonPressed) { // BAJO cuando se presiona (conectado a GND)
unsigned long currentTime = millis();
if (currentTime - lastButtonTime > DEBOUNCE_TIME) {
buttonPressed = true;
lastButtonTime = currentTime;
// Acción: Capturar punto de datos
if (captureMode && dataPointCount < MAX_DATA_POINTS) {
captureDataPoint();
}
}
} else if (digitalRead(BUTTON_PIN) == HIGH) {
buttonPressed = false;
}
}
/**
* Maneja la entrada por puerto serial para seleccionar modo
*/
void handleSerialInput() {
if (Serial.available()) {
char input = Serial.read();
switch (input) {
case '1':
Serial.println("Modo: Lectura Continua");
Serial.println("Presione pulsador para capturar punto de dato\n");
captureMode = false;
modeSelected = true;
break;
case '2':
Serial.println("Iniciando calibración...\n");
modeSelected = false; // No es un modo continuo
calibrateGoniometer();
showStartupMenu();
break;
case '3':
Serial.println("Modo: Captura de Datos Experimentales");
Serial.println("Cambie el potenciómetro a un ángulo específico y presione el pulsador");
Serial.print("Puntos capturados: ");
Serial.print(dataPointCount);
Serial.print("/");
Serial.println(MAX_DATA_POINTS);
captureMode = true;
modeSelected = true;
break;
case '4':
modeSelected = false; // No es un modo continuo
exportDataToCSV();
showStartupMenu();
break;
case '5':
modeSelected = false; // No es un modo continuo
showStatistics();
showStartupMenu();
break;
}
}
}
// ============ FUNCIONES AUXILIARES ============
/**
* Mapea el valor del ADC a un rango de ángulos
* Utiliza interpolación lineal
*
* @param adcValue: Valor leído del ADC (0-4095)
* @return: Ángulo correspondiente en grados
*/
float mapADCtoAngle(int adcValue) {
// Limitar el valor entre MIN_ADC y MAX_ADC
if (adcValue < MIN_ADC) adcValue = MIN_ADC;
if (adcValue > MAX_ADC) adcValue = MAX_ADC;
// Realizar mapeo lineal: (adcValue - min) / (max - min) * (angleMax - angleMin) + angleMin
float mappedAngle = ((float)(adcValue - MIN_ADC) / (float)(MAX_ADC - MIN_ADC)) *
(MAX_ANGLE - MIN_ANGLE) + MIN_ANGLE;
return mappedAngle;
}
/**
* Convierte voltaje a ángulos (función alternativa sugerida por el taller)
*
* @param voltaje: Voltaje medido en el pin ADC (0 - 3.3V)
* @return: Ángulo correspondiente en grados
*/
float convertirAGrados(float voltaje) {
// Limitar voltaje entre 0 y VREF
if (voltaje < 0) voltaje = 0;
if (voltaje > VREF) voltaje = VREF;
// Convertir voltaje a ángulo: (voltaje / VREF) * MAX_ANGLE
float angle = (voltaje / VREF) * MAX_ANGLE;
return angle;
}
/**
* Captura 100 muestras promediadas en la posición actual
* Almacena el dato con valor esperado ingresado por el usuario
*/
void captureDataPoint() {
if (dataPointCount >= MAX_DATA_POINTS) {
Serial.println("\nCapacidad máxima de puntos alcanzada.");
return;
}
// Capturar 100 muestras
long sumADC = 0;
float sumVoltage = 0;
float sumAngle = 0;
Serial.println("\nCapturando 100 muestras...");
digitalWrite(LED_PIN, HIGH);
for (int i = 0; i < SAMPLES_COUNT; i++) {
int reading = analogRead(POT_PIN);
float volt = (reading / 4095.0) * VREF;
float ang = mapADCtoAngle(reading);
sumADC += reading;
sumVoltage += volt;
sumAngle += ang;
// Mostrar progreso cada 10 muestras
if ((i + 1) % 10 == 0) {
Serial.print(".");
}
delay(5);
}
digitalWrite(LED_PIN, LOW);
Serial.println("\n¡Captura completada!");
// Calcular promedios
float avgADC = sumADC / (float)SAMPLES_COUNT;
float avgVoltage = sumVoltage / (float)SAMPLES_COUNT;
float avgAngle = sumAngle / (float)SAMPLES_COUNT;
// Solicitar ángulo esperado (teórico)
Serial.print("Ingrese el ángulo esperado (teórico) en grados: ");
while (!Serial.available()) {
delay(10);
}
float expectedAngle = Serial.parseFloat();
while (Serial.available()) Serial.read(); // Limpiar buffer
// Calcular error
float error = abs(expectedAngle - avgAngle);
// Almacenar datos
experimentalData[dataPointCount].expectedAngle = expectedAngle;
experimentalData[dataPointCount].measuredADC = avgADC;
experimentalData[dataPointCount].measuredVoltage = avgVoltage;
experimentalData[dataPointCount].measuredAngle = avgAngle;
experimentalData[dataPointCount].error = error;
dataPointCount++;
// Mostrar resumen
Serial.println("\n--- PUNTO DE DATO CAPTURADO ---");
Serial.print("Ángulo esperado: ");
Serial.print(expectedAngle, 2);
Serial.println("°");
Serial.print("ADC promedio: ");
Serial.println(avgADC, 1);
Serial.print("Voltaje promedio: ");
Serial.print(avgVoltage, 3);
Serial.println(" V");
Serial.print("Ángulo medido: ");
Serial.print(avgAngle, 2);
Serial.println("°");
Serial.print("Error: ");
Serial.print(error, 2);
Serial.println("°");
Serial.print("Puntos capturados: ");
Serial.print(dataPointCount);
Serial.print("/");
Serial.println(MAX_DATA_POINTS);
Serial.println("-------------------------------\n");
}
/**
* Exporta los datos capturados en formato CSV para análisis en Excel
*/
void exportDataToCSV() {
if (dataPointCount == 0) {
Serial.println("\nNo hay datos capturados para exportar.\n");
return;
}
Serial.println("\n========== DATOS EXPORTABLES (CSV) ==========");
Serial.println("Copie el siguiente contenido a un archivo .csv en Excel:\n");
// Encabezados
Serial.println("Angulo_Esperado_Grados,ADC_Promedio,Voltaje_Promedio_V,Angulo_Medido_Grados,Error_Grados");
// Datos
for (int i = 0; i < dataPointCount; i++) {
Serial.print(experimentalData[i].expectedAngle, 2);
Serial.print(",");
Serial.print(experimentalData[i].measuredADC, 1);
Serial.print(",");
Serial.print(experimentalData[i].measuredVoltage, 3);
Serial.print(",");
Serial.print(experimentalData[i].measuredAngle, 2);
Serial.print(",");
Serial.println(experimentalData[i].error, 2);
}
Serial.println("\n==========================================\n");
}
/**
* Muestra estadísticas de los datos capturados
*/
void showStatistics() {
if (dataPointCount == 0) {
Serial.println("\nNo hay datos capturados.\n");
return;
}
Serial.println("\n========== ESTADÍSTICAS ==========");
Serial.print("Puntos de datos capturados: ");
Serial.println(dataPointCount);
// Calcular promedio de errores
float totalError = 0;
float maxError = 0;
float minError = 999;
for (int i = 0; i < dataPointCount; i++) {
totalError += experimentalData[i].error;
if (experimentalData[i].error > maxError) maxError = experimentalData[i].error;
if (experimentalData[i].error < minError) minError = experimentalData[i].error;
}
float avgError = totalError / dataPointCount;
Serial.print("Error promedio: ");
Serial.print(avgError, 3);
Serial.println("°");
Serial.print("Error máximo: ");
Serial.print(maxError, 3);
Serial.println("°");
Serial.print("Error mínimo: ");
Serial.print(minError, 3);
Serial.println("°");
// Precisión porcentual
float accuracy = 100.0 - ((avgError / MAX_ANGLE) * 100.0);
Serial.print("Precisión: ");
Serial.print(accuracy, 1);
Serial.println("%");
Serial.println("==================================\n");
}
/**
* Suaviza lecturas del sensor usando promedio móvil
* Útil para reducir ruido
*
* @param newValue: Nuevo valor a añadir
* @param numSamples: Número de muestras a promediar
* @return: Valor promediado
*/
float smoothSensor(int newValue, int numSamples) {
static int readings[10] = {0};
static int readIndex = 0;
static float total = 0;
if (numSamples > 10) numSamples = 10; // Limitar a 10 muestras máximo
total = total - readings[readIndex];
readings[readIndex] = newValue;
total = total + readings[readIndex];
readIndex = (readIndex + 1) % numSamples;
return total / numSamples;
}
/**
* Muestra los datos en el monitor serie de forma organizada
*/
void displayData() {
Serial.print("ADC: ");
Serial.print(sensorValue);
Serial.print(" | Voltaje: ");
Serial.print(voltage, 2);
Serial.print(" V | Ángulo: ");
Serial.print(angle, 1);
Serial.println(" °");
}
/**
* Función para calibración manual del potenciómetro
* Permite ajustar los valores MIN/MAX basados en lecturas reales
*/
void calibrateGoniometer() {
Serial.println("\n=== CALIBRACIÓN DEL GONIÓMETRO ===");
Serial.println("Este proceso calibrará los valores mínimo y máximo del ADC.\n");
Serial.println("Paso 1: Gira el potenciómetro AL ÁNGULO MÍNIMO (0°)");
Serial.println("Presiona ENTER cuando esté listo...");
while (Serial.available() == 0) {
delay(10);
}
while (Serial.available() > 0) Serial.read();
// Tomar 50 muestras en la posición mínima
long minSum = 0;
Serial.print("Capturando muestras mínimas: ");
for (int i = 0; i < 50; i++) {
minSum += analogRead(POT_PIN);
Serial.print(".");
delay(10);
}
int minValue = minSum / 50;
Serial.print("\nValor mínimo: ");
Serial.println(minValue);
delay(1000);
Serial.println("\nPaso 2: Gira el potenciómetro AL ÁNGULO MÁXIMO (270°)");
Serial.println("Presiona ENTER cuando esté listo...");
while (Serial.available() == 0) {
delay(10);
}
while (Serial.available() > 0) Serial.read();
// Tomar 50 muestras en la posición máxima
long maxSum = 0;
Serial.print("Capturando muestras máximas: ");
for (int i = 0; i < 50; i++) {
maxSum += analogRead(POT_PIN);
Serial.print(".");
delay(10);
}
int maxValue = maxSum / 50;
Serial.print("\nValor máximo: ");
Serial.println(maxValue);
Serial.println("\n=== CALIBRACIÓN COMPLETADA ===");
Serial.println("Usa estos valores en el código:\n");
Serial.print("const int MIN_ADC = ");
Serial.print(minValue);
Serial.println(";");
Serial.print("const int MAX_ADC = ");
Serial.print(maxValue);
Serial.println(";");
Serial.println("\n");
}