¡Interrupciones! ¡A MÍ!La oración anterior me hace parecer súper héroe. Aunque yo no lo sea, pero con un RTOS y la técnica que veremos hoy, puedo considerarme uno de ellos, al menos en la programación de microcontroladores, y después de hoy, todos podemos aspirar a ser un RTOS PIC SÚPER-HÉROE.
En la entrega anterior estudiamos el paso de mensajes, pero solamente vimos el paso de mensajes entre tareas, esta posibilidad de los RTOS es poderosa y además es el mejor método para pasar información de una tarea a otra, por su sencillez. El paso de mensajes es una necesidad de los SO, porque como expliqué, las tareas no son como las funciones, a las cuales un segmento de código llama y se queda esperando hasta que la función retorna. Entonces como no sabemos en que momento una tarea entrará en su contexto, hay que crear un mecanismo eficiente para que trabaje con los datos que debe servir otra parte del código.
Además de lo anterior, durante todo el cursillo, no hemos visto ni utilizado ninguna interrupción y eso, desde mi punto de vista, no es nada bueno. Los procesos de interrupción nos permiten atender con eficiencia procesos asincrónicos como la conversión analógica, la escritura en memorias EEPROM, las interrupciones externas, la recepción de datos por la USAR, el PSP o el MSSP y también procesos sincrónicos como los que producen los temporizadores.
Pero el uso de un RTOS, nos plantea un dilema con el uso de las interrupciones, ya que en principio cuando utilizamos un RTOS estamos imponiendo que las tareas se van a ejecutar más o menos cada cierto tiempo o que estarán bloqueadas en espera de algo. Por lo tanto, si deseamos que una tarea atienda un proceso asincrónico, ésta debe hacerlo por encuesta, es decir cuando le toque ejecutarse debe comprobar si hay información que procesar. Por otro lado, las interrupciones cuando ocurren, deben ser atendidas en el instante y no cuando al RTOS considere que deben ser atendidas.
Ahora tenemos por un lado una herramienta que nos obliga a utilizar los mecanismos de encuesta, el RTOS, pero que nos ayuda a crear código robusto, eficiente y con velocidad, ventajas nada despreciables. Por otro tenemos un mecanismo para atender procesos que no pueden esperar mucho tiempo en ser atendidos o se corre el riesgo de perder la información, y perder datos es inaceptable para un sistema embebido. Entonces: ¿cómo hacer para aprovechar de las ventajas de ambos?
Para solucionar este problema los RTOS deben permitir que desde una ISR (subrutina de atención a interrupción) podamos pasarle mensajes a cualquier tarea que lo requiera. Vamos a ver esta ventaja con un ejemplito simple.
Supongamos que tenemos una aplicación que tiene varias tareas, entre ellas hay una dedicada a atender la recepción de datos por el puerto serie, ésta tarea se ejecuta con una frecuencia que permite procesar los datos que llegan desde el puerto serie. Un método para hacerlo sería, que cada vez que a la tarea le toque ejecutarse, ésta compruebe si ha llegado un dato al puerto serie para tomarlo y procesarlo. Eso está bien, pero que pasa si mientras la tarea está esperando su turno de ejecutarse llega más de un dato al puerto serie, por supuesto que se perderán datos y esto si que no podemos permitirlo.
La solución al problema anterior es separar la atención de la llegada de datos al puerto serie del procesamiento de los datos recibidos, para ello dejamos en manos de una ISR la lectura del registro de datos y el control de los registros de estado del puerto serie y que la tarea se encargue, cada cierto tiempo, de procesar la información recibida.
El método anterior se puede implementar si el RTOS permite el paso de mensajes desde una ISR hacia una tarea del RTOS, invocando una función adecuada. De esta forma cuando se produzca la interrupción y nos vayamos hasta la ISR, lo que tenemos que hacer es leer el dato y mandarle un mensaje a la tarea, si llega otro dato lo leemos y lo mandamos y así sucesivamente, si la tarea tiene una cola de mensajes suficientemente larga, no debemos perder datos en la recepción por el puerto serie, aún cuando la tarea se ejecute con una frecuencia menor que la de recepción de datos en el puerto serie.
En el ejemplo de hoy vamos a hacer con RTOS algo parecido a lo que hace la función gets(), con la diferencia de que en vez de quedarnos como tontos esperando a que llegue el carácter de fin de línea o retorno de línea vamos a ceder el procesador cada vez que comprobemos que no ha llegado el carácter de terminación adecuado.
Para la implementación utilizaremos dos PIC16F877, en uno de ellos pondremos un programa que envía una cadena por el puerto serie hasta el otro PIC con una frecuencia de 3 segundos. El PIC que recibe los datos, implementa esta funcionalidad mediante la interrupción de la USART, los cuales pone en la cola de una tarea, la cual va reconstruyendo la cadena, hasta que esta está completa y entonces también la envía por su USART.
Este es el código del PIC que envía la cadena
#include "D:\Documentos\Projects\RTOS\RTOS.h"
#use rs232(baud=9600,parity=N,xmit=PIN_C6,rcv=PIN_C7,bits=9)
#use RTOS(timer=0, minor_cycle=10ms)
int8 iBuffer; //Indice en el buffer para ir llenandolo
#task (rate=3s, max=10ms) //Creamos una cola con 10 bytes utiles
void Serial();
void main()
{
setup_timer_0(RTCC_INTERNAL|RTCC_DIV_1);
rtos_run();
}
void Serial()
{
printf("Prueba\n");
}
y este el del PIC que recibe y retransmite la cadena
#include "D:\Documentos\Projects\RTOS\RTOS.h"
#use rs232(baud=9600,parity=N,xmit=PIN_C6,rcv=PIN_C7,bits=9)
#use RTOS(timer=0, minor_cycle=10us)
char cBuffer[17] ; //Aqui guardamos el texto a enviar por el puerto serie
int8 iBuffer; //Indice en el buffer para ir llenandolo
#task (rate=100us, max=10us, queue = 3) //Creamos una cola con 2 bytes utiles
void Serial();
void main()
{
setup_timer_0(RTCC_INTERNAL|RTCC_DIV_1);
enable_interrupts(GLOBAL);
enable_interrupts(INT_RDA);
rtos_run();
}
void Serial()
{
char cDato;
rtos_await(rtos_msg_poll()); //Esperamos hasta que haya algun dato en cola
while(rtos_msg_poll()) //Procesamos la cola completa
{
cDato = rtos_msg_read();
cBuffer[iBuffer] = cDato;
iBuffer++;
}
if(iBuffer == 16 || cDato == '\n') //Si esta toda la cadena la enviamos
{
printf("%s\r", cBuffer);
iBuffer = 0;
}
}
#INT_RDA
void fINT_RDA(void)
{
rtos_msg_send(Serial, getc()); //Tomamos el dato del buffer y lo ponemos en la cola
}
Aquí está el fichero con los programas y la simulación en proteus:
RTOS_ISRComo verán en la implementación de estos programas he puesto al PIC que retransmite los datos a ejecutar su tarea que procesa los datos recibidos por el puerto serie con una frecuencia mucho mayor que la del PIC que le envía los datos, incluso es mucho más rápida que la velocidad de transmisión recepción de los mensajes, esto me permite tener una cola muy pequeña y que los mensajes no se pierdan.
Yo les aconsejo que jueguen con la frecuencia de ejecución de la tarea, (parámetro rate en la declaración #task) en el PIC que retransmite, y el tamaño de la cola, verán como la cadena a veces se corta, en ocasiones el PIC no retransmite nada. Pero esto les dará una idea de cómo funciona el mecanismo.
Si por ejemplo aumentan la frecuencia de ejecución pueden poner una cola más pequeña, es el código del ejemplo. Si tienen una frecuencia menor, entonces tendrán que poner una cola más grande para que el mensaje quepa completo.
Otra solución a este problema es que la ISR escriba directamente en el Buffer y solamente le mande un mensaje a la tarea cuando se haya recibido el mensaje completo para que esta lo retransmita. Con esto ahorramos memoria RAM, ya que podemos poner una cola muy pequeña, además podemos transferirle a la tarea que atienda los mensajes de error, el procesamiento de comandos y cosas por el estilo. Todo depende de la aplicación y de la imaginación del programador.
Me tomó 1:30 horas escribir el programa y ponerlo a punto, otra hora para escribir el texto y varios días para pensar como enfocarles el problema. Ahora es tarea de ustedes poner todo esto en práctica.
Les propongo que hagan un programa para enviar datos por la USART, similar a printf(), pero más eficiente. Pueden por ejemplo utilizar sprintf(), para poner la cadena en RAM y después que la tarea configure el servicio de interrupciones, mande el primer byte y la ISR envíe el resto, cuando la ISR termine debe notificar a la tarea que ya está en condiciones de enviar más datos, de modo que la tarea le puede crear otra cadena y volver a mandar a transmitir. Notarán que hay que tener unas cuantas cosillas en cuenta, pero es un buen problema para practicar.
Un saludo Reinier.