Как стать автором
Обновить

Жгём-шьём контроллеры и кормим ядерную подсветку

Уровень сложностиПростой
Время на прочтение37 мин
Количество просмотров11K
Так оно светится
Так оно светится
А так оно устроено
А так оно устроено

Схема: SVG тут хайрес PNG тут (высокое разрешение).

В предыдущей части я рассказал о том, как крепил ленты к трём теликам и огибал их геометрию используя, в числе прочего, 3D‑печать из алюминия.

В этой речь пойдет про электрику: питание лент, контроллер управления и разработку его прошивки с нуля, потому что стандартные библиотеки для лент здесь не сработали.

Дисклеймер №1: я не претендую на экспертизу, а просто делюсь опытом. Это мой первый железячно‑контроллеровый проект («мигание светодиодом на Ардуино»), который я делал в конце 2021 — начале 2022 г, до него я занимался только софтом. К некоторым решениям я пришёл эмпирически, иногда не до конца понимая происходящих процессов.

Дисклеймер №2: некоторые штуки, которые я тут делаю, могут быть опасны для жизни. Не разбираетесь в электричестве — не повторяйте это. Мне просто было скучно, и я делал всё на свой страх и риск.

Список всех частей

Система питания

Итак, мы собрали рамы и обильно обклеили их лентами. Настало время накормить их.

Подсветка кушает до 90 ампер под напряжением 5 вольт. Пиковая мощность всех лент — 450 ватт. Все эти десятки ампер надо раскидать по четырехметровой конструкции, да ещё и под весьма низким напряжением. Где бы ни находился источник питания — току ползти далеко, и падение напряжения на проводах передаёт привет. При больших токах можно легко потерять на проводе пару‑тройку вольт. Но одно дело потерять 3 вольта из 220, и совсем другое — 3 из 5. Лентам это определённо не понравится.

Можно решить проблему падения напряжения, увеличив толщину проводов и, тем самым, снизив сопротивление. Но если разместить блок питания на эти самые 90А на стене, то к рамам боковых теликов — а они шевелятся — придётся тянуть толстую медную колбасу. И тянуть её надо будет так, чтобы не зажевать в механизмах. Не круто.

.

У меня была мысль для эксперимента передавать энергию напрямую через петли и кронштейны. Они толстые, и падение напряжения явно будет невелико. Но для этого надо изолировать верхнюю и нижнюю части кронштейнов + обязательно всплывёт какая‑нибудь кристаллизация смазки в подшипниках под воздействием тока, или что‑то в таком духе — какая‑нибудь неочевидная электрохимическая дичь.

Верхняя половина кронштейна - это +5 вольт, нижняя половина - общий провод
Верхняя половина кронштейна - это +5 вольт, нижняя половина - общий провод

Оригинальный хайрес тут (высокое разрешение).

Я решил поступить проще: разбить нагрузку. Пусть за каждым экраном стоит свой блок питания и обеспечивает энергией только свои ленты. Мощность таким блокам понадобится уже не очень большая. Тем не менее, блоки выбраны всё‑таки с запасом — каждый по 200 Вт, чтобы сильно не грелись, и чтобы даже в пиковой нагрузке оставался резерв в 20 ампер. Область для меня новая, мало ли что пойдёт не так.

Блоки питания и провода

200 ватт — это самые мощные БП без активного охлаждения, которые мне удалось найти. Дальше уже начинаются вентиляторы и все сопутствующие вентиляторные проблемы, в первую очередь — шум и пыль.

Всего блоков питания я взял четыре штуки: три стоят на телевизорах, один запасной. Подключены они цепочкой к проводу 220В, идущему из распредкоробки справа на стене.

Размещение блоков питания и подвод 220 вольт к ним
Размещение блоков питания и подвод 220 вольт к ним

Из каждого БП идут пятивольтовые ветки проводов, раздающие энергию лентам соответствующего телевизора.

Слева направо: чёрный провод питания привода (отдельно распишу ниже), три контакта выходных +5В, три контакта выходного 0, заземление, два входящих 220В
Слева направо: чёрный провод питания привода (отдельно распишу ниже), три контакта выходных +5В, три контакта выходного 0, заземление, два входящих 220В
По три сегмента на боковых теликах + два на центральном
По три сегмента на боковых теликах + два на центральном

Ветки сделаны из многожильного кабеля сечением 2,5 мм² с прозрачной изоляцией, который продавцы очень‑очень любят называть акустическим.

Лудится оно отлично, но изоляция быстро начинает плавиться
Лудится оно отлично, но изоляция быстро начинает плавиться

Если учесть, что каждый сегмент лент жрёт не более 12 ампер, а средняя длина всех восьми веток около метра, получаем, что на кабелях падает меньше 200 мВ — вполне ок.

У боковых теликов таких веток по три: на нижний сегмент, на верхний и на боковой с прилегающими скосами, центральный БП питает только два сегмента: верхний и нижний.

Не убитые Хабром хайресы (высокое разрешение): все ветки, ветки центра, ветки боковушки.

Центральный блок питания никак не закреплён — он просто валяется прямо на телике. При необходимости его можно достать и повесить на центральный экран, только аккуратно.

Металлолом снизу слегка приподнимает БП над вентиляционными отверстиями в ТВ, чтобы он их не закрывал
Металлолом снизу слегка приподнимает БП над вентиляционными отверстиями в ТВ, чтобы он их не закрывал

С боковыми БП всё уже серьёзнее: телики двигаются, поэтому блоки питания прикреплены к кронштейнам. На левом телике БП прикручен к кронштейну через металлическую пластину — здесь кронштейн пришлось сверлить насквозь.

Правый таки удалось нормально закрепить, нарезав в кронштейне резьбу. Кстати, чёрный провод, выходящий из блоков питания слева — это питание сервопривода, шевелящего теликом. Привод жрёт не 5, а 12 вольт, и у него имеется свой маленький БП.

Правый БП получилось закрепить без пластины
Правый БП получилось закрепить без пластины
Внутри БП подсветки на 5В прячется маленький БП привода на 12В
Внутри БП подсветки на 5В прячется маленький БП привода на 12В

Чтобы сократить число проводов, тянущихся к телевизорам, я вытащил эти мелкие БП механики из родных корпусов и подселил внутрь БП подсветки — место там есть. Так сделано и на левом, и на правом экранах.

Поскольку всё это должно быть единой системой, управляемой одним контроллером, блоки питания следует занулить — соединить между собой выходные нули всех трёх БП, чтобы уравнять их потенциалы. А то они сделают это через ленты об контроллер, жизненный путь которого на этом закончится.

Сначала, ввиду отсутствия опыта, я занулил блоки тоненьким МГТФ‑проводком сечением 0,2 мм² — ведь он хорошо будет гнуться вместе с боковыми экранами.

МГТФ здесь участвует не только в питании, но и в передаче управляющих сигналов
МГТФ здесь участвует не только в питании, но и в передаче управляющих сигналов

Экспериментальный пуск состоялся, оно проработало некоторое время, однако, довольно быстро я понёс первые потери.

Моё обучение работе с контроллерами было не лишено жертв
Моё обучение работе с контроллерами было не лишено жертв

Я предположил, что дело как раз в тонком МГТФ: ведь токи тут большие, я везде юзаю толстые провода. Провод зануления — единственный, который имеет крошечное сечение.

Вот например, слева от меня тёмная скала, а справа — светлое небо. Но тут я увидел чувака на истребителе и развернулся на 180°. Пару раз.

Большая нагрузка интенсивно перекидывается с одного БП подсветки на другой
Большая нагрузка интенсивно перекидывается с одного БП подсветки на другой

Левый БП жрал свои 20 ампер, а правый — почти ноль. И за долю секунды всё стало наоборот: правый жрёт 20 ампер, а левый — почти ничего. И такое происходит постоянно. Наверное, потенциал блоков питания просто не успевает уравняться.

Исходя из этого предположения, я заменил МГТФ на необъятную синюю сварочную сосиску сечением 25 мм². Тесты подтвердили гипотезу — новый контроллер, вроде как, не умирал. По крайней мере, так быстро. Синей сосисочке пришлось гнуться вместе с теликами, но чувствует она себя вполне неплохо.

Система питания подсветки гнётся вместе с теликами
Система питания подсветки гнётся вместе с теликами

Статичный хайрес

Таким образом, блоки питания, низковольтные провода и сами ленты неподвижны относительно друг друга. Гнутся только синяя сосиска и входящие 220 вольт.

Провода прокладываются в специально предусмотренных кабель-каналах в рамах лент
Провода прокладываются в специально предусмотренных кабель-каналах в рамах лент

Низковольтные провода‑ветки идут от БП к лентам не по прямой, а по кабель‑каналам в рамах лент, чтобы телевизоры не размахивали этими ветками в процессе движения. А чтобы провода не сбежали из этих рам, они зафиксированы специально замоделенными и распечатанными нанотехнологиями, для крепления которых в рамах предусмотрено около 50 пар отверстий с резьбой М2. Эти отверстия есть даже во внутренних сегментах, где нет лент — просто на всякий случай.

В отличие от соединений сегментов рам между собой, здесь применяются не стальные, а латунные винты
В отличие от соединений сегментов рам между собой, здесь применяются не стальные, а латунные винты

Всего было задействовано 26 нанотехнологий — по 11 в боковых теликах и 4 в центральном. Они удерживают не только «акустические» провода питания, но и коаксиальные, по которым передаются сигналы управления на ленты.

Хайрес

На концах пятивольтовых веток сидят кисточки тех самых проводов МГТФ с тефлоновой изоляцией, передающие энергию от веток непосредственно лентам. На фоне наших токов сечение в 0,2 мм² выглядит несерьёзно, но это вынужденная мера.

МГТФ-кисточки разносят питание от толстой ветки до лент
МГТФ-кисточки разносят питание от толстой ветки до лент

Поскольку кисточки короче 10 см, падает на них, по расчётам, не более 150 мВ. Таким образом, ветка вместе с кисточкой откусывают не более 6% напряжения.

Используемый МГТФ под микроскопом
Используемый МГТФ под микроскопом

Кисточковые МГТФ‑проводки проходят через отверстия в рамах и разносят питание непосредственно лентам. Паять и монтировать всё это дело — занятие довольно муторное.

Каждому сегменту - своя кисточка. Восемь сегментов - восемь кисточек
Каждому сегменту - своя кисточка. Восемь сегментов - восемь кисточек

Чтобы минимизировать сопротивление, соединение кисточки с проводом из БП я сначала крепко связывал проволокой, а затем заливал кубометром припоя.

В ходе монтажа проводов оказалось настолько много, что в сегментах пришлось сверлить дополнительное отверстие — в одно всё просто не пролезало.

На всякий случай дополнительно заземлил боковые кронштейны — мало ли что.

Заземление правого кронштейна. Левый заземлён аналогично, а центральный - никак, ему и незачем
Заземление правого кронштейна. Левый заземлён аналогично, а центральный - никак, ему и незачем

Штош. Питание подано на все сегменты, БП занулены, кронштейны заселены. Параллельно я там уже контроллер сделал. Пробуем запустить.

Нашествие бубенчиков

В процессе тестовых эксплуатаций ленты часто подвисали, а иногда комп терял контроллер. В конце концов, контроллер решил отдохнуть навсегда.

Я предположил, что проблема в индуктивности проводов. При ярких всплесках энергия просто не успевает дойти до лент. Пока провода только раскукоживают своё магнитное поле, голодные ленты пытаются наконтрабандить себе амперов через управляющие провода с контроллера. А он немножечко (раз этак в 1000) от такого перегружается, и, в конце концов, умирает.

6,3В 2200 мкФ
6,3В 2200 мкФ

Ладушки — давайте дополним сегменты подсветки местными буферами энергии, которые будут питать ленты в момент всплесков, пока энергия из БП ещё не подоспела. Понадобится мешочек конденсаторов на 6,3В ёмкостью 2200 мкФ. Почему именно такие? Ну, 6,3 вольта — ближайшее напряжение к моим 5 вольтам, а 2200 мкФ — наибольшая ёмкость, при которой размер этих конденсаторов более‑менее бьётся с толщиной рам и всех подсветковых деталюшек — можно будет куда‑нибудь спрятать. О том, что при включении подсветки все эти конденсаторы начнут хором заряжаться, я тогда не подумал, но меня спасла большая мощность выбранных БП — не зря перестраховался :)

Я подселил по два электролитических бубенчика на каждый сегмент подсветки — отдельные для ближнего и дальнего света. Всего на это потребовалось 16 штук. Блоки питания мощные — при включении спокойно накачают всё это хозяйство зарядом.

Поскольку в рамах для них места не было, пришлось подселять их куда попало. Каждый бубенчик предварительно был облагорожен — ноги заменялись на провода, ибо нужна изоляция, после чего конденсатор окунался в краску — потому что нужна изоляция. Потому что переклеивать сгоревшую от КЗ ленту — то ещё удовольствие.

Провода-то в изоляции, но соединение контактов конденсатора с проводами тоже нужно заизолировать
Провода-то в изоляции, но соединение контактов конденсатора с проводами тоже нужно заизолировать

После сушки каждый конденсатор дополнительно одеваем в термоусадку — чтобы уж точно всё заизолировать.

Важно было пометить плюсовой провод заранее, а то в термоусадке не разберёшь
Важно было пометить плюсовой провод заранее, а то в термоусадке не разберёшь

Чтобы бубенчики не загораживали свет, пропихиваем их под или рядом с рамой, стараясь просунуть провода через штатные отверстия в рамах. Поскольку телики шевелятся, я старался зафиксировать новоиспечённые дополнения, чтобы они не болтались и не цеплялись за какую‑нибудь штору, и чтобы их не погрызли коты. В процессе апгрейда важно было случайно не капнуть припоем куда‑нибудь и не убить ленты, и, тем более, телевизор.

Прокачиваем блоки питания

Всё это мне показалось недостаточным, и, чтобы окончательно устранить проблему, в каждый блок питания был подселён мистер Улитка, расширяющий местный «кеш» энергии.

Вот это вот было подселено в каждый из трёх БП
Вот это вот было подселено в каждый из трёх БП
Конденсатор К73-17
Конденсатор К73-17

В улитке, помимо 6 бубенчиков, обитает маленький, но быстрый плёночный К73–17. Его ёмкость гораздо меньше (жалкие 0,47 мкФ против 2200 мкФ у бубенчика), но скорость заряда‑разряда намного выше. По задумке, если проблемы были связаны с короткими импульсами, эта штука будет глотать их лучше, чем вездесущие бубенчики.

Припой кубометрами закачивался под изоляцию, чтобы точно всё было хорошо
Припой кубометрами закачивался под изоляцию, чтобы точно всё было хорошо

Теперь надо подцепить улиток к блокам питания. Мне не хотелось это делать снаружи — итак все ленты в бубенчиках. Поэтому я сунул улиток внутрь БП. Для этого блоки питания пришлось полностью разобрать, вынуть плату и просверлить её насквозь, после чего пропустить усы улитки через новоиспечённое отверстие и припаять с обратной стороны.

Единственным более‑менее свободным местом, где можно было сделать отверстие были широченные высокотоковые дорожки, ведущие 5 вольт к выходу БП — те самые, к которым мы и паяемся. Ну ок, почему бы и нет. Тут только важно в процессе пропихивания провода не уничтожить изоляцию в месте сгиба, чтобы жилы не замкнули с просверленной дорожкой.

В дорожке +5В делаем отверстие и пропускаем через него усики
В дорожке +5В делаем отверстие и пропускаем через него усики

Припаянные контакты выпирают, и могут случайно замкнуться с корпусом БП. Чтобы избежать этого, я проложил между платой и корпусом волшебную изоляционную бумажку. После этого уверенно продолжил думать, что ничего не пойдет не так :)

Аккуратно прецензионно припаиваемся к дорожкам
Аккуратно прецензионно припаиваемся к дорожкам

Мистера Улитку пристроил рядом с трансформатором. Он греется, чему электролитическое содержимое улитки радо не будет, но пусть терпит, выбора всё равно нет.

Очередная плата с мистером Улиткой
Очередная плата с мистером Улиткой

С помощью скотча указал улиткам их место, чтобы не болтались в процессе путешествий теликов и ничего внутри не разнесли. После этого собрал блоки питания обратно, не забыв про термопрокладки силовых ключей. У одного из блоков они убились, и я заменил их на новые, сделанные для видеокарт (штатные по ощущениям напоминали картон, смазанный дешёвой термопастой). Не думаю, что это что‑то поменяет кардинально, но какой‑то процент надёжности прибавит.

Улитки заняли расчётные места, пора собирать БП
Улитки заняли расчётные места, пора собирать БП

Собранные БП обратно смонтировал на видеостену, подключил из запустил. И... ура! Проблема с зависанием контроллера пропала. Подсветка работала несколько дней, после чего левый БП внезапно отключился.

