STM32 USB, QT, Libusb, шаблон проекта

В очередной раз потребовался USB интерфейс в проекте, решил написать краткую заметку ...

На стороне контроллера используется стандартная библиотека от ST («Custom_HID»), на ПК установлен Libusb (windows сборка — «libusb-win32»). Обмен происходит 50-байтовыми пакетами, используются две конечные точки.

Передается ряд простых параметров:
— системное время на плате
— напряжение после 3,3v LDO стабилизатора и входное 5v
— температура с внутреннего «temperature sensor» (ADC_Channel_16)
— условный серийный номер устройства

Так как о самом USB в связке с stm32 уже многократно писали на хабре и здесь, сразу приведу код:
Контроллер
Дескриптор немного изменен, описано два репорта:
/* CustomHID_ConfigDescriptor */
const uint8_t CustomHID_ReportDescriptor[CUSTOMHID_SIZ_REPORT_DESC] =
  {                    
    0x06, 0x00, 0xff,              // 	USAGE_PAGE (Generic Desktop)
    0x09, 0x01,                    // 	USAGE (Vendor Usage 1)
    // System Parameters
    0xa1, 0x01,                    // 	COLLECTION (Application)
    0x85, 0x01,                    //   REPORT_ID (1)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //   LOGICAL_MAXIMUM (1)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, 50, 	                   //   REPORT_COUNT (1)
    0xb1, 0x82,                    //   FEATURE (Data,Var,Abs,Vol)
    0x85, 0x01,                    //   REPORT_ID (1)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x91, 0x82,                    //   OUTPUT (Data,Var,Abs,Vol)

    0x85, 0x02,                    //   REPORT_ID (4)
    0x09, 0x02,                    //   USAGE (Vendor Usage 4)
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x95, 50, 	                   //   REPORT_COUNT (N)
    0x81, 0x82,                    //   INPUT (Data,Var,Abs,Vol)
    0xc0                           // 	END_COLLECTION
  };
Также параметр «wMaxPacketSize:» увеличен до 0x40 (Max).


int main(void)
{
    /* настройка USB из примеров */
    Set_System();
    USB_Interrupts_Config();
    Set_USBClock();
    USB_Init();
    /* настройка RTC, установка начального времени и т.п.*/
    SetParam();	
    /* настройка АЦП */
    ADC_Config();
    /* всего одна задача */
    xTaskCreate(USB_Response,0, configMINIMAL_STACK_SIZE, NULL, tskIDLE_PRIORITY, NULL );
    vTaskStartScheduler();
    while (1){}
}

Задача, подготавливаются и отправляются данные:

