Обмен данными между двумя Teensy 4.0 и ПК: танцы с бубном

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Я решил написать эту заметку в надежде, что полученный мной опыт и найденные решения могут оказаться полезными тем, кто столкнется с аналогичными проблемами в проектах, где предполагается обмен данными между несколькими Teensy и ПК.

Началось это в конце марта. Мой товарищ (чистый электронщик и ни разу не программист) попросил помочь ему с программированием установки, которую он хотел запустить. Внешне все выглядело достаточно просто: есть 2 быстродействующих АЦП (AD5621) плюс кое-какая дополнительная периферия, которой надо управлять. АЦП надо опрашивать как можно чаще, желательно – с их предельной частотой (1МГц), какую-то часть данных сохранить в неизменном виде, а остальные – обработать. И все данные, после завершения измерений и обработки, передать в ПК. Учитывая требование «как можно чаще» я предложил использовать 2 штуки Teensy 4.0 – у них превосходные параметры (600 Мгц тактовой частоты, куча памяти, хорошая обвязка и все это – считай, в микроскопическом форм-факторе). Ну и в дополнение – возможность программировать в расширенной версии Arduino IDE (что-то более сложное для подобной задачи – это уже перебор).

Написание кода и отладка делались удаленно – сначала с использованием TeamViewer, а потом, когда он начал прерывать сессию через 60 секунд, мы перешли на Google Remote Desktop. Физически возле установки я никогда не появлялся и не знаю, как она выглядит и в целом работает.

За пару месяцев до этого я сделал свой проект на Teensy – 8-канальный измеритель биопотенциалов на ADS1298, и остался весьма доволен этой «крошкой» (так в вольном переводе звучит название контроллера). Поэтому я и порекомендовал своему приятелю использовать пару Teensy 4.0, с чем он согласился и был весьма доволен.

Кроме того, один Тинси должен быть ведущим (Master, СОМ3), а другой – ведомым (Slave, СОМ5), т.е. ведущий управляется непосредственно от ПК, а ведомый (как минимум, частично) – по UART от ведущего. При этом первоначально предполагалось, что оба контроллера будут подключены к ПК и передавать в него накопленные данные по USB.

Изначально видение проблемы выглядело примерно так: отлаживаем функционал на ведущем Тинси, затем переносим софт на ведомый, корректируем в соответствии с его функционалом, и убеждаемся, что все работает. Профит.

Первоначально я предполагал, что на все про все будет вполне достаточно 2-2.5 недель. Одной недели будет явно мало, а целых ТРИ недели… Ну что там делать так долго?? Ведь все подводные камни должны повылазить при отладке софта для ведущего, а дальше – надо лишь внимательно просмотреть логику работы ведомого.

Ок, сказано – сделано.

Вначале все шло довольно бодро. Ведущий Тинси собирал данные (с интервалом чуть менее, чем в 2 мкс, быстрее не получилось) и отправлял их в ПК. Пашу (так я буду условно называть своего товарища) это вполне устраивало. Следующий этап – проведение последовательно 100 циклов измерений, обработка каждого цикла, сохранение в памяти Тинси результатов обработки для каждого цикла.

А вот при передаче полученных данных возникла первая проблема. При использовании одной и той же передающей функции, первый массив данных («необработанный») обрезался самым жестоким и беспощадным образом, тогда как второй массив (с такой же самой структурой данных и примерно такой же длины) – передавался без проблем. Т.е. передача первого массива могла прерваться на 1048-м, 2036-м или 4076-м байте (при длине массива в 28800 байт), после чего Тинси считал свою задачу полностью выполненной.

Так как в данном проекте основная задача – не красота и элегантность кода, а его максимальная надежность, и максимальная скорость движения вперед, я принял решение передавать первый массив по пакетам. Размер пакета был выбран равным 512 байт. Тинси передает в ПК общий размер файла, ПК вычисляет, сколько всего пакетов следует передать, и запрашивает у Тинси очередной пакет, указывая его смещение и длину. Длину следует указывать, т.к. последный пакет обычно имеет размер менее 512 байт.

