Постановка задачи
Итак, мы хотим сделать устройство, которое обменивается с компьютером сообщениями произвольной длины через USB порт. Самый простой способ сделать это — воспользоваться USB классом символьных устройств (CDC), известным также под названием 'виртуальный последовательный порт'. Тогда на хост-системе, к которой вы подключите ваше устройство, автоматически будет создан последовательный порт, через который вы сможете обмениваться данными с устройством, работая с ним как с обычным файлом. На практике, однако, выясняется, что некоторые необходимые для этого функции в USB-стеке производителя либо не реализованы вовсе, либо реализованы с ошибками. Мы начнем с рассмотрения микроконтроллеров STM32 (первый случай) и закончим другим популярным семейством — Texas Instruments Tiva C (второй случай). Оба семейства имеют архитектуру ARM Cortex M4.
STM32 — просто добавь кода
Микроконтроллеры STM обычно имеют богатый функционал при весьма демократичной цене. Производитель поставляет широкий спектр библиотек на все случаи жизни. Среди них есть и библиотеки для поддержки USB, и библиотека для работы с прочей периферией, имеющейся на кристалле. В последнее время все эти библиотеки были объединены в один мега-пакет под названием STM32Cube. При этом, однако, о совместимости особо не заботились и поменяли все, что только смогли поменять, включая названия полей в структурах, описывающих конфигурацию портов ввода-вывода, при том, что само название структуры осталось прежним. Интресно, что есть еще и третий вариант примеров и библиотек, который можно найти на сайте stm32f4-discovery.com/. Однако, автор этого варианта очень любит переименовывать файлы, позаимствованные у STM, дабы увековечить свои инициалы, что тоже не добавляет совместимости со всем остальным кодом. Учитывая все вышеизложенное, я решил взять за основу последний до-кубический вариант библиотек, поставляемых STM. Сейчас их можно найти в комплекте поставки компиляторов (я использую IAR). Чтобы потом долго не искать, библиотеки включены в состав проекта, который вы можете взять из гита по ссылке внизу. Для экспериментов я использовал плату STM32F4DISCOVERY http://ift.tt/1agoxVt. Если у вас другая плата и код сразу не заработал, дело скорее всего в частоте внешнего кварцевого генератора. Хотя библиотеки изобилуют всяческими макроопределениями, и в последней версии библиотек среди них появился и макрос для внешней тактовой частоты, в коде этот параметр по-прежнему прописан в виде числа без всяких комментариев, видимо, чтобы разработчики не теряли форму и не забывали читать мануал. Вы можете найти это число — тактовую частоту в мегагерцах — в файле 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 http://ift.tt/19wxKgd. Для компиляции необходим пакет библиотек TivaWare http://ift.tt/1qPmkZK. Изучение библиотек показывает, что разработчики не обошли вниманием ни ZLP ни проблему буферизации — во входном и выходном канале имеются готовые к использованию кольцевые буфера. Однако автоматический тест дает все тот же результат — обмен данными внезапно прекращается. С помощью отладчика выясняется, что на этот раз данные застряли в кольцевом буфере передачи, причем с размером последнего пакета, а значит и с ZLP, проблема не связана никак.
Выявить проблему удается только путем тщательного изучения исходников библиотек. Оказывается, что для посылки ZLP необходимо выставить специальный флажок, который по умолчанию не выставлен. Возможно, это обстоятельство и подтолкнуло других разработчиков к тому, чтобы добавить код, посылающий ZLP еще в одном месте — на более низком уровне USB-стека, и уже без флажка. Это изменение и внесло баг, приводящий к остановке передачи. Проблема возникает следующим образом. Передатчик получает следующий пакет, когда заканчивается передача предыдущего, либо если предыдущего не было, а приложение добавило данные в буфер передачи. Код, который инициирует передачу, получает нотификацию о завершении передачи предыдущего пакета от нижнего уровня USB-стека. Проблема в том, что если нижний уровень стека инициировал передачу ZLP, то нотификацию о завершении он не присылает, т.к. инициировал передачу он сам. Верхний уровень не начинает передачу данных, пока передатчик занят передачей ZLP пакета, и не начинает передачу после ее завершения, поскольку не получает нотификации — процесс передачи останавливается. Исправить проблему очень просто — нужно убрать код нижнего уровня, посылающий ZLP, и предоставить это верхнему уровню стека. Вторая проблема, требующая решения, связана с тем, что процедура, начинающая передачу, может быть вызвана как из контекста обработчика прерывания (по завершении передачи), так и из контекста приложения по добавлении данных в буфер передачи. Чтобы сериализовать вызовы этой процедуры из разных контекстов, нужно запрещать прерывания на время ее исполнения.
Исходный код
Лежит тут http://ift.tt/1BVbfyz.
В папках stm и ti лежат по 2 тестовых проекта — usb_cdc_echo и usb_cdc_api. Первый просто посылает все полученные данные обратно, второй реализует пакетный протокол, который вы можете легко адаптировать под свои нужды. В папке tools — тестовые скрипты на питоне.
Recommended article: Chomsky: We Are All – Fill in the Blank.
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий