Emulador de un Casio F91W basado en FreeRTOS (1 de 2)

Emulador de un reloj Casio F91W basado en un PIC24 de Microchip, por José Antonio García Peiró.
El sistema pretende emular al modelo F91W, imitando las especificaciones y el funcionamiento del reloj de Casio.



El emulador dispone de 3 botones, un display y un altavoz que permiten al usuario interactuar de la misma manera que con el F91W, aunque con una interfaz totalmente distinta.

Este gadget no tiene ni pantalla ni botones, para visualizar la hora se utiliza un mecanismo similar al de los relojes «propeller clock» o «giroplay» y para detectar las teclas pulsadas se utilizan LEDs como sensores.

El altavoz es piezo-eléctrico, igual que en el Casio.

El proyecto tiene unos 5 meses y la mayor parte del tiempo la he dedicado a escribir el software, con el objetivo de iniciarme con los RTOS. En proyectos anteriores me encontré con la dificultad de mantener y ampliar el código cuando el proyecto iba creciendo. Por ejemplo: Si queremos añadir una nueva funcionalidad al sistema será fácil si nuestro sistema aun es pequeño, pero será cada vez mas y mas difícil a medida que el sistema vaya creciendo. Esto no ocurre si diseñamos el sistema basado en un RTOS. En este caso, el sistema se divide en subsistemas y al RTOS se le indican las interconexiones entre ellos. Si queremos añadir una nueva funcionalidad será tan fácil como escribirla e indicar al RTOS su relación con las demás.

Algunas fotos

Circuito
Este es el aspecto del circuito montado. En la parte inferior se acopla un portapilas con dos pilas tipo AAA. Tambien se puede ver como el conector ICSP tiene un formato miniUSB.

Pulsando teclas

Esta  imagen muestra como se usan los LEDs cuando se detecta una pulsación.

Aquí un vídeo del funcionamiento:

Funcionamiento

 

El sistema esta dividido en 8 tareas concurrentes comunicadas entre sí con colas «queues». Cada tarea incluye objetos y librerías para efectuar su función. Las tareas que acceden a un mismo recurso poseen mecanismos de sincronización como mutex. Las tareas se pueden agrupar en cuatro grupos diferentes según su rol en el sistema: productoras, procesadoras, consumidoras y control. Cada grupo de taras posee prioridades distintas, teniendo la menor las productoras … y la mayor la de control.

Para utilizar el gadget el usuario debe agitar continuamente la mano para leer la información. El aparato tiene una columna de 24 LEDs y genera en el aire una imagen estable de 128*24 pixels. Incorpora un acelerómetro para sincronizar el movimiento de la mano con el barrido de la imagen y así mantenerla lo más estable posible en el aire.
Los mismos 24 LEDs se pueden poner en modo inverso y así ser usados como sensores. De este modo se emulan los 3 botones (A B y C) del Casio.

El sistema también posee un botón, un botón físico real, que no existe en el reloj Casio. Este botón permite “despertar” al reloj durante un tiempo para que el usuario interactúe con él. Además el botón comparte pin IO con el altavoz y, aunque sus funciones son totalmente incompatibles, el sistema sincroniza perfectamente el pin IO de tal manera que las dos funciones se ejecuten en “paralelo” y NUNCA se interfieran entre sí.

Después de un reset, la función main configura todas las 8 tareas y arranca el RTOS. En este punto solo hay dos tareas en funcionamiento: TaskButton y TaskWakeUp.
Cuando el usuario pulsa el botón, la tarea TaskButton envía un mensaje a la tarea TaskWakeUp y esta ultima despierta todas las demás tareas. Pasados 20 segundos, las vuelve a suspender.

Mientras todas las tareas están despiertas, el usuario puede interactuar con el reloj visualizando en pantalla la misma información que en el reloj original y navegando por los mismos menús.
Cuando pulsa una tecla la tarea GetLeds envía un mensaje a la tarea TaskWatch con la tecla pulsada.


La tarea TaskWatch le pasará a la función WatchProcces la tecla pulsada para actualizar el estado del reloj y después enviará a la tarea TaskGraphics todas las primitivas que componen la pantalla del reloj en el modo actual ( texto, líneas, símbolos…).

La tarea TaskGraphics pintará todas las primitivas en una matriz de 128*24 y cuando termine le pasará esta matriz a la tarea TaskPutLeds.

La tarea TaskPutLeds recibirá el mensaje y esperará a que la tarea TaskAcce le indique el momento exacto para mostrar el mensaje en el aire.

La tarea TaskAcce mide constantemente la aceleración y envía un mensaje a la tarea TaskPutLeds cuando la aceleración alcanza un máximo o un mínimo.

Las tareas TasButton y TaskSpeaker comparten un recurso (un pin IO del PIC), y para evitar que ambas accedan al recurso al mismo tiempo están sincronizadas mediante un mutex. Las tareas GetLeds y PutLeds también comparten recursos (los 24 LEDs y el Mosfet) y también están sincronizadas con un mutex.

Diagrama de la aplicación

Diagrama

Este grafico muestra un diagrama de bloques de la tarea TaskAcce y su equivalente en C. Los bloques Avg, Diff y AND, RSFF están modelados como objetos.

TaskAcce

El código

El código esta dividido en 3 partes principales:
El main. Este se encarga de definir la aplicación en si (tareas y su interconexión) y de arrancar el RTOS llamando a la función vTaskStartScheduler.
Las tareas. Hay 4 tipos de tareas según su rol:
-Productoras: Cogen datos del exterior del sistema como un pinIO o el ADC y envían un mensaje con el dato leído.
-Procesadoras: Reciben un mensaje, procesan la información y reenvían los resultados en otro mensaje.
-Consumidoras: Reciben un mensaje y lo mandan al exterior del sistema como a un LED o a algún periférico externo.
-Control: Supervisan el funcionamiento del sistema “escuchando” la información que se pasan las tareas.
Las utilidades. Aquí se agrupan todas las funciones de utilidad general necesarias para las tareas, como por ejemplo la clase Watch.c o las funciones graficas GLCD.c.

Voy a empezar describiendo la tarea TaskButton. Es una tarea de tipo productor, y realiza una función muy sencilla en el sistema: informar a quien lo necesite de que el usuario ha pulsado el botón.

Tiene 2 funciones vStartTaskButton() y vTaskButton(). La primera es llamada por el main y se encarga de indicar al RTOS los recursos que tiene esta tarea (un mutex llamado xMutexIO y un queue llamado xQueueKey). La segunda función es la que ejecutara el RTOS cuando este arranque.

 

#ifndef TASKBUTTON_C
#define TASKBUTTON_C
#include ".TaskButton.h"
int startTaskButton( xTaskButtonResources *pxResources ){
if( pxResources->xMutexIO == NULL ){
if( NULL == ( pxResources->xMutexIO = xSemaphoreCreateMutex() ) )
return -1;
vQueueAddToRegistry( pxResources->xMutexIO, ( signed portCHAR * )"xMutexIO" );
}
if( pxResources->xQueueKey == NULL ){
if( NULL == ( pxResources->xQueueKey = xQueueCreate( 5, sizeof( xMessageButtonKey ) ) ) )
return -1;
vQueueAddToRegistry( pxResources->xQueueKey, ( signed portCHAR * )"xQueueButtonKey" );
}

if( pdPASS != xTaskCreate( vTaskButton, ( signed portCHAR * ) "Button",
configMINIMAL_STACK_SIZE, pxResources, pxResources->uxPriority, &pxResources->xHandle ) )
return -1;
return 0;
}
void vTaskButton( void *pvParameters ){
xTaskButtonResources *pxResources;
pxResources = (xTaskButtonResources*)pvParameters;
xMessageButtonKey xMessage;
while(1){
vTaskDelay( TASK_BUTTON_DELAY );
if( pdTRUE==xSemaphoreTake(pxResources->xMutexIO,portMAX_DELAY) ){
TRISAbits.TRISA1 = 1;
if( !BTN ){
while( !BTN )    // Esperamos a que el usuario suelte el boton, esto impide mandar
// 100 mensajes repetidos si se pulsa el boton durante X segundos
vTaskDelay( TASK_BUTTON_DELAY );
TRISAbits.TRISA1 = 1;
xSemaphoreGive( pxResources->xMutexIO );
xMessage.key = PRESSED;
xMessage.time = xTaskGetTickCount();
xQueueSend( pxResources->xQueueKey, ( void * )&xMessage, portMAX_DELAY );
}
TRISAbits.TRISA1 = 1;
xSemaphoreGive( pxResources->xMutexIO );
}
}
}
#endif // #ifndef TASKBUTTON_C<

