
❯ 0. Вступление
Привет, Хабр! Я Рома Горбачев, учусь в десятом классе и делаю Fabbit — открытый карманный мультитул электронщика на ПЛИС вместе с другом Яриком Павлюком. Этакий Flipper One, да не для сетей и вайфаев, а для низкоуровневой работы с платами, проводами и протоколами.
Задумка простая: выбираешь в меню нужный инструмент — микроконтроллер-супервизор прямо на ходу загружает в ПЛИС другой битстрим, и за полсекунды внутри кристалла меняется сама цифровая схема. Не запускается другая программа на том же процессоре, а на лету пересобирается аппаратная начинка! В одну коробочку я заложил 24-канальный логический анализатор, двухканальный осциллограф (2x400 MS/s) с генератором сигналов, лабораторный блок питания (0,8–22 В, до 20 Вт) со сквозным ваттметром (0,2 мкА…3 А) и аппаратным глитчером, мультиметр, отладчик с трассировкой (Debug & Trace) и аппаратную ретроконсоль. Да, в Марио и Сегу между дебагом тоже хочется!)

Двухканальный осциллограф: 8-битный АЦП HMCAD1511, 2x312–375 MS/s штатно и 2x400 MS/s после подстройки задержек LVDS. Входной тракт 1 МОм с бистабильными реле (делители 1:1/1:10, открытый и закрытый вход), JFET-буферами и щупами x1/x10.
Лабораторный блок питания и сквозной ваттметр: выход 0,8–22 В с шагом 10 мВ и ограничением тока (CV/CC). Отдаёт до 20 Вт от зарядника USB-C PD 3.1 и до 5 Вт от аккумулятора. Считает напряжение, ток, мощность и энергию (20-битный монитор INA228), а при выходе до 5,5 В меряет ток с автодиапазоном 0,2 мкА…3 А — видно и сон микроконтроллера, и пики радиопередатчика. Быстрый усилитель тока INA241 (1,1 МГц) заведён на АЦП ПЛИС, чтобы смотреть форму бросков тока во времени.
Аппаратный глитчер (Fault Injection): силовой ключ («лом»), который по тактам ПЛИС коротит питание цели на землю (расчётный фронт 15–20 нс) для атак сбоем питания.
Логический анализатор на 24 канала: Порт A — 16 входов 0–10 В (целевая стойкость защиты ±30 В, порог компараторов TLV3501 задаётся в диапазоне 0–6,8 В, отдельные входы CK и TRIG). Порт B — 8 двунаправленных линий с перестраиваемыми уровнями 1,1–4,7 В. Буферный захват до 400 MS/s во внутреннюю память ПЛИС и непрерывный поток в ПК по USB 3.0 (50 MS/s на 16 каналах или 100 MS/s на 8) напрямую в PulseView / sigrok.
Генератор сигналов (DDS / AWG): 14-битный ЦАП DAC904 (165 MS/s) с выходным каскадом OPA2673. Выдаёт 9,6 Впп без нагрузки и 4,8 Впп на 50 Ом (требование к защите выхода — встречные ±25 В). На спинке прибора стоят три разъёма SMA (CH1, CH2, GEN) — генератор и оба канала осциллографа работают одновременно, так что АЧХ фильтра (диаграмму Боде) можно снять за один проход.
Мультиметр (DMM): 24-битный АЦП ADS1220 со встроенными источниками тока — напряжение, сопротивление, прозвонка и проверка диодов.
Отладчик Debug & Trace: разъём MIPI-20, CMSIS-DAP v2 (SWD, JTAG) и 4-битный параллельный вход трассировки (trace) для OpenOCD, pyOCD, probe-rs и orbuculum.
Ретроконсоль: аппаратные ядра NES, Sega Mega Drive и SNES на встроенном 3,5-дюймовом IPS-экране (640x480) и полноразмерном HDMI.
Сменный мезонин и начинка: модуль ПЛИС Colorlight i9+ в разъёме SO-DIMM (Artix-7 XC7A50T: 32 600 LUT6, 2,7 Мбит BRAM, 120 DSP, 8 МБ SDRAM x32, 16 МБ флеш), постоянно живущий супервизор WCH CH32H417 (двухъядерный RISC-V до 400 МГц, 896 КБ ОЗУ, USB 3.0 5 Гбит/с и 16-битная параллельная шина UHSIF к ПЛИС), ESP32-C5 (Wi-Fi 6 на 2,4 и 5 ГГц, BLE 5, 802.15.4), опорный генератор TCXO 25 МГц ±0,5 ppm (шкала времени и частотомер), сменная плата-мезонин на разъёме BergStak 2x40 (35 линий ПЛИС под сменные модули расширения), ИК приём и передача, IMU, динамик 1 Вт, микрофон, линейный вибромотор LRA, Qwiic, аккумулятор Li-pol 2800 мА·ч и MCP-сервер для управления прибором через ИИ-агентов.
Звучит как заявка на грант от самого себя. Поэтому сразу скажу как есть: карманного прибора в корпусе пока нет. Собственную шестислойную плату под CH32H417 я сейчас черчу — схема в работе. Зато у меня на столе живёт и работает стенд: модуль Colorlight i9+ с Artix-7 на отладочной плате, рядом ESP32-S3, самодельный джойстик на 11 кнопок, пучок дюпонтов и HDMI-монитор.

