Traducción


by Transposh - Plugin de traducción para WordPress

Categorías

Compilaciones

Servidor de desarrollo

Envíos informativos de cómo configurar un servidor local para desarrollo web.

VUSiBino demo – Parte I – El Firmware

Antes de seguir con circuitos y “escudos para el VUSiBino, vamos a explicar un poco cómo funciona el programa que hemos cargado como ejemplo y el programa del “host” para controlar el microcontrolador. Su función básica es demostrar cómo podemos cambiar el estado de los pines, enviar y recibir información desde el PC hacia el aparato. Mis conocimientos de C y C++ son, por decirlo suavemente muy limitados, el código es por tanto aturullado, pero funciona. De momento sólo corre en Windows debido a no haber podido compilar librerías multiplataforma. Agradezco a Joonas Pihlajamaa de Code And Life su excelente guía sobre la programación del v-USB, sin la cual no creo que hubiera sido capaz de ocurrírseme esto.

*Nota del 10 de dieicembre de 2017, corregido el código donde sw_led se asignaba incorrectamente.

El Firmware.

Para desarrollar y compilar el firmware he usado AVR Studio 5, el “toolchain” winAVR, las librerías de v-usb.  Para subirlas al dispositivo, AVRdudess.

Una vez instalado AVR Studio 5 y winAVR basta con abrir el archivo vusibino.avrgccproj dentro del directorio Firmware.

En el archivo usbconfig.h del directorio usbdrv configuraremos el comportamiento del VUSiBino, la corriente con la que se alimentará (por defecto es de 50mA), nosotros la cambiamos a 100mA.

#define USB_CFG_INTR_POLL_INTERVAL      10
/* If you compile a version with endpoint 1 (interrupt-in), this is the poll
 * interval. The value is in milliseconds and must not be less than 10 ms for
 * low speed devices.
 */
#define USB_CFG_IS_SELF_POWERED         0
/* Define this to 1 if the device has its own power supply. Set it to 0 if the
 * device is powered from the USB bus.
 */
#define USB_CFG_MAX_BUS_POWER           100
/* Set this variable to the maximum USB bus power consumption of your device.
 * The value is in milliamperes. [It will be divided by two since USB
 * communicates power requirements in units of 2 mA.]
 */

Más abajo definimos las cadenas de identificación del dispositivo, si no disponemos de identificadores, Obdev nos permite usar sus identificadores siempre que cumplamos con las normas que se indican en el documento readme.txt, adjunto en el directorio usbdrv.

#define  USB_CFG_VENDOR_ID       0xc0, 0x16 /* = 0x16c0 = 5824 = voti.nl */
/* USB vendor ID for the device, low byte first. If you have registered your
 * own Vendor ID, define it here. Otherwise you may use one of obdev's free
 * shared VID/PID pairs. Be sure to read USB-IDs-for-free.txt for rules!
 * *** IMPORTANT NOTE ***
 * This template uses obdev's shared VID/PID pair for Vendor Class devices
 * with libusb: 0x16c0/0x5dc.  Use this VID/PID pair ONLY if you understand
 * the implications!
 */
#define  USB_CFG_DEVICE_ID       0xdc, 0x05 /* = 0x05dc = 1500 */

Podemos ahora definir  nombre del fabricante y del dispositivo, además de un número de serie. Muy útil para distinguir nuestro aparato de otro que estemos usando con el mismo identificador provisto por Obdev.

#define USB_CFG_VENDOR_NAME     'c', 'h', 'a', 'f', 'a', 'l', 'l', 'a', 'd', 'a', 's', '.', 'c', 'o', 'm'
#define USB_CFG_VENDOR_NAME_LEN 15
#define USB_CFG_DEVICE_NAME     'V', 'U', 'S', 'i', 'B', 'i', 'n', 'o'
#define USB_CFG_DEVICE_NAME_LEN 8
#define USB_CFG_SERIAL_NUMBER   'D', 'E', 'M', '0', '0', '1' 
#define USB_CFG_SERIAL_NUMBER_LEN   6

Si además planeamos usar varios de nuestros dispositivos, esta definición nos permite cambiar el número de serie dinámicamente en caso de necesidad. Con esto ya tendríamos configurado el USB para que se identifique y comunique correctamente con el PC. Se pueden ajustar muchas otras variables, e incluso definir un dispositivo HID, que no requiere de drivers, pero no es este el objetivo de este dispositivo.

#define USB_CFG_DESCR_PROPS_STRING_SERIAL_NUMBER (USB_PROP_IS_DYNAMIC | USB_PROP_IS_RAM)

Pasamos entonces al fichero “vusibino.c”, que es en el que programamos los eventos a los que responderá VUSiBino. En este ejemplo nos vamos a centrar en la comunicación con el host. Comenzaremos por definir la velocidad del reloj del MCU, caso de que no esté definida en el makefile o el IDE. Tras ello definimos la longitud del número de serie, que ha de ser igual a la definida en el fichero “usbconfig.h”. Creamos una variable global para manejar el número de serie y pasamos a definir las etiquetas que usaremos para manejar los comandos que nos envíe el programa de control, por comodidad estas etiquetas se definirán del mismo modo en el código del host. Y terminamos definiendo unas variables globales para los datos recibidos y enviados. No son muy recomendades en C, pero ahorran mucho trabajo en la programación del MCU a los no expertos como yo.

//#define F_CPU 16000000L // uncomment if not defined yet in the IDE or usbconfig.h

#define SERIAL_NUMBER_LENGTH 6 // the number of characters required for your serial number

static int  serialNumberDescriptor[SERIAL_NUMBER_LENGTH + 1];

#define USB_LED_ON 0
#define USB_LED_BLINK 1
#define USB_LED_OFF 2
#define USB_SEND_MESSAGE  3
#define USB_READ_MESSAGE  4
#define USB_SET_SERIAL 5

static char replyBuf[16] = "Hello, USB!";
static uchar dataReceived = 0, dataLength = 0; // for USB_DATA_IN
static char serialNum[16] = "DEM001";

int lapse_01 = 0;
volatile int countA = 0;
volatile int sw_led = 0b00000000;
volatile int operation = 0;

Pasamos a las rutinas de manipulación del número de serie. “usbFunctionDescriptor” nos devolverá el número de serie definido por “serialNumberDescriptor” y que modificaremos a nuestro gusto usando “SetSerial”. Dicho de otro modo, para cambiar el n´ñumero de serie nos bastará con llamar a SetSerial(serialNum); y trabajaremos con serialNum para manejar este dato antes de pasárselo al controlador.

uchar usbFunctionDescriptor(usbRequest_t *rq)
{
   uchar len = 0;
   usbMsgPtr = 0;
   if (rq->wValue.bytes[1] == USBDESCR_STRING && rq->wValue.bytes[0] == 3) // 3 is the type of string descriptor, in this case the device serial number
   {
      usbMsgPtr = (uchar*)serialNumberDescriptor;
      len = sizeof(serialNumberDescriptor);
   }
   return len;
}

static void SetSerial(char *data)
{
   serialNumberDescriptor[0] = USB_STRING_DESCRIPTOR_HEADER(SERIAL_NUMBER_LENGTH);
   serialNumberDescriptor[1] = data[0];
   serialNumberDescriptor[2] = data[1];
   serialNumberDescriptor[3] = data[2];
   serialNumberDescriptor[4] = data[3];
   serialNumberDescriptor[5] = data[4];
   serialNumberDescriptor[6] = data[5];
}

Lo siguiente es manejar la informaciçón que nos manda el host, usando la función “usbFunctionSetup” y decidiendo qué hacer en cada caso. La variable global operation la ponemos a cero para evitar confusiones más adelante. La variable sw_led es usada para saber en qué estado está el led, si su segundo bit es cero, se comportará de forma estática. Nos aseguramos de ello aplicando una máscara de bits con el operador “and”, cada bit de la variable es comparado con un bit de la máscara, si ambos son uno, el resultado es un uno, si alguno no lo es, será cero. La máscara son todo unos, excepto en el bit que queremos cambiar. Con la variable “sw_led” usaremos otra operación, “and”, más adecuada para poner los bits que deseemos a cero. Si un bit es uno, y el otro también, el resultado es uno, en otro caso en la posición se almacenará un cero. Como queremos poner a cero el segundo bit de la variable, todos los bits del comparador serán uno excepto ese.

