STM32 BluePill + RNDIS, или делаем из нехитрых приспособлений троллейбус…
Привет, Хабр. Год назад я круто изменил направление своей деятельности с desktop разработчика на программиста микроконтроллеров. Хочу поговорить о том, как прошел этот год, поделиться своими наблюдениями, рассказать о чем никто не рассказывает. А, еще и RNDIS на stm32f103 поднимем.
Статья будет просто набором заметок о технологиях, которые вроде бы многие знают, но как будто бы не все…
GIT для микроконтроллерщика
Скажу сразу: я не специалист — я типичный самозванец. Мои познания в схемотехнике ограничиваются законами Ома и Кирхгофа. Новая работа очень близка к железу, отладке плат и прочим электромагнитным экспериментам. Но вот вам обратное наблюдение (не только моё): люди, которые заняты программированием на микроконтроллерах зачастую далеки от программирования. Они запросто вам расскажут как подключить транзистор к ножке МК, но библиотеку к проекту будут подключать с горем пополам. Я это к чему. Я попал на пару десятков проектов на разных распространенных камнях STM32 (серия f103, f105, f407), с системой контроля версий в виде копирования папки проекта целиком и переименования в `_old_{дата}`, с проектами в просто морально устаревшей IDE (которую уже невозможно даже скачать), с файлами main.c в >5000 строк (и это без комментариев, просто спагетти, паста, код, в некоторые ветви которого никак попасть) и т.д. И все это работало, но работало только пока было под присмотром автора. Как только он ушел, и я остался с проектами один-на-один, а ко мне начали приходить с просьбами добавить/исправить фичу — я схватился за голову.
Система контроля версий одного из проектов
Первым делом, пока мы еще работали с прежним микроконтроллерщиком, я явил людям git. Для упрощения, в виде простой связки git + TortoiseGit с репозиторием в (хотя бы) локальной папке. Не нужно запоминать синтаксис команд, только понять что делает одна единственная операция: commit, — и вы на коне. Глаза моего коллеги, когда он увидел, как я в несколько кликов показал ему diff между двумя его папками, — не передать словами. Для каких-то проектов, которые уже не так активно велись мы просто сделали initial commit, для других я набросал скрипт, который пробегал по всем копиям папок и собирал историю изменений с комментариями в виде названий папок. Через несколько дней ко мне подошел человек из другого отдела и спросил, нет ли чего-нибудь похожего для его задач…
IDE
Мой опыт программирования МК все же был немного больше нуля (завершенный pet-проект датчика CO2 на attiny13 со стрелочным индикатором). Здесь же мне была вручена синяя таблетка — и с нее все началось. А с чего, собственно, начать? Я начал искать озвученную мне CooCox CoIDE. Чтобывыдумали? Нет уже ни сайта, ни разработчика, ни архивов. Где-то на просторах обучающих блогов еще можно найти установщики -, но это никуда не годится. Поставить её я все равно был вынужден для поддержки старых проектов. Откуда мне было знать, сколько должен собираться проект под МК, мы просто беседовали обо всем, пока чего-то там крутилось и собиралось… и раз в пять минут писали код. Однако, я начал искать варианты по-современнее. Из рассмотренных вариантов:
Visual Studio + VisualGDB — мощнейшая IDE, но платный аддон. Я пользовался этой связкой раньше при разработке под Embedded Linux, но со своим инструментарием в чужую избу не ходят. Большой и универсальный комбайн, к которому нужно привыкать, с избыточным набором инструментов под поставленные задачи.
VS Code + PlatformIO — еще вариант. Честно, не могу вспомнить что там было не так. (upd: да, там действительно в то время функция дебага была платной фичей только по подписке)
Keil uVision — нет темной темы — хаха!
STM32 CubeIDE — собственно IDE от производителя, на которой я и остановился. Современный Eclipse с поддержкой аддонов из Marketplace.
CubeIDE оказалась самой готовой к работе из всех. Плюс встроенная поддержка .ioc файлов, в которых можно разметить и настроить МК (об этом дальше). Очень быстро получилось разобраться где-чего-куда, перенести проекты из CoIDE в CubeIDE. И, о чудо или о горе, проекты почему-то стали собираться за несколько секунд. Магия, не иначе.
Перенос проекта из CoIDE в CubeIDE
New → STM32 Project
Выбрать соответствующий микроконтроллер в MCU/MPU Selector. Next
Ввести название проекта и выбрать для него папку. Target Language = C, Target Binary = Executable, Targeted Project = Empty. Finish
Готовим файлы
Удалить папки Inc, Src, Startup
Скопировать файлы *.c *.h из проекта CoIDE
Правой кнопкой мыши по проекту → Properties
C/C++ Build → Settings → Tool Settings →
MCU GCC Compiler → General → Language standard = GCC default
MCU GCC Compiler → Include paths В таблицу Includes добавить папки из проекта, где находятся .h файлы. В том числе корневой, если в нем находится *.h
MCU GCC Linker → General → поставить галочки если не стоят напротив:
— Do not use standard start files, — отвечает за startup файл для мк
— Do not use default libraries, No startup or default libs — отвечает за вызов выбранной syscalls.c имплементации (nosys и nano specs)
C/C++ General → Paths and Symbols
В таблицу Symbols добавить дефайны из проекта CoIDE (Configuration → Compile Defined Symbols) (например, STM32F103CB; STM32F10X_MD; USE_STDPERIPH_DRIVER;__ASSEMBLY__)
В список Source Location добавить папки, где находятся .c файлы.
Билд
Если билд не удался, смотреть что к чему. Удалить файлы, которые не участвовали в компиляции в проекте CoIDE.
Вы же пишете такие памятки? Для самого себя — это очень экономит время при возвращении к вопросу. Для кого-то другого — кому-нибудь, возможно, придется заново проходить этот путь. Пожалуйста, оставляйте после себя на рабочем месте такого рода информацию, помогите тем, кто придет на ваше место. Я завел отдельную папку таких вот паст на разные темы, с чем приходилось сталкиваться по работе.
Меня ждал сюрприз, когда поехала кодировка немногочисленных комментариев в коде. Оказалось, что все файлы были в CR1251. Ох!
foreach($file in get-ChildItem *.c -Recurse) { (Get-Content -Path $file) | Set-Content -Encoding UTF8 -Path $file}
CMSIS SPL LL HAL
Во время поиска IDE я уже понимал, что встанет и другой вопрос. Легаси проекты были собраны из смеси прямых обращений к регистрам и SPL. Выбранная CubeIDE в первую очередь заточена на генерацию проектов из .ioc на HAL или частично LL. Но давайте по порядку про основные понятия:
CMSIS — и не библиотека вообще-то, это интерфейс, заголовочные файлы от разработчиков архитектуры ARM. Штука почти независимая от производителя МК, содержит адреса только основных регистров и прерываний. Это практически нижайший уровень доступа к МК. К нему прилагается файл от производителя МК с описанием регистров периферии, которую они запихнули в конкретную модель МК.
SPL — вот это уже библиотека под определенные семейства микропроцессоров от ST. Реализация того, что написано в Programming Manual для вашего МК. Она обращается по адресам указанным в CMSIS и реализует в себе функции для доступа, инициализации, управления периферией. С помощью нее можно выразить практически весь функционал МК. На мой взгляд, лучшая библиотека. Но, к сожалению, «старая». ST решили прекратить поддержку и выдали нам…
LL — очень похожую библиотеку, но кастрированную. По сути брат близнец SPL, недоразвитый и неполноценный. Нет, например, поддержки CAN и некоторых других модулей (почему?), а еще какая-то странная политика именования функций и дефайнов. Какие-то начинаются с
LL_*
, какие-то нет; параметры у функций и принимаемые константы никаким смысловым наполнением не связаны. ST даже выкатили некий конвертер, но что-то серьезнее Blink, пережевывает он с трудом.HAL — библиотека с более высоким заявленным уровнем абстракции. И тоже странная, отчасти. Некоторые модули зависят от LL, другие вполне самостоятельны. HAL подразумевает абстракцию над железом, легкий перенос с одного семейства на другое, но чуда не происходит, все равно приходится портировать код, а иногда еще и долго искать почему что-то перестало работать. Библиотека вроде бы много берет на себя, но не все, и после её инициализации, приходится еще самому «довызывать» функции, причем долго гадать, чего же не хватило. И к тому же из-за абстрагирования и оверхеда имеет больший отпечаток, т.е. занимает много места в памяти, по сравнению с SPL или LL.
libopencm3 — популярная open source библиотека для ARM Cortex-M3 (под который попадает и наш f103), можно найти много примеров как простых, так и сложных проектов. Не пользовался, ничего не могу сказать.
По поводу именования функций и дефайнов, о котором я выше упомянул: вот вам для сравнения простейший пример, GPIO в SPL и LL/HAL:
Автодополнение в SPLАвтодополнение в LL/HAL
Та же беда и у HAL. Поле структуры называется TimeSeg1
, а значение ему впиши CAN_BS1_1TQ
. В противовес у SPL: поле CAN_BS1
и значение CAN_BS1_1tq
(если забыл, что такое BS1, нажми на поле структуры, и тебе явится подсказка `Specifies the number of time quanta in Bit Segment 1`).
Бомбит меня от этих поделий. Да, такой код пишется единожды, но когда оно сплошь такое — бомбит. И это не автодополнение глупое, это я глупый, раз не могу запомнить как же называется нужный мне дефайн для данного поля структуры.
CubeIDE Configuration tool / CubeMX
Однако преимуществом LL / HAL является сопутствующая утилита по конфигурации микроконтроллера. Как ни крути, очень удобно визуально увидеть занятые ножки, выгнать эти значения и передать в отдел разработки плат, визуально сконфигурировать периферию, одно удовольствие настраивать тут таймеры и тайминги. Но, опять же, генерирует код она только в LL или HAL. И, опять же, какая-то периферия не имеет имплементации на LL, и если вы хотели бы написать что-то без HAL, то это ваши проблемы.
Настройка тактирования
С помощью этой утилиты я проводил реверс-инжиниринг легаси проектов на SPL. Я собирал ioc и размечал те пины, которые были подведены на плате, дальше дело шло легче. В коде тут и там дергались модули, которые даже не были инициализированы, и я смело удалял эти куски. Главное не нажать случайно кнопку Generate Code.
Структура проекта
Сейчас новые проекты я стараюсь писать полностью на LL, хочется идти в ногу со временем, так сказать (пришлось переписать can и flash, чтобы не тянуть HAL). Однако большая часть легаси проектов на SPL, где не было генерации файлов и папок, и один несчастный main.c в over9000 строк кода — это неприемлемо, я начал искать в сети, как программисты МК организовывают структуру проекта. И я не нашел. Ладно, может что-то я нашел, но понял, что каждый делает так, как удобно себе. Так же сделал и я, собрал все полученные знания, волю в кулак и начал рефакторить. По итогу получилось придумать несколько логичную структуру проекта. Ничего не навязываю, просто даю пример, потому что хочется иногда найти в сети пример и использовать:
Структура проекта
Папка device — индивидуальна для каждого проекта. В ней лежит device.h, который хранит мои описания пинов, имен, адресов и прочего, конкретно для этого проекта. Сюда же ложатся файлы реализующие конфигурацию и логику работы модулей конкретного девайса. В папку lib же ложатся переносимые из проекта в проект библиотеки, не только CMSIS и SPL, но и мои helper функции, модули для периферии, внешних датчиков и т.д.
Параллельно с ежедневным рефакторингом и рабочими задачами я продолжал знакомится с новой для меня областью программирования, что вылилось в серию проектов на github, ничем не примечательных, но связанных между собой одним шаблоном. Побольше бы таких шаблонов для типовых задач и IDE.
RNDIS
Как оказалось, ничего катастрофически сложного в программировании STM32 нет. Знай да всегда помни (если что-то вдруг не работает), включил ли ты тактирование, правильно ли выставил множители, да всю ли инициализацию провел. Даташит твой лучший друг, код SPL библиотеки прекрасно документирован и иногда даже позволяет к лучшему другу не обращаться.
И вот, спустя почти год работы с камнями, я нашел свой макгаффин. Применить полученные знания и написать Ethernet поверх USB на той самой BluePill. Вообще-то что-то такое уже есть, в одном единственном экземпляре: @fse написал несколько статей на эту тему (upd:, а с недавних пор есть и в TinyUSB). Титанический труд, без понятия чего это стоило и как получилось заставить работать. Проект Сергея послужил для меня отправной точкой.
USB
RNDIS начинается с USB: качаем USB full speed device library (UM0424). Библиотека делится условно на две части: USB Full Speed device driver и Application Interface layer. Первая часть — это внутренняя кухня протокола USB: прием прерываний, заполнение буферов и т.д. Эта часть остается неизменной. Вторая — это непосредственно то, что мы хотим изобразить — интерфейс, которым наш USB должен прикинуться. Здесь будут наши USB-дескрипторы, эндпоинты, логика работы и т.д. Нельзя не упомянуть USB in Nutshell, которая помогла с большего разобраться с USB стеком. Давайте немного посмотрим на интересные моменты либы.
В заголовочном hw_config.h есть такой интересный параметр: USB_DISCONNECT_PIN
. На отладочных платах, по-хорошему, подразумевается транзистор, который электронно бы размыкал цепь между хостом и устройством. Что мы имеем на BluePill? Ничего. Однако на просторах интернета я отыскал хитрый прием: наличие устройства хост определяет по состоянию линии D+, стоит перевести пин в низкое состояние Out Push-Pull (высокого импеданса / HiZ) на >=5 мс — и хосту покажется, что мы отключили устройство. Поэтому мы назначаем этому USB_DISCONNECT_PIN
наш пин PA12(USB D+). Для чего это всё? Теперь при программной перезагрузке камня (например вход в дебаг), устройство само «отключается» от ПК и «подключается» обратно, запуская со стороны хоста соответствующие процессы по установлению USB соединения.
GPIO_InitStructure.GPIO_Pin = USB_DISCONNECT_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(USB_DISCONNECT, &GPIO_InitStructure);
GPIO_ResetBits(USB_DISCONNECT, USB_DISCONNECT_PIN);
DWT_Delay_ms(5);
GPIO_InitStructure.GPIO_Pin = USB_DISCONNECT_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(USB_DISCONNECT, &GPIO_InitStructure);
Описание USB устройства находится в usb_desc.c. Официальная классификация USB устройств содержит такой класс, как RNDIS (0xEF 0×04), но почему-то наше устройство отказывается определяться с такими значениями, пришлось использовать подсмотренное значение, которое работает как Wireless Controller (0xE0 0×00). В этом же файле описание эндпоинтов, а также текстовое представление устройства (String Descriptors).
Вся непосредственная магия RNDIS пакетов находится в usb_prop.c и usb_endp.c. В основном большую часть времени я тут и провел. На просторах я даже нашел чей-то дневник попыток переписать RNDIS на HAL. Почерпнул оттуда идею использования USB анализатора, перепробовал разных (USBlyzer, USBTrace, бесплатный WireShark + USBPcap) — и каждый выдавал мне различные, но полезные данные. Помни, юный USB-падаван: USB хост-центричен — IN (up) всегда к хосту, OUT (down) всегда к девайсу.
Data Breakpoint
Кстати, во время отладки мне в один момент понадобилось знать кто-и-когда меняет мне значение в памяти. Но переменная глобальная, к ней обращений из всех уголков USB библиотеки. Так я наткнулся на Data Breakpoints. Такие точки останова, которые вызываются в момент записи/чтения указанной переменной.
В CubeIDE зовутся они Watchpoint и настраиваются на вкладках Variables, Expressions и даже Memory.
Настройка Data Breakpoint в CubeIDE
Включаем дебаг, отправляем интересующий запрос — срабатывает точка останова, виден stack trace, виден подлец, который пишет туда-куда-не-надо. Таким арсеналом и получилось завести RNDIS.
LwIP
Теперь давайте посмотрим на подключение (и портирование) библиотеки LwIP. Не будем брать какую-нибудь древнюю готовую реализацию, а сделаем все по-взрослому: добавим в наш git репозиторий подмодуль и напишем порт по их гайду, чтобы понимать что и откуда берется.
Я уже упоминал ToroiseGit, в нем все делается проще простого: заходим в файловом менеджере в папку lib нашего проекта и через контекстное меню выбираем TortoiseGit → Submodule Add
Видим окно, в которое вводится адрес репозитория LwIP и опционально выбирается бранч (STABLE-2_1_x).
Добавления субмодуля git
Библиотека LwIP построена таким образом, что нам не надо ничего менять в её исходных файлах. Все настраивается «снаружи» библиотеки. Единственные манипуляции, которые необходимы над папкой библиотеки — это исключить из сборки папки lwip\doc, lwip\test, lwip\apps\http\makefsdata (утилита для хоста, о ней позже)
Рядом с папкой lwip я создал lwip_port, где и располагается все касаемо нашего портирования. Согласно гайду, в arch/cc.h ложатся адаптированные под нашу архитектуру и компилятор базовые методы (например htons, htonl), реализации функций работы со строками (itoa, strcmp, strstr), типы переменных и т.д. Если этого не приложить, LwIP будет использовать собственные софтовые реализации, которые могут быть медленнее и больше.
В файле lwipopts.h перечислены настройки библиотеки. Среди важных MEM_SIZE
, который определяет размер буфера под пакеты, и MEMP_NUM_PBUF
, который отвечает за количество этих пакетов. Чем больше траффиков©, тем выше должны быть эти значения. Тут мы немного сэкономим, чтобы все поместилось в память МК.
Строка
#define LWIP_IP_ACCEPT_UDP_PORT(p) ((p) == PP_NTOHS(67))
Необходима для работы DHCP сервера (он использует порт 67 для своих служебных коммуникаций).
Также на демо-странице я вставил картинку в формате .svg, но сам LwIP по умолчанию не знает о нем и воспринимает файл по умолчанию как «text/plain», исправляется путем добавления
#define HTTPD_ADDITIONAL_CONTENT_TYPES {"svg", HTTP_CONTENT_TYPE("image/svg+xml")}
Ту самую утилиту makefsdata, которую мы исключили из сборки, нужно собирать отдельно под вашу операционную систему уже после настройки lwipopts (он подтягивает некоторые настройки, например указанные нами Content types).
Связующим интерфейсом между USB-RNDIS и LwIP является rndisif.c (rndis interface). Здесь мы парсим RNDIS пакет и запаковываем в LwIP (rndisif_input()
), и наоборот (linkoutput_fn()
). Вся инициализация лежит в файле netconf.c, там же настраивается DHCP и DNS сервера (про них подробнее в статье у Сергея).
HTTPD
HTTPD одна из составляющих служб LwIP, отвечающая за работу с протоколом HTTP. Мы уже в настройках её включили, как и два механизма: SSI (Server Side Includes) и CGI (Common Gateway Interface). Пример использования лежит в device/httpd_cgi_ssi.c.
SSI позволяет нам на стороне сервера изменять представляемую пользователю страницу, выводя таким образом, например, значения переменных МК или состояние пинов. На стороне страницы мы помечаем тегом место, куда сервер должен будет вставить текст. Выглядит он так . На стороне сервера мы указываем список этих тегов и пишем обработчик, что подставить при встрече определенного тега в коде страницы. Так же в lwipopts есть настройка LWIP_HTTPD_SSI_INCLUDE_TAG
, которая определяет, нужно ли вставлять ли в код страницы необработанный тег.
CGI дает возможность отправлять со стороны клиента на сервер информацию через url в виде пары param=value. На странице размещается форма с сабмитом, а в коде МК в обработчике выполняется действие при встрече данной пары.
/* Check cgi parameter : example GET /leds.cgi?led=1 */
for (i = 0; i < iNumParams; i++)
{
/* check parameter "led" */
if (strcmp(pcParam[i], "led") == 0)
{
/* switch led1 ON if 1 */
if (strcmp(pcValue[i], "1") == 0)
GPIO_ResetBits(GPIOC, PIN_LED);
}
}
Обработчики и теги упаковываются в структуры и скармливаются LwIP через http_set_ssi_handler()
и http_set_cgi_handlers()
.
FS data
Уже к этому моменту память на моем BluePill начала трещать по швам, приходилось делать преждевременную оптимизацию, чтобы проект хотя бы собирался.
Прожорливость кода
Для уменьшения отпечатка можно бы переписать USB стек (как это делают народные умельцы), или использовать другой TCP/IP стек, или даже включить -Os/-O3 оптимизацию.
В проекте на камне побольше у меня прикручена fatfs, а для LwIP стоит опция LWIP_HTTPD_CUSTOM_FILES
с реализацией доступа к файлам через fs_open_custom()
, что позволяет изменять html файлы без перепрошивки самого МК. В нашем же проекте вместо растровой картинки, я нарисовал пилюлю в svg, уложившись в 3 кБ.
Чтобы собрать «сайт» в файл fsdata_custom.c, нужно скомпилировать ту самую утилиту makefsdata из папки LwIP в своем любимом C компиляторе.
По умолчанию утилита ищет папку ./fs и упаковывает всё, что найдет внутри. Либо можно указать ей путь к файлам самостоятельно. Подробнее можно посмотреть вывод --help.
Полученный файл fsdata.c, переименовываем в fsdata_custom.c (потому что в lwipopts мы указали HTTPD_USE_CUSTOM_FSDATA
, что в свою очередь приводит в недрах к #define HTTPD_FSDATA_FILE "fsdata_custom.c"
), кладем его в папку device и исключаем из билда. LwIP (в apps/httpd/fs.c) включит этот файла на этапе компиляции.
Итог
Вот и всё. Проект собираем, зашиваем, подключаем. В системе появляется новое сетевое устройство Remote NDIS Internet Sharing Device, в браузере можете вписать http://192.168.7.1 (по умолчанию), а благодаря поднятому DNS серверу, можно даже написать http://run.stm и увидеть веб-интерфейс нашей BluePill, поморгать светодиодом.
Читатель спросит: что с этим делать? Я не знаю. Вот вам еще один user interface для ваших проектов. Поздравьте своих товарищей новогодней открыткой, принесенной на платке, удивите коллег `интернетом` на флешке. У кого-то есть идеи получше?
з.ы.
Тайминг в статье немного нарушился, потому что она лежала в ящике после написания еще почти год. За это время действительно многое произошло как в личном плане, так и в глобальном, как в технологическом, так и в общественном.
Теперь действительно в TinyUSB есть реализация этого же RNDIS, причем используется код @fse. Я сам поднаторел достаточно, чтобы с полузакрытыми глазами смотреть на данный проект. Я бы поменял некоторые вещи, но статья не об этом.
Статья о том, какой минимальный набор инструментов хотелось бы дать в руки каждому. Чтобы новички не боялись пугающих терминов и легко вливались в программирование.