Hola a todos, ¿que tal? (espero que bien), este es mi primer mensaje en el foro y simplemente quería saludar y estrenarme con un proyectillo que hice el verano pasado.
Como algún compañero vuestro ya sabe
no soy una persona muy participativa y supongo que el tiempo, cansancio, (un poco vago) y demás excusas son eso simplemente, excusas para ser un poco más egoista. Así que bueno, vamos a remediarlo un pelín aprovechando que he tenido un contratiempo el cual me hará replantearme un proyecto sobre ethernet que me tiene la cabezita caliente, nada mejor que escribir un poco por aquí.
El código/proyecto/como lo querais llamar, lo desarrollé este verano con el fin de hacerme un panel de control o mando para el emulador mame, básicamente es una implementación de un teclado usb que sigue el estandar HID que implementan los sistemas operativos. Básicamente la motivación y el porqué lo realizé lo podeis encontrar en esta página de una comunidad arcade que amablemente tienen colgados algunos desarrollos de los aficionados a los juegos antiguos/retro/emu/....
MamandoMe (cuidadín con el juego de palabras elegido para el nombre jejeje)
Bueno básicamente la decisión de hacerlo fue porque los teclados usb normalmente vienen programados con una limitación de máximas teclas pulsadas a la vez (6 teclas) y para un mando de este tipo se necesitan bastantes más, aparte las soluciones comerciales, pues eso eran comerciales y apetecia hacerme uno desde cero patatero.
La verdad es que como experiencia saqué un montón de conocimientos bastante interesantes y sobre todo el comprender como funciona perfectamente el protocolo de comunicaciones en el que está basado usb. Claro que desde el verano hasta hoy las neuronas se han adormecido algo.
Si me gustaría dar una recomendación a todo aquel que comienze algún proyecto usb, y es que comprenda lo mejor posible como es el protocolo, los distintos modos de transmisión que existen y una cosa que le salvará el culo: Entiende/Maneja/Comprende y haz encaje de bolillos con los descriptores, el resto es coser y cantar. Además de esto y referente al código que aquí os dejo, la implementación usb del fabricante CCS para su compilador PICC está bastante enredada, así que si sirve de algo he intentado ordenar un poco el código y quizás os pueda servir de plantilla para otros proyectos.
Antes de comenzar pasaros por esta web y leedla porque os pondrá en antecedentes, aquí solamente voy a incluir código, en esa otra página podeis ver fotografias, el pcb y algunas cositas.
MamandoMeBueno el que quiera meterse en esto del usb mi recomendación es que se lea de pe a pa el "USB System Architecture (USB 2.0)" de Adisson Wesley y escrito por Don Anderson, luego que comprenda bien el modo de transmisión que se adapta a su aplicación (Interrupt, Bulk, Isosynchronous y Control), en este caso Interrupt (que no hace honor a su nombre ya que en usb nadie transmite por iniciativa propia sino que el host es el que manda y más bien este modo es un pooling) y desde luego que entienda y configure los "descriptores" como si la aplicación no fuese a funcionar de no hacerlo bien (es lo que ocurrirá ya que son las estructuras que se transmiten en el proceso de enumeración y por el cual un dispositivo se describe e identifica en el sistema).
En fin que me enrollo, la implementación usb de la compañia CCS, está hecha de una forma muy rígida y con una serie de restricciones, quizas la mas importante que solo soporta implementaciones con una sola configuración (aunque lo más probable es que no lo necesites, ya sabrás lo que es cuando lo necesites). Esta rigidez nos va a imponer la forma en la que se deberán declarar las cosas en nuestro código y como y cuando se incluiran los ficheros necesarios.
La forma que tiene no me gustaba nada y me parecia bastante enredosa, así que dandole vueltas un rato llegué a simplificar y hacer más clara una plantilla que podeis seguir para cualquier desarrollo.
En vuestro código deberíais hacer las cosas en el siguiente orden:
- En primer lugar las definiciones que configuran el dispositivo (tipo de microcontrolador, reloj, fuses, etc)
- Después y es donde comenzamos la parte de usb las definiciones que configurarán la libreria usb
- Para finalizar incluiremos tres ficheros en este orden:
- #include <hardware_layer.h>
- #include "usb_descriptors.h"
- #include <usb.c>
- Resto del código de vuestra aplicación
¿Porque este orden de definiciones y ficheros, pues bien, las definiciones iniciales indicarán si usamos un transceiver interno o externo, cuantos puntos de comunicación, tamaño de los buffers y modo de transferencia, etc. Esto es necesario que se haga al principio porque estos #define serán usados en los siguientes ficheros que incluyamos. Es decir:
// 1 to use a PIC with an internal USB Peripheral
// 0 to use a National USBN960x peripheral
#define __USB_PIC_PERIF__ 1
#define USB_HID_DEVICE TRUE
#define USB_EP1_TX_ENABLE USB_ENABLE_INTERRUPT
#define USB_EP1_TX_SIZE 14 //max packet size of this endpoint
#define USB_EP1_RX_ENABLE USB_ENABLE_INTERRUPT //turn on EP1 for IN bulk/interrupt transfers
#define USB_EP1_RX_SIZE 8
Después tenemos <hardware_layer.h> que puede ser pic18_usb.h o pic_usb.h, según nuestro dispositivo sea de la familia 18F o 16F.
Seguidamente y aquí es donde se empiezan a poner las cosas interesantes es el fichero usb_descriptors.h (podeis llamarlo como querais), en el que se deberán detallar los descriptores que se negociarán usando las estructuras del stack usb de la empresa CSS. Como es un pelín complicado os pongo abajo el fichero y ya vereis como lo acabais entendiendo. (digo yo)
Al final incluid el fichero <usb.c> donde se implementa el stack en si.
Creo que he sido muy escueto con los descriptores, asi que voy a daros una ligera idea de que va. Los descriptores son datos que se negocian cuando conectas un dispositivo usb a un hub usb, en nuestro caso el que tiene el pc y está controlado por el sistema operativo. Al enchufar el dispositivo se inicia un proceso que se llama de enumeración, en el que se negocian una serie de datos denominados descriptores. Estos descriptores dependiendo de que tipo son tienen una u otra estructura y sirven para que el sistema operativo sepa que tipo de cacharro se está conectando al bus.
El primer descriptor que se negocia es el de dispositivo y en el se dice la versión usb soportada, el codigo del fabricante el identificador del producto, etc, así como el nº de configuraciones soportadas (no tengo claro para que son, creo que por ejemplo una camara de fotos puede ser un dispositivo de almacenamiento y una web-cam, en nuestro caso nuestros dispositivos solo soportarán una configuración).
Como vereis los descriptores se encadenan unos a otros, el descriptor de dispositivo dice que hay una sola configuración, pues lo siguiente que se transmite es el primer y único (en este caso) descriptor de configuración.
En el descriptor de configuración se indican los interfaces implementados en el dispositivo, en este caso solamente uno: un teclado, pero podría ser un dispositivo compuesto raton+teclado+joystick por ejemplo y tendríamos tres interfaces (esto si lo soporta el stack usb CCS), además en este descriptor se dice si el dispositivo lleva alimentación externa o se alimenta del bus usb en cuyo caso hay que indicar los miliamperios máximos que consumirá.
Después vienen los descriptores de cada uno de los interfaces, en los que por supuesto se exponen las características del interfaz, en este caso un teclado. Se indica en primer lugar el número de puntos de comunicación, endpoints, o pipes o como querais llamarlos, dejemoslos en canales de comunicación, que requerirá el dispositivo, en este caso 1 (además del canal 0). Además en este descriptor se ha de especificar que es este interfaz, o mejor dicho, la clase a la que pertenece, es decir, "lo que es el cacharro" a que clase de dispositivo pertenece, existen unos predefinidos y otros que el fabricante decide que son (para los cuales hay que hacer un driver específico). Como este dispositivo es un teclado, cae dentro de la clase HID (Human Interface Device), se especifica y punto.
Seguidamente viene un descriptor de "clase" "class descriptor" que informará sobre la clase de dispositivo que es, en este caso al ser HID, hay que indicar la version HID que se soporta, algunos datos de localización, el número de descriptores que vienen detrás y terminarán la configuración.
Básicamente lo que se hace es decir, oye sistema operativo, yo soy un cacharro usb, y soy un dispositivo HID, cada vez que se pulsen unas teclas yo voy a enviar una trama con las teclas que hay pulsadas y tal, tu cojelas y inyectalas en tu "global keyboard".
Para rizar el rizo, como último descriptor HID se envia un descriptor que siguiendo una especificación de códigos se "dice" nada mas y nada menos el formato de la trama que se transmitirá, es decir, tu puedes decir, los dos primeros bytes para las coordenadas del raton, luego un byte con un mapa de bits para los botones, luego 6 bytes con las teclas pulsadas y al final 5 bits con el estado de los leds del teclado. Como si los das en ese orden o en el que tu quieras, lo único que hay que especificarlo en un lenguaje que son unos códigos que especifica el estandar HID y ya está (nada más y nada menos, lo mas facil es copiar uno que ya funcione o usar una herramienta que se llama HidDescriptorTool que te ayuda a crear ese descriptor).
Precisamente es en este descriptor donde se indica el "report" (o trama que se enviará en definitiva) donde hay que indicar que en vez de 6 teclas, se van a usar 12 (en mi caso).
Y al final final final del todo el último o ultimos descriptores que se negocian son los que definen los endpoints o canales de comunicación en el que se especifica el nº de canal, el modo de transferencia de la información, tamaño de la trama (tiene que coincidir con el tamaño de trama especificada en el descriptor HID).
Ummm, suficientemente liados?, yo también, igual en algún paso no soy todo lo explicito que me gustaría, supongo porque algunas cosas las tengo cojidas con alfileres.
Pues nada ese es el tema, que hay que conocerse muuuy bien el protocolo usb, los descriptores y todo lo que lleva asociado.
El resto de la aplicación es un bucle que controla las pulsaciones de los microinterruptores del mando con un sistema de debouncing que no se si será el mejor pero funciona, el resto es medio fácil de entender.
¿Dudas? ¿Preguntas? ¿Ruegos? (dinero tengo muy poco
)
Aquí viene el código, si esto no se puede hacer ruego a los moderadores que lo modifquen o me indiquen que debo hacer.
----------------------------------------- main.c ---------------------------------------------------------
#include <18F2550.h>
#device adc=8
#fuses HSPLL,PLL5,USBDIV,CPUDIV1,VREGEN,NOWDT,NOPROTECT,NOLVP,NODEBUG
#fuses WDT128, BROWNOUT, BORV20, NOPUT, NOCPD, STVREN
#fuses NOWRT, NOWRTD, IESO, FCMEN, NOPBADEN, NOWRTC, NOWRTB
#fuses NOEBTR, NOEBTRB, NOCPB, MCLR, LPT1OSC, NOXINST
#use delay(clock=48000000)
#use rs232(baud=19200,parity=N,xmit=PIN_C6,rcv=PIN_C7,bits=8,errors,stream=rs232)
#use fast_io(A)
#use fast_io(B)
/*
Variables de preprocesado para que la libreria usb del CCS se compile
de acuerdo a nuestra configuración usb, endpoints, etc.
*/
// 1 to use a PIC with an internal USB Peripheral
// 0 to use a National USBN960x peripheral
#define __USB_PIC_PERIF__ 1
#define USB_HID_DEVICE TRUE
#define USB_EP1_TX_ENABLE USB_ENABLE_INTERRUPT
#define USB_EP1_TX_SIZE 14 //max packet size of this endpoint
#define USB_EP1_RX_ENABLE USB_ENABLE_INTERRUPT //turn on EP1 for IN bulk/interrupt transfers
#define USB_EP1_RX_SIZE 8
/*
Includes de la libreria CCS, el orden es:
#include <hardware_layer.h>
#include <custom_config.h>
#include <usb.c>
Hardware layer dependerá del pic que estemos usando, para nuestro caso pic18_usb.h
Custom config incluirá los descriptores usb de nuestra aplicación, que se negociarán
en el proceso de enumeración.
*/
#include <pic18_usb.h>
#include "usb_descriptors.h"
#include <usb.c>
/*
Definitions
*/
#define P_UP PIN_A0
#define P_RIGHT PIN_A1
#define P_BOTTOM PIN_A2
#define P_LEFT PIN_A3
#define P_BTN1 PIN_B7
#define P_BTN2 PIN_B6
#define P_BTN3 PIN_B5
#define P_BTN4 PIN_B4
#define P_BTN5 PIN_B3
#define P_BTN6 PIN_B2
#define P_START PIN_B1
#define P_COIN PIN_B0
#define N_IO 12
int8 pins[N_IO] = {
P_UP, P_RIGHT, P_BOTTOM, P_LEFT,
P_BTN1, P_BTN2,P_BTN3, P_BTN4,
P_BTN5, P_BTN6, P_START, P_COIN
};
int8 keys[N_IO] = {
0x52,0x4F,0x51,0x50,
0xE4,0xE6,0x2C,0xE5,
0x1D,0x1B,0x1E,0x22
};
#define DB_COUNTER 10
int8 debouncing[N_IO] = {
DB_COUNTER,DB_COUNTER,DB_COUNTER,DB_COUNTER,
DB_COUNTER,DB_COUNTER,DB_COUNTER,DB_COUNTER,
DB_COUNTER,DB_COUNTER,DB_COUNTER,DB_COUNTER
};
int16 old_state = 0x0000;
int16 current_state = 0x0000;
/*
Function prototypes
*/
void SetUp( void );
void SetUpIO( void );
void SendKey( char KEY );
void ReceiveLeds( void );
static int8 send_buffer[USB_EP1_TX_SIZE];
static int8 receive_buffer[USB_EP1_RX_SIZE];
void main( void )
{
int8 I;
int8 J;
int8 changed = FALSE;
SetUp();
usb_init();
while ( 1 )
{
usb_task();
if( usb_enumerated() )
{
// First check the state of the IO pins, and later test if there are changes
// and if there are send a packet with the current pressed keys
for( I = 0; I < N_IO; I++ )
{
if ( input( pins[I] ) )
{
bit_set( current_state, I );
}
else
{
bit_clear( current_state, I );
}
}
// Now check the changes
memset( send_buffer, 0, sizeof(send_buffer) );
changed = FALSE;
J = 0;
for( I = 0; I < N_IO; I++ )
{
if ( bit_test( current_state, I ) != bit_test( old_state, I ) )
{
if( debouncing[I] == 0 )
{
// This is neccessary because if the keys are depressed we need to send
// at least an empty keys report
changed = TRUE;
if( bit_test( current_state, I ) )
{
bit_set( old_state, I );
// Add the key to the send buffer
send_buffer[2+J] = keys[I];
J++;
}
else
{
bit_clear( old_state, I );
// If it is depressed do nothing
}
debouncing[I] = DB_COUNTER;
}
else
{
debouncing[I]--;
}
}
}
if( changed )
{
// There are keys on the buffer
usb_put_packet(1, send_buffer, sizeof(send_buffer), USB_DTS_TOGGLE);
}
if( usb_kbhit(1) )
{
usb_get_packet(1, receive_buffer, sizeof(receive_buffer));
}
}
}
}
void SetUp( void )
{
setup_adc_ports(NO_ANALOGS|VSS_VDD);
setup_adc(ADC_OFF);
setup_spi(SPI_SS_DISABLED);
setup_wdt(WDT_OFF);
setup_timer_0(RTCC_OFF);
setup_timer_1(T1_DISABLED);
setup_timer_2(T2_DISABLED,0,1);
setup_timer_3(T3_DISABLED|T3_DIV_BY_1);
setup_comparator(NC_NC_NC_NC);
setup_vref(FALSE);
SetUpIO();
}
void SetUpIO( void )
{
set_tris_a(0xff);
set_tris_b(0xff);
}
El fichero usb_descriptors contiene los descriptores que se van a negociar, si veis algún ejemplo de los que trae el PICC, sabreis perfectamente que hay que incluir, como es código prácticamente de la compañia CCS, no lo voy a incluir aquí, pero es simplemente eso (copiad el archivo usb_desc_hid.h y renombradlo a usb_descriptors.h).
En fin, no se si esto le vendrá bien a alguien o no, igual acabo liandolo más
.
Nada lo dicho, un saludo y nos vemos.