Виновник торжества
Виновник торжества

Разбор полётов показал, что мистер Улитка всё таки нашёл способ всё разнести: самовольное саморазрушающее самозаземление. Фокус в том, что я покрасил боковые БП золотой краской. Металлизированной. Изолирующая бумажка оказалась зажата между выпирающим с обратной стороны платы усом улитки и покрашенным корпусом.

Привычка всё, что видишь, красить в золотое имеет не только плюсы :)
Привычка всё, что видишь, красить в золотое имеет не только плюсы :)

И, как я понимаю, под действием электрического потенциала краска начала пропитываться сквозь бумажку в сторону источника этого самого потенциала. В результате пропитанная бумажка, стала проводником и замкнула плюс с корпусом БП, от чего тот ушёл в защиту. Что характерно, у минусового контакта бумажка не пропиталась.

Схема сети питания лент
Схема сети питания лент

SVG, хайрес PNG.

Проблема решилась заменой бумажки на более изоляционно‑изолирующий изолятор из резины. После этого веселье с питанием было окончено, и, вроде как, оно заработало.

А параллельно с этим всем я занимался контроллером для адресных лент.

Почти всё - контроллеры, телики, БП с внутренностями и прочее моделил с нуля, ибо готовое не нашёл
Почти всё - контроллеры, телики, БП с внутренностями и прочее моделил с нуля, ибо готовое не нашёл

Контроллер и система управления

Чтобы это всё правильно светилось, нам надо анализировать инфу на экране и командовать лентами. Но напрямую подключить к компу ленты нельзя, только через контроллер. Можно ли купить готовый? Нет. Потому что всё совсем‑совсем непросто, когда ты ещё не открыл для себя SPI.

Производство лент (https://www.youtube.com/watch?v=pMjhJ9kcaU4)
Производство лент (https://www.youtube.com/watch?v=pMjhJ9kcaU4)

Я выбрал ленты с самой высокой плотностью светодиодов, которую смог найти — 144 на метр, причём каждый диод управляется независимо. Если что — бывают ленты, где светодиоды управляются группами. Именно поэтому мои ленты кушают 5 вольт, а не 12В или 24В — в групповых лентах диоды соединены последовательно, что увеличивает напряжение, снижает ток и проблемы с толстыми проводами.

144-диодные ленты поставляются катушками по 1 метру — для подсветки таких понадобилось больше 16 штук. Каждый «диод» на такой ленте — целое умное устройство‑пиксель, с чипом WS2812b и тремя светодиодами, которыми он управляет. У него даже есть ОЗУ — целых три байта.

Пиксель подсветки. Всего их 2315 штук. Да, микроскоп зафиксировать нормально мне так и не удалось
Пиксель подсветки. Всего их 2315 штук. Да, микроскоп зафиксировать нормально мне так и не удалось
Как он устроен

На ленте три медных дорожки, крайние для энергии, по центральной передаются управляющие команды о цветах.

Очевидно, что плюсовая дорожка - это та, где напротив каждого диода стоит знак "-"
Очевидно, что плюсовая дорожка - это та, где напротив каждого диода стоит знак "-"

В теории эти ленты можно резать/цеплять друг за друга. В том числе, подразумевается, что я соединяю все свои многочисленные кусочки в одну гигантскую 16-метровую гигаленту, цепляю начало к контроллеру и радуюсь.

Только это работать не будет. Фокус тут в том, что эта адресная лента — не адресная. Поменять цвет произвольного диода не получится, только заново задать цвета на всей ленте.

Смысл протокола WS2812b
Смысл протокола WS2812b

На дорожку управления по особому протоколу подаются байты, диоды передают их друг другу по цепочке и запоминают, каждый свой цвет. Каждый диод рассуждает так:

Если мне дали цвет, а я пустой - я запомню цвет
Если мне дали цвет, а я заполнен - я передам цвет дальше
Если инфы долго нет, я включаю запомненный цвет

Данные перестали поступать — цвета применились. А чипы медленные. Больше диодов в ленте — медленнее обновление. Один метр сложно обновлять чаще 50 раз в секунду. 16 метров — это слайд‑шоу в 1 кадр/сек. Так что соединить все куски в одну гигаленту нельзя.

Надо каждую ленту подцепить к отдельному контакту на контроллере и управлять ими параллельно.

Тернистый путь к параллелизму

Физически у нас 36 кусочков лент — многовато. Чтобы упростить себе жизнь, я всё же сцепил мелкие кусочки друг с другом, чтобы с точки зрения контроллера лент было не 36, а всего лишь 17.

Это уже лучше. Контроллер с 17 пинами-контактами найти не сложно
Это уже лучше. Контроллер с 17 пинами-контактами найти не сложно

Кусочки лент я группировал так, чтобы приблизить количество диодов в каждой группе к заветным 144. В горизонтальных сегментах диодов больше 144, потому что ширина теликов больше метра (123 см), а в вертикальных вместе со скосами — меньше 144 — потому что высота теликов меньше метра (70 см). Поэтому в горизонтальных сегментах есть дополнительные короткие «хвосты» в 10–15 диодов, а в вертикальных сегментах как раз есть недобор до 144 диодов. Именно поэтому я так заморочено соединил хвосты горизонтальных сегментов, вертикальные сегменты и скосы.

Теперь надо научиться одновременно управлять этими 17 лентами с контроллера. Дело, вроде бы, нетрудное — взял популярную плату, подцепил каждый кусок лент к своей ноге контроллера, скачал библиотеку, написал пару строк. Считываем с компа инфу о цветах, параллельно подаём на ленты, всё.

На практике путь занял больше месяца. Большое спасибо ребятам с CyberForum.ru, которые помогали мне разбираться с этим всем.

Попытка №1. Arduino Mega 2560

Все начинают с Ардуино. Я тоже начал. А раз лент много — то нужен Ардуино с максимальным числом контактов. Так у меня появился Arduino Mega 2560 — тот самый, который нынче управляет приводами теликов.

Пока вытаскивал из-за телика, чуток погнул разъёмы
Пока вытаскивал из-за телика, чуток погнул разъёмы

Библиотек для лент под него миллион, но все они заточены под одну ленту. Кучей лент они управлять как‑бы могут, но переключаясь попеременно с одной на другую, а не параллельно. А такой подход — это лаги и тормоза. Насколько я понял тогда, причина в том, что протокол управления лентой требует невероятно точно включать‑выключать напряжение на управляющем контакте. Импульсы длительностью 400 нс с отклонением не более 125 нс. И библиотеки делают это, опираясь на аппаратные фичи контроллеров (PWM). А эти аппаратные фичи так могут только с парой контактов, максимум четырьмя. Не с семнадцатью, в общем :)

Погрузившись в тему, я открыл для себя DMA — способ быстрого одновременного чтения/изменения состояния нескольких контактов контроллера за раз.

Наперевес с осциллографом, я родил протокол параллельного управления 17 лентами через nopы (ниже разберу эту реализацию на STM32). Это работало, но на это ушла почти вся память контроллера (7 из 8 Кб). А ведь надо еще данные с компа принимать и обрабатывать. Да и поток этих данных неслабый — около 2 Мбит/с. В общем, Ардуино такое не переварит, возьмем что‑нибудь помощнее. Малину, например. 4 ядра, 4 гига — уж точно хватит на всё, не правда ли?

Попытка №2. Raspberry Pi 4B

Интересная штуковина. Программировал её я прямо на ней самой в Geany, подключив к левому телику по HDMI. Быстро реализовал дичь с DMA и nopaми, смог рулить кучей лент. Предполагалось получать данные с компа по Ethernet, и затем отбивать нужные импульсы на контактах GPIO. Но проблема пришла откуда не ждали.

Та самая Raspberry Pi 4B, на которой я пытался делать протокол. Радиаторы оказались не лишними - она заметно грелась в ходе работы
Та самая Raspberry Pi 4B, на которой я пытался делать протокол. Радиаторы оказались не лишними - она заметно грелась в ходе работы

Малина — полноценный комп, ОС у меня там стояла многозадачная (стоковый малинковый Debian). Если говорить просто, многозадачная ОС попеременно выполняет код всех параллельно работающих программ. И в процессе передачи данных на ленту она иногда отвлекалась на другие программы, интервалы в 400 нс удлинялись в несколько раз, что выносило мозг ленте и она вспыхивала белым светом. На деле эти переключения — «прерывания» — могут происходить по куче разных причин, программных или аппаратных, но суть не в этом. Наверное:)