#define COEFF_0     (float)3.20
#define COEFF_1     (float)10.8
#define V25	    (float)1.38
#define AVG_SLOPE   (float)4.3
#define BUF_SIZE     50
void USB_Response(void *pvParameters)
{
    uint8_t i, i2;
    uint8_t UsbTrans[BUF_SIZE];
    float tempFloatPar;
    char *PointChar;
    sMain_Setting * PointMainSetting = NULL;
    for(;;)
    {
        /* Update Value ADC */
	PointMainSetting = (sMain_Setting*)&Main_Setting;
	for(i=0;i<ADC_CHANELS;i++)
	{
	    if(i==0)
	    {
	        PointMainSetting->ADC_Values[i] = ((COEFF_0 * ADC_Result[i])/4095);
	    }
	    if(i==1)
	    {
		PointMainSetting->ADC_Values[i] = ((COEFF_1 * ADC_Result[i])/4095);
	    }
	    if(i==2)
	    {
	    	PointMainSetting->ADC_Values[i] = ((V25 -((ADC_Result[i])* VREF)/4095)*1000/AVG_SLOPE)+25.0;
         }
	i =0;
	/* Send Pack USB */
	UsbTrans[i++]= USB_CurrentIndex.USB_Index_Catalog;
	UsbTrans[i++]= USB_CurrentIndex.USB_Index_SubCatalog;
	UsbTrans[i++]= USB_CurrentIndex.USB_Index_Param_One;
	UsbTrans[i++]= USB_CurrentIndex.USB_Index_Param_Two;
	switch(USB_CurrentIndex.USB_Index_Catalog)
	{
	    case U_CATLOG_SYS_PAR: 
	    if(USB_CurrentIndex.USB_Index_Param_One == U_SYS_PAR1)
	    {
	        /*Date-Time, Value ADC, SerialNumber */
		UpdateCalendar();
		UsbTrans[i++]=Main_Setting.Data_Time.year & 0x00FF;
		UsbTrans[i++]=Main_Setting.Data_Time.month;
		UsbTrans[i++]=Main_Setting.Data_Time.day;
		UsbTrans[i++]=Main_Setting.Data_Time.hour;
		UsbTrans[i++]=Main_Setting.Data_Time.minutes;
		UsbTrans[i++]=Main_Setting.Data_Time.seconds;
		
		/* get ADC 3,3 and 5 v */
		for(i2=0;i2<ADC_CHANELS;i2++)
		{
		    tempFloatPar = Main_Setting.ADC_Values[i2];
		    PointChar = (char*)&tempFloatPar;
		    UsbTrans[i++]= *PointChar; PointChar ++;
		    UsbTrans[i++]= *PointChar; PointChar ++;
		    UsbTrans[i++]= *PointChar; PointChar ++;
		    UsbTrans[i++]= *PointChar; 
		}
		/* Serial Number */
		PointChar = (char*)&Main_Setting.SerialNumberDev;
		UsbTrans[i++] = *PointChar; PointChar ++;
		UsbTrans[i++] = *PointChar; PointChar ++;
		UsbTrans[i++] = *PointChar; PointChar ++;
		UsbTrans[i++] = *PointChar; PointChar ++;
	    }
	break;
        }
	PrevXferComplete = 0;
	USB_SIL_Write(EP1_IN, (uint8_t*)&UsbTrans, BUF_SIZE);
    	SetEPTxValid(ENDP1);
    	vTaskDelay(1);
    }
}


Если все правильно, (после прошивки) при включении должно обнаружится новое USB-HID устройство.
VID-PID оставлен по умолчанию (0x483,0x5750)


Для работы с классом HID устройств на стороне ПК — существует несколько известных библиотек:
— HID API
— HID.dll
— USB HID for C#
— Libusb

Остановился на Libusb по причине кроссплатформености (хотя отличия под Win32 и Linux есть).
Скачав последную версию Libusb, распаковав, в каталоге «libusb\bin» обнаружил:

inf-wizard.exe — «конструктор» драйверов для устройств
в папках amd64-ia64-x86 находятся два системных файла (libusb0.dll и libusb0s.sys)
— libusb0.dll нужно скопировать в папку C:\Windows\System32
— libusb0.sys в C:\Windows\System32\drivers
Дальше необходимо установить «фильтр» на наше устройство.
Для этого запустим «install-filter-win.exe», выберем устройство по VID-PID номеру

После установки, устройство в системе будет видится как принадлежащее к «libusb-win32 devices»,
чтобы высвечивалось его название — для него необходимо создать inf файл, через «inf-wizard.exe».

После установки:


QT:
В первую очередь добавить пути к распакованному Libusb (в pro файл проекта)
LIBS +=          C:/libusb/lib/gcc/libusb.a
INCLUDEPATH +=   C:/libusb/libusb/include/
DEPENDPATH +=    C:/libusb/libusb/include/
PRE_TARGETDEPS+= C:/libusb/lib/gcc/libusb.a
В исходнике понадобится всего один дополнительный инклуид:
#include "C:\libusb\include/lusb0_usb.h"
Определения:
// Device vendor and product id.
#define MY_VID 0x0483
#define MY_PID 0x5750
// Device endpoint(s)
#define EP_IN 0x81
#define EP_OUT 0x01
// Device of bytes to transfer.
#define BUF_SIZE 50
// Device configuration and interface id.
#define MY_CONFIG 1
#define MY_INTF 0

Все остальное:

    usb_dev_handle *dev = NULL;
    usb_dev_handle *open_dev(void);

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{
    Timer.setInterval(1000);
    connect(&Timer, SIGNAL(timeout()), this, SLOT(ReadyCheck()));
    Timer.start();
    usb_init();
    usb_find_busses();
    usb_find_devices();

   if (!(dev = open_dev()))
   {
        printf("error opening device: \n%s\n", usb_strerror());
        QMessageBox::information(NULL, "Error", "Not Found Device!");
        return;
   }
   else
   {
        printf("success: device %04X:%04X opened\n", MY_VID, MY_PID);
        Connect_status = true;
   }

   if (usb_set_configuration(dev, MY_CONFIG) < 0)
   {
       printf("error setting config #%d: %s\n", MY_CONFIG, usb_strerror());
       usb_close(dev);
       QMessageBox::information(NULL, "Error", "error setting config");
       return;
   }
   else
   {
       printf("success: set configuration #%d\n", MY_CONFIG);
   }

   if (usb_claim_interface(dev, 0) < 0)
   {
       printf("error claiming interface #%d:\n%s\n", MY_INTF, usb_strerror());
       usb_close(dev);
       QMessageBox::information(NULL, "Error", "error claiming interface");
       return;
   }
   else
   {
       printf("success: claim_interface #%d\n", MY_INTF);
   }
}

void MainWindow::ReadyCheck()
{
    int ret=0;
    QString StrTemp;

    char USB_data[BUF_SIZE];
    if (!dev)
    {
        Connect_Stat->setStyleSheet("background-color: red");
        usb_close(dev);
        printf("Done.\n");
    }
    ret = usb_bulk_read(dev, EP_IN, USB_data, BUF_SIZE, 2000);
    if (ret < 0)
    {
        printf("error reading:\n%s\n", usb_strerror());
        Connect_Stat->setText("Not connection");
        Connect_Stat->setStyleSheet("background-color: red");
        Lab_VID_PID_num->setText("-");
        Lab_VID_PID_num->setStyleSheet("color: rgb(0, 0, 0)");
    }
    else
    {
        printf("success: bulk read %d bytes\n", ret);
        Connect_Stat->setText("Connect");
        Connect_Stat->setStyleSheet("background-color: green");
        Lab_VID_PID_num->setText("0x" + QString::number(MY_VID,16) + " 0x"+QString::number(MY_PID,16));
        Lab_VID_PID_num->setStyleSheet("color: rgb(100, 0, 0)");

        /* Parsing Read Data */
        switch(USB_data[0])
        {
            case U_CATLOG_SYS_PAR:
                /* Year, Month, Day */
                Lab_SysTimeData->setText(QString::number(2000 + USB_data[4],10) + "-");
                Lab_SysTimeData->setText(Lab_SysTimeData->text() + QString::number(USB_data[5],10)+ "-");
                Lab_SysTimeData->setText(Lab_SysTimeData->text() + QString::number(USB_data[6],10)+ "-");
                Lab_SysTimeData->setText(Lab_SysTimeData->text() + QString::number(USB_data[7],10)+ "-");
                Lab_SysTimeData->setText(Lab_SysTimeData->text() + QString::number(USB_data[8],10)+ "-");
                Lab_SysTimeData->setText(Lab_SysTimeData->text() + QString::number(USB_data[9],10));
                /* ADC Values */
                float TempFloat=0;
                char * pChar=0;
                pChar = (char*)&TempFloat;
                *pChar = USB_data[10];  pChar ++;
                *pChar = USB_data[11];  pChar ++;
                *pChar = USB_data[12];  pChar ++;
                *pChar = USB_data[13];
                Lab_Value_ADC_Pow->setText(QString::number(TempFloat,'g', 4)+"v, ");
                pChar = (char*)&TempFloat;
                *pChar = USB_data[14];  pChar ++;
                *pChar = USB_data[15];  pChar ++;
                *pChar = USB_data[16];  pChar ++;
                *pChar = USB_data[17];
                Lab_Value_ADC_Pow->setText(Lab_Value_ADC_Pow->text() + QString::number(TempFloat,'g', 4)+"v,");

                pChar = (char*)&TempFloat;
                *pChar = USB_data[18];  pChar ++;
                *pChar = USB_data[19];  pChar ++;
                *pChar = USB_data[20];  pChar ++;
                *pChar = USB_data[21];
                Lab_Temperature->setText(QString::number(TempFloat,'g', 4)+"°c");

                /* Serial number */
                pChar = (char*)&ret;
                *pChar = USB_data[18];  pChar ++;
                *pChar = USB_data[19];  pChar ++;
                *pChar = USB_data[20];  pChar ++;
                *pChar = USB_data[21];
                Lab_SerialNum_Info->setText(QString::number(ret, 10));
            break;
        }
    }
}
usb_dev_handle * open_dev(void)
{
    struct usb_bus *bus;
    struct usb_device *dev_local;

    for (bus = usb_get_busses(); bus; bus = bus->next)
    {
        for (dev_local = bus->devices; dev_local; dev_local = dev_local->next)
        {
            if (dev_local->descriptor.idVendor == MY_VID
                    && dev_local->descriptor.idProduct == MY_PID)
            {
                dev = usb_open(dev_local);
                return usb_open(dev_local);
            }
        }
    }
    return NULL;
}

Отправка с ПК на плату в этом примере не используется, хотя это можно использовать для переключения отсылаемого контента (максимальные 64 байта накладывают ограничения).
Для отправки можно использовать функцию «usb_usb_bulk_write()», в микроконтроллере возникнет прерывание и из обработчика можно забрать пришедшие данные и в зависимости от содержимого — поменять отсылаемые на ПК данные.
void EP1_OUT_Callback(void)
{
    uint8_t Data[BUF_MAX];
    USB_SIL_Read(EP1_OUT, Data);
    SetEPRxStatus(ENDP1, EP_RX_VALID);
}


Для разбора и поиска ошибок очень помог UsbLyzer (USB сниффер).
Он выдает полную информацию по USB устройству (Endpoint, Report, длины и т.д.), + наглядно отображает принятые-отправленные пакеты и их содержимое.
  • +7
  • 11 сентября 2014, 16:50
  • khomin
  • 2
Файлы в топике: Firmware.zip, qt.zip

Комментарии (45)

RSS свернуть / развернуть
примеры прикрепил
0
А где водится этот USBlyzer?
0
  • avatar
  • Vga
  • 11 сентября 2014, 16:59
он коммерческий, триал версия 30 дней.
Есть бесплатный UsbSnoop, но у меня так и не взлетел (
0
USBlyzer рекомендую применять вместе с RevoUninstaller'ом. Раз в 30 дней.
0
ну или так link
0
USBlyzer, кстати, очень приятная и удобная штука: уже не один девайс с его помощью «срисовал».
0
  • avatar
  • esp
  • 11 сентября 2014, 17:29
версию 2.1 крякнуть не получилось )
Перешел на free device monitoring studio
Доволен как слон )
0
А где оно водится? Кажется, разработчики уже убрали фри версию.
0
да нет, только вчера скачал с офсайта
На фри версию накладывается куча ограничений, но работать можно и пакеты нормально снифает
0
Дай линк. Мож я не тот сайт нашел, потому как там, где нашел — предлагается слить триал.
0
+1
Да, это действительно совсем не то, что я нашел.
0
Спасибо, надо попробовать при случае.
0
Какая максимальная скорость передачи данных (размер пакета и минимальный интервал передачи) через USB HID?
0
Low-speed, 10—1500 Кбит/c
Максимальный пакет — 64 байта (первый байт — дескриптор, считай 63).
интервал затрудняюсь ответить…
0
интервал 1 мсек — фрейм USB :-) соответственно, 64000 байт/сек
0
Для LS HID интервал опроса 8 фреймов, ЕМНИП, так что 8000 байт/с.
0
  • avatar
  • Vga
  • 11 сентября 2014, 19:11
