Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions apps/website/src/data/blog/bodies/features-that-change-no-bits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import type { Block } from '../types'

export const body: Block[] = [
{
kind: 'p',
text: 'On 2026-08-27 a parity report landed in openXC7/nextpnr-xilinx#165: two place-and-route trees — the himbaechel port and the released nextpnr-xilinx 0.9.3 — run over the same netlists, the same XDC, the same prjxray-db, the same yosys and the same back end, so that the only variable is the P&R binary. The report named its most concrete finding without hedging: 179 BRAM configuration features that 0.9.3 emits and the port does not, "the most concrete bitstream-parity item and the one I would fix first".',
},
{
kind: 'p',
text: 'One day later the same author retracted that sentence. Three days after that, three bitstreams ran on a physical board. Neither of those measurements is mine — I read the artefacts, I did not reproduce a number in them — and together they make one point worth writing down: the difference you can count is not necessarily the difference that matters.',
},
{
kind: 'h',
text: 'The 179 features that change no bits',
},
{
kind: 'p',
text: 'The missing features are real in the FASM text: RAMB18.READ_WIDTH_{A,B}_1, WRITE_WIDTH_{A,B}_1 and RSTREG_PRIORITY_{A,B}_RSTREG, 179 of them in the LiteX design, with RSTREG_PRIORITY_* going 88 → 0, WRITE_WIDTH_B_1 44 → 5 and READ_WIDTH_B_1 40 → 7 between the arms. They are also, all of them, zero-codepoints: their segbits are entirely negated.',
},
{
kind: 'code',
text: 'BRAM_L.RAMB18_Y0.READ_WIDTH_A_1 !27_35 !27_36 !27_37\nBRAM_L.RAMB18_Y0.RSTREG_PRIORITY_A_RSTREG !27_124',
},
{
kind: 'p',
text: 'Every bit in the definition is a negation, so writing the feature and not writing it produce the same frame. The retraction rests on three checks that fail independently of each other:',
},
{
kind: 'ul',
items: [
'The segbits themselves, read from the database rather than inferred from the feature names.',
'A bit2fasm round trip: the port’s LiteX bitstream reads those same features back off the silicon-side zeros, and re-assembling the readback gives byte-identical frames — 5 576 340 B — for both arms.',
'A Vivado ML 2026.1 golden of an SDP-36 RAMB18E1 in both halves of one tile on xc7a50tcsg324-1, which shows Vivado writing READ_WIDTH_A_1 in the Y0 half and READ_WIDTH_A_18 in the Y1 half. The one BRAM utilisation delta anyone had flagged — READ_WIDTH_A_18 at 17 versus 14 — is just which half of the tile each tool put the five SDP-36 blocks in. The is_y1 rule is the same in both trees and matches Vivado.',
],
},
{
kind: 'p',
text: 'So the work item that had been ranked first would have moved nothing in the bitstream for anything LiteX exercises. It was a difference in the text that describes the configuration, not in the configuration.',
},
{
kind: 'h',
text: 'The blocker nobody had counted',
},
{
kind: 'p',
text: 'The thing that actually stopped a bitstream from reaching a board was a routing choice, and it produced no feature-count delta at all. Building a blinky for the lowRISC Sonata (xc7a50tcsg324, board clock on P15 = LIOI3_X0Y23), the port takes the regional clock network out of the pad:',
},
{
kind: 'code',
text: 'pad → HCLK_IOI3_X1Y26.HCLK_IOI_IO_PLL_CLK3_DMUX ← I2IOCLK_BOT1\n → RCLK3 → BUFR divider bypass → CLK_HROW … CK_BUFRCLK_L1 → BUFG',
},
{
kind: 'p',
text: 'Deterministically, over eight seeds, with router2 and with the default router. 0.9.3 takes the dedicated CCIO → HCLK_CMT → CLK_HROW → BUFG backbone instead. The fork knows to do this explicitly: Arch::routeClock() treats a single-user net feeding BUFGCTRL.I0 as a global (to_bufg_input, xilinx/arch.cc:1812), with a comment saying that left to the general router the BUFG output is dead and the design freezes. The port’s XilinxImpl::route_clocks() has five forms and none of them is that one.',
},
{
kind: 'p',
text: 'What made this hard to see is that the same route fails in two different-looking ways depending on which database revision you have. With the db revision 0.9.3 ships (ab1fc60) that DMUX pip has no segbits entry at all, so fasm2frames rejects the design with a FasmLookupError — which reads like a database gap. With current db master (77e52f10, after prjxray-db#7 merged 2026-08-26) the same key is an all-zero default row, so it assembles cleanly and yields a bitstream — which reads like success.',
},
{
kind: 'quote',
text: '…but whether that regional path delivers a working clock to the BUFG is unknown.',
},
{
kind: 'p',
text: 'That sentence, written on 2026-08-28, is the honest state of a bitstream that assembles without complaint. Nothing short of silicon could settle it.',
},
{
kind: 'h',
text: 'Three bitstreams, one board',
},
{
kind: 'p',
text: 'On 2026-08-30 the loop closed. Jonathan (jrrk2) ran three Sonata blinky bitstreams on a physical Sonata — same netlist, same XDC, same prjxray back end, only the P&R differing — and all three blink.',
},
{
kind: 'table',
head: ['arm', 'tree', 'router', 'clock route'],
rows: [
['A', 'nextpnr-xilinx 0.9.3 (68aeeb39, released package)', 'router2', 'dedicated CCIO → CMT → HROW → BUFG'],
['B', 'himbaechel 2212c004', 'router1', 'dedicated path (router1 avoids the BUFR datapath)'],
['C', 'himbaechel + openXC7/nextpnr#1 (2bf0e1f9)', 'router2', "dedicated path (the PR's predicate refuses the BUFR datapath)"],
],
},
{
kind: 'p',
text: 'Arm C is the one that carries information. That arm previously produced either a FasmLookupError or a clock of unknown liveness, depending on the database; with the merged predicate from openXC7/nextpnr#1 it now blinks with the default router. The claim about what that means for the port was made with its hedge attached, and it is worth quoting rather than paraphrasing:',
},
{
kind: 'quote',
text: 'B and C are, to our knowledge, its first board-verified bitstreams from an A/B kit against the fork.',
},
{
kind: 'p',
text: 'One reproduction note for anyone with the same board: the Sonata is programmed by dropping a UF2 on its bootloader volume, not over JTAG, so the .bit needs a UF2 wrap with family id 0x6ce29e6b.',
},
{
kind: 'h',
text: 'What the fix actually is',
},
{
kind: 'p',
text: 'It is worth being exact about what closed and what did not. openXC7/nextpnr#1 makes a route through the BUFR datapath oblige a BUFR — that is, it teaches the router to refuse a path it cannot honestly assemble, so placement falls back to the dedicated backbone. The regional path itself was not made to work. The related placement problem, putting a pad-fed BUFIO on a site its pad can actually reach, is openXC7/nextpnr-xilinx#168 and is still open; so is #149, the issue that started this line, where BUFIO is not packed and the HCLK_L enables are never emitted.',
},
{
kind: 'p',
text: 'The pattern the week produced: a counted difference of 179 features was cosmetic, and an uncounted difference of one routing decision was the thing standing between a port and a board. Feature diffs are cheap to compute and that makes them attractive as a work queue. This one ranked its own items exactly backwards, and the correction came from segbits, a round trip and a Vivado golden — not from a bigger diff.',
},
]

