039-hclk-config: fuzz BUFIO_Yn.IN_USE and the BUFR RCLK2IOn leaf, with pin- and MMCM-fed sources

The fuzzer had no consumer for its BUFRs and no BUFIO at all, which left
three things in the database's HCLK_IOI3 model unexercised: BUFIO has no
rows, the four DMUX I2IOCLK positions could never resolve (openXC7's
047 finding: no IBUF specimen), and the regional-clock leaf into the IOI
column (RCLK2IOn <- CK_BUFRCLKn) was recorded as 'always' although it
carries a bit.  This extends the specimen class in the same fuzzer:

- BUFIO half: for each clock-capable IOB33M of the tile's neighbourhood,
  a BUFIO at its dedicated site (the k-th IOB by y drives the k-th BUFIO
  by y -- bijective on every HCLK_IOI3 of xc7a100t; LOC'ing elsewhere
  fails placement), randomly unused / fed from that pin (I2IOCLK leg) /
  fed from the region's MMCM CLKOUT0..3 (LOC'd on its MMCME2_ADV; a
  PLLE2 cannot reach BUFIO), sunk into an ISERDESE2 of the same bank
  (DRC REQP-4 wants a load).  Tag: BUFIO_Y{n}.IN_USE, read back from
  the placed BUFIO sites dumped by generate.tcl.  Mixed sources are what
  make IN_USE independent of the route.
- Consumed BUFRs: half of the IN_USE BUFRs now drive an ODDR through an
  OBUF LOC'd on the S half of the same clock-capable pair (unused by the
  IBUFs).  Tag: HCLK_IOI_RCLK2IO{n}.HCLK_IOI_CK_BUFRCLK{n} per RCLK
  index (slot rel-y -> RCLK {0:2, 1:3, 2:0, 3:1}, the mapping the same
  data measures via the IOCLK_PLL rows).

100 specimens on xc7a100tfgg676-1 seeded from openXC7/prjxray-db with
the fuzzer's own rows stripped: the 40 pre-existing rows return
byte-identical; new rows BUFIO_Y0..3.IN_USE (2 bits each) and
RCLK2IO0..3 (32_29 / 30_31 / 31_19 / 28_17), the latter matching one
Vivado 2026.1 golden per slot bit for bit.  Database change in the
companion prjxray-db PR.
This commit is contained in:
Carlos Venegas Arrabé 2026-08-18 18:02:53 +02:00
parent 8294aeb68d
commit 7191d33707
No known key found for this signature in database
GPG Key ID: EC73A5348B584C4C
3 changed files with 178 additions and 7 deletions

View File

@ -25,8 +25,33 @@ def main():
with open('params.json') as f:
params = json.load(f)
placed = {}
try:
with open('bufio_sites.txt') as f:
for line in f:
parts = line.split()
if len(parts) == 2:
placed[parts[0]] = parts[1]
except FileNotFoundError:
pass
used_sites = set(placed.values())
for row in params:
if row.get('kind') == 'BUFIO_TILE':
sites = row['bufio_sites']
ys = sorted(int(st.split('Y')[-1]) for st in sites)
y_min = ys[0] if ys else 0
for st in sites:
y = int(st.split('Y')[-1]) - y_min
segmk.add_tile_tag(
row['tile'], 'BUFIO_Y{}.IN_USE'.format(y), 1 if st in used_sites else 0)
continue
base_name = 'BUFR_Y{}'.format(row['y'])
# v5: regional-clock leaf into the IOI column, per RCLK index (slot rel-y -> RCLK {0:2,1:3,2:0,3:1})
rclk = {0: 2, 1: 3, 2: 0, 3: 1}[row['y']]
segmk.add_tile_tag(
row['tile'], 'HCLK_IOI_RCLK2IO{}.HCLK_IOI_CK_BUFRCLK{}'.format(rclk, rclk),
1 if (row['IN_USE'] and row.get('consumed')) else 0)
segmk.add_tile_tag(
row['tile'], '{}.IN_USE'.format(base_name), row['IN_USE'])

View File

@ -17,6 +17,11 @@ proc run {} {
place_design
route_design
set fp [open bufio_sites.txt w]
foreach c [get_cells -quiet -hierarchical -filter {REF_NAME == BUFIO}] {
puts $fp "[get_property NAME $c] [get_sites -quiet -of_objects $c]"
}
close $fp
write_checkpoint -force design.dcp
write_bitstream -force design.bit
}

View File

