r40 - 16 Jun 2006 - 10:32:58 - PabloHoffmanYou are here: pablohoffman.com >  Oscusb Web  >  OscusbDocumentacion > DocCap05Firmware

Capítulo 5. Firmware

Introducción

Este capítulo habla sobre los temas relacionados con la implementación del firmware, es decir, el programa que corre en el microprocesador y controla el funcionamiento de la placa. Incluye información sobre la estructura del código y detalles particulares de la implementación.

Herramientas de trabajo

El firmware es el programa que corre internamente en el PIC y sirve para controlar el osciloscopio. Fue escrito enteramente en C utilizando el Microchip MPLAB C18, un compilador de C provisto por el mismo fabricante del PIC que soporta el estándar de C ANSI '89 y que viene pensado para trabajar de forma conjunta con el MPLAB IDE, que es el entorno de desarrollo de Microchip. A través del mismo MPLAB IDE es se realiza la programación, simulación, y depuración paso a paso (por hardware) del PIC.

oscusb-mplabide.jpg
Fig 5.1 Captura de pantalla del MPLAB IDE

Una característica destacable del MPLAB C18 es la posibilidad de generar binarios optimizados (tanto en espacio, como cantidad de instrucciones) para PICs de la familia PIC18F (por ejemplo, nuestro PIC18F4550) utilizando las instrucciones extendidas provistas por dicha arquitectura.

El MPLAB C18 está disponible para bajar gratuitamente de la página de Microchip (ver link en referencias). Sin embargo, la versión gratuita (llamada versión estudiantil) tiene una duración de 60 días. A partir de esos 60 días, el programa seguirá funcionando pero sin las optimizaciones antes mencionadas, por lo cual el compilador generará binarios que seguirán funcionando pero ocuparán más espacio (al no estar optimizados) y utilizarán más instrucciones para realizar el mismo trabajo.

Clase de dispositivo

El estándar USB contempla varias clases de dispositivos para funcionalidades encontradas comúnmente en los dispositivos. Por ejemplo, existe un clase para las cámaras digitales, otra para los escáners, otra para las impresoras, etc. Las clases de dispositivos fueron inventadas para mejorar la interoperabilidad de los dispositivos. Así, cualquier sistema operativo que tenga un driver para trabajar con cámaras digitales puede leer fotos de la cámara digital que esté diseñada para cumplir las especificaciones de dicha clase de dispositivos. Por más información, ver el capítulo sobre USB.

En particular, para nuestro osciloscopio optamos por usar la clase de dispositivo CDC (Communication Device Class) que básicamente emula una conexión serie sobre el puerto USB. La razón por la cual optamos esta clase fue que el mecanismo de una conexión serie nos pareció un enfoque simple y efectivo para intercambiar simultáneamente información de control y datos. Además, al no haber ninguna clase prevista para un osciloscopio USB, una comunicación serie es el método más directo de implementar un driver propio puesto que solo basta con enviar y recibir cadenas de caracteres. En conclusión, escogimos la clase CDC por su sencillez y flexibilidad.

Firmware CDC

La comunicación USB se realiza mediante la ayuda del firmware CDC, un framework que brinda Microchip para poder establecer una comunicación (a través del puerto USB) de forma simplificada. El firmware CDC encapsula varias funciones ocultando toda la complejidad necesaria para la comunicación USB de forma de proveer una comunicación serie tradicional entre el PIC y el PC. También se encarga automáticamente de establecer la conexión con el host controlador USB y negociar la comunicación, el consumo de potencia, etc.

Existen diferentes firmwares previstos para otras funcionalidades, como por ejemplo el de almacenamiento masivo (Mass Storage) para hacer un lector de tarjetas o el de dispositivos de interacción humana (Human Interface Device o HID) pensado para hacer un mouse o similar. Inprovee también un firmware más abierto para realizar una comunicación más avanzada.

Algunas de las características del firmware CDC son las siguientes:

  • Tasa de transferencia máxima de 80 kbytes/s
  • Las librerías compiladas ocupan un tamaño relativamente chico (4 Kb)
  • Resuelve toda la comunicación en software (no requiere de ningún hardware extra especial)
  • El flujo de datos es manejado enteramente por el protocolo USB (no es necesario usar control de flujo por software ni hardware)
  • Negocia la potencia a usar por el dispositivo USB, conectándose primero a baja potencia (50 mA) y luego solicitando más, como lo exige el estándar USB

Por más información, ver el Capítulo 3. Bus serie universal (USB) .

Captura de datos

Modos de captura

Debido a la naturaleza de los diversos integrados que componen el osciloscopio fue necesario incorporar en el firmware tres modos de funcionamiento diferentes para poder cubrir todo el rango de frecuencias en la etapa de adquisición.

En el siguiente diagrama puede observarse un bosquejo del funcionamiento de cada uno de estos modos, y a continuación se explican detalladamente estos modos de funcionamiento junto con las limitantes para cada caso.

oscusb-diag-acqu.gif
Fig 5.2 Diagrama de funcionamiento del firmware (modos de captura)

Captura a alta velocidad (comando AQHI)

