Desarrollo de la biblioteca

En este documento se describe con detalle los apectos de implementación de SyncroARD: el proceso seguido, los problemas encontrados, así como los planes de futuro que se tienen sobre la biblioteca.

Tabla de contenidos:

1 Motivación

Un sistema distribuido es un conjunto de computadores conectados entre sí a través de una red de comunicación, en el que los distintos componentes software y hardware se comunican y coordinan exclusivamente mediante paso de mensajes. En este tipo de escenarios (en los que no existe una fuente temporal común a todos los nodos) la sincronización se vuelve fundamental por diversos motivos:

  1. Cada nodo puede compartir sus recursos con otros nodos. La sincronización ayuda a resolver conflictos en la asignación de recursos.
  2. Todos los computadores comparten sus propios datos. Para poder acceder a los datos actualizados de cada computador, el time stamp de todos los nodos debe ser el mismo.
  3. En ocasiones, puede requerirse que ciertos nodos se coordinen para realizar una determinada tarea, por ejemplo, que uno espere a otro o que ambos ejecuten un proceso al mismo tiempo.

Este proyecto se centra en dar una solución cómoda y efectiva para el último caso de uso de la sincronización: ejecutar tareas al mismo tiempo en un sistema distribuido. Esta restricción es bastante frecuente en campos como la robótica, donde puede requerirse que dos o más actuadores efectúen un mismo movimiento al unísono.

Además, en la robótica, también es muy frecuente el uso de tarjetas de Arduino. No existe una biblioteca, ni mecanismos sencillos, para llevar a cabo este tipo de sincronización en esta plataforma. Es en este contexto donde nace SyncroARD.

1.1 Objetivos del proyecto

  1. Desarrollar una biblioteca de Arduino que permita sincronizar una tarea en dos tarjetas.
  2. La biblioteca desarrollada debe ser robusta y lo más determinista posible.
  3. La bibloteca desarrollada debe seguir la filosofia de Arduino: Keep it simple.
  4. Documentar y ejemplificar exahustivamente su uso.

2 Mecanismos de comunicación

En un sistema distribuido en el que se produzca interacción entre los dispositivos, debe existir un mecanismo que permita mandar información entre ellos, es decir, un mecanismo (o varios) de comunicación. En Arduino, existen tres formas diferentes de comunicar tarjetas:

El puerto serie está pensando especialmente para comunicar una tarjeta de Arduino con un ordenador mediante un USB. Utilizarlo para comunicar dos Arduinos puede resultar engorroso, por lo que será descartado. Esto nos deja dos posible candidatos para la comunicación: SPI e I2C. A continuación se muestran las ventajas e incovenientes de cada uno:

SPI:

I2C:

El bus SPI presenta características interesantes, como la comunicación Full Duplex y la velocidad de tranmisión. No obstante, I2C es mucho más sencillo de utilizar (tanto a nivel de conexiones entre las tarjetas, como a nivel de programación). De cara a construir una biblioteca, la sencillez es un aspecto relevante. Además, I2C es el método más popular para conectar tarjetas de Arduino. La propia página oficial de Arduino tiene un tutorial sobre como realizar un Master Writer/Slave Reader utilizando el bus I2C.

En definitiva, pese a tener una valocidad de transmisión más lenta, I2C será el principal mecanismo de comunicación de SycroARD.

3 Time Triggering VS Event Triggering

El principal objetivo de la sincronización es lograr que una determinada tarea o procedimiento se ejecute al mismo tiempo en un conjunto de dispositivos. En consecuencia, debe de existir un mecanismo que dispare (triggering) la ejecución de la tarea en cada una de las tarjetas. En sistemas de tiempo real, este triggering puede ser de dos tipos: temporal o por eventos:

En definitiva, los sistemas disparados por eventos dan una respuesta más rápida cuando la carga es baja, pero si la carga es alta, tiene más overhead y posibilidades de fallo. Los sistemas disparados por tiempo tienen las propiedades opuestas, y sólo son adecuados en entornos relativamente estáticos en los que se sabe de antemano mucho sobre el comportamiento del sistema. Cuál es mejor depende de la aplicación.

