Программирование AVR: работа с USB AVR и USB: это просто! Sat, April 20 2024  

Поделиться

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

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

AVR и USB: это просто! Печать
Добавил(а) microsin   

Я хотел создать свое собственное устройство USB на микроконтроллере AVR, поскольку точно знал, что такое возможно. Начал разбираться с проектами USBtiny [2] и V-USB. В процессе исследований оказалось, что достаточно простого руководства по этой теме нигде нет, поэтому решил написать свое (перевод статьи [1]).

Примечание: это руководство не для фанатов Arduino, а для тех, кто хочет самостоятельно с нуля разобраться в том, как работает микроконтроллер. Подразумевается, что читатель имеет представление о таких понятиях, как напряжение, ток, резистор, конденсатор, мультиметр, программатор, AVRDUDE, avr-gcc, make, makefile, фьюзы, USB, хост, дескриптор, энумерация и т. п., или хотя бы готов самостоятельно прогуглить эти термины и разобраться. Рассматривается работа в условиях операционной системы Windows, хотя многие вопросы могут быть решены и на операционных системах Linux (есть некоторые различия, касающиеся устройства программного обеспечения на стороне хоста).

[Питание микроконтроллера]

Начну с самого начала - как организовать питание для создаваемого устройства USB. Для этого нам понадобится следующее:

• Кабель USB и линейка из 4 контактов с шагом 2.54 мм.
• Маленькая макетная плата для навесного монтажа (breadboard) и несколько проводов для перемычек.
• Светодиод (LED) и резистор на 330 Ом.
• Микросхема линейного стабилизатора с низким падением напряжения на 3.3V (Low dropout voltage regulator), наподобие LD1086V33 или LE33CZ.

Кабель USB. Он нужен для того, чтобы подключить к порту USB компьютера наше устройство USB на микроконтроллере. Возьмите обычный стандартный кабель USB, и отрежьте со стороны коннектора USB Type A male кусок достаточной длины, чтобы удобно было подключать Вашу отладочную плату к компьютеру. Обрезанный конец зачистите, и 4 провода шнура распаяйте на линейку контактов. Ниже в таблице показана цоколевка кабеля и цвета проводов. Имейте в виду, что цвета и количество проводов на кабеле USB могут отличаться от приведенных в таблице данных (например, может быть экран кабеля, или другие цвета проводов), поэтому на всякий случай проверьте цоколевку кабеля мультиметром.

USB type A male

№ контакта Цвет провода Назначение
1 Красный VCC (+5V)
2 Белый D-
3 Зеленый D+
4 Черный Земля (общий провод, GND)

На фотографии показан полученный результат - кабель для подключения к USB компьютера. Когда будете зачищать изоляцию кабеля, будьте осторожны и не повредите провода. Убедитесь, что после изготовления кабеля провода не замыкают друг на друга, иначе можно вывести из строя Ваш компьютер или хаб USB!

v usb tutorial cable

Примечание: если Вы хотите узнать больше про коннекторы USB и их электрические сигналы, рекомендую ознакомиться с руководством "USB in a NutShell" от компании Beyond Logic (перевод на русский язык этого замечательного руководства см. по ссылке [5]), и конечно же стандарт USB 2.0. На данный момент достаточно знать, что шина USB может предоставить небольшую мощность для питания подключаемого устройства USB от примерно 5V постоянного тока (обычно не больше 100 мА; максимум может быть 500 мА, если у дескрипторах устройства USB прописано соответствующее значение).

Простой тест кабеля и платы макетирования. Давайте теперь попробуем проверить наш кабель в работе. Рекомендую сначала подключить кабель к хабу USB (или к компьютеру), и мультиметром измерить напряжение на контактах 1 и 4 кабеля (красный VCC и черный GND). Мультиметр должен показать напряжение около 5V (допустимый диапазон около 4.8V..5.2V). После этого на плате макетирования соберите простейшую схему из светодиода и резистора 330 Ом, и подключите туда наш кабель, чтобы провода VCC и GND подали напряжение на светодиод через резистор. Светодиод должен зажечься. Если светодиод не загорелся, то проверьте, не перепутали ли Вы у него полярность, и правильно ли Вы запаяли контакты и собрали схему.

v usb tutorial cable test

Поздравляю! Теперь если захотите получить 5V питание от USB, не превышая при этом ограничение по току, то Вы знаете как это сделать.

Получение 3.3V. В то время как питание USB осуществляется напряжением 5V, сигналы данных шины USB (D+, D-) требуют логических уровней 3.3V. Если мы подадим питание 5V на микроконтроллер, то он будет работать с логическими уровнями 5V, что будет несовместимым с уровнями сигналов шины USB. В общем случае, как это рекомендуется в руководстве библиотеки V-USB, можно решить проблему согласований уровней сигналов следующими способами:

• Ограничить напряжение питания, поступающее от USB, уровнем 3.3V.
• Подать питание на схему внешнего источника постоянного напряжения 3.3V.
• Использовать резисторы, диоды или стабилитроны (диоды Зенера), чтобы преобразовать уровни сигналов 5V к сигналам уровня 3.3V.

Предлагаю выбрать первый вариант. Второй вариант Вы можете использовать на свое усмотрение, используя понравившийся источник питания. Вариантов много: это может быть 9V батарейка с регулятором напряжения 3.3V, зарядное устройство для мобильного телефона с регулятором напряжения 3.3V, лабораторный блок питания с выставленным выходным напряжением 3.3V, 3 последовательно соединенные батарейки AA и 2 диода (чтобы погасить лишнее напряжение 1.4..1.5 вольта). Для третьего варианта Вы можете взять за основу примеры схем организации интерфейса USB из библиотеки V-USB, или просто прогуглите картинки по ключевым словам zener diode usb. На страничке V-USB wiki есть хороший обзор этих опций (см. [4]).

В этой статье как пример будет рассмотрено использование микросхемы линейного регулятора LD1086V33. Как можно узнать из даташита на эту микросхему, у неё вывод 1 должен быть подключен к общему, минусовому проводу (земля, GND, шасси компьютера), вывод 2 будет выходом стабилизатора (выдает напряжение 3.3V) и вывод 3 вход (на него должно быть подано напряжение +5V от интерфейса USB, красный провод, подключенный к выводу 1 коннектора USB, см. таблицу выше).

v usb tutorial linear lowdropout voltage regulator 3V3 LD1086V33 TO220

Для снижения помех (пульсаций напряжения питания) на входе и выходе регулятора можно подключить фильтрующие (блокировочные) конденсаторы номиналом 1..22 мкф. Получится такая схема:

v usb tutorial linear lowdropout voltage regulator 3V3 sch

Соберите на плате макетирования схему стабилизатора, подключив его вход к шинам VCC и GND 4-выводного коннектора, и светодиод подключите к выходу регулятора через резистор:

v usb tutorial regulator 3V3 maket1

Добавьте в схему фильтрующие конденсаторы на 10..22 мкф рабочим напряжением не менее 6.3V (соблюдая полярность!). Еще раз проверьте правильность всех соединений, и подключите шнур к порту USB.

v usb tutorial regulator 3V3 maket2

Светодиод должен загореться. Проверьте мультиметром напряжение на выходе стабилизатора (напряжение между выводами 1 и 2 микросхемы LD1086V33), оно должно быть 3.3V. Теперь все готово к тому, чтобы подать питание на Ваш первый AVR-проект, работающий как устройство USB.

[ATtiny2313, V-USB]

В этом руководстве для создания устройства USB был использован AVR-микроконтроллер ATtiny2313 и библиотека V-USB. Для экспериментов понадобятся следующие детали:

