Arduino convertir PWM a analógico por un módico precio.

Antes de leer este post aviso, esta solución es barata pero muy mala, con una salida plagada de ruido. Para aplicaciones que necesiten mejor conversión es mejor adquirir un DAC que lo haga. Sin embargo para «cacharrear» es una buena solución.

Una de las pegas que encuentro a Arduino UNO es no tener una verdadera salida analógica, que permita devolver un voltaje entre 0v y 5v. Lo más parecido que tenemos es PWM que codifica el valor en forma de la anchura (duración) del pulso. Sin embargo es posible convertir esa señal a voltaje. Vamos a ver cómo conseguir una verdadera salida analógica, variación de voltaje, en Arduino UNO conectando un circuito RC (resistencia condensador) muy sencillo a la salida PWM. La parte teórica del asunto es que el circuito RC actúa como un filtro de paso bajo para filtrar la señal PWM y convertirla en un voltaje constante (más o menos). Usando mi simulador de filtros veamos gráficamente lo que ocurre cuando aplicamos a una señal cuadrada un filtro paso bajo con una frecuencia de corte unas 50 veces menor que la señal cuadrada (En el simulador elegimos como señal una onda cuadrada, filtro paso bajo, una frecuencia para el filtro de 0.0075 Hz y quitamos todas las fuentes de ruido el ruido). El resultado es una señal que tras un periodo tiende a ser estabilizarse alrededor de un valor:

Ese valor será proporcional al tamaño en anchura de la parte alta del pulso cuadrado. Por lo que podemos convertir una pulso PWM, que modula su valor como la anchura de la parte alta de la señal, en un voltaje.

El circuito tiene la siguiente forma: (ahora veremos de donde salen esos valores)

Filtro paso bajo RC

Como ya vimos, para calcular la frecuencia de un filtro paso bajo podemos usar la siguiente formula:

f = 1 / (2* Pi * R * C)

Siendo f la frecuencia, C la capacidad del condensador y R la resistencia.

Para nuestro caso f = 1 / (2 * Pi * 4400 * 10^-7) = 361.71 Hz

Pero hemos dicho que la frecuencia de corte ha de ser unas 50 veces menor que la funcionamiento…pero eso es en la teoría. Haciendo pruebas en la vida real va mejor con alrededor de 100 veces. Por lo que PWM debería funcionar a unos 36 kHz. El problema esta que PWM no trabaja a esa frecuencia….a no ser que la cambiemos como vimos en esta entrada Por lo que fijaremos su frecuencia en unos 32 kHz.

El código será el siguiente:

const byte PRESCALER2 = 0b001;
void setup() {
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
}

void loop() {
  analogWrite(3, 255); //5v
  delay(5000);
  analogWrite(3, 204); //4v
  delay(5000);
  analogWrite(3, 153); //3v
  delay(5000);
  analogWrite(3, 102); //2v
  delay(5000);
  analogWrite(3, 51); //1v
  delay(5000);
  analogWrite(3, 0); //0v  
  delay(3000);
}

El resultado tiene bastante ruido y los valores resultantes no son muy exactos. Por lo que no se puede usar para casos donde se requiera gran precisión en el valor.

Para ajustar más los valores podemos usar una pequeña corrección para subir el valor del la señal PWM en los valores más bajos (son los que más se descuadran) y que se aproxime al valor de voltaje que debería tener:

void analog(byte value){
  value += (255 - value) >> 4; 
  analogWrite(3, value); 
}

El ejemplo anterior con esta pequeña corrección:

const byte PRESCALER2 = 0b001;
void setup() {
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
}

void loop() {
  analog(255); //5v
  delay(5000);
  analog(204); //4v
  delay(5000);
  analog(153); //3v
  delay(5000);
  analog(102); //2v
  delay(5000);
  analog(51); //1v
  delay(5000);
  analog(0); //0v  
  delay(3000);
}

void analog(byte value){
  value += (255 - value) >> 4; 
  analogWrite(3, value); 
}

