четвер, 7 грудня 2017 р.

Зовсім просто про Virtual COM Port поверх USB плати STM32F3Discovery

Взято тут.
Шина USB, Universal Serial Bus, складна. Звична плата за універсальність і простоту використання. На жаль, я поки із нею знайомий лише побіжно. Однак, не тільки в мене такі проблеми :-). Тому багато популярних мікроконтролерів містять периферію, котра бере турботу про низькорівневі подробиці функціювання USB на себе. Зокрема, є підтримка і у багатьох MCU STM32. На платах STM32F3DiscoverySTM32F4Discovery та інших навіть виведено окремий роз'єм (Mini-B та Micro-B, відповідно).
Є він і у ряду плат серії STM32F0, на клонах Maple Mini із STM32F103, тощо. Про них напишу трішки пізніше. Але не ARM-ами єдиними. Поміж плат на AVR теж такі трапляються, зокрема, в сімействі плат Arduino. Я б виділив Digispark -- найменший із Arduino. Детальніше про AVR + USB див. серію статей "AVR ATtiny USB Tutorial Part", але, в принципі, такі фокуси можливі і з іншими Arduino.
Крім того, STMicroelectronics надає Middleware, програмні бібліотеки, які спрощують роботу із USB до майже тривіальної -- якщо не хотіти чогось хитрого.

І ось поверх цього USB демоплат можна влаштувати зручний та дуже швидкий (особливо в порівнянні із semihosting) канал обміну інформацією із комп'ютером --  віртуальний COM-порт (чи UART) , VCP. Подивимося, як це можна зробити.

Зміст:
  1. Огляд USB
  2.  USB VCP на STM32F3Discovery -- початок
  3.  USB VCP -- доводимо до юзабельності



Огляд USB


Заглиблюватися в подробиці USB не буду, однак деякі нюанси важливі навіть для зовсім бездумного використання. Проте, настійливо рекомендую ознайомитися із більш детальним розглядом:

 

Шина

USB -- шина, із топологією tiered-star, українською цей термін іноді перекладають активне дерево -- так як у розгалуженнях мають знаходитися спеціальні концентратори, хаби (hub). У вершині дерева знаходиться майстер (host). З певним уточненням, він завжди один. Глибина може складати до 6 рівнів:
Зображення взято із "USB made simple"
Уточнення -- за певних умов два хоста можуть домовитися, хто із них буде хостом, але це стосується лише пари -- ніяких розгалужень чи суб-хостів.

Трішки осторонь стоїть OTG -- USB On-The-Go (OTG), пристрої, які можуть виконувати роль як хоста, так і пристрою -- в залежності від "обставин". Використовуються вони, наприклад, в смартфонах -- коли його під'єднати до PC, він працює як пристрій, а якщо до нього під'єднати флешку -- як хост.

Фізичний рівень 

Фізично з'єднання відбувається чотирма провідниками:
  1. VBUS (+5 V)
  2. Data−
  3. Data+
  4. Ground
D+ і D- -- звиті між собою, передача бітів здійснюється диференціально, по їх різниці -- це зменшує вплив завад. Однак, використовуються і не диференційовані сигнали (англійською -- single-ended, як перекладають, не знайшов). В значній мірі, заради них цей мікро-огляд і пишеться... 
OTG  пристрій має ще один, 5-й вивід, ID, який, щоправда, нікуди не йде. Він заземляють А-роз'єми -- коли пристрій має служити хостом, і нікуди не під'єднаний в B-роз'ємах, коли пристрій має працювати, власне, підлеглим пристроєм.
Пристрій може живитися від USB. Базово йому виділяється до 100 мА (тобто, споживана потужність до 0.5Вт). Під час подальшої ініціалізації він може "попросити" до 500мА -- 2.5 Вт (900мА в USB3.0+, 4.5Вт; існують інші варіації). Потім це значення змінювати не можна (принаймні в USB 2.0, не впевнений щодо пізніших стандартів). Якщо пристрій якийсь час не активний (всього 3 мілісекунди, насправді), хост переводить його в режим енергозбереження, максимальне дозволене споживання в якому -- 2.5 мА. Більшість хостів не обмежують споживання примусово, але все ж!

