Generar una tensión variable con Arduino, ESP8266 y ESP32

Modificado por última vez hace 2 meses

Vamos a ver un tema muy necesario para algunos proyectos y es cómo podemos generar una tensión variable con Arduino, ESP8266 y ESP32 utilizando diferentes técnicas.

Por supuesto, aunque he puesto como ejemplos los Arduino (con ATmega328P, como el Arduino Uno, el Arduino Pro, el Arduino Nano), el ESP8266 y el ESP32, lo que veamos aquí será de aplicación para otros microcontroladores o incluso microprocesadores (Raspberry Pi, por ejemplo) adaptándonos a las características de cada plataforma.

Las técnicas se repiten en todas las plataformas, es cuestión únicamente de elegir la que tengamos disponible en aquella con la que estamos trabajando (o elegir entre varias disponibles, sabiendo las diferencias, ventajes e inconvenientes de cada una).

Vamos a ver lo básico que hay que saber sobre la generación de tensión variable con microprocesadores diferentes pero muy similares en su forma de uso.

El problema de generar una tensión variable

Los microcontroladores (y microprocesadores) con los que trabajamos pertenecen al mundo digital, solo saben de ceros y unos (y si hablamos de los voltajes que representan estos ceros y unos, hablaríamos de 0V y de 5V o 3.3V).

Los microcontroladores, de forma nativa, no saben nada del mundo analógico y de voltajes intermedios entre «bajo», o 0 lógico, y «alto», o 1 lógico.

Si a un microcontrolador que funciona a 5V le presentamos en un pin de entrada «normal» una tensión de, por ejemplo, 4.1V la tomará como un estado lógico alto (o 1) y no la diferenciará en absoluto de otra tensión de 4.3V o de otra de 4.7V.

Si en un microprocesador que funciona a 5V queremos generar una tensión de precisamente 5V, no tendremos ningún problema y podremos hacerlo directamente. Ponemos el pin que nos interese «a 1» y ya tendremos 5V en ese pin.

Cuando queramos que sea de 0V, solo tenemos que poner ese pin «a 0» y tendremos 0V en ese pin.

El problema es que, si queremos generar una tensión cualquiera, intermedia, como 2.7V o 1.42V, tendremos que recurrir a diferentes técnicas, más o menos «indirectas», que nos permitan sortear esta limitación.

Por supuesto, también sucederá para leer esas tensiones, pero eso es otra historia y la trataremos en otro post…

Las formas principales de generar una tensión variable

Tenemos, principalmente, tres opciones para generar el voltaje necesario con nuestro microcontrolador:

  1. Generación de voltaje con una señal PWM
  2. Generación de voltaje con un DAC (Conversor Digital Analógico)
  3. Generación de voltaje por modulación Sigma-Delta

No hay una mejor que otra, todas tienen ventajas e inconvenientes y en este artículo veremos las más importantes.

Generación de voltaje con una señal PWM

En el blog tienes varios posts hablando de las señales PWM y de cómo utilizarlas en varias situaciones, aquí nos vamos a centrar en convertir una señal PWM en un voltaje analógico que podamos usar.

Lo bueno del PWM es que todos los microprocesadores de los que estamos hablando (y la mayoría de los existentes) son capaces de generar señales PWM tanto por hardware como por software.

El PWM es uno de los primeros sistemas que se utilizaron con microprocesadores y sigue siendo el más usado cuando se trata de generar voltaje, siempre y cuando sus peculiaridades y limitaciones (de las que ahora hablaremos) lo hagan aceptable.

Si no tienes ni idea de lo que es una señal PWM, te recomiendo la lectura del siguiente artículo antes de seguir:

El sistema para generar el voltaje es muy sencillo, convertimos la longitud del pulso PWM (el ciclo de trabajo) en voltaje y lo utilizamos.

Peculiaridades y limitaciones de las señales PWM para generar tensión variable

El «voltaje PWM»

El problema del sistema PWM es que, si lo usamos «tal cual», lo que genera es un voltaje que «de media» tiene el voltaje que queremos, pero cada vez que la señal está a 1, el voltaje será el que el microcontrolador proporcione (normalmente 5V o 3.3V).

Dependiendo que lo que conectemos a esa salida (el «consumidor» de ese voltaje) esta técnica nos servirá muy bien (como para regular un LED o la velocidad de muchos motores) pero otros dispositivos no lo tolerarán y funcionará mal o se estropearán.

Imaginemos un dispositivo que funciona a 2.5V: si le proporcionamos una señal PWM de 5V con un 50% de ciclo de trabajo, la «media» de voltaje que recibirá será de 2.5V pero a lo mejor no tolera que el 50% del tiempo el voltaje que lo alimenta sea de 5V y se estropea.

Otro ejemplo más practico: si queremos alimentar un microcontrolador que funciona a 3.3V con una señal PWM de 5V y un ciclo de trabajo del 66%) ese microprocesador morirá inmediatamente porque, aunque el «voltaje medio PWM» sea de 3.3V, no tolerará los pulsos de 5V.

Es como si tuviéramos un grifo en el fregadero que entregara el agua » golpes». A lo mejor no hay problema con que el sumidero del fregadero no sea capaz de tragar esa cantidad de agua, pero cada vez que da uno de esos «golpes» lo salpica todo poniéndolo perdido.

Necesitamos convertir el «voltaje PWM» (que se mueve «a golpes» entre 0 y 5V, en un voltaje de 2.5V «real»).

Convertir «voltaje PWM» a «voltaje analógico»

