Implementación de un dispositivo USB en STM32 desde cero 
El STM32F103 es un microcontrolador muy asequible que incluye interfaz USB 2.0. La mayoría de desarrollos USB realizados para esta serie de microcontroladores utiliza la librería STM32Cube, desarrollada por el propio fabricante, de libre uso y que abstrae de los entresijos del protocolo al programador. Abordar, sin embargo, el desarrollo de esta funcionalidad desde cero en este o en otros microcontroladores permite profundizar y mejorar en el conocimiento del propio protocolo USB.



Un repaso rápido del protocolo USB

Aunque aquí intentaré desgranar a grandes rasgos el protocolo, recomiendo siempre las dos grandes y mejores fuentes de información sobre el mismo:

USB made simple
USB in a nutshell

Es de lo mejorcito que hay al respecto por la red ya que el documento oficial es un poco infumable. A nivel eléctrico, se trata de un protocolo serie asíncrono que utiliza dos hilos de señal balanceada. El protocolo consiste en una serie de "endpoints" multiplexados en tiempo y enumerados. Hay tres tipos de endpoint:

Control: usado para transferencias de control del dispositivo. Identificación, configuración, etc.

Bulk: usado para transferencias masivas de datos con control de errores (menos ancho de banda).

Interrupt: usado para transferencias pequeñas de datos pero con tiempo mínimo de entrega garantizado.

Isochronous: usado para transferencias masivas de datos sin control de errores (máximo ancho de banda).

Cada endpoint tiene un número asociado y un tipo. El estándar USB reserva el endpoint 0 como un endpoint de control sobre el que el host (ordenador) envía los mensajes de configuración iniciales al dispositivo que acaba de conectarse.

La secuencia ya se describió en un post anterior en el que se abordó el mismo proyecto pero utilizando el microcontrolador ATmega32u4 de AVR pero la volvemos a indicar a continuación:

1. El host detecta que hay un dispositivo conectado (detecta una resistencia pull up en D+ o en D-)

2. El host inicia una secuencia de reset poniendo a nivel bajo las líneas D- y D+ durante al menos 2.5 us.

3. El host envía un paquete de SETUP para pedir el descriptor de dispositivo al dispositivo. Este descriptor indica el tipo de dispositivo, el código de fabricante, código de producto, etc. Esta primera petición se realiza siempre indicando en el campo longitud la longitud máxima y en la respuesta proveniente del dispositivo, el host es capaz de deducir el tamaño máximo de buffer con el que trabaja el dispositivo. Hay que tener en cuenta que en el caso de dospisitivos low-speed los paquetes son siempre de 8 bytes de datos mientras que en dispositivos full-speed los paquetes pueden ser de 8, 16, 32 o 64 bytes.

4. Tras esta primera petición de descripción de dispositivo el host suele iniciar de nuevo una condición de reset y, a continuación vuelve a pedir el descriptor de dispositivo pero con el tamaño ajustado al tamaño indicado por el dispositivo en la primera petición.

5. Cada dispositivo tiene asignada una dirección en el bus que, tras es reset, es siempre 0. En este instante lo habitual es que el host envíe un paquete de SETUP de tipo SET_ADDRESS para indicarle al dispositivo que a partir de ahora el host se va a comunicar con el dispositivo usando una dirección concreta diferente a 0 y que el dispositivo debe recordar para posteriores paquetes que se transmitan.

6. Ya con la nueva dirección de bus configurada, el host envía otro paquete de SETUP para solicitar el descriptor de configuración. Este descriptor es más grande que el anterior e incluye información sobre la clase de dipositivo y los endpoints que utiliza. El descriptor de dispositivo indica en un campo cuántas configuraciones posee el dispositivo, que suele ser siempre 1, por lo que el host normalmente sólo pide un descriptor de configuración.

7. El host (normalmente el driver instalado en el host) decide qué configuración quiere activar (que suele ser la única) en el dispositivo enviando un paquete de SETUP de tipo SET_CONFIGURATION. A partir de este instante el dispositivo queda conectado y con sus endpoints preparados para recibir y enviar datos propios de la funcionalidad del dispositivo.

La secuencia puede variar ligeramente en función del sistema operativo del host. Hay que recordar que en el protocolo USB el host es siempre el que envía "tokens" al dispositivo, incluso para traer datos desde el dispositivo. Cuando el host quiere enviar datos a un dispositivo hace transferencias de tipo SETUP y OUT mientras que cuando quiere recibir datos del dispositivo, el host hace transferencias de tipo IN pero siempre es el host el que pregunta. Un dispositivo no puede enviar datos a un host hasta que el host mande un token de tipo IN al dispositivo.

Implementación en el STM32F103

La serie STM32F103 es la serie más sencilla y baratita de toda la familia STM32 con soporte USB 2.0 full-speed (en el momento que escribo esto se puede conseguir una placa mínima de desarrollo con STM32F103 por menos de 3 ¤ en AliExpress). La documentación de referencia para programar el módulo USB es algo oscura y no está pensada para que te sumerjas mucho en ella, sino para que utilices la librería STM32Cube que, aunque es open source y permite un uso sin restricciones, su uso le quita toda la gracia al concepto de programar un microcontrolador desde cero :-).

A continuación puede verse la implementación de un dispositivo USB consistente en dos endpoints sencillos de tipo bulk (uno de entrada y otro de salida). La razón para implementar un dispositivo así es el hecho de que desde Linux el driver "usbserial" permite intercambiar datos con cualquier dispositivo USB que cumpla que tenga un endpoint bulk de salida y otro de entrada sin importar su clase, ni el código de fabricante ni de producto. Es un driver ideal para depurar dispositivos USB y que instancia un "/dev/ttyUSB0". Escribiendo en "/dev/ttyUSB0" se envían bytes a través del endpoint de salida mediante paquetes OUT mientras que leyendo de "/dev/ttyUSB0" se reciben bytes desde el dispositivo a través del endpoint de entrada mediante paquetes IN que envía el host.

La función usbDeviceInit se encarga de inicializar los tranceptores y de activar la interrupción de "USB Reset":

void usbDeviceInit() {
    // enable USB clock
    RCC_APB1ENR |= (((uint32_t) 1) << 23);
    // enable USB interrupts
    NVIC_ENABLE_IRQ(20);
    NVIC_SET_PRIORITY(20, 0);   // highest priority
    // enable analog transceivers
    USB_CNTR &= ~(((uint16_t) 1) << 1);
    for (uint32_t i = 0; i < 20000; i++)
        ;
    USB_CNTR &= ~(((uint16_t) 1) << 0);
    USB_ISTR = 0;
    // enable and wait for USB RESET interrupt
    USB_CNTR |= (((uint16_t) 1) << 10);
}

A continuación definimos los diferentes descriptores del dispositivo (descriptor de dispositivo, descriptor de configuración y descriptor de cadena 0 que indica los idiomas disponibles en el dispositivo):

const UsbDeviceDescriptor MyUsbDeviceDescriptor = {
    0x12,      // descriptor size
    0x01,      // descriptor type (device)
    0x0110,    // USB protocol version 1.10
    0x00,
    0x00,
    0x00,
    0x08,      // max packet size for control endpoint 0 = 8 bytes
    0xF055,    // vendor id
    0x0001,    // product id
    0x0100,
    0x00,
    0x00,
    0x00,
    0x01       // num configurations
};

...