• Макетная плата побольше и дополнительные провода для перемычек.
• Микроконтроллер ATtiny2313.
• Кварцевый резонатор на 12 МГц.
• Два маленьких керамических конденсатора на 22..27 пф (для стабилизации работы кварцевого генератора).
• Два резистора на 68 Ом (они будут подключены между ножками микроконтроллера и сигналами D+ и D- интерфейса USB).
• 1 МОм pull-down резистор для сигнала D+ и pull-up резистор 1.5 кОм для сигнала D-.
• 6-pin коннектор для программирования ATtiny2313 и pull-up резистор 4.7 кОм для вывода RESET микроконтроллера.

Примечание 1: по даташиту использование ATtiny2313 на частоте 12 МГц и напряжении питания 3.3V будет вне гарантированного рабочего диапазона (тактовые частоты больше 10 МГц требуют напряжения питания 4.5V или выше). Но обычно микроконтроллер все равно нормально работает в условиях питания 3.3V и тактовой частоты 12 МГц. Если у Вас это не получилось, то используйте напряжение питания 5V, и согласование уровней D+ и D- с помощью стабилитронов на 3.6V (диоды Зенера 3V6).

Примечание 2: часто конденсаторы на 27 пф можно не устанавливать, кварцевый генератор микроконтроллера нормально запускается и без них.

Соединения на плате должны быть выполнены по вот этой схеме:

v usb tutorial ATtiny2313 schematic

Для программирования микроконтроллера нужно подключить коннектор ISP.

v usb tutorial ISP6 connector

Сборку можно осуществлять в любом удобном порядке, например так:

1. Соберите схему стабилизатора напряжения на LD1086V33 в одном из углов платы. Вместо подключения 5V от USB к положительной шине питания платы, подключите 5V непосредственно к ножке входа микросхемы стабилизатора.

2. Используйте провода перемычек, чтобы соединить выход генератора и общий провод GND с шинами питания платы.

3. Если на плате есть несколько шин питания, то вы можете соединить их друг с другом для удобства.

4. Дважды проверьте правильность подключения микросхемы регулятора напряжения и полярность подключения электролитических конденсаторов.

5. Подключите USB, и мультиметром проверьте наличие напряжения 3.3V на шинах питания платы.

6. Установите микроконтроллер в плату, и выполните проводами соединения цепей питания, pullup резистора сброса, и 6-pin коннектора программирования.

7. Ваш программатор может подавать питание на программируемый микроконтроллер, то отключите эту возможность.

8. Используйте программатор, чтобы проверить нормальный отклик микроконтроллера (это подтвердит, что микроконтроллер получает нужное питание и работает должным образом).

9. Подключите светодиод LED к ножке порта PB0 через резистор, и прошейте в память микроконтроллера простейший тест (об этом подробнее далее). После программирования отключите программатор, отключите питание (отключением шнура USB) и снова подключите его - светодиод должен мигать.

10. Подключите pulldown и pullup резисторы 1 МОм и 1.5 кОм к сигналам D+ и D- соответственно.

11. Через резисторы 69 Ом соедините D+ и D- с портами PD2 и PD3 соответственно.

12. Подключите кварцевый резонатор 12 МГц к выводам PA1 и PA0 микроконтроллера, и подключите к ним на GND керамические конденсаторы 27 пФ, как это показано на принципиальной схеме.

В результате получится примерно такая конструкция:

v usb tutorial ATtiny2313 maket1

На фото ниже показано подробнее, как собран стабилизатор питания, и как подключен кабель от USB. Обратите внимание, что напряжение 5V подключено только ко входу регулятора напряжения и входному фильтрующему конденсатору, и никуда более.

v usb tutorial ATtiny2313 maket2

[Простейший тест, мигающий светодиодом]

Если Вы еще не тестировали свой макет с ATtiny2313 на работоспособность, то самое время заняться этим. Самый простой способ тестирования: скомпилировать и прошить в память микроконтроллера вот такую программу:

#include < avr/io.h >
#define F_CPU 1000000UL       // 1 МГц
#include < util/delay.h >
 
int main()
{
   //Конфигурирование ножки PB0 как выход, чтобы
   // можно было управлять свечением светодиода:
   DDRB |= 1;
   
   while(1)
   {
      PORTB |= 1;       // Зажечь светодиод
      _delay_ms(500);
      PORTB &= ~1;      // Погасить светодиод
      _delay_ms(500);
   }
  
   return 1;
}

Эта программа хороша не просто тем, что проста, она дает хорошую возможность понаблюдать, как ускорится программа, когда мы фьюзами переключим тактовую частоту микроконтроллера с 1 Мгц на 12 МГц: светодиод будет мерцать в 12 раз быстрее.

По умолчанию фьюзы микроконтроллера ATtiny2313 (в том состоянии, в каком он приходит с завода Atmel) установлены так, что тактирование происходит от внутреннего тактового генератора 8 Мгц, частота которого поделена на 8. В результате получается тактовая частота 1 МГц. Чтобы в 12 раз ускорить работу программы, нужно, во-первых, фьюзами отключить делитель тактовой частоты (его коэффициент деления должен быть 1) и во-вторых переключить тактирование на внешний генератор с частотой выше 8 МГц и временем запуска 14 тактов + 4.1 мс. Это означает, что в младший байт фьюзов должно быть записано значение 0xEF, что делается следующей командой:

avrdude -U lfuse:w:0xef:m

Если Вы используете другой микроконтроллер, или хотите сами разобраться, для чего нужны биты фьюзов и как получить байты значений фьюзов для командной строки AVRDUDE, то пользуйтесь замечательным ресурсом AVR Fuse Calculator [6].

После того, как программатором Вы обновили значение байта lfuse, выключите и включите питание схемы, и проверьте, что светодиод теперь мигает 12 раз в секунду. Теперь обновите значение F_CPU на значение 12000000L, чтобы отразить новую тактовую частоту проекта, перекомпилируйте его и запишите в память микроконтроллера, и светодиод опять начнет мигать с частотой 1 Гц.

Теперь все готово к тому, чтобы начать эксперименты с библиотекой V-USB, которая позволит нам создать настоящее устройство USB и наладить обмен данными между устройством USB и компьютером.

[Добавление библиотеки V-USB к проекту]

Сначала скачайте последнюю версию библиотеки V-USB с сайта OBdev [3]. Следуйте ссылке Download, и найдите в списке последний по дате ZIP-архив. Дата создания релиза библиотеки представлена в имени файла архива в формате vusb-YYYYMMDD.zip (где YYYY год, MM месяц, DD дата).

Примечание: на момент перевода статьи последняя версия библиотеки V-USB была в архиве vusb-20121206.zip. Если Вы работаете под управлением Linux-системы, то возможно Вам удобнее будет скачать архив в формате tar.gz (файл vusb-20121206.tar.gz).

Распакуйте архив, в нем увидите папку usbdrv - это ядро библиотеки V-USB. Сделайте копию этой папки в корневом каталоге Вашего проекта (всю папку целиком, а не просто её содержимое). Зайдите в эту папку и сделайте копию файла usbconfig-prototype.h под новым именем usbconfig.h. Найдите в этом файле строки, которые определяют порт AVR и разряды этого порта, куда подключены сигналы D+ и D- USB (у нас D+ подключен к порту PD2, и D- подключен к PD3), и строку, где задается рабочая тактовая частота микроконтроллера (12 МГц). Проверьте и при необходимости исправьте эти строки, чтобы они отражали реальную схему Вашего проекта:

#define USB_CFG_IOPORTNAME      D
#define USB_CFG_DMINUS_BIT      3
#define USB_CFG_DPLUS_BIT       2
#define USB_CFG_CLOCK_KHZ       12000

Также будет хорошей идеей убедиться, что библиотека V-USB укажет в дескрипторах информацию для компьютера, что устройство получает питание от шины USB (т. е. не имеет собственного источника питания), и потребляет максимальный ток 50 мА (по умолчанию обычно задано 100 мА):