Puedes ver un vídeo sobre este artículo, con demostración del funcionamiento, en mi canal de Youtube:

Haz click para ver el video en Youtube

Modificar la frecuencia del PWM en Arduino.

Vamos a ver cómo modificar la frecuencia a la que trabaja el PWM de Arduino UNO. Para esto es necesario cambiar la frecuencia de uno de los tres timers que posee. Cada uno de ellos controla la frecuencia de dos pines PWM y cumple una función distinta:

TimerPines PWMRegistroFunciones
Timer 0D5, D6TCCR0Bmicros(), milis() y delay()
Timer 1D9, D10TCCR1BServo
Timer 2D3, D11TCCR2Btone()

En la columna «registro» se indica el registro cuyos tres últimos bits controlan el «prescaler» da cada timer. Sin entrar en detalle de como funciona, este prescaler nos permite ajustar la frecuencia de la señal PWM actuando como un divisor de la misma. En las siguientes tablas podemos ver los valores correspondientes. Recordar que los bits CS*2, CS*1, CS*0 corresponden a los tres bits de menor peso del registro de 8 bits.

Timer 0 – TCCR0B

CS02CS01CS00DivisorFrecuencia
000Parado
001162500 Hz
01087812.50 Hz
01164976.56 Hz
100256244.14 Hz
101102461.04 Hz

Timer 1 – TCCR1B

CS12CS11CS10DivisorFrecuencia
000Parado
001131372,55 Hz
01083921,16 Hz
01164490,20 Hz
100256122,55 Hz
101102430,64 Hz

Timer 2 – TCCR2B

CS22CS21CS20DivisorFrecuencia
000Parado
001131372,55 Hz
01083921,16 Hz
01132980,39 Hz
10064490,20 Hz
101128245,10 Hz
110256122,55 Hz
111102430,64 Hz

Ejemplo

En nuestro ejemplo nos vamos a centrar en los pines D3 y D11. ¿Por qué estos dos? Para evitar problemas innecesarios, ambos usan el timer 2 que va asociado únicamente a la función tone(). Además es el timer que más nos permite jugar con sus frecuencias.

El prescaler lo ajustaremos con la siguiente instrucción:

TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;

Donde PRESCALER2 indicara la configuración de los bits CS22, CS21 y CS20. Veamos el código del ejemplo:

const byte PRESCALER2 = 0b001;

void setup() {
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
}

void loop() {
  analogWrite(3, 128);
  delay(1000);
}

Si quieres profundizar más en este tema y saber más sobre Arduino puedes echar un vistazo a mi libro.

Si quiere ver el ejemplo funcionando puedes mirar este vídeo de mi canal de Youtube:

Haz click para ver el vídeo en mi canal de Youtube

Usar Serial Plotter con Arduino

Todos los que usamos el editor de Arduino estamos acostumbrados al Monitor Serie o Serial Monitor para ver los datos que se envían por el puerto serie del Arduino. Pero junto a él, en el menú «Tools», hay otra herramientas que permite ver estos datos forma visual el Serial Plotter que dibuja una gráfica (o varias) con los datos que le devuelva el puerto serie.

Su uso es muy sencillo los datos tiene que seguir ir codificados de la siguiente manera:

  • En cada linea van los datos separados por coma y espacio «, «
  • Cada nueva linea va separada de la anterior por un salto de linea

La forma mas sencilla de entenderlo es con este código:

Serial.print(var1);
Serial.print(", ");
Serial.print(var2);
Serial.println();

Si bien no es una herramienta muy avanzada y tiene bastantes limitaciones sirve para visualizar datos visuales de una forma sencilla. Solo puede representar un tipo de gráfica, el tiempo en el eje horizontal y el valor de la variable en el eje vertical. Con este tipo de gráficas tienes el problema de que algunos datos son muy rápidos y serial plotter no permite almacenar la gráfica para revisarla.

Veamos un ejemplo donde se muestran varias gráficas simultáneamente:

