Tutoriales

PIC - Puerto serial por USB CDC

led matrix usb cdcEn este tutorial podrás aprender a enumerar un microcontrolador PIC (PIC18F2550) como un puerto serial USB a través de la clase CDC (Communication Device Class). Esto te permitirá montar un puerto COM virtual en tu computadora y hacer programas muy sencillos que se comuniquen con el PIC a través del COM. Adicionalmente podrás ver código para aprender a usar una Matriz de LEDs y podrás tener un reloj de tiempo real en ella, entre otras cosas. 

 

Items necesarios

+ PIC18F2550 y demás componentes que verás en el esquemático
+ o puedes comprar una de mis tarjetas LED Matrix (checa disponiblidad)
+ Código fuente del PIC en Git (link)
+ Código fuente de la aplicación en C# en Git (link)
+ Microchip MPLAB X IDE v2.15 o mejor (link
+ Compilador XC8 de Microchip para PICs de 8 bits v1.32 o mejor (link
+ Microchip Libraries for Applications 
v2013_12_20 o mejor (incluye el USB Framework gratuito, link)
+ Visual C# 2008 Express Edition
+ Windows 7 en adelante (sólo si se desea el efecto Aero en la aplicación)
+ Esquemáticos (link

 

 

Esquemático 

Para lograr una conexión básica de un puerto serial USB CDC con tu computadora, debes tener listo el siguiente esquemático. No te preocupes por las etiquetas que puse en algunas terminales del PIC, sólo conecta lo mostrado en el esquemático. Te sugiero que siempre uses un fusible en el puerto USB para proteger tu computadora ante cualquier accidente.

Es importante colocar un capacitor electrolítico en VUSB (14) ya que éste se encarga de filtrar la fuente interna del PIC que permite que el periférico USB funcione correctamente.

 

Código fuente PIC - Configurando el modo CDC USB

El código fuente CDC USB se tomó de las librerías para aplicaciones de Microchip. El ejemplo usado es el que puedes encontrar en:

C:\microchip\mla\v2013_12_20\apps\usb\device\cdc_basic

Y nosotros lo modificaremos para que funcione a nuestro gusto.

Asegúrate de copiar el contenido del directorio siguiente en el directorio de tu proyecto. Este te servirá para poder compilar el proyecto posteriormente sin tener instaladas las librerías de Microchip.

C:\microchip\mla\v2013_12_20\framework\usb 

Dentro del subdirectorio src que recién copiaste, elimina todos los archivos salvo los dos siguientes. Ellos definen las funciones CDC y otros detalles necesarios para enumerar al PIC y que funcione todo el periférico USB correctamente.

usb_device.c
usb_device_cdc.c

Asegúrate de tener vinculados esos archivos C en tu árbol de proyecto, junto con cualquier otro H que necesites.

cdc project tree

En el archivo usb_descriptors.c se define el comportamiento de tu PIC ante el host USB. El único parámetro en el que debes estar interesado en modificar es el consumo de corriente de tu dispositivo USB. En mi caso dejé el circuito completamente alimentado por USB y pedí al host USB que después de la enumeración proporcione 400mA de corriente:

200,                     // Max power consumption (2X mA)

Pero si tú piensas alimentar tu circuito por tu cuenta, remueve el fusible F1 y coloca lo siguiente en el descriptor:

0,                      // Max power consumption (2X mA)

Toda la demás información se encarga de reportar el dispositivo como CDC y no es necesario modificarla, salvo el VID y PID, pero para efectos de este tutorial no hacen falta tampoco.

El archivo usb_config.h define los tamaños de los endpoints que usará el dispositivo para enviar y recibir datos del host. Asegúrate de tener los siguientes datos correctamente capturados.

#define CDC_DATA_OUT_EP_SIZE    64
#define CDC_DATA_IN_EP_SIZE     64 

Un punto importante a notar es que con las últimas librerías de Microchip, las direcciones RAM de los endpoints del PIC18F2550 deben ser configuradas manualmente. Asegúrate de capturar lo siguiente en el archivo system.h.

/**
 * The USB endpoint has to be used to store the CDC buffers
 */
#define FIXED_ADDRESS_MEMORY
#define IN_DATA_BUFFER_ADDRESS_TAG      @0x500    // for CDC device (XC8)
#define OUT_DATA_BUFFER_ADDRESS_TAG     @0x540
#define CONTROL_BUFFER_ADDRESS_TAG      @0x580 

En el archivo system.h es donde ahora Microchip sugiere que se declaren los periféricos de nuestro PIC. Ahí encontrarás todas mis definiciones de pinout del micro así como los prototipos de unas funciones que Microchip agregó en su librería.


Código fuente PIC - Inicialización en main.c

En main.c encontrarás la inicialización del sistema y sus periféricos, incluido el USB. La función SYSTEM_Initialize(SYSTEM_STATE_USB_START) es meramente otro agregado de Microchip a sus librerías para manejar los diversos estados USB en los que el dispositivo se puede encontrar. Puedes usar su switch case por si esos estados te sirven en tu aplicación específica.

La función UserInit() es en donde nosotros inicializaremos nuestro sistema en específico. En mi caso inicializo el TRIS de todo el pinout y echo a andar el Timer 2 (útil para llevar el tiempo real y el redibujado de la matriz).

Una vez inicializado tu hardware, deberás inicializar el periférico USB a través de:

    USBDeviceInit();
    USBDeviceAttach(); 

Esto sólo debe hacerse una vez en toda la ejecución del micro.

Para el caso de la Matriz de Leds, el Timer 2 se configuró para levantar el bit PIR1bits.TMR2IF cada 1ms, basado en el reloj del sistema de 12MIPS.

static void Initialize_Timer_2(void)
{
    PIR1bits.TMR2IF = 0;
    PIE1bits.TMR2IE = 0;
    IPR1bits.TMR2IP = 0;
 
    TMR2 = 0;
    PR2 = TIMER_2_PR2;
 
    T2CON = 0x17; /* Pre 1:3, post 1:16 */
} 

El oscilador es levantado desde 4MHz del cristal externo (buenos así ya que generan menos ruido) a 48MHz internos que son equivalentes a 12MIPS en el PIC.

 

La configuración de los relojes se hace a través de directivas #pragma, incluidas en el archivo system.c.

 

Código fuente PIC - Atrapando mensajes USB CDC

Una vez que el PIC ya fue configurado, el periférico USB empezará a funcionar por su cuenta, si es que no definiste la macro USB_POLLING en ningún momento. Esto es lo más recomendable ya que los eventos USB se atienden casi de inmediato y no tendrás el riesgo de desenumerar tu PIC por estar haciendo otras cosas.

Se crea un ciclo while(1) en el que el micro estará procesando tareas USB y propias de la aplicación. En mi caso de la matriz de LEDs, verifico si el timer 2 ya generó un tick de 1ms. Si es el caso, llamo a la función Process_Timer_2_Tick que se encarga de actualizar el reloj de tiempo real.

Después llamo a la función que siempre debe correrse para redibujar la matriz de LEDs. Esto lo veremos posteriormente.

Por último, se verifica que el dispositivo USB sí esté enumerado:

        if( USBGetDeviceState() < CONFIGURED_STATE )
        {
            /* Jump back to the top of the while loop. */
            continue;
        } 

Si no lo estuviera, vuelve a comenzar la ejecución del ciclo while. Si sí está enumerado, entonces llamamos la función getsUSBUSART(), que se encarga de leer datos del puerto USB en un buffer interno.

count += getsUSBUSART(&ReceivedDataBuffer[count], CDC_DATA_OUT_EP_SIZE - count);

La función pide 2 parámetros:

+ Una dirección en donde deberá empezar a depositar bytes leídos desde USB
+ y un número de bytes a leer desde lo que haya llegado

Devuelve el número de bytes que realmente leyó.

En mi caso, la forcé a siempre leer 64 bytes y que los vaya leyendo poco a poco. Cuando ya juntó 64 bytes, entonces es cuando proceso lo recibido por USB y ejecuto una acción. Esto es útil para que el host USB siempre mande comandos de 64 bytes y nunca se pierda el orden de recepción, tú puedes implementar tu propio protocolo:

        //Check if we have received CDC_DATA_OUT_EP_SIZE bytes
        if(CDC_DATA_OUT_EP_SIZE == count)
        {
           count = 0;
 
            /* Service the command */
            switch((PIC_Command_T)ReceivedDataBuffer[0])
            {
                case DRAW_RAW_FRAME:
                    Current_Command = DRAW_RAW_FRAME;
                    Proc_F = Process_DRAW_RAW_FRAME;
                    break;
 
                case SET_RTC:
                    Proc_F = Process_SET_RTC;
                    break;
 
                case DRAW_A_NUMBER:
                    Current_Command = DRAW_A_NUMBER;
                    Proc_F = Process_DRAW_A_NUMBER;
                    break;
 
                case DRAW_TIME:
                    Current_Command = DRAW_TIME;
                    Proc_F = Process_DRAW_TIME;
                    break;
 
                case SET_ROTATION:
                    Proc_F = Process_SET_ROTATION;
                    break;
 
                case GAME_OF_LIFE:
                    Current_Command = GAME_OF_LIFE;
                    Proc_F = Process_GAME_OF_LIFE;
                    break;
 
                default:
                    Proc_F = Process_UNUSED;
                    break;
            }
            (*Proc_F)(ReceivedDataBuffer, Column_Data);
 
//            if(USBUSARTIsTxTrfReady() == true)
//            {
//               memcpy(ToSendDataBuffer, ReceivedDataBuffer, CDC_DATA_OUT_EP_SIZE);
//               putUSBUSART(ToSendDataBuffer, CDC_DATA_OUT_EP_SIZE);
//            }
 
            CDCTxService(); 

Una vez ejecutada la acción, se puede enviar algo por la UART mediante putUSBUSART como se ve en el código que comenté. En mi aplicación no me ha hecho falta y lo dejé por si luego lo necesito. Al final, no olvides llamar CDCTxService().

Cuando proceso el comando recibido lo hago a través de un apuntador a función. No es complicado, primero defino su tipo...

typedef void (*Process_Function_T)(uint8_t* command, uint8_t* column_data);

Y luego creo un apuntador con ese tipo, el cuál se encargará de llamar la función deseada:

Process_Function_T Proc_F; 

En el caso en el que el host pida poner la hora del reloj de tiempo real:

                case SET_RTC:
                    Proc_F = Process_SET_RTC;

                    break; 

Me encargo de hacer que Proc_F apunte a mi función Process_SET_RTC y después de que apunta a ella, pues que la ejecute, mediante una derreferenciación:

(*Proc_F)(ReceivedDataBuffer, Column_Data); 

Si gustas evitar el uso de apuntadores a funciones, llama directamente la función deseada, esto lo hice por costumbre.

 


Matriz de LEDs

¿Cómo funciona la matriz de LEDs? Utiliza el método de persistencia de visión, o en inglés Persistance of Vision (POV).

 

Básicamente lo que hago es lo siguiente:

- Tengo 7 filas y 7 columnas, dando un total de 49 LEDs independientes.
- Cada fila y cada columna son controladas por un pin específico del PIC.
- Si yo quisiera prender todos los LEDs de la fila 0, entonces debo activar las 7 columnas y únicamente activar el MOSFET de la fila 0.
- IMPORTANTE: Jamás debe activarse más de un MOSFET a la vez ya que el efecto visual se perdería.
- Siguiendo esta lógica, no se deben prender más de 1 fila a la vez, pero ¿entonces cómo se logra dibujar 49 LEDs de forma independiente?

Nunca se logra.

Lo que se hace es:

- Activar los LEDs (columnas) de una fila en un instante
- Activar el MOSFET de esa fila
- Dejarlos activos por un brevísimo tiempo
- Apagar los LEDs (columnas)
- Apagar el MOSFET (fila)
- Y llevar a cabo la misma secuencia para la siguiente fila

Esto parece que generaría un efecto interrumpido de lo dibujado en la matriz, pero no lo es. El ojo humano no es capaz de distinguir cuando una fila está activa y las otras seis están apagadas, si la frecuencia de muestreo es suficientemente rápida.

El algoritmo de dibujado de la Matriz lo puedes encontrar en matrix.c. Te lo dejo de tarea. Igualmente puedes encontrar un algoritmo de rotación de matriz que implementé, que básicamente rota la matriz en 0°, 90°, 180° y 270°. Esto es útil para que ubiques tu tarjeta en la posición que mejor te guste. 

 

Código fuente PIC - Otras funciones

+ Dibuja un cuadro en bruto (DRAW_RAW_FRAME)

Con este comando puedes enviar datos en bits a apagar y prender desde la compu. Puedes dibujar lo que quieras. Lo he llegado a usar para hacer un analizador de frecuencias desde la compu, pero aún no está completamente terminado.



+ Establecer la hora en el reloj de tiempo real (SET_RTC)

Con este comando le pones la hora al PIC para usarlo como reloj de pared, es muy preciso Chulo.

+ Dibujar un número (DRAW_A_NUMBER)

Dibuja un número del 0 al 9 para probar la comunicación del PIC.

+ Dibuja la hora (DRAW_TIME)

Es el modo predeterminado del PIC. Básicamente por 2 segundos dibuja la hora

4:

y por otros 2 segundos dibuja los minutos

35

+ Establece la rotación (SET_ROTATION)

Define cómo dibujar los datos de la matriz: 0°, 90°, 180° y 270°.

+ Juego de la vida (GAME_OF_LIFE)

Es una implementación pobre que hice del juego de la vida, ya no me dio tiempo de acabarla. Si te interesa pulirla, no dudes en hacerlo. Sube tu código a GIT en cuanto lo logres y házmelo saber por favor.

 

Conectando por primera vez el PIC a la PC

Si estás en Windows, la compu te pedirá los drivers de instalación. Basta con indicar la ruta donde los siguientes archivos se encuentran guardados. Los puedes encontrar en el código fuente en GIT mencionado al principio del tutorial.

mchpcdc.cat
mchpcdc.inf

 


Código fuente C# - Controlando la LED Matrix desde Windows

Una vez listo el firmware y la PCB pues hay que controlarlos desde Windows. Hice una aplicación bastante trivial en C# 2008 Express Edition (.NET 2.0).

 

La aplicación básicamente hace uso de un puerto COM virtual pero no se preocupa por si es USB o si es real. 

- Los comandos se envían y reciben a través del objeto serialPort1.

- La matriz de LEDs con "rayitos" se hizo a través de un objeto TableLayoutPanel, albergando éste objetos tipo Label.

- El "rayito" se dibuja con la fuente Webdings.

- El texto del "rayito" es el siguiente:

String Set_LED = "~"; 

- La aplicación hace uso de Aero gracias a la reimplementación de la clase Forms con GlassForm:

    public partial class Main_Form : GlassForm{} 

- Basta con cambiar cómo se crea la forma actual, NO usar Main_Form : Form

- Para importar el modo Aero en C# basta con agregar la referencia Microsoft.WindowsAPICodePack.Shell.

- Agrega la DLL (Microsoft.WindowsAPICodePack.Shell.dll) en el menú Proyecto, Agregar Referencia..., Examinar.

Considero que el código fuente en C# es sencillo de comprender. Existen sólo un par de implementaciones raras como el Dictionary, pero son sencillos igualmente. Un diccionario recibe un tipo y te devuelve otro, es como un mapa en C++.

Si aún así tienes dudas, por favor contáctame.

Por favor avísame si falta algo en el tutorial o si hay algo que corregir.

Puedes modificar el código fuente y mejorarlo. Si lo haces, por favor súbelo a GIT en los links indicados y házmelo saber. 

Gracias. Riendo