diff --git a/apps/website/src/data/blog/bodies/two-bitstreams-one-bit-apart.ts b/apps/website/src/data/blog/bodies/two-bitstreams-one-bit-apart.ts new file mode 100644 index 0000000000..7e10edb359 --- /dev/null +++ b/apps/website/src/data/blog/bodies/two-bitstreams-one-bit-apart.ts @@ -0,0 +1,219 @@ +import type { Block } from '../types' + +export const body: Block[] = [ + { + "kind": "h", + "text": "What was measured" + }, + { + "kind": "p", + "text": "A row in Project X-Ray's bitstream database says which bits in a bitstream correspond to which feature in the silicon. Most rows were minted by a fuzzer that compares thousands of Vivado outputs. Some rows exist only as proposals, and a proposal that nobody can check is where this starts: eight rows for the Artix-7 regional clock buffer were offered in December 2024 and the pull request was closed on 14 August 2026, pending a fuzzer that could make them verifiable." + }, + { + "kind": "p", + "text": "One of those eight is now verified — not by a fuzzer, by a board. Two bitstreams for an ALINX AX7203 (xc7a200tfbg484-2), 22.7 MB of frames each, differing by exactly one bit: frame 0x00401C00, word 50, bit 31. With that bit set an LED blinks at 0.75 Hz. With it cleared the same design sits frozen — an unclocked counter stuck at zero. The regional clock does not reach the fabric without it." + }, + { + "kind": "p", + "text": "That is what the row claimed, and it is the whole claim: HCLK_L.ENABLE_BUFFER.HCLK_CK_BUFRCLK2 = 00_31 is the enable for that buffer." + }, + { + "kind": "h", + "text": "Why no design had ever asserted it" + }, + { + "kind": "p", + "text": "Every earlier probe in the upstream discussion sank the BUFR's clock into an ISERDESE2 or an ODDR. Both live in the I/O column, so the regional clock never leaves it, and nothing in the horizontal clock spine is ever switched on. The maintainer working the other half of this said plainly that he had no design producing a single HCLK_L line." + }, + { + "kind": "p", + "text": "Sink the clock into ordinary CLB fabric instead — a plain counter — and it has to reach the leaves. The probe is thirty lines. That is the entire trick, and it produced the whole chain at once: the source pip, the leaf row, the missing enable, and the consumer in the clock-management tile." + }, + { + "kind": "h", + "text": "Two placer defects had to be fixed first" + }, + { + "kind": "p", + "text": "On a small part the probe places by luck. On the xc7a200t it fails twice, at both ends of the buffer." + }, + { + "kind": "p", + "text": "The arc into the buffer fails because a pad-fed BUFR must sit on the one site its clock-capable pad can reach — its I pin has no fabric input at all — and nothing told the placer which. The arc out of it fails because a BUFR drives one clock region, the placer does not cost global nets, and so the flops it clocks drift toward whatever data pin they touch. On this board that is an LED half a die away, in the opposite half from the only clock-capable oscillator." + }, + { + "kind": "p", + "text": "Both patches are submitted upstream and both are open, not merged. CI is green on all three parts it tests. The second one has a trap worth repeating: the region has to be derived by asking the routing graph where the clock actually arrives, and the walk must collect only bel pins named CLK. Counting every pin it touches returns a rectangle covering the whole die, which is a constraint that constrains nothing." + }, + { + "kind": "h", + "text": "The divide ladder, four points" + }, + { + "kind": "p", + "text": "A BUFR can divide its clock. The database has carried D1 through D8 for years; the flow hardcoded BYPASS until a patch in June 2026 made the rest reachable, and nothing since had driven a divided BUFR onto a board. Same probe, same counter, only the divide parameter changes:" + }, + { + "kind": "table", + "head": ["BUFR_DIVIDE", "predicted lit time", "observed"], + "rows": [ + ["BYPASS", "0.67 s", "~0.65 s"], + ["2", "1.34 s", "~4x faster than D8"], + ["4", "2.68 s", "~3 s"], + ["8", "5.37 s", "~5 s"] + ] + }, + { + "kind": "p", + "text": "Predicted from 2^28 x divide / 200e6. The midpoint matters more than the endpoints: two readings can only show that something divides, while a third landing where the outer two predict shows the ratio tracks the parameter instead of the divider latching one value." + }, + { + "kind": "h", + "text": "Three ways I got it wrong first" + }, + { + "kind": "p", + "text": "The first divide run was void. I had reverted the emitter patch to keep the pull requests clean, then built the divide probes on branches made after that, so the flashed bitstream carried zero enable lines and the clock never reached the fabric at all. The LED sat frozen and I was one step from reporting that the divide is ignored on silicon. What caught it was checking that the bitstream contained the feature the experiment was about, before interpreting the LED rather than after." + }, + { + "kind": "p", + "text": "That void run turned out to be worth more than it cost: it is a second, independent negative control for the enable bit — a bitstream built by a different route, for a different purpose, again showing a dead counter when the bit is absent." + }, + { + "kind": "p", + "text": "The same probe also tied the buffer's CLR low. A BUFR in divide mode needs its divider released by a CLR pulse where BYPASS does not, so even with the enable present that build could not have separated \"divide ignored\" from \"divider never started\"." + }, + { + "kind": "p", + "text": "And a code search after the fact found prior art against my own patch: a fork has fixed the same placement bug since April 2025 with a single line, calling the existing preplace helper. I measured it — identical sites on both probes — and said so on the pull request, offering to close mine in favour of it. I should have run that search before sending, not after." + }, + { + "kind": "h", + "text": "What is not mine" + }, + { + "kind": "p", + "text": "The flow. Yosys is YosysHQ. nextpnr is YosysHQ and David Shah. Project X-Ray and the xc7 database are SymbiFlow and F4PGA. The Xilinx port, the Artix-7 support, the BUFR packer and the BUFIO site fix are openXC7, funded by NLnet — and the mechanism my region patch borrows is the one a maintainer wrote for BUFIO weeks earlier. The eight candidate rows are AdamLee7's, from a pull request I reviewed and agreed should be closed. What is mine is a thirty-line probe, two patches, and a board on a desk." + }, + { + "kind": "h", + "text": "Why a board and not a fuzzer" + }, + { + "kind": "p", + "text": "They answer different questions. A fuzzer asks what the bits are, across a whole tile, from thousands of vendor bitstreams. A one-bit A/B asks whether one specific bit does the one thing it is claimed to do. The second is narrower and much cheaper — about thirty minutes per bit, most of it spent flashing — and it needs no vendor tool at all." + }, + { + "kind": "p", + "text": "It also cannot replace the fuzzer. It cannot find bits, only test them; it cannot separate a divide of 8 from a divide of 7; and its negative result is ambiguous in a way its positive result is not, because a bit that changes nothing may be a wrong bit or a default that was already set." + } +] + +export const ruBody: Block[] = [ + { + "kind": "h", + "text": "Что измерено" + }, + { + "kind": "p", + "text": "Строка в базе Project X-Ray говорит, каким битам в прошивке соответствует какой признак в кремнии. Большинство строк добыл фаззер, сравнивая тысячи выводов Vivado. Некоторые существуют только как предложение — и предложение, которое никто не может проверить, это и есть начало истории: восемь строк для регионального тактового буфера Artix-7 предложили в декабре 2024-го, а 14 августа 2026-го запрос закрыли до тех пор, пока не появится фаззер, способный сделать их проверяемыми." + }, + { + "kind": "p", + "text": "Одна из этих восьми теперь проверена — не фаззером, а платой. Две прошивки для ALINX AX7203 (xc7a200tfbg484-2), по 22.7 МБ кадров каждая, различающиеся ровно одним битом: кадр 0x00401C00, слово 50, бит 31. С этим битом светодиод мигает на 0.75 Гц. Без него тот же проект стоит намертво — счётчик не тактируется и остаётся в нуле. Региональный клок не доходит до фабрики." + }, + { + "kind": "p", + "text": "Это ровно то, что утверждала строка, и всё, что она утверждала: HCLK_L.ENABLE_BUFFER.HCLK_CK_BUFRCLK2 = 00_31 — разрешение того самого буфера." + }, + { + "kind": "h", + "text": "Почему этот признак не включал ни один проект" + }, + { + "kind": "p", + "text": "Все прежние пробы в обсуждении сажали клок BUFR в ISERDESE2 или ODDR. И то и другое живёт в колонке ввода-вывода, поэтому региональный клок её не покидает, и на горизонтальной тактовой спине ничего не включается. Сопровождающий, работавший над другой половиной задачи, прямо написал, что проекта, дающего хоть одну строку HCLK_L, у него нет." + }, + { + "kind": "p", + "text": "Посадите клок в обычную фабрику CLB — в простой счётчик — и он обязан дойти до лучей. Проба занимает тридцать строк. В этом весь приём, и он выдал сразу всю цепь: источник, маршрут по лучу, недостающее разрешение и потребителя в тайле управления тактами." + }, + { + "kind": "h", + "text": "Сначала пришлось починить два дефекта размещения" + }, + { + "kind": "p", + "text": "На малом кристалле проба встаёт по везению. На xc7a200t она падает дважды — с обеих сторон буфера." + }, + { + "kind": "p", + "text": "Входная дуга не строится, потому что BUFR, питаемый от вывода, обязан сесть на единственный site, до которого этот тактовый вывод дотягивается: у его входа нет связи с фабрикой вовсе, — а плейсеру об этом не сообщали. Выходная не строится, потому что BUFR обслуживает один тактовый регион, плейсер не учитывает глобальные цепи в стоимости, и триггеры уезжают к тому выводу данных, к которому подключены. На этой плате — к светодиоду за полкристалла, в половину, противоположную единственному тактовому генератору." + }, + { + "kind": "p", + "text": "Обе правки отправлены в проект и обе открыты, не слиты. CI зелёный на всех трёх проверяемых кристаллах. У второй есть ловушка, которую стоит повторить: область надо спрашивать у графа маршрутизации — куда клок реально доходит, — и собирать при этом только тактовые входы. Если считать все выводы подряд, получится прямоугольник во весь кристалл, то есть ограничение, которое ничего не ограничивает." + }, + { + "kind": "h", + "text": "Лестница делителя, четыре точки" + }, + { + "kind": "p", + "text": "BUFR умеет делить свой клок. База несёт ступени D1..D8 годами; поток жёстко использовал BYPASS до июньской правки 2026-го, сделавшей остальные достижимыми, и с тех пор никто не выводил делённый BUFR на плату. Та же проба, тот же счётчик, меняется только параметр деления:" + }, + { + "kind": "table", + "head": ["BUFR_DIVIDE", "предсказано (горит)", "измерено"], + "rows": [ + ["BYPASS", "0.67 с", "~0.65 с"], + ["2", "1.34 с", "вчетверо быстрее D8"], + ["4", "2.68 с", "~3 с"], + ["8", "5.37 с", "~5 с"] + ] + }, + { + "kind": "p", + "text": "Предсказание из 2^28 x деление / 200e6. Середина важнее краёв: два замера показывают лишь, что что-то делится, а третий, легший туда, куда указывали крайние, показывает, что отношение следует за параметром, а не залипает на первом заданном значении." + }, + { + "kind": "h", + "text": "Три способа ошибиться, которыми я воспользовался" + }, + { + "kind": "p", + "text": "Первый прогон делителя был недействителен. Я откатил правку эмиттера, чтобы запросы вышли чистыми, а потом собрал пробы на ветках, созданных после отката, — и прошивка не содержала ни одной строки разрешения, так что клок вообще не доходил до фабрики. Светодиод стоял, и я был в шаге от вывода, что деление кремнием игнорируется. Спасла проверка того, что в прошивке есть признак, ради которого ставился опыт, — сделанная до толкования светодиода, а не после." + }, + { + "kind": "p", + "text": "Недействительный прогон оказался ценнее, чем стоил: это второй независимый отрицательный контроль для бита разрешения — прошивка, собранная другим маршрутом и по другому поводу, снова показала мёртвый счётчик при отсутствующем бите." + }, + { + "kind": "p", + "text": "В той же пробе вход CLR был завязан на ноль. Делителю BUFR нужен импульс сброса, режиму BYPASS — нет, поэтому даже при наличии разрешения та сборка не различила бы «деление игнорируется» и «делитель не запущен»." + }, + { + "kind": "p", + "text": "А поиск по коду, сделанный задним числом, нашёл предшествующий опыт против моей же правки: форк чинит ту же ошибку размещения с апреля 2025 года одной строкой, вызовом уже существующего помощника. Я это измерил — размещение совпало на обеих пробах — и написал об этом в запросе, предложив закрыть свой в пользу той строки. Такой поиск следовало сделать до отправки, а не после." + }, + { + "kind": "h", + "text": "Что здесь не моё" + }, + { + "kind": "p", + "text": "Сам поток. Yosys — это YosysHQ. nextpnr — YosysHQ и Дэвид Шах. Project X-Ray и база xc7 — SymbiFlow и F4PGA. Порт под Xilinx, поддержка Artix-7, пакер BUFR и привязка site для BUFIO — openXC7 при финансировании NLnet, и механизм, который заимствует моя правка региона, написан сопровождающим для BUFIO несколькими неделями раньше. Восемь строк-кандидатов принадлежат AdamLee7 — из запроса, который я рецензировал и с закрытием которого согласился. Моё здесь — проба в тридцать строк, две правки и плата на столе." + }, + { + "kind": "h", + "text": "Почему плата, а не фаззер" + }, + { + "kind": "p", + "text": "Они отвечают на разные вопросы. Фаззер спрашивает, каковы биты, — по всему тайлу, из тысяч вендорских прошивок. Однобитовое сравнение спрашивает, делает ли один конкретный бит то единственное, что ему приписано. Второе уже и намного дешевле — около получаса на бит, почти целиком уходящих на прошивку, — и вендорский инструмент ему не нужен вовсе." + }, + { + "kind": "p", + "text": "И оно не заменяет фаззер. Оно не находит биты, а только проверяет их; оно не отличит деление на 8 от деления на 7; и его отрицательный результат неоднозначен так, как положительный — нет, потому что бит, ничего не меняющий, может быть и неверным битом, и значением по умолчанию, уже установленным." + } +] diff --git a/apps/website/src/data/blog/index.ts b/apps/website/src/data/blog/index.ts index fd7784a98f..7e3dd9efb8 100644 --- a/apps/website/src/data/blog/index.ts +++ b/apps/website/src/data/blog/index.ts @@ -2,6 +2,66 @@ import type { PostMeta } from './types' /** Индекс блога: список и метаданные без тяжёлых тел публикаций. */ export const postsIndex: PostMeta[] = [ + { + "slug": "two-bitstreams-one-bit-apart", + "title": "Two bitstreams, one bit apart", + "summary": "A proposed row in the Xilinx bitstream database had been unverifiable for eighteen months. Two 22.7 MB bitstreams differing in a single bit settle it on silicon: with the bit an LED blinks, without it the counter is frozen.", + "date": "2026-09-01", + "readingMinutes": 8, + "tags": [ + "FPGA", + "openXC7", + "Project X-Ray", + "Hardware" + ], + "receipts": [ + { + "label": "nextpnr-xilinx #149 — the measurement, with the failed attempts · OPEN", + "href": "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/openXC7/nextpnr-xilinx/issues/149#issuecomment-5494347759" + }, + { + "label": "nextpnr-xilinx #149 — the divide ladder, four points · OPEN", + "href": "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/openXC7/nextpnr-xilinx/issues/149#issuecomment-5496565529" + }, + { + "label": "nextpnr-xilinx #170 — pad-fed BUFR to its dedicated site · OPEN, submitted upstream, CI green 3/3", + "href": "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/openXC7/nextpnr-xilinx/pull/170" + }, + { + "label": "nextpnr-xilinx #171 — regional buffer sinks kept in their clock region · OPEN, submitted upstream, CI green 3/3", + "href": "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/openXC7/nextpnr-xilinx/pull/171" + }, + { + "label": "nextpnr-xilinx #172 — MMCM-fed buffers cannot be assembled: PERFCLK rows missing · OPEN", + "href": "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/openXC7/nextpnr-xilinx/issues/172" + }, + { + "label": "prjxray-db #1 — the eight candidate rows, by AdamLee7 · CLOSED 2026-08-14", + "href": "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/openXC7/prjxray-db/pull/1" + } + ], + "openQuestions": [ + "One row of the eight is measured. BUFRCLK0 (00_23), BUFRCLK1 (01_23) and BUFRCLK3 (01_31) rest on a frame-pattern argument and nothing else. The index follows which BUFR site the clock pad maps to, and this board has exactly one clock-capable oscillator, so varying it is not possible here.", + "Both patches are OPEN, not merged. CI is green and no maintainer has reviewed them yet. Nothing in this post should be read as upstream acceptance.", + "#170 may be the wrong patch. A fork has fixed the same bug with one line since April 2025, and I measured identical placement from it. I disclosed that on the pull request and offered to close mine; the maintainers have not chosen.", + "An LED separates divide ratios of 2, 4 and 8. It would not separate 8 from 7. D1, D3, D5, D6 and D7 were never tested — the doubling steps were picked precisely because an eye can call them without argument.", + "MMCM-fed regional buffers still cannot be assembled at all: two mux legs have no segbits, and the cause is a specimen gap needing Vivado runs on someone else's bench, not a fix I can make.", + "Nothing here is independently replicated. One board, one operator reading an LED, one evening. The method is cheap enough that a second party could check it in about half an hour per bit, and until someone does that is the standing of it." + ], + "published": true, + "ru": { + "title": "Две прошивки, различие в один бит", + "summary": "Предложенная строка в базе прошивок Xilinx полтора года оставалась непроверяемой. Две прошивки по 22.7 МБ, различающиеся одним битом, закрывают вопрос на кремнии: с битом светодиод мигает, без него счётчик стоит.", + "openQuestions": [ + "Проверена одна строка из восьми. BUFRCLK0 (00_23), BUFRCLK1 (01_23) и BUFRCLK3 (01_31) держатся на узоре кадров и больше ни на чём. Индекс определяется тем, на какой site BUFR выводит тактовый пад, а тактовый генератор на этой плате ровно один, так что менять индекс здесь нечем.", + "Обе правки ОТКРЫТЫ, не слиты. CI зелёный, но ни один сопровождающий их пока не смотрел. Ничто в этом тексте не следует читать как принятие в проект.", + "Правка #170 может оказаться лишней. Форк чинит ту же ошибку одной строкой с апреля 2025 года, и я измерил, что размещение выходит то же. Я написал об этом в запросе и предложил закрыть свой; сопровождающие ещё не выбрали.", + "Светодиод различает деление на 2, 4 и 8. Деление на 8 от деления на 7 он бы не отличил. Ступени D1, D3, D5, D6 и D7 не проверялись — удваивающиеся выбраны именно потому, что их глаз называет без спора.", + "Региональные буферы, питаемые от MMCM, по-прежнему не собираются в прошивку вовсе: у двух ножек мультиплексора нет битов в базе, а причина — нехватка образцов, требующая прогонов Vivado на чужом стенде, а не правки, которую могу сделать я.", + "Ничто здесь не воспроизведено независимо. Одна плата, один человек, смотрящий на светодиод, один вечер. Метод достаточно дёшев, чтобы вторая сторона проверила его примерно за полчаса на бит, и до тех пор таково его положение." + ] + } + }, { slug: 'one-commit-nine-workflow-outcomes', title: 'One commit, nine workflow outcomes', diff --git a/apps/website/src/data/blog/posts.ts b/apps/website/src/data/blog/posts.ts index 3009de307c..9fd703682f 100644 --- a/apps/website/src/data/blog/posts.ts +++ b/apps/website/src/data/blog/posts.ts @@ -37,6 +37,7 @@ import { body as body_a_repair_reaches_only_the_copy_it_lands_in, ruBody as ruBo import { body as body_green_ci_does_not_mean_usable, ruBody as ruBody_green_ci_does_not_mean_usable } from './bodies/green-ci-does-not-mean-usable' import { body as body_scale_field_width_already_published, ruBody as ruBody_scale_field_width_already_published } from './bodies/scale-field-width-already-published' import { body as body_open_gigabit_ethernet_artix7, ruBody as ruBody_open_gigabit_ethernet_artix7 } from './bodies/open-gigabit-ethernet-artix7' +import { body as body_two_bitstreams_one_bit_apart, ruBody as ruBody_two_bitstreams_one_bit_apart } from './bodies/two-bitstreams-one-bit-apart' import { body as body_a_multiplicity_correction_changed_the_deployment_reading, ruBody as ruBody_a_multiplicity_correction_changed_the_deployment_reading } from './bodies/a-multiplicity-correction-changed-the-deployment-reading' import { body as body_the_full_adder_made_the_cost_claim_comparable, ruBody as ruBody_the_full_adder_made_the_cost_claim_comparable } from './bodies/the-full-adder-made-the-cost-claim-comparable' import { body as body_the_tail_that_had_never_run, ruBody as ruBody_the_tail_that_had_never_run } from './bodies/the-tail-that-had-never-run' @@ -111,6 +112,7 @@ const bodies: Record = { 'tri-claw-an-agent-you-can-audit': { body: body_tri_claw_an_agent_you_can_audit, ruBody: ruBody_tri_claw_an_agent_you_can_audit }, 'receipts-and-seals-over-radio': { body: body_receipts_and_seals_over_radio, ruBody: ruBody_receipts_and_seals_over_radio }, 'open-gigabit-ethernet-artix7': { body: body_open_gigabit_ethernet_artix7, ruBody: ruBody_open_gigabit_ethernet_artix7 }, + 'two-bitstreams-one-bit-apart': { body: body_two_bitstreams_one_bit_apart, ruBody: ruBody_two_bitstreams_one_bit_apart }, 'a-multiplicity-correction-changed-the-deployment-reading': { body: body_a_multiplicity_correction_changed_the_deployment_reading, ruBody: ruBody_a_multiplicity_correction_changed_the_deployment_reading }, }