float t = 0;
float oldDataT = 0;
float f = 1; //frecuencia
unsigned long oldMillis = 0;
unsigned long nowMillis = 0;

void setup() {
  Serial.begin(9600); //iniciamos el Serial para mostrar la gráfica
  while (!Serial) {}
}

void loop() {
  nowMillis = millis();
  double dt = double(nowMillis - oldMillis)/1000;
  
  t += dt;
  double tf = t*f;
  double sawtoothW = tf;
  double sinW = sin(tf*2*PI);
  double posSinW = sin(tf*PI);
  double squareW = (sin(tf*2*PI) > 0) ? 1: 0;
  double triangleW = (sin(tf*2*PI) > 0) ? 2*tf: 2-(2*tf);

  if(t - oldDataT > 0.05){ //si no lo datos van demasiado rápidos 
    oldDataT = t;
    Serial.print(sawtoothW);
    Serial.print(", ");
    Serial.print(sinW);
    Serial.print(", ");
    Serial.print(posSinW);
    Serial.print(", ");
    Serial.print(squareW);
    Serial.print(", ");
    Serial.print(triangleW);
    Serial.println();
  }

  oldMillis = nowMillis;    

  if(t >= 1/f){ //cuando alcanza un periodo se reinicia
    t = 0;
    oldDataT = 0;
  }
}

El resultado es el siguiente:

Gráficas en serial plotter

En la imagen se pueden ver gráficas de varias formas generadas desde Arduino.

Un ventaja que tiene este sistema es que los datos son compatibles con el formato CVS, habitual cuando se trabaja con datos.

Añadir guías a las gráficas

Hay algún truco que podemos usar para mejorar la visualización de las gráficas. Establecer guías, una guía no es nada más que una línea recta paralela al movimiento del gráfico y que tiene siempre un valor fijo. Se puede establecer una guía poniendo un valor fijo en los datos que se devuelven. Se usan como referencia visual. Si conoces los máximos y mínimos del valor puedes establecer otras dos guías una en cada valor y así evitar que la gráfica vaya dando «saltos» como a veces ocurre cuando varia mucho el valor.

Puedes ver el vídeo sobre este tema en mi canal de Youtube:

Haz click para ver el vídeo en Youtube

Filtro de media móvil exponencial

Este es posiblemente uno de los filtros más sencillos de implementar que hay. Su funcionamiento es similar al filtro de media móvil, pero solo tiene en cuenta el valor actual V y el anterior Vant. El filtro consiste en multiplicar Vant por un valor a y V por (1-a) y luego sumarlos.

V = ((1-a) * V) + (a * Vant)

Siendo a un valor entre 0 y 1. Cuanto mayor sea el valor de a mayor importancia se le dan a los valores anteriores, sería como aumentar la n, el número de muestras, del filtro de media móvil. Al igual que con la n, a mayor valor tenga a, mayor retardo se produce en la señal. Los valores habituales para a suelen oscilar entre 0,2 y 0,6.

Veamos en nuestro simulador como usarlo para, partiendo de una señal sinusoidal (verde)a la que añadimos ruido (azul) la filtramos usando un filtro de media móvil (naranja):

Filtro de media móvil exponencial

Es un filtro simple, rápido y no requiere prácticamente recursos, pero hay un un caso especial que es computacionalmente aun más rápido. Cuando a vale 0,5, en ese caso el filtro se resume en:

V = (V + Vant) / 2 

La división por dos se puede reemplazar por un desplazamiento a la derecha lo que permite usar números enteros. De esta forma el coste de aplicar este filtro es mínimo.

Filtro paso banda por software

Un filtro paso banda es un filtro que tiene dos frecuencias de corte. Una por debajo, atenúa las frecuencias por debajo de ella y otra encima, atenúa toda frecuencia por encima de ella.

