Наличие USB порта в современных микроконтроллерах открывает широкие возможности для самостоятельного изготовления разнообразных управляемых с компьютера устройств. На практике, однако, выясняется, что поставляемые производителем библиотеки для работы с USB нуждаются в доработке. Если вам интересен опыт подобной доработки для двух популярных семейств МК — добро пожаловать под кат.
Итак, мы хотим сделать устройство, которое обменивается с компьютером сообщениями произвольной длины через USB порт. Самый простой способ сделать это — воспользоваться USB классом символьных устройств (CDC), известным также под названием 'виртуальный последовательный порт'. Тогда на хост-системе, к которой вы подключите ваше устройство, автоматически будет создан последовательный порт, через который вы сможете обмениваться данными с устройством, работая с ним как с обычным файлом. На практике, однако, выясняется, что некоторые необходимые для этого функции в USB-стеке производителя либо не реализованы вовсе, либо реализованы с ошибками. Мы начнем с рассмотрения микроконтроллеров STM32 (первый случай) и закончим другим популярным семейством — Texas Instruments Tiva C (второй случай). Оба семейства имеют архитектуру ARM Cortex M4.
Микроконтроллеры 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 на все входящие пакеты.
Для экспериментов была взята плата 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 — тестовые скрипты на питоне.
Постановка задачи
Итак, мы хотим сделать устройство, которое обменивается с компьютером сообщениями произвольной длины через 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 — тестовые скрипты на питоне.