Про це треба пам'ятати, працюючи із USB на мікроконтролерах -- зокрема, коли мікроконтролер зупиняється на точці зупинки, він перестає обслуговувати звертання хоста, із результатами, які сильно залежать від ситуації -- від відсутніх, до дивних збоїв, потреби фізично відключати демоплату від живлення і навіть потреби перевантажувати ваш комп'ютер. Та й факт, що про струм, більший за 100мА слід "просити" окремо, не варто забувати.

Виявлення присутності пристрою

На стороні хоста лінії D+ i D- підтягнуті резисторами 15кОм до землі, Ground. Тому, коли пристрої відсутні, на обох лініях буде логічний нуль. 

Трішки відволікшись, нагадаємо, що USB існує у різних варіантах: USB 1.1, USB 2.0, USB 3.x. Стандарт 1.1 передбачав два режими роботи:
  • Low Speed (LS) -- 1.5 Mbit/s (187.5 KB/s)
  • Full Speed (FS) -- 12 Mbit/s (1.5 MB/s)
USB 2.0 додав:
  • High Speed (HS) -- 480 Mbit/s (60 MB/s)
USB 3.x ще декілька (а також додав дві лінії даних, дозволяючи подвоювати швидкодію, та й взагалі, там багато змін -- я поки не вникав, зізнаюся):
  • SuperSpeed  (SS) -- 5 Gbit/s (625 MB/s)
  • SuperSpeed  (SS+) -- 20 Gbit/s (2.5 GB/s)
Так от, після підключення, визначається, буде пристрій працювати по LS чи FS, а пристрої HS  починають працювати як FS, переключаючись вже під час ініціалізації.  (Щодо режимів USB 3 -- це окрема історія, див., наприклад, тут.)
Відбувається це просто:
Зображення взято із "USB made simple"
Якщо D+ підтягнуто до живлення -- FS, якщо ж D- -- LS. (Якщо два нулі, нагадую -- на шині нікого немає).

Чому це важливо для нас? Якщо ви змінили прошивку мікроконтролера, хост слід про це повідомити -- змусити його провести новий цикл ініціалізації. Якщо конструкція демоплати або мікроконтролер не дозволяє зробити це програмно (притягнувши D+ i D- до живлення або відключивши їх), доведеться фізично вимикати і вмикати демоплату. Зокрема, це стосується STM32F3Discovery.

Класи пристроїв, VID:PID

Опускаючи всі інші подробиці роботи USB  (хоча, що таке Control, Bulk, Interrupt та Isochronous  Transfer режими, варто глянути), згадаємо кілька слів про способи впорядкування зоопарку USB пристроїв.
Насправді, випущено із уваги тут дуже багато -- помітно більше ключових подробиць, ніж згадано. Тому настійливо рекомендую таки почитати, скажімо, згадані вище  "USB made simple".

Пристрої поділені на класи, для спрощення операційним системам вибору правильного драйвера. Їх доволі багато, згадаю лише декілька:
  • 01h -- аудіо: колонки, мікрофони, звукові карти, тощо.
  • 02h/0Ah -- communications device class (CDC)/CDC-Data: модеми, адаптери Ethernet та Wi-Fi, RS232, тощо.
  • 06h/0Eh -- зображення/відео: вебкамери, сканери.
  • 07h -- принтери.
  • 08h -- Mass storage (MSC чи UMS): USB-флешки, кард-рідери, аудіо-плеєри, цифрові камери, зовнішні диски.
  • FEh -- спеціалізовані: IrDA, DFU (Device Firmware Upgrade, працюючи із вбудованими пристроями постійно із ним стикатиметеся)  і т.д.
  • Та багато-багато інших!
Нас цікавитиме  клас CDC -- для емуляції UART поверх USB.

Щодо інших класів -- можливо, колись дійду і до них, а поки -- кілька (в міру рандомних) посилань:
Далі, різновид пристрою визначається парою VID:PID -- Vendor ID:Product ID. Визначається в тому сенсі, що пристрої із даними значеннями VID:PID користуються одним і тим же драйвером. Хоча, ОС чи драйвер може їх відрізняти завдяки серійному номеру. Отож, вид пристрою однозначно визначається парою VID:PID, а конкретний пристрій може мати унікальний серійний номер (S/N). Важливо це з точки зору вибору драйверів ОС. Наприклад, Windows, коли зустрічає унікальну комбінацію VID:PID та S/N, він інсталює для неї драйвер, а коли ця комбінація трапляється повторно -- просто його завантажує. Тому, якщо, в процесі експериментів та ж пара VID:PID буде використана для пристрою іншого класу -- обов'язково змініть серійний номер!

І VID i PID -- 16-бітні числа. Звідки їх беруть? Usb.org продає VID виробникам. Вартість станом на момент написання цього тексту -- 5000$. Власник VID може користуватися всіма своїми \(2^16\) PID, зокрема, дозволити комусь скористатися парою VID:конкретний PID (хоча, кажуть, USB Implementers Forum заперечує). Крім того, виробники деяких мікросхем (FTDI, скажімо), дозволяє використовувати певні PID із своїми мікросхемами.

Для домашніх потреб можна скористатися і менш-більш довільною комбінацією. Лише б не співпало із вже під'єднаними пристроями.

Тут неофіційний список VID:PID. Там же, внизу -- список класів пристроїв.  Наприклад, свою пару має STM32F407 -- 0483:5740.

На цьому наш ультра-мінімалістичний огляд USB закінчимо, і перейдемо до практики.

USB VCP на STM32F3Discovery -- початок


Працюватимемо із використанням STM32CubeMX (маю сумніви, чи дойду колись до роботи із USB з використанням лише CMSIS).

Мікроконтролер нашої плати, STM32F303VCT, підтримує:
  • USB 2.0 FS (full-speed)
  • Від 1 до 8 так-званих endpoints -- вище про них не згадувалося, але кожен USB-пристрій може містити до 16 таких "кінцевих точок" (окремо 16 для вводу (IN) і для виводу (OUT) -- дивлячись із точки зору хоста), які безпосередньо передають чи отримують дані).
  • Ізохронний режим (isochronous transfers).
  • USB Suspend/Resume.
  • Він також має 512 байт виділеного SRAM буфера.
Почнемо із генерації проекту саме для цієї плати: 

В Peripherals вмикаємо USB:
Після цього можна вибрати MiddleWare -- бібліотеку підтримки того чи іншого класу USB-пристрою, Communication Device Class у нашому випадку:

Тепер можна переключитися на вкладку налаштування тактування. Там нас чекає проблема. Блок USB потребує тактування 48 МГц, і частота повинна бути дуже точно дотриманою. Плата кварцевим резонатором не обладнана, а від внутрішнього HSI необхідної точності не отримаєш.

Перший варіант -- підпаяти кварцевий кристал  та внести інші відповідні зміни до плати, описані в документації: "UM1570: Discovery kit with STM32F303VC MCU" (URL доволі часто псуються, але номер і назва завжди допоможуть знайти його :-)

Такий варіант має свої мінуси. Але існує й альтернативний трюк. Поглянемо на частину схеми плати, в якій зображено місце для (відсутнього) кристалу:
Бачимо вхід, підписаний MCO (microcontroller clock output, іноді кажуть ще master clock output) -- це, власне, MCO-вихід мікроконтролера STM32F103C8T6, (на якому реалізовано програматор). А програматор має кварц! Щоб отримувати тактування із цього джерела, обираємо на вкладці периферії режим RCC Bypass:
Тепер можна повернутися до вкладки тактування і сконструювати щось таке:

Клікабельно.
Зразу отримуємо максимальну частоту ядра мікроконтролера.

На вкладці Configuration з'явилися дві кнопки, одна -- для конфігурації апаратного модуля USB, інша для конфігурації Middleware:

В діалозі USB (стовпця Connectivity) нічого змінювати не будемо.

Так само не змінюватимемо вміст першої вкладки діалогу налаштування Middleware -- USB_DEVICE (ми ще не настільки заглибилися в подробиці функціювання USB):


Друга вкладка, "Device Descriptor", цікавіша (на даний момент) :


VID та PID краще не змінювати -- ми таки покладаємося на драйвер VCP від STM, тому нам би хотілося, щоб він все ще розпізнавав цей USB-пристрій як "свій" (хоча, як показав експеримент, він сприймає як VCP цілий діапазон PID).

А ось стрічку виробника можна й змінити. Також, може бути корисним змінювати іноді, в процесі розробки, серійний номер -- він дозволяє ОС відрізняти один екземпляр пристрою від іншого, а значить, якщо, після певних модифікацій коду, хочеться прикинутися вже іншим пристроєм... Для початку можна залишити як є. (За подробицями -- див. посилання вище про природу USB).

