Skip to content

Commit 883699d

Browse files
Attempt to first-class vivado IP re-use with some new rules and
integration into the build system
1 parent a5fbf64 commit 883699d

10 files changed

Lines changed: 215 additions & 31 deletions

File tree

.buckconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ action_cache_address = grpc://172.21.252.165:50051
2525
cas_address = grpc://172.21.252.165:50051
2626
tls = false
2727
instance_name = main
28-
enabled = true
28+
enabled = false
2929
capabilities = true

hdl/projects/cosmo_seq/BUCK

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
load("//tools:hdl.bzl", "vhdl_unit", "black_box")
22
load("//tools:rdl.bzl", "rdl_file")
3-
load("//tools:vivado.bzl", "vivado_bitstream")
3+
load("//tools:vivado.bzl", "vivado_bitstream", "vivado_ip")
44

55
rdl_file(
66
name = "cosmo_seq_top_rdl",
@@ -44,6 +44,13 @@ vhdl_unit(
4444
standard = "2019",
4545
)
4646

47+
vivado_ip(
48+
name = "cosmo_pll_ip",
49+
tcl = "xilinx_ip_gen/cosmo_pll_ip_gen.tcl",
50+
module_name = "cosmo_pll",
51+
part = "xc7s100fgga484-1",
52+
)
53+
4754

4855
vhdl_unit(
4956
name = "cosmo_seq_top",
@@ -71,7 +78,10 @@ vivado_bitstream(
7178
top_entity_name="cosmo_seq_top",
7279
top= ":cosmo_seq_top",
7380
part= "xc7s100fgga484-1",
81+
ip=[":cosmo_pll_ip"],
7482
constraints=glob(["*.xdc"]),
7583
pre_synth_tcl_files=glob(["xilinx_ip_gen/*.tcl"]),
7684
post_synth_tcl_files=glob(["*ila.tcl"]),
85+
pre_synth_tcl_files=glob(["xilinx_ip_gen/*.tcl"]),
86+
#post_synth_tcl_files=glob(["*ila.tcl"]),
7787
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Generate the PLL (copied from tcl console using the IP generator)
2+
# with name and dir adjusted
3+
4+
# Get tclargs here
5+
set name [lindex $argv 0]
6+
set dir [lindex $argv 1]
7+
8+
# generate IP
9+
create_ip -name clk_wiz -vendor xilinx.com -library ip -version 6.0 -module_name $name -dir $dir
10+
set_property -dict [list \
11+
CONFIG.CLKIN1_JITTER_PS {200.0} \
12+
CONFIG.CLKOUT1_JITTER {154.207} \
13+
CONFIG.CLKOUT1_PHASE_ERROR {164.985} \
14+
CONFIG.CLKOUT1_REQUESTED_OUT_FREQ {125.000} \
15+
CONFIG.CLKOUT2_JITTER {142.107} \
16+
CONFIG.CLKOUT2_PHASE_ERROR {164.985} \
17+
CONFIG.CLKOUT2_REQUESTED_OUT_FREQ {200} \
18+
CONFIG.CLKOUT2_USED {true} \
19+
CONFIG.CLK_OUT1_PORT {clk_125m} \
20+
CONFIG.CLK_OUT2_PORT {clk_200m} \
21+
CONFIG.Component_Name {$name} \
22+
CONFIG.MMCM_CLKFBOUT_MULT_F {20.000} \
23+
CONFIG.MMCM_CLKIN1_PERIOD {20.000} \
24+
CONFIG.MMCM_CLKIN2_PERIOD {10.0} \
25+
CONFIG.MMCM_CLKOUT0_DIVIDE_F {8.000} \
26+
CONFIG.MMCM_CLKOUT1_DIVIDE {5} \
27+
CONFIG.NUM_OUT_CLKS {2} \
28+
CONFIG.PRIMARY_PORT {clk_50m} \
29+
CONFIG.PRIM_IN_FREQ {50} \
30+
] [get_ips $name]
31+
synth_ip [get_ips $name]

tools/hdl.bzl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ def _hdl_unit_impl(ctx: AnalysisContext) -> list[Provider]:
9494
providers.append(GenVHDLInfo(src=out_codec_pkg))
9595

9696
# do VUnit stuff here if this is a test bench
97-
# Note that this is not acutally generated in the buck_out/ folder
98-
# After playing with this a bit, putting the vunitout folder in the
97+
# Note that this is not actually generated in the buck_out/ folder
98+
# After playing with this a bit, putting the vunit-out folder in the
9999
# buck_out/ folder can be done (see info below) but is annoying since
100100
# we get a new buck_out/ each time the input changes meaning we have
101101
# to re-compile everything. We *could* attempt to do more here in buck

tools/vivado.bzl

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,77 @@ load(
2222
":compress.bzl", "compress_bitstream",
2323
)
2424

25-
VivadoConstraintInfo = provider(
25+
VivadoIPInfo = provider(
2626
fields={
27-
"srcs": provider_field(Artifact),
27+
"checkpoint": provider_field(Artifact),
28+
"xci": provider_field(Artifact),
29+
"name": provider_field(str, default=""),
2830
}
2931
)
3032

31-
VivadoCheckpointInfo = provider(
32-
fields={
33-
"checkpoint": provider_field(Artifact),
34-
"name":provider_field(str, default=""),
33+
34+
def _vivado_ip(ctx):
35+
""" We're doing some machinations here to try to get a checkpoint and xci file generated by Vivado that we can cache
36+
Vivado's a bit annoying in that it just generates piles of files and you can't strictly specify them. What we want
37+
here is the checkpoint (.dcp) and the xci file so we can read those into the project later. When using read_ip,
38+
Vivado should figure out a checkpoint exists and not rebuild it if it doesn't need to do so.
39+
"""
40+
module_name = ctx.attrs.module_name
41+
tcl = ctx.attrs.tcl
42+
part = ctx.attrs.part
43+
44+
dir_name = "{}_ip/{}".format(module_name, module_name)
45+
46+
# generate a tcl file that sets up an IP compile.
47+
# this wraps the user's script to get outputs we can use.
48+
vivado_ip_tcl = ctx.actions.declare_output("{}.tcl".format(module_name))
49+
py_ip_tcl_gen = ctx.attrs._vivado_ip[RunInfo]
50+
cmd = cmd_args()
51+
cmd.add(py_ip_tcl_gen)
52+
cmd.add("--output", vivado_ip_tcl.as_output())
53+
ctx.actions.run(cmd, category="vivado_ip_tcl_{}_gen".format(module_name), allow_cache_upload=True)
54+
55+
# Now we have a tcl file that we can run in vivado to generate the IP.
56+
# Make buck2 expect a checkpoint file and an xci file in a subdirectory of our choosing.
57+
checkpoint = ctx.actions.declare_output(dir_name, "{}.dcp".format(module_name))
58+
xci = ctx.actions.declare_output(dir_name, "{}.xci".format(module_name))
59+
# Run the vivado command
60+
# Since the tcl above will be the same all the time, we need this action to be re-run
61+
# if the inputs change.
62+
cmd = cmd_args(hidden=[tcl, module_name, part])
3563

36-
})
64+
cmd.add(ctx.attrs._toolchain[RunInfo])
65+
cmd.add("-mode", "batch")
66+
cmd.add("-source", vivado_ip_tcl)
67+
# This is a bit of a hack. Vivado is going to generate these (and more!) regardless of what we
68+
# specify here but this makes buck2 happy since we're "using" the outputs.
69+
cmd.add("-tclargs", module_name, checkpoint.as_output(), xci.as_output(), tcl, part)
3770

71+
ctx.actions.run(cmd, category="vivado_ip_gen", allow_cache_upload=True)
3872

39-
def _vivado_constraint_impl(ctx):
40-
# No dep management to worry about so just collect the things
41-
# and return a ConstraintProvider
42-
return VivadoConstraintInfo(
43-
srcs=ctx.attrs.srcs
44-
)
73+
return [
74+
VivadoIPInfo(checkpoint=checkpoint, xci=xci, name=module_name),
75+
DefaultInfo(default_output=xci),
76+
]
4577

46-
vivado_constraint = rule(
47-
impl=_vivado_constraint_impl,
78+
vivado_ip = rule(
79+
impl=_vivado_ip,
4880
attrs={
49-
"srcs": attrs.list(attrs.source(doc="Expected one or more XDC sources")),
81+
"module_name": attrs.string(),
82+
"tcl": attrs.source(doc="TCL file to generate the IP. expected args are module_name arg[0], output_dir arg[1]"),
83+
"part": attrs.string(doc="Vivado-compatible FPGA string"),
84+
"_vivado_ip": attrs.exec_dep(
85+
doc="Generate a Vivado ip generation tcl for this project",
86+
default="root//tools/vivado_ip:vivado_ip",
87+
),
88+
"_toolchain": attrs.toolchain_dep(
89+
doc="Vivado",
90+
default="toolchains//:vivado",
91+
),
5092
}
5193
)
5294

95+
5396
def _vivado_bitstream(ctx):
5497
synth = synthesize(ctx)
5598
opt = optimize(ctx, synth)
@@ -88,13 +131,15 @@ def synthesize(ctx):
88131
# Get list of all sources from the dep tree via the tset in HDLFileInfo
89132
source_files_tset = ctx.attrs.top[HDLFileInfo].set_all
90133
source_files = source_files_tset.project_as_json("json", ordering="postorder")
134+
ip_xci = [x[VivadoIPInfo].xci for x in ctx.attrs.ip if x.get(VivadoIPInfo)]
91135

92136
out_json = {
93137
"flow": "synthesis",
94138
"part": ctx.attrs.part,
95139
"max_threads": 8,
96140
"synth_args": "",
97141
"sources": source_files,
142+
"ip": ip_xci,
98143
"constraints": constraints,
99144
"top_name": ctx.attrs.top_entity_name,
100145
"pre_synth_tcl_files": ctx.attrs.pre_synth_tcl_files,
@@ -147,7 +192,7 @@ def synthesize(ctx):
147192

148193

149194
# Run vivado
150-
ctx.actions.run(vivado, category="vivado_{}".format(flow))
195+
ctx.actions.run(vivado, category="vivado_{}".format(flow), allow_cache_upload=True)
151196
providers.append(DefaultInfo(default_output=checkpoint))
152197
return providers
153198

@@ -182,7 +227,7 @@ def optimize(ctx, input_checkpoint):
182227

183228
# because we're using the inputs to generate a tcl that *just* lists them,
184229
# irrespective of their content, we make the checkpoint a hidden input
185-
# so that if it changesthis step is re-run rather than just relying
230+
# so that if it changes, this step is re-run rather than just relying
186231
# on cache. We need this step to run if the input file content, or
187232
# constraint file content changes
188233

@@ -198,7 +243,7 @@ def optimize(ctx, input_checkpoint):
198243
vivado.add(debug_probes.as_output())
199244

200245

201-
ctx.actions.run(vivado, category="vivado_{}".format(flow))
246+
ctx.actions.run(vivado, category="vivado_{}".format(flow), allow_cache_upload=True)
202247
providers.append(DefaultInfo(default_output=out_checkpoint))
203248
return providers
204249

@@ -236,7 +281,7 @@ def place(ctx, input_checkpoint, optimize=False):
236281
utilization_report.as_output()
237282
)
238283

239-
ctx.actions.run(vivado, category="vivado_{}".format(flow))
284+
ctx.actions.run(vivado, category="vivado_{}".format(flow), allow_cache_upload=True)
240285
providers.append(DefaultInfo(default_output=out_checkpoint))
241286
return providers
242287

@@ -281,7 +326,7 @@ def route(ctx, input_checkpoint):
281326
)
282327

283328

284-
ctx.actions.run(vivado, category="vivado_{}".format(flow))
329+
ctx.actions.run(vivado, category="vivado_{}".format(flow), allow_cache_upload=True)
285330
providers.append(DefaultInfo(default_output=out_checkpoint))
286331
return providers
287332

@@ -305,7 +350,7 @@ def bitstream(ctx, input_checkpoint):
305350

306351
# because we're using the inputs to generate a tcl that *just* lists them,
307352
# irrespective of their content, we make the checkpoint a hidden input
308-
# so that if it changesthis step is re-run rather than just relying
353+
# so that if it changes this step is re-run rather than just relying
309354
# on cache. We need this step to run if the input file content, or
310355
# constraint file content changes
311356

@@ -316,7 +361,7 @@ def bitstream(ctx, input_checkpoint):
316361
bitstream_bin.as_output(),
317362
)
318363

319-
ctx.actions.run(vivado, category="vivado_{}".format(flow))
364+
ctx.actions.run(vivado, category="vivado_{}".format(flow), allow_cache_upload=True)
320365
providers.append(DefaultInfo(default_output=bitstream_bin))
321366
return providers
322367

@@ -332,7 +377,7 @@ def _vivado_tcl_gen_common(ctx, flow, json):
332377
cmd.add("--input", in_json_file)
333378
cmd.add("--output", vivado_flow_tcl.as_output())
334379

335-
ctx.actions.run(cmd, category="vivado_tcl_{}_gen".format(flow))
380+
ctx.actions.run(cmd, category="vivado_tcl_{}_gen".format(flow), allow_cache_upload=True)
336381

337382
return vivado_flow_tcl, in_json_file
338383

@@ -361,6 +406,13 @@ vivado_bitstream = rule(
361406
attrs={
362407
"top_entity_name": attrs.string(),
363408
"top": attrs.dep(doc="Expected top HDL unit"),
409+
"ip": attrs.list(
410+
attrs.dep(
411+
doc="Vivado IPs to include in the project. These are generated by vivado_ip",
412+
providers=[VivadoIPInfo],
413+
),
414+
default=[],
415+
),
364416
"part": attrs.string(doc="Vivado-compatible FPGA string"),
365417
"constraints": attrs.list(attrs.source(doc="Part constraint files"), default=[]),
366418
"pre_synth_tcl_files": attrs.list(attrs.source(doc="TCL files for project to source for things like ip"), default=[]),

tools/vivado_gen/templates/synth.jinja2

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ set_part {{project.part}}
2020

2121
# Turn inferred latch warnings into errors
2222

23+
# Load any generated IPs
24+
{% for ip in project.ip %}
25+
read_ip {{ip.absolute().as_posix()}}
26+
{% endfor %}
27+
2328
# Load the sources
2429
{% for source in project.sources %}
2530
{% set suffix = source.path.suffix %}
@@ -35,6 +40,7 @@ read_ip {{source.path.absolute().as_posix()}}
3540
{% endif %}
3641
{% endfor %}
3742

43+
3844
## source pre-synth user tcl here
3945
{% for file in project.pre_synth_tcl_files %}
4046
source {{file.absolute().as_posix()}}

tools/vivado_gen/vivado_gen.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def __init__(self,
3434
pre_synth_tcl_files: list,
3535
post_synth_tcl_files: list,
3636
sources: List[Source],
37+
ip: list,
3738
synth_args: List[str],
3839
debug_probes: str,
3940
report_file: str,
@@ -48,6 +49,7 @@ def __init__(self,
4849
self.post_synth_tcl_files = [Path(x) for x in post_synth_tcl_files]
4950
self.debug_probes = debug_probes
5051
self.sources = sources
52+
self.ip = ip
5153
self.synth_args = synth_args
5254
self.report_file = report_file
5355
self.max_threads = max_threads
@@ -56,13 +58,13 @@ def __init__(self,
5658
@classmethod
5759
def from_dict(cls, inputs):
5860
srcs = inputs.get("sources", [])
59-
sources = []
61+
filtered_sources = []
6062
for file in srcs:
6163
# Drop any non-synth files here
6264
if not file.get("is_synth"):
6365
print("Dropping non-synth file {}".format(file))
6466
continue
65-
sources.append(Source(Path(file.get("artifact")), file.get("library"), file.get("standard")))
67+
filtered_sources.append(Source(Path(file.get("artifact")), file.get("library"), file.get("standard")))
6668

6769
return cls(
6870
flow=inputs.get("flow"),
@@ -72,7 +74,8 @@ def from_dict(cls, inputs):
7274
pre_synth_tcl_files=inputs.get("pre_synth_tcl_files", []),
7375
post_synth_tcl_files=inputs.get("post_synth_tcl_files", []),
7476
debug_probes=inputs.get("debug_probes", ""),
75-
sources=sources,
77+
sources=filtered_sources,
78+
ip=[Path(x) for x in inputs.get("ip", [])],
7679
synth_args=inputs.get("synth_args"),
7780
report_file=inputs.get("report_file"),
7881
max_threads=inputs.get("max_threads"),

tools/vivado_ip/BUCK

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
python_binary(
2+
name = 'vivado_ip',
3+
main = 'vivado_ip.py',
4+
deps = [':vivado_ip_lib'],
5+
visibility = ["PUBLIC"],
6+
)
7+
8+
python_library(
9+
name = 'vivado_ip_lib',
10+
srcs = glob(["*.py"]),
11+
base_module = "vivado_ip",
12+
resources = glob(["templates/*.jinja2"])
13+
)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Vivado.
2+
#
3+
# Expects 5 arguments:
4+
# [0]: ip module name
5+
# [1]: expected xci name # We have to pass these in even though we can't specify them to vivado for buck2 as these are "outputs"
6+
# [2]: expected checkpoint name
7+
# [3]: user's tcl file to run
8+
# [4]: target part (if blank use default)
9+
10+
# Get tclargs here
11+
set name [lindex $argv 0]
12+
set part [lindex $argv 4]
13+
# Need to go up 2 levels due to how vivado generates stuff
14+
set dir [file dirname [file dirname [lindex $argv 1]]]
15+
set user_tcl [lindex $argv 3]
16+
17+
# Vivado won't generate into the same folder, it continues appending _<number> to the end of the folder name so we blow away the folder
18+
# here.
19+
file delete -force [file dirname [lindex $argv 1]]
20+
21+
# This is needed to generate IP
22+
if {${part} eq "" } {
23+
create_project -in_memory
24+
} else {
25+
create_project -in_memory -part $part
26+
}
27+
28+
# Clear out argv and change them to what our user's want
29+
# Expects users expect only 2 arguments, the module name and the output directory:
30+
set argv [list $name $dir]
31+
# Run the IP generation TCL
32+
source $user_tcl

0 commit comments

Comments
 (0)