Программирование ARM: работа с USB USB in a NutShell - путеводитель по стандарту USB (окончание) Fri, December 13 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


USB in a NutShell - путеводитель по стандарту USB (окончание) Печать
Добавил(а) microsin   

[Глава 8: пример Firmware]

Firmware - PIC16F876, управляющий PDIUSBD11

Мы начнем наши примеры с устройства фирмы Philips PDIUSBD11 (I2C Serial USB Device), подключенного к микроконтроллеру PIC16F876 компании MicroChip (показан на схеме) или Microchip PIC16F877 (чип в бОльшем корпусе на 40 ножек). Несмотря на то, что у Microchip есть два low speed USB микроконтроллера PIC16C745 и PIC16C765, у них память программ только OTP, и нет поддержки внутрисхемной отладки - In-Circuit Debugging (ICD), а это никак не способствует нормальной разработке firmware. В настоящий момент микросхема Philips PDIUSBD11, подключенная к PIC16F876, дает те же возможности - Flash и внутрисхемную отладку.

USBinNutShell-pic16f876.gif
Схема необходимого железа показана выше на рисунке. Пример проходит энумерацию и позволяет прочитать аналоговое напряжение с пяти мультиплексированных входов ADC микроконтроллера PIC16F876. Код совместим с PIC16F877, позволяющим максимум 8 аналоговых каналов. Светодиод LED, подключенный к ножке порта RB3, горит, когда устройство сконфигурировано. Регулятор 3.3V не показан, но он необходим для PDIUSBD11. Если Вы запускаете этот пример схемы от внешнего источника питания, то можете использовать обычный регулятор 78L033 на 3.3V, однако если Вы хотите запустить устройство в режиме Bus Powered USB device (устройство, питаемое от шины USB), то Вам нужен регулятор с низким падением напряжения.

Отладка может быть осуществлена путем соединения TXD (ножка 17) к драйверу RS-232 и подключения к компьютеру на скорости 115,200 бит/сек. Операторы printf добавлены для отображения процесса энумерации.

Код написан на C и скомпилирован компилятор Hi-Tech PICC Compiler [4]. Имеется для загрузки демонстрационная версия компилятора PICC demo version (7.86 PL4), которая работает 30 дней. Скомпилированный HEX-файл имеется в архиве. Он скомпилирован для использования с ICD (или без него).

#include < pic.h >
#include < stdio.h >
#include < string.h >
#include "usbfull.h"
 
const USB_DEVICE_DESCRIPTOR DeviceDescriptor =
{
    sizeof(USB_DEVICE_DESCRIPTOR), /* bLength */
    TYPE_DEVICE_DESCRIPTOR,        /* bDescriptorType */
    0x0110,                        /* bcdUSB USB Version 1.1 */
    0,                             /* bDeviceClass */
    0,                             /* bDeviceSubclass */
    0,                             /* bDeviceProtocol */
    8,                             /* bMaxPacketSize 8 Bytes */
    0x04B4,                        /* idVendor (Cypress Semi) */
    0x0002,                        /* idProduct (USB Thermometer Example) */
    0x0000,                        /* bcdDevice */
    1,                             /* iManufacturer String Index */
    0,                             /* iProduct String Index */
    0,                             /* iSerialNumber String Index */
    1                              /* bNumberConfigurations */
};

Все структуры заданы в заголовочном файле (header, файл с расширением *.h). Мы получили этот пример на основе примера от Cypress - USB термометр, который Вы можете использовать вместе с USB Driver для Cypress USB Starter Kit [5]. Новый generic-драйвер написан для поддержки этого и других примеров, которые могут также скоро появиться. Только одна строка представлена для отображения производителя. Это иллюстрирует, как реализовать строковые дескрипторы без переполнения всей кодовой памяти устройства. Описание дескриптора устройства и его полей могут быть найдено здесь.

const USB_CONFIG_DATA ConfigurationDescriptor =
{
    {                              /* configuration descriptor */
    sizeof(USB_CONFIGURATION_DESCRIPTOR), /* bLength */
    TYPE_CONFIGURATION_DESCRIPTOR, /* bDescriptorType */
    sizeof(USB_CONFIG_DATA),       /* wTotalLength */
    1,                             /* bNumInterfaces */
    1,                             /* bConfigurationValue */
    0,                             /* iConfiguration String Index */
    0x80,                          /* bmAttributes Bus Powered, No Remote Wakeup */
    0x32                           /* bMaxPower, 100mA */
    },
    {                              /* interface descriptor */
    sizeof(USB_INTERFACE_DESCRIPTOR), /* bLength */
    TYPE_INTERFACE_DESCRIPTOR,     /* bDescriptorType */
    0,                             /* bInterface Number */
    0,                             /* bAlternateSetting */
    2,                             /* bNumEndpoints */
    0xFF,                          /* bInterfaceClass (Vendor specific) */
    0xFF,                          /* bInterfaceSubClass */
    0xFF,                          /* bInterfaceProtocol */
    0                              /* iInterface String Index */
    },
    {                              /* endpoint descriptor */
    sizeof(USB_ENDPOINT_DESCRIPTOR), /* bLength */
    TYPE_ENDPOINT_DESCRIPTOR,      /* bDescriptorType */
    0x01,                          /* bEndpoint Address EP1 OUT */
    0x02,                          /* bmAttributes - Interrupt */
    0x0008,                        /* wMaxPacketSize */
    0x00                           /* bInterval */
    },
    {                              /* endpoint descriptor */
    sizeof(USB_ENDPOINT_DESCRIPTOR), /* bLength */
    TYPE_ENDPOINT_DESCRIPTOR,      /* bDescriptorType */
    0x81,                          /* bEndpoint Address EP1 IN */
    0x02,                          /* bmAttributes - Interrupt */
    0x0008,                        /* wMaxPacketSize */
    0x00                           /* bInterval */
    }
};

Описание дескриптора конфигурации и их полей можно найти здесь. Мы сделали 2 дескриптора конечной точки в добавок к заданному по умолчанию каналу. Конечная точка EP1 OUT - 8 байт максимум Bulk OUT Endpoint, и конечная точка EP1 IN - 8 байт максимум Bulk IN Endpoint. Наш пример читает данные из конечной точки Bulk OUT и помещает их в кольцевой буфер из 80 байт. Посылка пакета IN к конечной точке EP1 читает 8-байтовые куски памяти из этого кольцевого буфера.

LANGID_DESCRIPTOR LANGID_Descriptor =
{ /* LANGID String Descriptor Zero */
    sizeof(LANGID_DESCRIPTOR),         /* bLenght */
    TYPE_STRING_DESCRIPTOR,            /* bDescriptorType */
    0x0409                             /* LANGID US English */
};
const MANUFACTURER_DESCRIPTOR Manufacturer_Descriptor = { /* ManufacturerString 1 */ sizeof(MANUFACTURER_DESCRIPTOR), /* bLenght */ TYPE_STRING_DESCRIPTOR, /* bDescriptorType */ "B\0e\0y\0o\0n\0d\0 \0L\0o\0g\0i\0c\0" /* ManufacturerString in UNICODE */ };

Строковый дескриптор с индексом 0 сделан для поддержки требований LANGID для строковых дескрипторов USB. Это указывает, что все описатели находятся на американском английском языке. Дескриптор производителя (Manufacturer Descriptor) может быть небольшим обманом, поскольку размер символьного массива фиксированно задан в заголовке и не динамичный.

#define MAX_BUFFER_SIZE 80
bank1 unsigned char circularbuffer[MAX_BUFFER_SIZE]; unsigned char inpointer; unsigned char outpointer;
unsigned char *pSendBuffer; unsigned char BytesToSend; unsigned char CtlTransferInProgress; unsigned char DeviceAddress; unsigned char DeviceConfigured;
#define PROGRESS_IDLE 0 #define PROGRESS_ADDRESS 3
void main (void) { TRISB = 0x03; /* Int & Suspend Inputs */ RB3 = 1; /* Устройство не сконфигурировано (LED) */ RB2 = 0; /* Сброс PDIUSBD11 */
InitUART(); printf("Initialising\n\r"); I2C_Init();
RB2 = 1; /* Вывод PDIUSBD11 из сброса */
ADCON1 = 0x80; /* Управление ADC - все 8 каналов разрешены, */ /* поддержка обновления для 16F877 */ USB_Init(); printf("PDIUSBD11 готова к соединению\n\r"); while(1) { if (!RB0) { D11GetIRQ(); /* Если IRQ в нуле, то на PDIUSBD11 имеется событие прерывания */ } } }

Функция main зависит от конкретного примера. Она отвечает за инициализацию входов и выходов портов ввода/вывода, инициализацию интерфейса I2C, ADC и PDIUSBD11. Как только все сконфигурировано, обработчик D11GetIRQ обрабатывает запросы на прерывание PDIUSBD11.

void D11GetIRQ(void)
{
        unsigned short Irq;
        unsigned char Buffer[1];
  
        /* Чтение регистра прерывания, чтобы определить его источник */
          D11CmdDataRead(D11_READ_INTERRUPT_REGISTER, (unsigned char*)&Irq, 2);
  
        if (Irq) printf("Irq = 0x%X: ",Irq);

Функция USB_Init инициализирует PDIUSBD11. Эта процедура инициализации опущена в даташите Philips на PDIUSBD11, но доступна в FAQ. Последняя команда разрешает мягкое подключение pull-up резистора на сигнале D+, что указывает хосту на подключение устройства USB на полной скорости (full speed), и представляет появление устройства USB на шине.

void USB_Init(void)
{
    unsigned char Buffer[2];
  
    /* Запрет функции хаба в PDIUSBD11 */
    Buffer[0] = 0x00;
    D11CmdDataWrite(D11_SET_HUB_ADDRESS, Buffer, 1);
  
    /* Установка адреса в 0 (по умолчанию) и разрешение функционирования */
    Buffer[0] = 0x80;
      D11CmdDataWrite(D11_SET_ADDRESS_ENABLE, Buffer, 1);
  
    /* Разрешение работы стандартных конечных точек */
    Buffer[0] = 0x02;
      D11CmdDataWrite(D11_SET_ENDPOINT_ENABLE, Buffer, 1);
  
    /* Установка режима - разрешение SoftConnect */
    Buffer[0] = 0x97; /* встроенная функция, SoftConnect, Clk Run, No LazyClk, Remote Wakeup */
    Buffer[1] = 0x0B; /* CLKOut = 4MHz */
    D11CmdDataWrite(D11_SET_MODE, Buffer, 2);
}

Main() осуществляет вызовы D11GetIRQ в цикле. Эта функция читает регистр прерывания PDIUSBD11, если какие-нибудь прерывания находятся в ожидании обработки. В этом случае они обрабатываются, иначе цикл продолжается. Другие устройства USB могут иметь несколько векторов прерывания, назначенных на каждую конечную точку. В этом случае каждый обработчик прерывания ISR будет обрабатывать отдельное прерывание, что позволяет убрать операторы if.

if (Irq & D11_INT_BUS_RESET) 
{
    printf("Bus Reset\n\r");
    USB_Init();
}
  
if (Irq & D11_INT_EP0_OUT) 
{
    printf("EP0_Out: ");
    Process_EP0_OUT_Interrupt();
}
  
if (Irq & D11_INT_EP0_IN) 
{
    printf("EP0_In: \n\r");
    if (CtlTransferInProgress == PROGRESS_ADDRESS) 
    {
        D11CmdDataWrite(D11_SET_ADDRESS_ENABLE,&DeviceAddress,1);
        D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP0_IN, Buffer, 1);
        CtlTransferInProgress = PROGRESS_IDLE;
    }
    else
    {
        D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP0_IN, Buffer, 1);
        WriteBufferToEndPoint();
    }
}

Условные операторы работают вниз в порядке приоритета. Наивысший приоритет у прерывания bus reset (сброс шины). Оно просто вызывает функцию USB_Init, которая переинициализирует функцию USB. Следующий наивысший приоритет – канал по умолчанию, основанный на конечной точке ноль - EP0 OUT и EP0 IN. Здесь обрабатываются энумерация и все управляющие запросы. Мы вызываем другую функцию для обработки запросов EP0_OUT.

Когда запрос сделан хостом, и он хочет принять данные, PIC16F876 отправит чипу PDIUSBD11 пакет из 8 байт. Поскольку шина USB управляется хостом, то микросхема PDIUSBD11 не может отправить данные когда пожелает, она буферизирует данные и ждет токена IN, который будет отправлен хостом. Когда PDIUSBD11 примет токен IN, она генерирует прерывание. В этот момент нужно загрузить новый пакет данных для отправки, что и происходит с помощью дополнительной функции WriteBufferToEndpoint().

Секция CtlTransferInProgress == PROGRESS_ADDRESS обрабатывает установку адреса устройства. Это мы обсудим позднее.

if (Irq & D11_INT_EP1_OUT) 
{
    printf("EP1_OUT\n\r");
    D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP1_OUT, Buffer, 1);
    bytes = D11ReadEndpoint(D11_ENDPOINT_EP1_OUT, Buffer);
    for (count = 0; count < bytes; count++) 
    {
        circularbuffer[inpointer++] = Buffer[count];
        if (inpointer >= MAX_BUFFER_SIZE) 
            inpointer = 0;
    }
    loadfromcircularbuffer(); //Kick Start
}
    
if (Irq & D11_INT_EP1_IN) 
{
    printf("EP1_IN\n\r");
    D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP1_IN, Buffer, 1);
    loadfromcircularbuffer();
}

Конечные точки EP1 OUT и EP1 IN реализованы для чтения и записи данных bulk в и из кольцевого буфера. Настройка позволяет использовать код в соединении с примером BulkUSB из Windows DDK. Кольцевой буфер задан ранее в коде как 80 байт, по длине занимающий весь bank1 PIC16F876 RAM.

if (Irq & D11_INT_EP2_OUT)
{
    printf("EP2_OUT\n\r");
    D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP2_OUT, Buffer, 1);
    Buffer[0] = 0x01; /* Stall конечной точки */
    D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + D11_ENDPOINT_EP2_OUT, Buffer, 1);
}
  
if (Irq & D11_INT_EP2_IN)
{
    printf("EP2_IN\n\r");
    D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP2_IN, Buffer, 1);
    Buffer[0] = 0x01; /* Stall конечной точки */
    D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + D11_ENDPOINT_EP2_IN, Buffer, 1);
    }
  
if (Irq & D11_INT_EP3_OUT)
{
    printf("EP3_OUT\n\r");
    D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP3_OUT, Buffer, 1);
    Buffer[0] = 0x01; /* Stall конечной точки */
    D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + D11_ENDPOINT_EP3_OUT, Buffer, 1);
}
  
if (Irq & D11_INT_EP3_IN)
{
    printf("EP3_IN\n\r");
    D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP3_IN, Buffer, 1);
    Buffer[0] = 0x01; /* Stall конечной точки */
    D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + D11_ENDPOINT_EP3_IN, Buffer, 1);
}

Конечные точки 2 и 3 в настоящий момент не используются, поэтому мы их приостанавливаем (stall), если будут приняты любые данные. PDIUSBD11 имеет команду Set Endpoint Enable, которую можно использовать для разрешения или запрещения обычных конечных точек (т. е. любых конечных точек, отличных от нулевой, составляющей по умолчанию канал управления). Мы можем использовать эту команду, чтобы отключить обычные конечные точки, которые мы не хотим использовать. В настоящий момент код предоставляет фундамент для дальнейшего усовершенствования.