@ -21,6 +21,18 @@ def gen_sites():
xy_fun = util.create_xy_fun('BUFR_')
db = Database(util.get_db_root(), util.get_part())
grid = db.grid()
# MMCME2_ADV site per clock region: an MMCM feeding a bank's BUFIOs must sit
# in that bank's own region (IOCLK_PLL is a region-local path). Only the
# MMCM's CLKOUT0..3 reach BUFIO -- a PLLE2's outputs are unroutable to BUFIO
# sites (measured), which is also why 047 registers only MMCM outputs as
# BUFIO-capable sources.
mmcm_by_region = {}
for tn in grid.tiles():
gi = grid.gridinfo_at_loc(grid.loc_of_tilename(tn))
for st, sty in gi.sites.items():
if sty == 'MMCME2_ADV' and gi.clock_region is not None:
mmcm_by_region[str(gi.clock_region)] = st
for tile_name in sorted(grid.tiles()):
loc = grid.loc_of_tilename(tile_name)
gridinfo = grid.gridinfo_at_loc(loc)
@ -28,6 +40,8 @@ def gen_sites():
xs = []
ys = []
bufio_sites = []
bufio_xy = util.create_xy_fun('BUFIO_')
for site, site_type in gridinfo.sites.items():
if site_type == 'BUFR':
x, y = xy_fun(site)
@ -35,6 +49,9 @@ def gen_sites():
ys.append(y)
sites.append((site, x, y))
elif site_type == 'BUFIO':
bx, by = bufio_xy(site)
bufio_sites.append((site, bx, by))
if not sites:
continue
@ -51,6 +68,8 @@ def gen_sites():
dx = -1
iobs = []
iobs_s = []
ilogics = []
for dy in (-1, -3, 2, 4):
iob = grid.gridinfo_at_loc((loc.grid_x + dx, loc.grid_y + dy))
@ -58,17 +77,28 @@ def gen_sites():
for site, site_type in iob.sites.items():
if site_type == 'IOB33M':
iobs.append(site)
elif site_type == 'IOB33S':
iobs_s.append(site)
yield tile_name, min(xs), min(ys), sorted(sites), sorted(iobs)
ioi = grid.gridinfo_at_loc((loc.grid_x, loc.grid_y + dy))
for site, site_type in sorted(ioi.sites.items()):
if site_type == 'ILOGICE3':
ilogics.append(site)
break
mmcm_site = mmcm_by_region.get(str(gridinfo.clock_region))
yield tile_name, min(xs), min(ys), sorted(sites), sorted(iobs), sorted(bufio_sites), ilogics, mmcm_site, sorted(iobs_s)
def main():
params_list = []
num_clocks = 0
num_outs = 0
outputs = []
luts = LutMaker()
for tile_name, x_min, y_min, sites, iobs in gen_sites():
for tile_name, x_min, y_min, sites, iobs, bufio_sites, ilogics, mmcm_site, iobs_s in gen_sites():
outs_used = 0
ioclks = []
for iob in iobs:
ioclk = 'clk_{}'.format(iob)
@ -121,8 +151,36 @@ def main():
params['CE'] = luts.get_next_output_net()
params['CLR'] = luts.get_next_output_net()
outputs.append(
'''
params['consumed'] = 0
if outs_used < len(iobs_s) and random.randint(0, 1):
params['consumed'] = 1
params['obuf_site'] = iobs_s[outs_used]
params['out_idx'] = num_outs
num_outs += 1
outs_used += 1
if params['consumed']:
outputs.append(
'''
wire {site}_o;
(* KEEP, DONT_TOUCH, LOC = "{site}" *)
BUFR #(
.BUFR_DIVIDE({BUFR_DIVIDE})
) buf_{site} (
.CE({CE}),
.CLR({CLR}),
.I({I}),
.O({site}_o)
);
wire {site}_q;
(* KEEP, DONT_TOUCH *)
ODDR #(.DDR_CLK_EDGE("SAME_EDGE")) oddr_{site} (
.C({site}_o), .CE(1'b1), .D1(1'b1), .D2(1'b0), .R(1'b0), .S(1'b0), .Q({site}_q));
(* KEEP, DONT_TOUCH, LOC = "{obuf_site}" *)
OBUF #(.IOSTANDARD("LVCMOS33")) obuf_{site} (.I({site}_q), .O(outs[{out_idx}]));
'''.format(**params))
else:
outputs.append(
'''
(* KEEP, DONT_TOUCH, LOC = "{site}" *)
BUFR #(
.BUFR_DIVIDE({BUFR_DIVIDE})
@ -131,14 +189,97 @@ def main():
.CLR({CLR}),
.I({I})
);
'''.format(**params))
'''.format(**params))
params_list.append(params)
# --- BUFIO half (extension v4, fully LOC'd) ---
rec = {}
rec['tile'] = tile_name
rec['kind'] = 'BUFIO_TILE'
rec['bufio_sites'] = [site for site, _, _ in bufio_sites]
rec['bufio_insts'] = []
rec['bufio_src'] = {}
rec['bufio_loc'] = {}
bys = sorted(by for _, _, by in bufio_sites)
by_min = bys[0] if bys else 0
clkout_for_rel_y = {0: 0, 1: 1, 2: 2, 3: 3}
mmcm_declared = False
mmcm_name = 'mmcm_{}'.format(tile_name)
for k, (iob, ioclk) in enumerate(zip(iobs, ioclks)):
if k >= len(ilogics):
break
# A clock-capable IOB feeds ONE dedicated BUFIO of its bank: the k-th
# clock-capable IOB (by y) of the tile's neighbourhood drives the k-th
# BUFIO site (by y). Measured on every HCLK_IOI3 of xc7a100t (24/24,
# bijective); a CCIO-fed BUFIO LOC'd anywhere else fails placement.
bufio_by_y = sorted((st for st, _, _ in bufio_sites), key=lambda st: int(st.split('Y')[-1]))
iobs_by_y = sorted(iobs, key=lambda st: int(st.split('Y')[-1]))
rank = iobs_by_y.index(iob)
if rank >= len(bufio_by_y):
continue
bsite = bufio_by_y[rank]
state = random.choice(('unused', 'CCIO', 'MMCM')) if mmcm_site else random.choice(('unused', 'CCIO'))
if state == 'unused':
continue
inst = 'bufio_{}'.format(iob)
rec['bufio_insts'].append(inst)
rec['bufio_src'][inst] = state
rec['bufio_loc'][inst] = bsite
if state == 'MMCM':
if not mmcm_declared:
mmcm_declared = True
outputs.append(
'''
wire {pll}_fb, {pll}_out0, {pll}_out1, {pll}_out2, {pll}_out3;
(* KEEP, DONT_TOUCH, LOC = "{mmcm_site}" *)
MMCME2_BASE #(
.CLKIN1_PERIOD(10.0), .CLKFBOUT_MULT_F(8.0), .DIVCLK_DIVIDE(1),
.CLKOUT0_DIVIDE_F(8.0), .CLKOUT1_DIVIDE(8), .CLKOUT2_DIVIDE(8), .CLKOUT3_DIVIDE(8)
) {pll} (
.CLKIN1({clkin}), .CLKFBIN({pll}_fb), .CLKFBOUT({pll}_fb),
.CLKOUT0({pll}_out0), .CLKOUT1({pll}_out1), .CLKOUT2({pll}_out2), .CLKOUT3({pll}_out3),
.RST(1'b0), .PWRDWN(1'b0)
);'''.format(pll=mmcm_name, clkin=ioclks[0], mmcm_site=mmcm_site))
rel_y = int(bsite.split('Y')[-1]) - by_min
bufio_i = '{}_out{}'.format(mmcm_name, clkout_for_rel_y[rel_y])
else:
bufio_i = ioclk
outputs.append(
'''
wire {inst}_o;
(* KEEP, DONT_TOUCH, LOC = "{bsite}" *)
BUFIO {inst} (
.I({bufio_i}),
.O({inst}_o)
);
(* KEEP, DONT_TOUCH, LOC = "{ilogic}" *)
ISERDESE2 #(
.DATA_RATE("SDR"),
.DATA_WIDTH(4),
.INTERFACE_TYPE("OVERSAMPLE"),
.IOBDELAY("NONE"),
.NUM_CE(2),
.SERDES_MODE("MASTER")
) iserdes_{inst} (
.CLK({inst}_o),
.CLKB(),
.CLKDIV(),
.D(1'b0),
.DDLY(),
.OFB(),
.OCLKB(),
.RST(),
.SHIFTIN1(),
.SHIFTIN2()
);
'''.format(inst=inst, bufio_i=bufio_i, bsite=bsite, ilogic=ilogics[k]))
params_list.append(rec)
print(
'''
module top(input [{n1}:0] clks);
'''.format(n1=num_clocks - 1))
module top(input [{n1}:0] clks, output [{n2}:0] outs);
'''.format(n1=num_clocks - 1, n2=max(num_outs - 1, 0)))
print("""
(* KEEP, DONT_TOUCH *)