La captura a alta velocidad se realiza utilizando los contadores para controlar las memorias, por lo cual es posible alcanzar velocidades de hasta 40 Mhz, que es la velocidad máxima de funcionamiento del conversor analógico-digital.

En este modo tenemos dos limitantes:

  • por arriba, el factor limitante es la velocidad máxima de trabajo del
conversor conversor AD, puesto que los contadores y las memorias pueden trabajar a velocidades aún mayores.
  • por abajo, la limitante es el tamaño de la memoria, puesto que el proceso de adquisición se ejecuta siempre a la velocidad del oscilador (en nuestro caso 8 Mhz) por lo cual la frecuencia mínima a muestrear (si tomamos como requisito capturar al menos 4 ciclos de la señal) sería: f = fs / 65536 muestras / 3 ciclos, lo cual en nuestro caso (fs = 8Mhz) da cerca de un 1 Khz.

Sin embargo, en este modo de captura no se tiene ningún control sobre las memorias durante el proceso de captura: el PIC simplemente dispara los contadores y queda esperando a ser interrumpido por los contadores una vez que finaliza el proceso de captura y las memorias están llenas.

En este modo de captura la señal se muestrea en "ventanas", por lo cual solo sirve para señales periódicas.

Estos son los pasos seguidos por el PIC para ejecutar una captura a alta velocidad:

  1. es presetean los contadores a cero
  2. se libera el bus de datos
  3. se setean los contadores para que cuenten hacia adelante (UPDN=1)
  4. se selecciona el clock rápido en el bloque de control de memoria (CKSEL=0)
  5. se habilita escritura del ADC en el bus (ADCOE=0)
  6. se habilita escritura de memoria (WR=0)
  7. se activan contadores (CKEN=0)
  8. se espera a ser interrumpido por el segundo contador lleno
  9. se transfieren los datos almacenados en la memoria por el puerto USB

Interrupción por contador lleno

Como se mencionó anteriormente, en el modo de captura a alta velocidad el PIC dispara el los contadores y estos se encargan de direccionar la memoria mientras los conversores AD colocan los valores digitalizados en los pines de datos de la memoria.

Debido a que el PIC, una vez que dispara los contadores, pierde el control sobre ellos se precisa un mecanismo para poder detener el proceso de captura. Para ello se ha conectado la pata TC del contador alto al PIC, de forma de que interrumpa al PIC cuando el contador se llene. La pata TC del contador, como puede observarse en la hoja de datos, vale cero únicamente cuando la salida del contador es 11111111, por lo cual se ha configurado el PIC para ser interrumpido por nivel en el pin donde se ha conectado el TC del contador. No cualquier pin del PIC puede ser utilizado para interrumpirlo. En particular, solo los pines RBx son capaces de brindar esa funcionalidad, por lo cual hubo que utilizar uno de éstos.

Al ser interrumpido, el PIC procede inmediatamente a deshabilita los contadores para evitar que se sobreescriban la mayor cantidad de valores (puesto que los contadores se resetean y la memoria se empieza a escribir a partir de la posición 0).

Otra forma de atacar este problema podría haber sido conectar la pata TC directamente a la pin que habilita los contadores de manera que al activarse TC automáticamente se deshabiliten los contadores. Esta solución efectivamente impediría que ningún valor de la memoria se sobreescribiese. Sin embargo, dado la gran cantidad de valores los pocos que resultan sobreescritos no afectan en absoluto para fines prácticos.

Sin embargo, éste mecanismo de detención por hardware resulta de mayor utilidad si se desea implementar un trigger por hardware, ya que en este caso las primeras muestras son muy relevantes.

Captura a media velocidad (comando AQME)

La captura a media velocidad se realiza controlando la escritura a memoria desde el PIC. En este modo también se capturan ventanas de la señal, para luego transferir los datos por el puerto USB, en una etapa posterior.

En este caso tenemos una limitante superior que es la velocidad de procesamiento del PIC. Este valor puede calcular exactamente partiendo de las instrucciones que son ejecutadas entre dos capturas consecutivas del PIC, y utilizando la cartilla de instrucciones del PIC donde viene especificada la duración de cada una. También sabemos que el PIC trabaja siempre a 12 MIPS, por lo cual es un calculo tedioso pero no presenta ninguna complicación. En nuestro caso, el código está escrito en C por lo cual habría que estudiar el código assembler generado por el compilar C18. De todas formas, encontramos de forma empírica que dicha limitante rondaba en los 6 Khz (tomando como requisito la captura de 4 ciclos de reloj).

Supongamos que se solicita una captura de media velocidad de N muestras. Los pasos seguidos por el PIC para realizar dicha captura son los siguientes:

  1. acq_sample = N
  2. se presetean los contadores a cero
  3. se libera el bus de datos
  4. se selecciona el clock lento en el bloque de control de memoria (CKSEL=1)
  5. se habilita la escritura del ADC en el bus (ADCOE=0)
  6. se habilita escritura de memoria (WR=0)
  7. se ejecuta un tick en el contador bajo (CKLO = 0, CKLO = 1, CKLO = 0)
  8. acq_sample = acq_sample - 1
  9. si acq_sample > 0 entonces se va al paso 7, sino se sigue de largo
  10. se transfieren los datos almacenados en la memoria (hasta la posición N) por el puerto USB

