Pull to refresh

Управление устройством USB HID на Windows 7

Reading time9 min
Views92K
В статье сделана попытка предоставить пошаговую инструкцию — как соединить самодельное устройство USB HID на микроконтроллере AVR и компьютер с операционной системой Windows 7 x64, чтобы обмениваться данными и управлять портами микроконтроллера. Пример приложения управляет через USB ножкой порта микроконтроллера (к ней подключен индикационный светодиод). Есть возможность также прочитать состояние состояние светодиода — потушен он или горит. Топик предназначен для новичков, поэтому большая просьба к знатокам программирования — приберегите тухлые яйца и гнилые помидоры иронические комментарии для более удобного случая.

Используемое программное обеспечение


1. Для микроконтроллера — библиотека V-USB [1] компании Objective Development и IDE Atmel Studio 6 [2] компании Atmel. Нужно также скачать и установить тулчейн WinAVR [3] для компиляции firmware микроконтроллера (для спецов это необязательно, потому что можно обойтись тулчейном, который входит в состав Atmel Studio).
2. Для написания программы Windows (ПО хоста) использовалась библиотека LibUsbDotNet [4] Тревиса Робинсона и IDE Visual Studio C# 2010 [5] компании Microsoft.

Все программное обеспечение, кроме Visual Studio 2010, бесплатное, хотя есть возможность использовать Visual Studio C# 2010 Express бесплатно в течение ограниченного срока. Все действия проводились в среде операционной системы Windows 7 x64, но наверняка подойдет и любая другая операционная система семейства Windows (Windows XP и более свежая).

Используемое железо


Благодаря библиотеке V-USB для создания устройства USB HID подойдет любой микроконтроллер AVR. Если Вы дружите с паяльником, то даже можете собрать подключение к USB самостоятельно по одной из опубликованных схем. Такая схема (взята из пакета V-USB [1]) в качестве примера приведена на картинке.



Чтобы экономить время и усилия, лучше использовать готовую макетную плату. Особенно удобно, если в плату будет записан USB-загрузчик (bootloader), тогда не понадобится покупать программатор для перепрошивки платы. Я использовал макетную плату AVR-USB-MEGA16 с микроконтроллером ATmega32A, в ней загрузчик есть (USBasploader, эмулирующий поведение программатора USBasp). Вот так платка выглядит в натуральную величину:

image

Можно взять также metaboard (на нем стоит ATmega168 или ATmega328), или даже программатор на микроконтроллере ATmega8. Подобные железки можно дешево купить на ebay.com или dx.com.

Создание firmware микроконтроллера с помощью Atmel Studio 6 и библиотеки V-USB


Сделайте новый проект в Atmel Studio 6 (далее просто AS6). Когда AS6 предложит выбрать микроконтроллер, выберите Atmega32 без буквы A, не Atmega32A (хотя на плате стоит Atmega32A) — это важно, так как тулчейн WinAVR не видит разницы, он знает только Atmega32. Эти микроконтроллеры по внутреннему устройству идентичны, так что для нас разницы нет, а для AS6 есть.

Теперь нужно правильно настроить компилятор. В верхнем меню AS6 нажите Tools, далее Options.. и появится вот такое окно:

image

Слева в списке выберите Toolchain. Справа появится список Flavours. Этим словечком Atmel закодировала возможные варианты используемого инструментария (тулчейны).

Примечание. В списке уже присутствует тулчейн Native, который используется по умолчанию (Default). Тулчейн Native - это компилятор GCC вместе с заголовочными файлами и библиотеками, которые предоставляют необходимую среду компилирования исходного кода для микроконтроллера. Этот тулчейн предоставила Atmel, он устанавливается автоматически вместе с установкой AS6. Как я уже упоминал, для компиляции можно использовать и этот тулчейн, но тогда в исходный код примеров V-USB (на основе примера USB HID будет работать наше устройство USB) придется вручную вносить исправления. Они несложные, но для новичков будет лучше добавить сюда тулчейн WinAVR, и использовать для компиляции именно его.

Для добавления в список Flavours тулчейна WinAVR нажмите кнопку Add Flavour, появится следующее окно:

image