void Process_EP0_OUT_Interrupt(void)
{
    unsigned long a;
    unsigned char Buffer[2];
    USB_SETUP_REQUEST SetupPacket;
  
    /* Проверка принятого пакета на Setup или Data, вместе с очисткой IRQ */
    D11CmdDataRead(D11_READ_LAST_TRANSACTION + D11_ENDPOINT_EP0_OUT, &SetupPacket, 1);
  
    if (SetupPacket.bmRequestType & D11_LAST_TRAN_SETUP) 
    {

Первое, что мы должны проделать – определить, является ли пакет принятый на EP0 Out пакетом данных или пакетом настройки (Setup Packet). Setup Packet содержит запрос, такой как Get Descriptor, где пакет данных содержит данные для предыдущего запроса. Нам повезло, что большинство запросов не отправляют пакеты данных от хоста в устройство. Запрос, который это делает, является запросом SET_DESCRIPTOR, но он редко осуществляется.

        /* Это пакет установки (Setup Packet) - чтение пакета */
        D11ReadEndpoint(D11_ENDPOINT_EP0_OUT, &SetupPacket);
  
        /* Подтверждение Setup Packet в конечную точку EP0_OUT
           & очистка буфера */
        D11CmdDataWrite(D11_ACK_SETUP, NULL, 0);
        D11CmdDataWrite(D11_CLEAR_BUFFER, NULL, 0);
/* Acknowlegde Setup Packet в конечную точку EP0_IN */ D11CmdDataWrite(D11_ENDPOINT_EP0_IN, NULL, 0); D11CmdDataWrite(D11_ACK_SETUP, NULL, 0);
/* Парсинг bmRequestType */ switch (SetupPacket.bmRequestType & 0x7F) {

Как мы уже рассматривали в писании управляющих передач (Control Transfers), пакет setup не может получать в ответ NAK или STALL. Когда микросхема PDIUSBD11 получает пакет Setup, она очищает (flush) буфер EP0 IN и запрещает команды Validate Buffer и Clear Buffer. Это гарантирует, что пакет setup будет подтвержден микроконтроллером путем отправки команды подтверждения настройки (Acknowledge Setup) в обе конечные точки EP0 IN и EP0 OUT до того, как эффективна команда Validate или Clear Buffer. Прием пакета setup также выводит управляющую контрольную точку 0 из состояния STALL, если она была остановлена.

Как только пакет прочитался в память и пакет setup подтвержден, мы начинаем анализировать запрос, начиная с определения типа запроса. Пока нас не интересует направление передачи, и мы операцией AND отключаем этот бит. Три запроса, на которые все устройства должны ответить - Standard Device Request, Standard Interface Request и Standard Endpoint Request. Мы реализовали наш функционал (чтение входов ADC) в запросе вендора (Vendor Request) – мы добавили оператор выбора (case) в запросы Standard Vendor. Если наше устройство поддерживает спецификацию стандартного класса USB, нам также необходимо добавить операторы case для Class Device Request, Class Interface Request и/или Class Endpoint Request.

            case STANDARD_DEVICE_REQUEST:
                printf("Standard Device Request ");
                switch (SetupPacket.bRequest) 
                {
                case GET_STATUS:
                    /* Получение Status Request устройства, который должен
                       быть возвращен, состояния Remote Wakeup и Self Powered */
                    Buffer[0] = 0x01;
                    Buffer[1] = 0x00;
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, Buffer, 2);
                    break;
                case CLEAR_FEATURE:
                case SET_FEATURE:
                    /* Мы не поддерживаем DEVICE_REMOTE_WAKEUP или TEST_MODE */
                    ErrorStallControlEndPoint();
                    break;

Запрос Get Status используется для сообщении о состоянии устройства в смысле питается ли устройство от шины или имеет собственный источник питания, и поддерживает ли устройство функцию удаленного пробуждения хоста (remote wakeup). В нашем устройстве мы сообщаем, что устройство имеет собственный источник питания (self powered, оно не питается от шины USB), и устройство не поддерживает remote wakeup.

На запросы Device Feature это устройство ответит, что не поддерживает ни DEVICE_REMOTE_WAKEUP, ни TEST_MODE, и вернет ошибку запроса USB Request Error.

                case SET_ADDRESS:
                    printf("Set Address\n\r");
                    DeviceAddress = SetupPacket.wValue | 0x80;
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, NULL, 0);
                    CtlTransferInProgress = PROGRESS_ADDRESS;
                    break;

Только команда Set Address выполняет обработку после стадии status. Все другие команды должны завершить обработку перед стадией status. Адрес устройства читается, по операции OR на него накладывается константа 0x80, и результат сохраняется в переменной DeviceAddress. Операция OR 0x80 – специфична для микросхемы PDIUSBD11, у неё старший бит адреса задает, разрешено устройство или нет. В качестве статуса на хост возвращается пакет нулевой длины, что сигнализирует ему об успешном завершении команды. Однако хост должен отправить токен IN, получить пакет нулевой длины и выдать ACK перед тем, как мы можем поменять адрес. Иначе устройство может не увидеть токена IN, отправленный на адрес по умолчанию (нулевой адрес).

Завершение стадии status сигнализируется прерыванием на конечной точке EP0 IN. Чтобы разделить ответ на установку адреса и обычное прерывание EP0_IN, мы установим переменную CtlTransferInProgress в значение PROGRESS_ADDRESS. В обработчике EP0 IN проверяется переменная CtlTransferInProgress. Если она равна PROGRESS_ADDRESS, то для PDIUSBD11 выдается команда Set Address Enable и CtlTransferInProgress устанавливается в значение PROGRESS_IDLE. Хост дает 2 мс на смену адреса устройством, перед тем, как отправить какую-нибудь другую команду.

                case GET_DESCRIPTOR:
                    GetDescriptor(&SetupPacket);
                    break;
                case GET_CONFIGURATION:
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, &DeviceConfigured, 1);
                    break;
                case SET_CONFIGURATION:
                    printf("Set Configuration\n\r");
                    DeviceConfigured = SetupPacket.wValue & 0xFF;
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, NULL, 0);
                    if (DeviceConfigured)
                    {
                        RB3 = 0;
                        printf("\n\r *** Device Configured *** \n\r");
                    }
                    else 
                    {
                        RB3 = 1; /* устройство не сконфигурировано */
                        printf("\n\r ** Device Not Configured *** \n\r");
                    }
                    break;
                //case SET_DESCRIPTOR:
                default:
                    /* не поддерживается - ошибка запроса - остановка Stall */
                    ErrorStallControlEndPoint();
                    break;
                }
                break;

Запросы Get Configuration и Set Configuration используются для того, чтобы "разрешить" устройство USB, что позволит передавать данные на конечные точки, отличные от конечной точки 0. Set Configuration должен быть выдан с полем wValue, равным соответствующему значению поля bConfigurationValue нужной конфигурации, которую Вы хотите разрешить. В нашем случае имеется только одна конфигурация – конфигурация 1. Нулевое значение для конфигурации означает, что устройство не сконфигурировано, а ненулевое значение означает, что устройство сконфигурировано. Код не делает полную проверку типа значения конфигурации, он просто копирует значение в локальную переменную для сохранения - DeviceConfigured. Если поле wValue не соответствует полю bConfigurationValue конфигурации, то необходимо возвратить USB Request Error.

        case STANDARD_INTERFACE_REQUEST:
            printf("Standard Interface Request\n\r");
            switch (SetupPacket.bRequest) 
            {
            case GET_STATUS:
                /* получение возвращаемого Status Request для интерфейса */
                /* нули (зарезервировано для дальнейшего использования) */
                Buffer[0] = 0x00;
                Buffer[1] = 0x00;
                D11WriteEndpoint(D11_ENDPOINT_EP0_IN, Buffer, 2);
                break;
            case SET_INTERFACE:
                /* по умолчанию поддерживается только устройство, состояние Stall возможно */
                /* returned in the status stage of the request */
                if (SetupPacket.wIndex == 0 && SetupPacket.wValue == 0) 
                    /* Interface Zero, Alternative Setting = 0 */
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, NULL, 0);    
                else 
                    ErrorStallControlEndPoint();
                break;
            case GET_INTERFACE:
                if (SetupPacket.wIndex == 0) 
                { /* Interface Zero */
                    Buffer[0] = 0; /* Alternative Setting */
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, Buffer, 1);
                    break;
                } /* иначе падение по RequestError */
            //case CLEAR_FEATURE:
            //case SET_FEATURE:
                /* На интерфейсе нет заданных опций (features). Возврат RequestError */
            default:
                ErrorStallControlEndPoint();
                break;
            }
            break;

