Algunas placas, como la conocida Raspberry Pi, añaden al repertorio de características de los sistemas embebidos o empotrados más convencionales muchas prestaciones genéricas, propias de una plataforma de escritorio, a la vez que exponen diferentes recursos de acceso al hardware, ya sean de tipo inespecífico, vía GPIO, o las habituales comunicaciones entre circuitos como I2C o SPI.
Este tipo de dispositivos, que disfrutan actualmente de una importante expansión gracias, en gran medida, al interés despertado por la placa Raspberry Pi, son un punto intermedio entre un pequeño sistema de escritorio, especialmente interesante por sus características gráficas, y una placa de desarrollo con la que se pueden obtener datos de sensores así como controlar actuadores. Estos dispositivos son muy útiles, por ejemplo, como nodos en la Internet de las cosas (IoT), pequeños servidores, clientes ligeros o quioscos interactivos.
Para explotar las prestaciones de estos equipos, Processing puede ser un sistema de desarrollo muy práctico ya que, por estar basado en Java, puede funcionar en sistemas GNU/Linux sobre la arquitectura ARM, la elegida habitualmente para la CPU de estos dispositivos, y porque es una opción muy sencilla para la programación de gráficos interactivos con los que resolver bien el GUI de un pequeño sistema de control.
Contenidos
- Preparar el sistema para usar Processing con la librería Hardware I/O
- Instalar la librería Hardware I/O en Processing
- Instalar y configurar la herramienta Upload to Pi en Processing
- Entrada y salida digital genérica (GPIO)
- Gestionar los LED del SBC desde Processing
- Modulación por ancho de pulso (PWM)
- Control de servomotores desde Processing
- Comunicaciones I2C
- Comunicaciones SPI
Preparar el sistema para usar Processing con la librería Hardware I/O
En función del modelo de SBC y de la distribución Linux seleccionados, puede que el sistema esté listo para usar o sea necesario configurarlo, en particular, es posible que sea necesario instalar un entorno de escritorio, normalmente uno ligero, frecuentemente LXDE.
En la guía de polaridad.es para instalar GNU/Linux en un SBC se explican los pasos genéricos para instalar y configurar un sistema genérico basado en Debian tomando como referencia Armbian, aunque la información expuesta puede aplicarse fácilmente a otros, como Raspbian, también basado en Debian.
Una vez instalado y configurado el sistema, cuando se dispone de un entorno gráfico, instalar Processing es trivial: basta con descargar Processing de su web, utilizando un navegador, y descomprimirlo en la carpeta que se desee, seguramente desde el propio explorador de archivos. En cualquier caso, también sería posible instalarlo desde la consola (especialmente útil si se trabaja en remoto) haciendo referencia a la versión correspondiente, y usando la secuencia de órdenes:
wget http://download.processing.org/processing-3.2.3-linux-armv6hf.tgz
tar zxvf processing-3.2.3-linux-armv6hf.tgz
Instalar la librería Hardware I/O en Processing
La filosofía de Processing incluye el criterio, por otra parte bastante extendido, de separar del grupo principal de características las que no correspondan más directamente al objetivo central de la herramienta: el desarrollo de gráficos interactivos. Parece lógico, por tanto, que el acceso al hardware se haya incluido en la librería Hardware I/O, desarrollada por la propia Processing Foundation, que hay que instalar expresamente para añadir sus métodos a Processing.
La instalación de la librería Hardware I/O se realiza desde el panel Contribution Manager. A este panel se puede acceder, por ejemplo, desde Añadir biblioteca, que está en la entrada Importar biblioteca del menú Sketch del PDE (Processing Development Environment).
En la solapa Libraries del panel Contribution Manager aparece una lista con las librerías disponibles. En la columna de la izquierda se indica con una marca las instaladas, en la del centro el nombre y una breve descripción y en la de la derecha el autor. Las librerías desarrolladas por la Processing Foundation muestran su logo en esta última columna.
Para instalar un nuevo componente basta con seleccionarlo de la lista y luego pulsar el botón Install. Si la librería ya está instalada, al seleccionarla en la lista se mostrará la última versión disponible en el cuadro inferior de la derecha y se puede actualizar (siempre que exista una nueva versión) con el botón Update. Igualmente, la librería seleccionada se puede desinstalar con el botón Remove, también en el cuadro de botones que se encuentra en la zona inferior derecha del panel Contribution Manager.
Una vez instalada la librería, para usarla desde un sketch de Processing, hay que incluirla con import
utilizando la expresión import processing.io.*;
Instalar y configurar la herramienta Upload to Pi en Processing
Además de utilizar Processing en el sistema objetivo, el que corre en la placa en la que funcionará la aplicación que se desarrolle con la librería Hardware I/O, también es posible utilizar un sistema de escritorio con Processing, que tenga acceso de red a la placa, escribir en él el programa y enviarlo al sistema objetivo con ayuda de la herramienta Upload to Pi, herramienta que, pese a no ser imprescindible para usar la librería Hardware I/O, puede hacer un poco más cómodo el proceso de trabajo.
Para instalar la herramienta Upload to Pi de Gottfried Haider en el sistema de escritorio se utiliza, igual que se hizo para la librería Hardware I/O, el panel Contribution Manager, en este caso usando la solapa Tools en lugar de usar la solapa Libraries. Al panel Contribution Manager se puede llegar, por ejemplo, desde la entrada Añadir herramienta, del menú Herramientas.
Igual ocurre con el resto de componentes, incluyendo la librería Hardware I/O, instalada en el paso anterior, para instalar la herramienta Upload to Pi basta con seleccionarla de la lista y pulsar sobre el botón Install. Para actualizarla o elimarla, si ya está instalada, se pulsa sobre el botón Update o sobre el botón Remove respectivamente.
Una vez instalada la herramienta Upload to Pi y antes de usarla, es necesario configurarla, al menos en lo relativo al acceso al sistema remoto (la placa Raspberry Pi o el sistema equiparable). La configuración se encuentra en el documento de preferencias de Processing, normalmente en la carpeta .processing de la carpeta del usuario, cuyo nombre y ubicación específico puede consultarse en el panel de preferencias al que se accede desde el menú Archivo.
El nombre de las entradas del documento con las preferencias que afectan a la herramienta Upload to Pi están precedidas por gohai.uploadtopi. Para tener acceso al sistema remoto hay que establecer, al menos, el nombre de usuario en el sistema remoto con gohai.uploadtopi.username, la clave en gohai.uploadtopi.password, y el nombre del sistema (si puede resolverse por el archivo hosts o por un DNS) o la dirección IP en gohai.uploadtopi.hostname. El resto de los parámetros no son imprescindibles y dependerán del criterio del desarrollador aunque, normalmente, se incluirá también la opción de hacer log al sistema local con gohai.uploadtopi.logging.
Para que la configuración sea estable, es interesante asignar a la placa sobre la que se desarrolla una dirección IP fija, preferiblemente en el servidor de nombre de dominio (DNS), utilizando como identificador su dirección MAC. Aunque no es imprescindible, esta forma de asignar la dirección IP facilita el acceso remoto a las diferentes placas (a cualquier sistema, en general) y simplifica la creación de una estructura en la red con una mínima complejidad. Si por cualquier causa no estuviera disponible esta opción, será necesario establecer una dirección IP fija en la configuración de red.
Además de Processing, en el sistema remoto debe estar instalado también el SDK de Java para que pueda ser usado por la herramienta Upload to Pi. Para instalar el kit de desarrollo Java abierto OpenJDK en una distribución Linux Debian o Ubuntu se pueden utilizar las órdenes:
sudo apt-get install default-jdk
si se opta por la versión por defectosudo apt-get install openjdk-8-jdk
para instalar una versión concreta (la 1.8 en este caso)
La herramienta Upload to Pi espera encontrar en el sistema remoto la versión modificada para la placa Raspberry Pi del entorno de escritorio LXDE, cuya configuración utiliza la carpeta LXDE-pi en lugar de la carpeta LXDE que es la que utiliza por defecto este entorno. Si se utiliza una distribución Linux más genérica (o sobre una placa diferente de la Raspberry Pi) se puede crear un enlace simbólico para que la herramienta Upload to Pi encuentre la carpeta LXDE-pi sin tener que modificar la carpeta original.
ln -s ~/.config/lxsession/LXDE ~/.config/lxsession/LXDE-pi
Una vez instalada y configurada la herramienta Upload to Pi ya puede utilizarse desde el entorno de desarrollo de Processing (PDE) utilizando la entrada Upload to Pi que se habrá creado en el menú Herramientas después de instalarla.
La herramienta Upload to Pi se encargará de grabar el programa en el sistema remoto, crear el ejecutable y lanzarlo, también en el sistema remoto. En su caso, desde el sistema local se podrá seguir el informe del funcionamiento (log) en la consola de Processing.
Entrada y salida digital genérica (GPIO)
Como ya se ha dicho, para poder utilizar la librería Hardware I/O en un sketch de Processing hay que incluirla primero con import
usando una expresión como import processing.io.*;
, para cargar todas las clases que contiene la librería o, para este caso, import processing.io.GPIO;
, que cargará solamente la clase GPIO
, que corresponde a la gestión de los GPIO.
Antes de utilizar cada GPIO hay que decidir si trabajará como entrada o como salida. Para establecer el modo de entrada o de salida en una patilla, se utiliza el método GPIO.pinMode
que espera como parámetros el número del pin y el modo: GPIO.INPUT (entrada) o GPIO.OUTPUT (salida).
El número de pin, su identificador, puede ser diferente entre distintos SBC y modelos. Además, algunas patillas pueden tener varias asignaciones, así que habrá que consultar previamente la documentación del SBC y asegurarse de su función. En el caso de la Raspberry Pi 3 B el pinout corresponde con la ubicación y la tabla de las siguientes imágenes.
En el esquema anterior se muestra la ubicación del conector en una Raspberry Pi 3 B y la distribución de los GPIO, las patillas conectadas a GND y las de tensión. Como algunas patillas tienen varias funciones alternativas, no necesariamente todos estarán disponibles para utilizarse como GPIO.
Cuando se haya determinado que, de entre las alternativas, el uso de un pin es como GPIO
es necesario establecer si trabajará como entrada o como salida con GPIO.pinMode
y ya será posible recibir datos o enviarlos. Para leer un GPIO se utiliza el método GPIO.digitalRead
, que espera como argumento el número de pin y devuelve como resultado el estado de esa patilla, GPIO.HIGH, un entero de valor 1, cuando el pin se encuentra en nivel alto, o GPIO.LOW, un entero con valor 0, que se devuelve cuando el nivel del estado del pin es bajo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// Pin 11 (GPIO 17) conectado en serie con una resistencia de 10 KΩ a GND y a un pulsador a 3V3 import processing.io.GPIO; int PIN_ENTRADA=17; float VELOCIDAD=10.0; color VERDE=color(100,255,100); color ROJO=color(255,100,100); void setup() { size(100,100); // Usar valores enteros, no variables GPIO.pinMode(PIN_ENTRADA,GPIO.INPUT); // GPIO como entrada frameRate(VELOCIDAD); } void draw() { if(GPIO.digitalRead(PIN_ENTRADA)==GPIO.HIGH) // Si el estado es alto cambiar el fondo a rojo y si no (si es bajo) a verde { background(ROJO); } else { background(VERDE); } } |
Para establecer el estado de un pin configurado como salida se utiliza GPIO.digitalWrite
, que espera como parámetros de entrada el número del pin y el estado: GPIO.HIGH (o también 1 y true como equivalentes) GPIO.LOW (también 0 y false).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Pin 9 (GND) conectado al cátodo de un LED y pin 11 (GPIO 17) a una resistencia de 220 Ω en serie con el ánodo del LED import processing.io.GPIO; boolean estado=false; int PIN_SALIDA=17; float VELOCIDAD=2.5; void setup() { GPIO.pinMode(PIN_SALIDA,GPIO.OUTPUT); // GPIO como salida frameRate(VELOCIDAD); } void draw() { GPIO.digitalWrite(PIN_SALIDA,estado); estado=!estado; } |
Para que otras aplicaciones del sistema puedan utilizarlo, lo correcto es liberar el GPIO cuando ya no esté en uso. Para liberar un GPIO que se ha reservado al declararlo como entrada o como salida se utiliza el método GPIO.releasePin
que espera como argumento de entrada el número del pin.
Gestionar con interrupciones las entradas de los GPIO
En el ejemplo de uso de digitalRead
el color de fondo se establece en función del estado de un GPIO pero solamente se comprueba el estado al dibujar (cada fotograma de la) ventana con draw
el número de veces indicado por frameRate
. Por alta que sea la velocidad con la que se refresca la ventana del sketch de Processing siempre sería posible que el estado cambiara más rápido (hasta la frecuencia del bus correspondiente). Al contrario, también se redibuja la ventana haya o no un cambio en el estado del GPIO, lo que podría suponer un uso inapropiado (excesivo) de la CPU del SBC.
Las interrupciones permiten ejecutar cierta parte del código, un método, cuando cambia el estado de un GPIO con lo que se resuelven los problemas anteriores: no se ejecuta el código si no hace falta y se ejecuta siempre que hace falta. Aunque esta segunda afirmación tiene sus matices, que se aclaran más adelante, por el momento supongamos que es correcta.
Para asignar un método al cambio de un GPIO, por medio de una interrupción, se utiliza el método GPIO.attachInterrupt
, que espera como argumentos ① el número del pin cuyo estado se va a gestionar, ② la clase que contiene el método llamado (normalmente this
, porque el método suele pertenecer a la clase actual, el sketch de Processing que se está escribiendo), ③ el nombre del método como una cadena de texto y ④ el cambio de estado que genera la interrupción. Los posibles cambios de estado se representan con GPIO.RISING para monitorizar el flanco ascendente (el cambio del nivel bajo al nivel alto), GPIO.FALLING para atender al flanco descendente o GPIO.CHANGE si se desean detectar los cambios en ambos flancos: el cambio de nivel de alto a bajo o de bajo a alto.
El método que se llama cuando se lanza la interrupción utiliza como argumento de entrada el número de pin que genera la interrupción. Esto permite que el mismo método pueda atender a las interrupciones generadas por diferentes patillas, generalizando y reutilizando el código siempre que sea posible. No es necesario (ni tampoco es posible) incluir este parámetro entre los argumentos de entrada del método GPIO.attachInterrupt
, que solamente acepta el nombre como texto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import processing.io.GPIO; int PIN_ENTRADA=17; color VERDE=color(100,255,100); color ROJO=color(255,100,100); color color_fondo=color(100,100,255); void setup() { size(100,100); GPIO.pinMode(PIN_ENTRADA,GPIO.INPUT); GPIO.attachInterrupt(PIN_ENTRADA,this,"cambiar_fondo",GPIO.CHANGE); noLoop(); } void draw() { background(color_fondo); } void cambiar_fondo(int numero_gpio) { color_fondo=(GPIO.digitalRead(numero_gpio)==GPIO.HIGH)?ROJO:VERDE; redraw(); } |
Para liberar la asignación de una interrupción a un GPIO se utiliza GPIO.releaseInterrupt
, que toma como parámetro de entrada el número de pin asociado a la interrupción que se deja de atender. También es posible desactivar temporalmente todas las interrupciones con el método GPIO.noInterrupts
y volver a activarlas usando el método GPIO.interrupts
.
Mientras se está ejecutando el código que responde a una interrupción no deben atenderse a otras interrupciones, sean o no generadas por el mismo evento, por lo que es conveniente desactivarlas al inicio y volver a activarlas al terminar. Lógicamente el código ejecutado como respuesta a una interrupción debe ser mínimo para procurar no dejar otras desatendidas en el proceso.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import processing.io.GPIO; int PIN_ENTRADA=17; color VERDE=color(100,255,100); color ROJO=color(255,100,100); color color_fondo=color(100,100,255); void setup() { size(100,100); GPIO.pinMode(PIN_ENTRADA,GPIO.INPUT); GPIO.attachInterrupt(PIN_ENTRADA,this,"cambiar_fondo",GPIO.CHANGE); noLoop(); } void draw() { background(color_fondo); } void cambiar_fondo(int numero_gpio) { GPIO.noInterrupts(); color_fondo=(GPIO.digitalRead(numero_gpio)==GPIO.HIGH)?ROJO:VERDE; GPIO.interrupts(); redraw(); // redraw podría disponerse antes de interrupts pero como es un proceso lento podrían desatenderse otras interrupciones mientras se completa su ejecución por lo que es una decisión muy relevante. } |
El caso contrario a la gestión del cambio de un GPIO con una interrupción es la espera a que se establezca un estado. El métodoGPIO.waitForInterrupt
sirve para detener la ejecución del código hasta que cierto GPIO cambie a cierto estado o transcurra un tiempo máximo.
Los parámetros de entrada de este método son ① el número del pin cuyo estado se monitoriza, ② el cambio de estado buscado (que puede ser un valor de entre las constantes GPIO.RISING para detectar el flanco ascendente, GPIO.FALLING para esperar al flanco descendente o GPIO.CHANGE para ambos flancos) y ③ el tiempo máximo de espera (timeout) expresado en milisegundos o -1 para esperar indefinidamente y devuelve un valor booleano que indica si se ha alcanzado el estado (true) o si ha transcurrido el tiempo máximo de espera (false) sin que el nivel del pin sea el buscado.
Gestionar los LED del SBC desde Processing
La clase LED
sirve para controlar desde Processing los LED que el SBC exponga al sistema para su programación, que no son necesariamente todos los del dispositivo ni tampoco existen los mismos en los diferentes tipos de placas. Para saber qué LED pueden utilizarse en un dispositivo y distribución concretos, la clase LED
de la librería Hardware I/O incluye el método LED.list
que devuelve un vector de cadenas de texto con los nombres de los LED. En la captura de pantalla de la imagen siguiente se muestra la salida de LED.list
en una Orange Pi Zero.
Una vez identificados los nombres de los LED disponibles, se crean objetos de la clase LED
para poder establecer el brillo con el que se desea que se se enciendan por medio del método brightness
. Cuando dejan de ser necesarios en la aplicación, los objetos LED
que se han asignado se deben liberar con el método close
para que pueda volver a usarlos el sistema del SBC.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import processing.io.LED; LED rojo=new LED("red_led"); LED verde=new LED("green_led"); float intensidad=0.0; void setup() { frameRate(4.0); } void draw() { rojo.brightness(intensidad); intensidad=intensidad<1.0?1.0:0.0; verde.brightness(intensidad); } |
En la primera línea del ejemplo anterior se carga la clase LED
de la librería Hardware I/O, en las dos siguientes se instancian los objetos usando los nombres obtenidos con LED.list
y en las últimas resaltadas se establece su intensidad. Aunque brightness
acepta un float
como parámetro de entrada, no todos los SBC ni todos sus LED pueden establecer valore intermedios de intensidad, lo más frecuente es que los valores enviados mayores que cero enciendan completamente los LED (como hace el valor 1.0) y el valor 0.0 los apague.
Modulación por ancho de pulso (PWM)
Con una salida digital solo se pueden establecer dos niveles: ① alto (cierta tensión, 3V3, por ejemplo) o ② bajo (la tensión de referencia en GND, 0V). Para simular un nivel intermedio se pueden alternar estos dos en cierta proporción a cierta velocidad, con lo que se consigue, por ejemplo, que dé la impresión de que un LED no está completamente encendido sino a una intensidad intermedia. El nombre que recibe esta técnica es modulación por ancho de pulsos (PWM) y en Processing se implementa, usando el hardware del SBC, con la clase PWM
de la librería Hardware I/O. Cuando es la aplicación, y no el hardware, la encargada de establecer los niveles en un GPIO durante cierto periodo para simular valores intermedios, se suele denominar software-PWM para distinguirla de la anterior, en la que es el dispositivo realiza la tarea.
Los dos parámetros que definen el PWM
son ① la frecuencia de la onda portadora (o su periodo: el uno es el inverso de la otra) y ② el coeficiente del ciclo de trabajo. La frecuencia de la onda portadora establece el número de veces que el ciclo de trabajo se envía en un segundo. El ciclo de trabajo es la proporción entre la parte en la que el nivel está alto y la parte en la que el nivel está bajo. Aunque se suele expresa como un coeficiente, el ciclo de trabajo también se puede encontrar a veces indicada como un porcentaje con el mismo significado: la parte a nivel alto con respecto al total.
Con esta técnica, además de simular valores intermedios, también se pueden enviar señales, es decir, también se puede utilizar como una forma básica de comunicación entre dispositivos. Un ejemplo típico de este uso es el manejo de servomotores, cuyo uso en Processing puede gestionarse también con la librería Hardware I/O, como se verá en el siguiente apartado. Como muy rara vez se alimentará con el pulso un dispositivo (poco más que un LED de bajo consumo), en realidad debe considerarse como el caso general el envío de una señal, aunque sea simplemente de un valor intermedio, dado que lo habitual es disponer un controlador entre el SBC y el dispositivo.
Pese a que la modulación por ancho de pulsos es una técnica muy utilizada en electrónica en general y en los sistemas embebidos en concreto, en los SBC como Raspberry Pi no es tan frecuente, en parte porque para un uso sencillo puede implementarse por software sobre los GPIO y en parte porque es un recurso hardware compartido para otros fines como la reproducción de audio, por eso no es raro encontrar PWM en pocos pines o incluso que no esté disponible para su uso en una aplicación. El caso de Raspberry Pi 3 B puede verse en el esquema de la siguiente imagen.
Para utilizar la clase PWM
se importa con import processing.io.PWM;
y se instancia utilizando el nombre del dispositivo. Para saber los recursos PWM que hay disponibles en el sistema se puede utilizar el método PWM.list
que devuelve un vector de cadenas de texto que representan estos nombres.
Para enviar un PWM se utiliza el método set
, que espera como parámetros de entrada la frecuencia en Hz y el ciclo de trabajo como un coeficiente (ambos parámetros son float
). En caso de indicar un único argumento al llamar al método set
se supone que es el ciclo de trabajo y se toma como frecuencia 1 KHz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import processing.io.PWM; float intensidad=0.0; float incremento=0.1; PWM luz=new PWM("pwm1"); void setup() { } void draw() { intensidad+=incremento; if(intensidad<0.0) { intensidad=0.1; incremento=0.1; } else { if(intensidad>0.9) { intensidad=1.0; incremento=-0.1; } } luz.set(intensidad); // Como no se indica la frecuencia, se utiliza 1 KHz } |
El PWM iniciado con el método set
se mantiene activo indefinidamente hasta que se mande detener expresamente utilizando el método clear
. Aunque se detenga el PWM con clear
, el recurso se mantiene ocupado desde que se instancia; para liberarlo y que otras aplicaciones del sistema puedan utilizarlo, la clase PWM
incluye el método close
.
Control de servomotores desde Processing
La forma habitual de manejar un servomotor es enviándole una señal PWM
que codifica el el ángulo de giro que se desea que adopte el eje. Con la clase SoftwareServo
de la librería Hardware I/O para Processing se puede usar directamente el ángulo sin necesidad de calcular ni gestionar el envío del pulso.
El ángulo de giro se expresa en grados sexagesimales y el rango que se puede solicitar usando la clase SoftwareServo
está comprendido entre 0° y 180°, que corresponde con los límites habituales de la capacidad de giro de un servomotor.
El ángulo de giro es proporcional al ciclo de trabajo del PWM y, aunque para confirmar el funcionamiento de un servomotor en concreto será necesario consultar la hoja de datos del dispositivo, la mayoría trabaja en unos rangos similares.
El pulso suele estar comprendido entre los 500 µs y los 2500 µs (la librería Hardware I/O de Processing toma por defecto el rango entre 544 µs y 2400 µs) con el punto intermedio en 1500 µs, valor aún más frecuente que los límites menor y mayor, que sirve como referencia para calcular o corregir los otros cuando se desconocen.
Una vez instanciado un objeto de la clase SoftwareServo
, se asigna con attach
a un pin, indicándole, opcionalmente, el valor de los pulsos mínimo y máximo (en microsegundos) con los que trabaja el tipo de servomotor utilizado. Posteriormente, para enviar el ángulo, se utiliza el método write
. La clase también incluye un método, attached
, que sirve para saber si se ha asignado el servomotor a un GPIO.
1 2 3 4 5 6 7 8 9 10 11 12 |
import processing.io.SoftwareServo; SoftwareServo servo; int pin_servo=17; // El cable de señal se conecta al GPIO 17 float angulo=45.0; void setup() { servo=new SoftwareServo(this); // Al instanciar el servo hay que indicar el contenedor, normalmente this, para hacer referencia al sketch actual de Processing servo.attach(pin_servo,1000,2000); // Un hipotético servo con 0° en 1000 µs y 180° en 2000 µs Ante la duda, es mejor no indicar mínimo y máximo para que tome los valores 544 µs y 2400 µs que luego se podrán corregir si el ángulo no es exacto servo.write(angulo); } |
El color de los cables tampoco es completamente estándar pero lo normal es que la tensión positiva (frecuentemente 5 V) corresponda con el cable rojo, la negativa (GND) con el negro o marrón y la señal sea el cable amarillo (lo más habitual), naranja, azul o blanco.
Aunque para hacer pruebas puede alimentarse directamente del SBC algún componente pequeño (como un LED) en general no es una buena idea hacerlo, ya que la salida suele soportar intensidades muy bajas. En el caso de componentes de fuerza, como puede ser un motor, no debe hacerse ni aún en pruebas, por su elevado consumo y porque, cuando contiene inductores, puede producirse una corriente transitoria, de polaridad inversa, que entraría en el SBC y seguramente dañaría la placa.
En el caso del servomotor, que dispone su propio controlador y la señal no está relacionada con la alimentación del dispositivo, puede ser suficiente alimentarlo por separado, compartiendo GND con el SBC, y conectar directamente el cable de señal al GPIO del SBC que se usará para manejarlo. Si acaso la circuitería del servomotor no aislara el motor y/o se desea más seguridad, además de una alimentación independiente, el cable de señal se puede conectar en serie con un diodo que conmute a una velocidad suficientemente alta o usar un optoacoplador, también de alta velocidad.
Para desconectar el servomotor del GPIO con el que se controla, se utiliza el método detach
. Como la desconexión es inmediata y el giro necesita cierto tiempo para completarse, más o menos dependiendo del dispositivo en concreto, si se desconecta antes de alcanzar el ángulo solicitado el eje no se detendrá en el ángulo correcto o puede que ni se llegue a apreciar movimiento alguno.
1 2 3 4 5 6 7 8 9 10 11 12 |
import processing.io.SoftwareServo; SoftwareServo servo=new SoftwareServo(this); int pin_servo=17; float angulo=180.0; void setup() { servo.attach(pin_servo); servo.write(angulo); // Esto no va a funcionar… servo.detach(); // …porque esto lo va a parar antes de que le dé tiempo de completar el giro } |
La clase SoftwareServo
es lo que su nombre sugiere: una implementación por software de la modulación por ancho de pulsos (PWM) así que no es difícil utilizarlo para otros fines diferentes del control de servomotores configurando los valores mínimo y máximo del ciclo de trabajo que se pueden pasar como argumentos, además del número de GPIO, al asignarlo con el método attach
.
Comunicaciones I2C
I2C, también escrito como I²C o, de forma más convencional, como IIC es un bus de comunicaciones diseñado originalmente a principios de los ’80 por Philips, actualmente NXP, que es a su vez una división de Qualcomm. Desde 2006 caducó la patente y actualmente se considera un estándar de hecho en comunicaciones que ha evolucionado hasta la actual versión 6 (de 2014) del protocolo I²C.
El bus I2C utiliza dos hilos, uno para los datos (SDA) y otro para la sincronización (la señal de reloj, SCL) y una estructura de direcciones que permite que un dispositivo principal, llamado maestro (el SBC en este caso) comunique con varios esclavos (como sensores o actuadores, aunque podrían ser a su vez otros SBC). Ambas líneas debe estar forzadas a nivel alto utilizando resistencias pull-up, típicamente entre 1 KΩ y 10 KΩ, dependiendo de la alimentación y la longitud del cable.
Es importante recordar que ① las líneas SDA y SCL deben forzarse a nivel alto con sendas resistencias pull-up, ② en general, los dispositivos no deben alimentarse desde el SBC (aunque pueden compartir alimentación), ③ es posible que cada dispositivo esté alimentado a una tensión diferente y sea necesario convertirla y ④ todos los dispositivos, incluyendo el SBC, deben compartir GND.
La versión actual del protocolo trabaja con un sistema de direcciones de 10 bits aunque el más utilizado es el sistema de direcciones de 7 bits original que permite 112 direcciones, ya que 16 de ellas están reservadas. La librería Hardware I/O de Processing utiliza 7 bits para las direcciones. Si en la hoja de datos del dispositivo expresa la dirección con 8 bits, hay que tomar los 7 bits más significativos (MSB), ya que el último se utiliza para indicar si es una operación de lectura o escritura.
Después de importar la clase I2C
con import processing.io.I2C;
, para instanciar un objeto es necesario conocer el nombre que le asigna el sistema, en lo que puede ayudar el método I2C.list
que devuelve un vector de cadenas de texto con estos nombres.
Los nombres de las conexiones I2C corresponderán con un pinout diferente, dependiendo del SBC, que habrá que consultar en la documentación correspondiente. En el esquema de la imagen de abajo se muestra el de un SBC Raspberry Pi 3 B.
La forma habitual de comunicar con un dispositivo usando un bus I2C consiste en ① iniciar la comunicación haciendo referencia a un dispositivo por su dirección en el bus I2C con el método beginTransmission
, ② enviar datos al dispositivo o recibirlos (frecuentemente, para los segundo también es necesario lo primero) con los métodos write
(indicando el valor que se envía) y read
(indicando el número de bytes que se desean recibir) y ③ finalizar la comunicación con el dispositivo I2C con endTransmission
para que otros dispositivos puedan usarlo. Mientras que las operaciones de lectura llevan implícita la finalización de las comunicaciones y usar endTransmission
no será obligatorio; sí que es imprescindible usarlo después de las operaciones de escritura (la última, si son varias) lo que además vaciará la cola de operaciones pendientes.
No hay que confundir endTransmission
, que finaliza la comunicación con un un dispositivo I2C, con close
, que libera el objeto I2C
y con él todo el bus I2C, de forma que otras aplicaciones del sistema podrán utilizar esa conexión.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import processing.io.I2C; // Cargar la clase I2C de la librería Hardware I/O para Processing I2C lm75; // Un temómetro I2C LM75… int DIRECCION_LM75=0x48; // …con todos los pines de dirección a nivel bajo. int DIRECCION_TEMPERATURA=0x00; // Dirección del registro del LM75 que contiene la temperatura. byte registro_temperatura[]=new byte[2]; // Vector para almacenar los dos bytes del registro de temperatura que leen del LM75. int temperatura; // Temperatura calculada usando el valor del registro (Solo por claridad del método de cálculo). float coeficiente_temperatura=0.00390625; // Coeficiente de conversión del valor del registro del LM75 en temperatura en grados Celsius (1/32/8 conforme al datasheet) void setup() { lm75=new I2C("i2c-1"); // El termómetro está conectado a I2C 1 frameRate(0.5); // Consultar la temperatura cada dos segundos } void draw() { lm75.beginTransmission(DIRECCION_LM75); // Contactar con el dispositivo en la dirección DIRECCION_LM75 lm75.write(DIRECCION_TEMPERATURA); // Pedir la lectura del registro de temperatura registro_temperatura=lm75.read(2); //lm75.endTransmission(); // Cuando la operación es read no es necesario terminar la transmisión temperatura=parseInt(registro_temperatura[0]&0xFF)<<8; // Rotar 8 bits el byte más significativo temperatura=temperatura|registro_temperatura[1]&0xFF; // Añadir el byte menos significativo println("La temperatura de la sala es de "+(parseFloat(temperatura)*coeficiente_temperatura)+" °C"); } |
El código del ejemplo anterior sirve para leer la temperatura de un termómetro I2C LM75 cada dos segundos. El procedimiento es muy similar en todos los sensores que almacenan en registros los valores monitorizados: se accede al dispositivo (línea 18), se escribe la dirección del registro (línea 19) y se lee cierto número de bytes con read
(línea 20). El código de la línea 21, endTransmission
, no es necesario porque la última operación es de lectura. Como puede verse, read
devuelve un vector de bytes, con la longitud solicitada como parámetro de entrada, que seguramente habrá que posprocesar para obtener el resultado en el formato deseado.
Comunicaciones SPI
El bus SPI es un protocolo de comunicaciones serie bidireccional (dúplex) diseñado por Motorola a finales de los ’80 y actualmente un estándar de hecho para las comunicaciones serie rápidas entre circuitos.
Para implementar la comunicación dúplex, de un dispositivo maestro, que controla las comunicaciones (el SBC, en este caso), a un dispositivo esclavo (como un sensor o un actuador) y de forma simultánea del esclavo al maestro, se utilizan tres hilos con diferentes señales. ① Una señal de reloj, llamada CLK, SCLK, SCK… generada por el maestro y que determina la velocidad del bus SPI. ② Una línea de datos desde el maestro hasta el esclavo llamada MOSI (Master Out Slave In) y ③ Una línea de datos desde el esclavo hasta el maestro llamada MISO (Master In Slave Out).
Para que en el mismo bus puedan disponerse varios dispositivos que comuniquen alternativamente con el dispositivo principal (el maestro) se suele implementar también ④ una señal de activación (a nivel bajo) con la que habilitar o deshabilitar las comunicaciones SPI en el esclavo. Serán necesarias tantas líneas de activación, llamadas SS (Slave Select), CS (Chip Select) o CE (Chip Enable)… como dispositivos esclavos haya en el bus.
Al conectar los dispositivos esclavos (periféricos) al SBC, que actúa como maestro, hay que considerar que ① en general, el dispositivo (el sensor, por ejemplo) no debe alimentarse desde el SBC sino desde una fuente de alimentación (aunque esa sí pueda compartirse con el SBC). ② Los valores de las tensiones de alimentación pueden ser diferentes y tal caso sería necesario utilizar algún tipo de regulador; ③ Todos los dispositivos deben compartir GND aunque la alimentación sea diferente. ④ Opcionalmente, para asegurar que las comunicaciones SPI están desactivadas en el dispositivo esclavo, puede disponerse una resistencia pull-up en la línea de habilitación (SS o CS). ⑤ Para minimizar la distorsión en el bus que pueden crear algunos dispositivos esclavos que pueden datos continuamente al maestro, se puede forzar el comportamiento de alta impedancia (desconectado) en la línea MISO con un buffer triestado o con un divisor de tensión que equilibre el nivel alto y el bajo.
Para instanciar un objeto de la clase SPI
, un bus SPI, es necesario conocer su nombre en el sistema. Para obtener los puertos SPI del sistema, la librería Hardware I/O proporciona el método list
que devuelve un vector de cadenas de texto con dichos nombres.
Cada SBC ubica en una posición diferente el pinout asociado a los puertos SPI, por lo que será necesario consultar la documentación del SBC para saber cómo conectar los dispositivos. En el esquema de la imagen de abajo se muestra la posición de la conexión SPI de una placa Raspberry Pi 3 B.
Aunque cualquier GPIO sirve para activar o desactivar las comunicaciones de un dispositivo SPI esclavo (estableciendo un nivel alto o bajo), muchos SBC, como los SPI CE de las Raspberry Pi, incluyen algunas conexiones especializadas que la librería Hardware I/O es capaz de usar ahorrando al usuario la tarea de habilitar o deshabilitar el dispositivo.
El procedimiento para utilizar las comunicaciones SPI con la librería Hardware I/O consiste en ① incorporar la clase al sketch de Processing con import processing.io.SPI;
, ② instanciar (con new
) un objeto de la clase SPI
utilizando el nombre que el sistema operativo asigna al puerto, ③ cuando proceda, configurar el puerto con settings
, ④ enviar y recibir información utilizando el método transfer
y ⑤ si fuera el caso, liberar con close
el puerto para que otras aplicaciones del sistema puedan utilizarlo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import processing.io.SPI; SPI max6675; byte temperatura_recibida[]=new byte[2]; int ESTADO_MAX6675=0B00000100; // bit de estado de la sonda (en el byte menos significativo) en formato binario para apreciar su posición byte nada[]={0,0}; void setup() { max6675=new SPI("spidev0.0"); // Usar el primer puerto SPI obtenido con SPI.list() frameRate(0.25); // Mostrar la temperatura una vez cada cuatro segundos } void draw() { temperatura_recibida=max6675.transfer(nada); // El puerto SPI recibe tantos datos como envía así que hay que simular que se están enviando datos al MAX6675, aunque no tenga ni conexión, para respetar el protocolo. if((temperatura_recibida[1]&ESTADO_MAX6675)>0) { println("La sonda está desconectada"); } else { // El MAX6675 no mide temperaturas bajo cero (las sondas K, sí son capaces); en otros casos habrá que verificar también el bit del signo (MSB) print("La temperatura del horno es de "); print(parseFloat(temperatura_recibida[0]*256+temperatura_recibida[1])/32.0); // Cálculo de la temperatura conforme al datasheet del MAX6675 println(" °C"); } } |
En el código de ejemplo anterior se muestra cómo leer la temperatura de un MAX6675, un conversor analógico-digital para sondas de termopar K con compensación de unión fría. Este dispositivo no espera datos del SBC pero el diseño del bus SPI implica que deben enviarse tantos datos como se reciban, por eso, en el código anterior se usa transfer
enviando tantos datos irrelevantes como datos del dispositivo se esperen recibir, dos bytes que representan la temperatura.
Normalmente, los dispositivos que actúan como maestros (el SBC, en este caso) son capaces de adaptarse y trabajar con otros más lentos. En caso de no ser así, es posible configurar la velocidad, por defecto 500000 Hz, con el método settings
, que además permite establecer el orden en el que se envían los bits (normalmente, primero el bit más significativo) y el modo SPI.
El método settings
espera como argumentos de entrada ① la velocidad como una frecuencia en Hz, ② el orden de bits como una de las constantes SPI.MSBFIRST (enviar primero el bit más significativo o MSB) o SPI.LSBFIRST (enviar primero el bit menos significativo o LSB) y ③ el modo (de reloj) SPI, que puede ser SPI.MODE0 (valor base del reloj en cero y flanco ascendente), SPI.MODE1 (base del reloj en cero y flanco descendente), SPI.MODE2 (base del reloj en uno y flanco ascendente) o SPI.MODE3 (base del reloj en uno y flanco descendente).
El Guille
Hola Víctor.
He ido a parar a tu web de forma casual, y dejarte ésta opinión, que no tiene nada que ver en absoluto con el artículo, pero como es el último que has escrito…
Felicidades, enhorabuena y sobre todo, gracias, muchas gracias por compartir todos los artículos que has escrito, y por trasladar tan bien al resto de los mortales, tus conocimientos.
Gracias a personas como tú, otras, como yo, podemos realizar algunos montajes que nos sacan de algún apuro, gracias a las magníficas explicaciones que que hay en tus artículos.
Te reitero mi felicitación y por favor, sigue adelante, no dejes de escribir (siempre que tengas ocasión, claro)
Un gran saludo.
Víctor Ventura
¡Muchas Gracias! 🙂
Me alegra mucho que te guste polaridad.es. He visto tu página web y puedo decir que, viniendo de ti, es un elogio especialmente valioso.
Por cierto, tu blog es buenísimo, no lo conocía y ahora lo voy a seguir con mucho interés 🙂
¡Vuelve pronto por aquí!
José Picó
Me parece fantástico como presentas la información.
Enhorabuena y Gracias por compartir.