Pull to refresh
0
НТЦ Метротек
Разработка и производство Ethernet устройств etc.

Как мы преодолевали передачу данных по USB

Reading time 6 min
Views 20K

С чего все началось


Итак, в нашем замечательном приборе Беркут-ММТ (на базе PXA320 и GNU/Linux) есть не менее замечательный модуль OTDR (на базе STM32 и NutOS), представляющий собой импульсный оптический рефлектометр. Эта связка работает следующим образом: пользователь нажимает на экране на различные элементы UI, в приборе происходит немножечко магии, и желания пользователя трансформируются в команды вида «duration 300», которые уходят в измерительный модуль. Конкретно эта команда выставляет длительность измерений в 300 секунд. Модуль к прибору подключен по USB, для передачи команд поверх USB поднят CDC-ACM.

Кратенько — CDC-ACM позволяет эмулировать последовательный порт через USB. Так что для верхнего уровня наш измерительный модуль в системе доступен как /dev/ttyACM0. CDC-ACM служит для передачи команд в модуль или чтения текущих настроек/состояния модуля. Для передачи самой рефлектограммы служил USB Bulk интерфейс, который все свое время посвящал только одному — передаче данных рефлектограммы из модуля в прибор, как бинарного потока данных. В какой-то момент мы заметили, что рефлектограмма приходит к нам не полностью. Так мы открыли для себя, что USB может терять данные.

Схематично это выглядело так:

image

b5-cardifaced — это демон, который принимает команды по D-Bus и отправляет их в карту через CDC-ACM интерфейс. Результат выполнения посылает обратно по D-BUS.

usbgather — небольшая программка, которая работает на базе libusb и занимается тем, что выгребает из модуля рефлектограмму через USB Bulk и выдает ее на stdout.

Костыли и велосипеды


Сели мы и подумали — нам нужно понимать вся ли рефлектограмма к нам пришла для возможности пропуска неполных рефлектограмм. Стали мы придумывать различные хитрые заголовки, контрольные суммы и тд. Потом поняли что изобретаем ТСР. И тогда было принято волевое решение вместо USB Bulk завести TCP/IP поверх CDC-EEM. Почему CDC-EEM? Потому что CDC-EEM позволяет наиболее просто использовать USB как транспорт для передачи сетевого трафика. На самом приборе поддержка CDC-ECM в ядре есть, а модулях мы используем NutOS в качестве операционной системы и поддержка CDC-EEM и TCP/IP стек в NutOS был.

Фикс длинною в жизнь 3 месяца


Казалось бы — ни что не предвещало беды. Подняли CDC-EEM, настроили IP адреса. Ping? Есть ping! Ура. Изменили механизм передачи данных с USB Bulk на передачу данных через TCP-сокет. Вот-вот должно было наступить счастье, но тут внезапно при тестировании сеть упала с криками в dmesg о своей непростой жизни, наших кривых руках и вставшей колом очереди на отправку для нашего сетевого интерфейса. Примерно так:

[  118.289339] ------------[ cut here ]------------
[  118.293978] WARNING: at net/sched/sch_generic.c:258 dev_watchdog+0x184/0x298()
[  118.301163] NETDEV WATCHDOG: usb2 (cdc_eem): transmit queue 0 timed out
[  118.307726] Modules linked in: cdc_eem usbnet cdc_acm wm97xx_ts ucb1400_ts ipv6 cards button pmmct
[  118.318671] [<c002f750>] (unwind_backtrace+0x0/0xec) from [<c003e5a4>] (warn_slowpath_common+0x4c)
[  118.328017] [<c003e5a4>] (warn_slowpath_common+0x4c/0x7c) from [<c003e668>] (warn_slowpath_fmt+0x)
[  118.337536] [<c003e668>] (warn_slowpath_fmt+0x30/0x40) from [<c02ab738>] (dev_watchdog+0x184/0x29)
[  118.346552] [<c02ab738>] (dev_watchdog+0x184/0x298) from [<c0049938>] (run_timer_softirq+0x18c/0x)
[  118.355731] [<c0049938>] (run_timer_softirq+0x18c/0x26c) from [<c0043f78>] (__do_softirq+0x84/0x1)
[  118.364819] [<c0043f78>] (__do_softirq+0x84/0x114) from [<c004404c>] (irq_exit+0x44/0x64)
[  118.372959] [<c004404c>] (irq_exit+0x44/0x64) from [<c0029074>] (asm_do_IRQ+0x74/0x94)
[  118.380843] [<c0029074>] (asm_do_IRQ+0x74/0x94) from [<c0029b04>] (__irq_svc+0x44/0xcc)
[  118.388792] Exception stack(0xc0425f78 to 0xc0425fc0)
[  118.393819] 5f60:                                                       00000001 c606b300
[  118.401958] 5f80: 00000000 60000013 c0424000 c0428334 c0454bac c0428328 a0022438 69056827
[  118.410100] 5fa0: a0022368 00000000 c0425f98 c0425fc0 c002b04c c002b058 60000013 ffffffff
[  118.418232] [<c0029b04>] (__irq_svc+0x44/0xcc) from [<c002b058>] (default_idle+0x34/0x40)
[  118.426367] [<c002b058>] (default_idle+0x34/0x40) from [<c002b5cc>] (cpu_idle+0x54/0xb0)
[  118.434425] [<c002b5cc>] (cpu_idle+0x54/0xb0) from [<c00089f0>] (start_kernel+0x28c/0x2f8)
[  118.442653] [<c00089f0>] (start_kernel+0x28c/0x2f8) from [<a0008034>] (0xa0008034)
[  118.450180] ---[ end trace d7e298087ff4c373 ]---

Если кратенько, буквально в двух словах — каждый сетевой интерфейс имеет таймер, который засекает время с каждой последней отправки данных и если оно превысило некий интервал — мы видим это сообщение. Дальше все зависит от драйвера — некоторые после этого нормально работают, некоторые — нет.

Корень зла


Все вышеперечисленное усугублялось сообщениями в dmesg о неизвестных link cmd. Добавили побольше дебага и узнали, что нам на USB host приходит ответ на echo request, который мы не посылали.

Когда ничего не работает — настает время читать документацию. Вот и мы раздобыли доку по CDC-EEM, да не откуда-нибудь, а прямо с usb.org. Оказывается первый EEM-пакет это не только кучка данных, но еще и EEM-заголовок, в котором содержится тип пакета (управление или данные) и длина данных. И да, у CDC-EEM есть свой echo request/echo response.

Но наше счастье было бы не полным, если бы не еще одна деталь — при приеме служебных пакетов модуль зависал. Наглухо. Вместе с CDC-ACM.

У нас в модуле USB было настроено так, что передача шла пакетами по 64 байта. Соответственно один Ethernet пакет бился на N пакетов по 64 и передавался через USB. Вот так:



После весьма продолжительного изучения ситуации мы пришли к выводу, что происходит вот что: мы теряем часть EEM-пакета (да, USB не гарантирует доставку). Но мы прочитали из заголовка длину и опираемся на нее. Соответственно мы из следующего пакета вычитываем N потерянных байт, а следующие данные воспринимаем как начало нового EEM-пакета и интерпретируем первые 2 байта как заголовок. А там может оказаться все что угодно. Вплоть до взведенного в 1 бита, который указывает что это служебный пакет. В совсем плохих случаях мы ловим такие данные, которые при интерпретации как EEM-заголовок дают нам Echo Response огромной длины. Гораздо большей, чем наша оперативная память. Так мы поняли что наша реализация usbnet в NutOS требует серьезных доработок.

Больше проверок хороших и разных


В процессе ковыряния usbnet в NutOS было выяснено, что текущий вариант вообще не готов к приему служебных пакетов. От слова совсем. Мы сделали новый вариант, который стал способен корректно обрабатывать служебные пакеты, а именно: мы смотрели тип пакета, ибо на echo по стандарту мы ответить обязаны; проверяли длину — если она больше MTU — то мы явно словили мусор. Еще нашли странность в функции, запускающей передачу данных по endpoint'у: мы проверяли — не занят ли сейчас нулевой endpoint, и если занят — просто выходили и все. Вызывающий эту функцию всегда считал что передача данных запущена, а часто получалось что нет. В итоге мы теряли данные, причем в обе стороны.

Были войны с ТСР-сокетом — иногда данные не передавались и мы не видели почему. Не знаю что руководило разработчиками NutOS, но множество функций, имеющих возвращаемый тип int в любой непонятной ситуации возвращали "-1". Некоторые из них записывали реальный код ошибки в информацию о сокете, некоторые нет. Так что пришлось позаниматься протаскиванием кодов возврата с самых низов, вроде функции отправки данных с сетевухи, до самых верхов — функций типа NutTcpDeviceWrite?(). После этого мы смогли видеть где случился затык.

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

Итоги


В нашей карте теперь есть маленький ТСР сервер, который служит для передачи данных из карты в прибор. Перед началом измерений в карте запускается TCP сервер и карта начинает ждать входящих подключений. После того, как клиент подключится к ТСР серверу, карта начинает измерения и через TCP сервер отправляет результаты в прибор.

Теперь схематично работа прибора с модулем выглядит примерно так:



Действующие лица:
b5-cardifaced — тот же что и раньше — транслирует команды из D-Bus в карту и отсылает результат обратно в D-Bus;
nc — собственно netcat, читает данные рефлектограммы из сокета и отдает их на stdout для дальнейшей обработки.

После всех этих приключений у нас теперь сетевой рефлектометр. Сетевой, правда, не на все 100% — управление происходит через CDC-ACM, а сбор данных из модуля — по TCP/IP через CDC-EEM. У нас все равно есть небольшая потеря данных, но за счет использования TCP/IP на выходе мы всегда получаем полную рефлектограмму. Мы узнали много нового о USB в целом и CDC-EEM в частности и USB я стал любить чуть меньше, чем раньше.

Нагрузочный тест показал, наш модуль на базе STM32F103 может прокачать 220 килобайт данных в секунду по TCP/IP over CDC-EEM, при том что модуль в это время занимается полезной работой и USB у нас работает без использования DMA.
Tags:
Hubs:
+17
Comments 21
Comments Comments 21

Articles

Information

Website
metrotek.spb.ru
Registered
Founded
Employees
31–50 employees
Location
Россия