La tabla a continuación es una “tabla de verdades” que muestra los resultados de las operaciones “and” y “or”.

A&B 1 0 A|B 1 0
1  1  0 1  1  1
0  0  0  0  1  0

 

Explico los casos:

La orden switch evualuará el dato enviado por el host y guardado en la variable “rq-bRequest” de v-USB.

-Caso USB_LED_BLINK. Si el host envía la orden “USB_LED_BLINK”, la rutina pondrá el segundo bit de sw_led a uno usando una “mascara de bits” con la operación or. Se compara cada bit con un uno o un cero, si el bit de la variable es cero y el del comparador tmbién, se guardará un cero, si alguno de ellos es un uno, se guardará un uno en esa posición. Como no le decimos que salga de la rutina ni de la secuencia de preguntas (swith case), el programa seguirá con la siguiente instrucción. No lo hacemos para aprovechar el código del siguiente caso, que enciende el led.

-Caso USB_LED_ON. Cambiamos el segundo bit de “sw_led” a cero con “and”. El programa pasará por aquí después de haber pasado por “USB_LED_BLINK” o si el host ha enviado “USB_LED_ON”. Es donde se encenderá el led cambiamos el estado del pin13 (PB5), que es donde tenemos el led rojo, para que sea de salida en el puerto DDRB del AVR aplicando una máscara. Para encender el led, activamos el pin13 con la misma técnica. Y salimos de la función con “return 0” para no procesar las siguientes instrucciones.

-Caso USB_LED_OFF. Cambiamos el segundo bit de “sw_led” a cero con “and”. Apagamos el pin previemente encendido con otra operación lógica y usando una macro que define ese pin en las librerías del AVR, “PB5” se refiere al pin cinco del puerto B, que se corresponde con el pin 13 de Arduino y VUSiBino.  La operación es poner un bit en PB5, compararlo con and con el estado actual, y negarlo en caso de que sea un uno, después saldremos de la función.

Este es un ejemplo de la notación más habitual que veremos para estas operaciones en los ejemplos que encontremos en la red. La notación no es muy intuitiva, pero es una operación muy rápida y se ahorra en escritura. También veremos que no se usan ceros y unos para expresar las máscaras de bits, sino su equivalente en hexadecimal, mucho más rápido de escribir que una cadena de unos y ceros. En caso de duda, siempre podemos usar una calculadora que nos haga la transformación de base binaria a hexadecimal.

-Caso USB_READ_MESSAGE. “Enviamos” el contenido de “replyBuf” al PC usando una variable de v-USB “usbMsgPtr”, salimos de la función diciendo la longitud de la cadena a enviar, v-USB se encargará del envío.

-Caso USB_SET_SERIAL. Ponemos el valor 9 a la variable “operation”, que usaremos más adelante, y seguimos procesando.

-Caso USB_SEND_MESSAGE. Escribimos en un “buffer” datos que nos envía el host. en dataLenght calculamos el número de bytes que nos han sido enviados, si es mayor que el bufer que usamos, recortamos lo que nos sobra. Salimos de la función devolviendo USB_NO_MSG, que llamará a la función donde trataremos estos datos.

// this gets called when custom control message is received
USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) {
	usbRequest_t *rq = (void *)data; // cast data to correct type
	operation=0;
	switch(rq->bRequest) { // custom command is in the bRequest field
	case USB_LED_BLINK: //
		sw_led = sw_led | 0b00000010;
		return 0;
    case USB_LED_ON: //
		sw_led = sw_led & 0b11111101;	
		PORTB = PORTB | 0b00100000; //turn LED on	
		return 0;
	case USB_LED_OFF: // Turn off the led
		sw_led = sw_led & 0b11111101;
		PORTB &= ~(1<<PB5); //  Turn off led
		return 0;
	case USB_READ_MESSAGE: //Read message from buffer to PC
        usbMsgPtr = replyBuf;
        return sizeof(replyBuf);
	case USB_SET_SERIAL:
		operation = 9;
	case USB_SEND_MESSAGE://Write from PC to buffer
		dataLength  = (uchar)rq->wLength.word;
        dataReceived = 0;	
		if(dataLength  > sizeof(replyBuf)) // limit to buffer size
			dataLength  = sizeof(replyBuf);
		return USB_NO_MSG; // usbFunctionWrite will be called now
    }

    return 0; // should not get here
}