Ширина двух импульсов схлопнулась почти в 0, потому что сработали прерывания
Ширина двух импульсов схлопнулась почти в 0, потому что сработали прерывания

Располагая опытом разработки реалтаймового ПО под винду, я подумал, что проблему можно побороть малой кровью. Ну‑ну. Подстава была в том, что то «реальное время» у меня было очень‑очень мягким, а здесь — полный жесткач.

Импульс в 400 нс превращается в 3000 нс - на такое лента реагирует яркой вспышкой
Импульс в 400 нс превращается в 3000 нс - на такое лента реагирует яркой вспышкой

Борясь с этой проблемой, я временно отложил DMA и сосредоточился на том, чтобы добиться стабильной ширины импульса. В итоге дошёл до написания драйвера, чтобы во время передачи бит цвета на ленту останавливать прерывания в ядре ОС (те самые отвлекания), сконфигурировал местный планировщик задач так, чтобы мой драйвер всегда работал на 4 ядре процессора, а все остальные программы на первых трех, заблокировал тактовую частоту — и всё равно ленты моргали.

Пытаюсь заблокировать прерывания и выдать равномерный меандр из модуля ядра. А они не хотят блокироваться
Пытаюсь заблокировать прерывания и выдать равномерный меандр из модуля ядра. А они не хотят блокироваться

На малине должно было работать ПО в user‑mode, которое читает по UDP пакетики с компа, и пишет их в модуль ядра. А модуль ядра уже в kernel‑mode шевелит пинами.

Я уже размышлял, что можно организовать еще две малины и три карты захвата, когда они появятся в продаже — в те времена HDMI 2.1 был редкостью, как и карты захвата для него, и полностью вынести анализ картинки из компа. То есть из видеокарты сигнал бы проходил через карту захвата и шел на ТВ, а малина бы его параллельно анализировала и командовала своими лентами — и так на каждом из трёх ТВ.

Но не срослось.

Кусочки кода под Raspberry Pi 4B, которые смог откопать

Привязка к 4 ядру:

void initRealtime()
{
	//Привязываемся к 4 ядру
	cpu_set_t cpu_list;
	CPU_ZERO(&cpu_list);
	CPU_SET(3, &cpu_list);
	if (sched_setaffinity(getpid(), sizeof(cpu_list), &cpu_list) == 0)
		printf("Affinity ok\n");
	else
		printf("Affinity fail\n");
	
	//Выставляем максимальный приоритет
	int prio = sched_get_priority_max(SCHED_FIFO);
	struct sched_param param;
	param.sched_priority = prio;
	sched_setscheduler(0, SCHED_FIFO, &param);
}

А так делалось скидывание всех остальных приложений на остальные 3 ядра:

console=serial0,115200 isolcpus=3 spidev.bufsize=32768 console=tty1 root=PARTUUID=db5b267f-02 rootfstype=ext4 fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles

Локи прерываний:

//Лок прерываний
__attribute__((always_inline)) static inline void __disable_irq(void)
{ 
	__asm volatile("cpsid if" : : : "memory");
}
//Анлок прерываний
__attribute__((always_inline)) static inline void __enable_irq(void)
{
	__asm volatile("cpsie if" : : : "memory");
}

Конфигурируем GPIO:

struct GpioRegisters *s_pGpioRegisters = NULL;
static void SetGPIOFunction(int GPIO, int functionCode)
{
    int registerIndex = GPIO / 10;
    int bit = (GPIO % 10) * 3;
 
    unsigned oldValue = s_pGpioRegisters-> GPFSEL[registerIndex];
    unsigned mask = 0b111 << bit;
    
    s_pGpioRegisters-> GPFSEL[registerIndex] = (oldValue & ~mask) | ((functionCode << bit) & mask);
}
void initGPIO(void)
{
	bool err;
	uint32_t gpioAddress = getGpioRegBase(&err);
	s_pGpioRegisters = (struct GpioRegisters*)ioremap(gpioAddress, 0x1000);
	
	int i;
	for (i = 0; i < 32; i++)
		SetGPIOFunction(GPIO_PINS_GROUP * 32 + i, 1);
}

Непосредственная передача данных на пины:

static void sendDataToLEDS(void)
{

    //Регистры DMA GPIO
	volatile register uint32_t* setReg = s_pGpioRegisters->GPSET + GPIO_PINS_GROUP;
	volatile register uint32_t* clrReg = s_pGpioRegisters->GPCLR + GPIO_PINS_GROUP;
	
	//Смысл такой: у нас есть большой буфер и малый
    //В большом лежат цвета для всех лент
    //В малом мы будем откладывать по 1 цвету для каждой ленты
    //И затем отбивать их на пинах
    
	volatile register uint32_t* smallBufferCurPos = smallBuffer;
	
	uint32_t* bigBufferBegin = (uint32_t*)bigBuffer.begin;
	uint32_t* bigBufferEnd = bufferGetEnd(&bigBuffer);
	uint32_t* bigBufferCurPos = bigBufferBegin;

     //для проверки на тестовых данных, что они не повредились
	uint32_t hashCounter;
	
	u64 t; 

	int debug_counter = 0;

	while (bigBufferCurPos < bigBufferEnd)
	{
		t = ktime_get_ns(); //дебаговое
		{
			//копируем в малый буфер цвета диодов (по 1 для каждой ленты)
			memcpy(smallBuffer, bigBufferCurPos, SMALLBUFFER_BYTE_COUNT);
			bigBufferCurPos += SMALLBUFFER_LEN;
			
  		    //дебаговое
			smallBufferCurPos = smallBuffer;
			while (smallBufferCurPos < smallBufferEnd)
				hashCounter+=*smallBufferCurPos++;
				
			while (ktime_get_ns() - t < LED_DELAY_NS)
				hashCounter<<=1;
		}
      
        //дебаговое
		smallBufferCurPos = smallBuffer;
		while (smallBufferCurPos < smallBufferEnd)
			hashCounter^=*smallBufferCurPos++;

        smallBufferCurPos = smallBuffer;
      
  	    local_irq_disable(); //Блокируем прерывания
    		
		volatile register uint32_t m;
		while (smallBufferCurPos < smallBufferEnd)
		{
			m = READ_ONCE(*smallBufferCurPos); //счиытываем 1 значение
			barrier(); //чтобы компилятор не устроил самодеятельность с перестановками инструкций в самом нужном месте
			WRITE_ONCE(*setReg, m); //записываем в регистр
			smallBufferCurPos++; //идём дальше
			barrier();

            //Делаем то же самое с clrReg
			m = READ_ONCE(*smallBufferCurPos); 
			barrier();
			WRITE_ONCE(*clrReg, m);
			smallBufferCurPos++;
			barrier();
						
		    //а это мы пытаемся в 400 нс
			asm volatile("nop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop\n\tnop");
		}
      
		local_irq_enable(); //Снимаем блокировку прерываний
	}
	
	//if (hashCounter != )
  //		printk("что-то не так\n");
}

Тут либо всё-таки ставить на малину ОС реального времени и разбираться с ней, либо попробовать что‑то ещё. Я и попробовал.

Попытка №3. STM32 Discovery

Всё получилось реализовать на контроллере STM32 Discovery F7. В отличие от Arduino, у него много памяти и всяких фич. Но это и не комп, как Raspberry Pi, с ОС и выносящими мозг прерываниями, необходимостью писать драйверы и конфигурировать планировщик задач.

STM32 Discovery F7
STM32 Discovery F7

Работает он с напряжением 3,3 вольта, а не 5, но опыты показали, что мои ленты прекрасно его понимают. Протокол управления лентами здесь сделан так же, как в Arduino и Raspberry Pi — в довольно нестандартном виде.

Один USB бортового программатора, второй пользовательский - по нему рулим лентами
Один USB бортового программатора, второй пользовательский - по нему рулим лентами
STM32CubeIDE, где всё это кодилось. Да, я сожрал всю оперативку :)
STM32CubeIDE, где всё это кодилось. Да, я сожрал всю оперативку :)

Пишем руками протокол WS2812b

Теперь про реализацию параллельной версии протокола. Запилил я его не под 17, а под 20 лент — три контакта остались в резерве, на случай, если я опять что‑нибудь там сломаю.

Протокол WS2812b
Протокол WS2812b

Хочешь передать единичку — выдавай один 800 нс, потом ноль 450 нс. Хочешь передать нолик — давай единичку 400 нс, потом ноль 850 нс. Передал так 24 бита задом наперёд — жди меньше 50 мкс, затем передавай цвет следующего диода. Подождешь дольше 50 мкс — вся лента применит цвета, которые запомнила.

