Fundamentos del DAC (Digital-to-Analog Converter)
Los Conversores Digital-Analógico (DAC) son dispositivos fundamentales que convierten señales digitales discretas en señales analógicas continuas. En sistemas mecatrónicos industriales, permiten que los microcontroladores generen voltajes precisos para controlar actuadores analógicos y sistemas de instrumentación.
Principio de Funcionamiento
Un DAC toma un valor digital binario y lo convierte en un voltaje proporcional:
- Entrada Digital: Código binario de n bits (ESP32: 8 bits = 256 niveles)
- Referencia de Voltaje: Define el rango de salida (0V a 3.3V en ESP32)
- Salida Analógica: Voltaje continuo proporcional al código digital
Fórmula de conversión:
V_out = (Código_Digital / 2^n) × V_ref
Conversión Digital-Analógica
Aplicaciones en Mecatrónica Industrial
- Control de velocidad de motores DC/AC
- Generación de señales de referencia
- Control de posición de servomotores
- Sistemas de instrumentación analógica
- Generación de formas de onda arbitrarias
- Control de válvulas proporcionales
- Sistemas de audio industrial
- Calibración automática de equipos
DAC del ESP32: Especificaciones Técnicas
Características del Hardware
Parámetro | Especificación |
---|---|
Resolución | 8 bits (256 niveles) |
Número de canales | 2 canales independientes |
Pines de salida | GPIO25 (DAC1), GPIO26 (DAC2) |
Rango de voltaje | 0V - 3.3V |
Velocidad máxima | 1 MHz (función integrada) |
Precisión típica | ±2 LSB |
Impedancia de salida | ~1kΩ |
Limitaciones y Consideraciones
- Corriente máxima: 20mA por canal
- Filtrado: Salida no filtrada, requiere filtro pasa-bajos externo
- Linealidad: INL/DNL típico ±1 LSB
- Ruido: ~20mVpp sin filtrado
- Temperatura: Deriva de ±0.5mV/°C
Calculadora de Valores DAC
Valores Comunes
Código Digital | Voltaje Salida | Porcentaje |
---|---|---|
0 | 0.00V | 0% |
64 | 0.83V | 25% |
128 | 1.65V | 50% |
192 | 2.48V | 75% |
255 | 3.30V | 100% |
Fórmulas de Conversión
Digital a Voltaje:
V = (código × 3.3V) ÷ 255
Voltaje a Digital:
código = (V × 255) ÷ 3.3V
Resolución:
LSB = 3.3V ÷ 255 ≈ 12.94mV
Implementación Práctica
Configuración Básica del DAC
/**
* Configuración básica del DAC en ESP32
* Autor: Curso ESP32 Mecatrónica UNAM
* Descripción: Control preciso de salida analógica
*/
// Pines DAC del ESP32
const int DAC1_PIN = 25; // Canal 1
const int DAC2_PIN = 26; // Canal 2
// Clase para manejo avanzado del DAC
class ESP32_DAC {
private:
int dac_channel;
int current_value;
float voltage_reference;
public:
ESP32_DAC(int channel) {
dac_channel = channel;
current_value = 0;
voltage_reference = 3.3; // Voltaje de referencia ESP32
}
void initialize() {
// Configurar el pin como salida DAC
pinMode(dac_channel, OUTPUT);
// Establecer valor inicial a 0V
setVoltage(0.0);
Serial.printf("DAC inicializado en GPIO%d\n", dac_channel);
Serial.printf("Rango: 0V - %.2fV\n", voltage_reference);
Serial.printf("Resolución: %.2fmV por paso\n",
(voltage_reference / 255.0) * 1000);
}
void setValue(int digital_value) {
// Limitar valor entre 0 y 255
digital_value = constrain(digital_value, 0, 255);
// Escribir al DAC
dacWrite(dac_channel, digital_value);
current_value = digital_value;
Serial.printf("DAC%d: Código=%d, Voltaje=%.3fV\n",
(dac_channel == 25) ? 1 : 2,
digital_value,
getCurrentVoltage());
}
void setVoltage(float target_voltage) {
// Limitar voltaje al rango válido
target_voltage = constrain(target_voltage, 0.0, voltage_reference);
// Convertir voltaje a código digital
int digital_code = round((target_voltage / voltage_reference) * 255.0);
setValue(digital_code);
}
void setPercentage(float percentage) {
// Limitar porcentaje entre 0% y 100%
percentage = constrain(percentage, 0.0, 100.0);
// Convertir porcentaje a código digital
int digital_code = round((percentage / 100.0) * 255.0);
setValue(digital_code);
}
float getCurrentVoltage() {
return (current_value / 255.0) * voltage_reference;
}
int getCurrentValue() {
return current_value;
}
float getCurrentPercentage() {
return (current_value / 255.0) * 100.0;
}
void printStatus() {
Serial.println("========== ESTADO DAC ==========");
Serial.printf("Canal: GPIO%d\n", dac_channel);
Serial.printf("Código Digital: %d/255\n", current_value);
Serial.printf("Voltaje Salida: %.3fV\n", getCurrentVoltage());
Serial.printf("Porcentaje: %.1f%%\n", getCurrentPercentage());
Serial.printf("Resolución: %.2fmV/LSB\n",
(voltage_reference / 255.0) * 1000);
Serial.println("==============================\n");
}
};
// Instanciar objetos DAC
ESP32_DAC dac1(DAC1_PIN);
ESP32_DAC dac2(DAC2_PIN);
void setup() {
Serial.begin(115200);
Serial.println("=== SISTEMA DAC ESP32 ===");
// Inicializar ambos canales DAC
dac1.initialize();
dac2.initialize();
Serial.println("Sistema DAC listo para usar");
Serial.println("Comandos disponibles:");
Serial.println("- 'V1 2.5' : Establecer DAC1 a 2.5V");
Serial.println("- 'P2 75' : Establecer DAC2 a 75%");
Serial.println("- 'C1 128' : Establecer DAC1 código 128");
Serial.println("- 'STATUS' : Mostrar estado de ambos DAC");
Serial.println("========================");
}
void loop() {
// Demostración automática - generar rampa en DAC1
static unsigned long last_update = 0;
static int ramp_value = 0;
static int direction = 1;
if (millis() - last_update > 50) { // Actualizar cada 50ms
dac1.setValue(ramp_value);
ramp_value += direction * 5;
if (ramp_value >= 255) {
direction = -1;
} else if (ramp_value <= 0) {
direction = 1;
}
last_update = millis();
}
// Procesar comandos serie
processSerialCommands();
// Estado cada 2 segundos
static unsigned long last_status = 0;
if (millis() - last_status > 2000) {
Serial.printf("DAC1: %.2fV | DAC2: %.2fV\n",
dac1.getCurrentVoltage(),
dac2.getCurrentVoltage());
last_status = millis();
}
}
void processSerialCommands() {
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim();
command.toUpperCase();
if (command.startsWith("V1 ")) {
float voltage = command.substring(3).toFloat();
dac1.setVoltage(voltage);
} else if (command.startsWith("V2 ")) {
float voltage = command.substring(3).toFloat();
dac2.setVoltage(voltage);
} else if (command.startsWith("P1 ")) {
float percentage = command.substring(3).toFloat();
dac1.setPercentage(percentage);
} else if (command.startsWith("P2 ")) {
float percentage = command.substring(3).toFloat();
dac2.setPercentage(percentage);
} else if (command.startsWith("C1 ")) {
int code = command.substring(3).toInt();
dac1.setValue(code);
} else if (command.startsWith("C2 ")) {
int code = command.substring(3).toInt();
dac2.setValue(code);
} else if (command == "STATUS") {
dac1.printStatus();
dac2.printStatus();
} else if (command == "HELP") {
Serial.println("Comandos disponibles:");
Serial.println("V1/V2 [voltaje] - Establecer voltaje (0.0-3.3V)");
Serial.println("P1/P2 [porcentaje] - Establecer porcentaje (0-100%)");
Serial.println("C1/C2 [código] - Establecer código digital (0-255)");
Serial.println("STATUS - Mostrar estado completo");
}
}
}
Generador de Formas de Onda
/**
* Generador de Formas de Onda con DAC ESP32
* Genera señales sinusoidales, triangulares, cuadradas y diente de sierra
*/
#include
enum WaveformType {
SINE,
TRIANGLE,
SQUARE,
SAWTOOTH,
NOISE
};
class WaveformGenerator {
private:
int dac_pin;
WaveformType current_waveform;
float frequency;
float amplitude;
float offset;
float phase;
unsigned long last_update;
uint32_t sample_counter;
// Tabla lookup para seno (optimización)
static const int SINE_TABLE_SIZE = 256;
uint8_t sine_table[SINE_TABLE_SIZE];
bool sine_table_initialized;
public:
WaveformGenerator(int pin) {
dac_pin = pin;
current_waveform = SINE;
frequency = 1.0; // 1 Hz por defecto
amplitude = 1.0; // Amplitud completa
offset = 0.5; // Centrado en 1.65V
phase = 0.0;
last_update = 0;
sample_counter = 0;
sine_table_initialized = false;
initializeSineTable();
}
void initializeSineTable() {
for (int i = 0; i < SINE_TABLE_SIZE; i++) {
float angle = (2.0 * PI * i) / SINE_TABLE_SIZE;
sine_table[i] = (uint8_t)((sin(angle) + 1.0) * 127.5);
}
sine_table_initialized = true;
}
void setWaveform(WaveformType type) {
current_waveform = type;
sample_counter = 0; // Reset counter
Serial.printf("Forma de onda cambiada a: %s\n",
getWaveformName(type).c_str());
}
void setFrequency(float freq_hz) {
frequency = constrain(freq_hz, 0.01, 1000.0); // 0.01Hz - 1kHz
Serial.printf("Frecuencia establecida: %.2f Hz\n", frequency);
}
void setAmplitude(float amp) {
amplitude = constrain(amp, 0.0, 1.0); // 0% - 100%
Serial.printf("Amplitud establecida: %.1f%%\n", amplitude * 100);
}
void setOffset(float off) {
offset = constrain(off, 0.0, 1.0); // 0V - 3.3V (normalizado)
Serial.printf("Offset establecido: %.3fV\n", offset * 3.3);
}
void setPhase(float phase_degrees) {
phase = fmod(phase_degrees, 360.0) * PI / 180.0; // Convertir a radianes
Serial.printf("Fase establecida: %.1f°\n", phase_degrees);
}
void update(uint32_t sample_rate_hz = 10000) {
unsigned long update_interval = 1000000 / sample_rate_hz; // Microsegundos
if (micros() - last_update >= update_interval) {
uint8_t dac_value = generateSample();
dacWrite(dac_pin, dac_value);
sample_counter++;
last_update = micros();
}
}
private:
uint8_t generateSample() {
float normalized_time = (float)sample_counter / (10000.0 / frequency);
normalized_time += phase / (2.0 * PI); // Aplicar fase
float sample = 0.0;
switch (current_waveform) {
case SINE:
sample = generateSine(normalized_time);
break;
case TRIANGLE:
sample = generateTriangle(normalized_time);
break;
case SQUARE:
sample = generateSquare(normalized_time);
break;
case SAWTOOTH:
sample = generateSawtooth(normalized_time);
break;
case NOISE:
sample = generateNoise();
break;
}
// Aplicar amplitud y offset
sample = (sample * amplitude) + offset;
sample = constrain(sample, 0.0, 1.0);
return (uint8_t)(sample * 255);
}
float generateSine(float time) {
int index = (int)(fmod(time, 1.0) * SINE_TABLE_SIZE) % SINE_TABLE_SIZE;
return (sine_table[index] / 255.0);
}
float generateTriangle(float time) {
float phase = fmod(time, 1.0);
if (phase < 0.5) {
return phase * 2.0; // Subida
} else {
return 2.0 - (phase * 2.0); // Bajada
}
}
float generateSquare(float time) {
float phase = fmod(time, 1.0);
return (phase < 0.5) ? 0.0 : 1.0;
}
float generateSawtooth(float time) {
return fmod(time, 1.0);
}
float generateNoise() {
return random(0, 256) / 255.0;
}
String getWaveformName(WaveformType type) {
switch (type) {
case SINE: return "Sinusoidal";
case TRIANGLE: return "Triangular";
case SQUARE: return "Cuadrada";
case SAWTOOTH: return "Diente de Sierra";
case NOISE: return "Ruido";
default: return "Desconocida";
}
}
public:
void printStatus() {
Serial.println("========== GENERADOR DE ONDAS ==========");
Serial.printf("Pin DAC: GPIO%d\n", dac_pin);
Serial.printf("Forma de onda: %s\n", getWaveformName(current_waveform).c_str());
Serial.printf("Frecuencia: %.2f Hz\n", frequency);
Serial.printf("Amplitud: %.1f%% (%.2fV p-p)\n",
amplitude * 100, amplitude * 3.3);
Serial.printf("Offset: %.3fV\n", offset * 3.3);
Serial.printf("Fase: %.1f°\n", phase * 180.0 / PI);
Serial.printf("Muestras generadas: %lu\n", sample_counter);
Serial.println("=======================================\n");
}
};
WaveformGenerator generator1(25); // DAC1
WaveformGenerator generator2(26); // DAC2
void setup() {
Serial.begin(115200);
Serial.println("=== GENERADOR DE FORMAS DE ONDA ===");
Serial.println("Comandos disponibles:");
Serial.println("SINE/TRIANGLE/SQUARE/SAWTOOTH/NOISE - Seleccionar forma");
Serial.println("FREQ [Hz] - Establecer frecuencia");
Serial.println("AMP [0.0-1.0] - Establecer amplitud");
Serial.println("OFFSET [0.0-1.0] - Establecer offset");
Serial.println("PHASE [grados] - Establecer fase");
Serial.println("STATUS - Mostrar estado");
Serial.println("===================================");
// Configuración inicial
generator1.setFrequency(2.0); // 2 Hz
generator1.setAmplitude(0.8); // 80%
generator1.setOffset(0.5); // 1.65V
generator2.setWaveform(TRIANGLE);
generator2.setFrequency(0.5); // 0.5 Hz
}
void loop() {
// Actualizar generadores
generator1.update(8000); // 8kHz sample rate
generator2.update(8000);
// Procesar comandos
processWaveformCommands();
// Status periódico
static unsigned long last_status = 0;
if (millis() - last_status > 5000) {
generator1.printStatus();
last_status = millis();
}
}
void processWaveformCommands() {
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim();
command.toUpperCase();
if (command == "SINE") {
generator1.setWaveform(SINE);
} else if (command == "TRIANGLE") {
generator1.setWaveform(TRIANGLE);
} else if (command == "SQUARE") {
generator1.setWaveform(SQUARE);
} else if (command == "SAWTOOTH") {
generator1.setWaveform(SAWTOOTH);
} else if (command == "NOISE") {
generator1.setWaveform(NOISE);
} else if (command.startsWith("FREQ ")) {
float freq = command.substring(5).toFloat();
generator1.setFrequency(freq);
} else if (command.startsWith("AMP ")) {
float amp = command.substring(4).toFloat();
generator1.setAmplitude(amp);
} else if (command.startsWith("OFFSET ")) {
float offset = command.substring(7).toFloat();
generator1.setOffset(offset);
} else if (command.startsWith("PHASE ")) {
float phase = command.substring(6).toFloat();
generator1.setPhase(phase);
} else if (command == "STATUS") {
generator1.printStatus();
}
}
}
Filtro Anti-Aliasing y Acondicionamiento
/**
* Sistema de Filtrado y Acondicionamiento para DAC
* Implementa filtros digitales y calibración automática
*/
class DACFilter {
private:
static const int BUFFER_SIZE = 16;
float output_buffer[BUFFER_SIZE];
int buffer_index;
bool buffer_full;
// Coeficientes del filtro pasa-bajos Butterworth
float alpha; // Factor de suavizado
float filtered_output;
// Calibración
struct CalibrationData {
float gain_error;
float offset_error;
bool calibrated;
} cal_data;
public:
DACFilter(float cutoff_frequency = 100.0) { // 100Hz por defecto
buffer_index = 0;
buffer_full = false;
filtered_output = 0.0;
// Calcular coeficiente del filtro
float rc = 1.0 / (2.0 * PI * cutoff_frequency);
float dt = 1.0 / 10000.0; // Asumiendo 10kHz de muestreo
alpha = dt / (rc + dt);
memset(output_buffer, 0, sizeof(output_buffer));
cal_data.gain_error = 1.0;
cal_data.offset_error = 0.0;
cal_data.calibrated = false;
}
uint8_t processOutput(uint8_t raw_dac_value) {
// Convertir a rango 0.0-1.0
float normalized = raw_dac_value / 255.0;
// Aplicar filtro pasa-bajos IIR
filtered_output = alpha * normalized + (1.0 - alpha) * filtered_output;
// Aplicar calibración si está disponible
float calibrated = applyCalibration(filtered_output);
// Almacenar en buffer para análisis
addToBuffer(calibrated);
// Convertir de vuelta a 8 bits
return (uint8_t)(constrain(calibrated, 0.0, 1.0) * 255);
}
void addToBuffer(float value) {
output_buffer[buffer_index] = value;
buffer_index = (buffer_index + 1) % BUFFER_SIZE;
if (!buffer_full && buffer_index == 0) {
buffer_full = true;
}
}
float applyCalibration(float input) {
if (cal_data.calibrated) {
return (input * cal_data.gain_error) + cal_data.offset_error;
}
return input;
}
void calibrate(float expected_low, float measured_low,
float expected_high, float measured_high) {
// Calibración de 2 puntos
float expected_span = expected_high - expected_low;
float measured_span = measured_high - measured_low;
if (measured_span != 0) {
cal_data.gain_error = expected_span / measured_span;
cal_data.offset_error = expected_low - (measured_low * cal_data.gain_error);
cal_data.calibrated = true;
Serial.println("Calibración completada:");
Serial.printf("Ganancia: %.6f\n", cal_data.gain_error);
Serial.printf("Offset: %.6f\n", cal_data.offset_error);
}
}
float getFilteredOutput() {
return filtered_output;
}
float getRMS() {
if (!buffer_full && buffer_index == 0) return 0.0;
float sum_squares = 0.0;
int count = buffer_full ? BUFFER_SIZE : buffer_index;
for (int i = 0; i < count; i++) {
sum_squares += output_buffer[i] * output_buffer[i];
}
return sqrt(sum_squares / count);
}
float getAverage() {
if (!buffer_full && buffer_index == 0) return 0.0;
float sum = 0.0;
int count = buffer_full ? BUFFER_SIZE : buffer_index;
for (int i = 0; i < count; i++) {
sum += output_buffer[i];
}
return sum / count;
}
void printFilterStatus() {
Serial.println("========== ESTADO DEL FILTRO ==========");
Serial.printf("Frecuencia de corte: %.1f Hz\n",
1.0 / (2.0 * PI * (1.0 - alpha) / alpha / (1.0/10000.0)));
Serial.printf("Salida filtrada: %.3f (%.1fV)\n",
filtered_output, filtered_output * 3.3);
Serial.printf("Promedio: %.3f (%.1fV)\n",
getAverage(), getAverage() * 3.3);
Serial.printf("RMS: %.3f (%.1fV)\n",
getRMS(), getRMS() * 3.3);
Serial.printf("Calibrado: %s\n",
cal_data.calibrated ? "Sí" : "No");
if (cal_data.calibrated) {
Serial.printf("Factor de ganancia: %.6f\n", cal_data.gain_error);
Serial.printf("Offset: %.6f\n", cal_data.offset_error);
}
Serial.println("=====================================\n");
}
};
// Sistema completo de DAC con filtrado
class HighPrecisionDAC {
private:
int dac_pin;
DACFilter* filter;
uint8_t target_value;
uint8_t current_output;
public:
HighPrecisionDAC(int pin, float filter_frequency = 100.0) {
dac_pin = pin;
filter = new DACFilter(filter_frequency);
target_value = 0;
current_output = 0;
}
~HighPrecisionDAC() {
delete filter;
}
void setTargetValue(uint8_t value) {
target_value = value;
}
void setTargetVoltage(float voltage) {
voltage = constrain(voltage, 0.0, 3.3);
target_value = (uint8_t)((voltage / 3.3) * 255);
}
void update() {
// Procesar a través del filtro
current_output = filter->processOutput(target_value);
// Escribir al DAC
dacWrite(dac_pin, current_output);
}
void calibrate(float low_voltage, float high_voltage) {
Serial.println("Iniciando calibración...");
// Punto bajo
setTargetVoltage(low_voltage);
delay(1000); // Estabilizar
Serial.printf("Establecer %.2fV y medir voltaje real...\n", low_voltage);
// En una implementación real, aquí se leería un ADC externo
// Por simplicidad, simulamos medición
float measured_low = low_voltage * 0.98; // Error simulado
// Punto alto
setTargetVoltage(high_voltage);
delay(1000);
Serial.printf("Establecer %.2fV y medir voltaje real...\n", high_voltage);
float measured_high = high_voltage * 1.02; // Error simulado
// Aplicar calibración
filter->calibrate(low_voltage/3.3, measured_low/3.3,
high_voltage/3.3, measured_high/3.3);
}
void printStatus() {
float target_voltage = (target_value / 255.0) * 3.3;
float output_voltage = (current_output / 255.0) * 3.3;
Serial.println("========== DAC DE ALTA PRECISIÓN ==========");
Serial.printf("Pin: GPIO%d\n", dac_pin);
Serial.printf("Objetivo: %d (%.3fV)\n", target_value, target_voltage);
Serial.printf("Salida: %d (%.3fV)\n", current_output, output_voltage);
Serial.printf("Error: %.3fV (%.2f%%)\n",
output_voltage - target_voltage,
((output_voltage - target_voltage) / target_voltage) * 100);
filter->printFilterStatus();
Serial.println("==========================================\n");
}
DACFilter* getFilter() {
return filter;
}
};
HighPrecisionDAC precision_dac(25, 50.0); // 50Hz filtro
void setup() {
Serial.begin(115200);
Serial.println("=== SISTEMA DAC DE ALTA PRECISIÓN ===");
Serial.println("Comandos:");
Serial.println("CAL - Ejecutar calibración");
Serial.println("SET [voltaje] - Establecer voltaje objetivo");
Serial.println("STATUS - Mostrar estado completo");
Serial.println("====================================");
// Demostración automática
Serial.println("Iniciando demostración...");
}
void loop() {
// Actualizar sistema
precision_dac.update();
// Demostración automática - onda lenta
static unsigned long last_demo = 0;
if (millis() - last_demo > 2000) {
static float demo_voltage = 0.5;
static float direction = 0.5;
demo_voltage += direction;
if (demo_voltage >= 3.0 || demo_voltage <= 0.5) {
direction = -direction;
}
precision_dac.setTargetVoltage(demo_voltage);
last_demo = millis();
}
// Procesar comandos
processPrecisionCommands();
// Status periódico
static unsigned long last_status = 0;
if (millis() - last_status > 5000) {
precision_dac.printStatus();
last_status = millis();
}
}
void processPrecisionCommands() {
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim();
command.toUpperCase();
if (command == "CAL") {
precision_dac.calibrate(0.5, 2.8); // Calibrar entre 0.5V y 2.8V
} else if (command.startsWith("SET ")) {
float voltage = command.substring(4).toFloat();
precision_dac.setTargetVoltage(voltage);
Serial.printf("Objetivo establecido: %.3fV\n", voltage);
} else if (command == "STATUS") {
precision_dac.printStatus();
}
}
}
Ejercicios Prácticos Interactivos
Objetivo: Crear un generador de tonos de audio usando el DAC para producir diferentes frecuencias audibles.
Aplicación: Sistemas de alarma, indicadores sonoros en equipos industriales.
Materiales Necesarios
- ESP32 DevKit
- Altavoz de 8Ω (0.5W-1W)
- Capacitor 100µF (acoplamiento AC)
- Amplificador LM386 (opcional)
- Resistor 10kΩ (control volumen)
- Potenciómetro 10kΩ
/**
* Ejercicio 1: Generador de Audio Simple
* Genera tonos de diferentes frecuencias para aplicaciones industriales
*/
#include
class AudioToneGenerator {
private:
int dac_pin;
float current_frequency;
float volume;
unsigned long phase_accumulator;
unsigned long frequency_word;
bool tone_enabled;
// Tabla de seno para generar tonos puros
static const int SINE_TABLE_SIZE = 256;
uint8_t sine_table[SINE_TABLE_SIZE];
public:
AudioToneGenerator(int pin) {
dac_pin = pin;
current_frequency = 440.0; // A4 (La central)
volume = 0.5; // 50% volumen
phase_accumulator = 0;
tone_enabled = false;
// Generar tabla de seno
for (int i = 0; i < SINE_TABLE_SIZE; i++) {
float angle = (2.0 * PI * i) / SINE_TABLE_SIZE;
sine_table[i] = (uint8_t)((sin(angle) + 1.0) * 127.5);
}
updateFrequencyWord();
}
void setFrequency(float freq_hz) {
current_frequency = constrain(freq_hz, 20.0, 8000.0); // Rango audible
updateFrequencyWord();
Serial.printf("Frecuencia: %.1f Hz", current_frequency);
printNoteInfo();
}
void setVolume(float vol) {
volume = constrain(vol, 0.0, 1.0);
Serial.printf("Volumen: %.0f%%\n", volume * 100);
}
void enableTone(bool enable) {
tone_enabled = enable;
if (!enable) {
dacWrite(dac_pin, 128); // Centrar en 1.65V cuando está apagado
}
Serial.printf("Tono: %s\n", enable ? "ENCENDIDO" : "APAGADO");
}
void playNote(String note, int octave = 4) {
float frequency = getFrequencyForNote(note, octave);
if (frequency > 0) {
setFrequency(frequency);
enableTone(true);
Serial.printf("Reproduciendo: %s%d (%.1f Hz)\n",
note.c_str(), octave, frequency);
}
}
void update() {
if (!tone_enabled) return;
// Incrementar acumulador de fase
phase_accumulator += frequency_word;
// Obtener índice de tabla
uint8_t table_index = (phase_accumulator >> 24) & 0xFF;
// Obtener muestra de seno
uint8_t sine_sample = sine_table[table_index];
// Aplicar volumen (mezclado con offset DC)
uint8_t dc_offset = 128; // Centro en 1.65V
int8_t ac_component = sine_sample - 128; // Componente AC
ac_component = (int8_t)(ac_component * volume); // Aplicar volumen
uint8_t final_sample = dc_offset + ac_component;
// Enviar al DAC
dacWrite(dac_pin, final_sample);
}
private:
void updateFrequencyWord() {
// Frecuencia de muestreo asumida: 40kHz
// frequency_word = (frequency * 2^32) / sample_rate
frequency_word = (uint32_t)((current_frequency * 4294967296.0) / 40000.0);
}
float getFrequencyForNote(String note, int octave) {
// Frecuencias de las notas musicales (octava 4)
float base_frequencies[] = {
261.63, // C
277.18, // C#
293.66, // D
311.13, // D#
329.63, // E
349.23, // F
369.99, // F#
392.00, // G
415.30, // G#
440.00, // A
466.16, // A#
493.88 // B
};
String notes[] = {"C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"};
int note_index = -1;
for (int i = 0; i < 12; i++) {
if (notes[i] == note.toUpperCase()) {
note_index = i;
break;
}
}
if (note_index == -1) return 0; // Nota no encontrada
// Calcular frecuencia para la octava especificada
float frequency = base_frequencies[note_index];
// Ajustar por octava (octava 4 es la base)
int octave_diff = octave - 4;
for (int i = 0; i < abs(octave_diff); i++) {
if (octave_diff > 0) {
frequency *= 2.0;
} else {
frequency /= 2.0;
}
}
return frequency;
}
void printNoteInfo() {
// Encontrar la nota más cercana
String closest_note = "";
float min_diff = 99999;
for (int octave = 1; octave <= 8; octave++) {
String notes[] = {"C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"};
for (int i = 0; i < 12; i++) {
float note_freq = getFrequencyForNote(notes[i], octave);
float diff = abs(note_freq - current_frequency);
if (diff < min_diff) {
min_diff = diff;
closest_note = notes[i] + String(octave);
}
}
}
if (min_diff < 5.0) { // Si está cerca de una nota
Serial.printf(" (%s)\n", closest_note.c_str());
} else {
Serial.println();
}
}
public:
void printStatus() {
Serial.println("========== GENERADOR DE AUDIO ==========");
Serial.printf("Pin DAC: GPIO%d\n", dac_pin);
Serial.printf("Estado: %s\n", tone_enabled ? "ACTIVO" : "INACTIVO");
Serial.printf("Frecuencia: %.1f Hz\n", current_frequency);
Serial.printf("Volumen: %.0f%%\n", volume * 100);
Serial.printf("Acumulador fase: 0x%08X\n", phase_accumulator);
Serial.println("======================================\n");
}
};
AudioToneGenerator audio_gen(25); // DAC1
// Secuencias predefinidas
struct ToneSequence {
String note;
int octave;
int duration_ms;
};
// Escala cromática ascendente
ToneSequence chromatic_scale[] = {
{"C", 4, 500}, {"C#", 4, 500}, {"D", 4, 500}, {"D#", 4, 500},
{"E", 4, 500}, {"F", 4, 500}, {"F#", 4, 500}, {"G", 4, 500},
{"G#", 4, 500}, {"A", 4, 500}, {"A#", 4, 500}, {"B", 4, 500}
};
// Alarma industrial
ToneSequence alarm_pattern[] = {
{"A", 5, 200}, {"", 0, 100}, {"A", 5, 200}, {"", 0, 100},
{"A", 5, 200}, {"", 0, 500}
};
void setup() {
Serial.begin(115200);
Serial.println("=== GENERADOR DE AUDIO INDUSTRIAL ===");
Serial.println("Comandos disponibles:");
Serial.println("PLAY [nota] [octava] - Reproducir nota (ej: PLAY A 4)");
Serial.println("FREQ [Hz] - Establecer frecuencia directa");
Serial.println("VOL [0-100] - Establecer volumen");
Serial.println("ON/OFF - Encender/Apagar tono");
Serial.println("SCALE - Reproducir escala cromática");
Serial.println("ALARM - Patrón de alarma");
Serial.println("STATUS - Mostrar estado");
Serial.println("====================================");
// Configurar pin DAC
pinMode(25, OUTPUT);
// Tono inicial
audio_gen.playNote("A", 4); // La central
audio_gen.enableTone(false);
}
void loop() {
// Actualizar generador de audio (máxima velocidad)
audio_gen.update();
// Procesar comandos serie
processAudioCommands();
// Manejar secuencias automáticas
static unsigned long sequence_timer = 0;
static int sequence_index = -1;
static ToneSequence* current_sequence = nullptr;
static int sequence_length = 0;
if (sequence_index >= 0 && current_sequence) {
if (millis() - sequence_timer >= current_sequence[sequence_index].duration_ms) {
sequence_index++;
if (sequence_index >= sequence_length) {
// Fin de secuencia
audio_gen.enableTone(false);
sequence_index = -1;
current_sequence = nullptr;
Serial.println("Secuencia completada");
} else {
// Siguiente nota
if (current_sequence[sequence_index].note == "") {
audio_gen.enableTone(false); // Silencio
} else {
audio_gen.playNote(current_sequence[sequence_index].note,
current_sequence[sequence_index].octave);
}
sequence_timer = millis();
}
}
}
}
void processAudioCommands() {
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim();
command.toUpperCase();
if (command.startsWith("PLAY ")) {
// Extraer nota y octava
int space1 = command.indexOf(' ');
int space2 = command.indexOf(' ', space1 + 1);
if (space1 != -1 && space2 != -1) {
String note = command.substring(space1 + 1, space2);
int octave = command.substring(space2 + 1).toInt();
audio_gen.playNote(note, octave);
}
} else if (command.startsWith("FREQ ")) {
float freq = command.substring(5).toFloat();
audio_gen.setFrequency(freq);
audio_gen.enableTone(true);
} else if (command.startsWith("VOL ")) {
float vol = command.substring(4).toFloat() / 100.0;
audio_gen.setVolume(vol);
} else if (command == "ON") {
audio_gen.enableTone(true);
} else if (command == "OFF") {
audio_gen.enableTone(false);
} else if (command == "SCALE") {
startSequence(chromatic_scale, 12);
} else if (command == "ALARM") {
startSequence(alarm_pattern, 6);
} else if (command == "STATUS") {
audio_gen.printStatus();
}
}
}
void startSequence(ToneSequence* sequence, int length) {
static unsigned long sequence_timer = 0;
static int sequence_index = 0;
static ToneSequence* current_sequence = sequence;
static int sequence_length = length;
sequence_index = 0;
sequence_timer = millis();
// Iniciar primera nota
if (sequence[0].note == "") {
audio_gen.enableTone(false);
} else {
audio_gen.playNote(sequence[0].note, sequence[0].octave);
}
Serial.println("Iniciando secuencia...");
}
Objetivo: Implementar control de velocidad de motor DC usando DAC como señal de referencia para un amplificador de potencia.
Aplicación: Control de velocidad variable en cintas transportadoras y sistemas de posicionamiento.
Materiales Necesarios
- ESP32 DevKit
- Motor DC 12V (hasta 2A)
- Driver de motor L298N
- Fuente 12V/3A
- Encoder rotatorio (feedback)
- Display LCD 16x2 I2C
- Potenciómetro 10kΩ
/**
* Ejercicio 2: Control de Motor con DAC
* Sistema de control de velocidad con retroalimentación
*/
#include
#include
#include "driver/adc.h"
#include "esp_adc_cal.h"
// Hardware configuration
const int DAC_SPEED_REF = 25; // DAC para referencia de velocidad
const int MOTOR_DIR_PIN1 = 4; // Control dirección
const int MOTOR_DIR_PIN2 = 16; // Control dirección
const int ENCODER_PIN_A = 18; // Encoder canal A
const int ENCODER_PIN_B = 19; // Encoder canal B
const int SPEED_POT_CHANNEL = ADC_CHANNEL_0; // GPIO36
// Display LCD
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ADC calibration
esp_adc_cal_characteristics_t adc_chars;
class MotorController {
private:
// Control parameters
float target_speed_rpm;
float current_speed_rpm;
float speed_reference_voltage;
// PID control
float kp, ki, kd;
float pid_error;
float pid_integral;
float pid_derivative;
float pid_output;
float last_error;
// Encoder feedback
volatile long encoder_pulses;
unsigned long last_speed_calculation;
int pulses_per_revolution;
// Motor state
bool motor_enabled;
int motor_direction; // 1 = forward, -1 = reverse
// Safety limits
float max_speed_rpm;
float min_speed_rpm;
public:
MotorController() {
target_speed_rpm = 0;
current_speed_rpm = 0;
speed_reference_voltage = 0;
// PID tuning parameters
kp = 0.8;
ki = 0.2;
kd = 0.1;
pid_error = 0;
pid_integral = 0;
pid_derivative = 0;
pid_output = 0;
last_error = 0;
encoder_pulses = 0;
last_speed_calculation = 0;
pulses_per_revolution = 20; // Típico encoder
motor_enabled = false;
motor_direction = 1;
max_speed_rpm = 3000;
min_speed_rpm = 50;
}
void initialize() {
// Configurar pines de motor
pinMode(MOTOR_DIR_PIN1, OUTPUT);
pinMode(MOTOR_DIR_PIN2, OUTPUT);
pinMode(DAC_SPEED_REF, OUTPUT);
// Configurar encoder
pinMode(ENCODER_PIN_A, INPUT_PULLUP);
pinMode(ENCODER_PIN_B, INPUT_PULLUP);
// Interrupciones para encoder
attachInterrupt(digitalPinToInterrupt(ENCODER_PIN_A),
encoderISR_A, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_PIN_B),
encoderISR_B, CHANGE);
// Configurar ADC
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(SPEED_POT_CHANNEL, ADC_ATTEN_DB_11);
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11,
ADC_WIDTH_BIT_12, 1100, &adc_chars);
// Estado inicial
stopMotor();
Serial.println("Motor controller inicializado");
Serial.printf("Max speed: %.0f RPM\n", max_speed_rpm);
Serial.printf("PID gains: Kp=%.2f, Ki=%.2f, Kd=%.2f\n", kp, ki, kd);
}
void update() {
// Leer setpoint de velocidad desde potenciómetro
readSpeedSetpoint();
// Calcular velocidad actual desde encoder
calculateCurrentSpeed();
// Control PID si motor está habilitado
if (motor_enabled && target_speed_rpm > min_speed_rpm) {
performPIDControl();
updateMotorOutput();
} else {
stopMotor();
}
// Actualizar display
updateDisplay();
}
private:
void readSpeedSetpoint() {
// Leer potenciómetro con promediado
uint32_t adc_sum = 0;
for (int i = 0; i < 8; i++) {
adc_sum += adc1_get_raw(SPEED_POT_CHANNEL);
}
uint32_t adc_average = adc_sum / 8;
// Convertir a porcentaje
float percentage = (adc_average / 4095.0) * 100.0;
// Mapear a RPM con zona muerta
if (percentage < 5.0) {
target_speed_rpm = 0;
} else {
target_speed_rpm = ((percentage - 5.0) / 95.0) * max_speed_rpm;
}
}
void calculateCurrentSpeed() {
unsigned long current_time = millis();
if (current_time - last_speed_calculation >= 100) { // Cada 100ms
// Calcular RPM basado en pulsos del encoder
long pulses_delta = encoder_pulses;
encoder_pulses = 0; // Reset counter
float time_delta_minutes = (current_time - last_speed_calculation) / 60000.0;
float revolutions = pulses_delta / (float)pulses_per_revolution;
current_speed_rpm = revolutions / time_delta_minutes;
// Aplicar filtro pasa-bajos
static float filtered_speed = 0;
filtered_speed = 0.8 * filtered_speed + 0.2 * current_speed_rpm;
current_speed_rpm = filtered_speed;
last_speed_calculation = current_time;
}
}
void performPIDControl() {
static unsigned long last_pid_update = 0;
unsigned long current_time = millis();
if (current_time - last_pid_update >= 20) { // 50Hz PID loop
float dt = (current_time - last_pid_update) / 1000.0;
// Calcular error
pid_error = target_speed_rpm - current_speed_rpm;
// Término integral con anti-windup
pid_integral += pid_error * dt;
pid_integral = constrain(pid_integral, -100, 100);
// Término derivativo
pid_derivative = (pid_error - last_error) / dt;
// Salida PID
pid_output = kp * pid_error + ki * pid_integral + kd * pid_derivative;
pid_output = constrain(pid_output, -255, 255);
last_error = pid_error;
last_pid_update = current_time;
}
}
void updateMotorOutput() {
// Determinar dirección
if (pid_output >= 0) {
motor_direction = 1;
digitalWrite(MOTOR_DIR_PIN1, HIGH);
digitalWrite(MOTOR_DIR_PIN2, LOW);
} else {
motor_direction = -1;
digitalWrite(MOTOR_DIR_PIN1, LOW);
digitalWrite(MOTOR_DIR_PIN2, HIGH);
}
// Calcular voltaje de referencia (0-3.3V)
float abs_output = abs(pid_output);
speed_reference_voltage = (abs_output / 255.0) * 3.3;
// Enviar señal DAC
uint8_t dac_value = (uint8_t)abs_output;
dacWrite(DAC_SPEED_REF, dac_value);
}
void stopMotor() {
digitalWrite(MOTOR_DIR_PIN1, LOW);
digitalWrite(MOTOR_DIR_PIN2, LOW);
dacWrite(DAC_SPEED_REF, 0);
pid_integral = 0; // Reset integral
pid_output = 0;
speed_reference_voltage = 0;
}
void updateDisplay() {
static unsigned long last_display_update = 0;
if (millis() - last_display_update >= 200) {
lcd.clear();
// Línea 1: Velocidades
lcd.setCursor(0, 0);
lcd.printf("T:%.0f C:%.0f RPM", target_speed_rpm, current_speed_rpm);
// Línea 2: Estado y PID
lcd.setCursor(0, 1);
if (motor_enabled && target_speed_rpm > min_speed_rpm) {
lcd.printf("PID:%.1f %c %.1fV",
pid_output,
motor_direction > 0 ? 'F' : 'R',
speed_reference_voltage);
} else {
lcd.print("MOTOR DETENIDO");
}
last_display_update = millis();
}
}
// ISR para encoder - debe ser static
static void IRAM_ATTR encoderISR_A() {
// Implementación simplificada de encoder incremental
static bool lastStateA = false;
bool currentStateA = digitalRead(ENCODER_PIN_A);
bool currentStateB = digitalRead(ENCODER_PIN_B);
if (lastStateA != currentStateA) {
if (currentStateA == currentStateB) {
motor_controller_instance->encoder_pulses++;
} else {
motor_controller_instance->encoder_pulses--;
}
}
lastStateA = currentStateA;
}
static void IRAM_ATTR encoderISR_B() {
// ISR para canal B (similar implementación)
}
public:
void enableMotor(bool enable) {
motor_enabled = enable;
if (!enable) {
stopMotor();
}
Serial.printf("Motor %s\n", enable ? "HABILITADO" : "DESHABILITADO");
}
void setDirection(int direction) {
motor_direction = (direction > 0) ? 1 : -1;
Serial.printf("Dirección: %s\n", motor_direction > 0 ? "ADELANTE" : "ATRÁS");
}
void tunePID(float new_kp, float new_ki, float new_kd) {
kp = new_kp;
ki = new_ki;
kd = new_kd;
Serial.printf("PID actualizado: Kp=%.3f, Ki=%.3f, Kd=%.3f\n", kp, ki, kd);
}
void printStatus() {
Serial.println("========== ESTADO DEL MOTOR ==========");
Serial.printf("Estado: %s\n", motor_enabled ? "ACTIVO" : "INACTIVO");
Serial.printf("Velocidad objetivo: %.1f RPM\n", target_speed_rpm);
Serial.printf("Velocidad actual: %.1f RPM\n", current_speed_rpm);
Serial.printf("Error: %.1f RPM\n", pid_error);
Serial.printf("Salida PID: %.1f\n", pid_output);
Serial.printf("Referencia DAC: %.2fV\n", speed_reference_voltage);
Serial.printf("Dirección: %s\n", motor_direction > 0 ? "ADELANTE" : "ATRÁS");
Serial.printf("Pulsos encoder: %ld\n", encoder_pulses);
Serial.println("====================================\n");
}
// Pointer estático para ISR
static MotorController* motor_controller_instance;
};
// Definición del puntero estático
MotorController* MotorController::motor_controller_instance = nullptr;
MotorController motor_controller;
void setup() {
Serial.begin(115200);
Serial.println("=== CONTROL DE MOTOR CON DAC ===");
// Inicializar LCD
lcd.init();
lcd.backlight();
lcd.setCursor(0, 0);
lcd.print("Sistema Motor");
lcd.setCursor(0, 1);
lcd.print("Inicializando...");
// Configurar referencia estática para ISR
MotorController::motor_controller_instance = &motor_controller;
// Inicializar controlador de motor
motor_controller.initialize();
motor_controller.enableMotor(true);
Serial.println("Sistema listo");
Serial.println("Comandos: ENABLE/DISABLE, STATUS, PID [kp] [ki] [kd]");
Serial.println("===============================");
delay(2000);
}
void loop() {
// Actualizar controlador principal
motor_controller.update();
// Procesar comandos serie
processMotorCommands();
// Status periódico
static unsigned long last_status = 0;
if (millis() - last_status > 10000) { // Cada 10 segundos
motor_controller.printStatus();
last_status = millis();
}
}
void processMotorCommands() {
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim();
command.toUpperCase();
if (command == "ENABLE") {
motor_controller.enableMotor(true);
} else if (command == "DISABLE") {
motor_controller.enableMotor(false);
} else if (command == "STATUS") {
motor_controller.printStatus();
} else if (command.startsWith("PID ")) {
// Extraer parámetros PID
int space1 = command.indexOf(' ');
int space2 = command.indexOf(' ', space1 + 1);
int space3 = command.indexOf(' ', space2 + 1);
if (space1 != -1 && space2 != -1 && space3 != -1) {
float kp = command.substring(space1 + 1, space2).toFloat();
float ki = command.substring(space2 + 1, space3).toFloat();
float kd = command.substring(space3 + 1).toFloat();
motor_controller.tunePID(kp, ki, kd);
}
} else if (command == "FORWARD") {
motor_controller.setDirection(1);
} else if (command == "REVERSE") {
motor_controller.setDirection(-1);
}
}
}
Referencias Técnicas Especializadas
Documentación Oficial
Teoría de Conversión DAC
- Analog Devices: "Data Conversion Handbook"
- Texas Instruments: "Understanding DAC Specifications"
- IEEE: "Digital-to-Analog Conversion Techniques"