usbFunctionWrite se encarga de leer los datos que el host nos envía, *data es un puntero a una cadena de bytes, y len es la longitud de esa cadena. Mientras recorremos cada byte de la cadena enviada, comprobamos el valor de la variable “operation” y almacenaremos esos datos en la cadena que guarda el número de serie, o en la que guarda el bufer de respuesta según sea 9 o 0. Al acabar salimos de la función devolviendo un valos que indica si la operación se completó con éxito.

USB_PUBLIC uchar usbFunctionWrite(uchar *data, uchar len) {
	uchar i;
			
	for(i = 0; dataReceived < dataLength && i < len; i++, dataReceived++)
	{
		if (operation==9)
		{
			serialNum[dataReceived] = data[i];
		}
		else
		{
			replyBuf[dataReceived] = data[i];
		}
	}
    return (dataReceived == dataLength); // 1 if we received it all, 0 if not	
}

Esta es la función principal del programa, definimos el número de serie llamando a SetSerial con el valor que contiene serialNum, declaramos una variabla de conteo, definimos el comportamiento de los puertos B y D con “or” y configuramos un temporizador del microchip para que cuente un lapso de tiempo.

TCCR1B representa el temporizador uno en los chips ATMEGA, le decimos que el bit WGM12 se active, y esto configura dicho temporizador para que cuente y haga una operación sobre una bandera cada vez que haya llegado al límite de la cuenta. TIMSK1 es la máscara de interrupción del temporizador uno, OCIE1A es un bit que le indica si la interrupción se activará o no, le ponemos un uno en ese bit para decir que esa interrupción se va a usar.

Activamos el “guardián” para que cada segundo lea el chip, esto permite que si hemos programado mal el firmware, podamos resetearlo para cambiar el programa, de otro modo nunca podríamos volver a usarlo.

Llamamos a la función v-USB usbinit que prepara el aparato para ser usado como un dispositivo usb, se comunica con los drivers de usb-lib y le dice al host qué clase de dispositivo es, cómo se llama y cómo lo usaremos. Iniciamos un ciclo de 500ms donde VUSiBino y el PC se preparan para entenderse, y le decimos al PC que nos hemos conectado y nos enumere. Estas son funciones de v-USB. Tras eso, permitimos que las interrupciones se activen.

Ahora definimos las unidades de tiempo, en el registro OCR1A ponemos un valor derivado de el preescalado que asignamos al reloj en TCCR1B. En en código fuente vienen comentados los posibles valores. Resumido de manera muy somera, lo que hacemos es decirle al temporizador mediante TCCR1B que divida la frecuencia del reloj (16MHz) entre un valor (256) y use esa cifra como referencia temporal, así la frecuencia que manejará el temporizador será de 62500 Hz, en OCR1A le decimos que cuente hasta 62 (0.000992 segundos) antes de cambier la bandera de interrupción. En el datasheet del atmega, este artículo, o este envío explican detalladamente cómo funcionan estos temporizadores.

Lo siguiente es un bucle infinito, el “bucle principal” que se repetirá mientras el microcontrolador esté encendido o fuera del “modo de programación”. No tiene demasiado, una llamada a usbpoll() para mantener la conexión activa y comprobar el estado de la variable “operation” para cambier cuando sea necesario el número de serie. Esto podríamos haberlo hecho más arriba, en la función “usbFunctionWrite”, pero me daba pena dejar el bucle vacío. Aparte de que hay una función de control de interrupciones que tengo que investigar un poco más.

