En muchas ocasiones es necesario de disponer de un osciloscopio para medir y analizar las señales que se ejecutan en nuestros programas con Arduino.
Con razón a un experimento; introduciré un ejemplo de cómo podemos llevar a cabo estos análisis para poderlos ver gráficamente a través de Processing.
Este ejemplo dispone de un sensor óptico que detecta los flancos de un encoder pegado a una rueda encoder pegado a un motor.
En mi caso tengo un sensor OPTEK; en cualquiera de sus series OPB; que se pueden encontrar facilmente desguazando un ratón con scroll o impresora viejas. Una vez conseguido, lo tendremos que disponer en un eje giratoria. Para hacer pruebas se puede buscar una manera de medir el paso de luz o no alternativamente.
Cuando ya lo tengamos conectado, el montaje sobre la placa se realiza como aparece en la siguiente imagen.
Ahora deberemos hacer un programa para leer los estados. Primero voy a proporcionar un código en el que se leen los estados en bajo o en alto. Pero seguidamente lo haremos con interrupciones, que es una manera mejor de elaborar este control.
Lectura de estado del sensor
const int Pin = 2; void setup() { Serial.begin(9600); pinMode(Pin, INPUT_PULLUP); } void loop() { bool value = digitalRead(Pin); Serial.write( 0xff ); Serial.write( (value >> 8) & 0xff ); Serial.write( value & 0xff ); }
Como se puede observar, hay unos Serial.write un poco raros. Estas funciones serán las encargadas de imprimir a través del puerto serie a nuestro ordenador los valores actualizados de nuestra rueda y Processing los recopilará para desarrollar una gráfica temporal en el que se podrá ver la evolución de los estados en todo momento.
Copiamos el siguiente código en Processing.
import processing.serial.*; Serial port; // Create object from Serial class int val; // Data received from the serial port int[] values; float zoom; void setup() { size(1280, 480); // Open the port that the board is connected to and use the same speed (9600 bps) port = new Serial(this, Serial.list()[0], 9600); values = new int[width]; zoom = 1.0f; smooth(); } int getY(int val) { return (int)(height - val / 3.0f * (height - 1))-height/3; } int getValue() { int value = -1; while (port.available() >= 3) { if (port.read() == 0xff) { value = (port.read() << 8) | (port.read()); } } return value; } void pushValue(int value) { for (int i=0; i<width-1; i++) values[i] = values[i+1]; values[width-1] = value; } void drawLines() { stroke(255); int displayWidth = (int) (width / zoom); int k = values.length - displayWidth; int x0 = 0; int y0 = getY(values[k]); for (int i=1; i<displayWidth; i++) { k++; int x1 = (int) (i * (width-1) / (displayWidth-1)); int y1 = getY(values[k]); line(x0, y0, x1, y1); x0 = x1; y0 = y1; } } void drawGrid() { stroke(255, 0, 0); line(0, height/2, width, height/2); } void keyReleased() { switch (key) { case '+': zoom *= 2.0f; println(zoom); if ( (int) (width / zoom) <= 1 ) zoom /= 2.0f; break; case '-': zoom /= 2.0f; if (zoom < 1.0f) zoom *= 2.0f; break; } } void draw() { background(0); drawGrid(); val = getValue(); if (val != -1) { pushValue(val); } drawLines(); }
Lectura incremental del sensor
const int Pin = 2; int wheel = 0; bool lastValue; void setup() { Serial.begin(9600); pinMode(Pin, INPUT_PULLUP); lastValue = digitalRead(Pin); } void loop() { bool value = digitalRead(Pin); if(value != lastValue){ wheel++; lastValue = value; } Serial.write( 0xff ); Serial.write( (wheel >> 8) & 0xff ); Serial.write( wheel & 0xff ); }
Este código lo único que hace es leer continuamente el valor leido de nuestro sensor y en el momento de cambio de estado sumar uno a la variable “wheel” que es la encargada de contar en qué parte del giro se encuentra ésta.
Copiamos el siguiente código en Processing en el que solo cambiaremos la escala para poder visualizar un rango amplio de valores.
import processing.serial.*; Serial port; // Create object from Serial class int val; // Data received from the serial port int[] values; float zoom; void setup() { size(1280, 480); // Open the port that the board is connected to and use the same speed (9600 bps) port = new Serial(this, Serial.list()[0], 9600); values = new int[width]; zoom = 1.0f; smooth(); } int getY(int val) { return (int)(height - val / 40.0f * (height - 1)); } int getValue() { int value = -1; while (port.available() >= 3) { if (port.read() == 0xff) { value = (port.read() << 8) | (port.read()); } } return value; } void pushValue(int value) { for (int i=0; i<width-1; i++) values[i] = values[i+1]; values[width-1] = value; } void drawLines() { stroke(255); int displayWidth = (int) (width / zoom); int k = values.length - displayWidth; int x0 = 0; int y0 = getY(values[k]); for (int i=1; i<displayWidth; i++) { k++; int x1 = (int) (i * (width-1) / (displayWidth-1)); int y1 = getY(values[k]); line(x0, y0, x1, y1); x0 = x1; y0 = y1; } } void drawGrid() { stroke(255, 0, 0); line(0, height/2, width, height/2); } void keyReleased() { switch (key) { case '+': zoom *= 2.0f; println(zoom); if ( (int) (width / zoom) <= 1 ) zoom /= 2.0f; break; case '-': zoom /= 2.0f; if (zoom < 1.0f) zoom *= 2.0f; break; } } void draw() { background(0); drawGrid(); val = getValue(); if (val != -1) { pushValue(val); } drawLines(); }
Ahora podremos visualizar el siguiente resultado mientras hacemos girar la rueda. El número de ranuras son 20, y como estamos elaborando un control de cambio de estados a lectura con ranura y sin ranura, multiplicando por 2 tendremos el giro de una vuelta completa de la rueda. Por ello la escala de la gráfica está limitada a 40. En el momento que superemos 40 ranuras llegará a arriba del todo.
De todas maneras, hemos comentado que hay un método mejor para recopilar la información de la señal para este caso; y es con interrupciones.
Las interrupciones son unos pines especiales capaces de detectar un evento ocurrido en la señal para ejecutar una acción. Por ejemplo, cada vez que cambia el estado de un sensor, se puede ejecutar la función sumar una unidad a un contador, como hacemos en el código anterior.
La mayor ventaja de este procedimiento es que la interrupción asociada al pin recoge ese evento e interrumpe la ejecución natural del código para actualizar ese estado y prioriza la función que se ha declarado dentro de la misma.
Existen 4 eventos dentro de una interrupción.
- LOW –> Dispara la función siempre y cuando el Pin está en bajo.
- CHANGE –> Dispara la función cuando el estado cambia. De bajo a alto o de alto a bajo indiferentemente.
- RISING –> Dispara la función cuando el estado cambia de bajo a alto.
- FALLING –> Dispara la función cuando el estado cambia de alto a bajo.
- HIGH (SOLO PARA ARDUINO DUE) –>Dispara la función siempre y cuando el Pin está en alto.
Hay que tener en cuenta que cada placa tiene un número de interrupciones definidos.Y no es casualidad haber escogido el Pin 2 para este ejercicio, ya que la placa Arduino UNO, tiene solo dos interrupciones en el PIN 2 y en el PIN 3.
El caso que nuestro código en Arduino quedará de esta nueva forma y ejecutará lo mismo, pero mejor.
const byte IntPin = 2; volatile int wheel = 0; void setup() { Serial.begin(9600); attachInterrupt(digitalPinToInterrupt(IntPin), counterwheel, CHANGE); } void loop() { Serial.write( 0xff ); Serial.write( (wheel >> 8) & 0xff ); Serial.write( wheel & 0xff ); } void counterwheel(){ wheel++; }
Seguidamente expondré otro ejemplo que se puede realizar para establecer el contador a cero cuando demos una vuelta completa; y de esta manera podamos visualizarlo en la gráfica en sucesivos giros.
const byte IntPin = 2; volatile int wheel = 0; void setup(){ Serial.begin(9600); attachInterrupt(digitalPinToInterrupt(IntPin), counterwheel, CHANGE); } void loop() { Serial.write( 0xff ); Serial.write( (wheel >> 8) & 0xff ); Serial.write( wheel & 0xff ); if (wheel >= 40){ wheel =0; } } void counterwheel(){ wheel++; }
Como se puede ver en la siguiente gráfica, cada subida es una vuelta completa a la rueda.
En la siguiente entrada aprovecharemos este código para realizar un control con PID.
2 comments