На стенде уже работают меню, подгрузка профилей микроконтроллером в ПЛИС прямо на ходу (горячее переключение из кэша за 0,6 с и аварийный возврат обратно), аппаратные Денди (NES) и Sega Mega Drive на 12 игр, а также буферный логический анализатор на 16 каналов с номинальными 400 MS/s, просмотром осциллограмм на мониторе и выгрузкой в PulseView. А 30 октября стенд прошёл первое боевое крещение на публике: я выступил с докладом про Fabbit на конференции пет-проектов Тыкворкинг’26 в МЭИ, там же развернул демо на нетворкинге (КДПВ как раз оттуда!) и все желающие рубились на живой ПЛИС в Mario, Sonic 3, Road Rash 3 и Ultimate Mortal Kombat 3. Исходники открыты на GitHub (код под MIT, железо под CERN-OHL-S-2.0).
Под катом: как я четыре раза перевыбирал архитектуру, как мы с Артёмом Кашкановым расковыряли закрытый драйвер нового микроконтроллера в эмуляторах без самого чипа, почему кадр экрана влез в память, а тактовый сигнал не пролез на ножки, и как проверка логического анализатора на живых проводах привела к детективу с байтом 0xFC.
Фух, краткую версию рассказал, картинки показал — быстрый дофамин получен! Теперь поподробнее: как я вообще до этого докатился?
❯ 1. Вообще-то нам просто задали проект
В десятом классе нам нужно выполнить индивидуальный проект. Делать нечего — пошёл придумывать тему. Желание сразу одно: железки. До угара железок! И ПЛИСятин =) Опыта разработки сложных проектов у меня при этом не было, но воображению это ни капельки не мешало)
Пнул Ярика — лучшего друга и одноклассника: давай, мол, вместе, так веселее. Ярик взял на себя менеджмент и часть 3D-моделирования корпуса, а я засел за всю электронику, архитектуру, Verilog и прошивки; параллельно искал нам и проекту выход в «люди».
У электронщика на столе частенько зоопарк: логический анализатор, USB-UART, программатор, отдельный блок питания, мультиметр, а если повезёт, ещё и осциллограф. Каждый со своим проводом, своей программой и своим геморроем. Хочется одну коробочку, которая перестраивается под задачу. А раз перестраивается, то пусть уж и в Марио умеет — отдыхать же надо!)
Двадцатого сентября в репозитории появился первый коммит. В README тогда было ровно две честные строчки: «Кода нет, платы нет». Зато уже лежал набросок корпуса в Blender. Приоритеты, ага.
❯ 2. Что Fabbit должен уметь и как он устроен
Начнём с главного понятия. Профиль — это аппаратное назначение прибора: битстрим ПЛИС, данные к нему и поведение в меню. Когда ты выбираешь профиль, микроконтроллер-супервизор прямо на ходу заливает нужный образ в ПЛИС и переключает кристалл в новую схему. При этом одна карточка в меню может объединять несколько битстримов: для «Ретроконсоли» я решил держать по отдельному полному образу ПЛИС на каждую приставку, и микроконтроллер сам подменяет их на лету при выборе игры.

Сценарий, ради которого всё затевалось: подключаю щупы к отлаживаемой плате, подаю на неё питание с ограничением тока, смотрю потребление, тем же прибором останавливаю микроконтроллер через SWD, ловлю событие на шине SPI и смотрю дорожки на экране прибора или выгружаю в PulseView. Без пачки отдельных коробочек в рюкзаке.
Чтобы уместить всю начинку в корпус 90x150x22,5 мм (над аналоговым модулем 24 мм), я разложил главную плату на две стороны. Спереди стоят экран, кнопки и динамик; сзади под крышкой — модуль ПЛИС в сокете SO-DIMM, плоский аккумулятор 904070 и сменный модуль-мезонин на скоростном разъёме BergStak 2x40. Мезонин сделан съёмным специально ради модульности: на его разъём выведены 35 быстрых линий ПЛИС и питание, так что простой перестановкой платки мезонина в прибор можно добавлять совершенно новые аппаратные функции (штатно туда ставится модуль осциллографа, генератора и мультиметра, а при желании можно воткнуть свою плату под SDR, специфичные интерфейсы или автоэлектронику).

Профиль | Что делает | Главное требование к платформе | Статус |
|---|---|---|---|
Логический анализатор | захват 16 линий 0–10 В (A) и 8 линий 1,1–4,7 В (B) | быстрые входы, память, точный триггер, USB 3.0 | на стенде 16x400 MS/s в буфер, с оговорками (глава 6) |
Ретроконсоль | NES, Sega Mega Drive, позже SNES | память SDRAM x32, видео, ввод | NES и Mega Drive работают на стенде, SNES в симуляции и сборках |
Демо-сигналы | меандры 10/100 кГц, ШИМ, UART, SPI с ESP | безопасные выводы и уровни | на стенде |
Отладчик Debug & Trace | CMSIS-DAP v2: SWD/JTAG, trace 4 бит | двунаправленный порт, быстрый разворот, MIPI-20 | в проекте целевой платы |
Осциллограф, генератор, мультиметр | 2x400 MS/s (HMCAD1511), ЦАП DAC904, DMM ADS1220 | сменный модуль-мезонин (BergStak 2x40), 10 пар LVDS, 3xSMA | в проекте целевой платы (выбран BOM и механика) |
Лабораторный БП и глитчер | 0,8–22 В (до 20 Вт), ток 0,2 мкА…3 А, ключ «лом» | USB-C PD 3.1, двунаправленные ключи от 0 В | в проекте целевой платы (схема в работе) |
❯ 3. Зачем тут ПЛИС и почему она не справится одна
Анализатору нужно смотреть на 16 входов сотни миллионов раз в секунду и сравнивать все биты с условием триггера одновременно. Ретроконсоли нужно потактово повторить железо старой приставки, где процессор, видеочип и звук работают параллельно. Осциллографу — принять поток с быстрого АЦП по дифференциальным парам LVDS. Микроконтроллер исполняет инструкции по очереди, а ПЛИС сама становится нужной цифровой схемой.
Но пока ПЛИС перезагружается в другой профиль, её логики не существует. Если меню, файлы и USB живут внутри неё, при каждой смене профиля прибор на секунду отваливается от компьютера и слепнет, а битый образ на карте памяти превращает его в кирпич.
Поэтому я разделил систему на две части: рядом с ПЛИС всегда работает микроконтроллер-супервизор. Он держит соединение по USB, читает microSD, управляет питанием, рисует меню, прямо на ходу заливает новые битстримы в ПЛИС (в кэш слотов флеша или с мгновенным переключением активного слота) и реанимирует систему после любого сбоя. А ПЛИС молотит быстрые параллельные потоки.