const UsbConfigurationDescriptor MyUsbConfigurationDescriptor = {
    0x09,    // descriptor size
    0x02,    // descriptor type (configuration)
    0x0020,  // configuration (9) + interface (9) + endpoint (7) + endpoint (7) = 32
    0x01,    // num interfaces = 1
    0x01,    // this configuration number = 1
    0x00,
    0x80,    // bus powered (not self powered)
    0x20,    // 32 * 2 = 64 mA
    {           // interface descriptor
        0x09,   // descriptor size
        0x04,   // descriptor type (interface)
        0x00,   // interface number (zero based)
        0x00,
        0x02,   // num endpoints = 2
        0xFF,   // class = vendor defined
        0xFF,   // subclass = vendor defined
        0x00,
        0x00
    },
    {             // in endpoint descriptor
        0x07,     // descriptor size
        0x05,     // descriptor type (endpoint)
        0x81,     // in endpoint 1
        0x02,     // bulk endpoint
        0x0008,   // max packet size = 8 bytes
        0x0A      // 10 ms for polling interval
    },
    {             // out endpoint descriptor
        0x07,     // descriptor size
        0x05,     // descriptor type (endpoint)
        0x02,     // out endpoint 2
        0x02,     // bulk endpoint
        0x0008,   // max packet size = 8 bytes
        0x0A      // 10 ms for polling interval
    }
};

...

const UsbString0Descriptor MyUsbString0Descriptor = {
    0x04,            // descriptor size
    0x03,            // descriptor type (string descriptor)
    0x0409           // 'en_US' language id
};


const uint16_t MyUsbStatus = 0x0000;

Los buffers de recepción y transmisión USB en el caso de STM32 deben ser direccionados y accedidos de forma particular. Desde el punto de vista del subsistema USB, la anchura del bus de datos es de 16 bits, en lugar de 32 bits aunque los datos están alineados a 32 bits. Gráficamente se ve mejor:

Offset desde el punto de     Offset desde el punto de
vista del controlador USB vista del programa (CPU)
0 0
1 1
2 4
3 5
4 8
5 9
6 12


Como se puede ver, por cada palabra de 32 bits direccionada desde la CPU sólo se puede acceder a los 16 bits menos significativos. Para leer los 2 primeros bytes de la memoria USB desde la CPU hay que acceder a los 4 primeros bytes de dicha memoria (como si fuese un entero de 32 bits) y quedarnos con los 16 bits menos significativos. Los siguientes 2 bytes no están en los 16 bits más significativos de la primera palabra de 32 bits, sino en los 16 bits menos significativos de la siguiente palabra de 32 bits y así sucesivamente. Teniendo en cuenta esta particularidad se implementan dos funciones de acceso a esta memoria USB para copiar hacia y desde ella:

void usbCopyFromPacketSRAM(volatile uint32_t *packetSRAMSource, volatile uint16_t *destination, uint16_t bytes) {
    volatile uint32_t *p = (volatile uint32_t *) packetSRAMSource;
    volatile uint16_t *q = destination;
    uint16_t n = bytes >> 1;
    if (bytes & 1)
        n++;
    for (uint16_t i = 0; i < n; i++, p++, q++)
        *q = (uint16_t) (*p & 0x0000FFFF);
}


void usbCopyToPacketSRAM(volatile uint16_t *source, volatile uint32_t *packetSRAMDestination, uint16_t bytes) {
    volatile uint32_t *p = (volatile uint32_t *) packetSRAMDestination;
    volatile uint16_t *q = source;
    uint16_t n = bytes >> 1;
    if (bytes & 1)
        n++;
    for (uint16_t i = 0; i < n; i++, p++, q++)
        *p = (uint32_t) *q;
}

A continuación definimos las rutinas de interrupción correspondientes. Primero escribimos la rutina usbDeviceISRReset, que se ejecuta en caso de que se genere una interrupción de "USB Reset" provocada por una condición de reset en el bus USB. Dicha condición de reset es iniciada por el host en cuanto detecta un nuevo dispositivo conectado a una de sus bocas USB (es el paso 2 de la secuencia descrita anteriormente):

void usbDeviceISR()  __attribute__ ((section(".usblp")));

...