NOTA: Debido a que el conversor AD trabaja a una frecuencia mínima de 5 Khz

Captura a baja velocidad (comando AQLO)

Si bien la captura a media velocidad no tiene una frecuencia mínima definida, es de especial interés tener un mecanismo de captura en "tiempo real" para señales de muy baja frecuencia, y es por ello que existe el modo de captura a baja velocidad.

A diferencia de los 2 modos anteriores, la captura a baja velocidad captura y transfiere los datos directamente por el puerto USB, sin pasar por la memoria. Esta captura en tiempo real (es decir, sin usar ventanas) permite ver una captura continua de señales de baja frecuencia.

La limitante en este caso es la velocidad de transferencia por el puerto USB junto con la velocidad de procesamiento del PIC para llevar a cabo todas las tareas. El resultado empírico nos da una frecuencia máximo de trabajo de 4 Hz.

La captura a baja velocidad permite dibujar la señal en pantalla en el momento que se está capturando lo cual brinda una funcionalidad análoga a las provistas por los osciloscopios tradicionales.

Dado su naturaleza, este modo de captura es invocado de forma diferente que el resto. En este caso la captura se "inicia" ejecutando el comando AQLO y continua indefinidamente (transfiriendo datos por el puerto USB) hasta que es detenido con el comando STOP.

Este es un bosquejo de los pasos a seguir para realizar esta captura.

  1. se presetean los contadores a cero
  2. se libera el bus de datos
  3. se setea el PIC para que lea del BUS de datos
  4. se habilita la escritura del ADC en el bus de datos (ADCOE=0)
  5. se lee el bus de datos y se almacena su contenido en un buffer
  6. se transfiere el valor del buffer por el puerto USB
  7. se espera una cantidad de tiempo determinada por el parámetro pasado al comando AQLO
  8. se vuelve al paso 5, a menos que se haya recibido un comando STOP

Modos de captura según la frecuencia de trabajo

A continuación se presenta en una tabla un resumen de todos los modos de captura junto con sus respectivas frecuencias de trabajo, como así también las limitantes para cada caso.

Modo F. mín Limitante por abajo F.MAX Limitante por arriba
AQHI 1 Khz tamaño de la memoria fs frecuencia del oscilador
AQME - - 6 Khz velocidad de procesamiento del PIC
AQLO - - 4 Hz transferencia por USB y procesamiento
Tabla 5.1 Modos de captura - frecuencias límite

La selección del modo captura depende de la frecuencia que se desea observar, y por consiguiente es responsabilidad de la aplicación que controla el osciloscopio solicitar el modo adecuado de captura para la frecuencia deseada.

Divisor horizontal (HDIV)

El funcionamiento de los modos de captura mencionados anteriormente puede ser afectado ligeramente utilizando el divisor horizontal. Este divisor es configurado a través del comando HDIV (ver Protocolo de comunicación) y es el que permite cubrir todo el rango de frecuencia.

El efecto del parámetro HDIV es el de retrasar el tiempo entre dos muestras. En todos los casos, HDIV=0 indica que no habrá ningun retraso y por lo tanto es la captura más rápida a ese modo de trabajo. Este se aplica de forma diferente según el modo de captura, a saber:

  • en la captura a alta velocidad se utiliza para leer la memoria: la memoria es leída salteando HDIV posiciones entre cada lectura. Por ejemplo, para HDIV=5 se leerán (y transferirán) las muestras grabadas en las direcciones: 0, 5, 10, 15, etc.
  • en la captura a media velocidad el parámetro HDIV es utilizado también par a leer la memoria, de análoga a la captura de alta velocidad
  • en la captura a baja velocidad el parámetro HDIV es utilizado para generar una demora arbitraria entre dos transferencias consecutivas de datos, lo cual resulta en un muestreo a más baja velocidad.

Otros parámetros de captura

Además del parámetro HDIV existen otros parámetros que afectan las rutinas de adquisición de datos. Ellos son:

  • CHAN - selecciona el canal a muestrear
  • DUAL - habilita el muestreo de ambos canales
  • CHOP - selecciona como serán muestreados ambos canales (solo para modo DUAL)
  • VDV1 - selecciona la escala de voltaje a utilizar en la etapa de entrada del canal 1
  • VDV2 - selecciona la escala de voltaje a utilizar en la etapa de entrada del canal 2

Resumen de las funciones del firmware

El firmware está compuesto por varias funciones que interactúan entre si para hacer posible su funcionamiento. A continuación se listas estas junto con una descripción de la tarea que desempeñan.

Funciones de inicialización

  • InitializePorts()
    • inicializa todos los pines del PIC para su correcto funcionamiento con el osciloscopio
  • ResetParams()
    • resetea los parámetros de configuración del osciloscopio (escala vertical, modo dual, modo binario, etc) a su valor por defecto (ver protocolo de comunicación)
  • BusFree()
    • libera el bus de datos, colocando todos los dispositivos conectados a él en modo lectura

Funciones de selección y configuración de canales

  • SetChan(unsigned int chan, BOOL write)
    • activa el canal especificado en el parámetro chan y lo pone en modo lectura (si write es FALSE) o en modo escritura (si write es TRUE)
  • SetVdiv(unsigned int can, unsigned int vdiv)
    • selecciona la división de voltaje a usar en el canal especificado

Funciones de comunicación por puerto USB

  • SendResp(unsigned int code)
    • envía una respuesta sin datos por el puerto USB siguiendo el protocolo dde comunicación. code es el código de respuesta a enviar (ver protocolo de comunicación)
  • SendRespData(char* data)
    • envía respuesta OK e inmediatamente los datos pasados por el parámetro data, siguiendo el protocolo de comunicación.
  • SendRespLen(unsigned int code, unsigned int len)
    • similar a SendResp, pero en este caso también el largo de los datos en la respuesta.
  • SendSample(byte data)
    • envía el valor de una muestra capturada (pasada en el parametro data) siguiendo el protocolo de comunicación. El envío se realiza en formato binario o ASCII según como esté configurado el modo binario.
  • SendData()
    • utiliza SendSample para transferir todo el contenido de la memoria. El límite de muestras a enviar viene dada por el parámetro del comando que solicito la captura. El comportamiento de esta función también depende del modo de funcionamiento DUAL/ALT/CHOP.

Funciones de lectura y ejecución comandos

  • CmdGet()
    • recibe, parsea y reconoce un comando recibido por el puerto USB, siguiendo el protocolo de comunicación
  • CmdRun()
    • ejecuta el comando antes recibido por CmdGet, devolviendo respuestas de error cuando corresponda (comando desconocido, respuesta fuera de rango, etc)
  • DoAQHI()
    • es llamada desde la rutina de interrupción disparada luego de finalizada la captura a alta velocidad. Se encarga de transferir los datos capturados (usando SendRespLen y SendData)
  • DoAQME()
    • ejecuta la captura de datos a media velocidad, siguiendo el procedimiento indicado arriba en Modos de captura, y tomando en cuenta los parámetros de configuración del osciloscopio (modo dual, chop, etc)
  • DoAQLO()
    • ejecuta la captura de datos a baja velocidad, siguiendo el procedimiento indicado arriba en Modos de captura, y tomando en cuenta los parámetros de configuración del osciloscopio (modo dual, chop, etc)
  • DoWRLO()
    • ejecuta el comando WRLO (función de depuración utilizada para escribir la memoria a baja velocidad)
  • DoWRHI()
    • ejecuta el comando WRHI (función de depuración utilizada para escribir la memoria a alta velocidad)
  • DoDUMP()
    • ejecuta el comando DUMP que vuelca el contenido de la memoria. Por tratarse de un comando de depuración, no utiliza la función SendData para enviar los datos sino que implementa el envío de datos de forma completamente independiente
  • DoSTOP()
    • ejecuta el comando STOP que detiene todo comando de captura en curso

Funciones de manejo del contador

  • CntPreset(int cnt_pres)
    • presetea los contadores al valor pasado por el parámetro cnt_pres
  • CntTick()
    • realiza un tick en el reloj del contador bajo, haciéndolo incrementarse
  • CntStep()
    • ejecuta repetidamente la función CntTick según el valor actual del parámetro de configuración del osciloscopio HDIV

Manejo de interrupciones

El manejo de las interrupciones a nivel del firmware se hace de la siguiente forma.

Instalación de la ISR

Para instalar la rutina de atención a la interrupción (ISR) es necesario colocar un comando de salto en una posición específica de la memoria. Para ello es necesario utilizar la directive #pragma code que provee el compilador C de Microchip. Esto se hace de la siguiente manera:

#pragma code high_vector=0x08
void interrupt_at_high_vector(void)
{
    _asm goto cntfull_isr _endasm
}

El efecto de utilizar la directiva #pragma code es que el código de la función interrupt_at_high_vector es colocado en la dirección de memoria 0x08 que es la dirección de memoria donde comienza a ejecutar el PIC una vez que recibe la interrupción. A su vez, también se puede observar el uso de las directvias _asm y _endasm que lo que hacen es permitir ingresar código assembler dentro del código C. Esto es necesario en este caso puesto que las instrucciones a colocar en la dirección 0x08 no pueden superar los 2 bytes puesto que se solaparían con las siguientes bytes de memoria que son reservados para otras operaciones. Por lo tanto, la forma de asegurar que este problema no ocurra es incrustar código assembler cuyo largo es conocido.

Rutina de atención a la interrupción

Luego, ya dentro de la propia rutina de atención a la interrupción se ejecuta lo siguiente:

volatile BOOL cntfull = FALSE;

void cntfull_isr(void) {
   INTCON3bits.INT2IF = 0;
   cntStop();
   cntfull = TRUE;
}

La primer línea baja el flag de atención a la interrupción, la segunda detiene los contadores, y la tercera setea el flag de contador lleno que luego es detectado desde el programa principal para ejecutar la transferencia de las muestras adquiridas.

Uso del calificador volatile

Vale recalcar que la variable cntfull debe ser declarada con el calificador volatile. La razón de esto se explica a continuación:

La mayoría de los compiladores de C (entre ellos, el MPLAB C18) realizan optimizaciones a la hora de generar el código assembler (a partir del código C), como por ejemplo, el cacheo del valor de una variable si se da cuenta de que el valor de la misma no cambia entre dos puntos dados del código. Sin embargo, el valor de la variable cntfull es modificado dentro de la rutina de atención a la interrupción, que a su vez es disparada (de forma "impredecible" y completamente asíncrona) por un evento de hardware, por lo cual su contenido puede cambiar de forma impredecible y sería incorrecto cachear su valor. Por lo tanto, el calificador volatile le indica al compilador que no debe realizar ninguna optimización de este tipo para esta variable, y que cada vez que se precise obtener su valor debe ir a consultarlo directamente a memoria.

Una forma de entender mejor este problema es a partir del siguiente código de ejemplo:

while(1) {
   if (cntfull) {
      SendData()
   }
}

En el caso de que la variable cntfull no hubiese sido declarada como volatile el compilador hace lo siguiente al compilar dicho código: como detecta que la variable cntfull no es modificada dentro del loop (ni dentro de la rutina SendData?) directamente toma el valor inicial que tenía la variable al comenzar el loop y trabaja con ese valor constante durante todo el loop, lo cual resulta en que la función SendData? nunca se ejecuta (o se ejecuta siempre!).

El uso del calificador volatile es muy común en el diseño de sistema de tiempo real y sistemas embebidos (como es nuestro caso).

Comunicación USB

A continuación se presentan las funciones utilizadas para la implementación del a comunicación USB en el firmware, junto con sus limitaciones.

Funciones de transferencia USB

Toda la comunicación a través del puerto USB se realiza utilizando un juego de funciones que provee el firmware CDC. Ellas son las siguientes:

Función Descripción
putrsUSBUSART Escribe un string terminado-nulo de la memoria de programa al puerto USB
putsUSBUSART Escribe un string terminado-nulo de la memoria de datos al puerto USB
mUSBUSARTTxRom Escribe un string de largo conocido de la memoria de programa al puerto USB
mUSBUSARTTxRam Escribe un string de largo conocido de la memoria de datos al puerto USB
mUSBUSARTIsTxTrfReady Devuelve TRUE si el driver está pronto para escribir datos en el puerto USB
getsUSBUSART Lee un string del puerto USB
mCDCGetRxLength Devuelve el largo del último string leído del puerto USB
Tabla 5.2 Funciones de transferencia USB

Para que la comunicación USB funcione correctamente y el dispositivo no pierda la conexión con el computador, el firmware CDC impone algunas restricciones que es necesario seguir estrictamente. Dichas restricciones se discuten a continuación:

Cuidados a tener en cuenta

Si bien las funciones de comunicación USB son cómodas de usar, éstas presentan algunas limitaciones que se listan a continuación.

Por más información consultar el Application Note de Microchip: Como migrar aplicaciones de UART RS-232 a USB con un mínimo esfuerzo (ver Referencias).

Chequear si está disponible para enviar

Antes de enviar cualquier string por el puerto USB es necesario chequear que se encuentra listo para enviar más datos, utilizando la función mUSBUSARTIsTxTrfReady.

No utilizar código bloqueante

Dado que las funciones del puerto USB se ejecutan en la rutina UsbTasks() (cada vez que se entra al loop principal) se debe evitar a toda costa utilizar funciones bloqueantes que dependan del estado del puerto USB puesto que éste no se actualiza hasta que se vuelve a correr el loop, y por lo tanto resultaría en una situación de deadlock.

A modo de ejemplo, el siguiente código es incorrecto:

while(!mUSBUSARTIsTxTrfReady());
putrsUSBUSART("Hola mundo");

La forma correcta sería la siguiente:

if (mUSBUSARTIsTxTrfReady()) {
    putrsUSBUSART("Hola mundo");
}

Como todo el código se corre continuamente dentro de un loop infinito, en principio, el resultado de ambos códigos es el mismo, pero en caso de que el código sea más complicado puede requerir un análisis más profundo.

Envío de datos

Las funciones de envío de datos (putsUSBUSART, putrsUSBUSART, mUSBUSARTTxRam y mUSBUSARTTxRom) no son funciones bloqueantes ni tampoco envían los datos inmediatamente, sino que habilitan determinados registros e inicializan una máquina de estados para la transferencia que se ejecutará posteriormente, una vez que se vuelva a iniciar el loop principal (dentro de la rutina CDCTxService() que se llama desde UsbTasks()).

Debido a esto, llamadas consecutivas a dichas funciones no funcionan pues la última llamada siempre sobreescribe a la anterior. La forma correcta de enviar varios strings consecutivamente es usando una máquina de estado o implementando rutinas que almacenen los datos en un buffer intermediarios. A continuación se muestran los dos casos:

Llamadas consecutivas (incorrecto)

if(mUSBUSARTIsTxTrfReady()) {
    putrsUSBUSART("Hola mundo");
    putrsUSBUSART("Hola de nuevo");
}

Usando máquina de estado (correcto)

byte state = 0;
if(state == 0) {
    if(mUSBUSARTIsTxTrfReady()) {
        putrsUSBUSART("Hola mundo");
        state++;
    }
} else if(state == 1) {
    if(mUSBUSARTIsTxTrfReady()) {
        putrsUSBUSART("Hola de nuevo");
        state++;
    }
}

Usando buffer intermedio (correcto)

if (mUSBUSARTIsTxTrfReady()) {
    putsUSBUSART(io_buffer);
    SendToBuffer("Hola mundo");
    SendToBuffer("Hola de nuevo");
}

En este caso la función SendToBuffer() concatenaría los strings en un buffer intermedio para ser enviando en la siguiente transferencia (a correrse en el próximo ciclo). Dicha función debe ser implementada por separado.

Este último fue la solución que adoptamos para manejar los envíos por el puerto USB, y dicha funcionalidad se encuentra implementada en oscusb/osc/usbtx.c.

Driver

Vendor ID y Product ID

El estándar USB exige que todos los dispositivos, durante la etapa de negociación, se identifiquen con un Vendor ID y un Product ID (en adelante, VID y PID). Dicho par de valores sirve para conocer el fabricante del dispositivo (VID) y el modelo particular del producto que se ha conectado (PID). Por lo tanto, modelos diferentes de un mismo producto generalmente tienen PIDs diferentes.

La utilidad principal de estos valores no es solamente la de identificar el dispositivo, sino la de encontrar y cargar los drivers apropiados para el mismo. Por consiguiente, cada driver que viene con Windows (o que bajamos de Internet) viene programado (al igual que el dispositivo) con uno o más PID y VID para los cuales sirve dicho driver. Esta es la forma que tiene Windows (o el sistema operativo en cuestión) de saber si el driver seleccionado es correcto.

En el caso de que el driver ya venga con el sistema operativo, el par VID/PID bastará para identificar el driver que es necesario cargar y por lo tanto cuando se conecta un dispositivo con VID/PID conocido el sistema lo detecta automáticamente e inmediatamente queda listo para usar. Sin embargo, en el caso de que el VID/PID no sea reconocido, el sistema operativo solicitará al usuario que suministre los drivers. Un ejemplo de esto es la conocida pantalla de Nuevo Hardware Detectado de Windows.

Driver para Windows 98/2000/XP

Como ya mencionamos anteriormente, el osciloscopio utiliza el estándar CDC para comunicarse con la PC por el puerto USB. Los sistemas operativos Windows, desde la versión 98SE, ya traen incluido un driver para control dispositivos CDC. Dicho driver es el archivo usbser.sys que, en Windows XP por ejemplo, se encuentra en c:\windows\system32\drivers.

Todos los dispositivos que utilizan dicho driver (por ejemplo, adaptadores Serie-USB) automáticamente agregan un puerto COM virtual al sistema, mientras están conectados, de forma que cualquier aplicación puede trabajar directamente con el puerto virtual como si se tratase de cualquier puerto serie real.

Nuestro osciloscopio, al utilizar el estándar CDC, hace uso de dicho driver y, por consiguiente, no requiere de un driver extra a partir de Windows 98SE en adelante.

Sin embargo, Windows a priori no sabe que driver debe asignar a nuestro osciloscopio porque no reconoce el par VID/PID con el cual éste se identifica. Por lo tanto, es necesario indicarle a Windows que driver debe utilizar para nuestro VID/PID. Esto se hace a través de un archivo .inf donde se especifica, entre otras cosas:

  • PID/VID del dispositivo
  • el driver a utilizar (en este caso usbser.sys)
  • nombre con el que aparecerá el osciloscopio en la lista de dispositivos conectados (en este caso, dentro de Puertos de Comunicaciones)

Archivo INF

A continuación se muestra el archivo inf del osciloscopio:

; Archivo INF para la instalacion del osciloscopio USB utilizando 
; el driver USB CDC ACM
;
; Proyecto de fin de carrera 2005 - Universidad ORT
; 
; Pablo Hoffman - Martin Szmulewicz

[Version] 
Signature="$Windows NT$" 
Class=Ports
ClassGuid={4D36E978-E325-11CE-BFC1-08002BE10318} 
Provider=%MCHP% 
LayoutFile=layout.inf
DriverVer=08/17/2001,5.1.2600.0

[Manufacturer] 
%MFGNAME%=DeviceList

[DestinationDirs] 
DefaultDestDir=12 

[SourceDisksFiles]

[SourceDisksNames]

[DeviceList] 
%DESCRIPTION%=DriverInstall, USB\VID_05F9&PID_FFFF 

;------------------------------------------------------------------------------
;  Windows 2000/XP Sections
;------------------------------------------------------------------------------

[DriverInstall.nt] 
CopyFiles=DriverCopyFiles
AddReg=DriverInstall.nt.AddReg 

[DriverCopyFiles]
usbser.sys,,,0x20

[DriverInstall.nt.AddReg] 
HKR,,DevLoader,,*ntkern 
HKR,,NTMPDriver,,usbser.sys 
HKR,,EnumPropPages32,,"MsPorts.dll,SerialPortPropPageProvider" 

