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

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


Итак, в нашем замечательном приборе Беркут-ММТ (на базе 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.
НТЦ Метротек
77,00
Разработка и производство Ethernet устройств etc.
Поделиться публикацией

Комментарии 21

    0
    Интересная у Вас работа :) А когда рефлектограмма приходила не полностью, как поняли? Файл не открывался, артефакты?
      0
      Поняли мы это по странному поведению цепочки утилит, которая обрабатывала данные на выходе с программы usbgather. Они-то рассчитывали на железобетонно целую рефлектограмму. Эхх, знали бы мы тогда как мы ошибались :)
      +4
      да, USB не гарантирует доставку
      Это, вообще говоря, неверно. Так же как и в IP (работающем поверх какого-то физического протокола, например Ethernet) тут есть варианты, например: TCP и UDP. Так вот, в USB и физический уровень и уровень протокола стандартизирован и есть их там (по большому счёту) два:
      а) bulk (почти оно же control и interrupt) — с гарантированной доставкой, но негарантированной задержкой и полосой
      б) isochronous — с гарантированной полосой и задержкой, но негарантированной доставкой

      По тексту у Вас вроде USB bulk. Поэтому ошибка где угодно, но только не в USB. Может быть в передатчике, может быть в приёмнике. Я имею ввиду программно-аппаратную часть которая буферирует/отправляет/принимает данные. Но вероятность, что данные теряются именно при передаче по USB исчезающе мала (там есть и блочный CRC и контроль чётности слов). Иначе флешками было бы невозможно пользоваться (а там протокол — навес над USB bulk).
        0
        Так вроде бы автор написал, что причина не в протоколе, а в кривой реализации CDC-EEM в NutOS.
          +1
          Так я вроде и не против. Я ж написал, что при поиске ошибки полагать, что usb bulk теряет пакеты методолгически неверно. Собственно только этот пассаж я прокомментировал и уточнил.
            0
            Ну, в реализации CDC-ACM, видимо, тоже не боги пакеты по горшкам раскладывают, если накосячили в EEM, кто мешает сделать аналогичный баг в ACM. (И вообще мне кажется это один и тот-же баг...)
            Т.е. если CRC не совпала, пакет надо-бы перепослать, но кто такой стандарт чтобы указывать что нам делать?

            urx, похоже что ваша STM не проверяет пришло ли ACK для текущего пакета, и тупо шлет следующий, в то время как хост думает, что она честно перепосылает предидущий.

            А фразы типа «Соответственно мы из следующего пакета вычитываем N потерянных байт, а следующие данные воспринимаем как начало нового EEM-пакета и интерпретируем первые 2 байта как заголовок.» заставляют шевелиться волосы на голове.
            Ладно потеряли пакет из середины, а что контрольную сумму итогового TCP пакета никто не проверял??

            И кто-бы вы думали виноват???
            и USB я стал любить чуть меньше, чем раньше.
              0
              по пунктам:
              ACM работало как надо. Просто при приеме кривого пакета мы в силу кривизны алгоритма убегали за пределы памяти.

              Насколько я помню — OTG корка генерит нам прерывание о возможности выгребать пакет из FIFO когда уже все ACK пришли.

              работа с ЕЕМ заголовком происходит на том уровне, когда никакого ТСР еще нет.

              И кто бы вы думали искал тут виноватых? Пост вообще-то не о том как какие-то нехорошие люди сделали плохой алгоритм, а мы его героически исправили. Даже наоборот — в силу сложившихся обстоятельств мы наступили на самолично написанные ранее грабли и узнали немного нового.
                +1
                Не пойму:
                ACM работало как надо. Просто при приеме кривого пакета мы в силу кривизны алгоритма убегали за пределы памяти.

                дык не должно быть никаких кривых пакетов, если алгоритм реализован правильно.

                Насколько я помню — OTG корка генерит нам прерывание о возможности выгребать пакет из FIFO когда уже все ACK пришли.

                Это на стороне прибора насколько я понимаю, я-же говорю что похоже что модуль, не проверяет ушел ли пакет правильно.

                мы наступили на самолично написанные ранее грабли и узнали немного нового

                А… Так EEM-код в модуле тоже ваш… Недопонял :)

                Так чем-же тогда USB провинился? :)
                  0
                  АСМ и ЕЕМ работают параллельно. И все жило до тех пор, пока в ЕЕМ не терялась часть данных и не происходило кривой интерпретации заголовка.

                  ОТГ — в модуле. А со стороны прибора там ОТГ, потом хаб, потом уже ОТГ модуля.

                  USB — лично для меня — сложность отладки.
          +2
          Наверное я выложил недостаточно предистории. Сейчас попробую исправить это тут.

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



          И все работало нормально. Но потом купить такой USB хаб стало невозможно, и мы заменили его на CY7C65642. Тут и начались проблемы. При этом если карта работала напрямую с USB хостом — данные не терялись. Мы начали дебажить и заметили что в какой-то момент урбы от хоста на модуль не уходят. Читали разные доки по USB и в одной наткнулись на такую фразу:

          4.3.1.3.6.1 Transmission Errors
          For errors in this category, USB defines a policy that allows the transaction to be retried for up to three times before the transfer is failed and returned to the client.

          Отсюда и возникло предположение что данные могут не дойти в USB bulk.

          Ситуация усугублялась тем, что много приборов уже было поставлено и заменой хаба проблему было не решить. После долгих копаний и отладки был выбран EEM + TCP/IP.

              0
              А не рассматривали идею подключения STM32 через модули UART <=> WiFi?
              Я пользуюсь — не нарадуюсь. Нужен роутер WiFi, если его нет в учреждении, и по одному модулю на прибор.
              Конечно модули денег стоят (от 400 р./шт. на aliexpress), но зато нету возни с USB стэком, максимальная скорость до 3 Мбит с модулями за 1000р. или до 460 кбит с модулями за 400-500р… Также нету возни с проводами (мобильность), ну и работаешь с хостом со стороны МК как с обычным последовательным портом.
                0
                Одно врем была идея делать что-то с вафлей, но дальше нее дело не пошло.
                  0
                  Почему, если не секрет? Может и я зря связался с ним?
                    0
                    А я не знаю почему. К сожалению.
                      0
                      потому что в пределах одного дивайса wifi не нужен.
                      а автономные модули (то есть, без прибора, с интерфейсом на планшете, телефоне, ноутбуке, ...) не хотят покупать. поэтому не делаем.
                        0
                        Хммм… Я смотрю на схему в этом комментарии.

                        USB Host мне видится как головная ЭВМ. На ней интерфейс? Между USB Host и несколькими узлами STM32 я и подразумевал расшаренную WiFi.
                          0
                          :)
                          это всё в пределах одного прибора. в нём два модуля, подключаемых по usb к управляющей плате. wifi здесь не совсем к месту.
                            +1
                            хехе… я себе представил больничный кабинет несколькими модулями (блоками), от которых тянутся USB кабели к головной машине. Некий такой стимпанк в пластиковом исполнении.
                            А тут оказывается комплекс внутри единого корпуса.
                0
                Не дойти-то они могут. Но клиенту (STM) об этом будет сообщено. И он может продолжать перепосылать недоставленный пакет столько, сколько захочет.
                В этом случае, даже если вы выдернете провод (usb), и вставите обратно, ни одного пакета потеряно не будет.
              +2
              Читаю и волосы дыбом встают…
              Я конечно не знаю, что за проблема с USB bulk оказалась у автора и почему теряются данные (как так?)… ведь bulk вообще-то гарантирует целостность данных…
              Меня настораживает тенденция крупноблочного мышления разработчиков.
              На первом месте у нас, конечно, time-to-market — то есть выдать продукт как можно быстрее.
              Есть проблема? Не будем сильно разбираться с корнем проблемы, а попробуем решить в лоб, добавим сверху еще уровней, слоев, протоколов, возьмем какой-нибудь готовый lib, обойдем проблему, авось та первая проблема замаскируется, не будет вылазить.
              Технологическая сингулярность — человечество изобретает технологии быстрее, чем осваивает старые. Как работают новые технологии? А никто не знает… Взяли за основу пример «один», добавили библиотек из источника «два» и «три» — вроде бы все работает.
              Покрыть тестами? Наверное процента на 2-3 сможем, а дальше? Не известно, мы же точно не знаем как тот стек протоколов внутри устроен… Потом удивляемся, что хакеры дистанционно автомобили останавливают…

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое