¡Bienvenido visitante ! ¡Apúntate al foro ! ¡Es gratuito !
Aquí encontrarás respuesta a muchas de tus preguntas sobre modelismo ferroviario.
¡Puedes preguntar a tus anchas y aportar tus conocimientos!

Lenguaje de programación Arduino

Un sitio específico para que cada cual hable de sus proyectos. Así no queda difuminado según el tema por los talleres.
Avatar de Usuario
xaviermbcn
Forero
Mensajes: 3175
Registrado: Mié Ene 18, 2006 7:54 am
20
Ubicación: Barcelona

Re: Lenguaje de programación Arduino

Mensaje por xaviermbcn »

Bondia Paco,

gracias por tantas explicaciones. Me tienta encargar una Arduino, la que sea, y ponerme a probar cosas ... Con gente en el foro siempre anima mas.

Sin saber de entradas ni salidas. Entiendo que si en vez de pulsadores fueran salidas de decoders (DA) podría funcionar ?
Mi idea siempre es TC como pieza central. Y Arduino me seduce por el echo de que podría "colgar" de TC para muchas cosillas....

Salut!
Xavier

Maqueta 0 operativa, en construcción.
Maqueta H0 operativa, en construcción.

TC3.3, 5.0, 5.5, 5.8, 7.0, 8.0G2, 9.0C2 Gold, TP 5.5 a 9.0, TA, +Street.
Electronica: Central Lenz 3.6 + LI LAN + LH100 + PacoMouse !!
Retro: LDT RS8, RS16, PpP, LR100, CTMS
Accesorios: LDT 1-DEC, S-DEC, SA-DEC, LS100, LS150, CTMS
Otros: Rramp, LDT KSM.
Avatar de Usuario
fmco
Forero
Mensajes: 945
Registrado: Vie Ene 20, 2006 4:09 pm
20
Ubicación: Reus
Contactar:

Re: Lenguaje de programación Arduino

Mensaje por fmco »

Sin saber de entradas ni salidas. Entiendo que si en vez de pulsadores fueran salidas de decoders (DA) podría funcionar ?
Mi idea siempre es TC como pieza central. Y Arduino me seduce por el echo de que podría "colgar" de TC para muchas cosillas....
Si se podría usar, pero tendrías que poner optos ya que los DA dan >12V y las entradas del Arduino solo admiten hasta 5V.

Pero depende de que cosas quieras "colgar" podrías hacer que el Arduino sea un decoder (mira el tutorial 6 "Otras librerías")

saludos,

Paco
Avatar de Usuario
palaycarbon
Forero
Mensajes: 615
Registrado: Jue Feb 16, 2006 4:54 pm
20

Re: Lenguaje de programación Arduino

Mensaje por palaycarbon »

No sé qué pretende Xavier, pero una de mis ideas es utilizar Arduino como decoder.

Desde TC, o desde el XBusTCO doy una orden sencilla (por ejemplo dia/noche o bajar/subir las barreras de un paso a nivel) y Arduino se encarga de ejecutar una secuencia más o menos compleja, por ejemplo:

-          Paso a nivel: activar la campana y las luces, bajar la barrrera, desactivar la campana y activar la señal luminosa que indica al maquinista que el paso de nivel es seguro (esto al menos en la DB). Luego, el proceso correspondiente para levantar la barrera.

-          Vivienda: me invento una secuencia de sucesos con los que voy activando y desactivando luces (las diferentes estancias e incluso la TV) recreando la vida en el hogar, lo que hacen los preysermanes desde que llegan a casa por la tarde hasta que se van a dormir.

 
Por cierto,

En Youtube corren algunos videos en los que utilizan un Arduino como programador de microcontroladores, pero no tengo localizado ninguno que explique en detalle cómo lo hacen. Tampoco es que haya buscado mucho...
¡Vivan las estaciones sin señales de entrada! :D
Avatar de Usuario
Alfred
Forero
Mensajes: 897
Registrado: Jue Abr 24, 2008 9:41 pm
18
Ubicación: Sant Martí de Provençals

Re: Lenguaje de programación Arduino

Mensaje por Alfred »

Aquí tienen diversos proyectos aplicados:

https://www.locoduino.org/
l'Alfred
Avatar de Usuario
fmco
Forero
Mensajes: 945
Registrado: Vie Ene 20, 2006 4:09 pm
20
Ubicación: Reus
Contactar:

Re: Lenguaje de programación Arduino

Mensaje por fmco »

9. Uso de 'shields': Plataforma deslizante con motor paso a paso

Para el siguiente proyecto vamos a necesitar un poco de bricolaje y hacer uso de circuitos electrónicos adicionales para controlar un motor paso a paso.

Gracias a la fácil disponibilidad de piezas mecánicas para la construcción de impresoras 3D las podemos aprovechar para construir una plataforma deslizante artesanal como esta :

https://mrr.trains.com/how-to/dcc-elect ... -traverser
Imagen
Imagen

Dejaremos para los manitas la parte mecánica y nos centraremos en la parte electrónica y la programación.

Los motores paso a paso tienen una construcción especial con varios bobinados. Dependiendo de cómo se controlen esos bobinados podemos hacer que se mantenga en una posición o que gire en uno u otro sentido. Las diferentes posiciones en las que puede quedar estacionario se denominan pasos. Para impresoras 3D es habitual encontrar motores con 200 pasos/revolución

Hay dos tipos de motores paso a paso, los unipolares y los bipolares dependiendo de la conexión de los bobinados:
Imagen

Motor unipolar:
Imagen

Un motor unipolar (como el 28BYJ-48) suele tener 5 o 6 cables y es el de más sencillo de controlar, solo requiere que se activen los bobinados uno tras otro para hacerlo girar:

Imagen

Motor bipolar:
Imagen

Un motor bipolar tiene 4 cables y para controlarlo no solo activamos uno u otro bobinado, sino que tenemos que cambiar la polaridad para conseguir que gire:
Imagen

Nota: Un motor unipolar podemos controlarlo como un bipolar si no conectamos la toma intermedia de cada bobina.

Un ‘shield’ en Arduino és una placa que se apila sobre el Arduino o sobre otro ‘shield’, de manera que nos permite ampliar el hardware o para dar funcionalidad extra a un Arduino.
Entre ellas hay que pueden controlar un motor paso a paso:

Imagen
Imagen

Otras placas de extensión no se pinchan en el Arduino pero también pueden ser controladas desde los pines analógicos y digitales para los motores paso a paso:

Imagen
Imagen

