Translate

viernes, 22 de junio de 2012

Comunicaciones puerto serie (UART)

Vamos a dedicar un par de entradas a examinar con cierto detalle el funcionamiento de las comunicaciones asíncronas (UART, o lo que habitualmente se conoce como el puerto serie) de la familia PIC18.

Nos centraremos en el software, dejando de lado el aspecto hardware, sobre todo la conversión de niveles si la comunicación es entre un PIC y el PC o entre dos microcontroladores con diferentes voltajes (p.e. 3.3V y 5V). En mis pruebas he usado el conversor de niveles integrado en la placa EasyPIC6. En general habrá que usar un conversor de niveles basado en el Max232 o similar. Una búsqueda de RS232 MAX232 TTL en ebay nos mostrará varias posibilidades a partir de unos $5.

En primer lugar presentaremos las funciones básicas que la mayoría de los compiladores tienen para abrir el puerto serie, enviar/recibir, etc. Luego escribiremos nuestras propias rutinas. El objetivo no es prescindir de las rutinas del compilador, sino entender los fundamentos para ser capaces (cuando sea necesario) de saltarnos las limitaciones de un compilador en particular.

También escribiremos una función que nos permita configurar el puerto serie a partir de la velocidad en baudios, sin tener que calcular y programar los registros asociados.

Código asociado a esta entrada:  uart0.c  uart1.c 
------------------------------------------------------------------------------------------------------------

Funciones básicas de manejo de puerto serie:  (uart0.c)

En primer lugar vamos a ver un sencillo código en C que establece una comunicación serie entre el PIC y p.e. un PC usando las rutinas del compilador. Dos compiladores que uso habitualmente son el C18 de Microchip y el MikroC Pro de Mikroelectronica. No importa que estéis usando algún otro compilador porque la idea es profundizar un poco más sobre que hace un compilador al manejar el puerto serie de un PIC.

Las funciones básicas de ambos compiladores  en relación al puerto serie son:

FUNCION
MikroC Pro
C18
Inicialización
UART1_init
Open_USART
Data ?
UART1_Data_ready
DataRdyUSART
Leer
UART1_read           
ReadUSART  / getcUSART
Escribir
UART1_write          
WriteUSART / putcUSART


Usando las funciones anteriores, un sencillo programa en C18 que abre el puerto serie a 9600 bauds, lo monitoriza el puerto serie y rebota lo que recibe sería el siguiente (notad que es necesario incluir usart.h):

#include <usart.h>

void main()
{
 char ch;
  
 TRISB=0; PORTB=0;

 OpenUSART(USART_ASYNCH_MODE & USART_EIGHT_BIT & USART_BRGH_HIGH,129);

 while(1)
   {
    if (DataRdyUSART())
     {
      ch = get_byte(); send_byte(ch+1); send_byte(0x0D); send_byte(0x0A);
      PORTB++;
     }
    Delay10KTCYx(5);
   } 
}


El mismo programa en MikroC Pro:

void main() {
char ch;
TRISB=0; PORTB=0;
  
UART1_Init(9600);
while(1)
  {
   if (UART1_Data_Ready())
    {
     ch = UART1_Read(); UART1_Write(ch+1); UART1_Write(0x0D); UART1_Write(0x0A);
     PORTB++;
    }
   delay_ms(10);
  }   
}


Si conectamos PIC y PC, cualquier cosa que tecleemos en un terminal nos es devuelta como el carácter siguiente en la tabla ASCII, debido a que enviamos (ch+1) donde ch es el carácter recibido. Además se manda un retorno de carro y salto de línea. Según la configuración de nuestro terminal es posible que veamos tanto el carácter que hemos tecleado como el que envía el PIC. Además PORTB se incrementa con cada carácter recibido de forma que podemos ver un feedback visual de los bytes recibidos.

Lo que vamos a hacer ahora es volver a escribir las funciones anteriores pero usando nuestro propio código, lo que implicará manejar los SFR (Special Function Register) del PIC asociados a la UART. El efecto  colateral será que entenderemos como funcionan a "bajo nivel" dichas comunicaciones.