Afortunadamente, solucionar el problema, convirtiendo ese «voltaje a impulsos» en un «voltaje analógico», más suave, es bastante sencillo.

Para la inmensa mayoría de las necesidades podemos hacerlo con un simple filtro paso-bajos que se puede construir con solo un condensador y una resistencia (también llamado filtro R-C por «Resistor|Capacitor»).

El filtro paso-bajo (filtro RC para PWM)

El montaje del filtro paso bajo es muy fácil, aunque calcularlo no es tan sencillo (y es diferente para cada caso), aunque tienes calculadoras en internet que facilitan mucho el trabajo.

Aquí tienes el esquema de un filtro que podrás usar en muchos casos porque funciona bien a 3.3V y a 5V con una señal PWM de 1Khz:

Y aquí puedes ver el efecto que tiene este filtro tan simple:

El canal 1 (en amarillo) corresponde a la señal PWM tal cual es generada por el ESP32.

El canal 2 (en azul) es la salida del filtro, con esa señal aplicada a la entrada.

Cómo puedes ver, la tensión de salida (línea azul) no es una tensión perfectamente estable, sino que tiene ligeras subidas y bajadas coincidiendo con la señal PWM. En este caso la tensión de aproximadamente 1V, tiene pequeñas subidas y bajadas de unos 42mV.

Es posible, con la elección adecuada de los valores del filtro R-C, hacer que la tensión sea más estable (a costa de que la conversión sea más lenta) o que la conversión sea más rápida (a costa de una tensión más inestable).

Aquí puedes ver este filtro construido en una pequeña placa:

La forma de generación de una señal PWM en Arduino, ESP8266 y ESP32 varía muy poco. Más abajo, te dejo el código básico que genera una señal de 1V en cada uno de ellos.

Si quieres puedes saber más sobre los filtro paso bajo en Wikipedia.

La resolución de la señal PWM

Es importante que tengas presente que, dependiendo del microcontrolador que estés utilizando, la resolución puede ser diferente.

A continuación, te hablo de la resolución PWM nativa de los diferentes microcontroladores, pero debes saber que es posible aumentar la resolución mediante técnicas software.

El Arduino Uno utiliza un microcontrolador ATmega328P como su unidad de procesamiento principal, y la resolución del PWM en este microcontrolador es de 8 bits. Esto significa que el rango de valores para el ciclo de trabajo (duty cycle) en las salidas PWM es de 0 a 255.

El término «8 bits» se refiere a la cantidad de bits utilizados para representar el valor de salida en binario. Con 8 bits, hay 2^8 = 256 posibles valores distintos, que van desde 0 hasta 255. En el caso de PWM, estos valores representan el ciclo de trabajo como un porcentaje del período de la señal PWM.

Por ejemplo, para una salida PWM con un valor de 128, el ciclo de trabajo sería del \frac{128}{255} \times 100\%, aproximadamente el 50%. Así, la resolución de 8 bits proporciona 256 pasos distintos para ajustar el ciclo de trabajo, lo que permite un control relativamente fino de la intensidad luminosa en LEDs, la velocidad de motores, entre otras aplicaciones.

El ESP8266, tiene una resolución de PWM de 10 bits. Esto significa que el ciclo de trabajo del PWM se puede ajustar en 1024 niveles diferentes, desde 0 hasta 1023. La resolución de 10 bits proporciona una mayor precisión en la modulación de ancho de pulso, permitiendo un control más fino sobre la salida PWM.

El ESP32 tiene una resolución de PWM de 8 bits por defecto. Esto significa que el valor del ciclo de trabajo puede variar de 0 a 255. La frecuencia del PWM también es configurable en el ESP32. La frecuencia por defecto es de 1 kHz, pero puede ajustarse según las necesidades del proyecto utilizando la función ledcSetup() para configurar el canal PWM y ledcWrite() para establecer el ciclo de trabajo.

Por ejemplo, ledcSetup(0, 1000, 10); configura el canal 0 con una frecuencia de 1000 Hz y una resolución de 10 bits. Luego, ledcWrite(0, dutyCycle); establece el ciclo de trabajo del canal PWM. Ten en cuenta que la frecuencia y resolución del PWM pueden ajustarse según los requisitos específicos de tu proyecto.

Generación una señal PWM en diferentes microcontroladores

Aunque el código necesario se parece mucho entre diferentes microcontroladores, hay varias diferencias que conviene conocer.

También las posibilidades que ofrece cada uno de los microcontroladores son diferentes.

A continuación, te dejo algunos códigos de ejemplo (son códigos orientativos, no todos los he probado adecuadamente)

Generación una señal PWM de 1V en Arduino (ATmega328P)

Para generar una señal PWM de 1V utilizando un Arduino Uno, puedes utilizar la función analogWrite() de Arduino. La resolución de la señal PWM en Arduino Uno es de 8 bits, por lo que el rango de valores para el ciclo de trabajo (duty cycle) va de 0 (completamente apagado) a 255 (completamente encendido).

Para obtener una señal de 1V, debes determinar qué porcentaje del ciclo de trabajo corresponde a 1V en comparación con el rango total de 0 a 5V (estamos utilizando un Arduino de 5V, por lo que el nivel lógico alto corresponderá a 5V). La relación es:

\text{Porcentaje del ciclo de trabajo} = \left( \frac{\text{Valor deseado}}{\text{Valor máximo posible}} \right) \times 100

Entonces, para obtener 1V, puedes utilizar el siguiente código:

const int pwmPin = 5;  // Puedes cambiar el número del pin según tus necesidades
 
