Crear un sonar en Arduino con un servo y un sensor de ultrasónico HC-SR04

Ya vimos como medir distancias con un sensor ultrasonico, ahora crearemos un sonar con él. Para ello lo pondremos encima de un servo que ira girando el sensor de 5 en 5 grados. Cada movimiento hará tres medidas con el sensor de la cual tomara la mediana (ya vimos que esto se hace para reducir el ruido). Esos datos serán enviados al puerto serie como «angulo, distancia». Para que luego los muestre un programa realizado en processing.

#include <Servo.h> // incluir la biblioteca para controlar el servo

Servo myservo; // declarar un objeto de tipo Servo

int trigPin = 12;  // pin para el trigger del sensor
int echoPin = 11; // pin para el echo del sensor
int angle = 0; // ángulo actual del servo

void setup() {
  Serial.begin(9600); // inicializar el puerto serie
  myservo.attach(9); // conectar el servo al pin 9
  pinMode(trigPin, OUTPUT); // configurar el pin trigger como salida
  pinMode(echoPin, INPUT); // configurar el pin echo como entrada
}

void loop() {
  for (angle = 0; angle <= 180; angle += 5) {
    myservo.write(angle); //movemos el servo
    delay(100); //para que el servo no este moviendose
    int distance = getMedianDistance(); // distancia medida por el sensor
    Serial.print(angle);
    Serial.print(", ");
    Serial.println(distance);
    delay(50);
  }
  for (angle = 180; angle >= 0; angle -= 5) {
    myservo.write(angle); //movemos el servo
    delay(100); //para que el servo no este moviendose
    int distance = getMedianDistance(); // distancia medida por el sensor
    Serial.print(angle);
    Serial.print(", ");
    Serial.println(distance);
    delay(50);
  }
}

int cmp_desc(const void *c1, const void *c2){  
  return *((int *)c2) - *((int *)c1);
}

int getMedianDistance() {
  int distances[3];
  for (int i = 0; i < 3; i++) { //3 mediciones
    long duration, distance;
    digitalWrite(trigPin, LOW);
    delayMicroseconds(2);
    digitalWrite(trigPin, HIGH);
    delayMicroseconds(10);
    digitalWrite(trigPin, LOW);
    duration = pulseIn(echoPin, HIGH);
    distance = (duration / 2) / 29.1;
    distances[i] = distance;
  }

  //ordenamos
  qsort(distances, 3, sizeof(int), cmp_desc);
  
  //tomamos la mediana (el central)
  return distances[1];
}

Para dibujar el resultado usaremos el siguiente programa en processing:

import processing.serial.*;

Serial port;
boolean drawLines = true; // indica si se deben dibujar las líneas
int x0, y0; // coordenadas del punto central
float r = 100; // radio
float angle, distance; // ángulo y distancia leídos desde el puerto serie
float zoom = 2.0; //multiplicamos la distancia para mejorar la visualizacion
int radio = 50; //se usa pra dibujar los circulos del fondo del radar

void setup() {
  size(400, 400);
  x0 = width / 2;
  y0 = height - 50;
  String portName = Serial.list()[0]; // elegir el primer puerto serie disponible
  port = new Serial(this, portName, 9600); // inicializar el puerto serie
  drawBackground();
}

void draw() {
  if (port.available() > 0) {
    String data = port.readStringUntil('\n'); // leer los datos desde el puerto serie
    if (data != null) {      
      String[] values = split(data, ','); // separar los datos en grados y distancia
      if(values.length == 2){ //tiene que haber dos datos
        angle = float(values[0]) / 180 * PI; // convertir los grados a radianes
        if (angle == 0.0) { //si angulo es 0 limpiamos el radar          
          drawBackground();
        }
        distance = float(values[1])*zoom;
        float x = x0 + cos(angle) * distance; // calcular las coordenadas x,y
        float y = y0 - sin(angle) * distance;
        println(distance, angle, x, y);
        stroke(0, 255, 0);
        fill(0, 255, 0);
        ellipse(x, y, 5, 5); // dibujar un punto en las coordenadas calculadas
      }
    }
  }
}

void drawBackground(){ //dibuja el fondo con forma de radar
  background(0);
  noFill();
  radio = 50;
  for (int i = 0; i < 10; i++) {
    stroke(100, 200, 100);
    strokeWeight(2);
    ellipse(x0, y0, 2 * radio, 2 * radio);
    radio += 50;
  }
}

Vamos a centrarnos en la parte que dibuja los datos

Primero vemos si el puerto está disponible, si hay datos en el puesto y si tenemos dos valores separados por una coma:

 if (port.available() > 0) {
    String data = port.readStringUntil('\n'); // leer los datos desde el puerto serie
    if (data != null) {      
      String[] values = split(data, ','); // separar los datos en grados y distancia
      if(values.length == 2){ //tiene que haber dos datos

Convertimos el angulo a radianes:

angle = float(values[0]) / 180 * PI; // convertir los grados a radianes

Ajustamos la distancia para que se vea bien en el canvas:

distance = float(values[1])*zoom;

Finalmente teniendo el angulo y la distancia proyectamos el punto usando, nuestra por todos querida (¿verdad?), trigonometría:

 float x = x0 + cos(angle) * distance; // calcular las coordenadas x,y
 float y = y0 - sin(angle) * distance;

La variables x0 y y0 son las coordenadas del centro de nuestro radar.

El resultado tiene este aspecto:

¡Ojo! Tener en cuenta que la orientación en el radar no tiene porque coincidir con el del sensor, puedes girar el sensor y la imagen de la pantalla no se girará. Seguramente ahora parezca una tontería pero cuando los datos salen invertidos horizontalmente puede resultar confuso.

Puedes ver todo esto en este vídeo de mi canal de Youtube:

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

Medir distancias con Arduino y el sensor de ultrasónico HC-SR04

El sensor HC-SR04 utiliza el principio de medición de tiempo de eco para determinar la distancia a un objeto. Consiste en un emisor de ultrasonidos y un receptor.

Cuando el emisor de ultrasonidos es activado, envía un pulso de sonido de alta frecuencia. Este pulso se refleja en un objeto cercano y vuelve al receptor. El sensor mide el tiempo transcurrido desde que se envió el pulso hasta que se recibió el eco y utiliza esta información para calcular la distancia del objeto.

La distancia se calcula utilizando la fórmula: d = (t * v) / 2 donde t es el tiempo transcurrido desde el envío del pulso hasta la recepción del eco, y v es la velocidad del sonido (en cm por microsegundo). El 2 es porque el sonido tiene que recorrer la distancia 2 veces, una para ir y otra para volver al sensor.

El sensor HC-SR04 tiene dos pines, TRIG (disparador) y ECHO (eco), que se utilizan para enviar y recibir el pulso de sonido, respectivamente. El Arduino se comunica con el sensor mediante estos pines para activar el emisor, medir el tiempo de eco y calcular la distancia.

En nuestro ejemplo conectamos el pin TRIG del sensor al pin 9 del Arduino, el pin ECHO al pin 8, el pin Vcc al pin 5V y el pin GND al pin GND.

En el bucle principal loop(), se envía un pulso de 10 microsegundos al pin TRIG para iniciar la medición de distancia. Luego, se mide el tiempo que tarda el pulso en volver al pin ECHO utilizando la función pulseIn().

A continuación, se calcula la distancia en cm utilizando la fórmula antes vista.

const int TRIGGER_PIN = 8;
const int ECHO_PIN = 9;
const double VEL_SOUND = 34000.0 / 1000000.0;
void setup() {
  Serial.begin(9600);
  pinMode(ECHO_PIN, INPUT);
  pinMode(TRIGGER_PIN, OUTPUT); 
}
void loop() {
  // Envía un pulso de 10 microsegundos al TRIGGER_PIN
  digitalWrite(TRIGGER_PIN, HIGH);
  delayMicroseconds(10);
  // Detiene el pulso
  digitalWrite(TRIGGER_PIN, LOW);
  
  // Lee el tiempo que tarda el pulso en volver al ECHO_PIN
  long duration = pulseIn(ECHO_PIN, HIGH);  
  // Convierte el tiempo en distancia (en cm)
  double distance = (duration * VEL_SOUND ) / 2.0; 
  
  Serial.print("Duracion: ");
  Serial.print(duration);
  Serial.print(" Distancia: ");
  Serial.println(distance);
  delay(100);
}

Los sensores ultrasonidos son una forma barata de medir distancias. Por otro lado para distancias muy cercanas o muy lejanas el sensor puede no funcionar. Tampoco da buenos resultados para objetos en movimiento, o de según que materiales o formas.

Es un sensor expuesto a ruidos. Las mayores fuentes de ruido son los ruidos sonoros ambiente en la misma frecuencia y los ecos.

Para reducirlo vamos a usar un filtro de mediana tomando 3 muestras de cada medida. A mayor número de muestras más seguro estaremos pero más tiempo tardaremos en tener una medida lo cual reducirá la frecuencia de muestreo (no confundir con la frecuencia del sonido enviado)

Vamos a ver el código para implementar un filtro de mediana de en el ejemplo anterior:

const int TRIGGER_PIN = 8;
const int ECHO_PIN = 9;
const double VEL_SOUND = 34000.0 / 1000000.0;
void setup() {
  Serial.begin(9600);
  pinMode(ECHO_PIN, INPUT);
  pinMode(TRIGGER_PIN, OUTPUT); 
}
void loop() {
  // Convierte el tiempo en distancia (en cm)
  long duration = readDuration();
  double distance = (duration * VEL_SOUND) / 2.0; 
  
  Serial.print("Duracion: ");
  Serial.print(duration);
  Serial.print(" Distancia: ");
  Serial.println(distance);
}
long readDuration(){
  long a,b,c;
  a = readSensor();
  delay(50);
  b = readSensor();
  delay(50);
  c = readSensor();
  delay(50);
  return medianFilter(a,b,c);
}
long readSensor(){
  // Envía un pulso de 10 microsegundos al TRIGGER_PIN
  digitalWrite(TRIGGER_PIN, HIGH);
  delayMicroseconds(10);
  // Detiene el pulso
  digitalWrite(TRIGGER_PIN, LOW);  
  
  // Lee el tiempo que tarda el pulso en volver al ECHO_PIN
  long duration = pulseIn(ECHO_PIN, HIGH);  
}
long medianFilter(long a, long b, long c) {
    long temp;
    if (a > b) { // Ordenar a, b
        temp = a;
        a = b;
        b = temp;
    }
    if (b > c) { // Ordenar b, c
        temp = b;
        b = c;
        c = temp;
    }
    if (a > b) { // Ordenar a, b de nuevo
        temp = a;
        a = b;
        b = temp;
    }
    return b; // El valor medio es el segundo de los tres números ordenados
}

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

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

Dibujos en el osciloscopio con Arduino

Ya vimos cómo crear un conversor de digital a analógico barato. Luego con ese conversor vimos cómo crear un generador de funciones. Ahora vamos a tomar ese generador de funciones y a usarlo para crear nuestros dibujos en el osciloscopio.

Los dibujos son algo limitados, podemos simplemente elegir la altura de la linea que dibuja el osciloscopio. Así que no esperéis grandes obras de arte.

Antes de explicar como funciona os dejo el cogido para realizar dibujos:

float f = 100; //frecuencia en Hz
float p = 1/f; //periodo
float t = 0;
int i = 0;
unsigned long oldMicros = 0;
unsigned long nowMicros = 0;
const int WAVE_POINTS = 44; //puntos generados

byte wave[WAVE_POINTS]; //valores generados
double waveDt = p/WAVE_POINTS; //tiempo entre cada punto 

const byte PRESCALER2 = 0b001;

void setup() {
  //ajusta la frec. salida PWM pin 3
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
  
  //Dibujamos la onda  
  wave[i++] = 0;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 100;  
  wave[i++] = 100;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 150;
  wave[i++] = 150;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 150;
  wave[i++] = 150;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 150;
  wave[i++] = 150;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 150;
  wave[i++] = 150;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 100;
  wave[i++] = 100;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 200;
  wave[i++] = 200;
  wave[i++] = 250;
  wave[i++] = 250;
  wave[i++] = 0;
    
}

double dt = 0;

void loop() {
  nowMicros = micros();
  dt = double(nowMicros - oldMicros)/2000000;

  t += dt/waveDt;
  analogWrite(3, wave[int(t)]);

  oldMicros = nowMicros;    

  //reinicia al recorrer todos los puntos
  if(t > WAVE_POINTS-1){ 
    t = 0;
  }
}

En este caso se dibuja un castillo, debajo podéis ver el resultado.

Señal con forma de castillo (más o menos)

Veamos paso a paso como

1 – Ajustar al frecuencia de nuestra señal, se hace en la línea:

float f = 100; //frecuencia en HZ

2 – Indicar el número de puntos que tendrá nuestro dibujo:

const int WAVE_POINTS = 44; //puntos generados

Mi consejo es que pongas el doble de puntos de los necesarios, es buena idea repetir el mismo punto dos veces seguidas, se debe a que el siguiente trozo de código hace que a veces se salte algún valor para mantener la frecuencia de la señal:

t += dt/waveDt;
analogWrite(3, wave[int(t)]);

3 – Rellenamos el array wave que es donde se indica la altura de cada uno de los trozos del dibujo con un valor entre 0 y 255 (0 – 5 V.)

wave[i++] = 0;
wave[i++] = 250;
wave[i++] = 250;
wave[i++] = 200;
wave[i++] = 200;

Es una buena idea que dejéis varios ceros antes del dibujo para separarlo claramente del anterior.

Con estos sencillos pasos ya podéis convertiros en artistas del osciloscopio y crear vuestras obras de arte.

Puede ver el proceso en vídeo en mi canal de Youtube:

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

Clonar un mando de radio frecuencia con Arduino y un receptor/emisor de 433Mhz

Esta técnica es una forma rápida y sencilla de clonar un mando de radiofrecuencia, aunque desgraciadamente no funciona con todos los mandos. Ahora con los que funciona puedes duplicar su funcionamiento en pocos minutos. Además de un Arduino necesitaremos un receptor y un emisor de la frecuencia en la que funcione el mando. En nuestro caso 433 MHz.

Vamos a usar un receptor muy sencillo que tiene cuatro patillas  a parte de +5v y GND necesitamos conectar la patilla por la que recibiremos los datos (en mi caso la que está junto a la de +5v) al puerto digital número 2. Es la patilla cuya interrupción escuchará la librería que vamos a usar: rc-switch. La librería se puede encontrar desde la sección librerías del IDE e instalarla.

Una vez instalada la librería cargaremos el ejemplo ReceiveDemo_Advance.

Una vez cargado abriremos el serial monitor donde deberían aparecer los datos que envía el mando cuando pulsemos el botón. Copiaremos el código decimal, el numero de bits, el protocolo y la duración del pulso. estos datos vamos a meterlos en este programa:

#include <RCSwitch.h>
RCSwitch mySwitch = RCSwitch();

void setup() {
  Serial.begin(9600);
  
  // Pîn digital al que se conecta el emisor
  mySwitch.enableTransmit(10);
  
  // Protocolo
  mySwitch.setProtocol(1);

  // Duracion del pulso
  mySwitch.setPulseLength(503);
  
  // Cuantas veces se repite la transmision
  mySwitch.setRepeatTransmit(10);
  
}

void loop() {
  Serial.println("Enviando señal");
  //código decimal y nº bits
  mySwitch.send(2351425, 24); 
  delay(10000);  
}

Para ejecutarlo necesitaremos un emisor de 433 MHz. en este caso ademas de a 5V y GND conectaremos el pin por el que envía los datos al pin digital 10 de la placa de Arduino.

Puedes ver un vídeo con un ejemplo de su uso y más información en mi canal de Youtube:

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

Comprobar lo rápido que ordena Arduino

Vamos a ver cómo medir lo rápido que es capaz de ordenar Arduino un listado de 400 enteros (con más qsort falla en Arduino UNO). Aprovecharemos esto para ver cómo medir el tiempo que le cuesta ejecutar el código en Arduino.

Evitar «interrupciones»

Lo primero es quitarnos de «en medio» cualquier molestia a nuestro código en Arduino

  • Procesos asíncronos como la comunicación por Serial o el Watchdog
  • Interrupciones

En el caso de tener el Watchdog activado debemos asegurarnos de que no salta mientras probamos el bloque de código que queremos medir. Podemos deactivarlo con wdt_disable();

Con Serial debemos vaciar la cache usando Serial.flush(); para volcarla

En el caso de las interrupciones se puede usar noInterrupts(); para evitar que se lancen.

Como medida de seguridad extra podemos añadir un delay para dar tiempo a que todo termine. Aunque es una medida más paranoica que otra cosa debido a que Arduino es una arquitectura de un solo hilo (a diferencia de los procesadores modernos que tienen múltiples hilos). No hay manera de que quede alguna «tarea pendiente». Pero, al menos yo, me quedo más tranquilo.

En el caso de nuestro ejemplo no tenemos ni watchdog ni interrupciones, pero si comunicación usando Serial.

Medir el tiempo

Para medir el tiempo debemos crear dos variables de tipo unsigned long una guardara el tiempo justo antes de ejecutar el código y otra justo después. Para saber el tiempo total de ejecución del código basta con restarlas. Si el codigo le cuesta ejcutarsemenos de 70 minutos podemos usar micros() si se cuesta mas millis().

Ejemplo

Veamos el ejemplo comentado al principio midiendo el coste de ordenar 400 elementos:

int cmp_desc(const void *c1, const void *c2){  
  return *((int *)c2) - *((int *)c1);
}
int cmp_asc(const void *c1, const void *c2){  
  return *((int *)c1) - *((int *)c2);
}
void setup() {
  Serial.begin(9600);
  Serial.println("Start");
  Serial.flush();
  int array[400] = {32, 162, 26, 82, 85, 220, 157, 149, 142, 255, 160, 66, 151, 37, 204, 0, 242, 92, 34, 24, 124, 14, 19, 173, 187, 160, 223, 225, 59, 151, 237, 245, 125, 35, 143, 125, 210, 53, 129, 136, 126, 174, 88, 138, 17, 132, 68, 116, 154, 7, 77, 31, 167, 59, 147, 165, 232, 59, 184, 169, 175, 116, 77, 110, 118, 211, 19, 111, 142, 247, 0, 120, 229, 138, 43, 173, 210, 98, 218, 75, 142, 101, 204, 14, 30, 130, 102, 142, 214, 176, 153, 178, 3, 199, 135, 191, 149, 22, 230, 64, 15, 165, 225, 187, 85, 204, 69, 144, 147, 9, 49, 134, 208, 171, 151, 231, 217, 37, 42, 68, 135, 102, 212, 216, 51, 177, 137, 242, 117, 154, 241, 32, 119, 210, 5, 180, 63, 209, 180, 39, 106, 11, 232, 6, 216, 249, 223, 113, 29, 78, 210, 138, 69, 2, 63, 37, 91, 146, 155, 243, 92, 174, 143, 122, 102, 130, 203, 168, 186, 255, 17, 167, 162, 41, 158, 81, 56, 213, 209, 11, 248, 108, 146, 82, 230, 159, 132, 17, 168, 165, 161, 190, 4, 53, 181, 149, 52, 64, 219, 215, 72, 4, 79, 186, 8, 16, 40, 32, 82, 115, 56, 138, 46, 126, 255, 124, 21, 85, 211, 13, 21, 21, 111, 227, 88, 128, 247, 158, 188, 210, 196, 190, 24, 38, 198, 81, 168, 245, 174, 40, 74, 236, 78, 68, 48, 44, 130, 34, 133, 118, 215, 242, 168, 21, 123, 84, 77, 140, 30, 83, 94, 29, 94, 138, 46, 223, 228, 13, 2, 70, 87, 74, 47, 100, 193, 86, 80, 237, 130, 142, 152, 239, 113, 114, 133, 160, 217, 34, 161, 214, 168, 92, 216, 178, 67, 188, 110, 136, 183, 147, 127, 209, 88, 102, 133, 196, 15, 66, 237, 189, 208, 0, 98, 147, 116, 130, 214, 231, 58, 150, 227, 155, 117, 133, 42, 98, 114, 254, 17, 80, 113, 63, 215, 190, 35, 171, 89, 180, 91, 26, 147, 39, 126, 66, 34, 1, 139, 87, 183, 129, 153, 106, 219, 245, 143, 182, 62, 99, 27, 82, 198, 234, 158, 122, 16, 119, 254, 241, 170, 186, 197, 192, 46, 133, 179, 54, 236, 35, 34, 97, 48, 150, 19, 26, 235, 17, 15, 182, 201, 151, 30, 40, 94, 188, 192, 149, 220, 250, 16, 35};
  unsigned long timeStart;
  unsigned long timeEnd;
  delay(500);
  timeStart = micros();    
  qsort(array, 400, sizeof(int), cmp_asc);
  timeEnd = micros();  
  Serial.print((timeEnd-timeStart));
  Serial.println();
  
}
void loop()
{
}

Puedes ver el vídeo donde ejecuto el ejemplo y muestro sus resultados:

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

Como ordenar un array en Arduino

A veces tras la librerías oficiales de Arduino nos «tapan» utilidades que tienen las librerías avr-libc del fabricante del controlador. en este caso vamos a usar la función qsort() que implementar el algoritmo quicksort. No entraremos en detalle, simplemente aceptaremos que ordena y lo hace rápido.

qsort(void *base, size_t n_memb, size_t size, cmp_t *cmp)

En este caso la función qsort() requiere 4 parámetros:

  • *base : array de elementos a ordenar
  • n_memb : número de elementos del array
  • size : tamaño de cada elemento
  • cmp : función que realiza la comparación

Como qsort no sabe mágicamente como quieres ordenar lo elementos del array hay que pasar una función de comparación entre dos elementos que tiene que devolver los siguientes valores:

  • Si p1 == p2 devuelve 0
  • Si p1 va antes de p2 devuelve -1 (o culaquier número negativo)
  • Si p2 va después de p1 devuelve 1 (o culaquier número positivo)

La función comparación tiene la sigueinte firma:

int cmp_desc(const void *c1, const void *c2)

Como los parámetros se pasan como un puntero (a void) hay que realizar un cast al tipo de elemento de nuestro array. En nuestro ejemplo int, veamos el código de ejemplo de la función ordenar descendente (primero los números mayores)

int cmp_desc(const void *c1, const void *c2){  
  return *((int *)c2) - *((int *)c1);
}

Veamos un ejemplo completo con ordenación ascendente y descendente:

int cmp_desc(const void *c1, const void *c2){  
  return *((int *)c2) - *((int *)c1);
}

int cmp_asc(const void *c1, const void *c2){  
  return *((int *)c1) - *((int *)c2);
}


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

  int array[10] = {10, 5, 34, 76, 7, 6, 5, 23, 2, 42};
  
  qsort(array, 10, sizeof(int), cmp_asc);
  
  Serial.println("Resultado ascendente: ");
  for(int i = 0; i < 10; i++){
    Serial.print(array[i]);
    Serial.print(", ");
  } 
  Serial.println("");

  qsort(array, 10, sizeof(int), cmp_desc);

  Serial.println("Resultado descendente: ");
  for(int i = 0; i < 10; i++){
    Serial.print(array[i]);
    Serial.print(", ");
  } 
  Serial.println("");
}


void loop()
{
}

Y así de sencillo podemos ordenar cualquier array siempre que podemos hacer una función que compare valores. No necesitamos ningún tipo de librerías externas.

Puedes ver como funciona este código en el siguiente vídeo de mi canal de Youtube:

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

Generador de funciones / señales barato y simple usando Arduino

Ya vimos como convertir de forma barata de PWM a analógico. Ahora que tenemos una salida analógica ¿Podemos usarla para generar distintos tipos de señales? Vamos aintentar generar señales de distinto tipo sinusoidal, triangular, cuadrada (aunque para eso tenemos PWM), triangular, ….

Sin embargo vamos a tener alguna limitaciones:

  • Las limitaciones son que el valor mínimo de salida es 0v y el máximo 5v.
  • La frecuencia no puede ser muy alta. La base de nuestro sistema es un señal cuadrada funciona a 32 kHz y convierte la anchura de la señal (0-100%) a voltaje (0-5v). Para que esto ocurra necesita algunos pulsos, ademas que para reconstruir la señal necesita varios puntos. ¿Cuántos? Depende del tipo de señal, pero tomando la sinusoidal como referencia en mis pruebas el limite es unos 500-600HZ con la primera versión del programa y unos 1500-2000Hz con la segunda versión.

Ya vimos como generar distintas funciones para las ondas, ahora hay que añadir un cambio, las generaremos devolviendo un valor entre 0 y 1. Las funciones modificadas son las siguientes:

//Distintas señales a elegir
double signal = tf; //dientes de sierra
double signal = (sin(tf*2*PI)+1)/2 ; //seno
double signal = sin(tf*PI); //seno positiva
double signal = (sin(tf*2*PI) > 0) ? 1: 0; //cuadrada
double signal = (sin(tf*2*PI) >= 0) ? 2*tf: 2-(2*tf); //triangular

Solo podemos usar una cada vez. Puedes definir tus propias funciones mientras que devuelvan un valor entre 0 y 1. Donde 0 representa 0v y 1 es igual a 5v, cualquier valor intermedio representa un voltaje proporcional. Por ejemplo 0.5 representa 2.5v

Veamos el código:

float t = 0;
float f = 100; //frecuencia
float p = 1/f; //periodo
unsigned long oldMicros = 0;
unsigned long nowMicros = 0;

const byte PRESCALER2 = 0b001;

void setup() {
  //ajusta la frec. salida PWM pin 3
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
}

double dt = 0;
double tf = 0;
double signal = 0;

void loop() {
  nowMicros = micros();
  dt = double(nowMicros - oldMicros)/1000000;
  
  t += dt;
  tf = t*f;
  //Distintas señales a elegir
  //signal = tf; //dientes de sierra
  signal = (sin(tf*2*PI)+1)/2 ; //seno  
  //signal = sin(tf*PI); //seno positiva
  //signal = (sin(tf*2*PI) > 0) ? 1: 0; //cuadrada
  //signal = (sin(tf*2*PI) >= 0) ? 2*tf: 2-(2*tf); //triangular
  
  byte value = byte(255 * signal);
  analog(value);

  oldMicros = nowMicros;
  
  //cuando alcanza un periodo se reinicia
  if(t >= p){ 
    t = 0;
  }
}

//compensa el desajuste de la salida analogica
void analog(byte value){
  value += (255 - value) >> 4; 
  analogWrite(3, value); 
}

Con esta versión se puede alcanzar una frecuencia máxima (probada con una señal sinusoidal) de entre 500-600Hz.

Alcanzando mayores frecuencias

Para intentar exprimir al máximo nuestro Arduino vamos a optimizar el código todo lo posible. Para ello vamos a reducir los cálculos necesarios en cada iteración. Lo haremos precalculando en un array los valores de la onda en varios puntos. Luego en cada iteración calcularemos que punto nos toca mostrar del array. Esta forma da lugar a ondas más «sucias» con más ruido. Pero también permite mayores frecuencias.

float f = 1000; //frecuencia
float p = 1/f; //periodo
float t = 0;
int i = 0;
unsigned long oldMicros = 0;
unsigned long nowMicros = 0;
const int WAVE_POINTS = 2000; //puntos generados

byte wave[WAVE_POINTS]; //valores generados
double waveDt = p/WAVE_POINTS; //tiempo entre cada punto 

const byte PRESCALER2 = 0b001;

void setup() {
  //ajusta la frec. salida PWM pin 3
  TCCR2B = (TCCR2B & 0b11111000) | PRESCALER2;
  pinMode(3, OUTPUT);
  
  //Precalculamos la onda
  for(i = 0; i < WAVE_POINTS; i++){
    t += waveDt;
    double tf = t*f;
    //double signal = tf; //dientes de sierra
    //double signal = (sin(tf*2*PI)+1)/2 ; //seno  
    //double signal = sin(tf*PI); //seno positiva
    //double signal = (sin(tf*2*PI) > 0) ? 1: 0; //cuadrada
    double signal = (sin(tf*2*PI) >= 0) ? 2*tf: 2-(2*tf); //triangular
    wave[i] = analog(byte(255 * signal));
  }
}

double dt = 0;

void loop() {
  nowMicros = micros();
  dt = double(nowMicros - oldMicros)/1000000;

  t += dt/waveDt;
  analogWrite(3, wave[int(t)]);

  oldMicros = nowMicros;    

  //reinicia al recorrer todos los puntos
  if(t > WAVE_POINTS){ 
    t = 0;
  }
}

//compensa el desajuste de la salida analogica
byte analog(byte value){
  value += (255 - value) >> 4; 
  return value;
}

Con esta versión se puede alcanzar una frecuencia máxima (probada con una señal sinusoidal) de entre 1500-2000Hz usando 2000 puntos para la tabla donde se precalculan los valores.

Esta versión tiene el problema de que ocupa casi todas la memoria SRAM de la placa, aunque se pueden reducir el número de puntos se reduce la calidad de la señal.

Ventajas y desventajas

Frente a cualquier generador de funciones barato tiene la desventaja de que la señal es más ruidosa y es posible que no alcance frecuencias tan elevadas.

Por otro lado en la parte de las ventajas esta el precio y en que la forma de la onda y su frecuencia es completamente programable, pudiendo hacerlo que nosotros queremos.

Puede ver un vídeo explicativo donde profundizo más en el tema en mi canal de Youtube:

Haz click para ver el vídeo en Youtube

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