Назад в библиотеку  

V-USV - программная реализация USB для AVR

  • Описание: Реализация протокола USB на микроконтроллерах семейства AVR

    Источник: https://xakep.ru/2013/09/14/v-usb/

  • Введение

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

    V-USB: размер имеет значение

    При разработке собственного гаджета часто встает проблема его подключения к компьютеру. Надо сказать, что порты LPT и COM — теперь экзотика на материнских платах ПК, не говоря о ноутбуках, у которых эти интерфейсы исчезли давным-давно. Поэтому у современных компьютеров практически не осталось альтернатив интерфейсу USB.

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

    Для микроконтроллеров Atmel существует замечательный проект V-USB, который предлагает программную реализацию низкоскоростного устройства USB 1.1. Код V-USB будет работать на любом устройстве AVR, у которого есть хотя бы 2 Кб Flash-памяти и 128 байт ОЗУ, с тактовой частотой 12; 12,8; 15; 16; 16,8 или 20 МГц.

    Использование продукта возможно как в рамках open source лицензии GPL, так и на коммерческой основе. Для того чтобы разрабатывать собственные USB-устройства, обычно также нужно покупать что-то вроде лицензии. Но ребята из V-USB позаботились и об этом, приобретя пару Vendor ID — Product ID и разрешив их использовать любому желающему.

    Аппаратная обвязка для подключения USB-шины к микроконтроллеру очень простая. Если устройство потребляет не слишком много, то запитать его можно прямо от шины (считается, что линия питания USB компьютера способна отдавать ток до 500 мА). Так как информационные линии (D+ и D–) используют уровень сигнала 3,6 В, кроме токоограничивающих резисторов, необходимы стабилитроны для согласования с 5-вольтовой логикой чипа. Чтобы обозначить тип подключения, нужно «подтянуть» напряжение питания через сопротивление 1,5 кОм к линии D–.


    Рисунок 1 – Подключение USB к микроконтроллеру ATtiny2313

    Альтернативный вариант сопряжения по USB — снизить напряжение питания контроллера посредством соответствующей микросхемы стабилизации или просто парой диодов. Последнюю схему можно найти на сайте проекта V-USB.

    Программатор USBtiny

    Для микроконтроллеров AVR существует множество различных программаторов. USBtiny здесь упоминается, в частности, потому, что содержит программную реализацию USB, аналогичную V-USB. Схема этого программатора проста: версия 2 содержит две микросхемы, а первая версия — лишь одну (собственно чип ATtiny2313). Благодаря подробному описанию на сайте и простым комплектующим устройство легко сделать даже начинающему. USBtiny совместим с популярной программой avrdude, используемой для программирования микроконтроллеров AVR.


    Рисунок 2 – Программатор USBtiny

    Единственная проблема заключается в заливке прошивки в чип программатора — для этого нужен… программатор. Если есть компьютер с LPT-портом, то можно сделать один из вариантов FBPRG ака «пять проводков».

    Готовим санки

    Программный инструментарий, необходимый для реализации простейшей прошивки USB-гаджета, предельно аскетичен: компилятор gcc-avr, библиотека avr-libc, программатор avrdude и набор binutils для AVR. В Debian/Ubuntu все, что нужно, устанавливается одной командой:

    $ sudo apt-get install avrdude binutils-avr gcc-avr avr-libc

    Рисунок 3 – Работа с программатором avrdude

    На безбрежных просторах интернета несложно найти очень подробное руководство по V-USB и libusb (на английском). Согласно мануалу, для добавления поддержки USB в проект потребуется папка usbdrv из архива с последней версией V-USB. В корне этой папки есть шаблон конфигурации usbconfig-prototype.h. Нужно сделать копию этого файла, назвав ее usbconfig.h. Далее — исправить usbconfig.h, указав порт (D), линии которого будут использоваться для ввода-вывода, непосредственно номера линии D+ (2) и D– (3), а также частоту (12 МГц), на которой работает чип (ATtiny2313):

        #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 для устройства, числовые идентификаторы производителя и устройства изменять не надо. А вот символьные имена можно выбрать по своему вкусу (они позволят отличить несколько устройств на основе V-USB, подключенных к одному и тому же компьютеру):

        #define USB_CFG_VENDOR_ID 0xc0, 0x16
        #define USB_CFG_DEVICE_ID 0xdc, 0x05
        #define USB_CFG_VENDOR_NAME 'n','e','t','s','4','g','e','e','k','s','.','c','o','m'
        #define USB_CFG_VENDOR_NAME_LEN 14
        #define USB_CFG_DEVICE_NAME 'U','S','B','e','x','a','m','p','l','e'
        #define USB_CFG_DEVICE_NAME_LEN 10
        

    Разнообразные гаджеты

    У тебя есть идея какого-нибудь устройства? Не спеши паять и кодить, а поищи, возможно, кто-то подобное уже делал. Если не получится воспользоваться готовыми схемами и исходниками, то хотя бы не придется начинать все с нуля.

    Например, проект V-USB благодаря лицензионной политике накопил приличную базу готовых (в том числе и свободно распространяемых) решений. Здесь можно найти различные реализации клавиатур, USB-адаптеров для джойстиков, геймпадов (в том числе и раритетных, например SNES/NES, Nintendo 64, ZX Spectrum джойстик, Sony PlayStation 1/2) и тому подобное. Адаптеры DMX, виртуальные порты COM и UART, i2c, Servo, беспроводные интерфейсы DCF77, IR — все, что поможет подключить к ПК больше новых устройств. Логгеры, платформы для датчиков и сенсоров, адаптеры для LCD-дисплеев, программаторы и загрузчики также могут оказаться полезными в хозяйстве.

    Программа для чипа — элементарно!

    При взаимодействии по шине USB компьютер — это главное устройство, которое периодически отправляет управляющие сообщения-запросы. Контроллер, соответственно, подчиненное и должен отвечать на запросы. Формат управляющего сообщения определяется структурой usbRequest_t из файла usbdrv.h:

        typedef struct usbRequest {
        uchar   bmRequestType;
        uchar   bRequest;
        usbWord_t   wValue;
        usbWord_t   wIndex;
        usbWord_t   wLength;
        } usbRequest_t;
        

    Создадим файл main.c на одном уровне с папкой usbdrv и опишем в нем необходимые заголовочные файлы, определения и переменные:

        #include 
        #include 
        #include 
        #include "usbdrv.h"
        #define F_CPU 12000000L // Частота МК
        #include 
        #define DATA_OUT 1 // Команда отправки
        #define DATA_IN 2  // Команда получения
    
        // Буфер
        static uchar replyBuf[16] = "Hello World!";
        static uchar dataLength = 0, dataReceived = 0;
        

    Далее научим контроллер принимать данные (DATA_IN) и отправлять их компьютеру (DATA_OUT). Тип запроса указывается в поле bRequest управляющего сообщения.

    В main.c переопределим функцию usbFunctionSetup, которая вызывается автоматически при получении нового запроса:

        USB_PUBLIC uchar usbFunctionSetup(uchar data[8]) {
          usbRequest_t *rq = (void *)data;
    
          switch(rq->bRequest) {
        case DATA_OUT: // Обработать команду отправки данных
          usbMsgPtr = replyBuf; // Указать буфер
          return sizeof(replyBuf); // Возвратить размер буфера
        case DATA_IN: // Обработка команды получения данных
          dataLength = (uchar)rq->wLength.word; // Получить длину
          dataReceived = 0; // Вызовов usbFunctionWrite будет много
          if(dataLength > sizeof(replyBuf)) // Проверка на переполнение
        dataLength = sizeof(replyBuf);
          return USB_NO_MSG; // Возвратить 255
          }
          return 0;
        }
        

    Как видно из листинга, самый простой способ отправить данные компьютеру — установить в usbFunctionSetup значение указателя usbMsgPtr на буфер ОЗУ (replyBuf), где находятся данные, а затем вернуть его длину. Размер буфера не должен превышать 254 байта. Для ATtiny2313 c его 128 байтами ОЗУ этого достаточно. Для более функциональных устройств есть второй способ — переопределение функции usbFunctionRead.

    Чтобы получить данные, во-первых, нужно в функции usbFunctionSetup извлечь длину сообщения из поля wLength запроса и сохранить ее в глобальной переменной dataLength. Во-вторых, в main.c требуется переопределить функцию usbFunctionWrite, предназначенную для обработки получаемых данных и вызываемую автоматически (и очевидно, несколько раз), если usbFunctionSetup возвращает значение USB_NO_MSG (255):

        USB_PUBLIC uchar usbFunctionWrite(uchar *data, uchar len) {
          uchar i;
          // Сохранить полученную порцию данных в буфер
          for(i = 0; dataReceived < dataLength && i < len; i++, dataReceived++)
        replyBuf[dataReceived] = data[i];
          return (dataReceived == dataLength);
        }
        

    Собственно, функция usbFunctionWrite занимается тем, что заполняет буфер replyBuf полученными данными.

    Кстати, чтобы этот метод работал, нужно внести изменения в usbconfig.h:

        #define USB_CFG_IMPLEMENT_FN_WRITE  1
        

    Ну и последняя функция прошивки — main:

        int main() {
          usbInit(); // Инициализировать USB
          usbDeviceConnect(); // Подключить устройство
          sei(); // Разрешить прерывания
          // В бесконечном цикле ждать управляющие сообщения
          while(1) usbPoll();
          return 0;
        }
        

    Задействуем USART/UART

    Хорошая альтернатива программному/аппаратному USB — использование в чипе популярного интерфейса USART/UART со сторонним преобразователем этого протокола в USB, который можно выполнить, например, на основе микросхемы FT232RL.

    Libusb: и не одетая, и не обнаженная

    Ты спросишь: а придется ли писать драйвер для операционной системы компьютера, чтобы подключить USB-устройство? Если использовать libusb, то можно обойтись без реализации полноценного модуля ядра. Libusb — это open source библиотека, которая позволяет быстро запрограммировать, во-первых, поиск устройства на шине, а во-вторых — обмен данными с ним.

    Под Linux библиотеку и необходимые заголовочные файлы можно получить из исходных кодов. А лучше воспользоваться стандартным репозиторием твоего дистрибутива. Для Debian/Ubuntu, например, так:

        $ sudo apt-get install libusb-dev
        

    Существует также порт libusb под Windows — libusb-win32. Вопреки названию проекта, также поддерживаются 64-битные ОС от Microsoft (начиная с версии 1.2.0.0).

    Но libusb — это отдельная тема разговора. Думаю, с программированием для ПК ты знаком и сможешь в этом разобраться сам. Поэтому буду краток. Создаем файл usbtest.c и начинаем наполнять его контентом. Сначала необходимые заголовочные файлы и определения:

        #include 
        [...]
        // Для компьютера смысл команд обратный,
        // но обозначения остаются те же
        #define DATA_OUT 1
        #define DATA_IN 2
        

    Функция usbOpenDevice для инициализации устройства:

        usb_init(); // Инициализировать USB
        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; // ...пропустить эту итерацию
        // Попробовать получить дескриптор устройства
        if(!(handle = usb_open(dev))) {
          fprintf(stderr, "%s\n", usb_strerror());
          continue;
        }
        return handle; // Вернуть дескриптор
          }
        }
        // Устройство не найдено
        return NULL;
        

    Как видно, параметрами usbOpenDevice выступают числовые идентификаторы производителя и устройства. В случае если устройство присутствует на шине, возвращается его дескриптор. Если устройств на V-USB будет несколько — придется дописать проверку символьных имен вендора и продукта.

    И функция main консольной утилиты usbtest:

        int main(int argc, char **argv) {
          // Дескриптор устройства
          usb_dev_handle *handle = NULL;
          int nBytes = 0;
          char buffer[256];
    
          // Ищем устройство
          handle = usbOpenDevice(0x16C0, 0x05DC);
          if(handle == NULL) {
        fprintf(stderr, "Could not find USB device!\n");
        exit(1);
          }
    
          // Аргумент out — получить данные от чипа
          if(strcmp(argv[1], "out") == 0) {
        nBytes = usb_control_msg(handle,
        USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN,
        DATA_OUT, 0, 0, (char *)buffer, sizeof(buffer), 5000);
        printf("Got %d bytes: %s\n", nBytes, buffer);
          // Аргумент in — отправить строку (следующий аргумент)
          } else if(strcmp(argv[1], "in") == 0 && argc > 2) {
        nBytes = usb_control_msg(handle,
        USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT,
        DATA_IN, 0, 0, argv[2], strlen(argv[2])+1, 5000);
          }
    
          if(nBytes < 0) fprintf(stderr, "%s\n", usb_strerror());
          usb_close(handle); // Закрыть дескриптор
          return 0;
        }
        

    Здесь правит бал функция usb_control_msg, которая объявлена во включаемом файле usb.h. Она имеет кучу параметров и собственно создает те управляющие сообщения, обработка которых реализована в прошивке микроконтроллера.

    Proteus отдыхает

    Всенародно любимый симулятор электрических схем Proteus ISIS бесполезен при разработке устройств с программной реализацией USB. Его эмулятор USB поддерживает только чипы с аппаратной поддержкой универсальной последовательной шины (например, AT90USB646 или AT90USB1286).

    Собираем, прошиваем, тестируем

    Ниже приведен небольшой, но очень полезный Makefile, c помощью которого командой make из main.c и usbtest.c легко получить прошивку для чипа — main.hex и бинарник утилиты usbtest:

        CC = avr-gcc
        OBJCOPY = avr-objcopy
        CFLAGS = -Wall -Os -Iusbdrv -mmcu=attiny2313
        OBJFLAGS = -j .text -j .data -O ihex
        OBJECTS = usbdrv/usbdrv.o usbdrv/oddebug.o usbdrv/usbdrvasm.o main.o
        CMDLINE = usbtest
    
        # Цель: собрать все
        all: main.hex $(CMDLINE)
    
        # Сборка утилиты для компьютера
        $(CMDLINE): usbtest.c
          gcc -I ./libusb/include -L ./libusb/lib/gcc -O -Wall usbtest.c -o usbtest -lusb
    
        # Очистить проект от бинарного кода
        clean:
          $(RM) *.o *.hex *.elf usbdrv/*.o
    
        # Получение файла прошивки из elf-файла
        %.hex: %.elf
          $(OBJCOPY) $(OBJFLAGS) $< $@
    
        # Сборка elf-файла
        main.elf: $(OBJECTS)
          $(CC) $(CFLAGS) $(OBJECTS) -o $@
    
        # Сборка файлов библиотеки V-USB
        $(OBJECTS): usbdrv/usbconfig.h
    
        # C в объектный код
        %.o: %.c
          $(CC) $(CFLAGS) -c $< -o $@
    
        # asm в объектный код
        %.o: %.S
          $(CC) $(CFLAGS) -x assembler-with-cpp -c $< -o $@
        

    Чтобы залить прошивку в микроконтроллер с помощью программатора usbtiny, набираем команду:

        $ sudo avrdude -p t2313 -c usbtiny -e -U flash:w:main.hex:i -U lfuse:w:0xef:m
        

    В avrdude фьюзы задаются не слишком наглядно, но их можно легко рассчитать в одном из online-калькуляторов.


    Рисунок 4 – Online-калькулятор фьюзов

    Подключаем устройство к компьютеру и проверяем, как оно работает (usbtest c параметром out считывает строку, in — записывает указанную строку в буфер чипа):

        $ sudo ./usbtest in all_ok
        $ sudo ./usbtest out
        

    Рисунок 5 – Тестирование взаимодействия с ATtiny2313 по USB (заливаем в чип строку, а затем cчитываем ее)

    Ложка дегтя

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

    Подглядываем

    На уровне логики протокол USB — это, по сути, многоуровневая пакетная передача данных. В этом нетрудно убедиться (а заодно узнать много интересного про USB), воспользовавшись анализатором сетевых протоколов Wireshark. Предварительно необходимо загрузить драйвер USB-монитора:

        $ sudo modprobe usbmon
        

    Теперь в списке интерфейсов Wireshark можно выбирать шины USB. Посмотреть номер шины устройства можно, например, в логах.


    Рисунок 6 – Перехват USB-пакетов в Wireshark

    Заключение

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