Una vez arrancado el RTOS el PC empezara a correr por la función vTaskButton y esta realiza lo siguiente en el bucle infinito while(1):
Primero comprueba si el recurso compartido esta disponible para su uso, de lo contrario esperará un tiempo y vuelve a comprobar. Hay que recordar que el botón y el altavoz están conectados al mismo pin y que hay que asegurar que NUNCA se lea del botón mientras se escribe en el altavoz. Para esto las dos tareas implicadas, vTaskButton y vTaskSpeaker comparten un mutex y se mantienen sincronizadas.
Cuando la tarea vTaskButton «se adueña» del mutex sabe que la tarea vTaskSpeaker no podrá tocar el pin IO hasta que vuelva a «liberar» el mutex.
Ahora, en posesión del mutex, sí podemos configurar el pin como entrada, leer su valor desconfigurarlo y liberar el mutex para que otra tarea pueda usarlo.
Si el valor leído es de “botón pulsado” se comunica a otras tareas enviando un mensaje.

¿Sencillo?, yo creo que si. ¿Eficiente?, no mucho…¿Robusto? 100%.
Las funciones xSemaphoreTake, vTaskDelay, xSemaphoreGive… son las herramientas que proporciona el RTOS y se encuentran perfectamente documentadas en su página web (ver links recomendados al final del artículo).

Tarea TaskAcce.

Esta tarea tiene como objetivo sincronizar el mensaje de la tarea TaskPutLeds con el movimiento de la mano y ofrecer así una imagen lo mas estable posible.
Debe leer continuamente el acelerómetro (a través del ADC) e indicar al sistema dos eventos clave del movimiento de la mano: cuando la mano esta a la izq. del todo y cuando esta a la drcha. del todo.
Con esta información se puede calcular el momento en el que se debe refrescar la imagen para que se superponga con la anterior.

Lo primero de todo es definir la relación que existe entre el movimiento en zig-zag de la mano y la aceleración medida.

Movimiento zig-zag

En esta figura se muestran los eventos que queremos detectar. En t = 1.54 la mano está a la izquierda del todo, su velocidad es 0 y su aceleración es mínima. En t = 4.68 la mano está a la derecha del todo, su velocidad es 0 y su aceleración es máxima.


Puesto que solo se puede leer la aceleración, se debe calcular cuando está en su punto máximo o mínimo, y esto se cumple cuando su derivada (la velocidad) es 0.

Diagrama de bloques del algoritmo.

Diagrama de bloques del algoritmo

En el diagrama de bloques esto esta implementado con el Diff y el Avg, aunque este último hace un promedio con un buffer de 8 muestras.

A partir de aquí los bloques son similares tanto para detectar el máximo como para el mínimo, así que solo describiré el máximo.

Lo primero que hay que hacer es definir un margen ( TRIGER_LEVEL ) por debajo del cual consideraremos que la señal es 0, y otro margen (ARM_LEVEL) que nos dirá si la aceleración es máxima o mínima.
También hay que evitar que se envie más de un mensaje por evento, y para eso se ha introducido un biestable RS. Cuando se envía un mensaje (simbolizado con una banderita levantada) se resetea el biestable, y este no vuelve a ponerse a uno hasta que la velocidad es inferior a -ARM_LEVEL.

En esta imagen se muestra el uso de los márgenes y el biestable en la detección.
Margenes y biestable

Aquí se muestra el código de la aplicación. Tiene, como la tarea anterior, 2 funciones vStartTaskAcce y vTaskAcce.
La primera función es llamada por el main durante la configuración de aplicación (antes de arrancar el RTOS). Recibe como parámetro una estructura que contiene manejadores (handler) de todos los recursos de la tarea, devuelve un -1 si ha habido error al inicializar crear algún recurso o 0 si todo ha sido correcto. Es responsabilidad del main leer el valor devuelto y actuar en consecuencia (en este caso el main aborta el programa).

La segunda es la que se encarga de detectar el máximo o mínimo con el algoritmo descrito antes y enviar un mensaje.