В верхней строчке этого окна введите имя компилятора WinAVR (произвольное), а в нижней строке введите полный путь, куда установлен сам компилятор тулчейна (с указанием папки \bin) и нажмите кнопку Add. В списке Flavours появится добавленный компилятор, как показано на скриншоте.



Выделите мышкой наш новый добавленный компилятор WinAVR и нажмите кнопку Set As Default (сделать его тулченом по умолчанию), и нажмите OK. После этой процедуры наша AS6 будет использовать компилятор WinAVR.

Пора настроить свойства нашего проекта, для этого курсором в Solution Explorer левым щелчком выберите имя проекта и нажмите Alt+F7 (меню Project -> Properties), появится окно с настройками:

image

Сделайте следующие настройки:
  • В разделе AVR/GNU C Compiler -> Symbols добавляем в поле -D строчку F_CPU=12000000UL — это соответствует частоте микроконтроллера 12 МГц (такой кварц установлен на моей макетной плате AVR-USB-MEGA16).
  • В разделе AVR/GNU Assemler -> General в поле Assembler flag надо добавить -DF_CPU=12000000UL.
  • В разделе AVR/GNU C Compiler -> Optimization в поле Optimization Level должно стоять Optimize for size (-Os).

Далее очень важный момент — в левой части окна, в списке выберите раздел Advanced, как показано на рисунке ниже.

image

В выпадающем списке Toolchain Flavour выберите добавленный компилятор WinAVR, чтобы при компилировании проекта AS6 использовала его. На этом настрока AS6 закончена.

Далее необходимо в созданный проект добавить файлы исходного кода проекта [6] — см. папку firmware\VUSB, файлы VUSB.c, usbdrv.c, usbdrvasm.S и oddebug.c. Проект ASS6 создан на основе одного из примеров библиотеки V-USB: hid-custom-rq, который изначально компилировался с помощью утилиты make из командной строки. На основе библиотеки V-USB можно найти много других примеров кода — в основном это устройства USB HID (мыши, клавиатуры, устройства ввода и вывода), но есть также и устройства USB CDC (виртуальный COM-порт). Если Вам лень самому создавать проект, просто откройте в AS6 файл проекта VUSB.atsln, в нем уже сделаны все необходимые настройки и добавлены все нужные файлы.

Если у Вас используется другая макетная плата, то нужно правильно настроить файл usbconfig.h. Это конфигурационный файл библиотеки V-USB, в нем задаются многие настройки и параметры (VID, PID, ножки микроконтроллера, значения для дескрипторов и другие настройки). Подробное описание всех настроек дано в комментриях этого файла. Основное внимание следует уделить назначению выводов микроконтроллера, которые используются под сигналы USD D+ и D- (макроопределения USB_CFG_IOPORTNAME, USB_CFG_DMINUS_BIT, USB_CFG_DPLUS_BIT), к этим ножкам предъявляются особые требования. Конфигурационный файл usbconfig.h из архива [6] предназначен под разводку ножек макетной платы AVR-USB-MEGA16, и он гарантированно работает. Моргать программа будет светодиодом, который уже имеется на макетной плате и подключен к ножке 0 порта B.

Создание программы для компьютера (ПО хоста)


Наша программа должна посылать через подключение USB пакеты, которые будут управлять микроконтроллером.

Примечание. Программа была создана на основе примера консольного приложения из той же библиотеки V-USB. Компиляция исходного кода консольного приложения выполнялась с помощью makefile и пакета MinGW, и использовала библиотеку LibUSB. В нашем примере мы будем использовать графическую среду Visual Studio и библиотеку LibUsbDotNet.

Однако главный цимус использования LibUsbDotNet вовсе не в том, что теперь легко и удобно можно делать не только консольные, но и графические приложения. Самый большой плюс - теперь не нужен драйвер фильтра, который таскала за собой библиотека LibUSB много лет. Для тех, кто в танке, драйвер фильтра - это особая программная надстройка над библиотекой LibUSB, через которую осуществлялся обмен данными с устройствами USB на платформе Windows. Теперь этот атавизм не нужен.