void setup() {
  pinMode(pwmPin, OUTPUT);
}
 
void loop() {
  int dutyCycle = map(100, 0, 500, 0, 255);  // Map 0 a 1V en un rango de 0 a 255
  analogWrite(pwmPin, dutyCycle);
 
  delay(1000);  // Espera 1 segundo antes de repetir
}

Este código configura el pin digital 5 (puedes cambiarlo según tus necesidades) como salida PWM y luego utiliza la función analogWrite() para establecer el ciclo de trabajo necesario para obtener 1V. El valor mapeado de 100 se obtiene utilizando la función map() para convertir el rango del ciclo de trabajo de 0 a 255 en el rango de voltage de 0 a 500 (0 a 5V, en milivoltios). La frecuencia de la señal PWM es manejada automáticamente por el hardware del Arduino Uno.

En un Arduino Uno, la frecuencia del PWM estándar es de aproximadamente 490 Hz. Puedes usar la biblioteca TimerOne para cambiar la frecuencia del PWM. Aquí está el código modificado:

#include <TimerOne.h>

const int pwmPin = 5;  // Puedes cambiar el número del pin según tus necesidades

void setup() {
  pinMode(pwmPin, OUTPUT);
  Timer1.initialize(1000);  // Establecer la frecuencia a 1000 Hz
  Timer1.pwm(pwmPin, 127);  // Establecer el ciclo de trabajo al 50% (127 de 255)
}

void loop() {
  // No es necesario cambiar el ciclo de trabajo en el bucle en este caso

  delay(1000);  // Espera 1 segundo antes de repetir
}

En este código, Timer1.initialize(1000); establece la frecuencia del temporizador a 1000 Hz, y Timer1.pwm(pwmPin, 512); establece el ciclo de trabajo a la mitad para obtener una salida de aproximadamente 1V.

Recuerda que necesitarás instalar la biblioteca TimerOne antes de compilar este código. Puedes hacerlo a través del Administrador de bibliotecas de Arduino o mediante PlatformIO.

Generación una señal PWM de 1V en ESP8266

Aquí tienes el código adaptado para un ESP8266.

Ten en cuenta que el ESP8266 utiliza una resolución de 10 bits para el PWM, por lo que el rango de valores para el ciclo de trabajo va de 0 a 1023.

const int pwmPin = D1;  // Puedes cambiar el número del pin según tus necesidades en la placa ESP8266

void setup() {
  pinMode(pwmPin, OUTPUT);
  analogWriteFreq(1000);  // Establecer la frecuencia a 1000 Hz
}

void loop() {
  int dutyCycle = map(205, 0, 1023, 0, 100);  // Map 1V a 205 en un rango de 0 a 1023
  analogWrite(pwmPin, dutyCycle);

  delay(1000);  // Espera 1 segundo antes de repetir
}

Este código configura el pin digital D1 (puedes cambiarlo según tus necesidades) como salida PWM y utiliza la función analogWrite() para establecer el ciclo de trabajo necesario para obtener 1V. El valor mapeado de 205 se obtiene utilizando la función map() para convertir el rango de 0 a 1023 al rango de 0 a 100. La frecuencia de la señal PWM es manejada automáticamente por el hardware del ESP8266.

Generación una señal PWM de 1V en ESP32

Para generar una señal PWM de 1V con una frecuencia de 1000 Hz utilizando un ESP32, puedes usar el siguiente código:

const int pwmPin = 5;  // Puedes cambiar el número del pin según tus necesidades en la placa ESP32

void setup() {
  pinMode(pwmPin, OUTPUT);
  ledcSetup(0, 1000, 10);  // Configurar el canal 0, frecuencia de 1000 Hz, resolución de 10 bits
  ledcAttachPin(pwmPin, 0);  // Asociar el pin al canal 0
}

void loop() {
  int dutyCycle = map(205, 0, 1023, 0, 1023);  // Map 1V a 205 en un rango de 0 a 1023
  ledcWrite(0, dutyCycle);

  delay(1000);  // Espera 1 segundo antes de repetir
}

En este código, ledcSetup(0, 1000, 10); configura el canal 0 con una frecuencia de 1000 Hz y una resolución de 10 bits. Luego, ledcAttachPin(pwmPin, 0); asocia el pin al canal 0. El ciclo de trabajo se ajusta como antes usando ledcWrite(0, dutyCycle);.

Asegúrate de que el pin que estás utilizando en la placa ESP32 sea compatible con PWM. Puedes ajustar el valor mapeado según tus necesidades para obtener la amplitud de 1V deseada.

En un ESP32, la frecuencia máxima del PWM depende de varios factores, incluyendo la resolución del PWM y el canal PWM específico que estás utilizando. La fórmula general para calcular la frecuencia del PWM en un ESP32 es:

\text{Frecuencia} = \frac{\text{Frecuencia del reloj del timer}}{\text{Divisor del timer} \times \text{Contador del timer}}

En el ESP32, los timers y los canales PWM están asociados. Por ejemplo, el Timer 0 está asociado con los canales 0 y 1, el Timer 1 con los canales 2 y 3, y así sucesivamente.

La frecuencia del reloj del timer puede variar y se define en el núcleo del ESP32, y el divisor del timer y el contador del timer son configurables.

Para calcular la frecuencia máxima del PWM en un ESP32 con resolución de 8 bits, puedes usar la siguiente fórmula:

\text{Frecuencia máxima} = \frac{\text{Frecuencia del reloj del timer}}{256 \times \text{Divisor del timer}}

Donde (256) es el número máximo de cuentas para una resolución de 8 bits.

En un caso práctico, si la frecuencia del reloj del timer es de 80 \, \text{MHz} y el divisor del timer es 80 (para propósitos de ejemplo), la frecuencia máxima sería:

\text{Frecuencia máxima} = \frac{80 \, \text{MHz}}{256 \times 80} \approx 3.125 \, \text{kHz}

Es importante señalar que estos valores son ejemplos y pueden variar según la configuración específica que se utilice en tu código. Puedes ajustar la configuración del timer y canal PWM según tus necesidades y restricciones específicas.

Generar una tensión variable por PWM en el ESP32

Para que nos resulte más sencillo hacer pruebas, he preparado el siguiente programa para ESP32.

Nos permitirá ajustar el ciclo de trabajo de la señal PWM a través de una página web y cambiar la resolución de la señal entre 8, 10 y 12 bits.

Además, podremos ver una sencilla representación gráfica de la señal PWM generada.

Solo tenemos que editar el código con el SSID y password de nuestra red wifi, cargarlo en el ESP y conectarnos a la IP del ESP32 desde un navegador web.

Puedes ver la IP del ESP32 en tu router. También puedes verla en el puerto serie del ESP32 cuando se conecta (por ejemplo, con el Serial Monitor de Arduino IDE).

La salida PWM está en el pin 27 del ESP32, puedes cambiarla como te convenga.

Recuerda, antes de grabar el programa en tu ESP32, editar el código con el SSID y contraseña de tu WIFI.

#include <WiFi.h>
#include <ESPAsyncWebServer.h>

// Configura tus credenciales de WiFi
const char* ssid = "nombre_de_tu_wifi";
const char* password = "contraseña_de_tu_wifi";


const int pwmPin = 27;
int pwmResolution = 8;
int pwmFrequency = 1000;

AsyncWebServer server(80);

void setup() {
  Serial.begin(115200);

  // Conectar a la red WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Conectando a WiFi...");
  }
  Serial.println("Conectado a la red WiFi");

  // Imprimir la dirección IP asignada
  Serial.print("Dirección IP: ");
  Serial.println(WiFi.localIP());

  // Configurar el pin como salida PWM
  ledcAttachPin(pwmPin, 0);
  ledcSetup(0, pwmFrequency, pwmResolution);  // Configurar el canal PWM
  ledcWrite(0, 0);

  // Configurar las rutas para el servidor web
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    String html = "<html><head><style>#pwmSlider { width: calc(100% - 40px); margin: 20px; }</style></head><body><h1>Control de PWM</h1>";
    html += "<label for='resolution'>Resolución PWM:</label>";
    html += "<select id='resolution' onchange='updateResolution()'>";
    html += "<option value='8'" + String((pwmResolution == 8) ? " selected" : "") + ">8 bits</option>";
    html += "<option value='10'" + String((pwmResolution == 10) ? " selected" : "") + ">10 bits</option>";
    html += "<option value='12'" + String((pwmResolution == 12) ? " selected" : "") + ">12 bits</option>";
    html += "</select><br>";

    html += "<input type='range' min='0' max='100' value='0' id='pwmSlider' oninput='updatePWM(this.value)' style='width: calc(100% - 40px); margin: 20px;'>";
    html += "<p>Valor PWM: <span id='pwmValue'>0</span></p>";
    html += "<p>Porcentaje: <span id='percentageValue'>0</span>%</p>";
    html += "<canvas id='pwmCanvas' width='600' height='300' style='display: block; margin: 0 auto;'></canvas>";
    html += "<script>function updatePWM(value) {var pwmValue = Math.round(value / 100 * " + String((1 << pwmResolution) - 1) + "); document.getElementById('pwmValue').innerHTML = pwmValue; document.getElementById('percentageValue').innerHTML = value; var xhr = new XMLHttpRequest(); xhr.open('GET', '/setPWM?value=' + pwmValue, true); xhr.send(); drawPWMCanvas(value);}</script>";

    html += "<script>function updateResolution() {var resolution = document.getElementById('resolution').value; var xhr = new XMLHttpRequest(); xhr.open('GET', '/setResolution?value=' + resolution, true); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { location.reload(); } }; xhr.send();}</script>";
    html += "<script>function drawPWMCanvas(value) {var canvas = document.getElementById('pwmCanvas'); var context = canvas.getContext('2d'); context.clearRect(0, 0, canvas.width, canvas.height); var numCycles = 10; var cycleWidth = canvas.width / numCycles; var highWidth = (value / 100) * cycleWidth; for (var i = 0; i < numCycles; i++) { var x = i * cycleWidth; var y = 0; context.fillStyle = 'blue'; context.fillRect(x, y, highWidth, canvas.height); context.fillStyle = 'lightblue'; context.fillRect(x + highWidth, y, cycleWidth - highWidth, canvas.height); }}</script>";
    html += "</body></html>";
    request->send(200, "text/html; charset=UTF-8", html.c_str());
  });

  server.on("/setPWM", HTTP_GET, [](AsyncWebServerRequest *request) {
    String valueStr = request->arg("value");
    int value = valueStr.toInt();
    ledcWrite(0, value);
    request->send(200, "text/plain", "OK");
  });

  server.on("/setResolution", HTTP_GET, [](AsyncWebServerRequest *request) {
    String valueStr = request->arg("value");
    int newResolution = valueStr.toInt();

    // Detener el PWM antes de cambiar la configuración
    ledcWrite(0, 0);
    delay(10);  // Dar tiempo al PWM para detenerse completamente

    // Configurar el nuevo canal PWM
    ledcSetup(0, pwmFrequency, newResolution);
    pwmResolution = newResolution;

    // Reiniciar el PWM
    ledcWrite(0, 0);

    request->send(200, "text/plain", "OK");
  });

  // Iniciar el servidor web
  server.begin();
}

void loop() {
  // Tu código principal puede ir aquí
}

Generar voltaje con un DAC (Conversor Digital Analógico)

Un DAC, o Conversor Digital Analógico, convierte un valor digital en un voltaje analógico.

Simplemente le decimos al DAC el voltaje que queremos y él lo genera.

Algunos microcontroladores incorporan uno o más DAC en el propio hardware, mientras que otros no lo tienen incorporan y es necesario ponerlo externamente.

Por ejemplo, ni el ATmega328P y similares (Arduino Uno, Pro, Micro y Nano) ni el ESP8266, tienen DAC interno.

El ESP32 normal incluye dos DAC idénticos listos para su uso, aunque tienen sus limitaciones.

El DAC interno del ESP32

El ESP32 tiene dos DAC internos de 8 bits, que se pueden utilizar para generar señales analógicas en el rango de 0 a 3.3 voltios.

A diferencia de otros microcontroladores, como el ESP8366 o el ATmega328P del Arduino Uno, que no tienen DAC interno, puede ser una importante ventaja.

Aquí hay una breve descripción de los DAC internos del ESP32:

  1. Resolución:
    • Cada DAC interno tiene una resolución de 8 bits, lo que significa que puede representar valores en un rango de 0 a 255.
  2. Voltaje de referencia:
    • El rango de voltaje para las señales analógicas generadas por los DAC va de 0 a 3.3 voltios, que es el voltaje de alimentación típico del ESP32.
  3. Número de DAC:
    • El ESP32 tiene dos DAC internos, por lo que puede generar simultáneamente dos señales analógicas.
  4. Uso:
    • Los DAC internos del ESP32 se pueden utilizar para diversas aplicaciones, como la generación de señales de audio, la generación de señales de control para dispositivos analógicos, la generación de formas de onda personalizadas y otras aplicaciones que requieran salidas analógicas.
  5. Configuración y Uso:
    • Para utilizar los DAC internos del ESP32, se pueden configurar mediante software. Es posible asignar valores específicos a los DAC para generar diferentes niveles de voltaje analógico.

Para calcular el voltaje de salida de los DAC del ESP32, puedes utilizar la siguiente acuación:

V_{out} = \left( \frac{value}{2^{12}} \right) \times 3.3

Esta ecuación muestra cómo se calcula el voltaje de salida V_{out} en función del valor digital value enviado al DAC, asumiendo una resolución de 12 bits y un voltaje de referencia de 3.3V.

Pines del DAC interno del ESP32

A diferencia de muchas otras cosas de ESP32, los DAC tienen sus salidas cableadas a pines dedicados.

En la mayoría de los módulos ESP32, encontrará el canal 1 en el pin GPIO 25 y el canal 2 en GPIO 26. En el ESP32-S2 el canal 1 está en GPIO 17 y el canal 2 está en GPIO 18.

ESP32 SoCDAC_CH_1DAC_CH_2
ESP32GPIO25GPIO26
ESP32-S2GPIO17GPIO18
Pines del DAC interno del ESP32

A continuación, te dejo un ejemplo muy simple en el que se establece un valor en uno de los DAC internos para generar una salida analógica de 1V en el pin 25 (DAC 1 del ESP32):

// Define el pin del DAC
#define DAC_CH1 25

void setup() {
  int voltaje_deseado = 1000; // Voltaje deseado en milivoltios (por ejemplo, 1000mV = 1V)
  
  // Convierte el valor de voltaje deseado al rango 0-255 para el DAC
  int dac_valor = map(voltaje_deseado, 0, 3300, 0, 255); // El rango 0-3300 es para 0-3.3V
  
  // Establece el valor del DAC con el voltaje deseado
  dacWrite(DAC_CH1, dac_valor);
}

void loop() {
}

Este otro código tan sencillo genera cinco voltajes (cero incluido) con una pausa de 2 segundos entre cambios.

// Define el pin del DAC
#define DAC_CH1 25
 
void setup() {
  // Configura e inicializa el monitor serie
  Serial.begin(115200);
}
 
void loop() {
 
  // Genera cinco voltajes parando dos segundos entre ellos
  dacWrite(DAC_CH1, 0);
  Serial.println("DAC Value 0");
  delay(2000);
 
  dacWrite(DAC_CH1, 64);
  Serial.println("DAC Value 64");
  delay(2000);
 
  dacWrite(DAC_CH1, 128);
  Serial.println("DAC Value 128");
  delay(2000);
 
  dacWrite(DAC_CH1, 192);
  Serial.println("DAC Value 192");
  delay(2000);
 
  dacWrite(DAC_CH1, 255);
  Serial.println("DAC Value 255");
  delay(2000);
}

Por último, te dejo este tercer código, muy útil para hacer pruebas, que te permite elegir el voltaje de salida a través de la interface web.

#include <WiFi.h>
#include <ESPAsyncWebServer.h>

// Configura tus credenciales de WiFi
const char* ssid = "nombre_de_tu_wifi";
const char* password = "contraseña_de_tu_wifi";

// Define el pin del DAC
#define DAC_CH1 25

AsyncWebServer server(80);