Para SyncroARD, tras varias pruebas, se utilizó finalmente event triggering para la implementación : el maestro manda a través de los mecanismos de comunicación una señal que genera una interrupción encargada de ejecutar la tarea en el esclavo . Pese a que esta es la decisión final, inicialmente se optó por un enfoque híbrido, aunque los problemas encontrados lo hicieron inviable. Todo esto se describe en el siguiente apartado.

4 Mecanismos de sincronización

En este apartado se describen todos los enfoques que se han ido adoptado durante el desarrollo de la biblioteca para afrontar la sincronización entre las tarjetas, cuáles fueron sus inconvenientes y por qué se desecharon hasta alcanzar la solución final.

4.1 Enfoque 1 (Sincronización de los relojes de ambas tarjetas)

En este primer enfoque, la sincronización funcionaría de la siguiente forma:

  1. Se sincronizan la frecuencia y los ticks de reloj de las CPU de ambas tarjetas. Para ello, el tick y frecuencia del maestro se asignan al tick y frecuencia de la tarjeta esclavo.
  2. Puesto que las frecuencias y los ticks están sincronizados, simplemente la tarea debe comenzar en el mismo tick en ambas tarjetas.
  3. Periódicamemente, se vuelve a comprobar que la frecuencia y los ticks de ambas tarjetas sigan sincronizados. En caso de que no, se vuelven a reasignar.

Problema:

El reloj de FreeRTOS de Arduino es sólo de lectura, no hay forma de modificarlo. Como mucho, se puede consultar el número de ticks de reloj que han pasado desde que se encendió la tarjeta, para cada tarea (xTaskGetTickCount). Si no se puede modificar el reloj, esta solución es completamente inviable.

4.2 Enfoque 2 (EventGroup de FreeRTOS)

Puesto que no se puede modificar el reloj, la siguiente idea que se pensó para intentar sincronizar las tareas, pasa por el uso de mecanismos clásicos de los sistemas concurrentes y distribuidos: semáforos binarios, mutex, barreras, etcétera. Concretamente, FreeRTOS dispone de una biblioteca complementaria que permite sincronizar varias tareas. Se trata de la biblioteca event_group.h.

Este complemento proporciona una barrera. En computación paralela, una barrera, para un grupo de hilos o procesos, significa que todos los que implementen esta barrera deberán parar en ese punto sin poder ejecutar las siguientes líneas de código hasta que todos los restantes hilos/procesos hayan alcanzado esta barrera. Event_group.h proporciona el método xEventGroupSync, una barrera para las tareas de FreeRTOS.

La idea de la sincronización para este enfoque es la siguiente: antes de la ejecución de la tarea en el bucle principal, se coloca la barrera xEventGroupSync. De esta forma, las tarjetas deberán esperarse la una a la otra hasta que ambas se encuentren al principio de la tarea. Si esto se hace en cada iteración del bucle principal, las tareas se ejecutarán a la par.

Problema:

Para funcionar, xEventGroupSync necesita de una estructura de datos global en la que almacenar la información de la barrera (EventGroupHandle_t). Dicha estrutura debería ser compartida por ambas tarjetas. Sin embargo, Arduino no dispone de ningún mecanismo de memoria compartida (Shared Memory).

4.3 Enfoque 3 (Comienzo simultáneo y sincronización periódica)

xEventGroupSync no puede utilizarse para sincronizar tareas de tarjetas diferentes, pero sí puede sincronizar tareas de la misma tarjeta. La idea de este tercer enfoque es la siguiente:

  1. El maestro tendrá dos tareas de FreeRTOS sincronizadas con la barrera de Event Group. Una será la tarea que se quiere sincronizar. La otra, se encargará de enviar a través del bus I2C una señal (un byte que no necesita ser leído) a la tarjeta esclavo. La idea es que la tarea comience a ejecutarse al mismo tiempo que se notifica a la tarjeta esclavo.
  2. El esclavo estará en espera activa hasta que reciba la notificación del maestro. Cuando la reciba, empezará a ejecutar la tarea en su loop.

