From 20f947fa3a636075c22a3ba9be7f7e6a725f3b0d Mon Sep 17 00:00:00 2001 From: Dmitriy Vasilev Date: Mon, 31 Aug 2026 21:54:49 +0700 Subject: [PATCH] blog: 179 features that change no bits A parity report between the himbaechel xilinx port and nextpnr-xilinx 0.9.3 ranked its own work items backwards: the 179 missing BRAM features are zero-codepoints, and the blocker that kept a bitstream off a board produced no feature-count delta at all. Adds the post with receipts for #165, its two corrections, openXC7/nextpnr#1, prjxray-db#7, and the two open BUFIO items. Every measurement is attributed upstream; none was reproduced here, and that is stated in openQuestions. Co-Authored-By: Claude --- .../bodies/features-that-change-no-bits.ts | 217 ++++++++++++++++++ apps/website/src/data/blog/index.ts | 38 +++ apps/website/src/data/blog/posts.ts | 2 + 3 files changed, 257 insertions(+) create mode 100644 apps/website/src/data/blog/bodies/features-that-change-no-bits.ts diff --git a/apps/website/src/data/blog/bodies/features-that-change-no-bits.ts b/apps/website/src/data/blog/bodies/features-that-change-no-bits.ts new file mode 100644 index 0000000000..a10083b111 --- /dev/null +++ b/apps/website/src/data/blog/bodies/features-that-change-no-bits.ts @@ -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 — не из диффа побольше.', + }, +] diff --git a/apps/website/src/data/blog/index.ts b/apps/website/src/data/blog/index.ts index 1cc515391c..f04448f3ef 100644 --- a/apps/website/src/data/blog/index.ts +++ b/apps/website/src/data/blog/index.ts @@ -2,6 +2,44 @@ import type { PostMeta } from './types' /** Индекс блога: список и метаданные без тяжёлых тел публикаций. */ export const postsIndex: PostMeta[] = [ + { + slug: 'features-that-change-no-bits', + title: '179 features that change no bits, and one routing choice that did', + summary: 'A parity report between two Xilinx place-and-route trees ranked its own work items backwards: the 179 missing BRAM features were zero-codepoints, and the blocker that kept a bitstream off a board produced no feature-count delta at all.', + date: '2026-08-31', + readingMinutes: 7, + tags: ['openXC7', 'FPGA', 'Bitstream', 'Upstream', 'Measurement'], + receipts: [ + { label: 'openXC7/nextpnr-xilinx #165 — the parity report itself · OPEN', href: 'https://github.com/openXC7/nextpnr-xilinx/issues/165' }, + { label: 'openXC7/nextpnr-xilinx #165 — the retraction of the BRAM item, 2026-08-28', href: 'https://github.com/openXC7/nextpnr-xilinx/issues/165#issuecomment-5456497915' }, + { label: 'openXC7/nextpnr-xilinx #165 — the Sonata board results, 2026-08-30', href: 'https://github.com/openXC7/nextpnr-xilinx/issues/165#issuecomment-5471151263' }, + { label: 'openXC7/nextpnr #1 — a route through the BUFR datapath obliges a BUFR · MERGED 2026-08-29', href: 'https://github.com/openXC7/nextpnr/pull/1' }, + { label: 'openXC7/prjxray-db #7 — artix7 HCLK_IOI3 rows and the DMUX default position · MERGED 2026-08-26', href: 'https://github.com/openXC7/prjxray-db/pull/7' }, + { label: 'openXC7/nextpnr-xilinx #168 — place a pad-fed BUFIO on a site its pad can reach · OPEN', href: 'https://github.com/openXC7/nextpnr-xilinx/pull/168' }, + { label: 'openXC7/nextpnr-xilinx #149 — BUFIO is not packed, HCLK_L enables never emitted · OPEN', href: 'https://github.com/openXC7/nextpnr-xilinx/issues/149' }, + ], + openQuestions: [ + 'Not one measurement in this post is mine. The parity table and both corrections are cavearr’s work in #165; the board run is jrrk2’s, on his own Sonata. I opened and read the artefacts; I reproduced nothing in them.', + 'One board, one die (xc7a50tcsg324), one design class. A blinky says the clock arrives and the fabric toggles. It says nothing about whether a himbaechel LiteX DDR3 bitstream runs on silicon, and nobody has tried.', + 'The hold-analysis policy is unchanged: the port still exits 1 on hold violations computed from a single DEFAULT speed grade with combinational-only cell timing, after writing the FASM. Whether it should warn instead is an open question in the thread, not a decision.', + 'fmax remains incomparable across the two trees. A one-LUT, one-FF design reports 2865 MHz on 0.9.3 and 1044 MHz on the port, so no quality-of-result claim between them is supported by anything published so far.', + 'The BRAM address cascade the port does emit and 0.9.3 never does (CASCOUT_ARD_ACTIVE ×34, CASCOUT_BWR_ACTIVE ×19 in the LiteX design) is legal and accepted by fasm2frames, but has not been checked against a Vivado golden and has not been seen on silicon.', + 'The regional clock path was routed around, not fixed. #168 and #149 are open, and a pad-fed BUFIO still cannot be placed on a site its pad can reach.', + ], + published: true, + ru: { + title: '179 фич, которые не меняют ни бита, и одно решение маршрутизации, которое меняло', + summary: 'Отчёт о паритете двух деревьев place-and-route для Xilinx отранжировал собственные задачи наоборот: 179 недостающих фич BRAM оказались нулевыми кодовыми точками, а блокер, не пускавший битстрим на плату, не давал в счётчиках фич никакой разницы.', + openQuestions: [ + 'Ни одно измерение в этом посте не моё. Таблица паритета и обе поправки — работа cavearr в #165; прогон на плате — jrrk2, на его собственной Sonata. Я открыл и прочитал артефакты; я ничего в них не воспроизвёл.', + 'Одна плата, один кристалл (xc7a50tcsg324), один класс дизайна. Blinky говорит, что клок доходит и фабрика переключается. Он ничего не говорит о том, работает ли на кремнии битстрим LiteX DDR3 от порта, — этого никто не пробовал.', + 'Политика анализа hold не изменилась: порт по-прежнему завершается с кодом 1 при нарушениях hold, посчитанных по единственному скоростному градусу DEFAULT и чисто комбинационным задержкам ячеек, уже после записи FASM. Должен ли он вместо этого предупреждать — открытый вопрос в треде, а не решение.', + 'fmax по-прежнему несопоставим между двумя деревьями. Дизайн из одного LUT и одного триггера показывает 2865 МГц на 0.9.3 и 1044 МГц на порте, так что ни одно утверждение о качестве результата между ними опубликованным пока не подкреплено.', + 'Каскад адресов BRAM, который порт выдаёт, а 0.9.3 не выдаёт никогда (CASCOUT_ARD_ACTIVE ×34, CASCOUT_BWR_ACTIVE ×19 в дизайне LiteX), легален и принимается fasm2frames, но не сверялся с эталоном Vivado и не проверялся на кремнии.', + 'Региональный путь клока обошли, а не починили. #168 и #149 открыты, и BUFIO, питаемый падом, по-прежнему нельзя поставить на сайт, до которого этот пад дотягивается.', + ], + }, + }, { slug: 'physical-width-changed-the-question', title: 'Physical width changed the question', diff --git a/apps/website/src/data/blog/posts.ts b/apps/website/src/data/blog/posts.ts index 85b02a14d5..0ff78cd444 100644 --- a/apps/website/src/data/blog/posts.ts +++ b/apps/website/src/data/blog/posts.ts @@ -1,6 +1,7 @@ import { postsIndex } from './index' import { body as body_a_health_snapshot_changed_its_denominator, ruBody as ruBody_a_health_snapshot_changed_its_denominator } from './bodies/a-health-snapshot-changed-its-denominator' import type { Post, PostBody } from './types' +import { body as body_features_that_change_no_bits, ruBody as ruBody_features_that_change_no_bits } from './bodies/features-that-change-no-bits' import { body as body_physical_width_changed_the_question, ruBody as ruBody_physical_width_changed_the_question } from './bodies/physical-width-changed-the-question' import { body as body_nobodys_example, ruBody as ruBody_nobodys_example } from './bodies/nobodys-example' import { body as body_the_silence_was_a_saturated_readout, ruBody as ruBody_the_silence_was_a_saturated_readout } from './bodies/the-silence-was-a-saturated-readout' @@ -56,6 +57,7 @@ import { body as body_the_scanner_scored_what_it_could_not_see, ruBody as ruBody import { body as body_i_wrote_the_post_then_did_the_thing, ruBody as ruBody_i_wrote_the_post_then_did_the_thing } from './bodies/i-wrote-the-post-then-did-the-thing' const bodies: Record = { + 'features-that-change-no-bits': { body: body_features_that_change_no_bits, ruBody: ruBody_features_that_change_no_bits }, 'physical-width-changed-the-question': { body: body_physical_width_changed_the_question, ruBody: ruBody_physical_width_changed_the_question }, 'nobodys-example': { body: body_nobodys_example, ruBody: ruBody_nobodys_example }, 'the-silence-was-a-saturated-readout': { body: body_the_silence_was_a_saturated_readout, ruBody: ruBody_the_silence_was_a_saturated_readout },