#define USB_CFG_IS_SELF_POWERED         0
#define USB_CFG_MAX_BUS_POWER           50

Вы наверное знаете, что в каждом устройстве USB имеются специальные лицензируемые идентификаторы VID и PID, которые характеризуют фирму-производителя устройства и его продукт. Мы воспользуемся идентификаторами, любезно предоставленными компанией OBdev, так что здесь нет необходимости что-то менять в конфигурации. Однако Вы можете захотеть поменять имя производителя (vendor name) и имя устройства (device name). Обратите внимание, что в этом примере обратный слеш используется для того, чтобы для удобства разделить имя на 2 части и разместить эти части в 2 строках:

#define  USB_CFG_VENDOR_ID       0xc0, 0x16 /* = 0x16c0 */
#define  USB_CFG_DEVICE_ID       0xdc, 0x05 /* = 0x05dc */
 
#define USB_CFG_DEVICE_VERSION  0x00, 0x01
 
#define USB_CFG_VENDOR_NAME     'c', 'o', 'd', 'e', 'a', 'n', 'd', 'l', \ 
                               'i', 'f', 'e', '.', 'c', 'o', 'm'
#define USB_CFG_VENDOR_NAME_LEN 15
 
#define USB_CFG_DEVICE_NAME     'U', 'S', 'B', 'e', 'x', 'a', 'm', 'p', 'l', 'e'
#define USB_CFG_DEVICE_NAME_LEN 10

Параметры USB_CFG_VENDOR_NAME_LEN и USB_CFG_DEVICE_NAME_LEN должны отражать точную длину имени вендора и имени устройства соответственно, так что если после изменений у Вас получится другая длина имен, то Вы должны в этих строках указать правильное десятичное значение длины строки в символах.

Файл заголовка usbconfig.h (это конфигурационный файл библиотеки V-USB) очень хорошо документирован, в нем подробно описаны все настраиваемые опции библиотеки. При желании Вы можете изучить содержимое этого файла, чтобы ознакомиться с возможностями, которые может предоставить библиотека для создания устройства USB. Теперь осталось сделать только одну вещь - должен быть модуль кода на языке C, который воспользуется библиотекой. Вот чистая версия main.c, с которой мы начнем делать реализацию устройства USB:

#include < avr/io.h >
#include < avr/interrupt.h >
#include < avr/wdt.h >
 
#include "usbdrv.h"
 
#define F_CPU 12000000L
#include < util/delay.h >
 
USB_PUBLIC uchar usbFunctionSetup(uchar data[8])
{
   return 0; // пока ничего не делаем
}
 
int main()
{
   uchar i;
   
   //Разрешить работу сторожевого таймера с периодом 1 сек:
   wdt_enable(WDTO_1S);
 
   usbInit();
   usbDeviceDisconnect();  // принудительная повторная энумерация
   for(i=0; i < 250; i++)
   { // ожидание 500 мс
      wdt_reset();         // сброс сторожевого таймера
      _delay_ms(2);
   }
   usbDeviceConnect();
   
   // Разрешить прерывания после повторной энумерации:
   sei();
   
   while(1)
   {
      wdt_reset();         // сброс сторожевого таймера
      usbPoll();
   }
    
   return 0;
}

Этот код довольно прост для понимания. Здесь выполняется следующее:

• Заголовочный файл usbdrv.h подключается, чтобы можно было получить доступ к функциям V-USB.

• Реализуется функция usbFunctionSetup(), в которой обрабатываются запросы USB от хоста (мы это рассмотрим подробнее).

• В функции main на период 1 секунду настраивается сторожевой таймер (watchdog), который автоматически сбросит микроконтроллер, если вызовами wdt_reset() не сбрасывать сторожевой таймер в течение 1000 мс.

• Вызов usbInit() для инициализации библиотеки V-USB.

• Принудительная повторная энумерация устройства USB с использованием вызова usbDeviceDisconnect(), задержки 500 мс (во время этой задержки сторожевой таймер сбрасывается каждые 2 мс) и вызова usbDeviceConnect().

• Разрешение прерываний.

• Бесконечный цикл, внутри которого постоянно сбрасывается сторожевой таймер и вызывается функция библиотеки обработки протокола USB: usbPoll().

Сторожевой таймер используется для того, чтобы исправить возможную проблему зависания программы из-за какой-то ошибки (например, были ошибочно прочитаны данные или в работе бесконечного цикла есть ошибка) - из-за зависания устройство USB могло бы перестать отвечать на запросы хоста. Если произойдет зависание, то перестанет периодически вызываться wdt_reset(), и по истечении 1 секунды сторожевой таймер автоматически сбросит микроконтроллер, и вся программа начнет выполняться сначала (т. е. произойдет то же самое, что происходит при аппаратном сбросе микроконтроллера, или при выключении/включении его питания). Хотя работа сторожевого таймера не абсолютно необходимая функция, его использование является хорошей практикой, и позволяет иногда быстро находить ошибки в программе, а также избавляет пользователя от необходимости извлекать устройство из порта USB и подключать его заново, если из-за зависания в программе с ним происходит что-то странное.

Другой момент, который может Вас поначалу удивить - зачем нужна начальная процедура отключение/задержка/подключение (disconnect/delay/connect). Причина в том, что хост USB может запомнить идентификатор, назначенный устройству USB, даже если наше устройство USB было сброшено, и забыло этот идентификатор. Принудительная повторная энумерация гарантирует, что и хост USB, и наше устройство USB получили одинаковый ID, используемый для обмена по шине USB.

Теперь давайте посмотрим, сможем ли мы это скомпилировать. Самый простой способ - использовать готовый Makefile из примеров проектов V-USB, который уже содержит все необходимые инструкции для компилятора avr-gcc, который вызывается из командной строки для компиляции исходного кода. Поместите файл makefile в корневой каталог Вашего проекта, и выполните команду run make main.hex, чтобы увидеть, как это работает. Таким способом Вы также увидите действительные команды, которые Makefile использует для процесса компиляции, причем это не относится специально к библиотеке V-USB. Основное, что Вам следует помнить, это то, что опция -Iusbdrv задает поиск заголовочных файлов библиотеки (так называемые хедеры, файлы с расширением .h) в каталоге usbdrv, и что будут созданы промежуточные объектные файлы библиотеки (usbdrv.o, oddebug.o, usbdrvasm.o), которые потом будут объединены в один .elf файл вместе с объектным кодом main.o.

Этот makefile комбинированный, т. е. он используется как для компиляции ПО хоста, так и для компиляции прошивки микроконтроллера, и для управления программатором микроконтроллера, чтобы можно было записать прошивку.

Вы можете использовать отдельные файлы makefile для ПО хоста и для микроконтроллера, за основу берите примеры из пакета библиотеки V-USB [3].

# Здесь используется кросс-компилятор (тулчейн) WinAVR
CC = avr-gcc
OBJCOPY = avr-objcopy
DUDE = avrdude
 
# Если Вы не используете ATtiny2313 и программатор USBtiny,
# обновите строки ниже, чтобы они соответствовали Вашему оборудованию:
CFLAGS = -Wall -Os -Iusbdrv -mmcu=attiny2313
OBJFLAGS = -j .text -j .data -O ihex
DUDEFLAGS = -p attiny2313 -c usbtiny -v
 
# Объектные файлы, которые преобразуются в двоичный код firmware
# (использование usbdrv/oddebug.o не обязательно)
OBJECTS = usbdrv/usbdrv.o usbdrv/oddebug.o usbdrv/usbdrvasm.o main.o
 
# Клиент командной строки (ПО хоста)
CMDLINE = usbtest.exe
 