Hay muchas formas de implementarlo, una de las más sencillas es usar dos filtros paso bajo, cada uno de ellos ajustado a una de las frecuencias. La forma de funcionar de este filtro es muy sencilla. Un filtro atenúa todo lo que hay por encima de la frecuencia superior y el otro filtro atenúa todo lo que hay por encima de la frecuencia inferior. Un momento, si queremos quedarnos con las frecuencias entre ambos filtros y el filtro inferior atenúa todo lo que hay por encima de su frecuencia de corte. ¿No está atenuando las frecuencias que queremos conservar? Efectivamente. La idea es que solo queden las frecuencias por debajo de la frecuencia inferior para restarlas del resultado obtenido por el filtro superior. Eso nos dejara solo las frecuencias comprendidas entre ambos filtros.

Veamos un poco de código:

//filtro paso bajo frecuencia inferior
double a_low = dt / (RC_low + dt)
output_low[t] = output_low[t-1] + a * (sensor[t] - output_low[t-1]); 

//filtro paso bajo frecuencia superior
double a_high = dt / (RC_high + dt)
output_high[t] = output_high[t-1] + a * (sensor[t] - output_high[t-1]); 

//restamos
output[t] = output_high[t] - output_low[t]; 

Y ahora usaremos nuestro simulador para hacer una prueba y eliminar ruido. En la imagen resultante podemos ver como la señal original (verde) es afectada por el ruido (azul) y que tras el filtrado (naranja) se elimina gran parte del mismo acercando la forma de la señal a la original.

Filtro paso banda

Filtro paso bajo por software

Un filtro paso bajo es un filtro que fija una frecuencia de corte y deja pasar cualquier frecuencia inferior a esa, mientras que atenúa frecuencias superiores. Al menos esa es la teoría, en la realidad «el corte» no es tan limpio y se ven afectadas las demás frecuencias. En realidad la frecuencia de corte y superiores no sufren una caída completa a cero si no una gran atenuación.

En electrónica el modelo más sencillo de filtro paso bajo es un filtro RC usando una resistencia y un condensador. En la imagen inferior se puede ver como montarla y como afecta a una señal ruidosa reduciendo el ruido de alta frecuencia.

Filtro paso bajo RC

Para realizar nuestro filtro paso bajo por software vamos a modelar matemáticamente un filtro RC.

double a = dt / (RC + dt)
output[t] = output[t-1] + a * (sensor[t] - output[t-1]); 

Donde:

  • dt – Es el tiempo, en sg, entre las muestras t-1 y t
  • RC – Resistencia * Capacidad del condensador
  • output[t] – Es la señal filtrada en el momento t.
  • sensor[t] – Es la entrada sin filtrar en el momento t.

RC en nuestro caso es un valor numérico que emula el comportamiento Para calcular RC partiremos de la frecuencia de corte «fc» que deseemos en Herzios:

RC = 1 / (2 * Pi * fc)

Tras esta visión general vamos a los detalles.

Cuando t = 0 se puede usar como output:

output[0] = a * sensor[0]

Si no necesitamos guardar los valores no hace falta que usemos un array, podemos usar una sola variable output guardando el valor del resultado anterior:

output = output + a * (sensor – output)

Veamos un ejemplo de señal (verde) sinusoidal sometida a ruido de alta frecuencia (azul) y filtrada usando un filtro de paso bajo (naranja):

Ejemplo de filtro paso bajo

Puedes probar aquí un simulador que cree para mostrar como funcionan los diferentes filtros.

Puedes ver esta misma explicación para filtros de paso alto.

Filtro de media móvil

Hemos visto que una forma de reducir el ruido es calcular la media. Pero esto plantea un problema cuando medimos un valor que cambia más rápido de lo que somos capaces de tomar las muestras necesarias. En ese caso hay que cambiar de estrategia, en lugar de capturar n mediciones del mismo valor, se captura solo una y se suman las anteriores n-1 mediciones. Es decir se borra la medición más antigua, se introduce la nueva y se vuelve a calcular la media, de esa forma que el número de mediciones a tener en cuenta para la media nunca cambia.

Los pasos serian los siguientes:

1. Capturar muestras hasta llegar a n muestras
2. Sumar todas las muestras
3. Dividir la suma entre n. Ese es el valor de la señal en ese punto.
4. Eliminar el valor más antiguo
5. Volver al punto 1

Lo ideal es que el tiempo entre mediciones sea el mismo y que sea suficiente para que la señal no varíe demasiado. Si la señal varía de forma continua y suave es posible que el filtro casi no afecte a la forma original de la señal. Sin embargo si hay cambios bruscos puede suavizar estos cambios (por ejemplo en una señal cuadrada). Dependiendo de la duración de la señal, el número de muestras que se usan para calcular la media y el tiempo entre ellas puede afectar más o menos a la señal original.

En la imagen inferior se puede ver como una señal cuadrada (verde) se ve afectada por un filtro de media movil (naranja)

Media móvil de una onda cuadrada

En la siguiente imagen se puede ver como una señal con forma de onda sinusoidal (verde) y su salida tras aplicar un filtro de media movil (naranja). Como la señal es continua y varia de forma suave no se ve casi afectada en la forma, pero si que hay un pequeño desfase en el tiempo, debido a que la media «tarda» en alcanzar el valor original. A mayor sea el número de muestras usadas para calcular la media mayor será este desfase.

Media móvil de una onda sinusoidal

Veamos un ejemplo de la misma señal pero añadiendo ruido de alta frecuencia y aleatorio:

Señal sinusoidal con ruido

Y el resultado tras añadirle un filtro de media movil:

Señal filtrada con un filtro de mediamovil

Para probar este y otros filtros puedes usar este pequeño simulador que cree para ilustrar estos articulos.

El algoritmo KNN ponderado en Arduino

Ya hemos visto como funciona el algoritmo KNN en Arduino, simplemente busca los vecinos más cercanos y mira cuales es la clase más numerosa entre ellos. Esta estrategia puede tener problemas cuando las distintas categorías están muy mezcladas o en puntos que están en la frontera entre dos categorías. En esos casos un punto puede estar muy cerca de unos pocos vecinos de una clase pero la mayoría de vecinos (más lejana) ser de otra clase.

Una forma de solucionar esto es que no todos los vecinos valgan lo mismo. Los más cercanos valen más que los más lejanos. En el algoritmo KNN tradicional cada vecino vale lo mismo «1», en la versión ponderada cada vecino vale una constante fija w dividida por la distancia (w/dist). Ahora sumamos cada uno de esos valores de cada clase y asignamos la clase que más sume. La constate w se usa para evitar que el resultado sea muy pequeño cuando las distancias son muy grandes.

La confianza también cambia, ahora es el valor de la suma de los vecinos de la clase mayoritaria, dividida entre el total de la suma de los pesos de todos los K vecinos.

Un ejemplo lo podemos ver en la imagen de debajo donde los puntos verdes son clasificados como pertenecientes a la clase azul (X) en lugar de a la clase roja (+) si se usa el algoritmo KNN normal, pero son correctamente clasificados si se usa la versión ponderada.

Para K = 3 los puntos verdes son mal clasificados por KNN

La librería ArduinoKNN no contempla este algoritmo por lo que tendremos que implementarlo nosotros usando esta librería como base. Podéis usar la librería Arduino_KNNw que implementa algunas mejoras sobre

En este caso tenemos que usar la función; classifyWeighted(input, K ,w ); Siendo input el elemento a clasificar (un array), K el número de vecinos que se tendrán en cuenta y w la constate por la que se dividirá la distancia, w = 1 por defecto. Si ya sabemos usar Arduino_KNN (si no aquí podéis leerlo) su uso es idéntico:

int class = myKNN.classifyWeighted(input, 3);
float confidence = myKNN.confidence();

Podéis ver un ejemplo aqui.

Asserts de código en Arduino