Я переформулировал этот протокол так, чтобы он сочетался с концепцией параллельного управления лентами:

Передаешь единичку 400 нс, потом передаешь свой бит 400 нс, потом передаёшь нолик 400 нс. Передал так 24 бита — жди 30 мкс, затем передавай цвет следующего диода. Передай так все цвета и жди следующего пакета данных с компа.

Ключевое тут то, что у трёх интервалов равная длительность, что позволяет работать с множеством лент синхронно. Длительность эта неправильная, не соответствует стандарту и немного плавает. Но она работает, а значит — мне подходит.

Передача разных 24 бит одновременно на четыре ленты
Передача разных 24 бит одновременно на четыре ленты

Одновременное изменение состояний нескольких ног контроллера делается способом, который называется DMA (Direct Memory Access). В коде пишем значение в специальный регистр STM32, и биты этого числа одновременно выставляются сразу на всех ногах, соответствующих этому регистру.

Задал одно значение - биты одновременно выставились на контактах контроллера
Задал одно значение - биты одновременно выставились на контактах контроллера

В моём STM32 таких регистра три: A, B и C, каждый по 16 ног. Я подключил ленты к контактам регистров B и C. Регистры 32 битные, но на контакты выводится, как я понял, только половина — поэтому все ленты в один регистр не поместились.

Ленты подключаем к контроллеру произвольно и не заморачиваясь — порядок в лентах будем потом наводить программно.

Во время передачи 24 бит цвета надо точно соблюдать временные интервалы. А вот между цветами можно тупить и тормозить, главное, не дольше 50 мкс.

Точное ожидание 400 нс я реализовал в коде через блокировку прерываний и стадо nopов, подобрав их количество с помощью осциллографа. Говорят, паузы надо делать на аппаратных таймерах STM32, но выжать из них 400 нс у меня не получилось.

Что интересно, та же прошивка, залитая в другой STM32 той же модели, даёт немного другие интервалы. Видимо поэтому все про таймеры говорят. Тем не менее, с этим экземпляром ленты тоже работают
Что интересно, та же прошивка, залитая в другой STM32 той же модели, даёт немного другие интервалы. Видимо поэтому все про таймеры говорят. Тем не менее, с этим экземпляром ленты тоже работают

Теперь про сам алгоритм в контроллере. Нам с компа несколько десятков раз в секунду по виртуальному COM-порту прилетает пакет в 8640 байт (20 лент × 144 диода × 3 канала RGB) с информацией о том, какие цвета выставить на всех лентах.

Для упрощения объясню алгоритм на примере с 6 лентами, а не с 20, и представлю, что DMA регистр 6-битный.

Мы берём из пакета данных первые 18 байт — 6 цветов по 3 байта для очередного диода каждой ленты. Нам надо эти 18 байт × 8 = 144 бита отбить одновременно на 6 ногах. Всего каждая нога при этом передаст 144 / 6 = 24 бита цвета для своей ленты.

На деле там всё сложнее, потому что цветов, лент и регистров больше, и каждый регистр шире
На деле там всё сложнее, потому что цветов, лент и регистров больше, и каждый регистр шире

Передача каждого бита в протоколе WS2812b требует трёх переключений: 1, наш бит, 0. Мы переключим ногу 24 × 3 = 72 раза. Лента не одна, а шесть, значит, каждый раз переключаем не одну ногу, а сразу шесть. Значит нам надо с паузами 400 нс записать 72 значения в DMA регистр.

Я не использую здесь циклы и массивы — каждая инструкция на счету. Это буквально стена кода из 72 переменных, приравниваний и nopов. Чтобы родить это, я накодил генератор на C#, который уже накодил мне всё это переупорядочивание бит на C.

Вот так выглядит формирование значений переменных:

//ptr - это указатель на байты пришедшего пакета с цветами для лент
//VALUE_C1 - одна из многочисленных переменных, которую потом пихаем в регистр
VALUE_C1 |= (int32_t)(ptr[0] & 1); //strip #0 bit #0 ch #0, надо заполнить 0 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 0 бит
VALUE_C1 |= (int32_t)(ptr[3] & 1) << 3; //strip #1 bit #0 ch #0, надо заполнить 3 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 3 бит
VALUE_C1 |= (int32_t)(ptr[6] & 1) << 4; //strip #2 bit #0 ch #0, надо заполнить 4 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 4 бит
VALUE_C1 |= (int32_t)(ptr[9] & 1) << 5; //strip #3 bit #0 ch #0, надо заполнить 5 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 5 бит
VALUE_C1 |= (int32_t)(ptr[12] & 1) << 6; //strip #4 bit #0 ch #0, надо заполнить 6 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 6 бит
...
ещё 100500 строк

А вот так они пихаются в регистр:

__disable_irq(); //Лочим прерывания
{
GPIOC->ODR = VALUE_C0; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); 
GPIOC->ODR = VALUE_C1; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
GPIOC->ODR = VALUE_C2; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
GPIOC->ODR = VALUE_C3; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
...
100500 строк
}
__enable_irq(); //Разрешаем прерывания

То есть, мы блокируем прерывания только на время передачи 24 бит цвета — там где нужны очень точные интервалы, а между этими передачами единичных цветов пусть себе прерывается сколько хочет, здесь отклонения не критичны. И здесь же — между этими передачами цветов — мы заготавливаем новую партию переменных для того, чтобы отбить новые 24 бита.

Сокращённый вариант кода STM32
/*
 * VBDLEDData.h
 *
 *  Created on: Jan 29, 2022
 *      Author: VBDUnit
 */

#ifndef SRC_VBDLEDDATA_H_
#define SRC_VBDLEDDATA_H_

//\#define INVERTORS

#define LEDS_PER_STRIPE 144
#define STRIPE_COUNT 20
#define LED_COUNT (LEDS_PER_STRIPE * STRIPE_COUNT)
#define BYTES_PER_LED 3
#define BYTES_PER_PACK (STRIPE_COUNT * BYTES_PER_LED)

static uint8_t ledData[LED_COUNT * BYTES_PER_LED];


static uint8_t* ledDataPtr = 0;
static uint8_t* ledDataEnd = 0;

static volatile int testVariable = 15;

static volatile int ledNoUpdateCounter = 0;


static void LED_Init(void)
{
	ledDataPtr = ledData;
	ledDataEnd = ledDataPtr + LED_COUNT * BYTES_PER_LED;
}

static void LED_WriteBuffer(uint8_t* sourcePtr, uint16_t sourceByteCount, int32_t* bytesCopyed, int32_t* bufferIsFilled)
{
	int32_t bytesToCopy = sourceByteCount;
	if (ledDataPtr + bytesToCopy >= ledDataEnd)
		bytesToCopy = ledDataEnd - ledDataPtr;

	memcpy(ledDataPtr, sourcePtr, bytesToCopy);
	ledDataPtr += bytesToCopy;

	*bufferIsFilled = ledDataPtr == ledDataEnd ? 1 : 0;
	*bytesCopyed = bytesToCopy;
}


static inline void Wait50us()
{

}
static void LED_DisplayData(void) __attribute__((noinline));
static void LED_DisplayData(void)

	uint32_t VALUE_C0, VALUE_C1, VALUE_C2, VALUE_C3, VALUE_C4, VALUE_C5, VALUE_C6, VALUE_C7, VALUE_C8, VALUE_C9, VALUE_C10, VALUE_C11, VALUE_C12, VALUE_C13, VALUE_C14, VALUE_C15, VALUE_C16, VALUE_C17, VALUE_C18, VALUE_C19, VALUE_C20, VALUE_C21, VALUE_C22, VALUE_C23, VALUE_C24, VALUE_C25, VALUE_C26, VALUE_C27, VALUE_C28, VALUE_C29, VALUE_C30, VALUE_C31, VALUE_C32, VALUE_C33, VALUE_C34, VALUE_C35, VALUE_C36, VALUE_C37, VALUE_C38, VALUE_C39, VALUE_C40, VALUE_C41, VALUE_C42, VALUE_C43, VALUE_C44, VALUE_C45, VALUE_C46, VALUE_C47, VALUE_C48, VALUE_C49, VALUE_C50, VALUE_C51, VALUE_C52, VALUE_C53, VALUE_C54, VALUE_C55, VALUE_C56, VALUE_C57, VALUE_C58, VALUE_C59, VALUE_C60, VALUE_C61, VALUE_C62, VALUE_C63, VALUE_C64, VALUE_C65, VALUE_C66, VALUE_C67, VALUE_C68, VALUE_C69, VALUE_C70, VALUE_C71;
	uint32_t VALUE_B0, VALUE_B1, VALUE_B2, VALUE_B3, VALUE_B4, VALUE_B5, VALUE_B6, VALUE_B7, VALUE_B8, VALUE_B9, VALUE_B10, VALUE_B11, VALUE_B12, VALUE_B13, VALUE_B14, VALUE_B15, VALUE_B16, VALUE_B17, VALUE_B18, VALUE_B19, VALUE_B20, VALUE_B21, VALUE_B22, VALUE_B23, VALUE_B24, VALUE_B25, VALUE_B26, VALUE_B27, VALUE_B28, VALUE_B29, VALUE_B30, VALUE_B31, VALUE_B32, VALUE_B33, VALUE_B34, VALUE_B35, VALUE_B36, VALUE_B37, VALUE_B38, VALUE_B39, VALUE_B40, VALUE_B41, VALUE_B42, VALUE_B43, VALUE_B44, VALUE_B45, VALUE_B46, VALUE_B47, VALUE_B48, VALUE_B49, VALUE_B50, VALUE_B51, VALUE_B52, VALUE_B53, VALUE_B54, VALUE_B55, VALUE_B56, VALUE_B57, VALUE_B58, VALUE_B59, VALUE_B60, VALUE_B61, VALUE_B62, VALUE_B63, VALUE_B64, VALUE_B65, VALUE_B66, VALUE_B67, VALUE_B68, VALUE_B69, VALUE_B70, VALUE_B71;
	VALUE_C0=VALUE_B0=65535;VALUE_C2=VALUE_B2=0;VALUE_C3=VALUE_B3=65535;VALUE_C5=VALUE_B5=0;VALUE_C6=VALUE_B6=65535;VALUE_C8=VALUE_B8=0;VALUE_C9=VALUE_B9=65535;VALUE_C11=VALUE_B11=0;VALUE_C12=VALUE_B12=65535;VALUE_C14=VALUE_B14=0;VALUE_C15=VALUE_B15=65535;VALUE_C17=VALUE_B17=0;VALUE_C18=VALUE_B18=65535;VALUE_C20=VALUE_B20=0;VALUE_C21=VALUE_B21=65535;VALUE_C23=VALUE_B23=0;VALUE_C24=VALUE_B24=65535;VALUE_C26=VALUE_B26=0;VALUE_C27=VALUE_B27=65535;VALUE_C29=VALUE_B29=0;VALUE_C30=VALUE_B30=65535;VALUE_C32=VALUE_B32=0;VALUE_C33=VALUE_B33=65535;VALUE_C35=VALUE_B35=0;VALUE_C36=VALUE_B36=65535;VALUE_C38=VALUE_B38=0;VALUE_C39=VALUE_B39=65535;VALUE_C41=VALUE_B41=0;VALUE_C42=VALUE_B42=65535;VALUE_C44=VALUE_B44=0;VALUE_C45=VALUE_B45=65535;VALUE_C47=VALUE_B47=0;VALUE_C48=VALUE_B48=65535;VALUE_C50=VALUE_B50=0;VALUE_C51=VALUE_B51=65535;VALUE_C53=VALUE_B53=0;VALUE_C54=VALUE_B54=65535;VALUE_C56=VALUE_B56=0;VALUE_C57=VALUE_B57=65535;VALUE_C59=VALUE_B59=0;VALUE_C60=VALUE_B60=65535;VALUE_C62=VALUE_B62=0;VALUE_C63=VALUE_B63=65535;VALUE_C65=VALUE_B65=0;VALUE_C66=VALUE_B66=65535;VALUE_C68=VALUE_B68=0;VALUE_C69=VALUE_B69=65535;VALUE_C71=VALUE_B71=0;

#ifdef INVERTORS
	VALUE_C0=~VALUE_C0; VALUE_C2=~VALUE_C2; VALUE_C3=~VALUE_C3; VALUE_C5=~VALUE_C5; VALUE_C6=~VALUE_C6; VALUE_C8=~VALUE_C8; VALUE_C9=~VALUE_C9; VALUE_C11=~VALUE_C11; VALUE_C12=~VALUE_C12; VALUE_C14=~VALUE_C14; VALUE_C15=~VALUE_C15; VALUE_C17=~VALUE_C17; VALUE_C18=~VALUE_C18; VALUE_C20=~VALUE_C20; VALUE_C21=~VALUE_C21; VALUE_C23=~VALUE_C23; VALUE_C24=~VALUE_C24; VALUE_C26=~VALUE_C26; VALUE_C27=~VALUE_C27; VALUE_C29=~VALUE_C29; VALUE_C30=~VALUE_C30; VALUE_C32=~VALUE_C32; VALUE_C33=~VALUE_C33; VALUE_C35=~VALUE_C35; VALUE_C36=~VALUE_C36; VALUE_C38=~VALUE_C38; VALUE_C39=~VALUE_C39; VALUE_C41=~VALUE_C41; VALUE_C42=~VALUE_C42; VALUE_C44=~VALUE_C44; VALUE_C45=~VALUE_C45; VALUE_C47=~VALUE_C47; VALUE_C48=~VALUE_C48; VALUE_C50=~VALUE_C50; VALUE_C51=~VALUE_C51; VALUE_C53=~VALUE_C53; VALUE_C54=~VALUE_C54; VALUE_C56=~VALUE_C56; VALUE_C57=~VALUE_C57; VALUE_C59=~VALUE_C59; VALUE_C60=~VALUE_C60; VALUE_C62=~VALUE_C62; VALUE_C63=~VALUE_C63; VALUE_C65=~VALUE_C65; VALUE_C66=~VALUE_C66; VALUE_C68=~VALUE_C68; VALUE_C69=~VALUE_C69; VALUE_C71=~VALUE_C71;
	VALUE_B0=~VALUE_B0; VALUE_B2=~VALUE_B2; VALUE_B3=~VALUE_B3; VALUE_B5=~VALUE_B5; VALUE_B6=~VALUE_B6; VALUE_B8=~VALUE_B8; VALUE_B9=~VALUE_B9; VALUE_B11=~VALUE_B11; VALUE_B12=~VALUE_B12; VALUE_B14=~VALUE_B14; VALUE_B15=~VALUE_B15; VALUE_B17=~VALUE_B17; VALUE_B18=~VALUE_B18; VALUE_B20=~VALUE_B20; VALUE_B21=~VALUE_B21; VALUE_B23=~VALUE_B23; VALUE_B24=~VALUE_B24; VALUE_B26=~VALUE_B26; VALUE_B27=~VALUE_B27; VALUE_B29=~VALUE_B29; VALUE_B30=~VALUE_B30; VALUE_B32=~VALUE_B32; VALUE_B33=~VALUE_B33; VALUE_B35=~VALUE_B35; VALUE_B36=~VALUE_B36; VALUE_B38=~VALUE_B38; VALUE_B39=~VALUE_B39; VALUE_B41=~VALUE_B41; VALUE_B42=~VALUE_B42; VALUE_B44=~VALUE_B44; VALUE_B45=~VALUE_B45; VALUE_B47=~VALUE_B47; VALUE_B48=~VALUE_B48; VALUE_B50=~VALUE_B50; VALUE_B51=~VALUE_B51; VALUE_B53=~VALUE_B53; VALUE_B54=~VALUE_B54; VALUE_B56=~VALUE_B56; VALUE_B57=~VALUE_B57; VALUE_B59=~VALUE_B59; VALUE_B60=~VALUE_B60; VALUE_B62=~VALUE_B62; VALUE_B63=~VALUE_B63; VALUE_B65=~VALUE_B65; VALUE_B66=~VALUE_B66; VALUE_B68=~VALUE_B68; VALUE_B69=~VALUE_B69; VALUE_B71=~VALUE_B71;
#endif

	uint8_t* ptr = ledData;
	int count = ledDataEnd - ptr;
	count/=60;
	count*=60;
	uint8_t* ptrEnd = ptr + count;
	while (ptr < ptrEnd)
	{

		//VALUE_C1=VALUE_B1=0;VALUE_C4=VALUE_B4=0;VALUE_C7=VALUE_B7=0;VALUE_C10=VALUE_B10=0;VALUE_C13=VALUE_B13=0;VALUE_C16=VALUE_B16=0;VALUE_C19=VALUE_B19=0;VALUE_C22=VALUE_B22=0;VALUE_C25=VALUE_B25=0;VALUE_C28=VALUE_B28=0;VALUE_C31=VALUE_B31=0;VALUE_C34=VALUE_B34=0;VALUE_C37=VALUE_B37=0;VALUE_C40=VALUE_B40=0;VALUE_C43=VALUE_B43=0;VALUE_C46=VALUE_B46=0;VALUE_C49=VALUE_B49=0;VALUE_C52=VALUE_B52=0;VALUE_C55=VALUE_B55=0;VALUE_C58=VALUE_B58=0;VALUE_C61=VALUE_B61=0;VALUE_C64=VALUE_B64=0;VALUE_C67=VALUE_B67=0;VALUE_C70=VALUE_B70=0;
		VALUE_C1=0; VALUE_C4=0; VALUE_C7=0; VALUE_C10=0; VALUE_C13=0; VALUE_C16=0; VALUE_C19=0; VALUE_C22=0; VALUE_C25=0; VALUE_C28=0; VALUE_C31=0; VALUE_C34=0; VALUE_C37=0; VALUE_C40=0; VALUE_C43=0; VALUE_C46=0; VALUE_C49=0; VALUE_C52=0; VALUE_C55=0; VALUE_C58=0; VALUE_C61=0; VALUE_C64=0; VALUE_C67=0; VALUE_C70=0;
		
		VALUE_C1 |= (int32_t)(ptr[0] & 1); //лента #0 bit #0 ch #0, надо заполнить 0 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 0 бит
		VALUE_C1 |= (int32_t)(ptr[3] & 1) << 3; //лента #1 bit #0 ch #0, надо заполнить 3 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 3 бит
		VALUE_C1 |= (int32_t)(ptr[6] & 1) << 4; //лента #2 bit #0 ch #0, надо заполнить 4 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 4 бит
		
		...
		
		VALUE_C70 |= (int32_t)(ptr[35] & 128) << 6; //лента #11 bit #7 ch #2, надо заполнить 13 бит маски, но инфа о нем хранится в 7, поэтому сдвигаемся на 6 бит
		VALUE_C70 |= (int32_t)(ptr[38] & 128) << 7; //лента #12 bit #7 ch #2, надо заполнить 14 бит маски, но инфа о нем хранится в 7, поэтому сдвигаемся на 7 бит
		VALUE_C70 |= (int32_t)(ptr[41] & 128) << 8; //лента #13 bit #7 ch #2, надо заполнить 15 бит маски, но инфа о нем хранится в 7, поэтому сдвигаемся на 8 бит
#ifdef INVERTORS
		VALUE_C1=~VALUE_C1; VALUE_C4=~VALUE_C4; VALUE_C7=~VALUE_C7; VALUE_C10=~VALUE_C10; VALUE_C13=~VALUE_C13; VALUE_C16=~VALUE_C16; VALUE_C19=~VALUE_C19; VALUE_C22=~VALUE_C22; VALUE_C25=~VALUE_C25; VALUE_C28=~VALUE_C28; VALUE_C31=~VALUE_C31; VALUE_C34=~VALUE_C34; VALUE_C37=~VALUE_C37; VALUE_C40=~VALUE_C40; VALUE_C43=~VALUE_C43; VALUE_C46=~VALUE_C46; VALUE_C49=~VALUE_C49; VALUE_C52=~VALUE_C52; VALUE_C55=~VALUE_C55; VALUE_C58=~VALUE_C58; VALUE_C61=~VALUE_C61; VALUE_C64=~VALUE_C64; VALUE_C67=~VALUE_C67; VALUE_C70=~VALUE_C70;
#endif

		__disable_irq();
		{
		GPIOC->ODR = VALUE_C0; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
		GPIOC->ODR = VALUE_C1; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
		GPIOC->ODR = VALUE_C2; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
		
		...
		
		GPIOC->ODR = VALUE_C69; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
		GPIOC->ODR = VALUE_C70; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
		GPIOC->ODR = VALUE_C71; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
		}
		__enable_irq();

		ptr+=20 * 3; //20 лент по 3 байта на ленту
	}
	ptr = ledData;
	while (ptr < ptrEnd)
	{
			//VALUE_C1=VALUE_B1=0;VALUE_C4=VALUE_B4=0;VALUE_C7=VALUE_B7=0;VALUE_C10=VALUE_B10=0;VALUE_C13=VALUE_B13=0;VALUE_C16=VALUE_B16=0;VALUE_C19=VALUE_B19=0;VALUE_C22=VALUE_B22=0;VALUE_C25=VALUE_B25=0;VALUE_C28=VALUE_B28=0;VALUE_C31=VALUE_B31=0;VALUE_C34=VALUE_B34=0;VALUE_C37=VALUE_B37=0;VALUE_C40=VALUE_B40=0;VALUE_C43=VALUE_B43=0;VALUE_C46=VALUE_B46=0;VALUE_C49=VALUE_B49=0;VALUE_C52=VALUE_B52=0;VALUE_C55=VALUE_B55=0;VALUE_C58=VALUE_B58=0;VALUE_C61=VALUE_B61=0;VALUE_C64=VALUE_B64=0;VALUE_C67=VALUE_B67=0;VALUE_C70=VALUE_B70=0;
			VALUE_B1=0; VALUE_B4=0; VALUE_B7=0; VALUE_B10=0; VALUE_B13=0; VALUE_B16=0; VALUE_B19=0; VALUE_B22=0; VALUE_B25=0; VALUE_B28=0; VALUE_B31=0; VALUE_B34=0; VALUE_B37=0; VALUE_B40=0; VALUE_B43=0; VALUE_B46=0; VALUE_B49=0; VALUE_B52=0; VALUE_B55=0; VALUE_B58=0; VALUE_B61=0; VALUE_B64=0; VALUE_B67=0; VALUE_B70=0;

			VALUE_B1 |= (int32_t)(ptr[42] & 1) << 2; //лента #14 bit #0 ch #0, надо заполнить 2 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 2 бит
			VALUE_B1 |= (int32_t)(ptr[45] & 1) << 3; //лента #15 bit #0 ch #0, надо заполнить 3 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 3 бит
			VALUE_B1 |= (int32_t)(ptr[48] & 1) << 4; //лента #16 bit #0 ch #0, надо заполнить 4 бит маски, но инфа о нем хранится в 0, поэтому сдвигаемся на 4 бит
			
			...
			
			VALUE_B70 |= (int32_t)(ptr[56] & 128) >> 1; //лента #18 bit #7 ch #2, надо заполнить 6 бит маски, но инфа о нем хранится в 7, поэтому сдвигаемся на -1 бит
			VALUE_B70 |= (int32_t)(ptr[59] & 128); //лента #19 bit #7 ch #2, надо заполнить 7 бит маски, но инфа о нем хранится в 7, поэтому сдвигаемся на 0 бит

#ifdef INVERTORS
			VALUE_B1=~VALUE_B1; VALUE_B4=~VALUE_B4; VALUE_B7=~VALUE_B7; VALUE_B10=~VALUE_B10; VALUE_B13=~VALUE_B13; VALUE_B16=~VALUE_B16; VALUE_B19=~VALUE_B19; VALUE_B22=~VALUE_B22; VALUE_B25=~VALUE_B25; VALUE_B28=~VALUE_B28; VALUE_B31=~VALUE_B31; VALUE_B34=~VALUE_B34; VALUE_B37=~VALUE_B37; VALUE_B40=~VALUE_B40; VALUE_B43=~VALUE_B43; VALUE_B46=~VALUE_B46; VALUE_B49=~VALUE_B49; VALUE_B52=~VALUE_B52; VALUE_B55=~VALUE_B55; VALUE_B58=~VALUE_B58; VALUE_B61=~VALUE_B61; VALUE_B64=~VALUE_B64; VALUE_B67=~VALUE_B67; VALUE_B70=~VALUE_B70;
#endif

			__disable_irq();
			GPIOB->ODR = VALUE_B0; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
			GPIOB->ODR = VALUE_B1; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
			GPIOB->ODR = VALUE_B2; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
			
			...
			
			GPIOB->ODR = VALUE_B69; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
			GPIOB->ODR = VALUE_B70; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
			GPIOB->ODR = VALUE_B71; asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop"); asm("nop");
			__enable_irq();

			ptr+=20 * 3; //20 лент по 3 байта на ленту
		}
	for (int i = 0; i < 1000;i++)
		asm("nop");


}
static void LED_ResetBuffer(void)
{
	ledDataPtr = &ledData[0];
}



