Блог
Отчёт о паритете двух деревьев place-and-route для Xilinx отранжировал собственные задачи наоборот: 179 недостающих фич BRAM оказались нулевыми кодовыми точками, а блокер, не пускавший битстрим на плату, не давал в счётчиках фич никакой разницы.

27 августа 2026 в openXC7/nextpnr-xilinx#165 появился отчёт о паритете: два дерева place-and-route — порт himbaechel и выпущенный nextpnr-xilinx 0.9.3 — прогнаны по одним и тем же нетлистам, одному XDC, одной prjxray-db, одному yosys и одному бэкенду, так что единственная переменная — бинарник P&R. Самый конкретный вывод отчёта был назван без оговорок: 179 конфигурационных фич BRAM, которые 0.9.3 выдаёт, а порт нет, — «самый конкретный пункт битстрим-паритета и тот, который я чинил бы первым».
Через день тот же автор эту фразу отозвал. Ещё через три дня три битстрима запустились на физической плате. Ни одно из этих измерений не моё — я перечитал артефакты, но не воспроизвёл в них ни одного числа, — и вместе они дают один вывод, который стоит записать: разница, которую можно посчитать, не обязательно та разница, которая важна.
Отсутствующие фичи реальны в тексте FASM: RAMB18.READ_WIDTH_{A,B}_1, WRITE_WIDTH_{A,B}_1 и RSTREG_PRIORITY_{A,B}_RSTREG, 179 штук в дизайне LiteX, причём RSTREG_PRIORITY_* идёт 88 → 0, WRITE_WIDTH_B_1 44 → 5, READ_WIDTH_B_1 40 → 7 между вариантами. И все они — нулевые кодовые точки: их segbits целиком отрицательные.
BRAM_L.RAMB18_Y0.READ_WIDTH_A_1 !27_35 !27_36 !27_37
BRAM_L.RAMB18_Y0.RSTREG_PRIORITY_A_RSTREG !27_124
Каждый бит в определении — отрицание, поэтому записать фичу и не записать её дают один и тот же фрейм. Отзыв опирается на три проверки, независимые друг от друга:
То есть пункт работы, поставленный первым, не сдвинул бы в битстриме ничего из того, что задействует LiteX. Это разница в тексте, описывающем конфигурацию, а не в самой конфигурации.
То, что на самом деле не давало битстриму дойти до платы, было решением маршрутизации и не давало вообще никакой разницы в счётчиках фич. При сборке blinky для lowRISC Sonata (xc7a50tcsg324, тактовый вход платы на P15 = LIOI3_X0Y23) порт выводит клок с пада через региональную сеть:
pad → HCLK_IOI3_X1Y26.HCLK_IOI_IO_PLL_CLK3_DMUX ← I2IOCLK_BOT1
→ RCLK3 → BUFR divider bypass → CLK_HROW … CK_BUFRCLK_L1 → BUFG
Детерминированно, на восьми сидах, и с router2, и с маршрутизатором по умолчанию. 0.9.3 вместо этого идёт по выделенному хребту CCIO → HCLK_CMT → CLK_HROW → BUFG. Форк умеет это явно: Arch::routeClock() трактует цепь с единственным потребителем, входящую в BUFGCTRL.I0, как глобальную (to_bufg_input, xilinx/arch.cc:1812), с комментарием, что при обычной маршрутизации выход BUFG мёртв и дизайн замирает. У XilinxImpl::route_clocks() в порту пять форм, и ни одна из них не эта.
Увидеть это было тяжело потому, что один и тот же маршрут ломается двумя по-разному выглядящими способами в зависимости от ревизии базы. С той ревизией, которую везёт 0.9.3 (ab1fc60), у пипа DMUX вообще нет записи segbits, и fasm2frames отвергает дизайн с FasmLookupError — это читается как дыра в базе. С текущим master базы (77e52f10, после prjxray-db#7, влитого 26 августа) тот же ключ — строка default из одних нулей, дизайн собирается без замечаний и даёт битстрим — а это читается как успех.
…но доставляет ли этот региональный путь работающий клок до BUFG — неизвестно.
Эта фраза, написанная 28 августа, и есть честное состояние битстрима, который собирается без единой жалобы. Решить вопрос не могло ничто, кроме кремния.
30 августа круг замкнулся. Джонатан (jrrk2) прогнал три битстрима blinky на физической Sonata — тот же нетлист, тот же XDC, тот же бэкенд prjxray, различается только P&R — и все три мигают.
| плечо | дерево | маршрутизатор | путь клока |
|---|---|---|---|
| A | nextpnr-xilinx 0.9.3 (68aeeb39, выпущенный пакет) | router2 | выделенный CCIO → CMT → HROW → BUFG |
| B | himbaechel 2212c004 | router1 | выделенный путь (router1 обходит датапат BUFR) |
| C | himbaechel + openXC7/nextpnr#1 (2bf0e1f9) | router2 | выделенный путь (предикат из PR отказывается от датапата BUFR) |
Информацию несёт плечо C. Раньше оно давало либо FasmLookupError, либо клок неизвестной живости — в зависимости от базы; с влитым предикатом из openXC7/nextpnr#1 оно мигает на маршрутизаторе по умолчанию. Утверждение о том, что это значит для порта, было сделано вместе с оговоркой, и его стоит привести дословно, а не пересказывать:
B и C — насколько нам известно, первые проверенные на плате битстримы порта в A/B-комплекте против форка.
Одна заметка для тех, у кого такая же плата: Sonata прошивается сбросом файла UF2 на её загрузочный том, а не через JTAG, поэтому .bit нужно завернуть в UF2 с family id 0x6ce29e6b.
Стоит быть точным в том, что закрылось, а что нет. openXC7/nextpnr#1 делает так, что маршрут через датапат BUFR обязывает поставить BUFR, — то есть учит маршрутизатор отказываться от пути, который он не может честно собрать, и размещение откатывается на выделенный хребет. Сам региональный путь работать не заставили. Смежная задача размещения — поставить BUFIO, питаемый падом, на сайт, до которого этот пад дотягивается, — это openXC7/nextpnr-xilinx#168, и она открыта; открыт и #149, issue, с которого началась эта линия: BUFIO не пакуется, а разрешения HCLK_L не выдаются никогда.
Итог недели: посчитанная разница в 179 фич оказалась косметикой, а непосчитанная разница в одном решении маршрутизации и была тем, что стояло между портом и платой. Диффы по фичам дёшево считать, и поэтому они выглядят удобной очередью работ. Эта очередь отранжировала свои пункты ровно наоборот, а поправка пришла из segbits, кругового прогона и эталона Vivado — не из диффа побольше.
Поработаем вместе
Работаю по контракту и part-time с hardware-AI, FPGA/RTL и ML-системами — от спецификации и открытого тулчейна до воспроизводимых измерений.