Доопрацювання USB-стека в мікроконтролерах STM32 і TivaC

Наявність USB-порту в сучасних мікроконтролерах відкриває широкі можливості для самостійного виготовлення різноманітних керованих з комп'ютера пристроїв. На практиці, однак, з'ясовується, що поставляються виробником бібліотеки для роботи з USB потребують доопрацювання. Якщо вам цікавий досвід подібної доопрацювання для двох популярних колекцій МК — ласкаво просимо під кат.


Постановка завдання
Отже, ми хочемо зробити пристрій, яке обмінюється з комп'ютером повідомленнями довільної довжини через USB порт. Найпростіший спосіб зробити це — скористатися USB класом символьних пристроїв (CDC), відомим також під назвою 'віртуальний послідовний порт'. Тоді на хост-системі, до якої ви підключіть ваш пристрій, автоматично буде створений послідовний порт, через який ви зможете обмінюватися даними з пристроєм, працюючи з ним, як із звичайним файлом. На практиці, однак, з'ясовується, що деякі необхідні для цього функції USB-стеку виробника або не реалізовані, яких реалізовано з помилками. Ми почнемо з розгляду мікроконтролерів STM32 (перший випадок) і закінчимо іншим популярним сімейством — Texas Instruments Tiva C (другий випадок). Обидва сімейства мають архітектуру ARM Cortex-M4.

STM32 — просто додай коду
Мікроконтролери STM зазвичай мають багатий функціонал за досить демократичною ціною. Виробник поставляє широкий спектр бібліотек на всі випадки життя. Серед них є і бібліотеки для підтримки USB, та бібліотека для роботи з іншою периферією, наявної на кристалі. Останнім часом всі ці бібліотеки були об'єднані в один мега-пакет під назвою STM32Cube. При цьому, однак, про сумісність особливо не піклувалися і поміняли все, що тільки змогли поміняти, включаючи назви полів у структурах, що описують конфігурацію портів вводу-виводу, при тому, що сама назва структури залишилося колишнім. Интресно, що є ще й третій варіант прикладів і бібліотек, який можна знайти на сайті stm32f4-discovery.com/. Однак, автор цього варіанта дуже любить перейменовувати файли, запозичені у STM, щоб увічнити свої ініціали, що теж не додає сумісність з усім іншим кодом. Враховуючи все вищевикладене, я вирішив взяти за основу останній до-кубічний варіант бібліотек, що поставляються STM. Зараз їх можна знайти в комплекті поставки компіляторів (я використовую IAR). Щоб потім довго не шукати, бібліотеки включені до складу проекту, який ви можете взяти з гіта за посиланням внизу. Для експериментів я використовував плату STM32F4DISCOVERY www.st.com/web/catalog/tools/FM116/SC959/SS1532/PF252419. Якщо у вас інша плата та код відразу не заробив, справа швидше за все в частоті зовнішнього кварцового генератора. Хоча бібліотеки рясніють всілякими макроопределениями, і в останній версії бібліотек серед них з'явився і макрос для зовнішньої тактової частоти, в коді цей параметр, як і раніше, прописаний у вигляді числа без всяких коментарів, мабуть, щоб розробники не втрачали форму і не забували читати мануал. Ви можете знайти це число — тактову частоту в мегагерцах — у файлі system_stm32f4xx.c визначення макросу PLL_M.

Отже, беремо за основу готовий приклад, який перекладає дані з USB в послідовний порт мікроконтролера і назад. Послідовний порт нам не знадобиться, а дані ми будемо просто перекладати з вхідного потоку у вихідний, тобто реалізуємо ехо. З допомогою PuTTY переконуємося, що воно працює. Але цього недостатньо. Для обміну даними з пристроєм нам знадобиться слати багато більше одного символу за раз. Пишемо тестову програму на пітоні, яка шле посилки випадкової довжини і вичитує відповідь. І тут нас чекає сюрприз. Тест працює, але недовго, після чого чергова спроба читання або зависає назавжди, або завершується по таймауту, якщо він виставлений. Дослідження проблеми за допомогою відладчика показує, що МК таки відіслав всі отримані дані, причому остання посилка мала довжину 64 байта. Що ж сталося?

USB-стек на хост-системі має багатошарову структуру. На рівні драйвера дані отримані, але залишилися у нього в кеші. Драйвер передає закэшированные дані додатком тоді, коли приходять нові дані і витісняють старі, або коли драйвер дізнається, що нових даних поки очікувати не слід. Звідки ж він може отримати це знання? USB шина передає дані пакетами. Максимальний розмір пакета в нашому випадку як раз 64 байта. Якщо в черговому пакеті даних прийшло менше, значить нових даних поки можна не чекати, і це є сигналом для того, щоб передати додатком всі отримані дані. А якщо даних настав рівно 64 байта? На цей випадок у протоколі передбачено здійснення пакету нульової довжини (ZLP), який і є сигналом переривання потоку. Отримавши його, драйвер розуміє, що нових даних поки очікувати не слід. В нашому випадку він його не отримав тому, що розробники USB стека про ZLP просто нічого не знали.

Друга проблема, яку розробники USB-стека незаслужено обійшли увагою — що робити з даними, які були отримані за USB, якщо їх нікуди дівати, т. к. вхідний буфер переповнений. За великим рахунком, їх взагалі не хвилювала проблема вхідного буфера — вони припускали, що всі отримані дані негайно обробляються, що, звичайно ж, не завжди може бути виконано. В USB протоколі на випадок, якщо дані не можуть бути отримані, передбачений відповідь NAK — негативне підтвердження. Після такої відповіді хост просто посилає дані ще раз. Якщо ми хочемо уникнути переповнення вхідного буфера, нам потрібно у випадку, якщо в ньому немає місця для повної посилки (64 байта), переводити канал в стан NAK, що забезпечує автоматичний відповідь NAK на всі вхідні пакети.

Tiva C — листковий пиріг з багами
Для експериментів була взята плата EK-TM4C123GXL www.ti.com/tool/ek-tm4c123gxl. Для компіляції необхідний пакет бібліотек TivaWare www.ti.com/tool/sw-ek-tm4c123gxl. Вивчення бібліотек показує, що розробники не обійшли увагою ні ZLP ні проблему буферизації — у вхідному і вихідному каналі є готові до використання кільцеві буфера. Однак автоматичний тест дає той же результат — обмін даними раптово припиняється. З допомогою відладчика з'ясовується, що на цей раз дані застрягли в кільцевому буфері передачі, причому з розміром останнього пакету, а значить і з ZLP, проблема ніяк не пов'язана.

Виявити проблему вдається тільки шляхом ретельного вивчення джерел бібліотек. Виявляється, що для посилки ZLP необхідно виставити спеціальний прапорець, який за замовчуванням не виставлений. Можливо, ця обставина і підштовхнуло інших розробників до того, щоб додати код, що посилає ZLP ще в одному місці — на більш низькому рівні USB-стека, і вже без прапорця. Ця зміна і внесло баг, що приводить до зупинки передачі. Проблема виникає наступним чином. Передавач отримує наступний пакет, коли закінчується передача попереднього, або якщо попереднього не було, а додаток додало дані в буфер передачі. Код, який ініціює передачу, отримує нотифікацію про завершення передачі попереднього пакету від нижнього рівня USB-стека. Проблема в тому, що якщо нижній рівень стека ініціював передачу ZLP, то нотифікацію про завершення він не надсилає, т. к. ініціював передачу він сам. Верхній рівень не починає передачу даних, поки передавач зайнятий передачею ZLP пакету, і не починає передачу після її завершення, оскільки не отримує нотифікації — процес передачі зупиняється. Виправити проблему дуже просто — потрібно прибрати код нижнього рівня, що посилає ZLP, і надати це верхнього рівня стека. Друга проблема, яка вимагає рішення, пов'язана з тим, що процедура, яка починає передачу, може бути викликана як з контексту обробника переривання (по завершенні передачі), так і з контексту програми за додавання даних в буфер передачі. Щоб сериализации виклики цієї процедури з різних контекстів, потрібно забороняти переривання на час її виконання.

Вихідний код
Лежить тут github.com/olegv142/stm32tivc_usb_cdc.
В папках stm і ti лежать по 2 тестових проекту — usb_cdc_echo і usb_cdc_api. Перший просто надсилає всі отримані дані назад, другий реалізує пакетний протокол, який ви можете легко адаптувати під свої потреби. В папці tools — тестові скрипти на пітоні.

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

0 коментарів

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