Всем спасибо за ответы.
0
там битовая скорость меньше, кроме того…
0
Поясни.
0
  • avatar
  • Vga
  • 11 сентября 2014, 21:57
п. 5.6 Using USB terminology, a device may send or receive a transaction every USB
frame (1 millisecond). A transaction may be made up of multiple packets (token,
data, handshake) but is limited in size to 8 bytes for low-speed devices and 64
bytes for high-speed devices.
www.usb.org/developers/hidpage/HID1_11.pdf
0
А, да, еще размер пакета на LS ограничен. На FS ЕМНИП тоже 64 байта.
Насчет опроса каждые 8 фреймов — это ограничение, вероятно, не со спекой связано, а с виндой. Ее HID драйвер опрашивает LS устройства каждые 8 фреймов (125Гц). Геймерские мыши с увеличенной частотой опроса (250-500-1000Гц) то ли патчат драйвер в винде, то ли ставят свой.
0
  • avatar
  • Vga
  • 12 сентября 2014, 06:41
а может они просто на full speed работают? 1 мс как раз во фрейм укладывается
0
Вы использовали устаревшую libusb версии 0.1, а в linux скорее всего смотрели версию 1.0. Поэтому и увидели различия. На текущий момент существует libusb-1.0 для windows и для linux (и еще для ряда платформ) они абсолютно идентичны, код переносится без проблем. Вообще библиотеке очень мощная, хотя и пришлось повозиться пока подключал к builder 6.
0
  • avatar
  • sprut
  • 11 сентября 2014, 18:55