#endif /* SRC_VBDLEDDATA_H_ */

Полный код на GitHub

Это работает. Итак, технология управления кучей лент у меня есть. Теперь надо как‑то разместить и подключить контроллер к компу и лентам, разбросанным по четырёхметровой видеостене.

В коаксиальной паутине

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

Суть
Суть

Первой нас встречает та же проблема, что и у питания — длинные провода. Наши импульсы управления лентами — это квадратные сигналы частотой больше 2 МГц.

Хайрес контроллер

Такое с длинными проводами не дружит, поэтому перво‑наперво контроллер пихаем в середину установки. Так получится максимально укоротить провода — они будут не больше метра.

Хайрес павук

И всё равно, передавать такое по метровым проводам чревато. Сигнал затухнет, обрастёт помехами, фронты завалялся и улетят в эфир. Чтобы избежать проблем, я использовал тонкие коаксиальные кабели, внешняя оплётка которых защищает от помех и удерживает сигнал от наглого побега.

Снимать бокорезами две изоляции с миллиметрового провода - это незабываемо
Снимать бокорезами две изоляции с миллиметрового провода - это незабываемо

Именно эти провода соединяют контроллер со всеми лентами установки, а также некоторые куски лент между собой.

Соединение контроллера с компом тоже в рубашке: пятиметровый USB‑удлинитель с экранированием и активным усилением. Напомню — системный блок у меня стоит в отдельном помещении, чтоб не шуметь, поэтому провода до него — все эти USB и HDMI — имеют длину от 5 до 10 метров.

Хайрес

Светодиодные ленты тупые, и если отключить питание, они попытаются протащить энергию через провод управления. Живой контроллер и 90 ампер — вещи несовместимые, поэтому каждый проводок подключаем не напрямую, а через резистор. Импульсы управления через него пройдут, а вот большой ток — нет.

Лента не одна, а 17 — сжигать контроллер они будут толпой, а значит, сопротивление резистора лучше взять побольше. Перебором я выяснил, что максимум — это 600 Ом, если брать больше — фронты сигналов валятся и ленты шизеют.

При длине провода до ленты 1 метр максимальное сопротивление резистора, при котором ленты понимают сигнал - это 600 Ом
При длине провода до ленты 1 метр максимальное сопротивление резистора, при котором ленты понимают сигнал - это 600 Ом

Разъёмы, которыми провода соединяются с контроллером, я изготовил путём разрезания пополам разноцветных проводков для макетирования. Разноцветность — это удобно. Не понимаю, почему ещё не догадались провода компов сделать разноцветными, всякие USB, HDMI и питание.

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

Теперь надо припаять всё это дело к коаксиальным проводам. Завешиваем телик бумажками, воображая, что они спасут его от расплавленного припоя, и вытаскиваем коаксиальные провода, идущие от всех лент.

К сердечнику каждого провода цепляем по одной штуке наших разъёморезисторов. Делать это ужасно неудобно — сердечник коаксиального провода тоньше волоса. А ещё каждую из 17 рубашек‑оплёток надо соединять с нулём, чтобы защищать сигнал. Но делаю я это на стороне лент, иначе контроллер вообще в проводах утонет.

На резисторы надеваем термоусадку, втыкаем в контроллер. И не забываем ещё соединить ноль центрального БП подсветки с GND контроллера, а то бо‑бо будет. Ноль всех трёх БП, всех лент, оплёток и контроллера должен быть общий.

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

Коаксиальные провода для лент протаскиваем в рамах, там же, где кабели питания, крепим теми же нанотехнологиями.

Провод хлипкий, поэтому цепляем к ленте не напрямую, а через МГТФ‑кусочки. Сердечник к среднему контакту ленты, а рубашку — к нулю.

Чтобы исключить веселье с КЗ, я промазал все ленты и контакты двумя слоями прозрачного цапонлака. Особенно тщательно — левый телик, над которым висит кондиционер.

Итак. К компу подсоединён контроллер 5-метровым USB‑кабелем с экранированием и активным усилением, а к контроллеру — ленты коаксиальными кабелями через резисторы максимального сопротивления. Звучит надёжно — тестим.

Вся подсветка в сборе
Вся подсветка в сборе

Хайрес всей подсветки в сборе

Отладковая наладка

Запускаем — работает. Правда, комп почему‑то периодически вырубается, где то раз в полчаса — но я этот вопрос решил отложить. Сначала надо разобраться с подсветкой.

Через некоторое время я допустил оплошность: обесточил блоки питания подсветки, а контроллер оставил подключенным к компу. Ленты закономерно потребовали энергию у контроллера, полномочия которого на этом всё. И резисторы его не спасли.

Хм. Давайте сделаем реле, которое при отсутствии питания подсветки будет отключать контроллер от USB. Поскольку тут везде бубенчики, при обесточивании БП пять вольт на них угасает небыстро, поэтому пусть реле параллельно будет подключено к 220В вместе с БП. Пропала сеть — реле разомкнуло USB. Звучит надёжно — тестим.

Синий и коричневый провода параллельно подключается ко входящим 220В блоков питания, контроллер подключается через чёрный и белый USB-порты. Если 220В пропадает - реле размыкает USB, отключая контроллер от компа
Синий и коричневый провода параллельно подключается ко входящим 220В блоков питания, контроллер подключается через чёрный и белый USB-порты. Если 220В пропадает - реле размыкает USB, отключая контроллер от компа

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

Во‑первых, я запитал подсветку напрямую от распределительной коробки, чтобы исключить физическую возможность её обесточить без обесточивания компа. У неё нет вилки. Я могу обесточить комп и подсветку, или только комп. А обесточить только подсветку, оставив включённым комп — не могу. И да, так делать нельзя — не убирайте вилку, не будьте мной.

Во‑вторых, между STM32 и компом я вставил гальваническую развязку USB. Она исключает прямое соединение между компом и контроллером (упрощённо говоря, всё передаётся через магнитное поле, а не по физическому проводу).


Гальваническая развязка USB электрически разделяет комп и подсветку, оставляя возможность питать контроллер и обмениваться с ним информацией
Гальваническая развязка USB электрически разделяет комп и подсветку, оставляя возможность питать контроллер и обмениваться с ним информацией

Через неё может пройти питание контроллера и информация (до 10 Мбит/с), а вот разность потенциалов уравниваться не будет. Внезапно, комп перестал вырубаться. Видимо, дело было как раз в разности потенциалов — в компе срабатывала защита. После того, как моя подсветка и комп стали гальванически развязаны, всё, вроде бы, встало на свои места и заработало хорошо.

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

Раскирпичить контроллер не помогло ни использование STM32CubeProgrammer, ни восстановление через пин BOOT0. Подозреваю, что его всё‑таки можно оживить, но пока этим не занимался.

Я выяснил, что STM32 виснет, даже если телик шевелить руками, а не электроприводом. Видимо, в приводе телика какая‑то пакость генерирует помехи.

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

Недолго думая, я заземлил рычаги приводов обоих теликов, соединив с кронштейнами акустическим проводом. Проблема исчезла: теперь всё работает.

Схема сети управления лентами. Коаксиальные провода показаны без рубашек, чтобы не перегружать схему. По факту все длинные оранжевые провода - коаксиальные, рубашка каждого соединяется с минусом ленты, на которую подаётся сигнал
Схема сети управления лентами. Коаксиальные провода показаны без рубашек, чтобы не перегружать схему. По факту все длинные оранжевые провода - коаксиальные, рубашка каждого соединяется с минусом ленты, на которую подаётся сигнал

Хайрес сети управления: SVG, PNG


Заключение

Спустя 3 года, со знаниями про SPI, STM32 BlackPill и прочие штуки, вся эта реализация кажется мне немножечко переусложнённой. Тем не менее, создавая всё это, опыт — и по электронике, и — самое главное — по решению задач в условиях острого дефицита знаний и понимания что вообще происходит и почему оно опять сгорело, эта штука принесла отличный. Особенно в преддверии распространения гуманоидных роботов :3

Непорезанные хайресы: ортография сверху, ортография снизу, спереди, сзади.

И кстати — после наладки, за три года сверхинтенсивной эксплуатации, в этой системе не сломалось ничего. Вообще ничего. У меня всё‑таки получилось сделать её надёжной.

Что ж, рамы для подсветки готовы, питание и контроллер тоже. Впереди самое сложное: софт, речь о котором пойдёт в следующей части.

Теги:
Хабы:
+119
Комментарии136

Публикации

Работа

.NET разработчик
49 вакансий

Ближайшие события