Мне для проекта USBTerm нужен USB11 хост, так как к моему тонкому клиенту я собираюсь подключить USB мышь и USB клавиатуру. В принципе, я думал, ничего особо сложного там быть не должно. Я ведь уже когда-то занимался USB проектами: когда-то делал и простое USB устройство в плате Марсоход, и USB трекер из платы Марсоход2. В общем, тема мне довольно знакомая, хоть и не любимая.
Почему не любимая? Слишком уж сложен USB протокол. Даже для USB11 низкоскоростных устройств слишком все сложно и запутанно. Если пытаться сделать машину состояний по обслуживанию этого протокола, то получается очень много состояний, много условий, вариантов движения данных, правила очередности пакетов. В общем не просто это. Вот зарылся я с головой в реализацию хоста и потратил на это очень много времени. Прямо неприлично много.. Жаль.
Ну вот попытаюсь рассказать, что и как я делаю.
Естественно, я не собираюсь выполнять все требования спецификации USB11. Мой тонкий клиент не летит в космос, нет никаких чрезвычайных требования о надежности. Поэтому я поставил перед собой задачу сделать все по минимуму: должно работать и все. Это позволит мне хотя бы частично снизить сложность реализации. Я отказался от проверки четности принимаемых пакетов данных и я отказался от проверки контрольных сумм принимаемых пакетов. Уже чуть легче.
Традиционные USB хост контроллеры типа UHCI или EHCI, которые реализованы в чипсетах компьютеров и различных SoC, работают в режиме DMA. Почему-то считается, что аппаратная реализация протокола USB очень проста (мне так не кажется), но вместо этого, программная часть, драйвера USB стека очень сложны (вот с этим соглашусь).
Обычные драйвера USB хост контроллеров создают в памяти SoC списки транзакций, которые необходимо выполнить. Транзакции выполняются по расписанию, это у них называется schedule. Вот, например, картинка из документации по контроллеру UHCI (Uneversal Host Controller Interface):
Аппаратный хост контроллер периодически читает из памяти системы на кристалле эти списки транзакций и их описатели, и выполняет обмен данных с подключенным USB устройством. При этом, хост контроллер изменяет в памяти описатели транзакций (TD - transfer descriptor) разные флаги по результату их исполнения.
Вот мне такой традиционный, сложный хост контроллер не нужен. Мне нужно что-то совсем простое. В протоколе USB инициатор передачи данных - это хост. У меня будет на сервере программа, которая отрисовывает изображение десктопа на тонком клиенте и пусть эта же программа периодически опрашивает подключенные к терминалу USB мышь и клавиатуру.
Я думаю так: послал команду терминалу "исполнить USB транзакцию" - получил ответ. Буду посылать такие команды с периодичностью 10-20 миллисекунд и будет хорошо.
Теперь я задумался, какой мне нужен протокол из ПК в плату Марсоход3бис. Нужно учесть, что протокол для отрисовки картинок на дисплее платы Марсоход3bis у меня уже есть и он простой. Сейчас для передачи пикселов в плату используется вот такой пакет:
- 2 байта - количество пикселов в пакете на запись во фрейм буффер
- 2 байта - простая сигнатура 0x55, 0xAA
- 4 байта - адрес начала записи пикселов во фрейм буффер
- N*2 байт - последовательность из N 16-ти битных (high-color) пикселов.
Вот такие блоки данных я уже могу передавать в плату через FTDI в режиме синхроного фифо.
Мне нужно придумать расширение для этого протокола с целью передавать команды для USB контроллера.
После некоторых раздумий и нескольких попыток реализации я решил, что проще всего мне будет посылать данные точно в таком же формате, но например, считать, что запись пакета по адресам 0x800000xx - это запись данных в мой USB хост контроллер, а не в память видеоадаптера. Данные, передаваемые в шину USB - это младший байт "пиксела", а вот старший байт "пиксела" будет обозначать разные полезные флаги:
- биты 0..7 - это байт передаваемый, если необходимо, в шину USB. Дополнительно бит0 и бит1 могут определять состояние сброса и разрешения шины, если установлен бит10.
- бит8 - номер USB канала, участвующий в USB операции.
- бит9 - опрос состояния линий USB DP/DM. В исходном состоянии USB шины оба сигнала DP/DM находятся в логическом нуле. Линии просто притянуты к земле резисторами. Когда подключается USB устройство, то в самом устройстве одна из линий уходит в единицу - в устройстве она подтягивается резистором к питанию +5В. Так можно определить, какое устройство подключено. Если в единицу уходит DM, то подключено низкоскоростное устройство, Low-speed. Когда контроллеру придет команда с этим битом установленным, то он отправит назад в ПК один единственный байт с битами состояний линий.
- бит10 - установка или сброс состояния USB RESET и/или ENABLE. Состояние шины RESET - это когда контроллер принудительно опускает обе линии USB (их там всего две) DP и DM в ноль. Каждый раз после подключения нового устройства ПО USB стека дает команду сброса шины как минимум на 100 миллисекунды. После сброса шина поддерживается в активном состоянии либо специальными пакетами SOF (Start-Of-Frame) для высокоскоростных устройств либо состоянием Keep-alive для низкоскоростных устройств. Пакет Keep-alive - это каждый USB фрейм, то есть каждую 1 мс, обе линии DP/DM опускаются в ноль на время 2х битов низкоскоростного устройства. А низкоскоростные устройства передают биты на частоте 1,5МГц (высокоскоростные на частоте 12МГц).
- бит11 - если установлен, то после передачи этого (последнего байта в пакете USB данных) хост контроллер переходит в состояние ожидания данных от подключенного устройства.
- бит12 - после отправки текущего байта нужно подождать ответ от устройства, дальше, после приема данных от устройства хост еще должен подтвердить прием ответным пакетом ACK (0x80, 0xD2)
- бит13 - этот байт последний в пакете данных отправляемых устройству. Хост контроллер должен после отправки последнего байта выдать на шину два бита SE0: опустить обе линии DP/DM в ноль.
- бит14 - установлен обозначает, что байт данных будет отправлен в шину USB
- бит15 - установлен обозначает, что это новый пакет данных, который нужно начинать именно в начале фрейма после Keep-alive состояния. Тут вот какая опасность. Время на шине USB течет само собой. Начало фрейма отмечается состоянием Keep-alive. Если я с компьютера буду присылать асинхронно пакеты для шины USB я не могу начинать их передачу в произвольный момент времени, так как может не хватить времени для всей транзакции до конца фрейма. Проще всего новые транзакции начинать прямо после начала фрейма - вот для этого такой бит в команде я и придумал.
Я конечно не уверен, что я тут понятно все написал, но что поделать.. вот такой он протокол USB. Тут все не просто и сделать проще мне кажется уже не куда.. Возможно здесь есть какая-то избыточность или наоборот, может статься я чего-то пока не учел. Дальше будет видно, может что-то придется менять.
Теперь еще какие проблемы есть. Протокол терминала USBTerm разбирается в моем проекте в verilog модуле ftdi.v. Там данные приходящие от FTDI чипа на тактовой частоте 60МГц кладутся в приемное FIFO, а вычитываются уже на частоте 148MHz - это частота работы памяти видеоадаптера. Вот протокол разбирается на этой частоте. Теперь получается, что не все приходящие команды - это команды записи в память фреймбуффера. Некоторые команды - это данные для USB контроллера. Я должен их передать туда, но контроллер работает совсем в другом темпе. Рабочая частота USB11 контроллера - это 12МГц. Данные для низкоскоростных устройств отправляются вообще на частоте 1,5МГц. Мне придется установить еще дополнительные модули ФИФО для синхронизации потоков данных из протокола в контроллер USB и назад, для отправке принятых пакетов от USB устройств назад к чипу FTDI и далее в ПК.
В результате я написал свой verilog модуль контроллера USB11 usb11ctrl.v, который содержит в себе согласующие ФИФО и два дополнительный модуля: модуль передатчика usb11send.v и модуль приемника usb11recv.v. Так же, был написан тестбенч, который помогает посмотреть как оно должно работать. Я использую icarus verilog для функциональной симуляции проекта.
Исходники можно скачать на github проекта: https://github.com/marsohod4you/UsbHwThinClient4Vm
Компилировать проект икарусом: запустите test_usb.bat. Симуляция - запустите из консоли "vvp qqq". Просмотр получившихся временных диаграмм "gtkwave out.vcd".
Покажу некоторые временные диаграммы, которые видно в программе GtkWave. Сразу скажу еще одну особенность и трудность работы с USB. Модули работают на частоте скажем 12МГц или выше, но протекающие процессы в шине USB иногда носят длительный характер. Например, состояние RESET должно длиться 100 миллисекунд. Второй пример, длительность одного фрейма 1 миллисекунда. Рассматривать такие временные диаграммы очень не удобно, так как все время приходится менять масштаб рассматриваемых сигналов.
Посмотрим, как verilog тестбенч tb_usb.v передает в шину USB два подряд пакета:
...
write_cmd(16'hc080);
write_cmd(16'h402D);
write_cmd(16'h4000);
write_cmd(16'h6010);
write_cmd(16'h4080);
write_cmd(16'h40E1);
write_cmd(16'h403F);
write_cmd(16'h603F);
...
Согласно "моему" протоколу, младший байт - это собственно байт для передачи в шину, а старший байт - это всякие флаги. Первый байт 0x80 передается с флагами 0xC0, то есть - этот байт будет началом USB пакета и отправлять его нужно в начале ближайшего фрейма. Потом отправляются просто два байта 0x2D (USB PID SETUP) и 0x00. Четвертый байт 0x10 отправляется с флагами 0x60 - то есть это последний байт в пакете, который будет завершен состоянием SE0 на шине USB. Далее идет новый пакет данных 0x80 0xE1 (USB PID OUT) и данные 0x3F 0x3F. Этот пакет будет следовать немедленно после первого пакета. Смотрим:
Вот на временных диаграммах видны сигналы шины USB, это DP и DM. Сигналы эти работают в противофазе, дифференциальный сигнал, https://en.wikipedia.org/wiki/Differential_signaling
Кроме того, DP и DM - это двунаправленные сигналы, они работают на выход только во время передачи, когда сигнал usb_out=1; В остальное время эти выводы работают как входы.
Видно начало фрейма, состояние Keep-alive, видно два пакета на вывод. Пакеты начинаются с байта синхронизации 0x80 (SYN) и за ними следует идентификатор типа пакета 0x2D (SETUP) или 0xE1 (OUT). Ну и далее прочие байты. Пакеты заканчиваются, как и положено, состоянием SE0 (обе линии в нуле на время 2 бита).
Вот более сложный случай. В тестбенче tb_usb.v он у меня описан вот так:
write_cmd(16'hc080);
write_cmd(16'h4069);
write_cmd(16'h4000);
write_cmd(16'h7010);
//wait data will be sent
@(negedge w_usb0_out);
#1;
@(negedge w_usb0_out);
#1;
//simulate attached device should respond..
write_dev_byte(8'h80,1'b0);
@(posedge w_show_next_dev);
#1;
write_dev_byte(8'h4B,1'b0);
@(posedge w_show_next_dev);
#1;
write_dev_byte(8'h12,1'b0);
@(posedge w_show_next_dev);
#1;
write_dev_byte(8'h44,1'b1);
Это тот случай, когда нужно прочитать что-то из подключенного USB устройства. Тогда на шине посылается пакет c идентификатором 0x69 (IN), дальше testbench симулирует ответ на запрос и посылает данные устройства, а хост должен получить этот пакет и автоматически ответить подтверждением 0xD2 (ACK).
ВОт смотрю временные диаграммы, что мы тут насимулировали:
Вроде бы в симуляторе все работает.
Теперь эти модули нужно интегрировать в реальный проект Altera Quartus для платы Марсоход3bis и посмотреть все ли там хорошо, все ли работает. Симуляция симуляцией, но в реальном железе может вылезти что-то, что не учел.
Кроме того, нужно еще написать программу на c++ для опроса USB хоста терминала USBTerm. Вот этим и займусь.
Подробнее...