# По умолчанию будет компилироваться firmware и ПО хоста, 
# и не будет происходить прошивка кристалла программатором
all: main.hex $(CMDLINE)
 
# Это позволит прошить firmware в память FLASH микроконтроллера,
# если просто ввести команду "make flash" в командной строке
flash: main.hex
	$(DUDE) $(DUDEFLAGS) -U flash:w:$<
 
# Строка для компилирования ПО хоста из файла usbtest.c
$(CMDLINE): usbtest.c
	gcc -I ./libusb/include -L ./libusb/lib/gcc -O -Wall usbtest.c -o usbtest.exe -lusb
  
# Очистка проектов
clean:
	$(RM) *.o *.hex *.elf usbdrv/*.o
 
# Преобразование .elf файла в .hex
%.hex: %.elf
	$(OBJCOPY) $(OBJFLAGS) $< $@
 
# Main.elf требует дополнительные объектные файлы для сборки firmware,
# не только main.o
main.elf: $(OBJECTS)
	$(CC) $(CFLAGS) $(OBJECTS) -o $@
 
# Без этой зависимости .o файлы не будут перекомпилироваться,
# если Вы изменили конфигурацию!
$(OBJECTS): usbdrv/usbconfig.h
 
# Преобразование исходного кода C в объектные файлы с расширением .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@
 
# Преобразование исходного кода ассемблера в объектные файлы с расширением .o
%.o: %.S
	$(CC) $(CFLAGS) -x assembler-with-cpp -c $< -o $@

Примечание: имейте в виду, что отступы слева в тексте makefile должны быть обязательно сделаны с помощью символа табуляции, ни в коем случае не пробелов, иначе утилита make будет работать некорректно. Помните об этом, когда редактируете makefile Вашим текстовым редактором (в редакторе текста должны быть заданы специальные настройки, позволяющие использовать символы табуляции).

[Как устройство USB отвечает на запросы управления]

Прежде чем рассматривать программирование на стороне ПО хоста (т. е. программы компьютера, которая обменивается данными с устройством USB), давайте допишем код устройства USB (программа микроконтроллера, т. е. firmware) так, чтобы оно реагировало на 2 основные команды, которые будут посланы через USB: включить светодиод и выключить светодиод. Обмен по шине USB всегда начинается по инициативе хоста, когда он посылает запрос по шине USB. Есть несколько разновидностей запросов, в данном примере будут использоваться так называемые управляющие запросы (control messages). Эти запросы по стандарту используются для того, чтобы устройство могло вернуть хосту свое имя и другие различные параметры, которые хранятся в дескрипторах устройства USB. Стандарт оставляет место и для специальных управляющих сообщений, функцию которых определяет сам производитель (так называемые vendor-specific control message). Давайте определим два таких сообщения:

#define USB_LED_OFF 0
#define USB_LED_ON  1

Библиотека V-USB написана так, что она автоматически вызовет функцию usbFunctionSetup(), когда устройство USB получит vendor-specific control message. В параметре функции data передается указатель на область памяти (буфер, массив из 8 байт), который в действительности содержит структуру usbRequest_t (эта структура определена в файле usbdrv.h). Предлагаю Вам ознакомиться с содержимым структуры. В этот момент нас интересует поле bRequest структуры, которое будет равно либо 0 (что соответствует запросу USB_LED_OFF, погасить светодиод), или 1 (USB_LED_ON, зажечь светодиод). Впоследствии мы увидим, как в ПО хоста создаются такие управляющие запросы, так что ничего магического тут не происходит. Просто со стороны компьютера ПО хоста генерируется заранее известные запросы, и они заранее известным образом интерпретируются на стороне устройства USB. Вот модифицированный вариант функции usbFunctionSetup(), которая будет зажигать или гасить светодиод LED:

// Эта функция будет вызвана, когда будет принято управляющее сообщение,
// определенное пользователем (производителем), так называемое
// custom control message, или vendor-specific control message.
USB_PUBLIC uchar usbFunctionSetup(uchar data[8])
{
   // Приведение data к нужному типу:
   usbRequest_t *rq = (void *)data;
   
   switch(rq->bRequest)
   { // в поле bRequest будет передана команда для светодиода:
   case USB_LED_ON:
      PORTB |= 1;    // LED on
      return 0;
   case USB_LED_OFF: 
      PORTB &= ~1;   // LED off
      return 0;
   }
   // Если все нормально, то сюда мы никогда не должны попадать:
   return 0;
}

Конечно же, чтобы светодиод действительно зажегся, нам нужно было ранее настроить ножку порта PB0 как выход (это делается в начале функции main(), до входа в бесконечный цикл). Советую это сделать сразу после строки "uchar i;":

DDRB = 1; // ножка порта PB0 будет работать как выход

Скомпилируйте код (для этого выполните сначала команду make clean и затем make hex). Теперь все готово для записи программы в память микроконтроллера. Запишите полученный файл main.hex в память FLASH ATtiny2313. Если у Вас подключен к компьютеру и к микроконтроллеру программатор, соответственным образом сконфигурированный в makefile, то можно просто выполнить команду make flash, и скомпилированный код запишется в память микроконтроллера.

Примечание: чтобы код работал правильно, микроконтроллер должен быть фьюзами сконфигурирован на работу с тактовой частотой 12 МГц (с использованием внешнего кварцевого резонатора).

[Windows: заморочки с драйвером LibUSB]

Если Вы работаете под операционной системой Linux, то поздравляю: все что нужно, это просто скачать библиотеку LibUSB и скомпилировать её вместе с исходным кодом, чтобы получить работоспособное ПО хоста, которое обменивается данными с Вашим новым устройством USB. Однако на Windows для библиотеки LibUSB нужен специальный настраиваемый драйвер устройства (custom device driver). Самое неприятное, что иногда по этой причине старые примеры ПО хоста из библиотеки V-USB не работают под 64-bit Windows 7 (сейчас все поменялось к лучшему, см. примечание ниже). Поэтому я обрисую в общих чертах основные шаги по созданию и установке этого драйвера.

Прим. переводчика: проблема раньше была связана с тем, что есть задержка с появлением совместимых с Windows 7 версий библиотеки LibUSB и драйвера фильтра, и в отсутствии цифровой подписи драйвера. Как декларируется на сайте libusb-win32 [7], начиная с версии 1.2.0.0 библиотека LibUSB поддерживает операционные системы Vista/7/2008/2008R2 64 bit благодаря тому, что Microsoft KMCS (Kernel-Mode Code Signing Walkthrough, система проверки цифровой подписи в режиме ядра) принимает цифровую подпись, встроенную в драйвер ядра libusb0.sys. По крайней мере, лично у меня описанная ниже процедура нормально работает на Windows 7 64 бита. Так что единственное неудобство в том, что эту процедуру нужно проделать, и устанавливать драйвер каждый раз, когда Вы переключаете свое устройство в другой порт USB.

Необходимость в наличии специального драйвера под Windows даже для устройств USB HID это особенность именно библиотеки LibUSB. Есть множество других библиотек под Windows (см. [8]), для которых не нужно устанавливать драйвер, однако эти библиотеки работают по-другому, и для них по-другому пишется ПО хоста.

Итак, процесс установки драйвера по шагам:

1. Перейдите на главную страничку проекта libusb-win32 [7].

2. Найдите заголовок "Device driver installation" (установка драйвера устройства) и следуйте инструкциям для загрузки последней версии. Сейчас текущая последняя версия libusb-win32-bin-1.2.6.0.zip (913.2 кбайт). Распакуйте содержимое архива в отдельную папку с именем libusb, которую создайте в каталоге проекта.

3. Подключите свое устройство USB к компьютеру.

4. Запустите программу inf-wizard.exe, которая находится в подкаталоге bin, и следуйте инструкциям. Эта программа создаст драйвер для Вашего USB-устройства. Рекомендую Вам сохранить этот драйвер в подкаталоге driver, созданном в корневом каталоге Вашего проекта.

5. После того, как Вы сохранили драйвер, установите его. Самый простой способ - прямо в мастере нажать кнопку Install, специально созданную для этой цели.

После описанной процедуры Ваше устройство USB будет нормально управляться через ПО хоста, написанное на основе библиотеки LibUSB. Если не заработало сразу, то попробуйте переподключить Ваше устройство в тот же самый порт USB. Вообще можно было бы написать отдельное руководство, касающееся моего опыта проб и ошибок, когда я разбирался, как установить и заставить драйвер устройства LibUSB. В случае возникновения у Вас проблем вот несколько полезных подсказок:

• Вы можете использовать Менеджер Устройств Windows (Device Manager) чтобы деинсталлировать неправильно установленные драйверы Вашего устройства USB.

• Если Вы получаете сообщение системы об не опознанном устройстве "Unknown device", то возможно проблема в схеме или в прошивке устройства, или следует обновить драйвер устройства.

• При установке драйвера выберите вариант ручного указания пути для драйвера ("Browse my computer for driver software"), и укажите Windows папку, в которую Вы сохранили драйвер (в этой папке находится созданный мастером inf-файл). Не обращайте внимания на предупреждения об отсутствии сертификата драйвера ("drivers not certified"), и подтвердите установку драйвера без сертификата.

• Обычно Windows корректно запоминает (кэширует) подключение устройства к одному и тому же порту USB, так что не нужно беспокоиться о том, что устройство подключено и отключено к порту USB множество раз. Однако иногда это не так, и переключение устройства к другому порту USB или к другому хабу требует повторной установки драйвера.

• Также для сброса кэша помогает перезагрузка компьютера.

Ниже приведены скриншоты, которые Вы будете наблюдать при запуске мастера создания драйвера (INF-wizard, bin\inf-wizard.exe:

v usb tutorial INF wizard select USB device

В этом окне можно вручную поменять параметры устройства, если что-то пошло не так (обычно не нужно ничего менять).

v usb tutorial INF wizard edit USB device parameters

После того, как кликните на последнем скриншоте Next, Вам предложат сохранить сгенерированный драйвер (укажите папку driver, созданную в корневом каталоге Вашего проекта). После этого появится окно, где хорошей идеей будет нажать на кнопку "Install now...", чтобы избежать последующей ручной процедуры установки драйвера. Установка драйвера может длиться довольно долго (нужно подождать 5 минут или более).

[ПО хоста: передача команд в устройство USB]

ПО хоста - это программа, работающая на хосте USB. Под хостом USB подразумевается компьютер (на шине USB всегда есть 1 хост и может быть одно или большее количество устройств USB), так что ПО хоста это компьютерная программа, которая управляет устройством USB.

Давайте сначала разберем, как создать и скомпилировать ПО хоста. Для этого рекомендую использовать компилятор GNU C gcc из проекта MinGW и набор инструментария MSYS [8] (их нужно предварительно установить), однако возможно использовать и другие компиляторы наподобие Visual C. Инсталлятор MinGW найти на сайте [8] довольно трудно (камень в огород авторов Wiki, которые совсем не озаботились удобством использования). Ищите в левой колонке меню сайта ссылку Downloads, которая должна привести Вас на страницу загрузки "MinGW - Minimalist GNU for Windows" сайта sourceforge.net.

В предыдущей секции, когда мы разбирали установку драйвера LibUSB, мы уже установили libusb-win32, и все нужные библиотечные и заголовочные файлы есть в наличии (это то, что Вы распаковали в подкаталог libusb проекта). Теперь просто сделайте копию файла со странным именем lusb0_usb.h (копии дайте имя usb.h) в папке libusb/include, все готово к началу работы.

Первое, что нужно сделать - написать маленькую вспомогательную функцию, чтобы расшифровать строки дескрипторов USB, используемые для имени производителя (vendor name) и имени устройства USB (device name). Для этой цели и впоследствии для обмена с нашим устройством USB будем использовать функцию usb_control_msg() из библиотеки LibUSB. В эту функцию нужно в основном передать хендл устройства (device handle), направление передачи сообщения (direction, USB_ENDPOINT_IN для направления передачи от устройства к хосту и USB_ENDPOINT_OUT для направления передачи от хоста к устройству) и другую информацию, используемую для определения получателя и направления, и само управляющее сообщения (control message request), коды индекса и значения, а также указатель на буфер ввода/вывода, размер буфера и максимально допустимое время таймаута ответа для вызова. Поначалу звучит дико, но все будет проще, когда мы рассмотрим конкретный пример.

Примечание: для тех, кто захочет лучше разобраться в управляющих сообщениях (control messages), рекомендую на удивление удобный для пользователя стандарт USB 2.0. Раздел, посвященный control messages, начинается на странице 248, и первая таблица должна быть 9-2 (стандарты USB можно найти на сайте usb.org). В стандарте приведены довольно простые и понятные значения констант, которые полностью отражаются операторами #define в библиотеке LibUSB, так что все будет понятнее, если Вы сравните константы и их описание из стандарта с константами, которые будут приведены далее в коде примера.

Если Вас интересует, что делает вспомогательная функция после вызова usb_control_msg, то она просто проверяет возвращаемое значение и длину ответа, и преобразует их из кодировки UTF16-LE к кодировке Latin1 (то же самое, что и ASCII при отсутствии специальных символов). Возвращаемый формат дескриптора USB жестко регламентирован, чтобы исключить любую ошибочную интерпретацию. Все это описано в части 9.5 стандарта USB. Вот код вспомогательной функции (это в основном немного исправленная версия вспомогательной функции, которая используется в примере V-USB PowerSwitch):

/* Функция используется для получения строк дескриптора
   при идентификации устройства */
static int usbGetDescriptorString(usb_dev_handle *dev, int index, int langid, 
                                  char *buf, int buflen)
{
   char buffer[256];
   int rval, i;
  
   // Создание стандартного запроса GET_DESCRIPTOR, тип string,
   //  с указанием индекса (например dev->iProduct):
   rval = usb_control_msg(dev, 
      USB_TYPE_STANDARD | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
      USB_REQ_GET_DESCRIPTOR, (USB_DT_STRING << 8) + index, langid, 
      buffer, sizeof(buffer), 1000);
   
    if(rval < 0) // если отрицательное число, то ошибка и возврат
      return rval;
   
   // rval должно содержать количество прочитанных байт, однако в buffer[0]
   // содержится реальный размер ответа
   if((unsigned char)buffer[0] < rval)
      rval = (unsigned char)buffer[0]; // string короче, чем количество прочитанных байт
   
   if(buffer[1] != USB_DT_STRING)      // во втором байте содержится тип данных
      return 0;                        // ошибочный тип данных
   
   // мы работаем с форматом UTF-16LE, так что в действительности символов наполовину
   // меньше rval, и индекс 0 не учитывается
   rval /= 2;
   
   /* Алгоритм преобразования (с потерями, т. е. фильтрацией) в ISO Latin1 */
   for(i = 1; i < rval && i < buflen; i++)
   {
      if(buffer[2 * i + 1] == 0)
         buf[i-1] = buffer[2 * i];
      else
         buf[i-1] = '?'; /* вне диапазона ISO Latin1 */
   }
   buf[i-1] = 0;
   
   return i-1;
}

Теперь эту вспомогательную функцию мы будем использовать при последовательном просмотре списка всех устройств USB в системе, чтобы распознать именно то устройство USB, которое нам нужно, по vendor name "codeandlife.com" и device name "USBexample". Документация по API библиотеки libusb-win32 дает нам хорошую начальную точку в секции examples. Не буду слишком долго останавливаться на описании, как это работает, основная логика в том, что по циклу опрашивается каждое устройство USB на шине, открывается к нему доступ, и запрашиваются имена device и vendor в устройстве, и в случае совпадения будет успешный возврат, а если совпадения не было, то происходит опрос следующего устройства в системе:

static usb_dev_handle * usbOpenDevice(int vendor, char *vendorName, 
                                      int product, char *productName)
{
   struct usb_bus *bus;
   struct usb_device *dev;
   char devVendor[256], devProduct[256];
  
   usb_dev_handle * handle = NULL;
  
   usb_init();
   usb_find_busses();
   usb_find_devices();
  
   for(bus=usb_get_busses(); bus; bus=bus->next)
   {
      for(dev=bus->devices; dev; dev=dev->next)
      {
         if(dev->descriptor.idVendor != vendor ||
            dev->descriptor.idProduct != product)
            continue;
  
         /* Чтобы запросить строки, нужно "открыть" устройство USB */
         if(!(handle = usb_open(dev)))
         {
            fprintf(stderr,
                    "Warning: cannot open USB device: %sn",
                    usb_strerror());
            continue;
         }
  
         /* получение vendor name */
         if(usbGetDescriptorString(handle, dev->descriptor.iManufacturer, 
                                   0x0409, devVendor, sizeof(devVendor)) < 0)
         {
            fprintf(stderr, 
                    "Warning: cannot query manufacturer for device: %sn", 
                    usb_strerror());
            usb_close(handle);
            continue;
         }
  
         /* получение product name */
         if(usbGetDescriptorString(handle, dev->descriptor.iProduct, 
                                   0x0409, devProduct, sizeof(devVendor)) < 0)
         {
            fprintf(stderr, 
                    "Warning: cannot query product for device: %sn", 
                    usb_strerror());
            usb_close(handle);
            continue;
         }
  
         if(strcmp(devVendor, vendorName) == 0 && 
            strcmp(devProduct, productName) == 0)
            return handle;
         else
            usb_close(handle);
      }
    }
  
    return NULL;
}

Число 0x0409 код языка для English, это также можно найти в стандарте USB. Обратите внимание, как должны быть получены имена vendor и device с помощью нашей вспомогательной функции – стандартный дескриптор устройства в dev говорит только о значениях параметра, которые нам нужны для использования usbGetDescriptorString(), чтобы получить соответствующие имена (поля iManufacturer и iProduct). Это понятно, поскольку у дескриптора устройства длина постоянная, но у имен vendor и device длина может быть разная.

Теперь, когда мы преодолели все эти трудности использованием вспомогательной функции, можно просканировать все устройства USB, и возвратить специально отформатированные сообщения дескриптора, чтобы просто получить хендл нужного устройства. Остальная часть кода очень простая. Обмен через USB отличается от стандартного порядка доступа к файлу только вызовом usb_control_msg(), в остальном делается все то же самое, обмен с устройством проходит шаги открыть (open) -> что-то сделать -> закрыть (close). Вот код основной программы (ПО хоста), которая общается с устройством USB:

int main(int argc, char **argv)
{
   usb_dev_handle *handle = NULL;
   int nBytes = 0;
   char buffer[256];
  
   if(argc < 2)
   {
      printf("Usage:\n");
      printf("usbtext.exe on\n");
      printf("usbtext.exe off\n");
      exit(1);
   }
  
   handle = usbOpenDevice(0x16C0, "codeandlife.com", 0x05DC, "USBexample");
  
   if(handle == NULL)
   {
      fprintf(stderr, "Could not find USB device!n");
      exit(1);
    
   if(strcmp(argv[1], "on") == 0)
   {
      nBytes = usb_control_msg(handle, 
                               USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
                               USB_LED_ON, 0, 0, (char *)buffer, sizeof(buffer), 5000);
   }
   else if(strcmp(argv[1], "off") == 0)
   {
      nBytes = usb_control_msg(handle, 
                               USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
                               USB_LED_OFF, 0, 0, (char *)buffer, sizeof(buffer), 5000);
   }
  
   if(nBytes < 0)
      fprintf(stderr, "USB error: %sn", usb_strerror());
  
   usb_close(handle);
  
   return 0;
}

Обратите внимание, что второй параметр для вызова usb_control_msg теперь использует USB_TYPE_VENDOR, чтобы показать, что теперь отправляются специальные управляющие сообщения, которые определены самим производителем, т. е. нами (vendor-specific control message). Как можно увидеть, параметры request, value и index (здесь USB_LED_ON/OFF, 0, 0) можно свободно использовать в нашем коде на стороне устройства USB.

Вы можете взять этот готовый код в архиве [10]. В файле usbtest.c архива есть две строки с #defines (для USB_LED_OFF, USB_LED_ON) которые соответствуют используемым константам main.c. Скомпилировать код можно, если выполнить команду make usbtest.exe. В действительности можно выполнить просто make, потому что usbtest.exe является одной из целей по умолчанию (makefile default targets). Теперь Вы можете управлять свечением светодиода в устройстве USB, вводя в консоли команды "usbtest on" и "usbtest off". Еще раз обращаю Ваше внимание, что для компиляции кода ПО хоста у Вас должны быть установлен MinGW, и команда gcc в консоли должна запускать компилятор. Компиляция, которая происходит при запуске в консоли команды "make", запустит вот это:

gcc -I ./libusb/include -L ./libusb/lib/gcc -O -Wall usbtest.c -o usbtest.exe -lusb

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

Если Вы внимательно рассмотрели наш код ПО хоста, то вероятно заметили, что для переключения светодиода использовались управляющие сообщения USB_ENDPOINT_IN, и в нашем распоряжении есть буфер размером 256 байт, чтобы получить любые данные, которые отправляет устройство. До сего момента мы не получали данные, и возвращаемое из функции usb_control_msg значение, сохраняемое в переменной nBytes было равно 0. Давайте теперь это изменим.

Если Вы читали документацию в файле usbdrv/usbdrv.h, особенно комментарии, относящиеся к usbFunctionSetup(), которую мы использовали на стороне нашего устройства USB со светодиодом LED, то можете увидеть, что есть два способа вернуть данные из usbFunctionSetup():

1. Установкой глобального указателя usbMsgPtr на блок памяти (static RAM, статическая оперативная память), где размещены данные, и возвратом длины данных из функции usbFunctionSetup (ранее мы из неё возвращали 0).

2. Создать код функции usbFunctionRead(), чтобы сделать это самостоятельно.

Мы будем использовать первый метод. Сначала определим дополнительное управляющее сообщение USB_DATA_OUT (ранее мы определили сообщения USB_LED_OFF и USB_LED_ON), и статический буфер, чтобы сохранить в нем данные, которые мы хотим отправить из устройства к компьютеру:

#define USB_DATA_OUT 2
 
static uchar replyBuf[16] = "Hello, USB!";

Затем добавим еще одну ветку case в функцию usbFunctionSetup() на стороне устройства USB, чтобы передать содержимое буфера к компьютеру:

case USB_DATA_OUT: // отправка данных к PC
   usbMsgPtr = replyBuf;
   return sizeof(replyBuf);

Неужели так просто?.. Да, это так, библиотека V-USB берет на себя всю остальную работу.

Теперь выполните make clean и make hex чтобы получить новую прошивку для микроконтроллера, и запрограммируйте её в память ATtiny2313 (или просто выполните make flash, если у Вас подключен программатор, описанный в makefile проекта микроконтроллера).

Теперь всего лишь осталось обновить нашу утилиту командной строки (ПО хоста), чтобы она понимала дополнительную команду "out". Сделайте копию оператора #define USB_DATA_OUT 2 в начало файла usbtest.c (сразу за имеющимися определениями для USB_LED_OFF и USB_LED_ON) и добавьте третий оператор if в функцию main:

   ...
}
else if(strcmp(argv[1], "out") == 0)
{
   nBytes = usb_control_msg(handle, 
                            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
                            USB_DATA_OUT, 0, 0, (char *)buffer, sizeof(buffer), 5000);
   printf("Got %d bytes: %sn", nBytes, buffer);
}
...

