V-USB и libusb: обмен с устройством USB HID с помощью управляющих сообщений (USB control messages) Печать
Добавил(а) microsin   

В статье разбирается метод использования функций usbFunctionSetup (из библиотеки V-USB) и usb_control_msg (из библиотеки libusb) для организации обмена между устройством USB HID (работает на основе библиотеки V-USB) программой на компьютере (ПО хоста, работает на основе библиотеки libusb).

Если коротко - библиотеки V-USB и libusb позволяют разрабатывать USB устройства на обычных микроконтроллерах AVR компании Atmel и писать кроссплатформенное (Windows, MAC OS, *nix) ПО хоста для них. Подробнее см. ссылки.

Обмен данными с помощью управляющих сообщений (control messages) удобен в том случае, когда не нужно передавать большой объем данных за один раз (до 4 байт). Все обмены информацией в обоих направлениях (и от устройства USB HID к ПО хоста, и обратно от ПО хоста к USB HID устройству) происходят только по инициативе хоста. Для передачи данных от устройства USB HID ПО хоста использует запрос со значением CUSTOM_RQ_GET_STATUS, а для отправки данных в USB устройство - запрос со значением CUSTOM_RQ_SET_STATUS (для этого в ПО хоста применяется вызов функции usb_control_msg с соответствующими параметрами). В ответ firmware USB HID внутри своей функции usbFunctionSetup должно проанализировать значение пришедшего запроса, и в зависимости от него либо получить данные (в ответ на запрос CUSTOM_RQ_SET_STATUS), либо подготовить данные для отправки (в ответ на запрос CUSTOM_RQ_GET_STATUS).

Разберем методику обмена на примере hid-custom-rq (этот пример можно скачать в архиве avr-usb-russian.rar [3]). В этом примере ПО хоста управляет светодиодом, подключенным к ножке микроконтроллера (см. для примера схему макетной платы AVR-USB-MEGA16), либо считывает его состояние. И в том и в другом случае передается только один байт, что вполне достаточно для такой задачи. Я рассмотрю только те участки кода примера, которые нужно будет модифицировать в случае создания собственного программного обеспечения.

[Отправка данных от ПО хоста к USB HID устройству]

В этом случае мы управляем состоянием светодиода. В ПО хоста для отправки байта применяется вызов функции usb_control_msg с параметром CUSTOM_RQ_SET_STATUS (см. модуль examples\hid-custom-rq\commandline\set-led.c, тело функции main):

usb_dev_handle      *handle = NULL; 
int                 cnt, isOn; 
char                buffer[4]; 
 
cnt = usb_control_msg(handle,
                      USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT,
                      CUSTOM_RQ_SET_STATUS, isOn, 0, buffer, 0, 5000);
if(cnt < 0)
{
    fprintf(stderr, "USB error: %sn", usb_strerror());
}

Теперь подробно о назначении каждой переменной.

handle - хендл открытого на доступ устройства USB HID. Тут не рассмотрен код, открывающий устройство USB HID и получающий ненулевое значение переменной handle, поскольку в нем мы ничего не меняем - все тупо оставляем так, как в примере.
cnt - количество байт, пришедших от устройства USB HID в ответ на запрос CUSTOM_RQ_SET_STATUS. В данном случае значение cnt просто сигнализирует об наличии ошибки, если cnt отрицательно.
isOn - передаваемый байт полезной информации. Если он равен 0, то светодиод погаснет, если 1, то загорится.
buffer - просто тупая переменная, которая тут никак не используется. При отправке запроса CUSTOM_RQ_SET_STATUS состояние данных буфера buffer на входе в функцию usb_control_msg может быть произвольным, и никакого значения эти данные не имеют. На выходе из функции usb_control_msg буфер buffer также не анализируется, никаких полезных данных в нем нет.

Назначение параметров функции usb_control_msg (см. документацию по функции usb_control_msg libusb Developers Guide site:libusb.sourceforge.net):

