Своё устройство на микроконтроллере я начал "пилить" еще в начале 2019-го. Тогда я даже не думал, что захочу связать его с компьютером, но устройство постоянно эволюционирует, и вот настала пора. Причём нужно не просто связать, а написать своё фирменное ПО, которое будет управлять устройством через USB. С подобной задачей я столкнулся впервые, а беглый поиск в сети выдал такие результаты, после которых появилось ещё больше вопросов и каша в голове. Про свой опыт решения данной задачи "От" и "До" делюсь в этой статье. О моём устройстве расскажу немного позже, сохраним интригу. Предвидя вопрос типа "В чём смысл этого топика?", отвечу, что простого и законченного решения в сети я так и не нашёл. Под простым и законченным решением я подразумеваю дуэт микроконтроллера (далее МК) с компьютером (далее ПК) и заготовки кода с функцией автоматического коннекта, а также мега-функцией "поморгать светодиодом". Пришлось повозиться и собрать все элементы мозайки в единую картину, которую я решил оформить в статью. Надеюсь, она поможет новичкам в вопросе "Как связать микроконтроллер с компьютером через USB" без лишних заумных слов. Уточню, что программированием занимаюсь для удовольствия, поэтому не претендую на образцово-показательный код. Это всего-лишь мой личный опыт, и вам судить, дорогой читатель, насколько он полезен. Обеспечить двусторонний обмен данными между МК и ПК через USB. Никаких плат и софта типа Arduino в готовом решении мы не используем. Сделать так, чтобы ПО компьютера автоматически определяло наше устройство при подключении его к любому USB-разъему. USB-устройство должно отображаться в диспетчере устройств под нашим собственным именем (например, под именем нашей компании). Примечание. Последний пункт я не стал раскрывать в рамках этой статьи, потому что бесконечную простыню текста и картинок читать утомительно. Оставлю материал для будущей статьи, если тема окажется интересной. Выполнив это, мы получим огромную власть над нашими устройствами. Однако, есть ограничение - скорость передачи данных будет небольшой. Но отправка несложных команд, текста, или легких файлов будет работать быстро. Знать C++ хотя бы на начальном уровне. Уметь работать с любым МК (главное, чтобы у него был интерфейс UART). Собственно сам МК (я взял Atmega328, т. к. она используется в моём проекте). Макетная плата и несколько простых радиодеталей для сборки тестовой схемы. Плата USB/UART преобразователя (о ней скажу ниже). Как вы понимаете, информации предстоит много, и здесь не будет объяснения того, как прошивать МК или какой компилятор выбрать. Скажу только, что для прошивки я использовал Arduino IDE, а для ПО компьютера - Visual Studio C++ 12. Проект, для которого я делаю канал связи с ПК, называется "Teslafon". Это название музыкальных катушек тесла с микроконтроллерным управлением (описание их работы тут). Пока катушка является автономным устройством и ни к чему не подключается, но недавно мне пришла идея сделать управление молниями с ноутбука. Несмотря на то, что тема специфическая, в сети можно найти примеры даже на неё, но там всё довольно индивидуально и я решил пойти своим путём. Так как слона лучше есть по частям, я раздробил задачу на несколько подзадач, первая из которых - тема данной статьи. Но не подумайте, что я хочу подключить катушку тесла напрямую к ноутбуку (мой ноутбук мне дорог). Катушка тесла это генератор адских помех и наводок, а значит между ней и ноутбуком должно быть не только существенное расстояние, но и промежуточное устройство-изолятор на несколько мегавольт... Его-то я и хочу подружить с ПК. Вдаваться в подробности катушек тесла не буду, это уже другая история. Просто скажу, что мы сделаем универсальную заготовку USB-устройства, которую вы сможете использовать в своём проекте, а я в своём. Сперва я думал, что всё будет проще простого. Возьму библиотеки типа V-USB и libusb, подключу МК напрямую к USB-разъему, быстренько найду готовый пример кода и будет мне счастье. Но "быстренько" не получилось. Оказывается, там много заморочек - например, дополнительная библиотека кушает ресурсы МК, а моя прошивка предполагается довольно ёмкой. Мне хотелось использовать встроенные аппаратные фишки по максимуму, чтобы не занимать память. Кроме того, настройка всех этих библиотек требует опыта. Рабочая частота МК подойдет не абы какая, а та, что задал автор библиотеки. Но реальный шок был тогда, когда я узнал, что для моего устройства нужны PID (Product ID) и VID (Vendor ID), за которые сегодня надо заплатить, на секундочку, 3500$ организации usb.org. Ох, не туда меня завели поиски, не туда... Наконец до меня дошло, что к чему. Оказалось, вопрос не в том, как передать данные через USB, а как передать данные через виртуальный COM-порт! Передача данных через COM-порт намного проще, хотя это устаревший вид портов. А раз так, то реального COM-порта у нас не будет, только виртуальный. Физически же мы по-прежнему будем использовать привычный USB. Виртуальный COM-порт будет предоставлен драйвером, о котором мы поговорим ниже. А еще к устройству добавится некая микросхема-посредник, которая снимет целый ворох проблем. Взгляните на схему: Между ПО хоста и нашей прошивкой есть целых 4 звена. С аппаратным UART-ом всё понятно, что же с остальными? Здесь роль посредника будет выполнять микросхема по кличке CP2102, у которой есть также куча аналогов (CH340, PL2303, FT232RL и другие). Её основная задача - преобразование интерфейса UART в USB и обратно. Как это делается, нам не важно, это просто чёрный ящик, имеющий вход и выход. CP2102 используется в некоторых программаторах, возможно на вашем она тоже есть, так как довольно распространена. Изучать распиновку этой микросхемы тоже не будем, потому что есть готовые платки с ней же и со всей обвязкой. И это хорошо, ведь наша задача - чем проще, тем целее нервы. Пример двух платок с CP2102 от компании Silicon Labs: Купить подобную железку можно, например, здесь. Но чтобы не ждать, можете поискать в местных радиомагазинах, товар не редкий, думаю, найдёте. Далее я начал изучать, как применить этот USB/UART преобразователь. Тут тоже всё обошлось - производитель железки уже позаботился о нас и написал драйвер для контроллера USB (см. рис. 1). Этот драйвер и будет эмулировать виртуальный COM-порт. Вы спросите, а где же брать PID и VID для нашего устройства? Ответ - их предоставляет тот же Silicon Labs бесплатно. Круто, теперь можно увидеть всю последовательность действий: Покупаем USB/UART преобразователь и собираем тестовое устройство; Скачиваем и устанавливаем драйвер; Пишем ПО хоста; Пишем прошивку МК; Тестируем интерфейс. Помните, что наше устройство кроме моргания светодиодом ничего пока уметь не будет? Чтобы не было скучно, давайте сделаем три светодиода: Зелёный - будет гореть, пока связь с хостом установлена (не путать с физическим подключением к порту). Красный - загорается или гаснет по нажатию 'r' на клавиатуре. Белый - загорается или гаснет по нажатию 'w' на клавиатуре. Нажатие 'x' на клавиатуре будет разрывать сеанс передачи данных, гасить зелёный светодиод и завершать работу ПО хоста. Но я забежал немного вперед, вернёмся к схеме устройства: Думаю, тут всё понятно. Я даже не стал ставить подтягивающий резистор R1, хотя в реальной схеме он нужен. Сейчас нам главное получить минимально рабочую модель. Вот так выглядит макетка: Кстати, очень удобно использовать витую пару в качестве соединительных проводков, они дешёвые и нарезать их можно разной длины. В жгуте из 5 проводов используются только 4, зелёный провод идёт с контакта DTR преобразователя на макетку и никуда не подключается (оставлен про запас). Atmega328 в корпусе DIP-28 прошла огонь, воду и трубы, но ещё жива - когда-то она была подопытным кролем в создании первой музыкальной катушки тесла. Кварцевый генератор я не ставлю, а использую встроенный на 8 МГц. Для удобства я сделал преобразователь съёмным и слегка подогнул вверх вывод 3.3V, чтобы избежать неправильного подключения. Примечание. Если у вас другой МК, подключите все пины в соответствии с его даташитом и схемой на рисунке 3. Наш USB/UART преобразователь уже вставлен в ПК, открываем диспетчер устройств и видим: Мы могли бы сделать своё красивое имя устройству, но я решил, что это будет тема другой статьи, т. к. материала получается слишком много. Пока ограничимся тем, что есть. Качаем готовый драйвер, устанавливаем и переходим к программированию. Итак, схема имеет три светодиода: зелёный, красный и белый. Логика работы будет такая: запускаем программу на ПК, если девайс не подключён, то программа будет ждать, пока мы его не подключим. При подключении автоматически будет осуществлён коннект с устройством и загорится зелёный светодиод. Клавиши "w" и "r" будут управлять белым и красным светодиодом соответственно. При вводе "x" произойдёт разрыв связи с нашим устройством и зелёный светодиод погаснет. Как видите, все очень просто. Сперва я написал программу в процедурном стиле, но если захочется работать сразу с двумя и более устройствами, то такой подход не годится. К тому же, по мере роста программа станет нечитабельной. И тогда я переписал код в стиль ООП. Прошивку МК наоборот, оставим в процедурном стиле, она будет маленькая и простая, оборачивать ее в классы не вижу смысла. Создадим класс устройства, но на самом деле это будет скорее класс COM-порта, т. к. в нём будут его настройки, функции подключения/отключения и передачи данных. Начало файла tesladevice.h: Постараюсь максимально комментировать прямо в коде, но некоторые моменты буду выносить отдельно. Так как изначально номер порта мы не знаем, подготовим строку portName из 7 двухбайтовых символов типа wchar_t, где будем перебирать имена в формате "COMxxx". В Windows по умолчанию доступно 256 виртуальных ком-портов, и мы будем перебирать все, пока не найдем своё устройство. Структура dcbParams включает основную массу настроек порта (разбирать её не будем, если интересно, можете загуглить сами). Структура ctParams будет содержать настройки таймаутов, её описание тоже опустим. Далее объявим публичные и приватные функции: Разберём каждую из них подробно. Функция Connect() самая большая, она делает всю черновую работу по подключению к устройству. Работает она так: запускается цикл перебора всех 256 возможных портов и пробует подключиться к каждому. Если подключение удалось, устройству посылается приветствие, затем в течение заданного таймаута ожидается приветствие в ответ. Если оно не поступило, значит устройство не наше и связь с ним прекращается. Если приветствие поступило, значит устройство распознало своего "хозяина" и приготовилось к работе (при этом загорается зелёный светодиод). Функция Connect(), видя знакомый ответ, завершается и отдает истину в основную программу. Происходит всё это быстро, так как обычно львиная доля портов из 256 возможных просто не существует. Такой способ идентификации устройства мне кажется надёжным. В коде я дал исчерпывающие пояснения, останавливаться на них не буду. Единственный вопрос, который может возникнуть - как отреагируют другие устройства (если они подключены) к вмешательству со стороны нашей программы? Скорее всего, никак. Я подключал одновременно несколько разных устройств, никаких проблем не было. Теперь разберём приватную функцию helloFrom(). Чтобы на 100% отличить наше устройство от чужого, будем принимать от него кодовое слово, которое другим устройствам неизвестно. Я взял слово "teslafon", вы можете придумать своё. Вот как это выглядит в коде: Здесь функция ReadFile() ждёт ответа от устройства в пределах заданных таймаутов, хранящихся в структуре ctParams. Если ответа нет или он не совпадает с ожиданиями, helloFrom() выдаёт false. Далее напишем SendData(), которая вступает в работу после подключения к устройству: Для простоты мы отправляем один байт, но если надо отправить более сложные данные, можно несколько раз вызвать SendData() в цикле. Следующая функция GetPortNum() просто возвращает номер порта, к которому мы подключились. Конечно, можно было сделать переменную portNum публичной, но правила хорошего тона велят закрывать данные класса от всеобщего доступа: И последняя функция нашего класса просто завершает работу с портом, возвращая результат операции: Я решил сэкономить на файле tesladevice.cpp и все тела функций разместил в tesladevice.h. Вы можете разделить их или оставить как есть. Теперь напишем основное тело нашего хоста. Так как это тестовый проект, я сделал его в консольной программе: На этом работа с хостом закончена. Это всего лишь заготовка, но вполне рабочая, из которой можно слепить что душе угодно. Осталось написать прошивку для USB-устройства и переходить к долгожданным тестам. Как уже говорилось выше, моё устройство будет работать на Atmega328, а значит и код я буду строчить для неё. Эксперимент проводился на частоте встроенного генератора 8 МГц, с другими частотами я не работал. В коде используются регистры МК напрямую и глобальные переменные, и вообще попахивает хаосом, но повторюсь - это пример, набросок, может быть не самый лучший, но рабочий. Если у вас другой камень, вам всё равно придется изменить инициализирующую часть кода, так что можете его оформить как вам нравится. Вот мой полный скетч для Arduino IDE: Ну вот, проделана большая работа. Теперь всё компилируем, прошиваем устройство, втыкаем его в ПК и с замиранием сердца запускаем программу... Если всё сделано правильно, программа выведет это: Дальше вводим разные буквы и жмём Enter. Результат работы я продемонстрирую на видео: Если у вас всё это добро не запускается или запускается криво, придется попыхтеть над отладкой. По крайней мере, я проверял эту систему на трёх разных компьютерах и на двух Windows (7 и 10), и результатами остался доволен. Желаю и вам удачного запуска. Статья получилась объёмной, но некоторые детали так и остались не раскрыты. Однако, поставленные задачи выполнены. Только представьте, какие штуки теперь можно делать, имея такой инструмент. Например, можно сделать пульт ДУ для ПК, или обработку разных датчиков умного дома. Можно придумать и более нестандартные и опасные штуки, как в моём случае - управление катушкой тесла. И даже если кто-нибудь скажет, что всё уже давно придумано, я соглашусь. Наверно, есть и более изящные решения задачи, но мне интересно (а порой и необходимо) разобраться во всём самому. Ведь именно так рождается новое. Надеюсь, что дорогой читатель меня поймёт. Удачных всем разработок!