export const ruBody: Block[] = [
{
kind: 'p',
text: '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 выдаёт, а порт нет, — «самый конкретный пункт битстрим-паритета и тот, который я чинил бы первым».',
},
{
kind: 'p',
text: 'Через день тот же автор эту фразу отозвал. Ещё через три дня три битстрима запустились на физической плате. Ни одно из этих измерений не моё — я перечитал артефакты, но не воспроизвёл в них ни одного числа, — и вместе они дают один вывод, который стоит записать: разница, которую можно посчитать, не обязательно та разница, которая важна.',
},
{
kind: 'h',
text: '179 фич, которые не меняют ни бита',
},
{
kind: 'p',
text: 'Отсутствующие фичи реальны в тексте 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 целиком отрицательные.',
},
{
kind: 'code',
text: 'BRAM_L.RAMB18_Y0.READ_WIDTH_A_1 !27_35 !27_36 !27_37\nBRAM_L.RAMB18_Y0.RSTREG_PRIORITY_A_RSTREG !27_124',
},
{
kind: 'p',
text: 'Каждый бит в определении — отрицание, поэтому записать фичу и не записать её дают один и тот же фрейм. Отзыв опирается на три проверки, независимые друг от друга:',
},
{
kind: 'ul',
items: [
'Сами segbits, прочитанные из базы, а не выведенные из названий фич.',
'Круговой прогон bit2fasm: битстрим LiteX от порта считывает те же фичи обратно из нулей на стороне кремния, и повторная сборка прочитанного даёт побайтово одинаковые фреймы — 5 576 340 Б — для обоих вариантов.',
'Эталон Vivado ML 2026.1 для SDP-36 RAMB18E1 в обеих половинах одного тайла на xc7a50tcsg324-1: Vivado сам пишет READ_WIDTH_A_1 в половине Y0 и READ_WIDTH_A_18 в половине Y1. Единственная замеченная разница в утилизации BRAM — READ_WIDTH_A_18, 17 против 14, — это лишь то, в какую половину тайла каждый инструмент положил пять блоков SDP-36. Правило is_y1 одинаково в обоих деревьях и совпадает с Vivado.',
],
},
{
kind: 'p',
text: 'То есть пункт работы, поставленный первым, не сдвинул бы в битстриме ничего из того, что задействует LiteX. Это разница в тексте, описывающем конфигурацию, а не в самой конфигурации.',
},
{
kind: 'h',
text: 'Блокер, который никто не посчитал',
},
{
kind: 'p',
text: 'То, что на самом деле не давало битстриму дойти до платы, было решением маршрутизации и не давало вообще никакой разницы в счётчиках фич. При сборке blinky для lowRISC Sonata (xc7a50tcsg324, тактовый вход платы на P15 = LIOI3_X0Y23) порт выводит клок с пада через региональную сеть:',
},
{
kind: 'code',
text: 'pad → HCLK_IOI3_X1Y26.HCLK_IOI_IO_PLL_CLK3_DMUX ← I2IOCLK_BOT1\n → RCLK3 → BUFR divider bypass → CLK_HROW … CK_BUFRCLK_L1 → BUFG',
},
{
kind: 'p',
text: 'Детерминированно, на восьми сидах, и с 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() в порту пять форм, и ни одна из них не эта.',
},
{
kind: 'p',
text: 'Увидеть это было тяжело потому, что один и тот же маршрут ломается двумя по-разному выглядящими способами в зависимости от ревизии базы. С той ревизией, которую везёт 0.9.3 (ab1fc60), у пипа DMUX вообще нет записи segbits, и fasm2frames отвергает дизайн с FasmLookupError — это читается как дыра в базе. С текущим master базы (77e52f10, после prjxray-db#7, влитого 26 августа) тот же ключ — строка default из одних нулей, дизайн собирается без замечаний и даёт битстрим — а это читается как успех.',
},
{
kind: 'quote',
text: '…но доставляет ли этот региональный путь работающий клок до BUFG — неизвестно.',
},
{
kind: 'p',
text: 'Эта фраза, написанная 28 августа, и есть честное состояние битстрима, который собирается без единой жалобы. Решить вопрос не могло ничто, кроме кремния.',
},
{
kind: 'h',
text: 'Три битстрима, одна плата',
},
{
kind: 'p',
text: '30 августа круг замкнулся. Джонатан (jrrk2) прогнал три битстрима blinky на физической Sonata — тот же нетлист, тот же XDC, тот же бэкенд prjxray, различается только P&R — и все три мигают.',
},
{
kind: 'table',
head: ['плечо', 'дерево', 'маршрутизатор', 'путь клока'],
rows: [
['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)'],
],
},
{
kind: 'p',
text: 'Информацию несёт плечо C. Раньше оно давало либо FasmLookupError, либо клок неизвестной живости — в зависимости от базы; с влитым предикатом из openXC7/nextpnr#1 оно мигает на маршрутизаторе по умолчанию. Утверждение о том, что это значит для порта, было сделано вместе с оговоркой, и его стоит привести дословно, а не пересказывать:',
},
{
kind: 'quote',
text: 'B и C — насколько нам известно, первые проверенные на плате битстримы порта в A/B-комплекте против форка.',
},
{
kind: 'p',
text: 'Одна заметка для тех, у кого такая же плата: Sonata прошивается сбросом файла UF2 на её загрузочный том, а не через JTAG, поэтому .bit нужно завернуть в UF2 с family id 0x6ce29e6b.',
},
{
kind: 'h',
text: 'Что именно починено',
},
{
kind: 'p',
text: 'Стоит быть точным в том, что закрылось, а что нет. openXC7/nextpnr#1 делает так, что маршрут через датапат BUFR обязывает поставить BUFR, — то есть учит маршрутизатор отказываться от пути, который он не может честно собрать, и размещение откатывается на выделенный хребет. Сам региональный путь работать не заставили. Смежная задача размещения — поставить BUFIO, питаемый падом, на сайт, до которого этот пад дотягивается, — это openXC7/nextpnr-xilinx#168, и она открыта; открыт и #149, issue, с которого началась эта линия: BUFIO не пакуется, а разрешения HCLK_L не выдаются никогда.',
},
{
kind: 'p',
text: 'Итог недели: посчитанная разница в 179 фич оказалась косметикой, а непосчитанная разница в одном решении маршрутизации и была тем, что стояло между портом и платой. Диффы по фичам дёшево считать, и поэтому они выглядят удобной очередью работ. Эта очередь отранжировала свои пункты ровно наоборот, а поправка пришла из segbits, кругового прогона и эталона Vivado — не из диффа побольше.',
},
]
Loading
Loading