1. usb_dev_handle *dev. Здесь используется значение переменной handle - передается идентификатор открытого USB HID устройства. Получают валидный handle после однократного вызова функции usbOpenDevice (из модуля opendevice.c).
2. int requesttype, тип запроса. Его значение равно USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT. Указывается конечная точка USB устройства USB_ENDPOINT_OUT.
3. int request, значение запроса. Тут подставляется значение CUSTOM_RQ_SET_STATUS, что означает, что мы передаем данные в запросе.
4. int value. В этом параметре можно передать максимум два байта (т. е. младшие 16 бит 32-разрядного параметра int value). В нашем примере тут передается значение переменной isOn, несущей информацию о том, что нужно светодиод выключить (isOn=0) или включить (isOn=1).
5. int index. Здесь передается ноль, и в нашем примере этот параметр не используется, однако тут можно передать еще 2 байта полезных данных. Таким образом, с помощью запроса CUSTOM_RQ_SET_STATUS можно передать максимум 4 байта полезной информации от ПО хоста в USB HID устройства (в значениях параметров 4 value и 5 index).
6. char *bytes. Указатель на буфер, выделенный в памяти. В случае запроса CUSTOM_RQ_SET_STATUS этот буфер никак не используется, и нет никакой возможности его использовать для передачи информации.
7. int size. Тут передается размер буфера. В случае запроса CUSTOM_RQ_SET_STATUS всегда передается 0, другое значение не дает никакого эффекта.
8. int timeout. Таймаут, в течение которого должен прийти ответ. Стоит значение 5000, наверное это означает 5000 миллисекунд).

В устройстве USB HID запрос CUSTOM_RQ_SET_STATUS обрабатывает код внутри тела функции usbFunctionSetup. Этот код зажигает или гасит светодиод:

usbMsgLen_t usbFunctionSetup(uchar data[8])
{
    usbRequest_t *rq = (void *)data;
 
    if((rq -> bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_VENDOR)
    {
        if(rq->bRequest == CUSTOM_RQ_SET_STATUS)
        {
            if(rq -> wValue.bytes[0] & 1)
            {   
                /* зажжем светодиод */
                LED_PORT_OUTPUT |= _BV(LED_BIT);
            }
            else
            {
                /* погасим светодиод */
                LED_PORT_OUTPUT &= ~_BV(LED_BIT);
            }
        }
        else if(rq -> bRequest == CUSTOM_RQ_GET_STATUS)
        {
            static uchar dataBuffer[1];     /* буфер должен оставаться валидным
                                                             при выходе из usbFunctionSetup */
            dataBuffer[0] = ((LED_PORT_OUTPUT & _BV(LED_BIT)) != 0);
            usbMsgPtr = dataBuffer;         /* говорим драйверу, какие данные вернуть */
            return 1;                       /* говорим драйверу послать 1 байт */
        }
    }
    else
    {
        /* вызовы запросов USBRQ_HID_GET_REPORT и USBRQ_HID_SET_REPORT не реализованы, 
         * поскольку мы их не вызываем. Операционная система также не будет обращаться
 * к ним, потому что наш дескриптор не определяет никакого значения. */
    }
    return 0; /* default для нереализованных запросов: не возвращаем назад данные хосту */
}

Из кода видно, что анализируются данные поля wValue структуры usbRequest_t. Значение wValue равно значению параметра 4 в вызове функции usb_control_msg, в нашем примере тут передается значение переменной isOn. Размер поля wValue 16 бит, здесь можно передать 2 байта полезной информации. Еще 2 байта можно передать через поле wIndex (соответствует параметру 5 int index в функции usb_control_msg). Возможно, что еще 2 байта можно передать через поле wLength, но я не пробовал.

[Отправка данных от USB HID устройства к ПО хоста]

В этом случае мы считываем состояние светодиода. В ПО хоста для получения байта, в котором записано состояние светодиода, применяется вызов функции usb_control_msg с параметром CUSTOM_RQ_GET_STATUS (см. модуль examples\hid-custom-rq\commandline\set-led.c, тело функции main):

cnt = usb_control_msg(handle,
USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN,CUSTOM_RQ_GET_STATUS,
0, 0, buffer, sizeof(buffer), 5000); if(cnt < 1) { if(cnt < 0) fprintf(stderr, "USB error: %sn", usb_strerror()); else fprintf(stderr, "only %d bytes received.n", cnt); } else printf("LED is %sn", buffer[0] ? "on" : "off");

Возвращенное значение переменной cnt, равное нулю и меньше нуля сигнализирует об ошибке, в случае возврата 1 и более означает, что запрос обработан успешно. Реально возвращается один байт, поэтому cnt должно быть равно 1 (оно как раз равно значению, которое возвращает функция usbFunctionSetup при обработке запроса CUSTOM_RQ_GET_STATUS: return 1). Полезные данные передаются в первом байте buffer. Назначение переменных в вызове функции usb_control_msg:

1. usb_dev_handle *dev. Тут ничего не поменялось, все то же самое, как в случае отправки запроса CUSTOM_RQ_SET_STATUS.
2. int requesttype, тип запроса. Его значение равно USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN. Тут отличие только в том, что указывается конечная точка USB_ENDPOINT_IN, а не USB_ENDPOINT_OUT.
3. int request, значение запроса. Тут подставляется значение CUSTOM_RQ_GET_STATUS, что означает, что мы передаем запрос на получение данных.
4. int value. Передается 0, параметр не используется.
5. int index. Передается 0, параметр не используется.
6. char *bytes. Указатель на буфер, выделенный в памяти. В случае запроса CUSTOM_RQ_GET_STATUS этот буфер используется для переноса данных от USB устройства к ПО хоста. В нашем примере передается только один байт, несущий информацию о состоянии светодиода. О том, что байт только один, говорит return 1 на выходе из usbFunctionSetup в случае обработки запроса CUSTOM_RQ_GET_STATUS (см. код устройства USB HID, модуль main.c).
7. int size. Тут передается размер буфера. В случае запроса CUSTOM_RQ_GET_STATUS передается значение 4, отражающее реальный размер буфера.
8. int timeout. Тут все то же самое.

На стороне USB HID устройства запрос CUSTOM_RQ_GET_STATUS обрабатывает простой код в теле функции usbFunctionSetup:

        ..
        else if(rq->bRequest == CUSTOM_RQ_GET_STATUS)
        {
            static uchar dataBuffer[1];     /* буфер должен оставаться валидным
                                                             при выходе из usbFunctionSetup */
            dataBuffer[0] = ((LED_PORT_OUTPUT & _BV(LED_BIT)) != 0);
            usbMsgPtr = dataBuffer;         /* говорим драйверу, какие данные вернуть */
            return 1;                       /* говорим драйверу послать 1 байт */
        }
        ..

В памяти выделяется однобайтовый буфер dataBuffer, в который записывается состояние светодиода. Указатель на этот буфер передается через значение переменной usbMsgPtr. Оператор return 1 говорит драйверу о том, что передаем 1 байт (он попадает в буфер buffer в ПО хоста). Реально можно передать 4 байта, если выделить буфер dataBuffer[1], и вернуть заместо 1 значение 4 (сделать return 4). Вячеслав, автор примера программы на Visual Basic (см. ссылку [5]) сообщает, что буфер (dataBuffer и buffer) может быть и размером в 8 байт.

Все! Больше ничего не надо знать, чтобы организовать простой работоспособный обмен данными до 4 байт в обе стороны. Еще раз повторю, что отправка данных в USB устройство происходит в запросе CUSTOM_RQ_SET_STATUS через параметры 4 value и 5 index вызова функции usb_control_msg (в USB HID устройстве эти данные попадают в соответствующие поля структуры usbRequest_t), а получение данных от USB устройства происходит через запросCUSTOM_RQ_GET_STATUS и выделенный буфер данных.

[Смена параметров USB устройства - имя устройства, его VID и PID]

Эти параметры задаются в файле usbconfig.h. При компиляции он общий как для firmware (основано на библиотеке V-USB), так и для ПО хоста (основано на библиотеке libusb). Параметры (имя устройства, VID и PID) менять совершенно необязательно, все и так будет работать, но это может понадобиться, если Вы хотите получить уникальное устройство, либо хотите обеспечить работу нескольких Ваших устройств USB одновременно на одном компьютере. О назначении всех параметров можете прочитать комментарии в файле usbconfig.h на русском языке, если скачаете русифицированный пакет V-USB (см. ссылку 3). Обращаю Ваше внимание, что после внесения изменений в файл usbconfig.h необходимо перекомпилировать как firmware, так и ПО хоста.

[Ссылки]

1. libusb Developers Guide site:sourceforge.net - документация по библиотеке libusb.
2. В русской википедии кратко объясняется, что это такое V-USB.
3. avr-usb-russian.rar - библиотека V-USB с русскими комментариями и документацией.
4. Разработка устройства USB - как начать работу с библиотеками V-USB и libusb. Пошаговое руководство, как установить весь нужный софт для работы с библиотеками V-USB и libusb и компилировать примеры.
5. AVR-USB-MEGA16: управление устройством USB из GCC, Visual Studio CPP, VB6.
6. Общение с контроллером шагового двигателя по USB site:speleoastronomy.org.