Запустите Microsoft Visual C# 2010 Express и создайте новый проект на основе Windows Form. Теперь нужно подключить к проекту библиотеку LibUsbDotNet.dll. В обозревателе решений нажмите правой кнопкой мыши на названии проекта, и выберите «Добавить ссылку».

image

появится ещё одно окно

image

здесь нужно найти путь на диске, где находится библиотека LinUsbDotNet.dll (по умолчанию она устанавливается папку C:\Program Files\LibUsbDotNet, но лучше сделать копию файла DLL в рабочий каталог проекта. После подключения библиотеки её нужно объявить в проекте, для этого добавьте в главный модуль программы (файл Form1.cs) строки:

using LibUsbDotNet;
using LibUsbDotNet.Info;
using LibUsbDotNet.Main;

Перейдите к визуальному редактору формы, и приведите её приблизительно к такому виду (добавьте 3 кнопки Button и 3 текстовых метки Label):

Внешний вид главной формы ПО хоста

Сделайте обработчик события загрузки формы. Он нужен для того, чтобы при старте программы происходила инициализации экземпляра класса LibUsbDotNet, через который осуществляется обмен с устройством USB. Перед началом обмена необходимо открыть доступ именно к нашему устройству, потому что к компьютеру может быть подключено несклько устройств USB HID, и необходимо уметь обращаться к каждому по отдельности. Для целей идентификации USB-устройств служат специальные идентификаторы, которые имеют абсолютно все устройства USB, это VID и PID.

Примечание. Иногда для идентификации устройства дополнительно используется уникальный серийный номер или отдельный текстовый дескриптор - когда к компьютеру подключено несколько устройств USB с одинаковыми VID и PID, но это не наш случай. Поскольку обычно у каждого USB-устройства, подключенного к компьтеру, своя пара VID/PID, отличающаяся от других устройств, то найти нужное устройство и обратиться к нему не составляет проблем.

VID это идентификатор производителя (Vendor ID), а PID — идентификатор устройства (Product ID). Наше USB-устройство имеет VID: 0x16C0, PID: 0x05DF, эти значение указаны в конфигурационном файле usbconfig.h (об этом файле мы уже упоминали) проекта микроконтроллера AS6. Чтобы ПО хоста обратилась к именно к нашему USB-устройству, нужно инициализировать объект MyUsbFinder такими же параметрами VID: 0x16c0, PID: 0x05df, как указаны в файле usbconfig.h. Для этого в область определения глобальных переменных класса Form1 добавьте следующий код:

        public static UsbDevice MyUsbDevice;
        public static UsbDeviceFinder MyUsbFinder = new UsbDeviceFinder(0x16c0, 0x05df); 

После того как мы определились с каким USB-устройством будем работать, можно к нему подключаться, и это удобно сделать в момент старта программы (открытия окна формы). Для этого выберите основную форму программы, и в редакторе свойств создайте обработчик события загрузки Form1_Load. В теле обработчика введите следующий код:

private void Form1_Load(object sender, EventArgs e)
{
    MyUsbDevice = UsbDevice.OpenUsbDevice(MyUsbFinder);
    if (MyUsbDevice != null)
    {
        label2.Text = " подключено !";
    }
    else label2.Text = " не найдено !";
}

Сделайте обработчик события клика на кнопке button1 («Вкл»), для этого сделайте в визуальном редакторе на кнопке двойной щелчок, и добавьте в тело обработчика события код:

private void button1_Click(object sender, EventArgs e)
{
    // Передать пакет, который включает светодиод на макетной плате AVR-USB-MEGA16.
    UsbSetupPacket packet = new UsbSetupPacket((byte)(UsbCtrlFlags.RequestType_Vendor | 
    UsbCtrlFlags.Recipient_Device | UsbCtrlFlags.Direction_Out), 1, (short)1, 0, 0);
    int countIn;
    byte[] data = new byte[1];
    MyUsbDevice.ControlTransfer(ref packet, data, 0, out countIn);
}

Для обработчика кнопки «Выкл» добавьте код:

private void button3_Click(object sender, EventArgs e)
{
    // Передать пакет, который погасит светодиод на макетной плате AVR-USB-MEGA16.
    UsbSetupPacket packet = new UsbSetupPacket((byte)(UsbCtrlFlags.RequestType_Vendor |
    UsbCtrlFlags.Recipient_Device | UsbCtrlFlags.Direction_Out), 1, (short)0, 0, 0);
    int countIn;
    byte[] data = new byte[1];
    MyUsbDevice.ControlTransfer(ref packet, data, 0, out countIn);
}

Код для обработки кнопки «Чтение»:

private void button2_Click(object sender, EventArgs e)
{
    //Получение данных от макетной платы AVR-USB-MEGA16 - состояние светодиода.
    UsbSetupPacket packet = new UsbSetupPacket((byte)(UsbCtrlFlags.RequestType_Vendor | 
    UsbCtrlFlags.Recipient_Device | UsbCtrlFlags.Direction_In), 2, (short)0, (short)0, (short)0);
    int countIn;
    byte[] data = new byte[1];
    if (MyUsbDevice.ControlTransfer(ref packet, data, 1, out countIn) && (countIn == 1))
    {
       label3.Text = "Прочитано значение " + data[0].ToString();
    }
}

Обработчик события закрытия формы (завершение работы программы) гасит светодиод, если он горит:

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
    UsbSetupPacket packet = new UsbSetupPacket((byte)(UsbCtrlFlags.RequestType_Vendor |
    UsbCtrlFlags.Recipient_Device | UsbCtrlFlags.Direction_Out), 1, (short)0, 0, 0);
    int countIn;
    byte[] data = new byte[1];
    MyUsbDevice.ControlTransfer(ref packet, data, 0, out countIn);
}