Выполните make clean и make, чтобы получить новый исполняемый файл usbtest.exe. Теперь попробуйте выполнить команду usbtest out, что получится? У микроконтроллера ATtiny2313 есть в распоряжении всего лишь 128 байт RAM, и библиотека и сам код программы устройства USB уже частично задействовали эту память, так что нельзя организовать буфер максимального размера из 254 байт (реализуйте функцию usbFunctionRead() так, чтобы она передавала большие блоки данных по частям), однако даже 16 байт это довольно много! И конечно Вы можете соединить в цепочку несколько запросов управления, чтобы передать больше данных.

[Как передать больше данных от хоста к устройству USB]

Ранее мы просто передавали в устройство USB флажок, говорящий о том, нужно ли включить или наоборот погасить светодиод, для этого достаточно одного байта. Но как быть, если нужно передать несколько байт?

Тем же самым способом, которым мы передали команду для управления светодиодом, можно передать до 4 полезных байт информации, для этого используют параметры wValue и wIndex запроса. Давайте рассмотрим, как это делается. Определите новое управляющее сообщение USB_DATA_WRITE. По нашему замыслу это сообщение будет менять часть "USB!" сообщения на любые другие 4 символа. Как обычно, сначала добавьте новый оператор #define в файлы main.c (программа для микроконтроллера) и в usbtest.c (ПО хоста).

Примечание: Вы наверное заметили, что неудобно каждый раз дублировать один и тот же код сразу в двух местах, как в этом случае, когда мы определяем новые запросы. Поэтому можно создать отдельный заголовочный файл userctrlmessages.h, поместить определения #define для наших запросов (USB_LED_OFF, USB_LED_ON, USB_DATA_OUT, USB_DATA_WRITE) туда, и подключать этот файл директивой #include. Удобство в том, что если нужно изменить управляющие сообщения (или добавить новые), то нужно только отредактировать файл userctrlmessages.h.

Затем нужно добавить новую ветку case в код программы микроконтроллера:

case USB_DATA_WRITE: // модификация буфера ответа
   replyBuf[7] = rq->wValue.bytes[0];
   replyBuf[8] = rq->wValue.bytes[1];
   replyBuf[9] = rq->wIndex.bytes[0];
   replyBuf[10] = rq->wIndex.bytes[1];
   return 0;

Обратите внимание, что поля wValue и wIndex в действительности являются объединениями языка C (union) так что можно обращаться к содержимому этих полей побайтно, что и используется в этом коде (через суффикс .bytes), или как 16-разрядному слову (через суффикс .word). Я не уверен, что на всех компьютерах порядок байт (endiandness) будет таким, как здесь, так что имейте это в виду.

На стороне утилиты ПО хоста добавьте новый оператор "else if", который будет передавать для упрощения просто фиксированный набор из 4 символов (TEST):

   ...
}
else if(strcmp(argv[1], "write") == 0)
{
   nBytes = usb_control_msg(handle, 
                            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
                            USB_DATA_WRITE, 'T' + ('E' << 8), 'S' + ('T' << 8), 
                            (char *)buffer, sizeof(buffer), 5000);
}
...

Снова, как в прошлый раз, перекомпилируйте firmware для микроконтроллера, прошейте микроконтроллер, перекомпилируйте ПО хоста. Попробуйте выполнить команду usbtest write. Обратите внимание, что в ответ на эту команду ничего не будет напечатано, и результат действия этой команды Вы можете увидеть, если запустите другую команду: usbtest out (до команды usbtest write, и после неё). Будет видно, как изменился буфер ответа командой usbtest write. Также обратите внимание на то, что измененное содержимое буфера "Hello, TEST" будет храниться до тех пор, пока Вы не сбросите или не переподключите устройство USB.

[usbFunctionWrite(): как передать в устройство USB больше 4 байт]

Использование wValue и wIndex подойдет для большинства приложений, но чтобы перезаписать весь 16-байтный буфер, нам потребуется как минимум 4 управляющих сообщения. Попробуем поступить по-другому, и теперь создадим наше первое сообщение USB_ENDPOINT_OUT. Код на стороне компьютера будет очень простым:

#define USB_DATA_IN 4
  
   ...
}
else if(strcmp(argv[1], "in") == 0 && argc > 2)
{
   nBytes = usb_control_msg(handle, 
                            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, 
                            USB_DATA_IN, 0, 0, argv[2], strlen(argv[2])+1, 5000);
}
...

Обратите внимание, что размер буфера на 1 больше, чем длина аргумента 2 (т. е. передается strlen(argv[2])+1), чтобы передать также завершающий 0, который показывает окончание строки (string-terminating NULL character). На стороне устройства добавим новую переменную, чтобы запомнить, сколько байт нужно прочитать, и использовать специальное возвращаемое значение из usbFunctionSetup() (это хорошо документировано в файле usbdrv.h):

#define USB_DATA_IN 4
  
static uchar dataReceived = 0, dataLength = 0; // для USB_DATA_IN
  
...
  
   case USB_DATA_IN: // прием данных от компьютера
      dataLength = (uchar)rq->wLength.word;
      dataReceived = 0;
      
      if(dataLength > sizeof(replyBuf)) // ограничение по размеру буфера
         dataLength = sizeof(replyBuf);
   
      return USB_NO_MSG; // теперь будет вызвана функция usbFunctionWrite

Теперь запрос будет типа control-out (USB_ENDPOINT_OUT в коде ПО хоста) и мы вернули USB_NO_MSG (со значением размера, ограниченным максимальным значением байта 255, это как раз та причина, по которой данных может быть максимум 254 байта), так что V-USB вызовет функцию usbFunctionWrite() для приема данных от хоста:

USB_PUBLIC uchar usbFunctionWrite(uchar *data, uchar len)
{
   uchar i;
  
   for(i = 0; dataReceived < dataLength && i < len; i++, dataReceived++)
      replyBuf[dataReceived] = data[i];
  
   // вернем 1, если приняли все, иначе 0:
   return (dataReceived == dataLength);
}

Последнее, что нужно сделать, это модифицировать usbconfig.h, чтобы указать V-USB, что бы предоставили функцию usbFunctionWrite():

#define USB_CFG_IMPLEMENT_FN_WRITE      1

Снова все перекомпилируйте, перешейте микроконтроллер. Попробуйте выполнить команду usbtest in abcdefgh, чтобы заменить значения в 16-байтном буфере устройства. Имейте в виду, что если Вы предоставите строку больше 15 символов, то завершающий символ NULL второго аргумента ПО хоста уже не поместится в буфер, и команда usbtest out может привести к печати мусора после 16 первых символов. Это просто демонстрационное приложение, в котором для упрощения не добавлены проверки на переполнение буфера.

[usbFunctionRead(): как передать из устройства USB больше 4 байт]

Вы создали свое собственное устройство на микроконтроллере AVR, и разобрались, как передавать ему команды от компьютера, и как обмениваться с устройством USB данными в обоих направлениях. Мы пока не разбирали, как работает функция usbFunctionRead(), но если Вы освоили разобранный здесь пример с usbFunctionWrite(), то легко разберетесь самостоятельно и с usbFunctionRead(). Примеры и документация в библиотеке V-USB есть. Это позволит проще по частям передавать достаточно большие массивы данных из устройства USB в компьютер.