De esta forma, las tareas comenzarán a ejecutarse a la vez en ambas tarjetas, con una diferencia de tiempo igual al tiempo del transmisión del bus I2C. Esta sincronización solo se realiza al comienzo, cuando ambas tarjetas se conectan.

Problema:

Las tareas comienzan a ejecutarse al mismo tiempo, pero al cabo del rato, una de las tarjetas acaba desfasándose. Para solucionar esto, cada cierto tiempo, el maestro debería comprobar el desfase (en ticks de reloj o en tiempo) existente entre ambas tarjetas y esperar (u ordenar al esclavo que espere, depende de quien vaya por detrás) tanto tiempo como sea necesario antes de realizar una nueva iteración del bucle principal, para así volver al estado de sincronización. El inconveniente de esta solución es que no existe una fuente temporal común a ambas tarjetas. Cada tiempo es relativo al punto en el que se encendió la tarjeta, lo que hace imposible medir el desfase.

4.4 Enfoque Final (Sincronización basada en Eventos)

El funcionamiento es exactamente el mismo que en el enfoque anterior, solo que ahora, el maestro no sólo notifica al esclavo al principio, sino que lo notifica en cada iteración del bucle principal de la tarea. Por su parte, el esclavo realizará una unica iteración de la tarea cada vez que reciba una notificación del maestro.

Dicho de otra forma, cada iteración en el maestro se sincroniza (usando Event Group) con el envío de la notificación al esclavo. Cada vez que el esclavo reciba una notificación por el bus I2C, realizará una sola ejecución de la tarea.

Se trata de una sincronización basada puramente en event triggering. Ahora no se produce nigún desfase, y la diferencia temporal en la ejecución de las tareas depende, exclusivamente, del tiempo de transmisión por el bus I2C.

5 Redundancia en la comunicación

La sincronización depende integramente del bus I2C. Si por cualquier motivo la notificación no se envia (o no llega) al esclavo, este no ejecutará la tarea. Para mejorar la reliability del sistema, se añadirá un segundo mencanismo de comunicación (redundancia).

Podría añadirse un segundo bus I2C, pero la literatura recomienda que cuando se añade redundancia a un sistema crítico, el segundo mecanismo sea de una tecnología diferente. Si fuesen las mismas, el defecto o fallo de una puede replicarse en la otra, anulando completamente la ventaja que proporciona la redundancia.

Por ejemplo, podría utilizarse el bus SPI. No obstante, al ser un mecanismo de respaldo, quizás SPI sea demasiado complejo. Además, cuanto más sencillos sean los mecanismos/procesos, menor será el riesgo de fallo. Por este motivo, y puesto que realmente no es necesario mandar ninguna información relevante por los canales, solo una señal o notificación, se empleó un Pin de Entrada/Salida de Propósito General (GPIO) como segundo mecanismo de comunicación. Ahora el proceso es el siguiente:

  1. El maestro sincroniza tres tareas con la barrera de Event Group: la ejecución de la tarea sincronizada, el envío de la notificación por el bus I2C, y el envío de un pulso de voltaje a través del pin 2.
  2. El esclavo ejecuta la tarea si ha recibido un byte por el bus I2C o un pulso por el pin 2.

De esta forma, si la notificación por el bus I2C no llega, la tarea se seguirá ejecutando gracias al pulso del GPIO. Además, hay que decir que no es obligatorio el uso de ambos canales. SyncroARD está programado para que se pueda utilizar uno, otro o ambos.

6 Trabajo futuro

A continuación se listan algunas ideas que pueden contribuir a mejorar la biblioteca, pero que aún no han sido implementadas:

7 Conclusión

Este proyecto toca dos contextos que pueden relacionarse fácilmente con el campo de los Sistemas Críticos:

Referencias