void setup() {
  Serial.begin(115200);

  // Conectar a la red WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Conectando a WiFi...");
  }
  Serial.println("Conectado a la red WiFi");

  // Imprimir la dirección IP asignada
  Serial.print("Dirección IP: ");
  Serial.println(WiFi.localIP());

  // Inicializar el DAC interno
  dacWrite(DAC_CH1, 0); // Configurar el DAC interno 1 (GPIO 25) a 0V

  // Configurar las rutas para el servidor web
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    String html = "<html><body><h1>Control de DAC Interno</h1>";
    html += "<input type='range' min='0' max='255' value='0' id='dacSlider' oninput='updateDAC(this.value)' style='width: calc(100% - 40px); margin: 20px;'>";
    html += "<p>Voltios DAC Interno: <span id='dacValue'>0.00</span> V</p>";
    html += "<p>Porcentaje: <span id='percentageValue'>0</span>%</p>";
    html += "<script>function updateDAC(value) {var volts = (value / 255 * 3.3).toFixed(2); document.getElementById('dacValue').innerHTML = volts; document.getElementById('percentageValue').innerHTML = ((value / 255) * 100).toFixed(2); var xhr = new XMLHttpRequest(); xhr.open('GET', '/setDAC?value=' + value, true); xhr.send();}</script>";
    html += "</body></html>";
    request->send(200, "text/html; charset=UTF-8", html.c_str());
  });

  server.on("/setDAC", HTTP_GET, [](AsyncWebServerRequest *request) {
    String valueStr = request->arg("value");
    int value = valueStr.toInt();
    dacWrite(DAC_CH1, value); // Establecer el valor del DAC interno 1 (GPIO 25)
    request->send(200, "text/plain", "OK");
  });

  // Iniciar el servidor web
  server.begin();
}

void loop() {
  // Tu código principal puede ir aquí
}

Modos de operación de los DAC internos del ESP32

El ESP32 DAC tiene 3 modos diferentes de funcionamiento, y los veremos brevemente en este tutorial, aunque sólo utilizaremos el primer modo en este tutorial.

  • Modo de Salida Directa de Voltaje
  • Salida Analógica Continua con DMA
  • Salida en forma de onda coseno con generador de onda coseno (CWG)
Modo de Salida Directa de Voltaje de los DAC del ESP32

Este es el modo más fácil y directo. Solo tienes que decir la salida digital (8-Bit) al DAC y éste lo convertirá al nivel de voltaje analógico correspondiente en el canal de salida DAC que hayas seleccionado. El voltaje permanecerá constante hasta que envíes una nueva órden al DAC. También se conoce como «Modo One-Shot» o «Modo Directo».

Modo de salida continua DMA de los DAC del ESP32

Los canales DAC del ESP32 pueden usar una técnica llamada «DMA» (Acceso Directo a Memoria) para generar las formas de onda de salida analógica sin que la CPU tenga que intervenir demasiado, gracias al DMA. Esto puede funcionar de 3 maneras diferentes:

El ESP32-IDF tiene muy buena documentación y APIs para esos modos y los usaremos en futuros tutoriales.

  1. Escritura síncrona: Los datos en el buffer DMA se transfieren continuamente al DAC (una forma de operación de bloqueo). Esto significa que no se pueden enviar más datos al buffer hasta que la operación de transferencia haya terminado. Es la forma más rápida de transferir un trozo de búfer de audio para su reproducción, por ejemplo.
  2. Escritura síncrona: Los datos en el buffer DMA se transfieren continuamente al DAC (una forma de operación de bloqueo). Esto significa que no se pueden enviar más datos al buffer hasta que la operación de transferencia haya terminado. Es la forma más rápida de transferir un trozo de búfer de audio para su reproducción, por ejemplo.
  3. Escritura cíclica: Los datos se cargan una vez en el búfer DMA y éste seguirá recorriendo los datos y enviándolos al DAC uno a uno hasta que termine. Entonces, repetirá desde el principio del buffer de datos sin necesidad de recargar el buffer, a diferencia de los 2 modos anteriores. Es muy eficiente para aplicaciones de generación de patrones/formas de onda.
Modo generador de onda coseno del ESP32 DAC

El ESP32 DAC tiene integrado un generador de forma de onda coseno que proporciona una forma de onda coseno en cualquiera de los dos canales de salida con frecuencia, amplitud y desplazamiento de fase ajustables. La frecuencia de salida de los DAC del ESP32 en este caso es de 130Hz-100kHz.

Los DAC internos del ESP32 también tienen algunas desventajas, que conviene conocer:

  1. Resolución Limitada:
    • El DAC interno del ESP32 tiene una resolución de 8 bits, lo que significa que puede representar valores en un rango de 0 a 255. Esta resolución limitada puede afectar la precisión de las señales analógicas generadas, especialmente en comparación con DAC de mayor resolución.
  2. Ruido y Linealidad:
    • Los DAC internos a menudo pueden introducir ruido y no linealidades en la señal de salida, lo que puede afectar la calidad de la señal analógica generada. Para aplicaciones sensibles a la calidad de la señal, podría ser necesario considerar DAC externos de mayor calidad.
  3. Número Limitado de DAC:
    • El ESP32 tiene dos DAC internos, lo que puede ser suficiente para muchas aplicaciones, pero podría ser limitante en casos donde se necesitan más canales DAC independientes.
  4. Frecuencia de Muestreo Limitada:
    • La frecuencia máxima de muestreo del DAC interno del ESP32 puede ser limitada en comparación con algunos DAC externos dedicados. Esto podría ser un factor a considerar en aplicaciones que requieran altas tasas de muestreo.
  5. Influencia de la Temperatura:
    • Al igual que muchos componentes electrónicos, el rendimiento del DAC interno del ESP32 puede estar influenciado por cambios en la temperatura ambiente. En entornos donde las condiciones térmicas son extremas, esto podría afectar la precisión de la salida analógica.
  6. Dependencia del Voltaje de Alimentación:
    • El rendimiento del DAC interno puede depender del voltaje de alimentación del ESP32. Variaciones en el voltaje de alimentación podrían afectar la precisión de las señales analógicas generadas.