Ниже приведен пример использования usbFunctionRead с микроконтроллером ATtiny85. В конфигурационном файле usbconfig.h сделайте настройку, которая говорит библиотеке V-USB, что должны быть реализованы передачи размером больше 254 байта, и что есть поддержка для функции usbFunctionRead():

#define USB_CFG_IMPLEMENT_FN_READ       1
#define USB_CFG_LONG_TRANSFERS          1

Интересно, что если Вы определили использование длинных передач, библиотека V-USB все равно будет использовать переменную размером в байт для хранения длины передачи, так что передачи с использованием функции usbFunctionRead() будут работать неправильно с буферами на стороне ПО хоста длиной больше 254 байта. Так что задайте USB_CFG_LONG_TRANSFERS в 1, и используйте на стороне ПО хоста буфер размером 254 байта или меньше.

По какой-то странной причине стандартный метод V-USB с использованием usbMsgPtr работал нормально с буфером размером 256 байт. Так или иначе, после того, как мы сконфигурировали длинные передачи, возвращаемое значение usbFunctionSetup должно быть изменено. V-USB автоматически определяет usbMsgLen_t, чтобы был корректный тип данных, так что это мы будем использовать. Вот изменения в main.c на стороне микроконтроллера ATtiny85:

// [...] Добавьте константу для нового сообщения,
// и счетчик данных для него:
#define USB_DATA_LONGOUT 5
static int dataSent;
  
// [...] Поменяйте возвращаемый тип с uchar на usbMsgLen_t:
USB_PUBLIC usbMsgLen_t usbFunctionSetup(uchar data[8])
{
   // [...] Добавьте обработку USB_DATA_LONGOUT:
   case USB_DATA_LONGOUT: // send data to PC
      dataSent = 0;
      return USB_NO_MSG;
  
// [...] Добавьте новую функцию usbFunctionRead:
USB_PUBLIC uchar usbFunctionRead(uchar *data, uchar len)
{
   uchar i;
  
   for(i = 0; dataSent < 1024 && i < len; i++, dataSent++)
      data[i] = '0'+i;
  
   // Добавление терминатора строки, если принят последний байт:
   if(i && dataSent == 1024) 
      data[i-1] = 0; // NULL
      
   return i; // i равно количеству записанных байт
}

Здесь мы используем переменную dataSent, чтобы отследить, сколько байт было возвращено, и делаем выход, как только было отправлено 1024 байта. Как только usbFunctionRead вернет значение меньше, чем len, передача автоматически прерывается.

Изменения со стороны ПО хоста (usbtest.exe):

#define USB_DATA_LONGOUT 5
  
// [...] измените размер буфера в функции main():
char buffer[2048];
  
// [...] добавьте следующую ветку if-else в функцию main():
   ...
}
else if(strcmp(argv[1], "longout") == 0)  
{
   nBytes = usb_control_msg(handle, 
                            USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, 
                            USB_DATA_LONGOUT, 0, 0, (char *)buffer, sizeof(buffer), 5000);
   printf("Received %d bytes: %sn", nBytes, buffer);
}
...

[Мышь USB HID]

Аббревиатура HID расшифровывается как Human Interface Device, т. е. интерфейс для взаимодействия с человеком. Это специальный класс устройств USB, который распознается операционной системой Windows как стандартный, и в операционной системе уже имеется драйвер для взаимодействия с устройством HID. На устройствах USB HID делаются клавиатуры, мыши и различные устройства ввода/вывода.

На сайте библиотеки V-USB [3] есть много готовых проектов, которые реализуют разнообразные устройства USB HID. Здесь будет рассмотрен пример реализации мыши USB HID, и показаны изменения, которые нужно сделать в шаблоне конфигурационного файла usbconfig.h библиотеки V-USB.

usbconfig.h. Вот что нужно изменить:

1. USB_CFG_HAVE_INTRIN_ENDPOINT следует установить так, чтобы была добавочная конечная точка.
2. USB_CFG_INTR_POLL_INTERVAL установить на 100 мс вместо 10, как было указано в шаблоне.
3. USB_CFG_IMPLEMENT_FN_WRITE это не нужно, так что задайте тут 0.
4. Нужно поменять идентификатор устройства (Device ID) и имя устройства.
5. USB_CFG_DEVICE_CLASS должен быть установлен как 0, а не 0xff.
6. USB_CFG_INTERFACE_CLASS должен быть 3 вместо 0.
7. USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH должен соответствовать длине структуры репорта.

Вот пример таких изменений:

#define USB_CFG_HAVE_INTRIN_ENDPOINT    1
#define USB_CFG_INTR_POLL_INTERVAL      100
#define USB_CFG_IMPLEMENT_FN_WRITE      0
#define USB_CFG_IMPLEMENT_FN_READ       0
#define USB_CFG_DEVICE_ID               0xe8, 0x03
#define USB_CFG_DEVICE_NAME     'M', 'o', 'u', 's', 'e'
#define USB_CFG_DEVICE_NAME_LEN 5
#define USB_CFG_DEVICE_CLASS        0
#define USB_CFG_INTERFACE_CLASS     3
#define USB_CFG_HID_REPORT_DESCRIPTOR_LENGTH    52

main.c. Обзор изменений:

1. Дескриптор репорта должен предоставлять информацию о состоянии мыши, для чего реализован соответствующая структура.
2. Устройство мыши должно уметь обрабатывать несколько нужных запросов со стороны хоста USB.
3. В главном цикле функции main, когда разрешены прерывания от сигналов USB, нужно отправлять наш буфер репорта.

Пример из V-USB использует числа с фиксированной запятой 10.6 вычисленной функции синуса и косинуса, чтобы рисовать круги с помощью мыши, но я выбрал некоторые случайные перемещение по координатам, которые вырабатывает генератор псевдослучайных чисел. Очень смешно, когда к мыши добавляется несколько случайных скачков и кликов, что приводит в безумие пользователя.

typedef struct
{
   uchar   buttonMask;
   char    dx;
   char    dy;
   char    dWheel;
} report_t;
  
// ...
  
int main()
{
   int rand = 1234; // начальное значение
   // ...
   while(1)
   {
      wdt_reset(); // сброс watchdog
      usbPoll();
   
      if(usbInterruptIsReady())
      { // если прерывание готово, подготовить данные генератора псевдо-случайных
        // чисел, благодарю Dan Frederiksen @AVRfreaks
        // http://en.wikipedia.org/wiki/Linear_congruential_generator
         rand=(rand*109+89)%251;
          
         // переместить курсор в случайном направлении
         reportBuffer.dx = (rand&0xf)-8; 
         reportBuffer.dy = ((rand&0xf0)>>4)-8;
  
         usbSetInterrupt((void *)&reportBuffer, sizeof(reportBuffer));
      }
   }
}

Все примеры исходного кода, рассмотренные в статье, и готовые двоичные файлы Вы можете скачать по ссылкам [10].

[Ссылки]

1. AVR ATtiny USB Tutorial site:codeandlife.com.
2. USBtiny site:dicks.home.xs4all.nl.
3. V-USB site:obdev.at.
4. V-USB Hardware Considerations site:vusb.wikidot.com.
5. USB in a NutShell - путеводитель по стандарту USB.
6. Engbedded Atmel AVR® Fuse Calculator site:engbedded.com.
7. libusb-win32 site:sourceforge.net.
8. Библиотеки для управления устройствами USB HID.
9. Home of the MinGW and MSYS Projects site:mingw.org.
10. 160108usb_tutorial.zip - исходный код и прошивки устройства USB, исходный код управляющей программы ПО хоста и скомпилированный бинарник, и дополнительные файлы и материалы (библиотека V-USB, библиотека LibUSB, драйвер LibUSB). usb_tiny85_20120224.zip - код на микроконтроллере ATtiny85. usb_hid_20120211.zip - код для мыши USB HID.

 

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


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

Top of Page