void usbDeviceISRReset() {
    // prepare buffer descriptor table for endpoint 0 (control)
    USB_BTABLE = 0;
    // endpoint 0 (bidireccional)
    USB_ADDR0_TX = 24;
    USB_COUNT0_TX = 0;   // 8
    USB_ADDR0_RX = 32;
    USB_COUNT0_RX = (((uint16_t) 4) << 10);   // 2 * 4 = 8 bytes
    // endpoint 1 (in, tx)
    USB_ADDR1_TX = 40;
    USB_COUNT1_TX = 0;   // 8
    USB_ADDR1_RX = 40;
    USB_COUNT1_RX = (((uint16_t) 4) << 10);   // 2 * 4 = 8 bytes
    // endpoint 2 (out, rx)
    USB_ADDR2_TX = 48;
    USB_COUNT2_TX = 0;   // 8
    USB_ADDR2_RX = 48;
    USB_COUNT2_RX = (((uint16_t) 4) << 10);   // 2 * 4 = 8 bytes
    // device address = 0
    USB_DADDR = ((uint16_t) 1) << 7;   // enable usb function
    usbNextAddress = 0;
    // prepare endpoint 0 for rx setup packets
    USB_EP0R = (((uint16_t) 1) << 9);
    usbDeviceEPRSetRxStat(USB_EP0R, STAT_NAK);
    usbDeviceEPRSetTxStat(USB_EP0R, STAT_NAK);
    usbDeviceEPRSetDtogRx(USB_EP0R, 0);
    usbDeviceEPRSetDtogTx(USB_EP0R, 0);
    // prepare endpoint 1 and endpoint 2
    USB_EP1R = 1;
    usbDeviceEPRSetRxStat(USB_EP1R, STAT_NAK);
    usbDeviceEPRSetTxStat(USB_EP1R, STAT_VAL);
    usbDeviceEPRSetDtogRx(USB_EP1R, 0);
    usbDeviceEPRSetDtogTx(USB_EP1R, 0);
    USB_EP2R = 2;
    usbDeviceEPRSetRxStat(USB_EP2R, STAT_VAL);
    usbDeviceEPRSetTxStat(USB_EP2R, STAT_NAK);
    usbDeviceEPRSetDtogRx(USB_EP2R, 0);
    usbDeviceEPRSetDtogTx(USB_EP2R, 0);
    // enable complete transfer interrupt
    USB_CNTR |= (((uint16_t) 1) << 15);
}

A continuación se define la rutina principal que atiende las interrupciones USB, usbDeviceISR. Esta función llama, en caso de darse una condición de reset a la función usbDeviceISRReset definida arriba:

void usbDeviceISR() {
    uint16_t istr = USB_ISTR;
    if (istr & (((uint16_t) 1) << 10)) {
        usbDeviceISRReset();
        USB_ISTR = 0;
    }
    else if (istr & (((uint16_t) 1) << 15)) {   // correct transfer interrupt
        USB_ISTR = 0;
        uint16_t epNum = istr & 0x000F;
        if (epNum == 0) {
            if (istr & 0x0010) {
                // out/setup packet
                if (USB_EP0R & (((uint16_t) 1) << 11)) {
                    // setup packet
                    usbCopyFromPacketSRAM((uint32_t *) USB_EP0RXBUF, usbRxBuffer, USB_COUNT0_RX & 0x03FF);
                    UsbSetupPacket *setupPacket = (UsbSetupPacket *) usbRxBuffer;
                    if ((setupPacket->bmRequestType == 0x80) && (setupPacket->bRequest == 0x06)) {
                        bool stall = false;
                        if ((setupPacket->wValue >> 8) == 1) {
                            ep0DataPtr = (uint8_t *) &MyUsbDeviceDescriptor;           // get_descriptor (device)
                            ep0DataCount = (sizeof(MyUsbDeviceDescriptor) < setupPacket->wLength) ? sizeof(MyUsbDeviceDescriptor) : setupPacket->wLength;
                        }
                        else if ((setupPacket->wValue >> 8) == 2) {
                            ep0DataPtr = (uint8_t *) &MyUsbConfigurationDescriptor;    // get_descriptor (configuration)
                            ep0DataCount = (sizeof(MyUsbConfigurationDescriptor) < setupPacket->wLength) ? sizeof(MyUsbConfigurationDescriptor) : setupPacket->wLength;
                        }
                        else if ((setupPacket->wValue >> 8) == 3) {
                            ep0DataPtr = (uint8_t *) &MyUsbString0Descriptor;    // get_descriptor (string)
                            ep0DataCount = (sizeof(MyUsbString0Descriptor) < setupPacket->wLength) ? sizeof(MyUsbString0Descriptor) : setupPacket->wLength;
                        }
                        else {
                            usart1SendString("\tg?");
                            usart1SendHexValue(setupPacket->wValue >> 8);
                            usart1SendString("\r\n");
                            ep0DataCount = 0;
                            stall = true;
                        }
                        if (stall)
                            usbDeviceEPRSetTxStat(USB_EP0R, STAT_STA);
                        else {
                            uint16_t size = (ep0DataCount > 8) ? 8 : ep0DataCount;     // copy bytes to packet SRAM
                            usbCopyToPacketSRAM((uint16_t *) ep0DataPtr, (uint32_t *) USB_EP0TXBUF, size);
                            USB_COUNT0_TX = size;
                            ep0DataCount -= size;
                            ep0DataPtr += size;
                            usbDeviceEPRSetTxStat(USB_EP0R, STAT_VAL);
                        }
                        usbDeviceEPRSetRxStat(USB_EP0R, STAT_STA);
                    }
                    else if ((setupPacket->bmRequestType == 0x00) && (setupPacket->bRequest == 0x05)) {
                        usbNextAddress = setupPacket->wValue;
                        USB_COUNT0_TX = 0;
                        usbDeviceEPRSetTxStat(USB_EP0R, STAT_VAL);
                        usbDeviceEPRSetRxStat(USB_EP0R, STAT_STA);
                    }
                    else if ((setupPacket->bmRequestType == 0x00) && (setupPacket->bRequest == 0x09)) {
                        USB_COUNT0_TX = 0;
                        usbDeviceEPRSetTxStat(USB_EP0R, STAT_VAL);
                        usbDeviceEPRSetRxStat(USB_EP0R, STAT_STA);
                    }
                    else if ((setupPacket->bmRequestType == 0x80) && (setupPacket->bRequest == 0x00)) {
                        usbCopyToPacketSRAM((uint16_t *) &MyUsbStatus, (uint32_t *) USB_EP0TXBUF, 2);
                        USB_COUNT0_TX = 2;
                        usbDeviceEPRSetTxStat(USB_EP0R, STAT_VAL);
                        usbDeviceEPRSetRxStat(USB_EP0R, STAT_STA);
                    }
                    else {
                        usart1SendString("\tother setup packet\r\n");
                        usart1SendString("x: ");
                        usart1SendHexValue(setupPacket->bmRequestType);
                        usart1SendString(" ");
                        usart1SendHexValue(setupPacket->bRequest);
                        usart1SendString("\r\n");
                    }
                }
                else {
                    // out packet
                    usbCopyFromPacketSRAM((uint32_t *) USB_EP0RXBUF, usbRxBuffer, USB_COUNT0_RX & 0x03FF);
                    // TODO process data
                }
            }
            else {
                // in packet
                if (usbNextAddress != 0) {
                    USB_DADDR = (((uint16_t) 1) << 7) | (usbNextAddress & 0x007F);
                    usbNextAddress = 0;
                }
                else {
                    uint16_t size = (ep0DataCount > 8) ? 8 : ep0DataCount;
                    usbCopyToPacketSRAM((uint16_t *) ep0DataPtr, (uint32_t *) USB_EP0TXBUF, size);
                    USB_COUNT0_TX = size;
                    ep0DataCount -= size;
                    ep0DataPtr += size;
                    usbDeviceEPRSetTxStat(USB_EP0R, STAT_VAL);
                    usbDeviceEPRSetRxStat(USB_EP0R, STAT_VAL);
                }
            }
            USB_EP0R &= 0x0F0F;    // ctr_rx = 0, ctr_tx = 0
        }
        else if (epNum == 1) {
            if (istr & 0x0010) {
                // out packet
            }
            else {
                // in packet 
            }
            usbDeviceEPRSetTxStat(USB_EP1R, STAT_VAL);
            usbDeviceEPRSetRxStat(USB_EP1R, STAT_NAK);
            USB_EP1R &= 0x0F0F;    // ctr_rx = 0, ctr_tx = 0
        }
        else if (epNum == 2) {
            if (istr & 0x0010) {
                // out packet
                usbCopyFromPacketSRAM((uint32_t *) USB_EP2RXBUF, usbRxBuffer, USB_COUNT2_RX & 0x03FF);
                usart1SendString("rx '");
                usart1SendBytes((uint8_t *) usbRxBuffer, USB_COUNT2_RX & 0x03FF);
                usart1SendString("'\r\n");
            }
            else {
                // in packet 
            }
            usbDeviceEPRSetTxStat(USB_EP2R, STAT_NAK);
            usbDeviceEPRSetRxStat(USB_EP2R, STAT_VAL);
            USB_EP2R &= 0x0F0F;    // ctr_rx = 0, ctr_tx = 0
        }
        //USB_ISTR = 0;
    }
}

Lo primero que hace la rutina es identificar el endpoint por el que se ha producido la transacción. En caso de que la transacción se haya producido a través del endpoint 0 se comprueba si es un token SETUP u OUT y, si es un token SETUP, se parsea y se mira a ver si el host está mandando algo (configuraciones) o si lo está pidiendo (descriptores). Si el host está pidiendo algo, hay que rellenar el buffer de transmisión con los datos que necesita, pues la siguiente transacción que realizará el host a través del endpoint 0 será utilizando uno o varios tokens IN y para entonces los datos tienen que estar ya preparados en dicho búffer.

Recordemos algunos elementos básicos sobre cómo son las transferencias USB a través del endpoint 0:

Control: Es un endpoint que debe ser siempre configurado como de tipo "Control" y es bidireccional. Un dispositivo puede definir endpoints de control adicionales pero el endpoint 0 de control siempre debe estar disponible.

Transacciones SETUP: Los endpoints configurados como de control permiten transferir un tipo especial de token denominado SETUP. Este token puede ser de entrada o de salida (siempre desde el punto de vista del host).

Transacciones SETUP de salida: El host manda un token SETUP, a continuación envía cero o más tokens OUT con datos anexos y por último manda un token IN para que el dispositivo mande 0 bytes a modo de ACK.

Transacciones SETUP de entrada: El host manda un token SETUP, a continuación envía cero o más tokens IN para recibir datos del dispositivo y al final el host manda un token OUT con 0 bytes anexos a modo de ACK hacia el dispositivo.

Si la transacción se ha producido en el endpoint 1 o 2, se asume que es una transacción simple de tipo bulk:

Endpoint 1: Es un endpoint configurado como de tipo IN y en esta implementación no hace nada, pues el STM32 no manda ningún dato cuando es leido a través del USB.
Endpoint 2: Es un endpoint configurado como de tipo OUT. Por lo tanto, el STM32 recibe por aquí los datos que son enviados desde el host y los manda formateados a través de la USART.

Nos limitamos a mandar por la USART1 todo lo que entra a través del endpoint de tipo "bulk out", mientras que las lecturas desde el host al endpoint de tipo "bulk in" devuelven siempre 0 bytes.

Para cargar el módulo "usbserial" en el kernel simplemente hay que hacer:
modprobe usbserial vendor=0xf055 product=0x0001

Esto nos permite comunicarnos con el dispositivo desde la misma shell:
echo "Hola, caracola" > /dev/ttyUSB1

Partiendo de este código se pueden implementar multitud de dispositivos USB en este microcontrolador (Mass storage, HID, DFU, etc.). Todo el código fuente puede descargarse desde la sección soft.

Quiero agradecer a Jian Jiao (mculabs.net) la ayuda prestada a la hora de comprender algunos entresijos en la programación del módulo USB del microcontrolador STM32.

Comentarios 
Lo sentimos. No se permiten nuevos comentarios después de 90 días.