A pesar de estas limitaciones, el DAC interno del ESP32 sigue siendo útil en muchas aplicaciones y puede ser suficiente para proyectos que no requieran una salida analógica extremadamente precisa o de alta resolución. Para aplicaciones más exigentes, podría ser necesario considerar soluciones externas, como DAC externos.

Una característica importante del DAC del ESP32 (y que a veces será una desventaja y otras veces no, dependiendo del caso de uso) es que no se consiguen cero voltios. La salida siempre tiene algunos mV, aunque la pongamos a cero.

Desde luego, su ventaja principal es que no es necesario ningún hardware adicional y que su programación es muy fácil.

Aplicar un offset al voltaje de salida del DAC del ESP32 con un OpAmp

Como decíamos, la mayor desventaja de los DAC internos del ESP32 es que su salida nunca baja a cero voltios, saliendo siempre por el pin algunos milivoltios, aunque nosotros pidamos un valor de cero (dacWrite(DAC_PIN, 0)).

Existiría una solución fácil a este problema y es utilizar un circuito integrado Amplificador Operacional.

Por ejemplo, el amplificador operacional LM358 que tiene dos amplificadores operacionales en su interior y su especialidad es precisamente hacer cosas con voltajes, (como sumar y restar voltajes).

Rizando el rizo podríamos generar con el microprocesador el voltaje de offset para meterlo en el OpAmp. Probablemente podríamos incluso automatizar la puesta a cero y el cálculo del offset con el propio microprocesador. ¿Bueno, para qué liarnos ahora…?

Utilizar un DAC externo MCP4725 para generar voltaje

Utilizar un DAC externo es la solución perfecta desde un punto de vista de calidad de la tensión generada y facilidad de uso. Es la forma «correcta» de generar un voltaje analógico arbitrario con un microcontrolador.

Algunos de los DACs populares incluyen el MCP4725 de Microchip o el ADS1220 de Texas Instruments. Estos dispositivos son capaces de proporcionar resoluciones de 12 bits (MCP4728) o más (el ADS1220 tiene una resolución de 24 bits).

Utilizando un DAC barato y fácil de encontrar, como el MCP4725, tendríamos 12bits de resolución (212 = 4096 niveles de voltaje distintos).

Por menos de un par de euros podemos añadir esta opción a nuestro proyecto.

El MCP4725 tiene las siguientes características básicas:

  • Resolución de 12 bits (0,0012 V a 5 V)
  • Interfaz I2C
  • Rango de voltaje de alimentación de 2.7 – 5.5V
  • Tiempo de establecimiento de 6 us
  • EEPROM para almacenar un valor de encendido

Con una tensión de alimentación de 5 V es posible seleccionar tensiones de salida entre 0 V y 4,9988 V en pasos de 0,0012 V. En el programa Arduino el voltaje de salida se representa mediante un número entero entre 0 y 4095. La conversión entre este valor y el voltaje real es posible mediante la siguiente ecuación:

V_{\text{out}} = \frac{V_{\text{CC}} \times \text{value}}{4095}

Aquí,

  • V_{\text{out}}​ es el voltaje de salida.
  • V_{\text{CC} es el voltaje de alimentación (VCC) del MCP4725.
  • {\text{value} es el valor digital configurado para el DAC (entre 0 y 4095).

He preparado un pequeño firmware para el ESP32 que te permitirá manejar el MCP4725 y hacer experimentos con él fácilmente.

El programa presenta una interface web en la que puedes seleccionar el voltaje de salida del MCP4725.

Recuerda, antes de grabar el programa en tu ESP32, editar el código con el SSID y contraseña de tu WIFI.

También puedes ajustar los pines I2C que vas a utilizar mediante SDA_PIN y SCL_PIN, si no quieres que estén en los pines 33 y 32.

#include <Wire.h>
#include <Adafruit_MCP4725.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>

// Configura tus credenciales de WiFi
const char* ssid = "nombre_de_tu_wifi";
const char* password = "contraseña_de_tu_wifi";

#define SDA_PIN 33
#define SCL_PIN 32
#define VCC_MCP4725 3.3 // Voltaje de alimentación del MCP4725 en voltios

Adafruit_MCP4725 dac;

AsyncWebServer server(80);

void setup() {
  Serial.begin(115200);

  // Conectar a la red WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Conectando a WiFi...");
  }
  Serial.println("Conectado a la red WiFi");

  // Imprimir la dirección IP asignada
  Serial.print("Dirección IP: ");
  Serial.println(WiFi.localIP());

  // Inicializa la comunicación I2C con los pines personalizados
  Wire.begin(SDA_PIN, SCL_PIN);

  // Configuración del MCP4725
  if (!dac.begin(0x60)) // La dirección I2C del MCP4725 puede variar (por defecto es 0x60)
  {
    Serial.println("No se pudo encontrar el sensor MCP4725. Asegúrate de que esté conectado correctamente.");
  }
  else
  {
    dac.setVoltage(0, false); // Establecer el voltaje en 0, no escribir en EEPROM
    Serial.println("MCP4725 encontrado");
  }

  // Configurar las rutas para el servidor web
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    String html = "<html><body><h1>Control de MCP4725</h1>";
    html += "<input type='range' min='0' max='4095' value='0' id='dacSlider' oninput='updateDAC(this.value)' style='width: calc(100% - 40px); margin: 20px;'>";
    html += "<p>Voltios MCP4725: <span id='dacValue'>0.00</span> V</p>";
    html += "<p>Porcentaje: <span id='percentageValue'>0</span>%</p>";
    html += "<script>function updateDAC(value) {var volts = (value / 4095 * " + String(VCC_MCP4725) + ").toFixed(2); document.getElementById('dacValue').innerHTML = volts; document.getElementById('percentageValue').innerHTML = ((value / 4095) * 100).toFixed(2); var xhr = new XMLHttpRequest(); xhr.open('GET', '/setDAC?value=' + value, true); xhr.send();}</script>";
    html += "</body></html>";
    request->send(200, "text/html; charset=UTF-8", html.c_str());
  });

  server.on("/setDAC", HTTP_GET, [](AsyncWebServerRequest *request) {
    String valueStr = request->arg("value");
    int value = valueStr.toInt();
    dac.setVoltage(value, false); // El segundo parámetro indica si se debe escribir en la EEPROM
    request->send(200, "text/plain", "OK");
  });

  // Iniciar el servidor web
  server.begin();
}