Yo voy a utilizar un viejo motor bipolar del desguace de una impresora o fax y la shield de motores (Arduino motor shield v3) que tenia por un cajón porque la compré para probar la central digital DCC++ por Arduino: (https://sites.google.com/site/dccppsite/home)

Imagen

En esta shield si el motor necesita otra tensión diferente de la que admite el Arduino 7V..12V, debemos cortar el jumper (puente) de la conexión Vin sino podríamos dañar la placa Arduino Uno. En este caso alimentaremos independientemente el Arduino de la shield

Imagen

Con las shields es interesante tener su esquema para saber que pines usa y cuales nos deja libres para otros usos (u otras shields) de forma que no tengamos incompatibilidades de hardware.
Para la Motor shield rev3 este es su esquema:
Imagen

Como vemos esta shield está basada en un integrado L298 que puede controlar dos motores de continua o uno paso a paso bipolar con 2A por canal. Estos son los pines que usa:
Imagen

Las entradas analógicas A0 y A1 nos dan la corriente del motor/bobina a razón de 1,65 V/A: Para 2A daría 3,3V.

Además del jumper de Vin tiene otros jumper en la PCB que se pueden cortar y utilizarlos para otros fines sino necesitamos esas señales para el control del motor : SNS0 (A0), SNS1 (A1) , BRAKEA (D9) y BRAKEB (D8).

También tiene una serie de conectores que nos dan acceso a las señales A2, A3, A4 (SDA), A5 (SCL), D5 y D6.

Finalmente las bobinas (o motores) se conectan una en A- y A+ y la otra en B- y B+
Imagen

Nota: Con esta shield no podríamos ponerle sonido como el proyecto anterior de placa giratoria ya que usa los pines del SPI necesarios para el lector SD. Si quisiéramos sonido, tendríamos que usar otra placa diferente de control de motores.

IMPORTANTE: Si usas motores de impresora 3D (NEMA17 o similar) ten en cuenta que se eligen por el par (torque) por lo que no suelen dar la tensión de funcionamiento sino la resistencia y la corriente por fase, con ayuda de la ley de Ohm: V = I x R podemos saber la tensión de funcionamiento, que suele ser baja, por lo que quizás esta no sea la shield más adecuada para vuestro motor bipolar. En ese caso mejor usar los Pololus o la CNC Shield:
http://carlini.es/manejar-un-motor-step ... y-arduino/

Si no tenemos el datasheet (hoja de datos) del motor paso a paso quizás no sabremos cuales son los pares de cables que van conectados a cada bobina.
Con un multímetro podemos medir la resistencia entre dos cables y según la medida averiguar cual corresponde a cada bobina.

Otro método mas manual pero que curiosamente funciona es este:
1- Con el motor bipolar desconectado gira un poco el eje con la mano
2- Conecta un par de cables entre ellos
3- Gira un poco el eje con la mano. Si es más difícil de girar que antes has encontrado los dos cables de la misma bobina

https://steppermotor.top/como-identific ... motor-pap/


La librería Stepper.h incluida en Arduino es una manera simple de controlar un motor paso a paso tanto unipolares como bipolares:
https://www.arduino.cc/en/Reference/Stepper

El inconveniente es que su función step() que hace girar el motor los pasos que se le pasen como parámetro, es bloqueante (como vimos con delay()). Detiene el programa hasta que el motor haya terminado de moverse.
Si establecemos la velocidad con setSpeed() en 1 RPM (revolución por minuto) con un motor de 200 pasos por revolución al ejecutar step(200) tardará un minuto en ejecutarse para girar una revolución, lo cual puede ser inaceptable para nuestro programa.

Una librería mas avanzada que podemos instalar desde el Gestor de Librerías es la librería AccelStepper:
https://www.airspayce.com/mikem/arduino ... index.html

Esta librería, no es bloqueante, controla el motor paso a paso y además se le puede indicar una aceleración/deceleración para el movimiento.

Nuestra plataforma tendrá un motor paso a paso para el movimiento, un interruptor de final de carrera que accionará la plataforma al llegar a un extremo y un pulsador para cada vía.

Para el programa haremos que al dar tensión la plataforma se mueva despacio en dirección al interruptor final de carrera, cuando lo toque pararemos y haremos que retroceda y vuelva a buscar más lentamente el final de carrera, ese punto será el paso 0. Luego moveremos la plataforma hasta el paso que corresponda con la primera vía.
El resto es fácil, esperaremos a que se pulse el pulsador de una vía, momento en que llevaremos la plataforma a la vía correspondiente.

Imagen
Imagen

Vamos a ver otra forma de organizar los datos. Podemos crear una especie de gran variable que contenga otras variables. En nuestro caso, crearemos una estructura para definir como es un punto de paro, que a su vez contendrá información del pulsador asociado y de la posición de ese punto de paro.
Como esta gran variable es como cualquier otra variable podremos hacer arrays con ella.

Para crearla se utiliza struct y el nombre de este tipo de estructura, luego entre {} se definen como siempre cada una de las variables que contendrá y se finaliza con ;
Para definirla, así como en las variables escribíamos el tipo y su nombre, aquí se escribe el nombre de la struct que hemos creado y el nombre que le vamos a dar y acabamos con ;

Si queremos inicializarla con valores se escribe el nombre de la struct creada, su nombre = entre {} los valores separados por , de cada una de las variables que componen la struct y se acaba con ;

Para usar el valor de una de las variables que forma la struct, se escribe el nombre (de la struct definida) después . y luego el nombre de la variable de la struct

Para determinados bucles vamos a usar la estructura while() que repite las instrucciones entre {} hasta que la expresión entre () sea falsa. La estructura do...while() funciona de la misma manera pero la condición entre () se comprueba al final por lo que las instrucciones entre {} se ejecutan al menos una vez.

Gracias a esta estructura el programa será más sencillo para mover el motor hasta que llegue a un determinado punto.


El programa seria este:

Código: Seleccionar todo

// Control de placa deslizante con motor paso a paso - Paco Cañada 2019

#include <AccelStepper.h>                 // Libreria motores paso a paso con aceleracion/deceleracion

// Placa Arduino Motor Shield rev3
// salidos digitales:
const int dirA = 12;                      // dir: Control del motor por la libreria
const int dirB = 13;
const int pwmA = 3;                       // pwm: Tiene que estar a HIGH
const int pwmB = 11;
const int brakeA = 9;                     // brake: Tiene que estar a LOW si no se ha cortado su jumper en la shield
const int brakeB = 8;
// entradas analogicas
const int sns0 = A0;                      // Lectura de la corriente del motor/bobina. (1.65V/A)
const int sns1 = A1;
// entradas pulsadores (conector en la shield)
const int pinA2 = A2;                     // Pulsar para ir a la via
const int pinA3 = A3;
const int pinD5 = 5;
  const int pinD6 = 6;  
const int pinSDA = A4;
// entrada final de carrera
const int pinSCL = A5;                    // Para buscar el punto 0

#define numVias 5                         // Numero de vias/pulsadores de nuestra plataforma deslizante

#define Via1 600                          // Pasos en que estan las vias. Deteminados por prueba y error
#define Via2 750
#define Via3 900
#define Via4 1050
#define Via5 1200

#define FinCarrera pinSCL                 // Para que sea mas facil de interpretar listado

struct PuntoParo {                            // struct de datos para el punto, contiene posicion y pulsador asociado
  int pulsador;
  long posicion;
};

PuntoParo Via[] = {                    // Array de puntos de parada
  { pinA2, Via1 },                        // pin pulsador y posicion
  { pinA3, Via2 },
  { pinD5, Via3 },
  { pinD6, Via4 },
  { pinSDA, Via5}, 
};

#define MaxVelocidad  200                 // Velocidad normal (pasos por segundo)
#define IniVelocidad  100                 // Velocidad al buscar final de carrera
#define ZeroVelocidad 50                  // Velocidad al buscar el paso 0
#define Aceleracion 20                    // Aceleracion (pasos por segundo al cuadrado)

// Define el objeto y el tipo de interface para AccelStepper:
// Con Arduino motor shield rev3 usar   2 -> FULL2WIRE < 2 wire stepper, 2 motor pins required (H-Bridge)
// Con Pololu A4988/DRV8825 usar        1 -> DRIVER    < Stepper Driver, 2 driver pins required (driver board)
// Con motor unipolar y ULN2803 usar    4 -> FULL4WIRE < 4 wire full stepper, 4 motor pins required (4 transistors)
//                                      AccelStepper stepper( 4, pinA1, pinA2, pinB1, pinB2);

AccelStepper stepper( 2, dirA, dirB);     // se usan pines dir_a y dir_b 


void setup()
{  
  pinMode(pwmA, OUTPUT);                  // Inicializa pines de salida del Arduino motor shield
  pinMode(pwmB, OUTPUT);
  pinMode(brakeA, OUTPUT);
  pinMode(brakeB, OUTPUT);
  
  digitalWrite(pwmA, HIGH);
  digitalWrite(pwmB, HIGH);
  digitalWrite(brakeA, LOW);
  digitalWrite(brakeB, LOW);

  pinMode (FinCarrera, INPUT_PULLUP);     // Inicializa pines de entrada (pulsadores y final de carrera)
  for (int n=0; n<numVias; n++)
    pinMode (Via[n].pulsador, INPUT_PULLUP);

  BuscaZero();                            // Busca el paso 0
  
  stepper.setMaxSpeed(MaxVelocidad);
  stepper.setAcceleration(Aceleracion);
  stepper.moveTo(Via[0].posicion);        // Vamos a la primera via
}

void loop(){  
  if (stepper.distanceToGo() == 0) {
    stepper.run();                          // let the AccelStepper to disable motor current after stop
    CompruebaBotones();
  }
  stepper.run();
}

void BuscaZero () {
  long distancia;

  distancia  = Via[0].posicion / 2;         // Para asegurar encontrar el final de carrera, preveemos movernos
  distancia += Via[numVias -1].posicion;    // la distancia a la ultima via mas la mitad hasta la primera
  stepper.setMaxSpeed(IniVelocidad);        // A velocidad de busqueda
  stepper.setAcceleration(Aceleracion);
  stepper.move(-distancia);                 // Posicion sera relativa al punto actual (antihorario)
  
  while (digitalRead(FinCarrera) == HIGH) { // Mover mientras no se pulse el final de carrera
    stepper.run();
  }

  stepper.setCurrentPosition(0);             // Paso 0 provisional
  distancia = IniVelocidad;                  // alejarse un segundo de velocidad de busqueda
  stepper.setMaxSpeed(ZeroVelocidad);        // pero mas despacio para menos inercia
  stepper.moveTo(distancia);                 // Distancia absoluta (horario)
  while (stepper.distanceToGo() != 0) {        // Mover hasta alejarse para soltar el final de carrera
    stepper.run();
  }
  stepper.moveTo(-distancia);                 // Distancia absoluta desde el cero provisional para asegura que toca
  do {                                        // Movemos hasta que pulse el final de carrera
    stepper.run();
  } while (digitalRead(FinCarrera) == HIGH);
  
  stepper.setCurrentPosition(0);              // ya hemos encontrado el paso 0
  delay(1000);                                 // Esperamos para que nos vean
}

void CompruebaBotones() {
  for (int n=0; n< numVias; n++) {
    if (digitalRead(Via[n].pulsador) == LOW)  // Mira si esta pulsado
      stepper.moveTo(Via[n].posicion);        // pone el nuevo destino
  }
}
Primero incluimos la librería y definimos todos los pines de la placa shield, de esta manera tendremos información de cómo está conectada y los pines ocupados por si mas tarde añadimos otras funcionalidades.

También definimos cuantas vías tenemos y las posiciones en pasos desde el final de carrera obtenidas a base de prueba y error. (Dependerá de los pasos por vuelta del motor, la placa de control, si se usa correa o varilla, y la distancia entre vías)

Luego renombramos el fin de carrera a algo más coherente que la señal en que esta conectada.

Ahora definimos nuestra estructura de datos para los puntos de paro con struct, contendrá dos variables: pulsador, que será el pin asociado del pulsador para ir a esa vía y posicion, pasos donde está situada la vía (un long como requiere luego la librería)

Con esta estructura hacemos un array de puntos de paro llamado Via[] y los inicializamos. Observar las {} para ir asignando valores a cada una de las variables de la struct y las , para el array y dentro de cada struct

Definimos también la aceleración y diferentes valores para velocidad para que sea fácil adaptarlas, tendremos una velocidad de desplazamiento normal, otra más lenta para ir al final de carrera y otra aún más lenta para determinar el paso 0.

Finalmente definimos el objeto stepper con los valores adecuados para inicializar la librería AccelStepper según la placa de control utilizada.

En setup() colocamos los pines de la shield con los valores adecuados. También hacemos un bucle para inicializar los pines de los pulsadores como entrada, estos valores están en el array de puntos de paro Via[], concretamente en la variable pulsador, observar el . entre ambos nombres.

Hemos escrito una función llamada BuscaZero() que nos hará el ciclo de búsqueda del paso 0. En ella tendremos una variable local llamada distancia para ir calculando las posiciones a las que prevemos movernos, es tipo long como necesitan las funciones de la librería.

Para buscar el paso 0 en la función BuscaZero() haremos que la plataforma se mueva hacia el final de carrera , para asegurarnos que lo tocará tendrá que desplazarse al menos la distancia de la última vía y poco más. Ese poco más puede ser cualquier distancia pero para hacer el programa lo más genérico posible haremos que sea la mitad del recorrido hasta la primera vía.

Con las funciones de la librería estableceremos el movimiento que deseamos, con setMaxSpeed() pondremos la velocidad de búsqueda, con setAccesleration() la aceleración y con move() diremos que se mueva la distancia que hemos calculado en modo relativo (desde la posición actual), el signo negativo hará que se acerque al final de carrera

Para que se mueva hay que llamar a la función run() al menos una vez por paso, así que utilizaremos un bucle while() que llamará a run() mientras el final de carrera este a HIGH (sin pulsar).

Cuando se pulse finalizará el bucle y con setCurrentPosition() estableceremos provisionalmente el último paso como paso 0. Como por las inercias del movimiento ese puede que no sea exactamente el paso 0, nos alejaremos más despacio una pequeña distancia y lo volveremos a buscar más lentamente.

La distancia puede ser cualquiera que nos asegure que el final de carrera no está pulsado, podríamos movernos y mirar el pulsador a ver cuando deja de estar pulsado pero vamos a movernos una distancia concreta. La velocidad a que nos movíamos la habíamos establecido en pasos por segundo, así que si queremos retroceder a la posición donde estábamos un segundo antes de tocar el final de carrera, la distancia en pasos será igual a la velocidad (IniVelocidad)

Ahora ponemos una velocidad más lenta con setMaxSpeed() y establecemos la posición absoluta propuesta desde el paso 0 provisional con moveTo(). Con otro bucle while() ejecutamos run() pero ahora la condición es que hayamos hecho el recorrido, o sea que la distancia para llegar, que devuelve la función distanceToGo() sea 0

Cuando ha finalizado el movimiento establecemos un nuevo movimiento para llegar a esta misma distancia pero al otro lado del paso 0 provisional (movimiento absoluto y signo negativo) así nos aseguramos que tocará el final de carrera.

Con un nuevo bucle, pero esta vez do...while() moveremos con run() mientras el final de carrera no esté pulsado (a HIGH). Cuando lo toque finalizará el bucle y estableceremos este como el paso 0 definitivo. Finalmente con delay() esperaremos un segundo para que se vea donde ha tocado.

Después de la búsqueda del paso 0 en el setup() solo nos queda poner la velocidad y aceleración normal e indicar que queremos ir a la posición de la primera vía. El primer valor (indice 0) valor posicion del array de puntos de paro Via[]

En el loop() solo hay que ir ejecutando run() para que se mueva el motor paso a paso si es necesario y comprobar si hemos llegado a un destino, la función distanceToGo() devuelve 0, en ese caso comprobaremos los botones con nuestra función CompruebaBotones() para ver si tenemos que ir a otro destino.

La función CompruebaBotones() simplemente hace un bucle for() por todos los botones, que están en la variable pulsador del array de puntos de paro y si esta pulsado (LOW) establece el nuevo destino con moveTo() a la posición absoluta guardada en la variable posicion del array de puntos de paro para ese índice. En el loop() la función run() ira moviendo el motor.

Nota: Si se pulsan varios botones, como el bucle for() recorre los pulsadores del primero al último se irán estableciendo los destinos, por lo que el destino final será el del pulsador con el número de vía más alta
Avatar de Usuario
fmco
Forero
Mensajes: 945
Registrado: Vie Ene 20, 2006 4:09 pm
20
Ubicación: Reus
Contactar:

Re: Lenguaje de programación Arduino

Mensaje por fmco »

10. Los Strings: Botón Pulsador de Acción

Los Strings en Arduino son parecidos a los arrays de caracteres pero con más posibilidades ya que se pueden usar ciertas operaciones con ellos que con arrays habría que implementar con código. Serian como un objeto de una librería con sus funciones propias.
https://www.arduino.cc/reference/en/lan ... ingobject/

Al contrario que los arrays no ocupan una posición de memoria fija sino que se asigna dinámicamente por lo que son más lentos y tienden a ocupar mucha memoria al manipularlos, a cambio ofrecen más funcionalidad para trabajar con cadenas de caracteres.

Los Strings normalmente se usan para enviar textos al puerto serie o a una pantalla conectada al Arduino. En micros con poca memoria RAM como el Arduino Uno es preferible usar arrays en lugar de Strings o hacer que se almacenen en la ROM en lugar de la RAM

En este ejemplo, para ver cómo funcionan los Strings los vamos a usar para codificar las órdenes que se ejecutaran al pulsar un Botón de Acción como los que hay en la maqueta de Miniatur Wunderland para realizar diferentes animaciones:

Imagen
Imagen

O como en mi diseño KDaktion:

http://usuaris.tinet.cat/fmco/dccacc_sp.html#kdaktion

Imagen


La idea es tener un String con las ordenes codificadas a ejecutar que el Arduino irá interpretando y ejecutando para controlar hasta 3 servos y 10 salidas digitales de forma que sea fácil de modificar para adaptarlas a los diferentes efectos y animaciones que queramos realizar al pulsar el botón.

Las órdenes serán un String formado por simples caracteres en mayúsculas, seguidas de números si la orden necesita un parámetro. Por ejemplo "IL0H3P1000L3H0"

las ordenes serán las siguientes:

I: Espera la pulsación del Botón de Acción
H: Pone a nivel HIGH una salida digital, necesita un parámetro (entre 0 y 9) que será la salida
L: Pone a nivel LOW una salida digital, necesita un parámetro (entre 0 y 9) que será la salida
P: Hace una pausa, necesita un parámetro que será la duración en milisegundos (hasta 65535)


En el String de ejemplo, si se conecta un LED en la salida 0 y otro en la 3 se realizaría la siguiente secuencia:

Se espera a que se pulse el Botón de Acción, se apaga el LED de la salida 0, se enciende el de la salida 3, hace una pausa de 1000ms (un segundo) apaga el LED de la salida 3 y enciende el de la salida 0.

Haremos que una vez ejecutado el String, se vuelva a cargar, de forma que se repita. En el ejemplo, volvería a esperar la pulsación del Botón de Acción.

También tendremos un String que se ejecutará una sola vez al dar tensión al Arduino o hacer un reset de forma que podremos colocar las salidas en una determinada posición inicial.

Para los servos definimos otras órdenes relacionadas con ellos, como para las posibles animaciones nos interesa controlar la velocidad de movimiento de los servos en lugar de la librería Servo.h vamos a usar la librería VarSpeedServo.h

https://github.com/netlabtoolkit/VarSpe ... vo-library

Esta librería nos permite controlar hasta 8 servos y controlar su velocidad de movimiento (0: máxima velocidad, 1 a 255: velocidad desde lenta a rápida).

Para instalarla, la descargamos en formato ZIP, y en el Arduino IDE vamos a Programa->Incluir Librería->Añadir Librería ZIP...
Imagen
Imagen

Las ordenes que podremos colocar en el String son:

X: Servo X, necesita un parámetro que será la posición a la que se ha de mover (entre 0 y 180)
Y: Servo Y, necesita un parámetro que será la posición a la que se ha de mover (entre 0 y 180)
Z: Servo Z, necesita un parámetro que será la posición a la que se ha de mover (entre 0 y 180)
V: velocidad de movimiento del servo, necesita un parámetro que será la velocidad (entre 0 y 255). Se ha de usar antes de X, Y o Z para que les afecte.
W: Espera a que acabe el movimiento de los servos y los desconecta para evitar vibraciones


También vamos a implementar unas órdenes para poder repetir una serie de acciones un determinado número de veces, lo que nos facilitará hacer parpadeos de un LED, por ejemplo.

R: Repite las ordenes hasta la orden F, necesita un parámetro que es el número de repeticiones (entre 2 y 65535)
F: Fin del bucle, vuelve al principio del bucle si no se han realizado todas la repeticiones.


En este ejemplo: "R10H3P500L3P500F", repetimos 10 veces: encender el LED de la salida 3, hacemos una pausa de 0,5s, apagamos el LED y hacemos otra pausa de 0,5s. Lo que nos dará un parpadeo durante 10 segundos.

Finalmente para que nos sea fácil identificar las diferentes fases de la animación tendremos una orden que nos permitirá escribir anotaciones.

<: Ignora el texto hasta >


Así una animación completa podría quedar en un String como este, fácil de modificar e interpretar:

"<inicio>IH0H3P1000L3<servo>X25WX150W<parpadeo>R10H3P500L3P500F<fin>L0"



El programa seria este:

Código: Seleccionar todo

// Control de un pulsador de accion con tres servos y 10 salidas digitales - Paco Cañada 2019

#include <VarSpeedServo.h>    // https://github.com/netlabtoolkit/VarSpeedServo?utm_source=rb-community&utm_medium=forum&utm_campaign=better-arduino-servo-library

/* PulsadorAccion lee y ejecuta un String que contiene los siguientes comandos:
 *  Xnn Ynn Znn   Servo a posicion nn
 *  Vnn           Velocidad servo nn
 *  W             Espere hasta que acabe el movimiento de los servos
 *  Hn            Salida digital n a HIGH
 *  Ln            Salida digital n a LOW
 *  I             Espera pulsador
 *  Pnn           Pausa de nn ms (maximo 65535ms)
 *  Rnn           Repetir n veces hasta F
 *  F             Fin de bucle, vuleve a R hasta ejecutar n veces
 *  <...>         Comentario
 */

        // Acciones que se ejecutan tras el reset  
String  Reset     = "H0<servo inicio>V20X25Y50Z180";
        // Acciones que se repiten
String  Acciones  = "<boton>WL0IH0<acciones>H4P1000L4x100y20z50WH3X25Y50Z180WL3<parpadeo espera final>R10H0P500L0P500F";

#define servoXpin 12                      // Definicion de los pines
#define servoYpin 11
#define servoZpin 10
#define pulsadorPin 13

int digitalPins[] = {A0,A1,2,3,4,5,6,7,8,9};

VarSpeedServo servoX;                     // Objetos de la libreria VarSpeedServo
VarSpeedServo servoY; 
VarSpeedServo servoZ; 

String  Operacion;                        // Acciones a ejecutar
int pos;                                  // Posicion en el String Operacion que se esta decodificando
unsigned int parametro;                   // Valor del parametro
int velocidad;                            // Velocidad del servo
int bucle=0;                              // contador para el bucle
int buclepos=0;                           // posicion a la que ha de volver el bucle

void setup() {
  int n;
  
  for (n=0; n < sizeof(digitalPins); n++) { // Inicializa salidas digitales
    pinMode (digitalPins[n], OUTPUT);
    digitalWrite (digitalPins[n], LOW);
  }
  pinMode (pulsadorPin,INPUT_PULLUP);     // Inicializa pin pulsador
  velocidad = 30;                         // velocidad servo por defecto
  pos = 0;                                // inicializa decodificacion al inicio del String
  Operacion = Reset;                      // Primero ejecutamos el String Reset
  Operacion.toUpperCase();                // Pone los comandos en mayusculas
}

void loop() {
  if (pos >= Operacion.length()) {         // Si hemos alcanzado el final del String, cargamos las acciones
    Operacion = Acciones;
    Operacion.toUpperCase();               // Pone los comandos en mayusculas
    pos = 0;                               // inicializa el puntero de decodificacion  
  }
  if (isAlpha (Operacion.charAt(pos)) || isPunct (Operacion.charAt(pos))) {
    switch (Operacion.charAt(pos++)) {
      case 'I':                             // Esperar a que se pulse el boton
        while (digitalRead(pulsadorPin) == HIGH);
        break;
      case 'H':                             // salida digital a HIGH
        parametro = ExtraeNumero(9);
        digitalWrite (digitalPins[parametro],HIGH);
        break;  
      case 'L':                             // salida digital a LOW
        parametro = ExtraeNumero(9);
        digitalWrite (digitalPins[parametro],LOW);
        break;  
      case 'P':                             // Pausa de ms
        parametro = ExtraeNumero(65535);
        delay (parametro);
        break;
      case 'V':                             // velocidad del servo
        velocidad = ExtraeNumero(255);      
        break;
      case 'X':                             // Mueve Servo X
        parametro = ExtraeNumero(180);
        servoX.attach(servoXpin);
        servoX.write(parametro,velocidad);
        break;
      case 'Y':                              // Mueve Servo Y
        parametro = ExtraeNumero(180);
        servoY.attach(servoYpin);
        servoY.write(parametro,velocidad);
        break;  
      case 'Z':                             // Mueve Servo Z
        parametro = ExtraeNumero(180);
        servoZ.attach(servoZpin);
        servoZ.write(parametro,velocidad);
        break;
      case 'W':                             // Espera a que acabe el movimiento de los servos
        servoX.wait();                      // Espera a Servo X
        servoX.detach();
        servoY.wait();                      // Espera a Servo Y
        servoY.detach();
        servoZ.wait();                      // Espera a Servo Z
        servoZ.detach();
        break;
      case 'R':
        bucle = ExtraeNumero(65535);        // Lee el numero de repeticiones
        buclepos = pos;                     // guarda la posicion a volver
        break;
      case 'F':
        bucle--;                            // decrementa contador de bucle
        if (bucle > 0)                      // si quedan bucles por hacer, cambia la posicion de decodificacion
          pos = buclepos;
        break;
      case '<':
        parametro = Operacion.indexOf ('>', pos);
        if (parametro == -1) 
          pos = Operacion.length();
        else
          pos = parametro + 1; 
        break;
    }
  }
  else  {
    ExtraeNumero(0);
  }
}

int ExtraeNumero (unsigned int maximo) {
  String numero;
  unsigned int valor;

  numero = "";                                                              // string para contener el numero
  while (isDigit (Operacion.charAt(pos)) && (pos < Operacion.length()) ) {  // lee caracter mientras sea un numero
    numero.concat (Operacion.charAt(pos++));                                // y lo añade al nuevo string
  }
  valor = min (numero.toInt(), maximo);                                     // convierte el string en numero sin que pase del maximo
  return (valor);                                                           // devuelve el valor obtenido
}
Primero incluimos la librería VarSpeedServo.h y definimos dos Strings con las órdenes a ejecutar.

El String Reset serán las órdenes que se ejecutaran únicamente al dar tensión al Arduino o hacer un reset y nos servirá para posicionar inicialmente los servos y salidas.

El String Acciones serán las órdenes que queremos ejecutar para realizar la animación. Una vez ejecutada se volverá a cargar.

Después definimos los pines donde están conectados los servos y el Botón Pulsador.
También definimos un array con los pines dónde van conectadas las 10 salidas digitales. Ya que los pines D0 y D1 son usados normalmente por el puerto serie (por si nos hiciera falta usarlo en un futuro) hemos definido las salidas 0 y 1 en los pines A0 y A1 respectivamente.

Luego definimos los objetos de la librería, en este caso los tres servos y una serie de variables globales. Operacion será el String que contiene las ordenes que se están ejecutando. pos almacenará el índice dentro del String de la instrucción que se interpretará. parametro guardará el numero leído que corresponde al parámetro de la instrucción, velocidad es uno de esos parámetros leídos que corresponde a la instrucción V (velocidad del servo) y finalmente dos variables para poder ejecutar un bucle, bucle que almacena el número de repeticiones y buclepos que guardará el índice dentro del String donde ha de volver si queda repeticiones por hacer.

En el setup() inicializamos los pines digitales de salida leyéndolos del array digitalPins[]. En el bucle for para el límite en lugar de poner 10 usamos la función sizeof() que nos devuelve el tamaño de la variable pasada como parámetro, así si cambiamos la cantidad de pines en el array no tendremos que modificar el valor del límite del bucle for ya que sizeof() nos lo calculará.

Luego inicializamos el pin del pulsador como entrada. También daremos una velocidad por defecto a los servos por si en el String de la animación no usamos la orden V.

Ponemos a cero el índice pos para que decodifique el String desde el principio y copiamos en el String Operacion el String Reset simplemente usando el operador de asignación =

Con arrays para hacer la copia tendríamos que conocer previamente la longitud del array para hacer un bucle que fuera copiando carácter a carácter de uno a otro y que el array de Operacion fuera lo suficiente grande para contener al array de reset
Para finalizar se usa la función toUpperCase() que pasa a mayúsculas todo el contenido de un String por si por descuido hemos usado minúsculas para las órdenes.

En el loop() primero comprobamos que el índice pos para la interpretación no haya llegado al final del String Operacion, para ello lo comparamos con la función length() que nos da la longitud actual del String. Si hemos llegado al final copiamos el String Acciones en Operacion, ponemos las ordenes en mayúsculas y ponemos el índice de nuevo al principio.

NOTA: La memoria del Arduino es limitada y necesitamos que se puedan almacenar los Strings tanto el original como la copia, por ello la longitud máxima del String queda limitada por el mismo y por las variables usadas. Vigilad de no abusar de los comentarios.

Ahora leemos en la posición del índice del String el carácter con charAt(), este debería corresponder a una orden, que es una letra o un signo de puntuación.
En el if hacemos la comparación, si el carácter es alfabético, la función isAlpha() devuelve true, si fuera un signo de puntuación la función isPunct() devolvería true. Usamos la operación lógica || que devuelve true si isAlpha() o isPunct() ha devuelto true.

Si el carácter era una orden, usamos un switch para comparar el carácter leído con cada una de nuestras posibles ordenes.
Observad que en este charAt() hemos post-incrementado el índice con ++ así lo dejamos apuntando al siguiente carácter que puede ser un parámetro para esta orden o una nueva orden.

En el case de la orden I leemos la entrada mediante un bucle while mientras siga a HIGH (sin pulsar), al pulsar será LOW y acabará el bucle. Observad que como no tenemos que hacer nada mas, no hemos usado las {} y hemos puesto el ; de fin de instrucción

En el case de la orden H, como necesitamos un parámetro numérico, llamamos a nuestra función ExtraeNumero() para leer el parámetro y lo usamos para poner a HIGH con digitalWrite() el pin correspondiente almacenado en el array digitalPins[]

A nuestra función ExtraeNumero() le vamos a pasar un parámetro que será el número más alto que podemos devolver por si hemos cometido un error al escribir el String con las órdenes.
Inicializamos el String numero, que usaremos para ir guardando los números leídos, a una cadena vacía: "".
Realizamos un bucle while en el que comprobamos que el carácter apuntado por el índice pos es un número con isDigit(), que devolverá true, y no hemos llegado al final del String de órdenes por lo que la longitud del String devuelta por length() es menor que pos. El operador && es el y lógico.

Con charAt() se lee el carácter numérico y lo añadimos con concat() al String numero. Además avanzamos el índice con ++
Si no era un número, hemos acabado de extraer el parámetro numérico y numero contendrá los caracteres del número, así que con toInt() lo convertimos en un número entero.
Además comprobamos que no exceda del máximo, para ello la función min() devuelve el menor de los valores, nuestro número convertido o el valor máximo que pasamos como parámetro a la función ExtraeNumero(). Finalmente devolvemos con return() el valor.

Para el case de la orden L el proceso es el mismo que para la orden H pero poniendo la salida a LOW

Para la orden P, leemos el número desde el String con ExtraeNumero() y lo usamos como parámetro para delay()

El case de V simplemente lee el número con ExtraeNumero() y lo guarda en la variable velocidad.

Para los servos, las tres órdenes X, Y y Z son iguales, conectar el servo a su pin con attach(), leer el número que indica la posición a donde se tiene que mover desde el String y pasarlo como parámetro a la función write() de la librería VarSpeedServo, el segundo parámetro es la velocidad que lo tenemos guardado en la variable velocidad.

Para la orden W simplemente usamos la función wait() de la librería con cada uno de los objetos VarSpeedServo lo que hará que se espere a que todos los servos hayan acabado su movimiento para continuar. Además los desconectamos con detach() para evitar vibraciones.

En las ordenes para realizar un bucle necesitamos saber cuántas veces tenemos que repetir el bucle que lo guardaremos en la variable bucle y dónde tenemos que volver en la secuencia en cada repetición que lo guardamos en la variable buclepos.
En el case R leemos el número de repeticiones con ExtraeNumero() y lo guardamos en la variable bucle. Ahora el índice pos apunta a la primera orden que se ha de repetir así que lo copiamos en buclepos.
Al final del bucle con la orden F decrementamos bucle, el número de repeticiones, con el operador -- , si aún queda por realizar algún bucle modificamos el índice pos con el valor buclepos de forma que la siguiente orden que decodificaremos será la primera del bucle.

NOTA: No se puede anidar los bucles (poner un bucle dentro de otro). Esto lo dejo como ejercicio :D

Para los comentarios que empiezan con < tendremos que buscar el carácter > que indica el final del comentario. La función indexOf() localiza el parámetro, un carácter, dentro del String. Si lo localiza devuelve la posición en la que lo ha localizado y si no devuelve el valor -1
Si hay varios comentarios debemos usar indexOf() con dos parámetros, el carácter y la posición desde donde buscar, pos en este caso
Si encuentra el carácter > actualizamos el índice pos con la posición siguiente al carácter >, pero si no lo localiza actualizamos pos con la longitud del String de forma que apunte a su final.

Solo nos queda poner en el else qué hacer si por error la orden no era un carácter alfabético o un signo de puntuación, por ejemplo, hemos escrito el parámetro pero se nos ha olvidado escribir la orden. En este caso lo leeremos con ExtraeNumero() pero no haremos nada con él, así avanzará el índice pos hasta una nueva orden.

NOTA: Si usamos este montaje en nuestras maquetas podríamos hacer un hilo para compartir nuestros proyectos y Strings de este Botón Pulsador de Acción.
Avatar de Usuario
Norber
Forero
Mensajes: 102
Registrado: Jue Sep 13, 2012 10:09 pm
14
Ubicación: Salamanca

Re: Lenguaje de programación Arduino

Mensaje por Norber »

¡ Qué bonito, Paco !
¡ Qué hilo tan bonito has escrito !
Muchísimas gracias, una vez más, por tu generoso esfuerzo.
¡ Te admiro !
Saludos
[Multimaus + z21f + GenLi-S88 + RocRail (MacOsX 10.8]
H0 Renfe, sin catenaria
Avatar de Usuario
fmco
Forero
Mensajes: 945
Registrado: Vie Ene 20, 2006 4:09 pm
20
Ubicación: Reus
Contactar:

Re: Lenguaje de programación Arduino

Mensaje por fmco »

11. Comunicaciones (Serie e I2C): Reloj-Pantalla de andén

Algunos puertos de entrada/salida del Arduino tienen funciones alternativas que permiten establecer comunicaciones con otros dispositivos. El más conocido es el interface Serie (pines D0:RX y D1:TX) que es el que está conectado al chip del conector USB por donde cargamos el programa en nuestro Arduino.

Imagen

Otro interface es el I2C (pines A4: SDA y A5: SCL) que permite comunicarse con pantallas LCD y OLED, relojes RTC, expansiones de puertos, módulos PWM, memorias EEPROM, inclinometros, etc.

Imagen

El interface SPI (pines D13: SCK, D12: MISO, D11: MOSI y D10: SS) permiten comunicarse con lectoras de tarjetas SD, pantallas OLED, módulos Ethernet, etc.

En este proyecto vamos a usar el bus I2C para comunicarnos con una pantalla OLED y un reloj RTC (Reloj de Tiempo Real) para mostrar la hora real y los mensajes de los trenes como en una pantalla de andén enviando ordenes desde el puerto serie.

Imagen

Este es el esquema de conexión:
Imagen

El interface I2C requiere que incluyamos la librería Wire del entorno Arduino:
https://www.arduino.cc/en/Reference/Wire

Para el reloj RTC vamos a usar un módulo con el chip DS3231, que además tiene una memoria EEPROM pero que no usaremos en este proyecto. Este módulo tiene una pila de botón tipo CR2032 que mantiene el cómputo de la hora aunque se quite la alimentación.

Además el DS3231 tiene un sensor de temperatura que aunque no es muy preciso (+/- 3ºC) como otros módulos específicos para temperatura como el DTH11 le dará un plus a nuestra pantalla de andén.

El reloj no solo mantiene la hora, minutos y segundos, también mantiene la fecha (día, mes y año) y el día de la semana. Incluso se le pueden programar un par de alarmas aunque nosotros en este proyecto solo usaremos la hora actual.

Imagen

De las librerías disponibles para los módulos RTC que soporten el DS3231, he escogido la uRTCLib disponible a través del Gestor de Librerías ya que parece más ligera que otras.

https://github.com/Naguissa/uRTCLib

Como pantalla OLED conectada al bus I2C usaremos una con el chip SSD1306 de 128x32 pixels (0.91") que permite unas 4 líneas de caracteres, es rectangular y parece más adecuada para un andén que una cuadrada de 128x64 (0.96"). Estas pantallas se alimentan a 3.3V

Imagen

El bus I2C es lento comparado con el SPI para transferir datos a una pantalla, pero para pequeños tamaños de imagen sigue siendo válido. Como ventaja, solo usa dos pines para comunicarse con ella.

Para estas pantallas hay varias librerías, una de las más usadas es la de Adafruit:
https://github.com/adafruit/Adafruit_SSD1306
Esta librería permite escribir texto y dibujar gráficos en la pantalla, aunque para ello necesita de otra librería, la Adafruit-GFX-Library que tiene las primitivas de gráficos y una gran variedad de fuentes de texto:
https://github.com/adafruit/Adafruit-GFX-Library

El problema con esta librería es que necesita mucha RAM ya que usa un buffer del tamaño de la pantalla completa para las escrituras que luego transfiere en bloque para tener una adecuada velocidad de refresco. En micros con poca memoria RAM como el Arduino Uno deja poco espacio para las variables y strings.

Otra alternativa es la u8glib, esta usa un buffer más pequeño pero requiere que el repintado de la pantalla se repita en un bucle con lo que la velocidad de refresco es lenta.
https://github.com/olikraus/u8glib

Una nueva versión de esta librería es la u8g2 en la que se puede elegir entre el modo de buffer completo o parcial para adaptarlo a la capacidad de nuestro Arduino.
https://github.com/olikraus/u8g2

Finalmente he escogido la librería SSD1306Ascii, que requiere muy poca cantidad de RAM, como contrapartida solo permite texto, aunque tiene bastantes fuentes de texto.
https://github.com/greiman/SSD1306Ascii

Imagen

Para el puerto serie utilizaremos las funciones incluidas en Arduino del objeto Serial:
https://www.arduino.cc/reference/en/lan ... on/serial/

Nos comunicaremos con el Arduino desde el Monitor Serie. Se accede desde el menú Herramientas -> Monitor Serie o desde el icono de la parte superior derecha del Arduino IDE. En Herramientas -> Puerto tiene que elegirse el puerto que corresponde a nuestro Arduino.

Imagen

El monitor de puerto serie es una pequeña utilidad integrada dentro del Arduino IDE que nos permite enviar y recibir fácilmente información a través del puerto serie. Su uso es muy sencillo, y dispone de dos zonas, una que muestra los datos recibidos, y otra para enviarlos.

En la parte inferior derecha del Monitor Serie tenemos que poner la misma velocidad en baudios que la que definamos en nuestro sketch para visualizar correctamente los datos.

Imagen

Las funciones principales a utilizar con el puerto Serie son:

Serial.begin(velocidad) inicializa el puerto serie, velocidad serán los baudios por segundo con los que nos comunicaremos.

Serial.print(mensaje) Utilizado para enviar datos como texto. Si mensaje es un número, será enviado como sus caracteres equivalentes para representar ese número; si queremos enviar textos, se escribe entre ' para caracteres, y " para cadenas de texto.

Serial.println(mensaje) Igual que print() pero agregando al final el carácter no imprimible retorno de carro (ASCII 13, o ‘\r’); y el carácter de nueva línea o enter (ASCII 10, o ‘\n’).

Podemos ahorrar memoria RAM usando la macro F() si tenemos que enviar grandes cantidades de texto aunque en este proyecto no nos vamos a preocupar en exceso ya que la librería de la pantalla usa poca memoria.

Código: Seleccionar todo

Serial.print  ("Este texto se copia en RAM");
Serial.print (F("Este texto no."));
Para recibir datos normalmente comprobamos si hay caracteres en el buffer de recepción con Serial.available() y en caso de que los haya los leemos uno a uno con Serial.read() lo que puede ser algo farragoso para interpretar.
Por suerte hay funciones como Serial.parseInt() que leen e interpretan los caracteres numéricos y nos los devuelven como un int. La interpretación se detiene si se recibe un carácter no numérico o se acaba el tiempo de espera para recibir caracteres (por defecto, un segundo).

Para recibir textos en un String la función Serial.readStringUntil() lee la cadena de texto hasta que encuentra el carácter que se le pase como parámetro, normalmente '\n' para recibir una línea completa.

Del lenguaje Arduino también veremos cómo escribir los números hexadecimales (se les precede de 0x para distinguirlos de los textos) y una alternativa al if-else como condicional, el operador ternario:

Imagen

Con esta construcción se escribe la condición a probar luego ? la instrucción en caso de resultar verdadera, luego : y la instrucción en caso de resultar falsa.
Este código sería equivalente:

Código: Seleccionar todo

(n>1) ? x = 1 : x = 0 ;	// operador condicional ternario

if (n > 1)		// if-else
  x = 1;
else
  x = 0;
En nuestro proyecto leeremos únicamente la hora, minutos y segundos del reloj RTC y cada segundo refrescaremos la nueva hora en pantalla. Además cada cierto tiempo leeremos y mostraremos la temperatura durante unos segundos.

Usaremos el Monitor Serie en el que el Arduino nos podrá mostrar un menú de ordenes con el que podremos leer la hora y la temperatura y poner en hora el reloj.
Además las ordenes nos permitirán elegir entre mostrar la hora/temperatura y uno de los varios mensajes definidos en un array, de esta forma tendremos un panel de anuncios de un andén.
El anuncio consistirá en tres líneas, la primera con el tipo de tren, la segunda con el destino con una letra de mayor tamaño y la tercera la haremos móvil para mostrar un mensaje largo del tipo estaciones en las que el tren efectúa parada.

Video:

Imagen

El programa es el siguiente:

Código: Seleccionar todo

// Control de una Pantalla OLED y reloj para panel de estacion - Paco Cañada 2020

#include "Wire.h"               // Libreria I2C
#include "uRTCLib.h"            // Libreria RTC DS3231    https://github.com/Naguissa/uRTCLib/blob/master/src/uRTCLib.h
#include "SSD1306Ascii.h"       // Librerias OLED SSD1306 https://github.com/greiman/SSD1306Ascii
#include "SSD1306AsciiWire.h"

uRTCLib rtc(0x68);                    // La direccion I2C del modulo de reloj DS3231

#define I2C_ADDRESS 0x3C              // la direccion I2C de la pantalla OLED SSD1306
#define RST_PIN -1
SSD1306AsciiWire oled;

int hora,minutos,segundos;            // variables para mostrar hora y mensaje
int ultSegundo=99;
bool muestraHora;
int mensaje;

TickerState state;                    // Mantiene puntero a la cola de mensajes en movimiento 
uint32_t tickTime = 0;

#define maxMensaje 5                  // Numero de mensajes en la pantalla. Se definen en el array text[]

  const char* text[] = {
  "RE  REGIONAL EXPRESS", "BARCELONA", "EFECTUA PARADA EN VILASECA, TARRAGONA, ALTAFULLA, TORREDEMBARRA, SANT VICENS Y VILANOVA",
  "AVE 03062", "MADRID ATOCHA", "EFECTUA PARADA EN TARRAGONA CAMP, LLEIDA PIRINEUS, ZARAGOZA DELICIAS Y GUADALAJARA",
  "RE  REGIONAL", "AEROPUERTO", "DIRECTO A AEROPUERTO",
  "T.HOTEL 10156","BARCELONA SANTS", "PARA ESTE TREN NO SON VALIDOS LOS BILLETES DE CERCANIAS",
  "HD DIURNO 17194", "BADAJOZ", "EFECTUA PARADA EN LEGANES Y LUEGO SIGUE RECTO" 
};

  
void setup() {
  Serial.begin(115200);               // Usamos 115200 baud para el monitor serie
  Serial.println ("Pantalla Reloj OLED by Paco");
  Serial.println ("? para ver comandos\n");

  Wire.begin();                       // Inicializacion puerto I2C. SDA = A4, SCL = A5
  Wire.setClock(400000L);
  
  rtc.refresh();                      // lee modulo RTC DS3231
  ultSegundo = rtc.second();
  muestraHora = true;

  oled.begin(&Adafruit128x32, I2C_ADDRESS);   // Inicializacion pantalla OLED 128x32 SSD1306
  oled.clear();
  oled.setFont(Verdana12_bold);
  oled.setCursor (22,1); 
  oled.print ("Design by Paco");
  delay (2000);
}

void loop() {
  if (Serial.available() > 0)         // Si hay caracteres en el puerto serie los interpretamos
    entradaSerie();
    
  rtc.refresh();                      // Actualizamos desde el reloj RTC la hora actual
  segundos = rtc.second();        
  if (muestraHora) {                  // Mostrar hora
    if ((ultSegundo != segundos)) {   // Cada nuevo segundo actualizamos la pantalla
      minutos = rtc.minute();
      hora = rtc.hour();
      ultSegundo = segundos;
      if ((segundos > 25) && (segundos < 35))
        oledTemp();                   // Temperatura
      else {
        oled.setFont(lcdnums14x24);   // Hora
        oled.clear();
        oled.setCursor (8,1);         // columna en pixels. fila en 8 filas de pixel.
        oledHora();
      }
    }
  }
  else {                              // Mostrar mensaje movil
    if (tickTime <= millis()) {
      tickTime = millis() + 30;
      int8_t rtn = oled.tickerTick(&state);
      if (rtn <= 0)                   // Si fin de mensaje, vuleve a cargarlo
        oled.tickerText(&state, text[(mensaje*3)-1]);
    }
  }
}


void entradaSerie () {
  int c = Serial.read ();             // Lee caracter del comando
  switch (c) {
    case '#':                         // Comando #HH:MM:SS poner en hora
      hora = Serial.parseInt();
      minutos = Serial.parseInt();
      segundos = Serial.parseInt();
      hora = constrain(hora, 0, 23);
      minutos = constrain(minutos, 0, 59);
      segundos = constrain(segundos, 0, 59);
      //  RTCLib::set(byte second, byte minute, byte hour, byte dayOfWeek, byte dayOfMonth, byte month, byte year)
      rtc.set(segundos, minutos, hora, 1, 23, 02, 20);
      rtc.refresh();
      Serial.print ("Nueva ");    
    case '@':                         // Comando @ leer hora
      Serial.print("Hora: ");
      Serial.print(rtc.hour());
      Serial.print(':');
      Serial.print(rtc.minute());
      Serial.print(':');
      Serial.println(rtc.second());  
      break;   
    case 'T':                         // Comando T lee temperatura
      Serial.print("Temp: ");
      Serial.print(rtc.temp());  
      Serial.println (" ºC");
      break;
    case '-':                         // Comando - muestra hora
      muestraHora = true;
      break;
    case '+':                         // Comando +N muestra mensaje N en la pantalla OLED
      mensaje = Serial.parseInt();
      mensaje = constrain (mensaje,1,maxMensaje);
      muestraHora = false;
      oled.clear ();
      oled.setFont(lcd5x7);
      oled.println (text[(mensaje*3)-3]);         // Primera linea mensaje. Tipo tren
      oled.setFont(Verdana12_bold);
      oled.println (text[(mensaje*3)-2]);         // Segunda linea mensaje. Destino
      oled.tickerInit(&state, Adafruit5x7, 3);    // mensaje movil en linea 3
      break;
    case '\n':                        // filtra caracter nueva linea
    case '\r':                        // filtra caracter retorno del carro
      break;
    case '?':                         // Comando ? ayuda
    default:                          // Cualquier otro comando no soportado muestra ayuda
      Serial.println (F("Introduzca uno de estos comandos:"));
      Serial.println (F("@         Lee hora actual"));
      Serial.println (F("T         Lee temperatura actual"));
      Serial.println (F("#HH:MM:SS Poner en hora"));
      Serial.println (F("+N        Muestra mensaje N en la pantalla"));
      Serial.println (F("-         Muestra hora en la pantalla"));
      Serial.println (F("?         Esta ayuda\n"));
      break;
  }
}

void oledHora() {
  if (hora<10)                          // muestra hora actual en la pantalla OLED
    oled.print ("0");
  oled.print(hora);
  (minutos < 10)  ? oled.print (":0") : oled.print (":");
  oled.print(minutos);
  (segundos < 10) ? oled.print (":0") : oled.print (":");
  oled.print(segundos);
  }

void oledTemp () {
  oled.setFont(Verdana12_bold);         // muestra temperatura en pantalla OLED
  oled.clear();
  oled.setCursor (40,0);                // columna en pixels. fila en 8 filas de pixel.
  oled.set2X ();
  oled.print(int (rtc.temp()));  
  oled.print(" 'C");
  oled.set1X ();
  oled.setCursor (44,3); 
  oled.setFont(lcd5x7);
  oledHora();
}
Incluimos las librerías necesarias e inicializamos el objeto rtc para el reloj DS3231 indicando la dirección I2C del mismo (0x68). También indicamos la dirección de la pantalla (0x3C) e inicializamos el objeto oled.

Los chips para I2C tienen una dirección con la que nos comunicamos con ellos, algunos dan la posibilidad de elegir otra colocando unos puentes con lo que se pueden tener varios iguales pero con diferente dirección. Por ejemplo para cambiar dirección del bus I2C, de una pantalla OLED 128x64

Imagen

Tras definir unas variables para leer la hora( hora, minuto, segundo), el último segundo mostrado (ultSegundo), si mostramos la hora (muestraHora) y el mensaje que se muestra (mensaje) definimos las variables state y tickTime para la librería SSD1306Ascii que nos permitirán manejar una línea de texto móvil en la pantalla.

En maxMensaje indicamos el numero de mensajes que podremos elegir mostrar y que están contenidos en el array text[], cada mensaje son tres cadenas, el tipo de tren, destino y mensaje móvil, así que como maxMensaje es 5 tendremos que tener 15 cadenas en el array.

La cadena tipo de tren y destino han de tener como máximo los caracteres que quepan en un línea de la pantalla OLED, el mensaje móvil puede tener cualquier tamaño pero tened en cuenta el consumo de RAM así que no los hagáis muy largos.

Observad que el tipo del array es char* que indica que es un array de punteros a caracteres (cadenas).

En setup() inicializamos el puerto serie con Serial.begin() a 115200 baudios, para nuestro caso podemos elegir cualquier velocidad (9600, 19200, etc..) mientras corresponda con la del Monitor Serie, para otros casos dependerá del módulo con el que nos comuniquemos.
Luego enviamos un par de mensajes con Serial.println() para comprobar que la comunicación funciona e informar al usuario de que hay disponible un menú de ayuda.

Inicializamos el bus I2C con Wire.begin() y establecemos la velocidad del bus al máximo (400kHz) con Wire.setClock(), esto último realmente no es necesario. Observad la L al final del número que indica que la cifra se trate como un long en lugar de como un int que evidentemente estaría fuera de rango.

Ahora ya podemos refrescar los registros de tiempo del objeto rtc con refresh() y asignar a la variable ultSegundo, el segundo acabado de refrescar con second() . También pondremos la variable booleana muestraHora a true para que por defecto mostremos la hora actual.

El objeto oled lo inicializamos con begin() indicando el tipo de pantalla y su dirección I2C como requiere esta función. Observad el & en el dato tipo de pantalla, esto indica que no se le pasa el valor de ese dato sino que se le pasa un puntero a ese dato así la función podrá manipularlo.

Luego usaremos diferentes funciones de la librería para mostrar el mensaje de bienvenida 'Design by Paco' en la pantalla durante un par de segundos. Vanidoso que es uno :D

clear() borra la pantalla, setFont() selecciona una de las fuentes de texto, setCursor(columna, fila) indica la posición de la pantalla donde se empezará a escribir. La columna se indica en pixels (entre 0 y 127) y la fila en 8 pixels, como tenemos disponibles 32 pixels, fila ira de 0 a 3.

En loop() comprobaremos el puerto serie por si recibimos una instrucción, leeremos el reloj y actualizaremos la hora o temperatura en la pantalla cada segundo si muestraHora es true, en caso contrario se está mostrando un mensaje y si ha pasado un tiempo desplazaremos el mensaje móvil de la pantalla.

Si Serial.available() devuelve un valor mayor que cero indicando que hay caracteres en el buffer los leeremos con nuestra función entradaSerie() que mirará que comando introduce el usuario desde el Monitor Serie.

En entradaSerie() leemos el primer caracter del buffer con Serial.read() y con un switch comparamos el carácter con cada una de las ordenes disponibles para el usuario en los diferentes case.

Para el case '#' que se corresponde con el comando # en el que el usuario introduce la hora, minutos y segundos separados por : en el formato #HH:MM:SS, como ya hemos leído el # quedan por leer del buffer unos valores numéricos separados por un carácter no numérico.

La función Serial.parseInt() nos es muy útil ya que leerá hasta el carácter : descartándolo y convirtiendo los caracteres numéricos en su valor y devolviéndonos un int con el valor total. Sin esta práctica función tendríamos que hacer nosotros la conversión a base de Serial.read() y comparando el carácter recibido con uno numérico para convertirlo en su valor y llevar la cuenta para el valor final.
Lo repetiremos tres veces para leer el número correspondiente para las variables hora, minutos y segundos.

La función constrain() devuelve un valor entre el segundo y tercer parámetro del valor indicado como primer parámetro.
No vamos a hacer un chequeo completo de lo que ha introducido el usuario pero como mínimo aseguraremos que los valores estén dentro del rango de horas, minutos y segundos. Si el usuario introduce #45:85,6 obtendremos 23:59:06 que al menos esta dentro del rango. Si solo introduce # o falta algún valor, debido al timeout (fin de tiempo de espera) la función Serial.parseInt() devolverá un valor que será un cero con lo que para # serán las 00:00:00 y para las #12:15 serán las 12:15:00

Finalmente con la función set() pondremos en hora el RTC. Solo actualizamos los valores de hora, minutos y segundos, el resto: día de la semana, día, mes y año lo ponemos a un valor cualquiera.
Ahora refrescamos los registros de la hora actual y mostramos y respondemos al usuario con la cadena "Nueva ". Observad que en este case no hemos finalizado con break así que se seguirá ejecutando lo que viene a continuación, que es la respuesta al comando @

El case para @ hace que se envíe al usuario la hora actual. Con Serial.print() enviamos un texto o un valor que obtenemos del objeto rtc (directamente del RTC), horas con hour(), minutos con minute() y segundos con second(), el ultimo es un Serial.println() que nos enviará al final el carácter nueva línea \n con lo que el siguiente texto que se envíe aparecerá en la siguiente línea del Monitor Serie.
Aquí si finalizamos con break, por lo que si hemos entrado por # el usuario verá "Nueva Hora: 23:59:06" y hemos entrado por @ solo verá "Hora: 23:59:06"

El case T lee la temperatura del RTC con temp() y se la muestra al usuario.

El case - pone muestraHora a true para que se muestre la hora en la pantalla OLED

El case + de forma parecida a # lee un parámetro numérico del buffer, que limita con constrain() a un valor entre 1 y maxMensaje (el numero de mensajes definidos en el array text[] y lo guarda en la variable mensaje. Se pone muestraHora a valor false para indicar que se está mostrando un mensaje y no la hora y a continuación lee las cadenas del mensaje del array y las muestra en la pantalla OLED.

Primero borra la pantalla, con clear() y selecciona una fuente pequeña para la cadena del tipo de tren con setFont(). Con println() imprime la cadena en la primera línea de pantalla. El índice de la cadena a mostrar será el numero que ha introducido el usuario que está en la variable mensaje, multiplicado por 3 ya que cada mensaje son 3 cadenas pero restando 3, ya que el índice del array empieza en 0. Si el usuario introduce +1 el cálculo dará 0 que es la primera cadena del array, si introduce +2 será 3 que es la cuarta cadena del array, etc..
Ahora seleccionamos una fuente de texto más grande para el destino y hacemos el cálculo para imprimir la segunda cadena del mensaje y la imprimimos.

De mostrar el mensaje móvil se encargará la librería SSD1306Ascii, con tickerInit() inicializamos su presentación. El primer parámetro es un puntero a una variable del tipo TickerState para su control, el segundo es la fuente que usará y el tercero es la línea de la pantalla en que lo mostrará.
El case ? y para cualquier otro comando del que no tengamos definida respuesta (default) mostrará el menú de ayuda, utilizaremos la macro F() con Serial.println() para imprimir las diferentes líneas y ahorrar memoria RAM

Los case \n y \r evitarán que se nos muestre el menú tras introducir un comando si tenemos seleccionado en el Monitor Serie algún tipo de ajuste de línea.

Imagen

Siguiendo en el loop() refrescamos la hora del reloj y leeremos los segundos, si tenemos que mostrar la hora en la pantalla( muestraHora es true), comprobamos si se ha cambiado de segundo comparando segundos con ultSegundo, en ese caso leemos lo minutos y horas y actualizamos ultSegundo.

Normalmente mostraremos la hora con oledHora(), pero también mostraremos la temperatura durante unos segundos, he escogido hacerlo cada minuto entre los segundos 25 y 35, así que si segundos está entre esos dos valores llamaremos a la función oledTemp()

Nuestra función oledTemp() seleccionara un tipo de letra grande, que haremos el doble de grande con set2X() e imprimiremos la temperatura leída con temp(), luego pondremos con set1X() la letra normal a tamaño normal , y una fuente pequeña e imprimiremos la hora con nuestra función oledHora().

oledHora() imprimirá la hora en formato HH:MM:SS por lo que si algún dato solo tiene un digito, está entre 0 y 9, añadirá un '0' antes de imprimirlo. Para el primer dato (hora) usamos un if() y para los siguientes el operador ternario, imprimiendo ':' o ':0' según el caso.
Si no mostramos la hora, muestraHora es false, es que se está mostrando uno de los mensajes, así que tenemos que ir moviendo el mensaje móvil. Si ya ha pasado un tiempo desde la última vez que se movió, tickTime es menor que el tiempo devuelto por millis(), se actualiza tickTime para dentro de 30ms. Variando este valor haremos que el mensaje vaya más rápido o más lento.

Llamamos a tickerTick() con el puntero a state para que nos mueva el texto. El valor devuelto con esta función si es 0 o negativo (<= 0) indica que se ha terminado de mostrar completamente la línea móvil, así que la volvemos a cargar con tickerText() para que muestre el mensaje del array text[] correspondiente.
Si cambiamos <= 0 por <=1 no se esperará a acabar completamente el mensaje sino que lo mostrará a continuación.

saludos,

Paco
Última edición por fmco el Dom Mar 22, 2020 1:24 pm, editado 2 veces en total.
Avatar de Usuario
fmco
Forero
Mensajes: 945
Registrado: Vie Ene 20, 2006 4:09 pm
20
Ubicación: Reus
Contactar:

Re: Lenguaje de programación Arduino

Mensaje por fmco »

hola,

Esta pequeña guía sobre la programación de Arduino de este hilo la tenéis disponible en formato de libro en una primera edición mi web:

http://usuaris.tinet.cat/fmco/download/ ... rduino.pdf
Imagen

Iré añadiendo más capítulos en sucesivas ediciones a medida que vaya creciendo este hilo :D

saludos,

Paco
Avatar de Usuario
sls_h0e
Forero
Mensajes: 2328
Registrado: Sab Ene 21, 2006 2:27 pm
20
Ubicación: Madrid

Re: Lenguaje de programación Arduino

Mensaje por sls_h0e »

Paco, gracias por ser así
Un abrazo, Salus
Lenz + 2C + K.Bay
amorrortu
Forero
Mensajes: 80
Registrado: Jue Ene 15, 2009 6:17 am
17
Ubicación: Ibarra-Pais Vasco

Re: Lenguaje de programación Arduino

Mensaje por amorrortu »

Muchas gracias por todo Paco.
Saludos Luis.
Multimaus,DCC-gen,NanoX-88+Minimaus(ultimo firm),RENFE-H0 y de momento sin maqueta.
Avatar de Usuario
xaviermbcn
Forero
Mensajes: 3175
Registrado: Mié Ene 18, 2006 7:54 am
20
Ubicación: Barcelona

Re: Lenguaje de programación Arduino

Mensaje por xaviermbcn »

Gracias Paco,

voy a mirarlo detenidamente las próximas semanas !! :D :D

Salut!
Xavier

Maqueta 0 operativa, en construcción.
Maqueta H0 operativa, en construcción.

TC3.3, 5.0, 5.5, 5.8, 7.0, 8.0G2, 9.0C2 Gold, TP 5.5 a 9.0, TA, +Street.
Electronica: Central Lenz 3.6 + LI LAN + LH100 + PacoMouse !!
Retro: LDT RS8, RS16, PpP, LR100, CTMS
Accesorios: LDT 1-DEC, S-DEC, SA-DEC, LS100, LS150, CTMS
Otros: Rramp, LDT KSM.
Avatar de Usuario
fmco
Forero
Mensajes: 945
Registrado: Vie Ene 20, 2006 4:09 pm
20
Ubicación: Reus
Contactar:

Re: Lenguaje de programación Arduino

Mensaje por fmco »

12. Bits y PWM: Tren lanzadera analógico

Ahora tomamos un sencillo montaje del maquetismo analógico, un tren lanzadera, y vamos a añadirle nuevas funcionalidades aprovechando las opciones del lenguaje Arduino y su hardware.

Imagen

En un tren lanzadera en analógico se usa un relé para invertir la polaridad de la corriente de tracción una vez se llega a una de las estaciones de los extremos.

En su forma más simple, se monta un imán en la locomotora y un sensor Reed en cada una de las estaciones, este Reed cuando la locomotora se sitúa sobre él cierra el circuito de una de las bobinas de un relé biestable que hace que bascule e invierta la polaridad de la vía con lo que el tren cambia bruscamente de dirección y se dirige hacia la otra estación donde al llegar a su Reed activa la otra bobina del relé basculando nuevamente para cambiar la polaridad de la vía.

Otra versión que evita los cambios bruscos de dirección , sustituye los Reed por un temporizador y divide la vía en tramos. Los tramos extremos que corresponden a las estaciones se alimentan a través de un diodo que solo conducen cuando la polaridad que proporciona el relé hace que el tren vaya hacia la otra estación, si no es así, el tren se detiene bruscamente en ese tramo y espera a que el temporizador haga bascular el relé.

Imagen

Usar el Arduino como un temporizador usando delay() o leer unos Reed para hacer bascular un relé es una tarea sencilla pero ya hemos visto que fácil se resuelve con la electrónica normal.

Una de las primeras mejoras que se nos ocurre es controlar la velocidad para así evitar las paradas bruscas. Ya que tenemos unas placas de control de motores (como la shield usada en la plataforma deslizante) podemos prescindir del relé y hacer un frenado suave una vez detectemos que hemos llegado a una estación, por lo que tendríamos algo parecido a esto:

Imagen

Si no queremos montar imanes, los Reed los podemos sustituir por detectores de infrarrojos:

Imagen

Con estos sensores tendríamos una detección puntual, quizás sería mejor usar detectores de consumo como hacemos en DCC:
Imagen

Un problema de usar detectores de consumo en analógico es que cuando el tren este parado, no tendremos tensión y no veremos el consumo. Esto lo podemos solucionar alimentando brevemente la vía (del orden de microsegundos) , el pulso es tan corto que no moverá el motor pero veremos que hay consumo. También tendríamos que usar otro tipo de opto como el PC814 con diodos LED en anti paralelo, de otra forma no detectaríamos en una de las dos polaridades de la vía, aunque lo podríamos solventar de la misma forma que la detección en parado con un pulso con la polaridad correcta aunque no sé cómo afectaría a la larga al motor.

Si usamos la Motor Shield rev3 que vimos en un capitulo anterior (9. Uso de 'shields': Plataforma deslizante con motor paso a paso) ya incluye un manera de ver el consumo, de hecho hay dos, una por salida. Podemos medir el consumo (a razón de 1.65V/A) de cada salida con analogRead() lo que nos sirve para el tramo de las estaciones.

Para entender como detecta el consumo hay que ver cómo funciona el chip L298 cuando alimenta la vía:

Imagen
Imagen

Según el esquema de la Motor Shield rev3, cuando la salida PWMA (EnA) esta a HIGH se activa la salida A, la polaridad depende de DIRA (si BRAKEA está a LOW) que activa los transistores del L298 (entrada In1 o In2). Vemos que la corriente (en verde) circula atravesando el motor (OUT1-OUT2) y finalmente la resistencia donde medimos el consumo por el pin SNSA (A0), así que da igual la polaridad siempre que PWMA (EnA) esté a HIGH podremos medir la corriente en SNSA.

Si PWMA está a LOW ocurrirá como en la salida B (lado derecho de la imagen) no se activaran los transistores y no tendremos paso de corriente por la resistencia por lo que no tendremos medida de corriente.

En la siguiente imagen vemos de forma simplificada como se puede controlar el sentido de giro del motor (los transistores serian los interruptores) y medir corriente por el polo del motor que se conecta a masa.

Imagen

Si encontramos la manera de alimentar el tramo intermedio discerniendo cuando se está en él o cada una de las estaciones no necesitaríamos detectores de consumo externos.

Vamos a alimentar la vía así:

Imagen

Para que esto funcione correctamente y sin cortocircuitos ambas salidas A y B han de estar en fase, (tienen que tener la misma polaridad a la vez), así no hay cortocircuito al pasar de un tramo a otro. Recordad que la resistencia de senseo está en el lado del transistor que conecta a masa el pin de salida.

Así, si A+ y B+ son positivas y A- y B- son masa el tren irá de derecha a izquierda, si está sobre la estación derecha o en el tramo central habrá consumo de corriente en la salida B y no en la A. Si esta sobre la estación izquierda habrá consumo de corriente en la salida A pero no en la B.

De forma parecida si A+ y B+ son masa y A- y B- son positivas el tren irá de izquierda a derecha, si está sobre la estación izquierda o en el tramo central habrá consumo de corriente en la salida A y no en la B. Si esta sobre la estación derecha habrá consumo de corriente en la salida B pero no en la A.

Simplificando: Cuando el tren se mueve hacia la izquierda ( A+ y B+ son positivas) si hay consumo en A debemos frenar. Cuando el tren se mueve hacia la derecha (A+ y B+ son masa) si hay consumo en B hay que frenar.

Las salidas del L298 ponen en la vía la tensión de alimentación con lo que el tren iría a la máxima velocidad. Para acelerar y frenar, y para controlar la velocidad de marcha ya que no tenemos una salida analógica vamos a usar la modulación por ancho de impulso (PWM), que es una señal cuadrada de frecuencia constante, en la que podemos variar la duración del pulso (duty cycle) obteniendo una tensión media proporcional al ancho del pulso.

Imagen

En Arduino Uno, Mini y Nano, disponemos de 6 salidas PWM de 8bits (valor entre 0 y 255) en los pines 3, 5, 6, 9, 10 y 11 a través de la función analogWrite()

https://www.arduino.cc/reference/en/lan ... alogwrite/

Las funciones PWM por hardware emplean los Timer (temporizador) para generar la onda de salida. Cada salida conectada a un mismo temporizador comparte la misma frecuencia, aunque pueden tener distintos duty cycles.

El Timer0 controla las salidas PWM 5 y 6.
El Timer1 controla las salidas PWM 9 y 10.
El Timer2 controla las salidas PWM 3 y 11.

Ya que necesitamos que ambas salidas del L298 estén a la misma tensión al mismo tiempo tenemos que escoger el par de salidas que usen un mismo Timer y usar el mismo duty cycle, afortunadamente en la shield de motores v3 los pines PWM usados son el D3 y el D11 que son controlados por el Timer 2.

La frecuencia estándar para las salidas PWM en Arduino Uno, Mini y Nano es de 490Hz para todos los pines, excepto para el 5 y 6 cuya frecuencia es de 980Hz.

La frecuencia de los PWM se puede modificar cambiando el preescalado de los Timer correspondientes. El preescalador es un valor de 3 bits almacenado en los tres bits menos significativos del correspondiente registro de Timer/Counter: TCCR0B, TCCR1B y TCCR2B.

Para el Timer2, la frecuencia es de 31250Hz, y los preescalados de 1, 8, 32, 64, 128, 256, y 1024.

https://arduinoinfo.mywikis.net/wiki/Ar ... -Frequency

En nuestro caso, los motores de locomotoras funcionan mejor con una frecuencia baja de alrededor de 100Hz o menor por lo que cambiaremos el preescalado del Timer 2.

Podemos utilizar un patrón (máscara) con los bits a borrar, poner a 0 los tres menos significativos, y usar el operador binario & (Y binario, diferente del Y lógico &&) y luego poner a 1 los bits necesarios con el operador | (O binario, diferente del O lógico ||). Observad la B delante del número para indicar que es un numero binario.

Código: Seleccionar todo

TCCR2B = (TCCR2B & B11111000) | B00000110;    // set timer 2 divisor to  256 for PWM frequency of   122.55 Hz for D3 & D11
Por otra parte, para trabajar con bits hay una serie de funciones en lenguaje Arduino, entre ellas bitSet() para poner un bit a 1 y bitClear() para poner un bit a 0. También obtendremos unos 122Hz modificando el prescalado del Timer 2 a 256 cambiando el valor de sus bits de la siguiente forma:

Código: Seleccionar todo

bitSet(TCCR2B,2);		// Timer 2 divisor: 256, frecuencia 122.55 Hz (8ms)
bitSet(TCCR2B,1);
bitClear(TCCR2B,0);
La velocidad máxima la podremos regular con un potenciómetro conectado en A2, leeremos su valor con analogRead(), entre 0 y 1023, y lo adaptaremos para usarlo con el PWM mediante analogWrite(), entre 0 y 255.

Podríamos usar la función map() que realiza una serie de operaciones matemáticas para obtener el valor final. Esta es su definición:

Código: Seleccionar todo

long map(long x, long in_min, long in_max, long out_min, long out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
En nuestro caso una manera más rápida de cálculo es usando bits con el operador >> que desplaza los bits hacia la derecha, cada desplazamiento sería equivalente a dividir por 2.

Código: Seleccionar todo

valor = analogRead (A2) >> 2;
Esto nos dividiría el valor leído en la entrada analógica por 4, al hacer dos desplazamientos.

De forma análoga el operador << desplaza los bits hacia la izquierda, cada desplazamiento equivaldría a multiplicar por 2.

Cuando el tren pare en una estación estaremos un tiempo antes de reiniciar la marcha en sentido contrario para que los pasajeros puedan subir y bajar del tren.

Podemos usar un valor numérico para distinguir cada una de las fases (acelerar, correr, frenar, esperar) pero una manera que facilita la lectura del código es usar una enumeración.

El tipo de datos enum enumera automáticamente cualquier lista de identificadores que se le pase entre {} separados por , asignándoles valores de 0, 1, 2, etc.

Como es un tipo de datos, la variable que se defina solo contendrá alguno de estos identificadores válidos. Se puede asignar un valor entero a cualquier identificador de la enumeración con =, al siguiente identificador se le asignará el siguiente numero entero.

El programa seria este.

Código: Seleccionar todo

// Tren lanzadera analogico - Paco Cañada 2020

// Placa Arduino Motor Shield rev3
// salidas digitales:
const int dirA = 12;                      // dir: Polaridad de la salida
const int dirB = 13;
const int pwmA = 3;                       // pwm: activa/desactiva tension salida 
const int pwmB = 11;
const int brakeA = 9;                     // brake: Tiene que estar a LOW si no se ha cortado su jumper en la shield
const int brakeB = 8;
// entradas analogicas
const int snsA = A0;                      // Lectura de la corriente del motor. (1.65V/A)
const int snsB = A1;
const int poti = A2;                      // Potenciometro

#define deAaB LOW                         // sentido de marcha
#define deBaA HIGH
#define tiempoEspera 10000                // Tiempo de espera en estacion: 10s
#define numPulsos 12                      // numero de pulsos antes de cambiar de velocidad
#define minCorriente 6                    // Corriente minima para deteccion

int velActual;                            // velocidad actual del tren
int velObjetivo;                          // velocidad objetivo del tren (potenciometro)
int dirActual;                            // direccion actual del tren
int SensorA, SensorB;                     // lectura de la corriente
unsigned long AccDec;                     // temporizador para acelerar/frenar

enum Estado {ACELERA, CORRE, FRENA, ESPERA};

Estado Fase;

void setup()
{  
  pinMode(pwmA, OUTPUT);                  // Inicializa pines de salida del Arduino motor shield v3
  pinMode(pwmB, OUTPUT);
  pinMode(brakeA, OUTPUT);
  pinMode(brakeB, OUTPUT);
  pinMode(dirA, OUTPUT);
  pinMode(dirB, OUTPUT);

  bitSet(TCCR2B,2);                       // Timer 2 divisor: 256, frecuencia PWM en D3 y D11: 122.55 Hz (8ms)
  bitSet(TCCR2B,1);
  bitClear(TCCR2B,0);

  setVelocidad (0);                       // Detiene el tren
  digitalWrite(brakeA, LOW);              // Liberamos el freno
  digitalWrite(brakeB, LOW);
  buscaTren ();                           // localiza el tren y pone la direccion correcta
  AccDec = millis();                      // inicializamos tiempo temporizador
  leeVelObjetivo ();                      // velocidad objetivo segun potenciometro
  Fase = ACELERA;                         // arrancamos tren
}

void loop() {
  switch (Fase) {
    case ACELERA:
      if ((millis() - AccDec) > (numPulsos * 8)) {  // si tiempo mayor de numPulsos de 8ms (122hz) 
        AccDec = millis();
        velActual = (velActual > 251) ? 255 : velActual + 4; // Aumentamos velocidad
        setVelocidad (velActual);
        leeVelObjetivo();                 // Leemos velocidad objetivo segun potenciometro
      }
      if (velActual >= velObjetivo)       // Comprobamos si ha llegado a la velocidad objetivo
        Fase = CORRE;
      if (enEstacion())                   // Si ha llegado a la estacion frena el tren
        Fase = FRENA;
      break;
    case CORRE:
      leeVelObjetivo ();                  // Actualizamos velocidad segun potenciometro
      if (velActual != velObjetivo)
        setVelocidad (velObjetivo);
      if (enEstacion())                   // Si ha llegado a la estacion frena el tren
        Fase = FRENA;
      break;
    case FRENA:
      if ((millis() - AccDec) > (numPulsos * 8)) {  // si tiempo mayor de numPulsos de 8ms (122hz) 
        AccDec = millis();
        velActual = (velActual > 4) ? velActual - 4 : 0; // Disminuimos velocidad
        setVelocidad (velActual);
      }
      if (velActual == 0)                 // Si ha frenado, tenemos que esperar en la estacion
        Fase = ESPERA;
      break;
    case ESPERA:
      delay (tiempoEspera);               // Esperamos a que suban y bajen los pasajeros
      cambiaDireccion();                  // Vamos en direccion hacia la otra estacion
      Fase = ACELERA;
      break;
  }
}

void setVelocidad (int velocidad) {
  analogWrite (pwmA,velocidad);           // pins como PWM
  analogWrite (pwmB,velocidad);
  velActual = velocidad;
}

void setDireccion (int sentido) {
  digitalWrite (dirA, sentido);
  digitalWrite (dirB, sentido);
  dirActual = sentido;
}

void cambiaDireccion () {
  dirActual = (dirActual == deAaB) ? deBaA : deAaB ;
  setDireccion (dirActual);
}

void leeVelObjetivo() {
  int valor = analogRead (poti);          // lee potenciometro: valor de 0 a 1023
  valor = valor >> 4;                     // se divide entre 16. Son 64 escalones de velocidad PWM.
  velObjetivo = valor << 2;               // se multiplica por 4, para que este entre 0 y 255.
}

void leeAnalog () {
  SensorA = analogRead (snsA);            // Lee corriente del L298
  SensorB = analogRead (snsB);  
}

void leeSensores () {
  if (velActual > 0)                      // Solo si esta en marcha
    leeAnalog ();                         // lee corriente
}

void buscaTren () {
  setDireccion (deAaB);                   // direccion de A a B por defecto
  digitalWrite(pwmA,HIGH);                // activa tension salida. pins como salida digital
  digitalWrite(pwmB,HIGH);    
  delay (1);                              // esperamos un poco a que estabilize
  leeAnalog ();                           // Leemos la corriente
  setVelocidad (0);                       // pins como PWM y velocidad 0
  if (SensorB > minCorriente)             // Si esta en la estacion B, cambia la direccion
    setDireccion (deBaA);
}

bool enEstacion () {
  leeSensores();                          // Lee corriente del sensor segun direccion
  if ((dirActual == deAaB) && (SensorB > minCorriente))
    return (true);
  if ((dirActual == deBaA) && (SensorA > minCorriente))
    return (true);
  return (false);
}
Primero definimos todos los pines de la placa shield, de esta manera tendremos información de cómo está conectada y los pines ocupados además del pin para el potenciómetro.

También definimos unos valores para el estado del pin de dirección según vayamos de derecha a izquierda (deBaA) o viceversa (deAaB), el tiempo de espera en la estación(tiempoEspera), cuánto pulsos se han de producir antes de cambiar de velocidad para que sea progresiva (numPulsos), cuantos más pulsos mas tardará en acelerar y FRENAR!

Definimos el valor mínimo de consumo de corriente (minCorriente) para que se considere que hay ocupación, esto lo hacemos porque la placa tiene en sus salidas unos LED que indican su activación y que consumen algo de corriente que nos podría llevar a error.

Como variables definimos la velocidad actual del tren (velActual) y la velocidad objetivo (velObjetivo) según la lectura de la posición del potenciómetro, la dirección actual del tren (dirActual), el valor leído de corriente en los sensores (SensorA y SensorB) y por último un temporizador para acelerar/frenar (AccDec).

Vamos a usar un enum llamado Estado para los valores de las distintas fases o estados: ACELERA, CORRE, FRENA y ESPERA, que compararemos en un switch(), así que definimos la variable Fase del tipo de este enum

En setup() inicializamos los diferentes pines como salidas (OUTPUT) y cambiamos la frecuencia del PWM del Timer2 modificando los bits de su registro TCCR2B para 122,55Hz lo que nos da un periodo de unos 8ms

Primero, por seguridad, detenemos el tren poniendo el parámetro de nuestra función setVelocidad() a 0.

En la función setVelocidad() mediante analogWrite() se ponen ambos pines pwmA y pwmB a la vez como PWM con el duty cycle pasado como parámetro (de 0 a 255), además guardamos este valor en la variable velActual.

Siguiendo en setup(), se libera el freno de los motores poniendo las salidas brakeA y brakeB a LOW lo que permite que haya tensión según el PWM en las salidas del L298.

Tenemos que localizar el tren para poner el sentido de movimiento inicial correcto por lo que nuestra función buscaTren() llama a nuestra función setDireccion() con la dirección deAaB como parámetro inicialmente.

setDireccion() escribe el parámetro a la vez en los dos pines dirA y dirB con digitalWrite() para que ambas salidas estén en fase, además lo guarda en la variable dirActual.

Una vez seleccionada una dirección activamos las salidas pwmA y pwmB durante 1ms con lo que podremos leer un consumo en los sensores con leeAnalog().

Al usar digitalWrite() la función PWM de los pines se desconecta (usando digitalRead() también ocurriría) por lo que tenemos que recordar volver a ponerlos como PWM mas adelante.

También podríamos haber usado setVelocidad(255) ya que con el valor 255 la función analogWrite() pone la salida a HIGH y con 0 la pone a LOW, pero así se ve más claro lo que queremos hacer.

Nuestra función leeAnalog() lee ambos sensores con analogRead() y guarda su valor en las variables SensorA y SensorB respectivamente.

Una vez leída la corriente, volvemos a poner los pines en función PWM llamando a setVelocidad() con velocidad 0.

Si en el SensorB se lee más corriente que la mínima minCorriente indicando ocupación, se cambia la dirección de movimiento llamando a setDireccion() con el parámetro deBaA.

En setup() seguimos inicializando el temporizador AccDec con millis() y leemos la velocidad objetivo mediante nuestra función leeVelObjetivo().

En leeVelObjetivo() leemos la entrada analógica poti del potenciómetro. Como en cada paso de aceleración vamos a aumentar el duty cycle en 4 para que no sea excesivamente lenta vamos a convertir este valor en uno adecuado a analogWrite() pero de 4 en 4.

Si usamos map() o dividimos valor entre 4 estará entre 0 y 255 pero de 1 en 1. Así que con el operador >> dividimos entre 16 (valor pasara de entre 0 y 1023 a otro valor entre 0 y 63) luego con el operador << se multiplicara por 4 con lo que ya estará entre 0 y 255, además irá de 4 en 4.

Ya solo queda poner Fase a ACELERA antes de entrar en el loop()

El loop() es bastante simple, con un switch() compararemos Fase con los distintos estados y actuaremos en consecuencia.

En el case ACELERA comprobamos que haya pasado un tiempo suficiente para cambiar de velocidad (el tiempo del número de pulsos de 8ms definidos en numPulsos)

Si ha pasado el tiempo, se actualiza AccDec y se incrementa velActual en 4 si no es mayor de 252 usando el operador ternario y se cambia el duty cycle con setVelocidad(), además leemos el potenciómetro con leeVelObjetivo().

Ahora, la parte de control, comprobamos si se ha alcanzado la velocidad objetivo para cambiar de fase a CORRE, y por último la parte de seguridad, si se ha llegado a la estación se pasará a la fase FRENA.

La función enEstacion() devolverá true o false en función de si hay consumo en el sensor de la estación, así que llama a leeSensores() para actualizar SensorA y SensorB.

leeSensores() llama a leeAnalog() solo si velActual es mayor que 0, o sea se está moviendo el tren.

Luego en los if() según la dirección actual y (&&) si hay consumo de corriente en el sensor correspondiente se devolvería true. Si no es el caso, se devolvería false.

En el case CORRE leemos el potenciómetro y ponemos la velocidad actual a la velocidad objetivo. Luego si se llega a la estación se cambia la fase a FRENA

En el case FRENA de forma parecida al case ACELERA se disminuye la velocidad de 4 en 4 a medida que vaya pasando el tiempo. Si la velocidad actual es 0 pasaremos a la fase ESPERA.

En el case ESPERA simplemente esperamos un tiempo con delay() para que los pasajeros puedan subir y bajar del tren y cambiamos la dirección de movimiento con cambiaDireccion() antes de volver a la fase ACELERA.

En cambiaDireccion() según el valor de dirActual mediante el operador ternario se cambia a la contraria y se actualizan los pines de dirección del L298 con setDireccion()

Video:


Como ejercicio, ya que hay pines libres, se pueden poner unos semáforos a la entrada y/o salida de cada una de las estaciones que cambien su aspecto en concordancia con el movimiento del tren. También como mejora se podría leer otro potenciómetro para poder variar el tiempo de espera en las estaciones.

Problema: Cambia la frecuencia del PWM a 30Hz (periodo 32ms)
► Mostrar Spoiler
saludos,

Paco
Avatar de Usuario
xaviermbcn
Forero
Mensajes: 3175
Registrado: Mié Ene 18, 2006 7:54 am
20
Ubicación: Barcelona

Re: Lenguaje de programación Arduino

Mensaje por xaviermbcn »

Me supera la parte de electrónica pero, la verdad, combinado con Arduino se pueden hacer mil cosas.

Gracias por el ejemplo. Cuando pueda volver al taller seguiré con los inventos...

Salut!
Xavier

Maqueta 0 operativa, en construcción.
Maqueta H0 operativa, en construcción.

TC3.3, 5.0, 5.5, 5.8, 7.0, 8.0G2, 9.0C2 Gold, TP 5.5 a 9.0, TA, +Street.
Electronica: Central Lenz 3.6 + LI LAN + LH100 + PacoMouse !!
Retro: LDT RS8, RS16, PpP, LR100, CTMS
Accesorios: LDT 1-DEC, S-DEC, SA-DEC, LS100, LS150, CTMS
Otros: Rramp, LDT KSM.
Avatar de Usuario
Alfred
Forero
Mensajes: 897
Registrado: Jue Abr 24, 2008 9:41 pm
18
Ubicación: Sant Martí de Provençals

Re: Lenguaje de programación Arduino

Mensaje por Alfred »

Manolito.jpg
Manolito.jpg (50.12 KiB) Visto 2620 veces
Manolito.jpg
Manolito.jpg (50.12 KiB) Visto 2620 veces
l'Alfred
Responder
 
  • Contacto
  • Nombre de Usuario:
    Email:
    Asunto:
    Mensaje: