Módulo 3

DAC: Salida analógica en ESP32

Periféricos Analógicos

ESP32 Mecatrónica IoT UNAM

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ón8 bits (256 niveles)
Número de canales2 canales independientes
Pines de salidaGPIO25 (DAC1), GPIO26 (DAC2)
Rango de voltaje0V - 3.3V
Velocidad máxima1 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 DigitalVoltaje SalidaPorcentaje
00.00V0%
640.83V25%
1281.65V50%
1922.48V75%
2553.30V100%
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

C++ - ESP32 Arduino IDE
/**
 * 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

C++ - Generador de Señales
/**
 * 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

C++ - Filtro Digital
/**
 * 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

01
Generador de Audio Simple
Básico 30-40 min

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Ω
Generador de Audio
/**
 * 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...");
}
02
Control de Motor con Señal Analógica
Intermedio 40-55 min

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Ω
Control de Motor Avanzado
/**
 * 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

Teoría de Conversión DAC
  • Analog Devices: "Data Conversion Handbook"
  • Texas Instruments: "Understanding DAC Specifications"
  • IEEE: "Digital-to-Analog Conversion Techniques"