Завершивши, можна розпочати генерацію коду. Пам'ятаємо вказати генерацію для SW4STM32. Як не раз згадував у попередніх постах, надаю перевагу генерувати по парі файлів .c/.h для кожної периферії -- так (принаймні, мені) простіше навігувати, і створювати резервні копії -- просто про всяк випадок -- помилки бувають у всіх, а коду потім шкода...


Після того, як не раз описано ("GPIO мікроконтролерів STM32F303 з використанням HAL"), імпортуємо проект в SW4STM32. У ньому, поміж іншого, буде ряд файлів із іменем usb*.*:


На їх вмісті не зупинятимуся -- пост і так (знову!) виходить непомірно великим, зауважу тільки:
  • Вже включений в main.c usb_device.h містить, крім оголошення функції ініціалізації MX_USB_DEVICE_Init(), ще й ім'я структури, що описує наш USB-пристрій -- hUsbDeviceFS. Іноді ця структура може бути потрібною.
  • Інший файл, usbd_cdc_if.h, оголошує дуже корисну для початківців функцію -- CDC_Transmit_FS(), за допомогою якої можна передавати текстові стрічки віртуальним COM-портом. 
  • На жаль, схожа за назвою функція, CDC_Receive_FS(), не оголошена в файлі заголовків і зроблена статичною. Так зроблено із вагомих причин! До неї муситимемо повернутися трішки пізніше -- отримання даних від комп'ютера вимагатиме ще деяких дій. 
На вміст інших файлів рекомендую подивитися самостійно!

Отож, підключаємо usbd_cdc_if.h, string.h (для strlen() -- скомпілюється воно і так, але це небезпечно! -- див. неявне оголошення функцій в С) та вписуємо наступний код в головний цикл програми -- після всіх ініціалізацій:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/* USER CODE BEGIN Includes */
#include "usbd_cdc_if.h"
#include <string.h>
/* USER CODE END Includes */