[DriverInstall.nt.Services] 
AddService=usbser, 0x00000002, DriverService

[DriverService] 
DisplayName=%SERVICE% 
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%12%\usbser.sys 

;------------------------------------------------------------------------------
;  String Definitions
;------------------------------------------------------------------------------

[Strings] 
MCHP="pablohoffman.com"
MFGNAME="pablohoffman.com"
DESCRIPTION="Osciloscopio USB" 
SERVICE="USB RS-232 Emulation Driver"

En la línea:

%DESCRIPTION%=DriverInstall, USB\VID_05F9&PID_FFFF

Se puede observar la definición del VID y PID, en este caso VID=05F9 y PID=FFFF. Dichos valores deben coincidir con lo valores del firmware (ubicados en oscusb/autofiles/usbdsc.c), para Windows reconozca el dispositivo y automáticamente le asigne el driver usbser.sys.

Driver para Linux

Linux, al igual que Windows, trae incluidos muchos drivers, entre ellos uno para dispositivos CDC. Por lo tanto, lo que hicimos fue utilizar un VID y PID conocidos para que linux lo reconociera y entonces asignara el driver apropiado al dispositivo. Esta es la razón por la cual elegimos los números VID=05F9 y PID=FFFF, no fue una decisión arbitraria.

Trabajando con esos valores, Linux automáticamente detecta el dispositivo (una vez conectado) y le asigna un archivo (/dev/ttyACM0) que puede ser utilizado para comunicación en forma serie como cualquier puerto serial estándar (/dev/ttySxx). Es importante tener en cuenta que el kernel de Linux debe estar compilado para soportar dispositivos CDC (cualquier kernel actual ya viene preparado).

De esta forma queda resuelta la comunicación USB desde Linux sin tener proporcionar un driver especial o siquiera un archivo INF (como en el caso de Windows).

Estructura del código fuente

Esta es una descripción de los archivos y directorios del código fuente del firmware. Las entradas que terminan en / son directorios, mientras que el resto son archivos.

Archivo/directorio Función que implementa
oscusb/autofiles/ Archivos descriptores del dispositivo USB
oscusb/inf/ Archivo INF que describe el dispositivo para windows (driver Windows 2000/XP)
oscusb/system/ Librerías del firmware CDC
oscusb/_output/ Archivo de salida del compilador y el enlazador
oscusb/main.c Archivo principal desde donde arranca a correr el firmware
oscusb/oscusb.mcp Archivo Project del MPLAB IDE
oscusb/oscusb.mcw Archivo Workspace del MPLAB IDE
oscusb/oscusb.lkr Archivo de comandos para el enlazador
oscusb/io_cfg.h Definición básica de patas del PIC para el funcionamiento de la comunicación USB (definidas por el firmware CDC)
oscusb/osc/interrupt.c Control y manejo de interrupciones
oscusb/osc/osc.c Control y manejo del osciloscopio
oscusb/osc/usbtx.c Control de transferencias por el puerto USB
oscusb/osc/osc_io.h Definición de patas de E/S y macro del PIC para el osciloscopio
Tabla 5.3 Archivos del firmware

Limitaciones del MPLAB C18

Loop infinito

Un requisito de los programas escritos en C para el PIC es que no pueden terminar nunca puesto que si terminan el PIC automáticamente se resetea. Por lo tanto, todos los programas deben correr en un loop infinito. El nuestro no es una excepción y efectivamente así lo hace. Por lo tanto, existen dos grandes rutinas principales:

  1. una rutina de inicialización (OscInit())
  2. una rutina que se ejecuta repetidamente en un loop infinito (OscLoop())

La rutina OscInit() se encarga de inicializar los puertos y las interrupciones del PIC para el funcionamiento correcto del osciloscopio y se llama desde la rutina InitializeSystem(), quien a su vez también inicializa el firmware CDC para la comunicación USB.

Inmediatamente después de inicializado el sistema, se entra en un loop infinito que corre dos rutinas indefinidamente:

  1. la rutina UsbTasks() que mantiene viva la conexión USB y envía/reciba los datos que haya pendientes
  2. la rutina OscLoop() que contiene toda la lógica principal del osciloscopio

Dado que el firmware de nuestro osciloscopio corre sobre el firmware CDC fue necesario hacer un par de modificaciones mínimas al mismo para contemplar el funcionamiento paralelo de ambos (firmware CDC y firmware del osciloscopio). Para lograr ello solo bastó modificar el archivo principal (main.c) y colocar allí las llamadas a las funciones antes mencionadas (OscInit() y OscLoop()) en el lugar apropiado.

Variables y manejo de memoria

Si bien el compilador C18 de Microchip se ajusta bastante bien al estándar ANSI '87, vale recalcar que tiene algunas particularidades que es necesario tener en cuenta a la hora de escribir código, como ser el manejo de las variables en memoria.

En concreto, las variables pueden estar almacenadas tanto en memoria RAM como en memoria ROM. El compilador C18 soporta un par de calificadores (no contemplados en el estándar de C) rom y ram que permiten especificar donde se guardará la variable. Por ejemplo:

rom int version;
ram char[10] command;