void loop() {
  // Tu código principal puede ir aquí
}

La modulación delta-sigma

El último método de generación de voltaje del que vamos a hablar es la modulación delta-sigma.

Es un tipo de señal en la que el valor analógico lo determina la «densidad de pulsos» de la señal (en vez de la «anchura de pulsos» de una señal PWM).

La principal ventaja de la modulación delta-sigma es que es muy rápida realizando cambios, lo que puede ser interesante para síntesis de sonidos, por ejemplo, pero nosotros no necesitamos esa velocidad para «generar voltajes», que es de lo que trata este artículo.

Una cosa importante es que, como en el caso de la señal PWM, es necesario un filtro a la salida y aunque en ocasiones puede ser suficiente con un filtro R-C (como utilizábamos con la señal PWM), el fabricante del ESP32 sugiere utilizar un filtro paso-bajo bastante más complejo.

Código para generar voltaje mediante modulación delta-sigma

De los microprocesadores que estamos viendo aquí, el único que tiene soporte nativo para generar señales con modulación delta-sigma es el ESP32, ni el Arduino ni el ESP8266 tienen esta posibilidad.

La modulación delta-sigma, también conocida como modulación por densidad de pulso, es una técnica utilizada en la conversión analógica-digital (ADC) y en la modulación de señales analógicas. Esta técnica se destaca por su capacidad para lograr una alta resolución y precisión en la conversión de señales analógicas a digitales, especialmente en aplicaciones donde se requiere una alta relación señal-ruido.

A continuación, te proporciono una breve descripción de cómo funciona la modulación delta-sigma:

  1. Delta (Δ): La modulación delta-sigma toma su nombre de la primera etapa, que implica la formación de la señal delta (Δ). En esta etapa, la diferencia entre la señal de entrada analógica y la señal de retroalimentación se calcula y se utiliza para representar la información de la señal.
  2. Sigma (Σ): La señal delta se suma acumulativamente (se integra) en el dominio del tiempo para formar la señal sigma (Σ). Este proceso se realiza utilizando un integrador.
  3. Modulación por densidad de pulso: La señal sigma se compara con un valor de referencia y se genera una secuencia de pulsos binarios en función de si la señal sigma es mayor o menor que el valor de referencia. Estos pulsos binarios, que representan la información de la señal original, se conocen como pulsos de densidad.
  4. Filtrado y Muestreo: La secuencia de pulsos de densidad se filtra para eliminar las altas frecuencias no deseadas. Luego, la señal filtrada se muestrea a una frecuencia mucho más alta que la frecuencia de Nyquist, lo que permite reconstruir la señal analógica con alta precisión.
  5. Decodificación: En la etapa final, la señal digital resultante se decodifica para obtener la representación digital de la señal analógica original.

La modulación delta-sigma se utiliza comúnmente en la conversión de señales analógicas a digitales en aplicaciones como audio, comunicaciones y sensores. Proporciona una alta resolución y una buena relación señal-ruido, aunque a expensas de una mayor velocidad de muestreo. Este enfoque se ha vuelto muy popular en convertidores analógico-digitales de sobremuestreo (ADC) utilizados en aplicaciones de audio de alta fidelidad y otros sistemas donde la precisión es crítica.

Aunque la modulación delta-sigma no es el objetivo principal del artículo, porque está pensado para la generación de otro tipo de señales, te dejo un ejemplo de código para ESP32, muy básico, para que sepas «qué aspecto tiene».

#include <Arduino.h>

void setup() {
  //Configura delta-sigma en pin 27, canal 0 con una frecuencia de 1000 Hz
  sigmaDeltaSetup(27, 0, 1000);
  // Inicializa canal 0 a 0V
  sigmaDeltaWrite(0, 0);
}

void loop() {
  // Sube el voltaje suavemente
  // Hará overflow a 256
  static uint8_t i = 0;
  sigmaDeltaWrite(0, i++);
  delay(100);
}

Material que he utilizado y que recomiendo

👉 MCP4725

👉 Multímetro OWON XDM2041

👉 Fuente de alimentación RIDEN RD6006W

👉 Osciloscopio Rigol DS1054Z en Amazon España

Deja un comentario