// ................................

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  const char str[] = "Hello!\n";
  while (1)
  {
   CDC_Transmit_FS(str, strlen(str));
   HAL_Delay(500);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

В такому простому прикладі затримка між викликами CDC_Transmit_FS() важлива. Справа в тому, що USB-CDC канал -- багато швидший за semihosting чи UART-9600! Тому без неї текст поступатиме дуже швидко. І, як показала практика, більшість GUI-шних термінальних емуляторів при цьому тупо зависають, намагаючись вивести цей потік на екран. (Позорище, чесно кажучи...) Звичайно, це не стосується випадку, коли дані обробляє ваші програми чи вони зберігаються на диск. Тут необхідну швидкість потоку даних визначатимуть потреби, а не пропускна здатність емулятора термінала.

Компілюємо, заливаємо в мікроконтролер. І нічого не бачимо, навіть якщо він вже був підключений через User USB до комп'ютера. Справа в тому, що, як натякалося вище, взаємодія по USB -- складний процес, зокрема, він потребує від підлеглого пристрою (мікроконтролера) відповідати головному на запити, інакше хост вирішує, що з пристроєм проблема. І після запуску (нової) програми необхідно попросити ОС наново ініціалізувати пристрій. USB надає механізм для цього -- якщо обидві лінії, D+ i D-, підтягнуті до Vcc -- пристрій відсутній, підтягнувши потрібну лінію (D+ для FS) до землі, повідомляємо про його появу. Однак, наша плата, STM32F3Discovery, не містить ніякого механізму для таких маніпуляцій, лінія підтягнута "статично" впаяним резистором:
Взято із інструкції на плату, код UM1570.
Тому, єдиний (точніше, єдиний простий -- без перепаювання чи зовнішніх компонент), вихід -- вимкнути USB-кабель та ввімкнути його наново після залиття нової програми.

Те ж стосується запуску програми під управлінням дебагера -- контролер на час зупинки перестає відповідати, і USB-канал може поводитися дивно.

І ось, коли ми все це зробили, нарешті побачимо в емуляторі термінала:


Нагадую, що baudrate для VCP просто ігнорується.


До речі, в ролі емулятора термінала використовую Hercules -- різностороння програмка, часто дуже корисна. Хоч і не без недоліків. (Наприклад, вона не детектує наявних COM-портів -- доводиться вгадувати). Але вміє багато ще що. Скачати можна тут.

Взнати ім'я пристрою, під Windows можна, наприклад, за допомогою Device Manager:
"Наш" COM-порт -- COM11, вказано зеленою стрілкою.
Червоною стрілкою вказано порт програматора плати. В принципі, за його допомогою теж можна спробувати організувати обмін по віртуальному COM-потру, але успіх цього залежить від прошивки програматора. Можливо, напишу про нього пізніше.
Під Linux (і MacOS?) -- див. /dev/tty*.*, він має з'явитися поміж них.

Не те щоб тут не можна було зупинитися, передати на комп'ютер інформацію ми вже можемо -- іноді цього достатньо. Але, з одного боку, хотілося б мати можливість використовувати які-небудь засоби форматування, типу printf(), з іншого -- приймати інформацію від комп'ютера теж часто потрібно. Спробуємо організувати і те і друге.

USB VCP -- доводимо до юзабельності

 

Отримання даних 



Функція CDC_Receive_FS() зроблена static -- доступною лише із файлу, де вона визначена, не просто так. Справа в тому, що викликається ця функція автоматично, (із обробника переривань), коли приходять дані . Вона, в початковій реалізації, всього лиш підтверджує отримання даних, даючи можливість знову щось прийняти.

Щоб зберегти отримані раніше дані, перш ніж підтверджувати, слід забрати із буфера попередні -- щойно отримані. Проблема ускладнюється обмеженою пам'яттю мікроконтролера. Справді, USB потрібен буфер (вимагається 2Кб), і дані з цього буфера слід негайно забирати, бо наступна "транзакція" по шині їх затре. Значить треба ще один буфер. Який теж не може бути особливо великим -- пам'яті у нашого мікроконтролера всього 48Кб, але у ньому ми хоча б самі можемо контролювати момент затирання. Підходів тут декілька. З врахуванням фіксованого розміру буфера, базових два: або кільцевий буфер, коли нові дані затирають старі, або відкидати нові дані, поки не прочитано старі.


Кільцевий буфер


Зупинимося на кільцевому буфері. Для ілюстрації в проект включено примітивну його реалізацію (файли ringbuf.h і ringbuf.c). Інтерфейс наступний:
  • Одиницею інформації є байт (uint8_t).
  • Кільцевий буфер описується структурою ringbuf_t. Вона містить вказівник на зовнішній блок пам'яті для даних, його розмір, індекси початку (tail) та кінця (head) буфера (дані додаються з кінця, забираються з початку), кількість зайнятих елементів в буфері та код помилки останньої операції.
  • Для ініціалізації можна скористатися функцією:
    void ringbuf_init(ringbuf_t* rb, uint8_t* buf, size_t sz);
    якій слід передати вказівник на структуру кільцевого буфера, його зовнішній блок даних (buf), разом із розміром (sz).
  • Покласти байт в буфер:
    void ringbuf_put(ringbuf_t* rb, uint8_t data);
    Якщо буфер вже заповнений, затирає найстаріший байт, виставляє код останньої помилки RB_Overflow.
  • Забрати байт із буфера:
    uint8_t ringbuf_get(ringbuf_t* rb);
    Якщо буфер порожній, повертає -1 (0xFF, фактично) та встановлює код останньої помилки RB_Underflow (інакше -- RB_OK).
    Просто підглянути на останній елемент буфера (той, який буде забрано наступним викликом ringbuf_get()):
    uint8_t ringbuf_peek(ringbuf_t* rb);
  • Подивитися, скільки в буфері даних, чи він повний або порожній:
    size_t ringbuf_size(ringbuf_t* rb);
    int ringbuf_empty(ringbuf_t* rb);
    int ringbuf_full(ringbuf_t* rb)
    ;
  • Скопіювати весь вміст кільцевого буфера  за вказаною адресою:
    size_t ringbuf_read_all_data(ringbuf_t* rb, uint8_t* buf);
    Лише n байт -- за допомогою:
    size_t ringbuf_read_n_data(ringbuf_t* rb, uint8_t* buf, size_t n);
  • Код помилки, в цілях оптимізації, в RB_OK встановлюють лише ті функції, які його взагалі змінюють, функції, що завжди спрацьовують безпомилково його не чіпають.  
    Подивитися код останньої помилки:
    ringbuf_err_t ringbuf_last_error(const ringbuf_t* rb);

Примітивність реалізації в тому, що: а) структура має певну надлишковість, для спрощення коду, б) згаданий код маніпулює окремими байтами в циклі, замість скористатися memcpy() (див., скажімо, тут) або, ще краще -- DMA). Однак, на інтерфейс такі зміни майже не вплинуть.