К слову, в конце концов и второй массив перестал целиком передаваться, и я тоже перевел его на передачу пакетами.

Окей, эту неприятность мы пережили. Ведущий Тинси стал без проблем передавать в ПК все необходимые данные. Теперь настала очередь ведомого.

Естественно, его тоже подключили к ПК, и я прописал в программе для ПК (она была написана на WPF) работу с двумя СОМ-портами: СОМ3 – ведущий Тинси, СОМ5—ведомый. Отлично. Включаем установку, и… я вижу в обработчике принимаемых данных СОМ5 команду, которая ДОЛЖНА быть передана в СОМ3! Естественно, поскольку эта команде НЕ поступает на СОМ3, все летит в тартарары. Это как это так, товарищи??

Следуя нашему главному принципу («вперед и быстро»), было принято решение вообще отказаться от прямого общения между ведомым Тинси (СОМ5) и ПК, и перевести все управление СОМ5 и передачу данных от него исключительно через UART, используя ведущий Тинси (СОМ3) в качестве передаточной станции. В результате восстановилась нормальная работа СОМ3. Уже легче.

Правда, после этого мне пришлось потратить несколько дней на то, чтобы внимательно прописать логику взаимодействия ПК, СОМ3 и СОМ5. Учитывая, что происходящее в недрах Тинси остается для программиста черным ящиком (если не использовать покупное ПО для отладки под Ардуино, например Visual Micro), единственное, по чему я могу судить о его более-менее нормальной работе – это данные, которые он выдает в ПК.

Поэтому вся работа ведущего Тинси была целиком и полностью подчинена командам ПК: ПК выдает «подготовительную» команду, Тинси подтверждает прием команды, ПК выдает команду на исполнение. Своеобразный хэндшейк. Ответы Тинси передаются в бинарном или текстовом виде (в зависимости от режима работы), например: «6 28800», где 6 – код команды (хочу передать первый массив данных), 28800 – объем передаваемых данных, в байтах. Аналогичный хэндшейк был использован для взаимодействия между обоими Тинси.

Такой подход к взаимодействию позволяет вести лог происходящего, по которому можно, теоретически, выявить место, где возникает проблема. Если требуется перейти к передаче бинарных данных (пакетная передача результатов измерений), то в этом случае данные в лог не пишутся.

Еще одним преимуществом хэндшейка является то, что он позволяет избавиться от «слипания» команд: из-за того, что Тинси передает данные в ПК с высокой скоростью (передача данных по USB идет со скоростью 12 Мбит/с), он успевает передать следующую команду так, что она становится в конец приемного буфера ПК, например: «6 28800 5»), где 5 – это команда следующей операции. Задержки при передаче данных не помогали. Тут надо или делать парсинг принятой строки (с рассмотрением всех возможных вариантов), или использовать хэндшейк, что позволяет работать без головной боли.

Возвращаемся к нашим баранам. Подключаем СОМ3 и СОМ5, запускаем программу. СОМ3 отрабатывает без проблем. А вот СОМ5… Вместо ожидаемого (условно) «6 28800» — имеем «6 1» или «6 8400» или вообще «6 0». То есть СОМ5 принимает от СОМ3 управляющую команду и реагирует на нее, но данные, которые он при этом посылает, находятся в глубоком сферическом вакууме. Так кто же виноват?

Первое, что приходит в голову – где-то в UART проблемы. Просто грешить больше не на кого. Отлично. Делаем тестовую программу: СОМ5 генерирует 100 чисел (от 0 до 99) и посылает их в СОМ3, а тот – напрямую пересылает принятые данные в ПК. Так и есть: данные передаются с большими пропусками. Даже при снижении скорости передачи с 500 кбод до 9600 бод. WTF???