#ifndef TASKACCE_C
#define TASKACCE_C
#include ".TaskAcce.h"
int startAcceTask( xTaskAcceResources *pxResources )
{ // Create Queue and check it.
if(  NULL == ( pxResources->xQueue = xQueueCreate( 5, sizeof( xMessageAccMove ) ) ) )
return -1;
vQueueAddToRegistry( pxResources->xQueue, ( signed portCHAR * )"xQueueAcce" );

if( pdPASS != xTaskCreate( vAccelerometerTask, ( signed portCHAR * ) "Accelero",
configMINIMAL_STACK_SIZE, pxResources, pxResources->uxPriority, &pxResources->xHandle ) )
return -1;

return 0;
}
#define BUFF_SIZE 5//1800
int buff_A[BUFF_SIZE];
int buffcnt=0,  buffcntcnt=0, buffcntcntend=10;
void vAccelerometerTask( void *pvParameters )
{
xTaskAcceResources *pxAcceR;   // Task parameteres handler.
xMessageAccMove xMessage;    // Queue message.
int input;
Differentiator dffrnttr1;    // Differentiator block object.
RSFF rsff1, rsff2/*, rsff3, rsff4*/;  // Two RS flip-flops block objects.
Average avrg1/*, avrg2*/;     // Average block object.
ANDGate andgt1, andgt2/*, andgt3, andgt4*/; // Two input logic AND gate block objetc.
DifferentiatorConstruct( &dffrnttr1 );
DifferentiatorSetAt( &dffrnttr1, DERIVATOR_DT );
RSFFConstruct( &rsff1 );
RSFFConstruct( &rsff2 );
AverageConstruct( &avrg1 );
AverageSetBufferSize( &avrg1 , AVERAGE_LEVEL );
ANDGateConstruct( &andgt1 );
ANDGateConstruct( &andgt2 );
ACCS = 1;        // Enable ACCELEROMETER
pxAcceR = (xTaskAcceResources*)pvParameters;

vTaskSuspend( pxAcceR->xHandle );
while( 1 ){
#ifdef __MPLAB_DEBUGGER_ICD2=1
IFS0bits.AD1IF = 0;     // Convert ADC
AD1CON1bits.ASAM = 1;
vTaskDelay(1);//Si la tarea tiene prioridad suficientemente alta mejor usar vTaskDelay(1).
while(!IFS0bits.AD1IF);
AD1CON1bits.ASAM = 0;

input = ReadADC10(2);
#else
vTaskDelay(1);//Si la tarea tiene prioridad suficientemente alta mejor usar vTaskDelay(1).
#endif
DifferentiatorProcessV( &dffrnttr1, input );
AverageProcessV( &avrg1, dffrnttr1.out );
RSFFProcessV( &rsff1, andgt1.out, (avrg1.out>ARM_LEVEL) );
ANDGateProcessV( &andgt1, rsff1.Q, (avrg1.out<-FIRE_LEVEL) );
if( andgt1.out == HIGH ){
xMessage.direction = 1;
xMessage.time = xTaskGetTickCount();
xQueueSend( pxAcceR->xQueue, ( void * )&xMessage, ( portTickType ) 0 );
}
RSFFProcessV( &rsff2, andgt2.out, (avrg1.out<-ARM_LEVEL) );
ANDGateProcessV( &andgt2, rsff2.Q, (avrg1.out>FIRE_LEVEL) );
if( andgt2.out == HIGH ){
xMessage.direction = -1;
xMessage.time = xTaskGetTickCount();
xQueueSend( pxAcceR->xQueue, ( void * )&xMessage, ( portTickType ) 0 );
}
}
}
#endif //#ifndef TASKACCE_C

El código se podría haber implementado de muchas maneras, pero se ha intentado acercar a lo que seria la programación orientada a objetos(POO).
En lugar de utilizar una función que se llame diferencia, se han creado dos archivos: differenciator.c y differenciator.h
Differenciator.h- Contiene los atributos ( una estructura ) y la declaración de los métodos del objeto( declaraciones de las funciones).
Differenciator.c- Contiene las definiciones de los métodos del objeto.

Para utilizar un objeto tipo diferentiator podríamos utilizar un programa de este tipo:

#include
#include "differentiator.h"
Main(){
Differentiator diff1;
DifferentiatorConstructor( &diff1 );
while(i++){
DifferentiatorProcces( &diff1, i );
printf(%d”, diff1.out );
}
}

La tarea TaskWatch
Ésta es la tarea central del emulador. Se encarga de mantener el reloj y sus menús actualizados en pantalla.
Recibe la tecla pulsada de la tarea TaskGetLeds, actualiza el estado del reloj y envía una serie de primitivas que representan la pantalla a la tarea TaskGraphics.
Está dividida, como las dos anteriores, en dos funciones vStartTaskWatch y vTaskWatch.
El código de la tarea en si es sencillo y toda la implementación del reloj se encuentra en una clase llamada Watch. La clase Watch se apoya sobre otra clase llamada Menu.

#ifndef TASKWATCH_C
#define TASKWATCH_C
#include "TaskWatch.h"
// Casio Watch - FE10-1A Menu Emulation
int startTaskWatch( xTaskWatchResources *pxResources ){
if( pxResources->xQueueSound == NULL ){
if( NULL == ( pxResources->xQueueSound = xQueueCreate( 5, sizeof( xMessageWatchSound))))
return -1;
vQueueAddToRegistry( pxResources->xQueueSound, ( signed portCHAR * )"xQueueWatchSound" );
}
if( pxResources->xQueueKey == NULL ){
if( NULL == ( pxResources->xQueueKey = xQueueCreate( 5, sizeof( xMessageWatchKey ) ) ) )
return -1;
vQueueAddToRegistry( pxResources->xQueueKey, ( signed portCHAR * )"xQueueWatchKey" );
}
if( pxResources->xQueuePrimitive == NULL ){
if(NULL==(pxResources->xQueuePrimitive= QueueCreate(10,sizeof(xMessageWatchPrimitive))))
return -1;
vQueueAddToRegistry(pxResources->xQueuePrimitive,(signed portCHAR *)"xQueueWatchPrimitive");
}
if(pdPASS!=xTaskCreate(vTaskWatch,(signed portCHAR * ) "Watch", configMINIMAL_STACK_SIZE,
pxResources, pxResources->uxPriority, &pxResources->xHandle ) )
return -1;
return 0;
}
void vTaskWatch( void *pvParameters ){
xTaskWatchResources *pxResources;
pxResources = (xTaskWatchResources*)pvParameters;
xMessageWatchKey xMessageKey;
xMessageWatchSound xMessageSound;
xMessageWatchPrimitive xMessagePrimitive;
char text_message[20]; // make sure _sprint no write out of array.
Watch watch1;
int tmp=-1;
int i;
int refresh2 = 0;

WatchConstruct( &watch1 );

vTaskSuspend(NULL);

Qglcd_textArial( 4, 0, "MLS09", 1 );
Qglcd_update();
vTaskDelay(8000);

while(1){
vTaskDelay( 250 );

/*  if( IFS3bits.RTCIF ){
for( i = 0 ; i < 20 && IFS3bits.RTCIF == 1 ; i++ ){
// clear in IFS3bits.RTCIF in WatchStopAlarm() function.
xMessageSound.tone = 4000;// int freq
xMessageSound.reps = 4;//  int pattern
xMessageSound.lenght = 100;// int pitch
xMessageSound.time = xTaskGetTickCount();
xQueueSend( pxResources->xQueueSound, ( void * )&xMessageSound, portMAX_DELAY );
vTaskDelay( 1000 );
}
IFS3bits.RTCIF = 0;
}
*/

if( watch1.SIG && watch1.timedate.f.min == 0 && watch1.timedate.f.sec == 0 ){
//   xMessageSound.tone = 4000;
xMessageSound.reps = 0x5050;
//   xMessageSound.lenght = 100;
xMessageSound.time = xTaskGetTickCount();
xQueueSend( pxResources->xQueueSound, ( void * )&xMessageSound, portMAX_DELAY );
vTaskDelay( 2000 );
}

WatchProcessClock( &watch1 ); // Update clock from RTCC

while( xQueueReceive( pxResources->xQueueKey, &xMessageKey, 0 ) ){
//  xMessageSound.reps = (xMessageKey.key < 3)?0x0001:0x0007;
//  xMessageSound.time = xTaskGetTickCount();
//  xQueueSend( pxResources->xQueueSound, ( void * )&xMessageSound, portMAX_DELAY );

if( 1 == watch1.menu1.state%5 && xMessageKey.key == 3 ){
xMessageSound.reps = 0x5050;
xMessageSound.time = xTaskGetTickCount();
xQueueSend( pxResources->xQueueSound, ( void * )&xMessageSound, portMAX_DELAY );
xMessageSound.reps = 0x5050;
xMessageSound.time = xTaskGetTickCount();
xQueueSend( pxResources->xQueueSound, ( void * )&xMessageSound, portMAX_DELAY );
}
WatchProcessKey( &watch1, xMessageKey.key ); // insert value to internal FSM

// MenuPrint( Menu *this );
// If you have change a menu redraw.
if( tmp != watch1.menu1.state%5 ){
tmp = watch1.menu1.state%5;

glcd_fillScreen( 0 );
Qglcd_textArial( 15, 0, MenuGetNodeName( &watch1.menu1 ), 1 );
Qglcd_update();

// Delay 4000 Ticks. If Queue received, resume task.
if( xQueuePeek( pxResources->xQueueKey, &xMessageKey, 4000 ) )
continue;

// MenuFunctionsPrint( Menu *this );
// If press "unusable" function key, show "usable" keys.
}else
if(watch1.menu1.tree[watch1.menu1.state].connections[xMessageKey.key].pfunction==NULL ){
glcd_fillScreen( 0 );
Qglcd_pixel( 6, 2, 1 );
Qglcd_pixel( 6, 4, 1 );
Qglcd_pixel( 6, 6, 1 );
Qglcd_pixel( 6, 10, 1 );
Qglcd_pixel( 6, 12, 1 );
Qglcd_pixel( 6, 14, 1 );
Qglcd_pixel( 6, 18, 1 );
Qglcd_pixel( 6, 20, 1 );
Qglcd_pixel( 6, 22, 1 );

Qglcd_line( 64, 2, 64, 7, 1 );
Qglcd_line( 64, 10, 64, 15, 1 );
Qglcd_line( 64, 18, 64, 23, 1 );

Qglcd_text57( 10, 3, MenuGetNodeFunctionName( &watch1.menu1, 0 ), 1, 1 );

Qglcd_text57( 10, 10, MenuGetNodeChildName( &watch1.menu1, 1 ), 1, 1 );

Qglcd_text57( 10, 16, MenuGetNodeFunctionName( &watch1.menu1, 2 ), 1, 1 );
Qglcd_text57( 70, 3, MenuGetNodeFunctionName( &watch1.menu1, 3 ), 1, 1 );
Qglcd_text57( 70, 10, MenuGetNodeFunctionName( &watch1.menu1, 4 ), 1, 1 );
Qglcd_text57( 70, 16, MenuGetNodeFunctionName( &watch1.menu1, 5 ), 1, 1 );
Qglcd_update();
// Delay 4000 Ticks. If Queue received, resume task.
if( xQueuePeek( pxResources->xQueueKey, &xMessageKey, 4000 ) )
continue; // Implicit because his position.
}
}
// Show watch status...WatchPrint( Watch *this );
glcd_fillScreen( 0 );
switch( watch1.menu1.state ){
case 0:
case 5+0:
// LUNES P 23:59  ALM
// 31-12  23:59 59 SIG
Qglcd_text57( 9, 16, wday_names[ watch1.timedate.f.wday  ], 1, 1 );

_sprintf( text_message, "%02x-%02x", watch1.timedate.f.mday, watch1.timedate.f.mon  );
Qglcd_text57( 9, 7, text_message, 1, 1 )
if( watch1.PM24 ){
// ...Conver hour to 12 hour format...
int hour = DECtoBCD( BCDtoDEC(watch1.timedate.f.hour)%12 );
_sprintf( text_message, "%2x:%02x", hour, watch1.timedate.f.min  );
}else{
_sprintf( text_message, "%02x:%02x", watch1.timedate.f.hour, watch1.timedate.f.min  );
}
Qglcd_text57( 38, 7, text_message, 2, 1 );
_sprintf( text_message, "%02x", watch1.timedate.f.sec  );
Qglcd_text57( 93, 7, text_message, 1, 1 );
if( watch1.PM24 ){ Qglcd_text57( 32, 14, "P", 1, 1 );}
if( watch1.ALM ){ Qglcd_text57( 108, 16, "ALM", 1, 1 );}
if( watch1.SIG ){ Qglcd_text57( 108, 7, "SIG", 1, 1 );}
Qglcd_update();
vTaskDelay(2000);
break;

case 1:
case 5+1:
// ALARM  23:59  ALM
//    23:59   SIG
Qglcd_text57( 9, 16, "ALAR", 1, 1 );

if( watch1.PM24 ){
// ...Conver hour to 12 hour format...
_sprintf( text_message, "%02x:%02x", watch1.alarm.f.hour, watch1.alarm.f.min  );
}else{
_sprintf( text_message, "%02x:%02x", watch1.alarm.f.hour, watch1.alarm.f.min  );

}
Qglcd_text57( 38, 7, text_message, 2, 1 );

if( watch1.PM24 ){ Qglcd_text57( 32, 14, "P", 1, 1 );}
if( watch1.ALM ){ Qglcd_text57( 108, 16, "ALM", 1, 1 );}
if( watch1.SIG ){ Qglcd_text57( 108, 7, "SIG", 1, 1 );}

Qglcd_update();
vTaskDelay(2000);
break;
case 2:
case 5+2:
// ALARM  __:__  ALM
//    __:__   SIG
Qglcd_text57( 9, 16, "ALAR", 1, 1 );

if( watch1.PM24 ){
// ...Conver hour to 12 hour format...
_sprintf( text_message, "%02x:%02x", watch1.alarm.f.hour, watch1.alarm.f.min  );
}else{
_sprintf( text_message, "%02x:%02x", watch1.alarm.f.hour, watch1.alarm.f.min  );

}
Qglcd_text57( 38, 7, text_message, 2, 1 );

if( watch1.PM24 ){ Qglcd_text57( 32, 14, "P", 1, 1 );}
if( watch1.ALM ){ Qglcd_text57( 108, 16, "ALM", 1, 1 );}
if( watch1.SIG ){ Qglcd_text57( 108, 7, "SIG", 1, 1 );}

if( (xTaskGetTickCount()/500)%2 ){
switch( watch1.ChangeAlarmCounter ){
case 0: Qglcd_line( 40, 5, 60, 5, 1 ); break;
case 1: Qglcd_line( 70, 5, 90, 5, 1 ); break;
default: break;
}
}

Qglcd_update();
vTaskDelay(2000);
break;
case 3:
case 5+3:
// STOP  23:59 SPL
//    23:59 59
Qglcd_text57( 9, 16, "STOP", 1, 1 );

_sprintf( text_message,"%02d:%02d",(int)(watch1.out/3600)%24,(int)(watch1.out/60)%24);
Qglcd_text57( 38, 7, text_message, 2, 1 )

_sprintf( text_message, "%02d", (int)(watch1.out)%60  );
Qglcd_text57( 93, 7, text_message, 1, 1 );

if( watch1.SPL ){ Qglcd_text57( 92, 14, "SPL", 1, 1 );}

Qglcd_update();
vTaskDelay(2000);
break;
case 4:
case 5+4:
// _____ P __:__
// __-__  __:__ __
Qglcd_text57( 9, 16, wday_names[ watch1.timedate.f.wday  ], 1, 1 );

_sprintf( text_message, "%02x-%02x", watch1.timedate.f.mday, watch1.timedate.f.mon  );
Qglcd_text57( 9, 7, text_message, 1, 1 )
if( watch1.PM24 ){
// ...Conver hour to 12 hour format...
int hour = DECtoBCD( BCDtoDEC(watch1.timedate.f.hour)%12 );
_sprintf( text_message, "%02x:%02x", hour, watch1.timedate.f.min  );
}else{
_sprintf( text_message, "%02x:%02x", watch1.timedate.f.hour, watch1.timedate.f.min  );
}
Qglcd_text57( 38, 7, text_message, 2, 1 );
_sprintf( text_message, "%02x", watch1.timedate.f.sec  );
Qglcd_text57( 93, 7, text_message, 1, 1 );
if( watch1.PM24 ){ Qglcd_text57( 32, 14, "P", 1, 1 );}
if( (xTaskGetTickCount()/500)%2 ){
switch( watch1.ChangeTimeCounter ){
case 0: Qglcd_line( 93, 5, 103, 5, 1 ); break;
case 1: Qglcd_line( 40, 5, 60, 5, 1 ); break;
case 2: Qglcd_line( 70, 5, 90, 5, 1 ); break;
case 3: Qglcd_line( 24, 5, 34, 5, 1 ); break;
case 4: Qglcd_line( 9, 5, 19, 5, 1 ); break;
case 5: Qglcd_line( 9, 15, 34, 15, 1 ); break;
default: break;
}
}

Qglcd_update();
vTaskDelay(2000);
break;
default:
break;
}
}
}
#endif // #ifndef TASKWATCH_H

