Совсем недавно мы опубликовали статью о протоколе USB здесь .
И вот, пользуясь нашими знаниями об USB, мы попытались подключить плату Марсоход через интерфес USB к компьютеру. Дело это на самом деле оказалось совсем не простое.
Самых главных проблем три:
- Малое количество элементов в микрохеме CPLD, которая установлена на плате Марсоход. Микросхема EPM240T100C5 имеет всего 240 логических элемента (каждый элемент содержит триггер и некоторую программируемую логику).
- Отсутствие подходящего тактового генератора. В чипе есть генератор, который работает на частоте примерно 5МГц, но это даже для низкоскоростного устройства не очень подходит. У Low Speed устройств частота передачи 1,5МГц, что всего в 3 раза (с хвостиком) ниже, чем имеющиеся у нас 5МГц.
- Необходимо писать драйвера для USB устройства.
Ну что же, мы не привыкли отступать, попробуем нашу «блоху подковать» и сделать невозможное возможным.
Начну описание, пожалуй, с генератора. Как я уже сказал, у нас в наличии нет нужной нам тактовой частоты. Соотношение имеющейся частоты к нужной примерно равно 5МГц / 1,5МГц = 3,33... Это значит, что если пользоваться имеющейся опорной частотой 5МГц, то придется не все биты передаваемых данных делать одинаковой длины – некоторые будут уже (3 такта), а некоторые шире (4 такта). Я думаю, это вполне приемлимое решение, ведь приемник USB всегда синхронизируется по принимаемым данным. Именно изменение состояний линий DP/DM на противоположные обозначает бит «ноль» на приемном конце. По этому же изменению состояний и синхронизируется приемник. А что же «единица»? Если передается бит «единица», то линии DP/DM не меняют своего состояния. Если передается несколько подряд единиц, то генераторы на приемной и передающей сторонах теоретически могут разойтись и это приведет к неправильному приему. К счастью в протоколе USB применяется алгоритм «bit stuffing». По этому алгоритму если в передаваемом битовом потоке встретились шесть «единиц» подряд, то всегда принудительно вставляется один «ноль». На приемной стороне после шести «единиц» подряд следующий «ноль» принудительно удаляется. Значит, если за длительность шести бит генераторы приемника и передатчика расходятся немного, то это не страшно. Следующий «ноль» пересинхронизирует приемник и все будет ОК.
По поводу логики USB. Мы написали на VERILOG модуль передатчика ls_usb_send.v и модуль приемника ls_usb_recv.v. Первый модуль просто передает RAW пакет, а второй просто принимает RAW байты приходящих пакетов. RAW – значит пакет или байты «как есть, без обработки». Эти модули довольно простые. На мой взгляд гораздо труднее было написать третий модуль ls_usb_core.v. Этот модуль собственно реализует нашу USB функцию. Попробую объяснить в чем проблема. Любое настоящее USB устройство при подключении к компьютеру (хосту) должно пройти целую процедуру «опознания и конфигурации». Примерно вот такая последовательность:
1) Сброс шины RESET (обе DP/DM в нуле более 10 миллисекунд).
2) Хост читает описатель устройства (DeviceDescriptor). Эта процедура занимает несколько транзакций на шине USB:
a) транзакция SETUP (состоит из трех пакетов: SETUP, DATA0, ACK);
b) несколько транзакций чтения из устройства IN (каждая состоит из трех пакетов IN, DATAx, ACK);
c) транзакция записи в устройство пустого пакета OUT (состоих из трех пакетов OUT, DATA1, ACK);
Вот как может выглядеть протокол чтения DeviceDscriptor:
От того, что вернуло устройство в этом описателе многое зависит далее в процедуре инициализации. Так, например, в нем написано, есть ли у устройства серийный номер и вообще какие-нибудь строковые описатели (те, что появляются в виндовз в баллоне «найдено новое устройство...»).
3) Хост устанавливает USB адрес для нового подключенного устройства. Это тоже еще две транзакции и каждая по три пакета.
4) Хост читает ConfigurationDescriptor. Еще куча транзакций...
5) Хост читает описатели строк.
6) Хост устанавливает конфигурацию.
И все вышеописанное – это только самый минимум. Задача модуля ls_usb_core – правильно отвечать в нужной последовательности на все запросы хоста анализируя и разбирая запросы от хоста. Нужно зараннее приготовить все описатели, желательно сразу просчитать их контрольные суммы (потому что наш модуль из-за его простоты не считает контрольных сумм). Все эти данные нужно где-то хранить (а памяти как таковой у нас нет, есть только логика). Вся вот эта логика оказывается довольно мудреная.
Итак подведем некоторый итог. Что мы реализовали, а что не реализовали:
- Сделали приемник, но он не запоминает весь принятый пакет, он только передает для core принятый байт и его номер в пакете. Core должна решить, нужен ей этот байт или нет. Так экономим немного места в чипе.
- Приемник не проверяет контрольную сумму принятых пакетов (так же для экономии места в чипе).
- Приемник не следит за правильным чередованием пакетов четный-нечетный (toggle).
- Передатчик не считает контрольные суммы передаваемых пакетов, Core должна передать ему их вместе с остальными байтами пакета.
- Передатчик так же не формирует четный-нечетный пакеты. Он передает байты «как есть», те что приходят от Сore.
- Core не проверяет правильность PID пакетов и следит только за младшей тетрадой PID. Идентификаторы пакетов PID всегда должны быть вида 0x69, 0xe1, 0x2d – тоесть младшая тетрада это инверсия старшей. Мы не анализируем старшую тетраду.
- Core не проверяет USB адрес принятых пакетов! Внимание – это может служить причиной конфликтов устройств на шине USB! Не вставляйте наш девайс совместно с другими если используете USB HUB.
- Core имеет жестко закодированные в мультиплексор таблицы описателей (descriptors) и прочие пакеты, которые нужно посылать.
- Core имеет только один байтовый регистр на запись – мы будем записывать в него данные с помощью программки с компьютера.
- Пока мы не можем читать из нашего устройства произвольные данные (для этого нам понадобилось бы реализовать подсчет контрольной суммы передаваемых пакетов).
- Пока мы можем записывать в наше устройство только один байт с помощью CONTROL transfers, его будет видно на светодиодах платы.
- Описатели устройства мы сделали максимально похожими на существующий device – ключ защиты Rainbow. Почему именно его? Ну не знаю, попался под руку.
Ну и последний немаловажный вопрос – что делать с драйверами устройства? Сразу скажу, мы не будем писать драйвер. Ну может когда нибудь позже... Сейчас мы напишем программу, которая просто просматривает дерево подключенных USB устройств и найдет наше устройсто. Посылая этому нашему устройству специальные пакеты мы сможем зажечь светодиодики на плате Марсоход. Программа, которую мы написали выглядит вот так:
Интерфейс минимальный. Сперва нужно нажать кнопку "Scan USB" и в заголовке диалога должна появиться строка найден наш девайс или нет. Потом устанавливая галочки в CheckBox одновременно зажигаете светодиоды на плате. Программу пробовали и на Windows XP и на Windows 7.
Вы можете взять исходный код этой программы написанной на C++ (Microsoft Visual Studio 6.0) и исходный код проекта для платы Марсоход здесь:
Посмотрите на это демонстрационное видео. Здесь показано, как при подключении платы Марсоход компьютер обнаруживает устройство и в DeviceManager появляется новая строка с желтым восклицательным знаком (нет драйверов). Потом с помощью нашей программы мы зажигаем светодиоды на плате и драйвера нам не нужны.
Подробнее...