Доопрацювання 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 стека для STM32 про 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 - тестові скрипти на пітоні.

logo