Esta tarea es mas larga, pero su código es más sencillo de entender, ya que la mayor parte del código solo trata de enviar las primitivas a la tarea TaskWatch.
Todas las funciones Qglcd_… están definidas en el archivo TaskWatch.h más o menos como:

#define Qglcd_pixel( x, y, c )
xMessagePrimitive.function = __glcd_pixel;
xMessagePrimitive.intA = x;
xMessagePrimitive.intB = y;
xMessagePrimitive.intC = c;
xMessagePrimitive.time = xTaskGetTickCount();
xQueueSend( pxResources->xQueuePrimitive, ( void * )&xMessagePrimitive, portMAX_DELAY );

La clase Menu

La case Menu esta compuesta de dos archivos:
Menu.h que declara los métodos y atributos del objeto.
Menu.c que define los métodos del objeto.
Un objeto del tipo menú implementa una maquina de estados finitos(FSM) de Mealy. Está declarado de la siguiente manera:

typedef struct connection{
int  newnode;
char functname[5];
void (*pfunction)(void*);
}connection;
//enum KEY_VALUE{NONE, APRESS,BPRESS,CPRESS,AHOLD,BHOLD,CHOLD};
typedef struct{
char name[5];
// int number;
connection connections[6];
}node;
typedef struct{
node *tree;
int state;
int size;
}Menu;