int main()
{
	SetSerial(serialNum);
	uchar i;

	DDRB = DDRB | 0b00101011; // PB0 PB1 PB3 PB5 as output Rest as input
	DDRD = DDRD | 0b01101000; // PD3 PD5 PD6 as output Rest as input

	TCCR1B |= (1 << WGM12); // Configure timer 1 for CTC mode
	TIMSK1 |= (1 << OCIE1A); // Enable CTC interrupt
	
    wdt_enable(WDTO_1S); // enable 1s watchdog timer

    usbInit();
	
    usbDeviceDisconnect(); // enforce re-enumeration
    for(i = 0; i<250; i++)
		{ // wait 500 ms
        wdt_reset(); // keep the watchdog happy
        _delay_ms(2);
    	}
    usbDeviceConnect();

    sei(); // Enable interrupts after re-enumeration

	OCR1A    = 62;  // 1 Tic = 0.000992 Secs
        TCCR1B |= (1 << CS12); // Start timer at Fcpu/256 16MHz/256=62500 ticks per second.
    while(1)
	{
        wdt_reset(); // keep the watchdog happy
        usbPoll();
		if (operation == 9)
		{
			SetSerial(serialNum);
		}		
	}
    return 0;
}

Para el final dejamos la rutina llamada por la interrupción que activamos antes. Una interrupción es un evento que detiene el flujo normal de un programa para hacer una operación. Son de las funciones más antiguas y útiles de los circuitos programables, pero teniendo en cuenta que detienen un programa para hacer algo en medio de la ejecución, son delicadas. Si las interrupciones manejan algún dato que use el programa, deberemos estar muy atentos a ese dato, ya que la interrupción puede ser activada en cualquier momento de la ejecución y causar un desastre si no tenemos cuidado.

Para manejar el código de una interrupción usaremos las definiciones de la librería AVR, ISR indica que el código a continuación se ejecuta cuando una interrupción se activa, entre paréntesis decimos quien es esa interrupción, en nuestro caso “TIMER1_COMPA_vect”, el vector de interrupción del comparador A en el temporizador 1. En el código anterior definimos que el temporizador 1 tendría una frecuencia de 62500Hz y que cada 62 ciclos saltaría el contador OCR1A. Como a TCCR1B le dijimos que sería de comparación y a TIMSK1 que activaría la interrupción, tenemos que cada 0.000992 segundos esta interrupción será procesada. Aquí tenemos una guía muy bien hecha sobre las interrupciones en AVR.

Lo que hacemos al ser activada es sumar otro contador, countA comprobar que, primero el bit de parpadeo está activo en sw_led y si es así ver si la interrupción fue llamada cien veces (casi una décima de segundo), de ser así hacemos dos cosas, cambiamos el estado del led y el del último bit de la variable de led. Esto último por comprobar variables únicamente. Para hacer este cambio en los bits, usaremos otra operación lógica básica, el “xor”, si un bit es cero y el otro es cero, se queda a cero, si un bit es uno y el otro es cero, pasa un uno, y si un bit es uno y el oto también, pasa un cero, o simplemente que si son iguales pone un cero y si son distintos un uno.
Esta es su tabla de verdad.

A^B 1 0
1 0 1
0 1 0
ISR(TIMER1_COMPA_vect) //Timer 1 Compare A triggered.  We use Timer 1 for B port pins, no special reason for that.
{
countA++;
lapse_01++;
if(sw_led & 0b00000010) // Check if blink flag is active
	{
	if (countA > 100)
		{
		PORTB = PORTB ^ 0b00100000; // Toggle  the  LED
		sw_led = sw_led ^ 0b00000001; // Toggle  the  Switch
		countA = 0;
		}
	}			
}

En el siguiente envío veremos la parte del “host”, el programa de PC desde el cual manejaremos el VUSiBino. Explicaremos cómo usar las funciones de v-USB y usbLib para comunicarnos, y un poco por encima cómo crear y usar un diálogo de windows.

Ficheros.

Esquemas

Binarios

Código fuente.

Github.

VUSiBino Demo en Github

 

Deja un comentario