Еще пару дней потерял в попытках победить эту фигню методом научного тыка. Не помогло.

Но тут, как говорится, «не было бы счастья, да несчастье помогло.»

С самого начала работы с двумя Тинси мы заметили, что программировать их получается только по одному: если при подключении только СОМ3 (или только СОМ5) в них можно заливать программу, используя кнопки Arduino IDE на экране, то, если оба Тинси подключены к ПК, их более-менее удачно можно программировать только физически нажимая на кнопку перезапуска на плате Тинси. Т.е. скомпилировал софт для СОМ3 – нажал на кнопку, ждешь, пока появится заветное Reboot Ok. После этого скомпилировал софт для СОМ5, нажал на его кнопку, ждешь Reboot Ok.

Так вот, и этот механизм в какой-то момент поломался. Т.е. СОМ5 при попытке перепрограммирования тут же переставал определяться как устройство Тинси. Независимо от того, сколько раз мы его отключали, дожидались, пока это устройство исчезнет из списка устройств и подключали, дожидаясь пока оно появится в списке. Даже более того, эта болезнь распространилась и на СОМ3!

Тут меня начали терзать смутные сомнения. А не является ли источником проблем сам факт одновременного подключения СОМ3 и СОМ5 к одному и тому же ПК?

Для проверки этой гипотезы я уговорил Пашу запитать СОМ5 от стороннего PowerBank (старого ноутбука). И – о чудо! – UART начал передавать данные без потерь и на любой скорости (от 9600 до 500к). После этого Паша запитал СОМ5 от источника +5В, который имеется в установке, поставив для защиты последовательно диод, как рекомендуется в руководстве по Тинси.

Единственное но: первоначально в тестах СОМ5 не хотел передавать более 64 байт (1 байт команды + 63 байта данных), после 63 байта данных счетчик передаваемых данных сбрасывался в 0. Проблема была решена путем расширения буферов передачи/приема до 9000 байт:

Serial1.addMemoryForRead(buffer, 9000);
Serial1.addMemoryForWrite(buffer, 9000);

Т.о. в рабочем режиме СОМ5 питается от своего собственного источника, а при программировании – подключается к ПК через USB (при отключенном СОМ3). К сожалению, это не решило проблему с передачей результатов измерений от СОМ5, но теперь я точно знал, что UART тут не при чем, и искать надо в моем собственном коде. После этого победа разума над сансапариллой воспринималась лишь как вопрос времени.

Шел 28-й день с начала работы над проектом… А потом еще месяц был потерян потому, что я заболел COVID-19… Но, в конце концов, работа над проектом возобновилась. И тут возникла еще одна проблема, связанная с UART.

Передача данных — как однобайтовых команд, так и 3-5 байтовых массивов (команда + параметры) из СОМ5 в СОМ3 проходила без проблем – я видел на ПК передаваемые данные, и там не было ошибок. Но вот от СОМ3 в СОМ5 аналогичный массив не проходил – только первый байт. Причем функция, осуществляющая прием данных, в обоих контроллерах была одной и той же. Перестановка контроллеров проблему не решала. Так как массив, передаваемый из СОМ3 в СОМ5, содержал информацию о смещении начала чтения данных и об объеме данных, затребованных ПК, в ответ я, естественно, ничего не получал. Добавление задержек в подпрограмму чтения буфера UART проблему не решало.

Мы уже хотели задействовать еще и I2C для передачи накопленных данных из ведомого Тинси в ведущий, благо свободных ног вполне хватало, но и тут был облом – на выходе, где должен был быть тактовый синхросигнал, присутствовал высокий потенциал, и никаких импульсов. Шаря по Сети в поисках решения проблемы, я наткнулся на каком-то форуме на упоминание, что в Ардуино одновременная работа UART и I2C невозможна. Правда, это относилось к другому контроллеру, но, возможно, и к нам тоже.