Y tiene los siguientes métodos:
Menu *MenuConstruct( Menu *this, node *tree, int size );
void MenuDestruct( Menu *this );
void MenuProcessV( Menu *this, int input, void * object );
char *MenuGetNodeName( Menu *this );
char *MenuGetNodeChildName( Menu *this, int input );
char *MenuGetNodeFunctionName( Menu *this, int input );
En la función  MenuConstructor se asigna al puntero tree la dirección de una estructura que contendrá la organización del menú. Esta es la usada para emular los menús del F91W:

Menú

Como el reloj tiene 6 posibles teclas (A,B,C cortos y A,B,C largos) cada estado de la maquina tiene 6 posibles acciones. Cada acción puede llamar a una cambiar de estado y/o llamar a una función apuntada por el parámetro pfunction.

Después de “construir” un objeto menú (MenuConstructor) solo debemos llamar a la función MenuProcessV. Ésta acepta 3 parámetros:
Menu *this: El menú que se pretende procesar.
int input: La tecla pulsada.
void * object: El parámetro(normalmente un objeto) que se le pasará a la función apuntada por pfunction.

Un objeto menú tiene en cada uno de sus nodos un nombre. Para obtener información sobre los nombres la clase tiene 3 métodos:
MenuGetNodeName: Devuelve el nombre del nodo actual.
MenuGetNodeChildName: Devuelve el nombre de un nodo inferior.
MenuGetNodeFunctionName, Devuelve el nombre de la función a la que apunta pfunction.

La tarea TaskWatch llama a la función MenuGetNodeName cada vez que el reloj cambia de menú y su texto es representado en formato Arial(24 pixels de alto).

La clase Watch
typedef struct Wacth{
Menu menu1;
rtccTimeDate timedate;
rtccTime alarm;
rtccTime stopwatch;
rtccTime splittime;
long start, stop, split, out;
int ChangeAlarmCounter, ChangeTimeCounter;
int PM24;
int ALM;
int SIG;
int RUN;
int SPL;
}Watch;
La clase Watch encapsula todo el funcionamiento interno del reloj. Almacena el valor de la hora, la alarma y el cronometro, así como variables de estado. Si por ejemplo ponemos el reloj en modo 24H se pone a uno el campo de Watch.PM24 = 1 y esto será usado en la representación del reloj.

Continúa con la segunda parte

Share