Код (отчасти), расчёты по даташитам и тесты я пишу вместе с ИИ-агентами: так быстрее, и это часть идеологии проекта — свои профили потом сможет собирать любой. Но решения, паяльник, провода и ответственность за то, что прошито во флеш, остаются на мне.
По бумагам все шикарно! Давайте к проверке реальностью.
❯ 4. Как менялась архитектура
4.1. Модуль ПЛИС: от Tang Nano до Artix-7 в разъёме памяти
Первым кандидатом был Tang Nano 20K: дешёвый, открытый тулчейн, много примеров. Но свободных ножек мало, а запаса по логике хотелось больше. Паять голый чип в каком-нибудь BGA-484 и разводить вокруг него память на своей первой плате — верный способ закопать себя с проектом.
Решение нашлось у китайских производителей контроллеров для светодиодных видеостен. Сначала мне приглянулся модуль Colorlight i9 на Lattice ECP5 — платка в стандартном ноутбучном разъёме SO-DIMM на 200 контактов, под который легко развести свою материнку. И почти сразу я увидел его старшего брата Colorlight i9+ с Xilinx Artix-7 XC7A50T (32 600 LUT6, 2,7 Мбит BRAM, 120 DSP, рядом SDRAM 8 МБ x32 и флеш 16 МБ) в том же форм-факторе всего за 2500 рублей на Али:

Первый заказ продавец отменил, сославшись на проблемную партию памяти (объяснение меня не убедило — товарищ явно путался в показаниях XD). Перезаказал у другого, заплатил неделей ожидания. На самом модуле, кстати, ещё стоят два гигабитных Ethernet PHY Broadcom — для карманного прибора они лишние, поэтому на стенде и в целевой плате я держу их общий пин сброса (шар H2) в нуле, экономя 25 мВт.
Готовой отладочной платы специально под i9+ я не нашёл, взял плату расширения от i5/i9. Нюанс вскрылся: у i9+ выводы JTAG (TCK, TMS, TDI, TDO) не выведены на разъём SO-DIMM, а сидят на четырёх сквозных площадках J2–J5 в углу модуля (у i9+ они находятся слева, а у i5/i9 — снизу). Припаялся, подключил — и 10 октября прочитался IDCODE 0x0362C093, а следом замигала первая мигалка:

4.2. Супервизор: ESP32-S3 → CH569W → CH32H417 и допрос блоба в эмуляторе
Начинал я со связки ESP32-S3 N16R8 (меню, SD-карта, питание и кнопки) плюс отдельный мост FT232H и USB-хаб GL850G для скоростного потока анализатора в ПК. Первые грабли прилетели из даташита модуля N16R8: линии GPIO33–37 внутри модуля заняты восьмибитной памятью Octal PSRAM. Из-за этого связь с ПЛИС пришлось урезать с 8 бит до 4-битного Quad SPI.
Потом откликнулся Артём Кашканов (@radiolok) и предложил добавить отладчик с трассировкой (Debug & Trace). Одной новой строчкой в меню это не закончилось. Во-первых, входы анализатора с делителями и компараторами умеют только слушать, а отладчику по SWD/JTAG нужно быстро разворачивать линию на передачу. Единый порт я разделил на два: Порт A для анализатора (16 входов 0–10 В, целевая защита ±30 В) и Порт B с восемью двунаправленными линиями 1,1–4,7 В, плюс разъём MIPI-20. Во-вторых, потоковой трассировке и 16-канальному ЛА понадобился быстрый канал в ПК — USB 3.0.
Артём посоветовал посмотреть на WCH CH569W: один чип со встроенным USB 3.0 и параллельной шиной HSPI выкидывал из схемы ESP32, FT232H и USB-хаб. Я почти переехал на него. Но при детальном разборе у CH569 обнаружилось всего 128 КБ ОЗУ, отсутствие АЦП и I²C (понадобились бы три расширителя портов и I²C-мультиплексор), дефицит GPIO, конфликт 16-битной шины с контроллером SD-карты и ноль штук на складе LCSC.
Мне требовался чип, который совмещает аппаратный USB 3.0 и быструю параллельную шину к ПЛИС с большой памятью и богатой периферией. Раскуривая документацию WCH и мучая Клода, я вышел на свежий двухъядерный CH32H417QEU6: 896 КБ ОЗУ, 95 GPIO, АЦП, четыре I²C, USB 3.0 SuperSpeed и 16-битная параллельная шина UHSIF к ПЛИС. Минусы тоже честные: молодой SDK, часть драйвера шины в закрытой библиотеке libUHSIF.a и корпус QFN-128 с шагом 0,35 мм (ой, ну и гемор будет ручками паять!).