Совсем отказаться от UART я не мог – мне было необходимо, чтобы начало измерений в обоих контроллерах было строго привязано к синхросигналу (отрицательному фронту одного и того же синхроимпульса), идущему с частотой в несколько кГц, и гарантировать это, используя I2C, я не мог. UART же, работая на частоте 500 кГц, с этой проблемой вполне справлялся.

Поэтому я отказался от идеи использовать I2C, и вернулся к «чистому» UART. Была еще мысль задействовать дополнительный канал SPI, но тут воспротивился Паша — необходимость лезть с паяльником в плату Тинси ему никак не нравилась: в Тинси на разъемы выведен только SPI0, а выводы SPI1 и SPI2 – это площадки на нижней стороне платы.

Идея поставить промежуточный буфер и использовать одну и ту же шину SPI0 для работы СОМ5 в режиме Master при опросе АЦП и в режиме Slave при передаче массивов данных в СОМ3 также встретила решительное сопротивление – Паша панически боялся, что где-то в программе произойдет сбой, выводы контроллеров перейдут в неправильное состояние, и оба контроллера поубивают друг друга. Так как это был все-таки Пашин проект, сильно пробивать это решение я не стал, хотя, учитывая четкость работы по SPI с периферией, я предполагал, что при синхронной передаче данных все должно работать как часы.

Таким образом, мне не оставалось ничего другого, как упереться изо всех сил и толкать эту UART-телегу, пока она не доедет, куда надо.

После пары дней, проведенных в попытке найти ответ на вопрос «какого черта?!» я написал очередной микро-тест, и вдруг – о чудо! – увидел пакет данных, переданный без искажений! Беглый анализ показал, что в «рабочем» режиме данные из приемного буфера UART считывались только один раз – в начале функции, реализующей обработку команд. Она обращалась к подпрограмме чтения из буфера, и рассматривала первый полученный байт как команду, а последующие – как ее параметры или данные. При этом, как показывала «обратная» выдача на ПК (массив байтов, принятый ведомым Тинси и переданный обратно в ведущий, а от него – на ПК), ведомый Тинси «видел» только первый байт, как было сказано выше.

Примерно такая же беда поджидала и при передаче большого тестового массива данных из СОМ5 в СОМ3 – при тестировании команда принималась и корректно обрабатывалась, но вместо данных я видел одни нули. Однако в новом тесте я «на автомате» поставил внутри теста еще одно обращение к функции считывания из входного буфера UART. И данные появились!

После вставки дополнительного чтения в код для ведомого Тинси проблема «исчезновения» данных ушла. Правда, по ходу дела выяснилось, что данные лучше всего передавать только малыми пакетами (до 64 байт, несмотря на «расширение» буферов чтения/записи в обоих Тинси), ну и задержки пришлось вставить, но все это Пашу устраивало.

В конце концов, после дополнительного дваркования логики влендишным способом (читай — отладки и дебагинга), все заработало, как положено.

The End.
Источник: https://habr.com/ru/post/564088/


Интересные статьи

Интересные статьи

«Корутины - это легковесные потоки», сколько раз вы слышали эту формулировку? Она что-нибудь вам говорит? Скорее всего не очень много. Если вы хотите узнать больше о том,...
История накопителей представляет собой гонку между носителями и вычислительными мощностями. На пути к компьютерной нирване встаёт узкое место – хранение миллиардов нулей и единиц. Сам...
Статья о том, как упорядочить найм1. Информируем о вакансии2. Ведём до найма3. Автоматизируем скучное4. Оформляем и выводим на работу5. Отчитываемся по итогам6. Помогаем с адаптацией...
Вы наверняка знаете, что мир процессоров разбит на два лагеря. Если вы смотрите это видео со смартфона, то для вас работает процессор на архитектуре ARM, а если с ноутбука, для вас тр...
Карантин запирает нас в четырех стенах, но с другой стороны — стирает расстояния. Например, в эту субботу можно будет оказаться на митапе минского сообщества: ребята решили использовать возможнос...