Як ми долали передачу даних по 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.