connect(&Timer, SIGNAL(timeout()), this, SLOT(ReadyCheck()));

А почему вы не используете новый синтаксис для connect()? ИМХО, он безопаснее и удобнее. Или вы ориентируетесь на Qt4?
0
  • avatar
  • e_mc2
  • 11 сентября 2014, 20:06
Qt5 ещё пока нету. Когда KDE будет — будет смысл перелезать. А там и вкусности constexpr из 14 С++ с assert'ами могут запилить в Qt и сигнал-слотовая систем наконец будет разрешаться в компил-тайме.
0
Qt5 ещё пока нету. Когда KDE будет — будет смысл перелезать.

А кокой смысл ориентироваться на KDE? KDE — по сути, дополнительный фреймворк, который базируется на Qt. В силу своего масштаба — он достаточно «консервативен», и сильно отстает (по времени внедрения) от развития Qt.

 А там и вкусности constexpr из 14 С++ с assert'ами могут запилить в Qt

Это больше зависит от компилятора: вы и сейчас можете использовать, например, анонимный код (С++11), хотя внутренняя реализация Qt5 придерживаются (ЕМНИП) стандарта С++98

 сигнал-слотовая систем наконец будет разрешаться в компил-тайме

Здесь не понял. Ведь динамическое связывание — это именно основная идея «сигналов и слотов».
0
А теперь немного хорошек:
Одно из свойств С++ — полиморфизм и наследование — порождает классы с виртуальными членами. Виртуальные члены порождают рантайм накладки — vtable.
А хорошие компилляторы способны соптимизировать вызов виртуальной функции потомка через обращенияе к предку не используя vtable, разрешая всё в compile-time.
Теперь представим с вами, что connect — это constexpr-выражение, которое на стадии компилляции может понять, что on_button1Click — это слот только для сигнала button1, и не генерить Qt'шный рантайм.
Да и много из moc-обработки, делаемое сейчас препроцессором, может перейти в constexpr + немного шаблонов, что повысит информативность выдачи отладчика и сделает код более прозрачным для компиллятора и его оптимизаций.
0
А хорошие компилляторы способны соптимизировать вызов виртуальной функции потомка через обращенияе к предку не используя vtable, разрешая всё в compile-time.

