Skip to content
Merged
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
219 changes: 219 additions & 0 deletions apps/website/src/data/blog/bodies/two-bitstreams-one-bit-apart.ts
Original file line number Diff line number Diff line change
@@ -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; и его отрицательный результат неоднозначен так, как положительный — нет, потому что бит, ничего не меняющий, может быть и неверным битом, и значением по умолчанию, уже установленным."
}
]
Loading
Loading