Платы с H417 на руках тогда не было, а узнать, какие ножки в реальности захватывает закрытая библиотека libUHSIF.a, было необходимо до рисования схемы. Поэтому 17 октября я устроил блобу допрос без железа: написал на Python небольшой эмулятор RISC-V, скормил ему скомпилированный код инициализации и прогнал все 144 комбинации параметров UHSIF_GPIO_Init(), записывая каждое обращение к регистрам GPIO и мультиплексора выводов.
Так ещё до разводки платы я узнал точный список из 28 ножек шины под ПЛИС и поймал реальный конфликт: сам блоб чужие пины не трогает, а вот штатная функция SD_GPIO_Init() из открытой части SDK молча перетирает линии данных шины PC8/PC9 (для SD-карты напишу свою короткую инициализацию). Мы обсудили этот закрытый блоб с глубокоуважаемым Артёмом Кашкановым, и он решил помочь — буквально на следующий день нырнул в эту задачу в своём проекте rln-trace! Артём прогнал всю библиотеку под трассировкой в эмуляторе Spike/Unicorn, раскопал внутри таблиц настройки настоящий «микрокод» конечного автомата диаграмм (очень похожий на GPIF II из Cypress FX3, который приходится вытаскивать из блоба как есть) и написал открытый драйвер fw/uhsif — он собирается обычным GCC без кастомных инструкций WCH и в CI до единого обращения к регистрам сверяется с оригинальной библиотекой!
Слинковал
libUHSIF.aштатным компилятором xPack GCC 15.2 и разобрал дизассемблером. Внутри нашлись 14 проприетарных 16-битных инструкций расширенияXW(c.lbu/c.sb— сжатые байтовые загрузки/сохранения WCH) и требование включать аппаратный стек прерыванийHPE.Мой питоновский эмулятор (
emu.py+runall.py) исполняет код инициализации по дизассемблированному ELF-файлу и перехватывает все обращения к периферии выше0x40000000(GPIO, тактирование и AFIO): прогон всех 144 комбинаций подтвердил, что спорные выводыPF12,PF13,PE7,PE11,PE15библиотека не трогает ни в одном из режимов.Из отладочной информации DWARF и трассы эмулятора я вытащил карту 76 регистров UHSIF (
0x40038000–0x4003812C): их имена и смещения совпали с регистровой картой GPIF II из Cypress FX3.Когда мы обсудили эту находку с Артёмом, он в
rln-trace/doc/ch32h417-uhsif.mdразобрал работу DMA-сокетов (0x40039000), двух банков программы конечного автомата (0x4003A000 / 0x4003B000) и поднял открытый C-драйвер с автоматической проверкой эквивалентности трасс в эмуляторе (12 сценариев, ~12 тыс. обращений). Поведение на реальном кремнии под потоком DMA проверим, когда приедет плата.
Вот тут вопрос к вам. CH569W: проверенный многими проектами, но тесный, часть функций пришлось бы выносить на дополнительные корпуса. CH32H417: всё в одном кристалле с 896 КБ памяти, но свежий, с закрытым кусочком драйвера и шагом выводов 0,35 мм. Бюджет околонулевой, плата ручной сборки, ошибка в выборе стоит месяца. Что выбрали бы вы?
Я выбрал H417, оставив CH569 запасным вариантом. Девборду с ним нам заказал Артём. А стенд на ESP32-S3 никуда не делся: на нём живёт всё, что вы увидите в пятой и шестой главах.
4.3. Экран: кадр в память влезает, а pixel clock — нет
Мне очень хотелось, чтобы в меню ПЛИС спала выключенной, а экран вёл микроконтроллер: экономия батареи и красивая архитектура. У H417 есть встроенный контроллер дисплея (LTDC), а банки памяти DTCM (256 КиБ) и основная SRAM (512 КиБ) идут в адресном пространстве подряд — так что полный кадр 640x480 в RGB565 (600 КиБ) целиком помещается в ОЗУ. Казалось бы, готово!
Но потом начинаешь раскладывать альтернативные функции ножек… У LTDC ровно четыре возможных вывода тактового сигнала пикселей LTDC_CLK: PF1, PB14, PE14, PA15. При включённой 16-битной шине UHSIF, SD-карте и USB заняты все четыре (DATA7, CS#, WRNF, DATA15), да ещё и конфликтуют линии цвета G4 и B6:
Можно было бы урезать шину к ПЛИС с 16 до 8 бит, но тогда вдвое падает пропускная способность анализатора. Ножек 95, а нужной функции не хватает одной!
Тогда я посчитал в Vivado и померил на живом модуле тестовый образ «монитора меню» без умножителя частоты MMCM (развёртка и SDRAM напрямую от генератора 25 МГц): сколько вообще съедает ПЛИС в таком режиме? Оказалось — 0,56 Вт на весь модуль. За пару минут, которые пользователь проводит в меню за сессию, это около 0,2 % ёмкости батареи.
Решение: экран всегда подключён к ПЛИС. Микроконтроллер рисует меню в свой буфер (я написал лёгкий UI-движок на C11: 4 бита на пиксель, 150 КиБ на кадр, пересылка только изменённых прямоугольников dirty rectangles), ПЛИС выводит картинку на IPS-экран и HDMI, а когда МК на ходу подгружает в ПЛИС боевой профиль, экраном без задержек начинает владеть уже сама схема профиля.
4.4. Лабораторный источник и глитчер: один тракт для трёх задач
В проекте целевой платы от силовых пинов питания (P1 и P2 на верхнем торце) я заложил сразу три режима:
Выдавать регулируемые 0,8–22 В с ограничением тока (преобразователь TPS55289) и мерить потребление.
Работать сквозным измерителем: когда внутренний БП выключен, на
P1подаётся внешнее питание до 20 В / 3 А, сP2уходит в нагрузку, а шунт посередине считает ток, мощность и энергию.Работать глитчером: по команде ПЛИС коротить выход
P2на землю на десятки наносекунд.
Первый вариант схемы с электронным предохранителем TPS25947 отпал ещё при сверке даташита: он не работает ниже 2,7 В (а мне нужно от 0,8 В!) и запирает обратный ток. Тогда я разделил коммутацию и ударный ключ:

Направление тока переключают встречно-включённые N-MOSFET PXN012-60QL под управлением фотовольтаических оптодрайверов VOM1271 — они держат от 0 до 20 В в обе стороны, но открываются за 1,3–3,2 мс. А прямо на выходе P2 стоит отдельный скоростной ключ AO3400A с драйвером UCC27517: когда нужен сбой питания, ПЛИС бьёт в его затвор (расчётный фронт просадки — 15–20 нс!).
4.5. Аналоговый мезонин: модульность, 1 МОм и сопромат крышки
Аналоговую часть (быстрый АЦП HMCAD1511, ЦАП DAC904, точный ADS1220, реле, делители) я сначала хотел держать на отдельном мезонине, потом перенёс на основную плату ради коротких LVDS-связей, а затем вернул обратно на сменный модуль. Довод «LVDS через межплатный разъём не пройдёт» не подтвердился (разъём BergStak 0,8 мм держит до 12 Гбит/с!). Зато тащить через разъём высокоомную точку 1 МОм после входного делителя было бы фатальной ошибкой из-за утечек и наводок.
И главное — сменный мезонин даёт прибору настоящую аппаратную модульность. На 80-контактный разъём выведены 35 линий ПЛИС (включая дифференциальные пары LVDS) и питание: вынимаешь аналоговый мезонин с осциллографом и ставишь на его место другую платку под новую задачу, вообще не переделывая шестислойную материнку. А под капотом самого мезонина стоит собственная EEPROM с описанием модуля, чтобы супервизор при включении сразу понимал, какое железо сейчас пристёгнуто, и предлагал нужные профили ПЛИС.
На спинке модуля встали три разъёма SMA (на BNC в карманном корпусе места нет). Расплатились задней крышкой: когда мезонин вырос до 73x37,3 мм, верхний край крышки повис в воздухе, и в прочностном расчёте (CalculiX FEM во FreeCAD, нагрузка 50 Н) пластик прогнулся на 1,24 мм прямо до деталей. Простое утолщение стенки не спасало, а вот замковое соединение шип-паз, опёртое на поясок корпуса, снизило расчётный прогиб спинки до 0,19–0,20 мм при безопасном зазоре 0,4 мм!

К этому моменту выбирать платформу я уже немного научился. Хотелось бы теперь ещё перестать… её перевыбирать.
❯ 5. Что я уже сделал на стенде
Возвращаемся с небес к макетке на столе.


Меню → загрузка битстрима на ходу → возврат
Как работает смена прибора на лету? Включаем питание: с нулевого адреса 16-мегабайтной флешки ПЛИС стартует аппаратно защищённый от стирания образ golden, микроконтроллер ESP32-S3 рисует меню (640x480, 4 бита на пиксель), а ПЛИС выводит его на HDMI.
Дальше ты выбираешь в меню нужный прибор или игру — и микроконтроллер прямо на ходу заливает и переключает битстрим в ПЛИС: сверяет CRC образа в кэше из пяти слотов флеша, если такого профиля в кэше ещё нет — тут же по шине QSPI дописывает .fbp-образ в свободный слот и подаёт команду IPROG. ПЛИС через внутренний блок ICAPE2 за доли секунды пересобирает свою логику из указанного слота, а при выходе из профиля (или если образ вдруг оказался битым и сработал сторожевой таймер) так же на лету возвращается обратно в golden-меню!
0x000000 golden, 4 МиБ, защищён аппаратно битами BP=7, TB=1
0x400000 слот 0 ┐
0x640000 слот 1 │
0x880000 слот 2 ├─ кэш на 5 слотов по 0x240000 = 2,25 МиБ
0xAC0000 слот 3 │
0xD00000 слот 4 ┘
0xFF0000 таблица слотов "FBST" (CRC32, длина, счётчик LRU)Цифры с реального стенда:
Операция | Время |
|---|---|
Запись микроконтроллером нового профиля в слот на ходу, 2,2 МБ | 20,8 с (из них шина QSPI 4,6 МБ/с занимает ~0,5 с, остальное — запись самой флеш-памяти) |
Горячее переключение профиля из кэша (CRC слота + реконфигурация ПЛИС) | 0,60–0,66 с (385 мс проверка CRC + 279 мс старт ПЛИС) |
Аварийный возврат с пустого или битого слота по сторожевому таймеру | 1,3 с |

Три бага загрузчика за один день
Пятнадцатого октября загрузчик заработал целиком. Немного тамошних багов:
1. Профиль стартовал 7 секунд. Открытый тулчейн openXC7 пишет в заголовок битстрима регистр COR0 с полем OSCFSEL = 0: ПЛИС при перезагрузке читает свой флеш на частоте ~3 МГц. Мой упаковщик образов fbp_pack.py находит в потоке запись 0x30012001 и вписывает туда частоту 33 МГц, как у Vivado с CONFIGRATE 33. Старт ускорился до 0,56 с — в 12 раз быстрее! А, ну и подвох: битстрим, собранный в самом Vivado, так патчить нельзя — у него в конце потока стоит аппаратная проверка CRC (0x30000001), и после правки одного бита ПЛИС упадёт с CRC Error.
2. ICAP читал мусор. Чтобы golden понимал причину аварийного отката, он читает регистр BOOTSTS через примитив ICAPE2. В ответ всегда возвращалось 0xFFFFFFDB — слово состояния самого интерфейса ICAP (CFGERR_B, DALIGN, IN_ABORT_B), а не данные регистра. Я добавил отладочную команду icapdbg, которая выплёвывает 16 слов выхода ICAP подряд после CSIB = 0. Выяснилось, что данные регистра живут на выходе ровно на 4–5-м такте, а мой автомат забирал 10-й! Поменял одну константу:
// данные регистра на выходе ICAP только на 4-5 такте после CSIB=0,
// до и после - слово состояния (FFFFFFDB). Проверено на железе 2026-10-15
if (step == 4'd5) rdata <= bitswap(dout);Сразу прочитались STAT 0x461079FC, IDCODE 0x0362C093, а после отката с пустого слота — BOOTSTS 0x00000D03 (VALID, FALLBACK). В сборках openXC7 примитив ICAPE2 у меня так и не заработал (регистры читаются нулями, IPROG ничего не делает), поэтому golden и профили с возвратом в меню я собираю в Vivado.
3. Такт SCK не выходил на ножку ESP32-S3. В драйвере SPI фреймворка ESP-IDF флаги режимов определены вложенными битами (OCTAL включает биты QUAD), а при настройке мультиплексора выводов (spi_common.c) программист написал проверку if (flags & SPICOMMON_BUSFLAG_OCTAL) без ==:
bus_iomux_pins_set_quad(host, bus_config);
if (flags & SPICOMMON_BUSFLAG_OCTAL) { // true уже при одном флаге QUAD!
bus_iomux_pins_set_oct(host, bus_config);
}Просишь QUAD — драйвер включает 4-битный режим и тут же следующей строкой перетирает SCK и IO0–3 восьмибитной функцией IO_MUX, отключая тактовый сигнал от пина! Строкой выше, в проверке ножек, тот же флаг сравнивали правильно: (flags & OCTAL) == OCTAL. Решение? — Не передавать флаг QUAD — драйвер сам включает 4-битный режим по наличию пинов WP/HD.
Гребёнка P3: из 7 линий ESP↔ПЛИС сначала доходили только 2. Перепаял, прозвонил тестовым битстримом
wiretest_p3. Пин прерыванияIRQнаGPIO46мешал прошивке ESP (strapping-пин), переехал наGPIO5.Мусор при перезагрузке ESP: открытие COM-порта на ПК перезагружает ESP32-S3, и болтанка на шине уводила
goldenвBOOT. Теперь опасные команды (ERASE,PROG,BOOT,TUNE,LA_ARM) требуют 4-байтный ключFBKYв кадре.HDMI на одиночных выводах: на отладочной плате разъём HDMI разведён на восемь одиночных пинов
LVCMOS33, а не на дифференциальные пары. Собрал псевдодифференциальный TMDS с программной инверсией N-линий иODDR5x в режиме 640x480. Декодер в Verilator сверил все 1,5 млн пикселей кадра. Но мой монитор Samsung C32R50 при стандартном токе драйвераDRIVE = 8 мАпоказывал чёрный экран (притом без плашки «Нет сигнала», то есть сигнал был), а при снижении тока до4 мАсразу выдал чистую картинку!Кнопки: первую раскладку посадил на
GPIO35–37, а у модуля N16R8 они заняты Octal PSRAM — ESP ушёл в цикл перезагрузок. Перенёс на свободные пины.
Ретроконсоль: почему три приставки не поместились в один образ
Зачем в приборе электронщика приставка? Все и так знают =) А кроме отдыха это лучший комплексный тест платформы: память SDRAM, видео, пересечение тактовых доменов и кнопки работают одновременно, и любая ошибка видна сразу глазами.
Двадцать четвёртого октября в журнале появилась строчка: «Завёл HDMI. Портировал NES и собрал самодельный джойстик. Super Mario Bros. 3 работает!» Ядро NES — форк NESTang, вокруг него моя обвязка: такты, контроллер SDRAM (окно достоверного чтения 13,3 из 15,56 нс периода на 64,29 МГц), загрузчик ROM с флеша ESP, кнопки и скейлер видео.
Следом я интегрировал в проект ядра Mega Drive (MDTang) и SNES (SNESTang) и попробовал собрать все три консоли в один монолитный битстрим. Выяснилось, что неактивные ядра в сбросе всё равно занимают свои таблицы LUT и память: объединённая сборка съела 99,61 % слайсов Artix-7 (8 118 из 8 150) и провалила статический временной анализ с запасом WNS = −7,000 нс.
И тут во всей красе пригодилась наша подгрузка битстримов на ходу! Я разделил приставки во времени: в меню одна карточка «Ретро» с общим списком игр, а под каждую консоль в слотах флеша лежит свой отдельный битстрим (retro-nes.fbp, retro-md.fbp). Выбираешь в списке Sonic 3 — микроконтроллер видит расширение картриджа, за 0,6 секунды переключает ПЛИС в схему Сеги и тут же заливает в SDRAM саму игру! В отдельном образе Mega Drive занял всего 37 % LUT, прошёл тайминги с запасом +1,246 нс, и 29 октября — прямо накануне нашего выступления на Тыкворкинг’26 в МЭИ — Сега ожила на стенде! На конференцию мы привезли уже 12 игр для Денди и Сеги (Super Mario Bros. 3, Darkwing Duck, Crystalis, Sonic 3, Road Rash 3, Rock n' Roll Racing, Ultimate Mortal Kombat 3), и весь вечер стенд без единого зависания отработал в руках гостей. А у SNES я сейчас дожимаю расписание контроллера памяти — историю портирования всех трёх консолей расскажу отдельно, когда доведу третью приставку до экрана, там материала на хорошую статью!

❯ 6. Логический анализатор: проверка дошла до проводов
Первый анализатор в проекте появился раньше самого профиля и не от хорошей жизни. Пятнадцатого октября шина ESP↔ПЛИС стабильно рвала кадр чтения на 42-м байте. Осциллографа у меня нет, внешнего ЛА тоже. Зато на столе лежит ПЛИС — а внутри ПЛИС можно собрать прибор, чтобы посмотреть на саму ПЛИС!)
Добавил в golden маленький модуль la_capture: 8 внутренних линий шины, запись 4096 выборок с шагом 10 нс от спада CS# и выгрузка hex-строкой в UART:
Armed: if (p2[1] && !p1[1]) state <= Cap; // спад CS# запускает запись
Cap: begin wa <= wa + 12'd1;
// 4096 выборок по 10 нс (100 МГц)
if (wa == 12'(Depth - 1)) state <= Head;
// дальше выгрузка "L<hex>" в UART
endПитоновский скрипт на ПК разложил дамп на фронты, и первый же захват показал, на чём обрывается чтение: посреди кадра на линии CS# проскакивала единица длительностью ровно в одну выборку (шаг сетки 10 нс), и ведомый приёмник честно считал это концом кадра! Предположил наводку на макетке и добавил в qspi_slave.sv цифровой фильтр:
// кадр кончается, только если CS#=1 держится 4 такта подряд (40 нс)
end else if (cs_hi != 3'd4) cs_hi <= cs_hi + 3'd1;
else active <= 1'b0;
// такт SCK принимается, когда две выборки подряд совпадают
if (sck_s[1] == sck_s[2]) sck_q <= sck_s[1];После этого 500 пакетов по 8 КБ туда и обратно на 10 МГц и 200 на 5 МГц прошли с нулём ошибок (4,6 МБ/с). А через десять дней, 25 октября, из этой идеи за один день вырос уже отдельный полноценный профиль анализатора на 16 каналов и 400 MS/s.
Зачем ему своя память и откуда 400 MS/s при логике на 100 МГц
На стенде анализатор буферный: он быстро пишет окно в блочную память ПЛИС (64 блока RAMB36, или 256 КиБ), останавливается по триггеру, рисует дорожки на мониторе и отдаёт запись в компьютер для PulseView. В диагностическом режиме полное окно 256 КиБ уезжает в ПК через текстовую консоль ESP в hex за 13 секунд (~20 кБ/с)... Ну, зато глазами видно! Причём после остановки память замораживается и читается по адресам, а не высыпается из одноразового FIFO: если передача оборвалась, захват можно выгрузить повторно, сверив CRC32 каждого блока и номер эпохи захвата.

Чтобы не разгонять всю логику кольцевого буфера и триггеров до 400 МГц, входной сэмплер отделён от ядра. Я прошёл три ступени:
100 MS/s (
IOB): один отсчёт за такт 100 МГц.200 MS/s (
IDDR): входной примитив ловит данные по обоим фронтам такта 100 МГц и отдаёт по 2 отсчёта за такт.400 MS/s (
ISERDESE2): аппаратный последовательно-параллельный преобразователь прямо на каждой входной ножке ПЛИС. Он принимает данные по обоим фронтам такта 200 МГц (номинальный шаг 2,5 нс) и выдаёт внутрь ПЛИС пачку из 4 отсчётов на спокойном такте 100 МГц!

Шестнадцать каналов в два байта на 400 MS/s — это поток 800 МБ/с внутри ПЛИС. Буфер на 131 068 отсчётов заполняется за 327,67 мкс: до события память крутится кольцом (сохраняя предысторию), а после триггера дописывает заданную часть окна и останавливается. С экрана по умолчанию включается децимация до 10 MS/s — куда больше на этих макарошках из проводов?

Почему 786 408 безошибочных отсчётов не проверили вход
Сначала я проверил цифровую часть в симуляторе на официальных моделях AMD (UNISIM для IDDR и шифрованной SecureIP в Vivado Simulator для ISERDESE2). Затем прошил профиль на стенд, подал на захват внутренний тестовый счётчик и снял 6 полных буферов на 400 MS/s и 17 угловых сценариев: 786 408 отсчётов, ноль ошибок!
Красота? Только внутренний счётчик в этой схеме стоял после входного сэмплера:

Чтобы проверить настоящий входной тракт, понадобилась физическая петля наружу. На соседней гребёнке P6 (банк 35) я поднял 8-канальный тестовый генератор (la_loopback.sv): статика, двоичный счётчик, бегущие единица и ноль, псевдослучайная последовательность PRBS-8. Внутри ПЛИС генератор к захвату не подключён — сигнал обязан выйти через физическую ножку P6, пройти по дюпонт-перемычке, войти в ножку P2 и оцифроваться в ISERDESE2. По умолчанию все выходы генератора заперты в Hi-Z и включаются только записью с 16-битным ключом 0x4C47.
Восемь выходов генератора, шестнадцать входов анализатора — значит, проверяем двумя группами по 8 перемычек (сначала D0–D7, затем переставляем провода на D8–D15 при выключенном питании). Сначала статика (проверка карты проводов), потом динамика.
На верхней восьмёрке статика тут же поймала ошибку монтажа: подаю единицу на выход T5 (жду D13), а приходит D15; подаю T7 — приходит D13.

Переставил местами два провода на P2, повторил тест — карта сошлась. Ну где моя внимательность?
Как в захвате появилось число 0xFC
Запускаю двоичный счётчик: 1 миллион состояний в секунду, захват на 400 MS/s. Каждое значение счётчика должно держаться ровно 400 отсчётов и сразу сменяться следующим. Но в нижней группе строгая проверка нашла 10 промежуточных слов, каждое из которых попало ровно в одну выборку (при номинальном шаге 2,5 нс):
0xDF 1101 1111 <- старое значение (400 отсчётов)
0xFC 1111 1100 <- одна выборка на переходе (шаг 2,5 нс)!
0xE0 1110 0000 <- новое значение (400 отсчётов)Откуда взялось 0xFC между 0xDF и 0xE0? На этом переходе счётчик меняет сразу шесть битов из восьми. Снаружи ПЛИС это восемь отдельных физических проводов: в момент выборки бит 5 уже поднялся в единицу, биты 0 и 1 уже упали в ноль, а биты 2, 3 и 4 ещё задержались в старой единице!

Снизил частоту счётчика в десять раз (до 100 тысяч состояний в секунду) — промежуточное слово осталось на месте. Менять значения реже и делать сам физический фронт одновременным — разные вещи! При этом псевдослучайная последовательность PRBS-8 во всех прогонах прошла с нулём ошибок.
Заодно на верхней группе упал сам питоновский скрипт анализа: он проверял бегущую единицу, искал каждое прочитанное число в списке из 16 ожидаемых кодов и споткнулся о промежуточный байт 0x21 (ValueError: 33 is not in list). Починил анализатор так, чтобы неожиданный код записывался как провал строгой проверки, и прогнал те же сохранённые RAW-файлы заново без перезахвата. Отрицательный результат остался в отчёте (TRANSITION SKEW IS REPORTED, NOT HIDDEN).
Через три дня я вернулся к сырым данным
Ежели все RAW дампы сохранились (суммарно 1 126 414 отсчётов по обеим группам), я написал для статьи скрипт и посчитал прямо по этим файлам, при каких условиях выскакивают промежуточные байты:
Все 33 промежуточных отсчёта случились на переходах, где одновременно падают в ноль 5 и более линий. Таких переходов во всех записях было 49, и в 33 из них (67 %) поймалось переходное слово. А среди ~3 370 переходов, где одновременно падают 4 линии или меньше, промежуточных слов ровно ноль! Это хорошо согласуется с тем, что PRBS-8 в моих прогонах оказался чистым: в нём почти не бывает пяти одновременных спадов.
Опаздывают только спады (
1 → 0). Новые единицы в промежуточном слове всегда уже стоят на месте.Задержка переехала вместе с выходом генератора. Чаще всех опаздывал выход
T4(пинP6-7, шарM1): во всех 17 случаях нижней группы (где он шёл наD4) и в 10 из 16 случаев верхней (где он шёл наD12). При переходе на верхнюю восьмёрку я переставлял провода только со стороны входовP2, а наP6они оставались на тех же ножках.


Рабочая гипотеза — эффект одновременного переключения слабых выходных буферов (SSO / Ground Bounce): 5–8 драйверов в режиме DRIVE = 4 мА, SLEW = SLOW разом тянут линии вниз, возвратный ток бежит через индуктивность земляных дюпонтов, и самые нагруженные линии пересекают порог нуля чуть позже соседей. Но строго отделить выходную площадку от провода и от входного сэмплера по этим данным нельзя — если у вас есть красивая идея, как чисто разделить их вклад на стенде без гигагерцового осциллографа, напишите в комментариях, с удовольствием проверю!)
С проводами на этот вечер я разобрался (а заодно вывел дорожки ЛА на HDMI-экран, починил шрифты и сделал безопасный демо-режим с генерацией UART/SPI/PWM с ножек ESP). Теперь вопрос попроще: откуда у двух школьников вообще взялись все эти железки?
❯ 7. Где мы взяли ресурсы и что изменило сообщество
На старте у нас двоих в карманах было не больше тысяч 15ти. Скинулись, заказали модуль ПЛИС, отладочную плату, экран, ESP32-S3 и мелочь для макета (экран, кстати, 4 октября я уже пробовал включать отдельно и спалил платку повышайки подсветки — на нынешнем стенде всё идёт через HDMI).
Дальше нужны были компоненты, паяльное оборудование и опытные люди, которые посмотрят на мои схемы. Школа помогла доступом к 3D-принтерам и оборудованию, а вот бюджета на такие проекты там нет...
Третьего октября я написал админу сообщества FPGA Systems в Telegram: можно опубликовать пост? Так и так, два десятиклассника делают открытый прибор на ПЛИС, ищем инженерную помощь и спонсоров. После публикации начали откликаться люди:
Андрей в тот же день оплатил наш первый счёт на компоненты в ЧипДипе.
crytech7 поддержала проект крупным донатом и предложила помощь с поставками деталей.
Благодаря переводу Виктора Белова у меня появилось нормальное домашнее оборудование: паяльная станция YIHUA 982, фен 858D, мультиметр UNI-T UT61E+, дымоуловитель и расходники.
На 30 октября публичный счётчик на сайте показывал 44 011 ₽ поступлений и 25 053 ₽ расходов: 21 042 ₽ ушло на инструмент и расходники, 4 011 ₽ — на первые компоненты, а остаток в копилке отложен под комплектацию первой печатной платы. Каждая покупка и чек открыто выложены на fabbit.dev/spent.

Мультиметр UT61E+ из этого списка уже отработал своё: 17 октября я снимал им потребление модуля i9+ на разных битстримах, чтобы оценить автономность от батареи. И первый замер вышел раз в десять меньше правды (1–13 мА)! Оказалось, тактовая кнопка питания S1 на отладочной плате нормально замкнутая и в покое шунтировала щупы амперметра. Зажал кнопку пальцем — ток стал честными 107–337 мА (0,52–1,64 Вт). Заодно я сравнил прирост входной мощности модуля от простоя к нагрузке (+0,60 и +1,12 Вт на 25 % и 50 % ПЛИС) с расчётной прибавкой самого кристалла в модели Vivado (+0,51 и +0,92 Вт). Отношение вышло около ~1,2x (с линейным стабилизатором от 5 В было бы 3–5x) — так по наклону графика я косвенно оценил, что под радиатором ядро 1,0 В питает импульсный buck-стабилизатор с КПД в районе 80–85 %.
Откликнулся и уже не раз упомянутый здесь глубокоуважаемый Артём Кашканов (@radiolok) — да-да, без него в этой статье не обошлась почти ни одна глава!) Если человек собирает безумные компьютеры из реле и радиоламп, наш школьный проект его, видимо, уже не пугает) Артём обсуждал со мной архитектуру, подкинул идею отладчика с трассировкой и USB 3.0, взялся помочь с реверсом закрытого блоба UHSIF после нашего обсуждения и заказал нам девборду CH32H417.
Поддержал нас и завод печатных плат «Резонит», согласившись помочь с изготовлением плат для прототипа. Под целевую 6-слойную плату (1,5 мм, ENIG, контроль импеданса ±10 %) я сразу сверил их технологические нормы с корпусом QFN-128 (шаг 0,35 мм) и выяснил важную деталь: паяльную маску нужно выбирать зелёную (или синюю/красную). У чёрной и белой маски минимальный масочный мостик 0,15 мм, а у зелёной — 0,10 мм, иначе между площадками выводов H417 маска просто не ляжет. Паять платы буду сам =)
Ещё у нас появился научный консультант — доцент кафедры МИФИ, и намечаются дела с YADRO (без обещаний — если договоримся, ждите меня в их лабе в следующей статье XD). А 30 октября мы впервые вывезли Fabbit в люди — я выступил с докладом на конференции пет-проектов Тыкворкинг’26 в МЭИ. Было классно: угощения, мерч, бодрые вопросы про ПЛИС, а после докладов вокруг нашего стола со стендом собиралась живая очередь поиграть в Денди и Сегу с самодельного джойстика.
❯ 8. От стенда к карманному прибору
Итак, стенд подтвердил главное: связка из постоянно живущего супервизора и ПЛИС, в которую микроконтроллер прямо на ходу подгружает нужные битстримы, работает отлично и не превращается в кирпич при сбоях. А питание от батареи, защищённые входы 0–10 В, сменный модуль-мезонин, ЛБП и USB 3.0 предусмотрены в проекте собственной платы, схему которой я сейчас рисую в KiCad 10 (для трассировки перееду на Altium).
Главный осознанный компромисс на бумаге — автономность в тяжёлых профилях. По замерам модуля и расчёту остальных узлов от аккумулятора 2800 мА·ч (9,32 Вт·ч, КПД питания 0,88) прибор в расчёте живёт 8,7 ч в меню, 5,9–7,0 ч в ретро-играх, 5,6–6,6 ч в анализаторе и около 3,1 ч в режиме двухканального осциллографа:

Изначально хотелось не меньше пяти часов везде. Осциллограф в эту планку не укладывается (быстрый АЦП и синтезатор такта берут своё), увы, но полный набор функций в кармане важнее.
Почему вообще не упростить прибор до дешёвой настольной коробки с половиной функций? Я считал урезанный вариант «Core + Lite» — он выходил втрое дешевле и куда разумнее для серии. Но мы с Яриком решили, что сначала хочется собрать один флагманский портативный прототип без компромиссов, как НИР и инженерную витрину, и набить на нём все шишки! А уже потом на базе этого опыта сделать простой и модульный настольный вариант без экрана и батареи. Правда, после переезда на CH32H417, скоростной АЦП HMCAD1511 и ЦАП DAC904 список деталей под первую ревизию платы заметно разросся и перешагнул остаток нашей копилки — но тем интереснее довести этот прототип до текстолита!
❯ 9. Финал
Следующий шаг — дочертить схему собственной платы, проверить каждый пин по даташитам, развести 6 слоёв, запаять и первым делом запустить подгрузку профилей и поток анализатора по USB 3.0.
Исходники, платы и корпус:github.com/fabbit-dev/fabbit. Код под MIT, железо под CERN-OHL-S-2.0, инженерный дневник — в каталоге journal/.
Задачи, где мне очень пригодится ваша помощь:
Как чисто разделить выход генератора ПЛИС, провод и входной сэмплер ISERDESE2 в петле ЛА на 400 MS/s.
Незамыленный взгляд на схему и будущую трассировку перед отправкой на завод.
Да и вообще любая инженерная подмога: если вы собаку съели на ВЧ-аналоге, быстрых платах, USB 3.0 или ПЛИС и готовы вовремя крикнуть «стой, сейчас бахнет!» — милости прошу, убережёте нас от лишнего волшебного дыма =)
Сайт проекта:fabbit.dev — там можно со всех сторон покрутить 3D-модельку прибора и поддержать наш проект (будем рады помощи от каждого — и советом, и деталями из BOM, и вкладом в сборку первой платы; все закупки и чеки открыто ведём на fabbit.dev/spent).
Телеграм-канал с новостями со стола — @fabbit_dev (моя личка для советов, критики и предложений — @sshareds).
Осталось довести это до рабочего текстолита и с серьёзным лицом объяснить на школьной защите, почему для индивидуального проекта в десятом классе жизненно понадобились двухканальный осциллограф на 400 MS/s и аппаратный глитчер питания… До встречи, Хабр!
Может быть интересно:
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram‑канале ↩

Комментарии 0