Повертаємося до отримання даних

Автоматично згенеровані файли usbd_cdc_if.c/.h допускають редагування -- у них багато секцій "USER CODE", які Cube поважатиме при перегенерації. Так як редагувати цей файл все рівно потрібно, то, щоб не плодити сутності, додаткові функції включимо в нього ж.

Каюся, хоча Cube генерує такі коментарі для користувацьких макросів, змінних, функцій і т.д., я таки полінувався їх акуратно розпихувати по секціях. Все ж, їх мало, і "розмазування", більш правильне у великому проекті, швидше заплутає читачів.

Поїхали. Почнемо із usbd_cdc_if.c. У функції зворотного виклику, CDC_Receive_FS(), додамо виклик, що копіює отримані дані у внутрішній кільцевий буфер:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static int8_t CDC_Receive_FS (uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */

  USB_ReceiveData_Callback(Buf, Len);

  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
  USBD_CDC_ReceivePacket(&hUsbDeviceFS);
  return (USBD_OK);
  /* USER CODE END 6 */ 
}

Сама USB_ReceiveData_Callback(), разом із необхідними їй даними, виглядає так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
static uint8_t USBRxBuffer[USBRxBufferSize];
volatile ringbuf_t USB_Buffer = {USBRxBuffer, USBRxBufferSize, 0, 0, 0,  RB_OK };


static uint8_t USB_ReceiveData_Callback (uint8_t* Buf, uint32_t *Len)
{
     if(USB_Buffer.buf == NULL)
  return USBD_FAIL;
     size_t counter = 0;
     USB_Buffer.last_error = RB_OK;
     while(counter < *Len){
      ringbuf_put(&USB_Buffer, Buf[counter++]);
     }
     return (USBD_OK);
}

Власне, вона примітивна, як двері -- по байту забирає свіжі дані і кладе їх в кільцевий буфер. (Зрозуміло, що з точки зору ефективності її слід переписати із використанням memcpy() або DMA! Але, заради простоти, поки залишимо як є.)

Інтерфейс описано в usbd_cdc_if.h. Він теж трохи примітивний:

1
extern volatile ringbuf_t USB_Buffer;

Одна змінна плюс інтерфейс кільцевого буфера, описаний вище... (Розмір буфера, USBRxBufferSize, оголошено там же). Можна приступати до отримання даних. :-) Єдине, не забуваємо забороняти переривання! Бо буде страшний звір під назвою data race.

Приклад -- що отримали з комп'ютера, те і передаємо назад:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  /* USER CODE BEGIN WHILE */
  const char endline[] = "\n";

  while (1)
  {
   if( !ringbuf_empty(&USB_Buffer) )
   {
    __disable_irq();
    size_t sz = ringbuf_read_all_data(&USB_Buffer, LocalRxBuffer);
    __enable_irq();
    CDC_Transmit_FS(LocalRxBuffer, sz);
    CDC_Transmit_FS(endline, strlen(endline));
   }
   HAL_Delay(50);
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

Готовність USB

Нам бракує ще однієї важливої можливості -- перевіряти, а чи дійсно до USB "хтось" приєднався.
Поки USB не під'єднано та не ініціалізовано, CDC_Transmit_FS() повертатиме USBD_BUSY, а отримання даних, природно, не відбуватиметься. Тобто, нічого страшного не відбуватиметься. Але, все рівно, краще знати.
Допомогти в цьому можуть дві інші функції із usbd_cdc_if.c, CDC_Init_FS() i CDC_DeInit_FS(), які автоматично викликаються, коли відбувається приєднання чи від'єднання USB. Доповнимо їх volatile-змінною, яка зберігатиме true, якщо під'єдналися і false, якщо (вже) ні:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
volatile int USB_CDC_ready = 0;

int USB_CDC_Is_Ready()
{
 return USB_CDC_ready;
}

static int8_t CDC_Init_FS(void)
{ 
  /* USER CODE BEGIN 3 */ 
  /* Set Application Buffers */
  USBD_CDC_SetTxBuffer(&hUsbDeviceFS, UserTxBufferFS, 0);
  USBD_CDC_SetRxBuffer(&hUsbDeviceFS, UserRxBufferFS);
  USB_CDC_ready = 1;
  return (USBD_OK);
  /* USER CODE END 3 */ 
}

static int8_t CDC_DeInit_FS(void)
{
  /* USER CODE BEGIN 4 */ 
  USB_CDC_ready = 0;
  return (USBD_OK);
  /* USER CODE END 4 */ 
}


Їх код, окрім маніпуляцій USB_CDC_ready, створено Cube.

Природно, оголошення функції USB_CDC_Is_Ready() додаємо до usbd_cdc_if.h.

Тепер у коді ваших програм, там, де слід дочекатися наявності каналу USB, перш ніж щось робити, можна вставляти код виду:

1
while(!USB_CDC_Is_Ready()){}

Інтеграція із стандартною бібліотекою

Коду, описаного вище, достатньо для повноцінного обміну мікроконтролера із комп'ютером. Однак, використання printf()/scanf() таки зручніше, ніж маніпуляція лише текстовими буферами. sprintf()/sscanf() трішки спрощують життя, але турбуватися про буфери все ще треба.

Згадаємо пости "Стандартна бібліотека C та SemiHosting (на прикладі STM32 і CoIDE)" та "Зовсім просто про semihosting" -- навчити printf()/scanf() працювати із USB не так складно. Звичайно, повноцінна, POSIX-сумісна реалізація, все ж, буде відносно складною. Колись ми до цієї теми повернемося детальніше (якраз в наступному семестрі, на курсі ОС ;-), а поки обмежимося більш примітивною реалізацією:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
__attribute__ ((used))
int _open (const char *name, int flags, int mode)
{
 if(strcmp(name, "/dev/usb0") == 0)
 {
  while(!USB_CDC_Is_Ready()){}
  return 3;
 }
 errno = EACCES;
 return -1;
}

__attribute__ ((used))
int _write(int file, char *ptr, int len)
{
     if(file == 1 || file == 2 )
     {
          int txCount;
          for ( txCount = 0; txCount < len; txCount++)
          {
               SH_SendChar(ptr[txCount]);
          }
     }
     if(file == 3)
     {
      CDC_Transmit_FS( (uint8_t*)ptr, (uint16_t)len);
     }
     return len;
}

__attribute__ ((weak))
int _read(int fd, char *ptr, int len )
{
 if(file != 3)
 {
  ERRNO = EINVAL;
  return -1;
 }

 __disable_irq();
 size_t data_len = len > ringbuf_size(&USB_Buffer) ? ringbuf_size(&USB_Buffer) : len;
 int sz = ringbuf_read_n_data(&USB_Buffer, (uint8_t*)ptr, data_len);
 __enable_irq();
 return sz;
}

Тоді зможемо робити так:

1
2
3
4
5
FILE* usb0 = fopen("/dev/usb0", "rw");

int a = 3;
fprintf(usb0, "%x\n", a);
fscanf(usb0, "%x\n", &a);

(Якщо захочете друкувати double -- підключіть їх підтримку, див. "Зовсім просто про semihosting", розділ "Підтримка floating-point").
 
Більш загальний огляд способів вводу-виводу: "Exploring printf on Cortex-M".

Проект із кодом вище можна скачати тут. Він також містить бонуси: ранню, ще сиру, реалізацію спеціальних файлів для роботи із USB, SemiHosting, і т.д. та приклади для інших демоплат. Щодо реалізації системних викликів, коли доведу її до ладу -- опублікую окремо. Для наочності, якщо код "слухає" дані з USB, мигатиме світлодіодом LD5. Пам'ятайте "пересмикнути" кабель, під'єднаний до USER!

Наступного разу поговоримо про використання кількох інших демоплат із серії STM32 Discovery.

А поки --

Дякую за увагу!

2 коментарі:

  1. Авторе, велике дякую за вашу працю! Дивно що досі не знаходив ваші статті, вони дуже корисні!

    ВідповістиВидалити