El problema es que las funciones de manejo de string no permiten trabajar con variables ubicadas en distintas memorias. Por lo tanto, la función strcmp() no permite comparar una cadena almacenada en la ROM con otra cadena almacenada en la RAM.

Todas las variables definidas sin el calificador rom/ram quedan por defecto almacenadas en la RAM, mientras que las constantes como "Hola mundo" quedan almacenadas en la ROM.

Tomemos como ejemplo el siguiente código:

#include <string.h>

char cadena[10] = "hola";

bool comparo() {

    if (strcmp(cadena, "hola")) {
        return TRUE;
    } else {
        return FALSE;
    }
}

En principio, la función comparo() debería retornar TRUE. Sin embargo, debido a los problemas mencionados del almacenamiento en la RAM/ROM, retorna FALSE puesto que strcmp() no sabe comparar datos de la RAM (cadena) con datos de la ROM ("hola"). Por lo tanto es necesario reescribir el código de la siguiente manera para su correcto funcionamiento:

#include <string.h>

char cadena[10] = "hola";

bool comparo() {
    char hola[5] = "hola";

    if (strcmp(cadena, hola)) {
        return TRUE;
    } else {
        return FALSE;
    }
}

Y por lo tanto así es como está implementado (en osc.c) la comparación de los datos entrantes del puerto USB para detectar el comando que fue solicitado.

Trabajar de esta forma resulta bastante tedioso y perdimos bastante tiempo para darnos cuenta de donde estaba el problema. Incluso fue necesario el uso del debugger por hardware. Además, nos parece que estos problemas deberían estar resueltos a nivel del compilador.

Configuration bits

Los configuration bits sirven para configurar el modo de funcionamiento del PIC (por ejemplo, la frecuencia del oscilador) y deben asignarse durante la programación. La configuration bits son seteados por el MPLAB durante la programación y se pueden asignar de 2 formas:

  1. a través de la lista de configuration bits del MPLAB (en Configure -> Configuration bits)
  2. a través de macros en el propio código, utilizando la declaración #pragma config

A continuación se muestra la pantalla de selección de los Configuration bits del MPLAB (opción 1).

configbits.jpg
Fig. 5.3 Configuration bits del PIC 18F4550

Nosotros preferimos asignar los Configuration bits a través de definiciones en el propio código, para no depender de la configuración del entorno de trabajo del MPLAB. Dichas definiciones se encuentran en el archivo oscusb/osc/confbits.h.

Actualizaciones de firmware

Otra característica muy importante a implementar, en lo que se refiere al firmware, es la posibilidad de poder actualizar el firmware a través del mismo puerto USB, sin necesidad de recurrir a un programar. Esta característica, si bien no es de gran utilidad para el desarrollo del prototipo, será de vital importancia a la hora de fabricar el producto para su distribución masiva.

Afortunadamente, esta característica viene de regalo con el firmware CDC de Microchip, que a su vez utiliza el Microchip Bootloader (ver Referencias), que es quien provee el mecanismo de re-programación del PIC a través del mismo puerto USB.

El único requisito necesario es conectar un pulsador al pin RB4 del PIC (lo cual está hecho en nuestro osciloscopio). Luego, para que el PIC entre en modo programación ha que prenderlo (o resetearlo) con dicho pulsador presionado.

Luego, basta con utilizar la aplicación PDFSUSB.exe incluída en el Framework USB de Microchip (del cual el Firmware CDC es parte) para re-programar el dispositivo utilizando a través del puerto USB.

Referencias

toggleopenShow attachmentstogglecloseHide attachments
Topic attachments
I Attachment Action Size Date Who Comment
jpgjpg configbits.jpg manage 169.8 K 25 Feb 2006 - 16:54 PabloHoffman configuration bits para el funcionamiento correcto
txtinf oscusb.inf manage 1.5 K 05 Mar 2006 - 01:53 PabloHoffman Archivo INF para el driver del osciloscopio (Windows)
jpgjpg oscusb-mplabide.jpg manage 194.8 K 08 Jun 2006 - 13:37 PabloHoffman Captura de pantalla del MPLAB IDE
gifgif oscusb-diag-acqu.gif manage 34.1 K 08 Jun 2006 - 15:18 PabloHoffman diagrama de procesos de captura
elsevsd oscusb-diag-acqu.vsd manage 85.5 K 08 Jun 2006 - 15:18 PabloHoffman diagrama de procesos de captura - fuentes visio
Edit | Attach | Printable | Raw View | Backlinks: Web, All Webs | History: r40 < r39 < r38 < r37 < r36 | More topic actions
Oscusb.DocCap05Firmware moved from Oscusb.DocFirmware on 27 May 2006 - 21:11 by PabloHoffman - put it back

Osciloscopio USB


Esta Web
Cronograma
  • DONE planificación
  • DONE análisis
  • DONE presentación oral
  • DONE diseño
  • DONE implementación
  • DONE documentación
  • DONE imprevistos
  • DONE defensa
  • MOVED TO... correcciones
 
Powered by pablohoffman.com
This site is powered by the TWiki collaboration platformCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding pablohoffman.com? Send feedback