Для стандартных запросов к интерфейсу (Standard Interface Requests) не выполняется никаких реальных функций. Запрос Get Status должен вернуть нулевое слово – зарезервировано для использования в будущем. Запросы Set Interface и Get Interface используются с альтернативными дескрипторами интерфейса. У нас не заданы никакие альтернативные дескрипторы интерфейса, поэтому Get Interface возвращает 0 и любой запрос на установку интерфейса, не равного интерфейсу 0 с альтернативной установкой 0 обрабатывается с ошибкой запроса Request Error.

        case STANDARD_ENDPOINT_REQUEST:
            printf("Standard Endpoint Request\n\r");
            switch (SetupPacket.bRequest) 
            {
            case CLEAR_FEATURE:
            case SET_FEATURE:
                /* особенность Halt(Stall) нужно реализовать на всех прерываниях и */
                /* конечных точек Bulk. Это не требуется, не рекомендовано на
                   Default Pipe (канал по умолчанию) */
                if (SetupPacket.wValue == ENDPOINT_HALT)
                {
                    if (SetupPacket.bRequest == CLEAR_FEATURE) 
                        Buffer[0] = 0x00;
                    else
                        Buffer[0] = 0x01;
                    switch (SetupPacket.wIndex & 0xFF) 
                    {
                    case 0x01:
                        D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP1_OUT, Buffer, 1);
                        break;
                    case 0x81:
                        D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP1_IN, Buffer, 1);
                        break;
                    case 0x02:
                        D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP2_OUT, Buffer, 1);
                        break;
                    case 0x82:
                        D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP2_IN, Buffer, 1);
                        break;
                    case 0x03:
                        D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP3_OUT, Buffer, 1);
                        break;
                    case 0x83:
                        D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP3_IN, Buffer, 1);
                        break;
                    default:
                        /* Invalid Endpoint - RequestError */
                        ErrorStallControlEndPoint();
                        break;
                    }
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, NULL, 0);
                }
                else 
                {
                    /* Нет других опций на конечной точке - ошибка запроса */
                    ErrorStallControlEndPoint();
                }
                break;

Запросы Set Feature и Clear Feature используются для специфичных фич конечных точек. Стандарт задает один селектор фичи - ENDPOINT_HALT. Мы соответственно проверяем запрос на установку/сброс бита STALL. Фича HALT не требуется для конечной точки по умолчанию (конечной точки 0).

                case GET_STATUS:
                    /* Получение Status Request для конечной точки, который нужно вернуть */
                    /* Halt Status в D0 для Interrupt и Bulk */
                    switch (SetupPacket.wIndex & 0xFF) 
                    {
                    case 0x01:
                        D11CmdDataRead(D11_READ_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP1_OUT, Buffer, 1);
                        break;
                    case 0x81:
                        D11CmdDataRead(D11_READ_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP1_IN, Buffer, 1);
                        break;
                    case 0x02:
                        D11CmdDataRead(D11_READ_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP2_OUT, Buffer, 1);
                        break;
                    case 0x82:
                        D11CmdDataRead(D11_READ_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP2_IN, Buffer, 1);
                        break;
                    case 0x03:
                        D11CmdDataRead(D11_READ_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP3_OUT, Buffer, 1);
                        break;
                    case 0x83:
                        D11CmdDataRead(D11_READ_ENDPOINT_STATUS + \
                        D11_ENDPOINT_EP3_IN, Buffer, 1);
                        break;
                    default: 
                        /* Invalid Endpoint - RequestError */
                        ErrorStallControlEndPoint();
                        break;
                    }
                    if (Buffer[0] & 0x08) 
                        Buffer[0] = 0x01;
                    else
                        Buffer[0] = 0x00;
                    Buffer[1] = 0x00;
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, Buffer, 2);
                    break;
             
                default:
                    /* Unsupported - Request Error - Stall */
                    ErrorStallControlEndPoint();
                    break;
            }
            break;

Запрос Get Status, направленный на конечную точку, возвращает статус конечной точки, т. е. остановлена ли она (halted), или нет. Как и в запросе Set/Clear feature ENDPOINT_HALT, нам нужно всего лишь сообщить о статусе обычных (не равных 0) конечных точек.

Любые неопределенные стандартные запросы к конечным точкам (Standard Endpoint Request) обрабатываются с ошибкой запроса USB Request Error.

        case VENDOR_DEVICE_REQUEST:
        case VENDOR_ENDPOINT_REQUEST:
            printf("Vendor Device bRequest = 0x%X, wValue = 0x%X, wIndex = 0x%X\n\r", \
            SetupPacket.bRequest, SetupPacket.wValue, SetupPacket.wIndex);
            switch (SetupPacket.bRequest) 
            {
            case VENDOR_GET_ANALOG_VALUE:
                printf("Get Analog Value, Channel %x :",SetupPacket.wIndex & 0x07);
                ADCON0 = 0xC1 | (SetupPacket.wIndex & 0x07) << 3;          
                /* Wait Acquistion time of Sample and Hold */
                for (a = 0; a <= 255; a++);
                ADGO = 1;
                while(ADGO);
                Buffer[0] = ADRESL;
                Buffer[1] = ADRESH;
                a = (Buffer[1] << 8) + Buffer[0];
                a = (a * 500) / 1024;
                printf(" Value = %d.%02d\n\r",(unsignedint)a/100,(unsigned int)a%100);
                D11WriteEndpoint(D11_ENDPOINT_EP0_IN, Buffer, 2);
                break;

Теперь мы подошли (наконец!) к функциональной части устройства USB. Запросы (Vendor Requests) выдумываются самим разработчиком. Мы придумали 2 запроса, VENDOR_GET_ANALOG_VALUE и VENDOR_SET_RB_HIGH_NIBBLE. VENDOR_GET_ANALOG_VALUE читает 10-битное значение напряжения ADC канала x, указанного в поле wIndex. На значение x мы накладываем по AND маску 0x07, что разрешает 8 возможных номеров каналов (что поддерживает более крупный чип PIC16F877, если это нужно). Аналоговая величина напряжения возвращается в 2 байтах пакета данных (примечание переводчика – от фазы данных можно было бы отказаться, передавая эти 2 байта в wValue или wIndex. В этом случае wLength нужно указать не 2, а 0).

                case VENDOR_SET_RB_HIGH_NIBBLE:
                    printf("Write High Nibble of PORTB\n\r");
                    PORTB = (PORTB & 0x0F) | (SetupPacket.wIndex & 0xF0);
                    D11WriteEndpoint(D11_ENDPOINT_EP0_IN, NULL, 0);
                    break;
                default:
                    ErrorStallControlEndPoint();
                    break;  
            }
            break;