Un assert es una comprobación que se realiza en el código para verificar que todo es correcto, si la comprobación no es correcta el programa termina. En el caso de Arduino se llama a la función abort() que deshabilita todas las interrupciones y entra en un bucle infinito. Es una medida de precaución para evitar continuar ejecutando un programa cuando algo va mal y no tiene solución o no se sabe que es lo que falla.

Para usar asserts es necesario incluir la librería assert.h. Un assert se escribe usando la función assert pasando como parámetro una comparación lógica que será verdadera o falsa. Si es verdadera el programa continuará, si es falsa se detendrá.

Veamos un ejemplo:

#include <assert.h>
int a = 5;
int b = 1;
int c;

void setup() {
  Serial.begin(9600);
  while (!Serial) {
    ; // esperamos a que el puerto este inicializado
  }
}

void loop() {
  a--;
  divide();
  delay(1000);
}

void divide(){
  assert(a != 0);
  c = b/a;
  Serial.print(b);
  Serial.print("/");
  Serial.print(a);
  Serial.print(" = ");
  Serial.println(c);
}

Este texto mejorado y ampliado forma parte de mi libro sobre como mejorar tus programas en Arduino. Puedes echarle un vistazo aquí.

Puedes echar un vistazo a la versión en vídeo de este entrada:

Haz click para ver el vídeo en mi canal de Youtube

Calcular la altura usando la presión atmosférica y la temperatura. Arduino Nano Sense.

Teniendo un sensor de presión y uno de temperatura podemos hacer el «truco» de estimar la altura de un punto. Para ello usaremos una versión de la ecuación hipsométrica . Para su mayor comprensión voy a des componerla en partes:

Tk = T + 273.15</p>
Pd = (P0 / P) ^ 1/ 5,257
h = (Pd - 1) * Tk / 0,0065

T es la temperatura medida en grados Celsius, Tk es el resultado de convertir a grados Kelvin.

P0 es la presión a nivel del mar o en el nivel que se tome como origen. Ese será el punto h = 0. La presión al nivel del mar es de 1013,25 milibares.

P es la presión en el lugar de la medición.

h es la altura en metros entre los puntos donde se han medido P0 y P.

Esta formula toma alguna simplificaciones, como que T es constante para todas las alturas, despreciar la humedad del aire o considerar la variación de la presión lineal respecto a la altura. Sin embargo suele dar buenos resultados hasta los 9000 metros.

Partiendo del código que ya vimos para leer la temperatura y presión atmosférica con un Arduino Nano Sense solo hemos de añadir los cálculos para hallar la altura:

#include <Arduino_LPS22HB.h>

void setup() {
  Serial.begin(9600);
  while (!Serial);

  if (!BARO.begin()) {
    Serial.println("Failed to initialize pressure sensor!");
    while (1);
  }
}

float hmean = -1;

void loop() {
  // read the sensor value
  float p = BARO.readPressure(MILLIBAR);

  // print the sensor value
  Serial.print("Pressure = ");
  Serial.print(p);
  Serial.println(" kPa");

  float t = BARO.readTemperature();

  // print the sensor value
  Serial.print("Temperature = ");
  Serial.print(t);
  Serial.println(" C");

  float p0 = 1013.25; //milibares
  float tk = t + 273.15;
  float pd = pow((p0 / p), (1 / 5.257));
  float h = (pd - 1) * tk / 0.0065;
  if(hmean == -1){
    hmean = h;
  } else {
    hmean = (hmean + h) / 2;
  }
  Serial.print("h = ");
  Serial.print(hmean);
  Serial.println(" m");
  // print an empty line
  Serial.println();

  // wait 1 second to print again
  delay(1000);
}

La variable h tiene la altura calculada. Pero no se usa directamente, se usa la variable hmean, que calcula la media de los valores anteriores con el actual. Esto es un truco para suavizar los «saltos» que da debido al ruido de los sensores.

En este caso hemos usado P0 como el valor de la presión atmosférica a nivel del mar, sin embargo P0 puede ser medido en cualquier punto y con eso calcular la altura respecto a ese punto.