Как пакеты USB декодируются в firmware микроконтроллера


Прием и обработка данных на стороне микроконтроллера осуществляется в функции usbFunctionSetup (находится в главном модуле VUSB.c проекта firmware AS6). Вот эта функция:

usbMsgLen_t usbFunctionSetup(uchar data[8])
{
    usbRequest_t    *rq = (void *)data;

    if((rq->bmRequestType & USBRQ_TYPE_MASK) == USBRQ_TYPE_VENDOR){
        DBG1(0x50, &rq->bRequest, 1);   /* отладочный вывод: печатаем наш запрос */
        if(rq->bRequest == CUSTOM_RQ_SET_STATUS){
            if(rq->wValue.bytes[0] & 1){    /* установить LED */
                LED_PORT_OUTPUT |= _BV(LED_BIT);
            }else{                          /* очистить LED */
                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 для нереализованных запросов: не возвращаем назад данные хосту */
}

Наше устройство USB HID простейшее, и реагирует оно только на управляющие передачи (control transfer), которые проходят через конечную точку 0 (default control endpoint). По типу запроса (поле bRequest) декодируется направление передачи данных. Если CUSTOM_RQ_SET_STATUS, то это данные, предназначаемые для микроконтроллера. Данные декодируются и микроконтроллер выполняет заложенную там команду. В этом случае в самом первом по порядку принятом байте данных закодировано состояние светодиода — если там в младшем бите единичка, то светодиод включается, а если нолик, то гаснет. Если же в поле bRequest принято значение CUSTOM_RQ_GET_STATUS, то в ответ заполняется буфер текущим состоянием светодиода, и данные буфера отправляются обратно хосту. Все очень просто, и при желании поведение кода можно легко переделать под свои нужды.

Видео, как это работает:



Буду рад ответить в комментариях на вопросы и конструктивные замечания.

Ссылки


1. V-USB.
2. Atmel Studio 6.
3. WinAVR.
4. LibUsbDotNet C# USB Library.
5. Visual Studio 2010 Express.
6. Исходный код для микроконтроллера и для ПО хоста.

P. S. Толчком к познаниям стал сайт microsin.net, где нашлось много информации по протоколам USB и практическому применению микроконтроллеров в USB-устройствах. Огромное спасибо! разработчику этого сайта за отзывчивость и помощь в вопросах касаемо данной тематики.

В дальнейшем по возможности планирую сделать тоже самое, но на микроконтроллере с аппаратным интерфейсом USB.
Tags:
Hubs:
+29
Comments20

Articles