VENDOR_SET_RB_HIGH_NIBBLE может быть использован для установки бит старшего ниббла PORTB[3:7].

        default:
            printf("UnSupported Request Type 0x%X\n\r", SetupPacket.bmRequestType);
            ErrorStallControlEndPoint();
            break;
        }
    }
    else
    {
        printf("Data Packet?\n\r");
        /* Это пакет данных (Data Packet) */
    }
}

Любые не поддерживаемые типы запросов, такие как class device request, class interface request и т. д. обрабатываются с ошибкой запроса USB Request Error.

void GetDescriptor(PUSB_SETUP_REQUEST SetupPacket)
{
    switch((SetupPacket->wValue & 0xFF00) >> 8) 
    {
    case TYPE_DEVICE_DESCRIPTOR:
        printf("\n\rDevice Descriptor: Bytes Asked For %d, Size of Descriptor %d\n\r", \
                SetupPacket->wLength,DeviceDescriptor.bLength);
        pSendBuffer = (const unsigned char *)&DeviceDescriptor;
        BytesToSend = DeviceDescriptor.bLength;
        if (BytesToSend > SetupPacket->wLength)
            BytesToSend = SetupPacket->wLength;
        WriteBufferToEndPoint();
        break;
    case TYPE_CONFIGURATION_DESCRIPTOR:
        printf("\n\rConfiguration Descriptor: Bytes Asked For %d, Size of Descriptor %d\n\r", \
                SetupPacket->wLength,sizeof(ConfigurationDescriptor));
        pSendBuffer = (const unsigned char*)&ConfigurationDescriptor;
        BytesToSend = sizeof(ConfigurationDescriptor);
        if (BytesToSend > SetupPacket->wLength)
            BytesToSend = SetupPacket->wLength;
        WriteBufferToEndPoint();
        break;

Запросы Get Descriptor влекут за собой ответы размером больше, чем лимит на максимальный размер пакета 8 байт для конечной точки. Таким образом, ответ должен быть разбит на куски по 8 байт. Оба запроса Device и Configuration загружают адрес соответствующих дескрипторов в pSendBuffer, и устанавливают BytesToSend в значение длины дескриптора. Запрос будет также указывать длину дескриптора в поле wLength, что дает максимум данных для отправки. В любом случае мы проверяем действительную величину, запрашиваемую от хоста, и обрезаем размер по необходимости. Затем делается вызов WriteBuffertoEndpoint, который загружает первые 8 байт в буфер конечной точки и увеличивает указатель, чтобы быть готовым к следующему пакету из 8 байт.

    case TYPE_STRING_DESCRIPTOR:
        printf("\n\rString Descriptor: LANGID = 0x%04x, Index %d\n\r", \
            SetupPacket->wIndex, SetupPacket->wValue & 0xFF);
        switch (SetupPacket->wValue & 0xFF)
        {
        case 0:
            pSendBuffer = (const unsigned char*)&LANGID_Descriptor;
            BytesToSend = sizeof(LANGID_Descriptor);
            break;
        case 1:
            pSendBuffer = (const unsigned char*)&Manufacturer_Descriptor;
            BytesToSend = sizeof(Manufacturer_Descriptor);
            break;
        default:
            pSendBuffer = NULL;
            BytesToSend = 0;
        }
        if (BytesToSend > SetupPacket->wLength)
            BytesToSend = SetupPacket->wLength;
        WriteBufferToEndPoint();
        break;

Если имеется любой строковый дескриптор, должен присутствовать строковый дескриптор 0, описывающий языки, которые поддерживает устройство. Любые запросы строковых дескрипторов с ненулевыми номерами имеют LanguageID в поле wIndex, который говорит о том, какой язык поддерживается. В нашем случае мы немного обманываем и игнорируем значение wIndex (LANGID) возвращаемый в строке, независимо от того, какой язык запрашивался.

    default:
        ErrorStallControlEndPoint();
        break;
    }
}
  
void ErrorStallControlEndPoint(void)
{
    unsigned char Buffer[] = { 0x01 };
    /* 9.2.7 RequestError - возврат STALL PID в ответ на слеующую DATA Stage Transaction */
    D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + D11_ENDPOINT_EP0_IN, Buffer, 1);
    /* или в status stage сообщения. */
    D11CmdDataWrite(D11_SET_ENDPOINT_STATUS + D11_ENDPOINT_EP0_OUT, Buffer, 1);
}

Когда мы сталкиваемся с неправильным запросом, параметром, или запросом, который устройство не поддерживает, мы должны сообщить об ошибке запроса (request error). Это задано в части 9.2.7 стандарта. Request error возвращает STALL PID в ответ на следующие данные стадии транзакции или стадии статуса сообщения. Однако чтобы предотвратить нежелательный трафик шины, сообщение должно возвращаться на следующей стадии данных, вместо того чтобы ждать до стадии состояния (status stage).

unsigned char D11ReadEndpoint(unsigned char Endpoint, unsigned char*Buffer)
{
    unsigned char D11Header[2];
    unsigned char BufferStatus = 0;
  
    /* Выбор конечной точки */
    D11CmdDataRead(Endpoint, &BufferStatus, 1);
  
    /* Проверка - заполнен ли буфер */
    if(BufferStatus & 0x01)
    {
        /* Чтение dummy-заголовка - указатель на буфер D11 инкрементируется
           при каждом чтении и сбрасывается только командой Select Endpoint */
        D11CmdDataRead(D11_READ_BUFFER, D11Header, 2);
        if(D11Header[1]) D11CmdDataRead(D11_READ_BUFFER, Buffer, D11Header[1]);
        /* Разрешить принимать новые пакеты. */
        D11CmdDataWrite(D11_CLEAR_BUFFER, NULL, 0);
    }
    return D11Header[1];
}
  
void D11WriteEndpoint(unsigned char Endpoint, const unsigned char*Buffer, unsigned char Bytes)
{
    unsigned char D11Header[2];
    unsigned char BufferStatus = 0;
    D11Header[0] = 0x00;
    D11Header[1] = Bytes;
    /* Выбор конечной точки */
    D11CmdDataRead(Endpoint, &BufferStatus, 1);
    /* Запись заголовка */
    D11CmdDataWrite(D11_WRITE_BUFFER, D11Header, 2);
    /* Запись пакета */
    if (Bytes) D11CmdDataWrite(D11_WRITE_BUFFER, Buffer, Bytes);
    /* Проверка буфера на валидность */
    D11CmdDataWrite(D11_VALIDATE_BUFFER, NULL, 0);
}

Функции D11ReadEndpoint и D11WriteEndpoint специфичны для микросхемы PDIUSBD11. PDIUSBD11 имеет два dummy – байта, которые являются префиксом к любой операции чтения или записи данных. Первый байт зарезервирован, а второй байт показывает количество принимаемых или передаваемых байт. Эти две функции обеспечены своим заголовком.

void WriteBufferToEndPoint(void)
{
    if (BytesToSend == 0) 
    {
        /* Если BytesToSend равна 0, и процедура была вызвана снова,
           то предположите, что буфер меньше, чем размер Setup Request,
           и покажите конец отправкой пакета нулевой длины 
           (Zero Lenght packet) */
        D11WriteEndpoint(D11_ENDPOINT_EP0_IN, NULL, 0);
    } 
    else if (BytesToSend >= 8) 
    {
        /* Запись других 8 байт в буфер, и отправка */
        D11WriteEndpoint(D11_ENDPOINT_EP0_IN, pSendBuffer, 8);
        pSendBuffer += 8;
        BytesToSend -= 8;
    } 
    else 
    {
        /* Буфер должен иметь в запасе меньше 8 байт. */
        D11WriteEndpoint(D11_ENDPOINT_EP0_IN, pSendBuffer, BytesToSend);
        BytesToSend = 0;
    }
}

Как мы уже упоминали, функция WriteBufferToEndPoint отвечает за загрузку данных в микросхему PDIUSBD11 порциями по 8 байт и подстройку указателей для готовности к следующему пакету. Функция вызывается один раз обработчиком запроса для загрузки первых 8 байт в буфер конечной точки. Затем хост посылает токен IN, читает эти данные и PDIUSBD11 генерирует прерывание. Обработчик конечной точки EP0 IN вызовет затем WriteBufferToEndpoint для загрузки следующего пакета в готовность к следующему токену IN от хоста.

Трансфер считается завершенным, если все запрошенные байты прочитаны, если принят пакет с полезной нагрузкой меньше, чем bMaxPacketSize или если возвращен пакет нулевой длины (zero length packet). Таким образом, если счетчик BytesToSend достигает 0, мы предполагаем что данные были отправлены несколько раз по 8 байт, и отравляем zero length packet, что указывает – это были последние данные. Однако если у нас осталось меньше чем 8 байт для отправки, мы посылаем только оставшиеся байты. Здесь не требуется дополнять данные нулями.

void loadfromcircularbuffer(void)
{
    unsigned char Buffer[10];
    unsigned char count;
  
    // Чтение состояния заполненности буфера
    D11CmdDataRead(D11_ENDPOINT_EP1_IN, Buffer, 1);
  
    if (Buffer[0] == 0)
    {
        // Буфер пуст
        if (inpointer != outpointer)
        {
            // есть байты для отправки
            count = 0;
            do
            {
                Buffer[count++] = circularbuffer[outpointer++];
                if (outpointer >= MAX_BUFFER_SIZE)
                    outpointer = 0;
                if (outpointer == inpointer)
                    break; // больше нет данных
            }while (count < 8); // максимальный размер буфера
            // Теперь загрузим это в EP1_In
            D11WriteEndpoint(D11_ENDPOINT_EP1_IN, Buffer, count);
        }
    }
}

Процедура loadfromcircularbuffer() обрабатывает загрузку данных в буфер конечной точки EP1 IN. Процедура нормально вызывается после прерывания EP1 IN для перезагрузки буфера и для готовности к следующему токену IN на конечной точке EP1. Однако для того, чтобы отправить первый пакет, нам надо загрузить данные перед получением прерывания EP1 IN. Таким образом, процедура также вызывается после принятых данных в конечную точку EP1 OUT.

Вызывая также процедуру из обработчика EP1 OUT, мы возможно перезапишем данные в буфере IN независимо от того, были ли уже посланы предыдущие данные буфера IN, или нет. Чтобы это предотвратить, мы проверяем – пуст ли буфер EP1 IN перед попыткой загрузить его новыми данными.

void D11CmdDataWrite(unsigned char Command, const unsigned char*Buffer, unsigned char Count)
{
    I2C_Write(D11_CMD_ADDR, &Command, 1);
    if(Count)
        I2C_Write(D11_DATA_ADDR_WRITE, Buffer, Count);
}
   
void D11CmdDataRead(unsigned char Command, unsigned char Buffer[], unsigned char Count)
{
    I2C_Write(D11_CMD_ADDR, &Command, 1);
    if(Count)
        I2C_Read(D11_DATA_ADDR_READ, Buffer, Count);
}

D11CmdDataWrite и D11CmdDataRead – две функции, специфичные для микросхемы PDIUSBD11. Они отвечают за работу интерфейса I2C – за отправку сначала команд Address/Command и затем отправки принятых данных по шине I2C. Дополнительные функции нижнего уровня добавлены в исходный код, но здесь не приведены, чтобы лучше сфокусироваться именно на специфике протокола USB.

Этот пример может использоваться вместе с примером bulkUSB.sys, который является частью Windows DDK. Для загрузки драйвера bulkUSB.sys либо поменяйте код для самоидентификации по VID 0x045E и PID 0x930A, либо поменяйте для bulkUSB.sys файл bulkUSB.inf в соответствии с применяемыми в этом примере VID/PID.

Теперь можно использовать консольную программу пользовательского режима rwbulk.exe для отправки пакетов из кольцевого буфера. Используйте следующую команду для отправки порций по 80 байт от PIC16F876:

rwbulk -r 80 -w 80 -c 1 -i 1 -o 0

Использование полезной нагрузки свыше 80 байт вызовет переполнение кольцевого буфера PIC в памяти BANK1.

Этот пример был закодирован для лучшей удобочитаемости в ущерб размеру кода. Он компилируется в 3250 слов памяти FLASH (39% емкости PIC16F876).

Благодарности 

Выражаем особую признательность Michael DeVault из компании DeVaSys Embedded Systems [6]. Этот пример базируется на коде, написанным Michael. Код легко был разработан на плате для USB разработки USBLPT-PD11 [6] компании DeVaSys перед портированием на PIC.

Загрузка исходного кода

История ревизий

  • 6 апреля 2002 - версия 1.2 – увеличена скорость I2C для соответствия комментариям. Улучшена обработка прерываний (IRQ) PDIUSBD11
  • 7 января 2002 - версия 1.1 – добавлены процедуры Bulk обработки EP1 IN и EP1 OUT и сделаны дескрипторы, загружающиеся из FLASH
  • 31 декабря 2001 - версия 1.0.

[Термины]

ADC Analog-to-Digital Converter, АЦП, аналого-цифровой преобразователь.

bit stuffing вставка бит по специальному алгоритму в последовательном потоке бит на линии. Применяется для целей синхронизации или уменьшения/устранения постоянной составляющей в сигнале.

bus powered devices устройства USB, получающие питание +5V от шины USB (линия VBUS). Питающее напряжение на линии VBUS формирует хост.

daisy chained термин, относящийся к линейной (иногда кольцевой) топологии сети. Иногда такую топологию называют шлейфовой. Дословный перевод daisy chain – «цепочка маргариток», что означает венок из маргариток.

downstream нисходящее соединение. Такое соединение имеет место для хоста по отношению к устройству USB.

Endpoint, конечная точка специфическое понятие стандарта USB, символизирующее источник или приемник потока данных.

EPx EndPoint (конечная точка) номер x.

errata исправления, добавления к стандарту.

Feature фича, какая-нибудь возможность (особенность) устройства.

Feature Selector селектор фичи, число, от которого зависит выбор какой-нибудь возможности (особенности) устройства.

FLASH энергонезависимая память для хранения программ или данных.

FrameWork фреймворк, рабочая среда, рабочее окружение.

handshaking «рукопожатие», процедура установления связи.

Host хост, главное устройство на шине. Обычно это компьютер.

ICD In Circuit Debug, внутрисхемная отладка.

ISR Interrupt Service Routine, подпрограмма обработки прерывания.

Latency латентность, время ожидания на обработку, время задержки.

LDO regulator, Low Dropout регулятор, имеющий маленькое падение напряжения, достаточное для нормальной работы (стабилизации).

OTP ROM однократно программируемая память (обычно для программы).

Padding дополнение байтами до нужного количества (обычно нулевыми байтами).

payload полезная нагрузка, передаваемые в потоке данные.

PCB печатная плата.

peer-to-peer возможность обмена данными между равноправными устройствами.

Pipe(s) буквальный перевод «труба» («трубы»). Означает специальным образом сформированные частные потоки данных через интерфейс USB. Иногда pipe называют каналом.

plug штеккер, коннектор на конце кабеля или на торце устройства USB (например, флешки). Этот коннектор втыкается в сокет, который обычно ставится на корпусе устройства (компьютера или периферии USB).

pull up, pull-up нагрузочный для сигнальной линии резистор, подключенный между линией сигнала и плюсом питания.

pull down, pull-down нагрузочный для сигнальной линии резистор, подключенный между линией сигнала и землей.

self powered devices устройства USB, питающиеся от отдельного источника (не от шины USB).

socket сокет, разъем на корпусе компьютера или периферии USB, куда втыкается plug (коннектор).

status reporting получение информации о статусе устройства.

tiered star топология многоярусная звезда.

token символ.

Token Packet символ, показывающий, какие дальше идут данные.

trade-off компромисс.

upstream восходящее соединение. Такое соединение имеет место для устройства USB по отношению к хосту.

[Ссылки]

1USB in a NutShell site:beyondlogic.org - в оригинале на английском.
2
Разработка устройства USB - как начать работу с библиотеками AVR USB (V-USB) и libusb
3Макетные платы с интерфейсом USB.
4Hi-Tech PICC Compiler site:htsoft.com.
5. USB Driver для Cypress USB Starter Kit site:beyondlogic.org
6. USBLPT-PD11 site:www.devasys.com.
7. DeVaSys Embedded Systems site:devasys.com

 

Комментарии  

 
+7 #1 Skip 01.12.2010 20:23
Пожизненный респект и аллиллуйя! Спас :-)
Цитировать
 

Добавить комментарий


Защитный код
Обновить

Top of Page