Разве что в каких-то очень предельно упрощенных случаях. Идея полиморфизма именно в динамике, когда на стадии компиляции не известно ни компилятору, ни даже программисту, который писал этот код, какой именно вариант переопределенного метода вызывать.

connect — это constexpr-выражение, которое на стадии компилляции может понять, что on_button1Click — это слот только для сигнала button1

Это не constexpr — я могу соединять сигнал от кнопки соединять с разными слотами (динамически, в зависимости от логики), потом вызвать disconnect() и связать все по другому. И т. д. В общем случае, я динамически могу пересвязывать сигналы (для условного, on_button1Click()) и компилятор никак это не может оптимизировать на этапе компиляции.
+1
Разве что в каких-то очень предельно упрощенных случаях.
и 99 % реальных

constexpr — это weak деректива. Поэтому если компиллятор не сможет на стадии сборки разрешить connect — оно останется в рантайме. Но вот если сможет (вызовы данной функции данного экземпляра, которые встречаются в приложении происходят с констснтными параметрами) — то сам бог велел разрешать в compile-time.
0
А как определяете пропажу устройства во время работы в винде?
Вопрос не праздный: лезть в винду не хочу, а иногда проблемы у моих устройств под виндой из-за кривых операторов/кабалей возникают.
0
  • avatar
  • dekar
  • 11 сентября 2014, 23:33
в примерах на Libusb было почти так же:

    ret = usb_bulk_read(dev, EP_IN, USB_data, BUF_SIZE, 2000);
    if (ret < 0)
    {
        /* ошибка */    
    }
    else
    {
        /* нормально */
    }
Думается мне — задумка такова, что за указанный «int timeout» пакет от устройства должен успевать приходить.
0
не знаю. Можете проверить в винде?
0
ну в windows так и работает, в Linux не знаю
0
А проверить удавалось?
В Linux'е я просто слушаю udev
0
да, конечно.
usb_bulk_read возвращает -1 при удачном чтении, это и используется.
За несколько часов работы не возникало нештатных ситуаций, да и скорости высокие лично мне не требуются )
0
т.е. наоборот
0
в винде есть событие WndProc
сообщение 0x8000 — новое подключение
сообщение 0x8004 — отключение
0
Вот бы кто запилил ликбез по Qt для электронщиков…
0
Все в ваших руках. Качаете qtcreator и книгу Бланшет «Qt4-бла-бла-бла». Потом последовательно и методично повторяете, что там написано. К концу книги вы уже матерый кутешник! Главное — руками перебить все тексты (а не копипастить из прилагаемого диска) иначе ничего не получиться.
+3
Книга скачана, первый шаг сделан! Спасибо!
0
Еще одна очень хорошая книга Макс Шлее «Qt4. Профессиональное программирование на C++».
0
а что надо поменять в усб конфиге, чтобы виделось 2 разных устройства как в мфу принтер и сканер?
0
Надо сделать USB устройство с двумя интерфейсами. Часто так делаю.
0
есть пример?
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.