Hay 5 registros que tendremos que conocer y manejar:

TXSTA  -> Status de TX, con el indicamos las opciones de transmisión
RXSTA  -> Status de RX, opciones de recepción.
SPBRG  -> determina la velocidad de las comunicaciones (bauds) junto con el bit BRGH de TXSTA

RCREG  y TXREG -> registros donde se guarda el byte recibido y donde ponemos el byte a transmitir.

La más complicada es la inicialización de la UART. Afecta a los distintos bits de TXTSTA, RXSTA y SPBRG.

void setup_UART(uint8 brgh,uint8 spbrg)
 {
  TRISCbits.TRISC6=0; TRISCbits.TRISC7=1;  // RC6 out (tx), RC7 in (rx)
 
  TXSTA =  0b00100110;
/*TXSTA = 0x00;  // Clear TX status
  TXSTA.SYNC=0;  // Async mode
  TXSTA.TXEN=1;  // Enable TX
  TXSTA.TRMT=1;  // TRS empty  (we start with an empty TX register)*/

  RCSTA = 0b10010000;
/*RCSTA =0x00;  // Clear RC status
  RCSTA.SPEN=1; // Enable Serial Port
  RCSTA.CREN=1; // Enable Receiver*/

 // BRGH=0 -> Baud Rate = Fosc / (64 * (SPBRG+1))
 // BRGH=1 -> Baud Rate = Fosc / (16 * (SPBRG+1))
  TXSTAbits.BRGH=brgh; SPBRG = spbrg; 
 }

La rutina setup_UART inicializa la UART para una comunicación asíncrona con 8 bits. La velocidad (bauds) se calcula en función de los parámetros BRGH y SPBRG, junto con la frecuencia del oscilador Fosc:

BRGH=0  ->  baud = F_osc / (64 x(SPBRG+1))  
BRGH=1  ->  baud = F_osc / (16x(SPBRG+1))

Con un oscilador de 20 MHz, para una velocidad de 9600 baudios usaríamos BRGH=1 y SPBRG=129, obteniendo

  baud = 20000000/(16 x 130) = 9615

suficientemente próxima al valor correcto.

En la rutina también se determina la dirección (out) del pin TX y la del pin RX (in). En este ejemplo (familia PIC18F2520/4520) son RC6 y RC7 respectivamente. No estoy seguro de que esto sea necesario (tal vez la dirección de RC6/RC7 se establezca automáticamente al habilitar el receptor y el emisor) pero por si acaso...

Enviar un byte es una simple cuestión de esperar a que el buffer de salida esté vacío (consultando flag TXSTA.TRMT) y cuando lo esté, poner el carácter a transmitir en el buffer de TX. Automáticamente el bit TRMT se pondrá a 0 (buffer lleno), por lo que una siguiente llamada a la  función tendría que esperar a terminar de mandar el carácter.

void send_byte(unsigned char ch)
{
 while (TXSTAbits.TRMT==0);   // wait until TX buffer is empty
 TXREG=ch;
}

Para ver si se ha recibido algo se consulta la bandera de la interrupción de recepción del puerto serie (definida como RX_flag como ya vimos):

char rx_ready(void) {  return RX_flag; }

Finalmente, si se ha detectado (rx_ready) una recepción, una llamada a get_byte recupera el byte recibido (que está esperándonos en RCREG). También se quita la bandera RX_flag para indicar que estamos de nuevo a la espera.

char get_byte(void)
{
 RX_flag=0; // Clear RX flag
 return RCREG;  // Devuelve byte recibido
}

El siguiente código usa estas funciones para hacer exactamente lo mismo que antes:

void main()
{
 char ch;
  
  TRISB=0; PORTB=0;

  setup_UART(1,129);

  while(1)
    {
     if (rx_ready())
      {
       ch = get_byte(); send_byte(ch+1); send_byte(0x0D); send_byte(0x0A);
       PORTB++;
      }
     Delay10KTCYx(5);
    }
}

¿Hemos ganado algo?  Obviamente nada si sólo queríamos hacer lo que hacíamos en el programa inicial. Para ello nos bastaban las rutinas que ya existían en nuestro compilador. Las ventajas aparecen cuando necesitamos apurar un poco más: 

  1. Las rutinas standard en muchos compiladores solo soportan el modo de comunicación más usual (8 bits sin paridad). Si por ejemplo queremos usar paridad no nos sirven. Sin embargo ahora que sabemos lo fácil que es enviar o recibir bytes será muy sencillo cambiar algunos bits en TXSTA y/o RXSTA para establecer una comunicación usando p.e. paridad o 9 bits.
  2. Las rutinas standard (y las que hemos escrito, ya que eran intercambiables) son bloqueantes. Antes de escribir debemos esperar a que el buffer de transmision (1 byte) se libere. Antes de recibir debemos estar continuamente preguntando (rx_ready) si se ha recibido algo.

Imaginad que el PIC está conectado a un sensor que genera unos 1000 bytes/segundo que deben ser enviados al PC). Cada byte supone como mínimo 10 bits (start + 8 bits + stop) , por lo que para poder enviar 1000 bytes/sec precisaremos un enlace de aproximadamentelo unos 1000 x 10 (podría valernos 9600). Si en estas circunstancias usamos las rutinas originales, todos los recursos del PIC estarían prácticamente dedicados a la transmisión de los datos.

Esta situación puede implementarse mucho más eficientemente usando interrupciones y esperando que la UART nos avise de que ha recibido un dato o que está libre para enviar otro. El resto del tiempo el PIC puede estar haciendo otras cosas. La diferencia puede ser grande. Si esperamos a que se complete la TX, enviar un byte puede costarnos del orden de 1 ms ( @ 9600 bauds). Usando interrupciones el único trabajo del microcontrolador será poner un byte en TXREG, lo que esencialmente nos llevará una instrucción. Transmitir dicho byte seguirá llevando 1 ms, pero el que estará ocupado durante ese tiempo será el periférico (UART).

Habiendo aprendido como funciona por debajo la UART será muy sencillo implementar tanto la recepción como la transmisión usando interrupciones.

Determinación de SPBRG y BRGH en función de la velocidad en baudios

Vimos que aunque la función de MikroC Pro era menos versátil en la configuración del puerto serie tiene la ventaja de que bastaba con darle la velocidad y ella se encargaba de determinar los parámetros SPBRG y el bit BRGH. Esto es posible porque en un proyecto de MikroC Pro se especifica la velocidad del oscilador. Por la misma razón en MikroC Pro podemos especificar delays en unidades  absolutas (msec o usec) en vez de limitarnos a ciclos como en C18.

Es muy sencillo escribir una función que calcule los valores correctos de SPBRG y BRGH y nos lo devuelva para usarlos en una llamada a nuestra rutina setup_UART o a la OpenUSART de C18:

uint8 get_usart_speed(uint32 baud, uint32 Fosc, uint8* spbrg)
// baud rate 1200,2400,4800,9600,19200, ..., 115200
// Fosc = Frequency osc in KHz -> 20MHz = 20000 
// Returns 0 (USART_BRGH_LOW), 1 (USART_BRGH_HIGH), or 2 (cannot get speed requested)
// It places in spbrg the value to be used as argument spbrg in OpenUSART
 {
  uint16 FF;
  uint8 brgh;
  uint8 shift;

  Fosc=Fosc*1000; 
  baud>>=1; Fosc=Fosc/baud+1; Fosc>>=1;  // computes round(Fosc/baud)

  FF=(uint16)Fosc;
 
  if ((FF>16320)||(FF<16)) return 2;   // Baud rate too low or too high
 
  brgh = (FF>4096)? 0:1;  // Decides between BRGH=0 o BRGH=1
  shift= (brgh==1)? 3:5; 
  FF>>=shift; FF--; FF>>=1; // Computes round(FF/64 -1) or round(FF/16-1)

  *spbrg = (uint8)FF;
  return brgh;
 }

La forma de usar esta rutina para inicializar la UART en conjunción con setup_UART() sería:

void main()
{
 uint8 brgh,sp; 

 brgh=get_usart_speed(38400,20000,&sp);
 setup_UART(brgh,sp); 
}

Si preferimos seguir usando la rutina de C18 (OpenUSART) para inicializar la USART:

void main()
{
 uint8 brgh,brgh_config,sp; 

 brgh=get_usart_speed(38400,20000,&sp);
 brgh_config=(brgh)? USART_BRGH_HIGH:USART_BRGH_LOW;
 OpenUSART(USART_ASYNCH_MODE & USART_EIGHT_BIT & brgh_config, sp); 
}

Si solo vamos a inicializar el puerto es obviamente más eficiente calcular nosotros los valores a usar y programarlos directamente. De esta forma nos ahorraríamos los 30-40 palabras de memoria de programa que ocupa la rutina anterior. Este enfoque es más útil si queremos dinámicamente cambiar la velocidad del puerto serie durante el programa.








24 comentarios:

  1. Este comentario ha sido eliminado por un administrador del blog.

    ResponderEliminar
  2. Saludos. Te tengo una pregunta que para mi es un poco desconcertante. Si estoy usando un PIC 18f45k22 con dos módulos de comunicación serial, ¿Cómo hago para comunicarme?

    El PIC deberá leer un protocolo de comunicación a partir de la PC, procesarlo y enviar una acción respectiva a una tarjeta SPARTAN3. Mi duda va relacionada a como dirigir la escritura y/o lectura de cada módulo de comunicación. Si pudieras contestarme estaría eternamente agradecido.

    ResponderEliminar
    Respuestas
    1. No debería ser un problema, aunque la forma concreta depende del compilador usado. Por ejemplo si usas C18 verás que para los PIC con múltiples USARTS tienes las funciones:

      Read1USART, Read2USART, Write1USART, Write2USART, etc.

      Basta usar una u otra para leer de un puerto o del otro.

      Si vas a usar los registros directamente como hacemos en esta entrada consulta el datasheet del PIC a usar. Veras que en vez de un registro TXREG tendrás TXREG1 y TXREG2. Usaras uno u otro dependiendo de que puerto quieras usar. Lo mismo sucede con los registros de status TXSTA y RXSTA que se encuentran duplicados.

      Antonio.

      Eliminar
    2. En la misma tarjeta Spartan del FPGA, puedes tu generar tantos modulos UART como tu quieras sin necesidad de utilizar otro dispositivo como el PIC. Esa es precisamente una de las virtudes de la lógica programable, no te compliques es sencillo implementar tantos RX's como TX's requiereas, configurando los parámetros de bauds que tu requieras, incluso por ejemplo si manejas baudajes diferentes, en uno 9600 y en el otro 115200.

      Eliminar
  3. Hola, disculpa estoy intentando controlar un pic a través de otro pic(ambos 16f886) por infrarrojos, con ayuda del pwm el pic emisor envía caracteres por el UART que determinan acciones para el receptor, el receptor atiende los datos con una interrupción y los recibe bien. Pero mi problema es el emisor ya que trato de hacerlo funcionar como un control remoto cuando hay un 1 en ciertos pines este debería enviar un carácter pero algo ocurre y manda de mas, algunos no funcionan y los que funcionan luego como que traban el pic emisor. Lo e intentado con las librerías buton, con revisar los pines en un while infinito de tal manera que no se tomen como dobles 1 pero sigue sin funcionar adecuadamente. ¿Podrías aconsejarme algo?

    ResponderEliminar
    Respuestas
    1. Yo en tu lugar intentaría depurar los subsistemas por separado.

      Por lo que entiendo en el emisor detectas 1's en ciertos pines (con pulsadores p.e.) y dependiendo del pin puesto a 1, haces una acción.

      Por lo que cuentas, no está claro que esa parte esté funcionando correctamente. Yo me aseguraría de que la detección de la entradas es a prueba de fallos antes de preocuparme de la parte de comunicaciones.

      Al mismo tiempo podrías poner a prueba el emisor haciendo que mande una secuencia de ordenes, pero sin usar los "botones", simplemente programando el código.

      Sólo si los dos subsistemas están OK pasaría a intentar depurar su "interacción".

      Un saludo,

      Antonio

      Eliminar
  4. Hola.
    Pregunta:
    Tengo una tarjeta que he comprado en el que la única forma de comunicación es via seria.
    Requiero comunicarla con un PIC16F84A (no hay discusión en cuanto al cambio), es necesario poner un MAX232 como interfase o lo puedo hacer directo.

    Gracias!

    ResponderEliminar
    Respuestas
    1. Dependerá de las especificaciones de la tarjeta.

      El protocolo serie es una descripcición "digital" (0/1) de lo que debe estar pasando en la línea (por ejemplo, 1 mientras se espera, 0 indica el bit de start, etc.), pero no especifica que voltajes representan el 0/1 lógicos.

      Por ejemplo en los PIC se usa la convención (lógica) que el 0 lógico son 0V y el 1 lógico (línea alta) son 5V.

      En el puerto serie de un PC (standard RS232) la relación lógica/voltajes está invertida. Un 0 lógico se representa con un voltaje entre 3 y 15V (12 es típico) y un 1 lógico por un voltaje negativo (entre -3 y -15V). Es por esto por lo que para comunicar PIC-PC necesitamos un Max232 o equivalente.

      Las especificaciones de tu tarjeta te dirán cuales son los voltajes asociados a su puerto serie. Si dice algo de RS232 tendrás que usar seguramente un buffer tipo MAX232.

      Si dice algo asi como lógica TTL podrás conectarla la PIC directamente.

      Podrías tener otras posibilidades. Tu tarjeta podría ir con 3.3V y entonces necesitarías un conversor de niveles (5V <-> 3.3V), aunque en ese caso no tengas "inversión" de la lógica.

      Un saludo, Antonio

      Eliminar
  5. Que tal Antonio, eh checado tu blog, me parece muy interesante y me ha servido de mucho, ya entrando en materia estuve checando este post y queria preguntarte a cerca de la variable RX_flag, al momento de compilar me marca un error ya que dice que no esta definida, yo quiero pensar que no es un registro si no una variable, a menos que me puedas decir que es verdaderamente, esa es mi duda, quiero decirte que estoy usando SDCC, pero no debe de haber ningún problema ya estoy utilizando los registros, salu2.

    ResponderEliminar
    Respuestas
    1. RX_flag es simplemente un define que yo hago de la bandera de la interrupción de recepción en el puerto serie. Si miras el código de uart0.c/uart1.c verás que al principio hay un #include:

      #include "..\int_defs_C18.h"

      En ese fichero hay un monton de definiciones para no tener que acordarme de donde están las diferentes flags de las interrupciones, bits de enable, etc. Ese fichero puedes encontrarlo en la entrada dedicada a las interrupciones:

      http://picfernalia.blogspot.com.es/2012/06/interrupciones-conceptos-basicos.html

      En particular contiene las líneas:

      #define RX_flag PIR1bits.RCIF
      #define enable_RX_int PIE1bits.RCIE=1
      #define disable_RX_int PIE1bits.RCIE=0
      #define set_RX_high IPR1bits.RCIP=1
      #define set_RX_low IPR1bits.RCIP=0

      donde se definen diversos bits relacionados con la interrupción RX de la UART.

      UN saludo,

      Antonio

      Eliminar
  6. Disculpa diego orozco me gustaría saber si pudiste lograr la comunicación por los dos puertos y como lo realizaste

    ResponderEliminar
  7. tengo el mismo problema quiero comunicarme con un solo puerto físico con pic18f452 y no lo e logrado e realizado con conmutación del puerto con hardware y no lo e logrado si alguien me pudiera ayudar se los agradecería mucho

    ResponderEliminar
  8. Muchas gracias, en serio, he querido hacerlo hace tiempo pero no tenia idea, y esto me brinda una luz, gracias.

    ResponderEliminar
  9. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  10. buen aporte
    Por si os interesa maneras de emplear uart mikroc.
    controlxic.blogspot.com/‎

    ResponderEliminar
  11. Hola, estoy tratando de implementar una comunicación básica por medio del USART (PIC 16F628A, sin cristal oscilador, a 9600 bauds) y bluetooth (HC-05), para prender 4 leds, es decir elegir cual prender mediante el hyperterminal, tengo mi programa y lo hace, muy lento, pero lo hace, además que en el hyperterminal me muestra una serie de signos raros, sin sentido, y en algunas ocasiones algunos trozos de los mensajes de mi programa, Mi pregunta es, ¿Será que esto pasa porque el pic no esta usando un oscilador externo? ¿O algo más que yo no este viendo? Alguna ayuda será beinvenida
    Saludos

    ResponderEliminar
    Respuestas
    1. También lo vi alguna vez. Luego de eso, concluí que USART es muy sensible a la frecuencia, así que el oscilador interno no es una opción. Usando un cristal se corrige el problema.

      Eliminar
  12. Hats off ;)
    muchas gracias, con tus explicaciones he conseguido hacer funcionar la USART!!! ya no tendré que hacer troubleshooting encendiendo y apagando leds haha
    Salutacions!!

    ResponderEliminar
  13. Saludos Antonio. He estado haciendo pruebas con un PIC18F4685 y la asignación de TRIS
    TRISCbits.TRISC6=0; TRISCbits.TRISC7=1; // RC6 out (tx), RC7 in (rx)
    antes de inicializar la UART no sólo no es necesario según el datasheet sino que además es contraproducente. Me explico. Yo tengo los pin RC6/RC7 al aire y cuando quiero abrir una consola conecto mi adaptador USB/USART TTL para monitorizarlo (no lo dejo como parte intrínseca del circuito para ahorrar espacio y costes, ya que su uso es ocasional, para mi uso personal como debugger). Pues bien, el asignar los tris sin tener el adaptador USB/USART conectado provoca que el sistema se vuelva inestable (a no se que se deje un bucle hecho), cosa que no ocurre eliminando estas lineas de código. Saludos!

    ResponderEliminar
  14. hola antonio disculpa estoy tratando de hacer un programa en el cual envie una letra a atraves y esta me de una salida en un lcd que me diga señal activada y si envio b señal desactivada y c como sistema apagado.

    ResponderEliminar
  15. Hola

    Estaba mirando el codigo pero hace cosas raras cuando se usa el modo de 16-bit, porque ese codigo es para 8-bit. Mayormente selecciona un spbrg pero que no tiene porque ser el de menor error, y a veces es demasiado error.

    La clave esta aqui:

    if ((FF>16320)||(FF<16)) return 2; // Baud rate too low or too high

    brgh = (FF>4096)? 0:1; // Decides between BRGH=0 o BRGH=1

    Pero no entiendo el porque se han elegido estos valores. ¿Podrias arrojar algo de luz sobre el porque de la eleccion de estos valores (16320 y 4096)?

    Gracias de antemano.

    ResponderEliminar
  16. saludos, estoy usando un pic18f45k22 y deseo resibir una secuencia ce caracteres por
    compilador microbasic for pic
    ejemplo

    envio: casa y la quiero poner en la variable comida



    dentro del programa osea el pic

    if comida = (un valor) them
    .........
    end if
    como puedo recibir los datos y luego optener los resultados

    gracas

    ResponderEliminar
  17. Hola.
    ¿Es posible enviar por el puerto serie palabras de más de 8 bits, por ejemplo palabras de 32 bits?
    31 bits de datos + 1 bit de paridad
    Gracias

    ResponderEliminar
    Respuestas
    1. Se puede pero solo si implementas el protocolo por software. Mediante hardware embebido, los PICs solo permiten 8 bits.

      Eliminar