En el
tutorial anterior hemos visto que lo que un compilador hace al escribir/leer
de la UART no es muy complicado. Esencialmente se reduce a leer/escribir de un
par de registros (TXREG, RCREG) verificando previamente que están libres (caso
de TXREG) o que sus contenidos se han renovado (caso de RCREG).
Aprovecharemos
estos conocimientos para implementar un programa de transmisión/recepción de
datos más eficaz, usando las interrupciones asociadas a la UART, tanto de recepción (RX, activada al recibir un byte) como de transmisión (TX, activada cuando la UART está libre para mandar otro byte).
Asimismo
usaremos dos buffers de mayor capacidad, uno de recepción (donde la
interrupción RX depositará los datos recibidos) y otro de transmisión (de donde
la interrupción de TX cogerá los datos a transmitir).
Veremos
como la implementación de estas técnicas puede llevarnos a drásticas
reducciones de la ocupación del PIC, liberándolo para otras tareas.
También
veremos como nuestra implementación con buffers e interrupciones puede
configurarse como la salida por defecto stdout, pudiendo ser usada directamente
por rutinas como printf, puts, etc. de forma transparente para el usuariuo.
Código asociado a esta entrada: uart3_int.c
--------------------------------------------------------------------------------------------
En la
implementación con buffers que haremos, el "usuario" no toca los registros
RXREC ni TXREG, sino sólo los buffers de rx/tx. Dichos buffers estarán
implementados como buffers circulares, de forma que si el usuario p.e. no va
procesando los datos recibidos estos se sobrescribirán si se supera la longitud
del buffer.
Lo ideal
en el caso de buffers circulares es que su tamaño sea una potencia de 2, lo que
facilitará resetear el índice del buffer al llegar al final. Si por ejemplo el
tamaño del buffer es 64, cada incremento del índice debe venir seguido de:
ind++;
ind&=63; lo que es más eficaz que: ind++; if(ind==64)
ind==0;
Lo ideal
sería usar buffers de 256 entradas que son especialmente eficaces. En efecto,
si usamos una variable de 8 bits (unsigned char) como índice, dicha variable se
reseteará automáticamente al llegar a 256 sin necesidad de añadir la
instrucción extra. Para una variable de 8 bits, la instrucción ind &= 255 es superflua.
La
inconveniencia es que en C18 si queremos declarar un array de 256 bytes o más
hay que hacer algunos apaños. No es complicado, pero retrasaremos dicha lección
hasta más adelante. Para no complicarnos, en este ejemplo usaremos un tamaño de
128. Veamos como reservar los buffers de entrada/salida así como los punteros
(índices) asociados:
#define BUF_SIZE 128
#define inc(x) {x++; x&=(BUF_SIZE-1);}
uint8 tx_buf[BUF_SIZE]; // TX buffer
uint8 tx_next=0; uint8
tx_sent=0; // TX indexes
uint8 rx_buf[BUF_SIZE]; // RX buffer
uint8 rx_next=0; uint8
rx_read=0; // RX indexes
Hay dos
punteros asociados a cada buffer, inicializados ambos a 0.
RX buffer:
- escribe en él la interrupción de RX, incrementando rx_next.
- lee de él el usuario, incrementando rx_read.
- Si rx_read==rx_next es que no hay nuevos caracteres a procesar.
TX buffer:
- escribe en él el usuario (lo que desee transmitir), incrementando tx_next.
- lee de él la interrupción de TX, cada vez que manda un byte, incrementando tx_sent.
- Si ambos son iguales, no hay nada nuevo que mandar.
Se ha
definido una macro para incrementar los punteros. En ella se incrementa el
puntero y se voltea si se sale del intervalo [0, BUF_SIZE-1].
Si el
usuario no procesa los caracteres recibidos es posible que se pierdan al
voltear el buffer de recepción. En este caso se podría disponer de una bandera
de buffer_overflow que se dispararía si rx_next supera a rx_read. En nuestro
caso no se implementará este control, ignorando posibles overflows del buffer.
Las correspondientes
rutinas de interrupción (ISR) son muy simples (como por otra parte debería ser todo
código de interrupción). Para la recepción, si se ha recibido algo (RX_flag) se
coloca el contenido de RCREG en rx_buf, incrementándose rx_next:
// RX_isr gets
executed when RX_flag is set. That means a new byte is waiting in RCREG.
// The code picks it
and places it in the next available (rx_next) pos of the RX buffer,
// increasing rx_next
in the process. Somewhere else the user must read the
// new bytes (from
rx_read to rx_next) from the RX buffer.
void RX_isr(void)
{
rx_buf[rx_next] = RCREG; //
Read RX char and place it in the RX buffer
inc(rx_next);
//rx_next++; rx_next&=(BUF_SIZE-1);
RX_flag=0;
}
Si el
buffer de transmisión está libre (TX_flag) y hay algo que transmitir se carga
el correspondiente byte de tx_buf en el registro de transmisión TXREG y se
incrementa tx_sent:
// TX_isr gets called
when TX_flag is set. That means that the port is ready to tramsmit.
// If
tx_next==tx_sent, there is nothing to send and TX_INT is disable.
// If
tx_next!=tx_sent, next byte in TX buffer is loaded in TXREG, and the
// pointer is
incremented, making sure it remains within the [0,BUF_SIZE-1] range
void TX_isr(void)
{
if (tx_sent==tx_next) disable_TX_int;
else { TXREG=tx_buf[tx_sent]; inc(tx_sent); }
TX_flag=0;
}
La
interrupción de recepción estará siempre activa (no queremos perder nada) pero
la de transmisión se desactiva si no hay nada que mandar (tx_sent==tx_next).
Esto evita que estemos entrando continuamente en la interrupción de transmitir
(porque el registro TXREG está disponible) cuando no tenemos nada que mandar.
Es responsabilidad del usuario activar la interrupción TX (enable_TX_int) cuando
escriba algo al buffer de salida.
Juntando
ambas rutinas en la rutina de interrupción:
// High priority
interruption
#pragma interrupt high_ISR
void high_ISR (void)
{
if (RX_flag) RX_isr();
if (TX_flag) TX_isr();
}
// Code @ 0x0008 ->
Jump to ISR
#pragma code high_vector = 0x0008
void code_0x0008(void) {_asm goto high_ISR _endasm}
#pragma code
Y eso es
todo. Ahora lo único preciso es que
nuestras rutinas de escribir al puerto serie deben mover caracteres al buffer
de transmisión (de tx_next en adelante, incrementando tx_next) y las rutinas de
lectura cogerlos del buffer de recepción (desde rx_read hasta rx_next,
incrementando rx_read):
El usuario
sólo debe modificar (incrementándolos adecuadamente) los punteros tx_next y
rx_read. El incremento de los punteros tx_sent y rx_next es responsabilidad de
las rutinas de interrupción.
Veamos las
ventajas de este tipo de enfoque.
Imaginad un programa en el que cada cierto tiempo hay que mandar cierta
información por el puerto serie. En C18 el stream stdout corresponde por
defecto al puerto serie, por lo que al usar rutinas como puts o fprintf los
resultados se volcarán al puerto serie. El siguiente programa abre el puerto
serie a 9600 y usar puts para mandar la cadena "0123456789ABCDEF" por
el puerto serie:
#include <usart.h>
#include <stdio.h>
void main()
{
uint8 sp, brgh, brgh_config;
TRISC=0; PORTC=0;
brgh=get_usart_speed(9600,20000,&sp);
brgh_config = (brgh)?
USART_BRGH_HIGH:USART_BRGH_LOW;
OpenUSART(USART_ASYNCH_MODE &
USART_EIGHT_BIT & brgh_config, sp); //Open port @ 9600
while(1)
{
PORTCbits.RC0=1; puts("0123456789ABCDEF"); PORTCbits.RC0=0;
Delay10KTCYx(25); // 50 msec
delay
}
}
Hemos incluido <usart.h> y <stdio.h> para poder usar OpenUSART() y
puts(). Tras abrir el puerto entramos en un bucle donde mandamos la cadena y
ponemos un delay de 50 msec. Abriendo un terminal @ 9600 veríamos llegar repetidamente la cadena "ABC …
".
Notad que hemos puesto el pin RC0 a
1 durante el tiempo en que puts está ocupado mandando el mensaje. La figura
siguiente corresponde a una captura de RC0 con el osciloscopio mientras el programa está
ejecutándose:
Claramente es lo esperado. La
rutina puts() bloquea al PIC durante unos 18 msec (tiempo necesario para mandar 16 chars + LF a
9600 bauds). Luego viene un espaciado de 50 msec (delay) y el proceso se
repite. El PIC está mandando 16 bytes en 68 msec (unos 230 bytes por segundo) y
esto le ocupa un 25% (18/68) de su tiempo.
Incluso sin tener un osciloscopio se puede estimar el tiempo de
ocupación tomando una medida del voltaje en RC0. En este caso obtenemos unos
1.3V. 5V corresponderían a una ocupación total (RC0 alto todo el tiempo), por
lo que 1.3 V correspondería a un porcentaje de un 25-30% (obviamente este
método es aproximado y puede depender de las características del voltímetro
usado).
Veamos como mejoran las cosas
usando nuestro enfoque de interrupciones. Es interesante darnos cuenta de que
podemos aprovechar nuestro trabajo sin tener que modificar apenas nada. Hemos
dicho que por defecto C18 usa el puerto serie como salida standard (stdout). El
usuario puede redireccionar la salida standard sin más que:
- Hacer stdout = _H_USER; para indicar a C18 que
sus rutinas de salida deben usar la función de usuario _user_putc( ) en
vez de la definida por defecto.
- Escribir una rutina propia llamada _user_putc(char) que escriba un carácter en el stream escogido.
Nosotros
seguiremos usando el puerto serie como salida, pero ahora en la rutina
_user_putc, en vez de usar los comandos que escriben directamente al puerto
serie usaremos nuestro buffer de salida:
int _user_putc(char ch)
{
tx_buf[tx_next]=ch; inc(tx_next);
enable_TX_int;
return ch;
}
La rutina
coloca el carácter recibido en el buffer de transmisión (buffer TX), en la posición tx_next. A
continuación incrementa dicho puntero y habilita la interrupción de
transmisión, indicando que tenemos algo que mandar.
Y ya esta.
A partir de ahora (tras redefinir stdout) siempre que usemos las rutinas de salida
puts, printf, etc. por debajo estaremos usando nuestro sistema de buffers de
una manera transparente.
El nuevo
programa principal apenas ha cambiado:
void main()
{
uint8 sp, brgh, brgh_config;
stdout =
_H_USER;
TRISC=0; PORTC=0;
brgh=get_usart_speed(9600,20000,&sp); //setup_UART(brgh,sp);
brgh_config = (brgh)?
USART_BRGH_HIGH:USART_BRGH_LOW;
OpenUSART(USART_ASYNCH_MODE &
USART_EIGHT_BIT & brgh_config, sp);
enable_RX_int; enable_global_ints;
enable_perif_ints;
while(1)
{
PORTCbits.RC0=1; puts("0123456789ABCDEF"); PORTCbits.RC0=0;
Delay10KTCYx(25); // 50 msec
delay
}
}
La única
diferencia es la redefinición de stdout (_H_USER) y la habilitación de las
interrupciones de las que depende nuestro sistema de buffers. El bucle
principal no ha cambiado. Si ejecutamos el programa volveremos a ver llegar por
el terminal la misma cadena "ABCD …".
Veamos el
retardo de la función puts (ahora usando nuestra _user_putc) a través del pin
RC0. Como en este caso estamos "pasándole" el trabajo a las
interrupciones hemos puesto también RC1=1 durante el tiempo en el que la
interrupción está activa ya que es también tiempo dedicado a mandar cosas por
el puerto serie. Veamos la ocupación tanto en puts (RC0, arriba en amarillo)
como en la interrupción (RC1, abajo en azul).
La
ocupación de RC0 ha sido reducida drásticamente, ya que con nuestra nueva
rutina puts() no espera a que los caracteres hayan sido enviados, simplemente
los copia al buffer de salida. De hecho ahora estamos enviando más bytes por
segundo ya que el siguiente bloque se manda 50 msec después (sin contar el
tiempo que tardan en salir los caracteres). El ritmo es ahora de unos 16/17
bytes cada 50 msec (unos 340 bytes/sec, un 50% más que antes).
En RC1 la
cosa parece más similar. El ancho del pulso sigue siendo unos 18 msec (ya que
16 caracteres a 9600 baudios no pueden tardar menos en salir). Sin embargo
dicho periodo de actividad no es sólido (ya que la interrupción solo tiene que
poner un byte en TXREG). La clave es no esperar a que el periférico termine.
Si hacemos
un zoom de la zona de un periodo de actividad veremos lo siguiente:
El pulso
amarillo inicial (menos de 1 msec) es el tiempo usado por puts para volcar los
16 bytes al TX buffer. Los pulsos azules (aún más cortos) corresponden al
tiempo usado por la interrupción en ir colocándolos en TXREG. Están espaciados
porque la interrupción no vuelve a entrar hasta que el periférico no ha termina
y libera TXREG.
Todo el
tiempo en el que RC1 está abajo (la mayoría) el PIC puede estar haciendo otras
tareas. De hecho la dedicación en el periodo de 18 msec que antes tardábamos en
mandar el mensaje es del orden de 1 msec. En total el PIC está dedicado a la
tarea de comunicaciones 1 msec cada 50 msec. Un 2% frente al 25% de antes (a
pesar de estar mandando un 50% más de bytes como vimos).
De nuevo
podríamos usar un voltímetro para estimar la ocupación sin tener un
osciloscopio. Basta usar el mismo pin, p.e. RC0, tanto para la interrupción
como para puts. El voltaje medido es de 0.12V lo que corresponde a 0.12/5
= 0.024 (2.4%) lo que es compatible con
lo visto en el osciloscopio.
La
desventaja del enfoque de interrupciones es que somos nosotros los responsables
de no mandar más bytes por segundo que los que puede procesar el puerto serie.
Si lo hacemos tendremos un overrun del buffer de TX y habrá caracteres
perdidos. Con el enfoque de esperar obviamente eso no puede pasar (a costa de
no hacer otra cosa).
Muy buen tutorial.
ResponderEliminar;)
Gracias por el feedback. Estoy un poco liado en el curro con el arranque del curso, pero espero poder añadir algún que otra entrada adicional durante este mes, posiblemente de PWM.
EliminarAntonio
Es justo lo que estaba pensando en crear para poder leer y escribir correctamente de la UART sin perder datos o bloquear el PIC.
ResponderEliminarLa mejor explicación de uso de la UART que he encontrado.
Estoy utilizando un PIC18F97j60 de la placa PICDEM.net 2, y pegándome con el código de Microchip, voy a adaptarlo a la misma y a ver si así no se bloquea el servidor web
saludos