From 234c08dfb21291acc7e5ac748b4a9f386b737c44 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Thu, 14 May 2026 19:08:52 +0200 Subject: [PATCH 01/15] add initial Synthesis object with function to generate sources for Vivado --- src/main/scala/approx/util/Synthesis.scala | 66 +++++++++++++++++++ .../scala/approx/util/SynthesisSpec.scala | 25 +++++++ 2 files changed, 91 insertions(+) create mode 100644 src/main/scala/approx/util/Synthesis.scala create mode 100644 src/test/scala/approx/util/SynthesisSpec.scala diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala new file mode 100644 index 0000000..d279376 --- /dev/null +++ b/src/main/scala/approx/util/Synthesis.scala @@ -0,0 +1,66 @@ +package approx.util + +import chisel3.RawModule + +import circt.stage.ChiselStage + +import java.nio.file.{Files, Paths} + +import scala.util.{Try, Success, Failure} + +object Synthesis { + + /** Generates Vivado synthesis and implementation TCL scripts along with the + * corresponding SystemVerilog source for a given Chisel module + * + * @param gen a function that generates the Chisel module to be synthesized + * @param part the target FPGA part number (defaults to "xc7a35t") + * @return a Try containing a tuple of (build directory, SystemVerilog + * filename, synthesis TCL filename, implementation TCL filename) + */ + def generateVivadoSources(gen: () => RawModule, part: String = "xc7a35t"): Try[(String, String, String, String)] = { + Try { + // Generate SystemVerilog source first to get the module name; + // regex extraction assumes the top module is the last one defined + val moduleNameRegex = "module\\s+([A-Za-z_]\\w*)\\s*\\(".r + val sv = ChiselStage.emitSystemVerilog(gen()) + val topName = moduleNameRegex.findAllMatchIn(sv) + .map(_.group(1)).toList + .lastOption + .getOrElse(throw new RuntimeException("Failed to extract top module name from generated SystemVerilog")) + + // Ensure unique build directory exists + val buildDir = Paths.get(s"build/Vivado/${topName}_${part}") + Files.createDirectories(buildDir) + + // Generate SystemVerilog source file + val svFile = s"${topName}.sv" + Files.write(buildDir.resolve(svFile), sv.getBytes) + + // Generate TCL synthesis script + val synTclFile = s"${topName}_syn.tcl" + val synTcl = s""" + |read_verilog ${svFile} + |synth_design -top ${topName} -part ${part} + |opt_design + |report_utilization -file ${topName}_syn.rpt + |write_checkpoint -force ${topName}_syn.dcp + """.stripMargin + Files.write(buildDir.resolve(synTclFile), synTcl.getBytes) + + // Generate TCL implementation script + val implTclFile = s"${topName}_impl.tcl" + val implTcl = s""" + |read_checkpoint -force ${topName}_syn.dcp + |opt_design + |place_design + |route_design + |report_utilization -file ${topName}_impl.rpt + |write_checkpoint -force ${topName}_impl.dcp + """.stripMargin + Files.write(buildDir.resolve(implTclFile), implTcl.getBytes) + + (buildDir.toString, svFile, synTclFile, implTclFile) + } + } +} diff --git a/src/test/scala/approx/util/SynthesisSpec.scala b/src/test/scala/approx/util/SynthesisSpec.scala new file mode 100644 index 0000000..4506dd4 --- /dev/null +++ b/src/test/scala/approx/util/SynthesisSpec.scala @@ -0,0 +1,25 @@ +package approx.util + +import java.nio.file.{Files, Paths} + +import scala.util.{Try, Success, Failure} + +import org.scalatest.matchers.should.Matchers +import org.scalatest.flatspec.AnyFlatSpec + +class SynthesisSpec extends AnyFlatSpec with Matchers { + behavior of "Synthesis" + + it should "generate Vivado synthesis and implementation sources for an RCA adder" in { + val (dir, sv, syn, impl) = Synthesis.generateVivadoSources(() => new approx.addition.RCA(8)) match { + case Success(result) => result + case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") + } + val svPath = Paths.get(dir, sv) + val synPath = Paths.get(dir, syn) + val implPath = Paths.get(dir, impl) + Files.exists(svPath) shouldBe true + Files.exists(synPath) shouldBe true + Files.exists(implPath) shouldBe true + } +} From 436a3fa7dab388111a9d76dbfb6db13bef6a2d15 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Thu, 14 May 2026 19:52:29 +0200 Subject: [PATCH 02/15] prepare running Vivado synthesis and implementation with helper makefile --- src/main/scala/approx/util/Synthesis.scala | 57 ++++++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index d279376..ae7b9c0 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -10,6 +10,10 @@ import scala.util.{Try, Success, Failure} object Synthesis { + case class VivadoSynthesisResults(buildDir: String, success: Boolean, synReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + + case class VivadoImplementationResults(buildDir: String, success: Boolean, implReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + /** Generates Vivado synthesis and implementation TCL scripts along with the * corresponding SystemVerilog source for a given Chisel module * @@ -39,28 +43,71 @@ object Synthesis { // Generate TCL synthesis script val synTclFile = s"${topName}_syn.tcl" + val synRptFile = s"${topName}_syn.rpt" + val synDcpFile = s"${topName}_syn.dcp" val synTcl = s""" |read_verilog ${svFile} |synth_design -top ${topName} -part ${part} |opt_design - |report_utilization -file ${topName}_syn.rpt - |write_checkpoint -force ${topName}_syn.dcp + |report_utilization -file ${synRptFile} + |write_checkpoint -force ${synDcpFile} """.stripMargin Files.write(buildDir.resolve(synTclFile), synTcl.getBytes) // Generate TCL implementation script val implTclFile = s"${topName}_impl.tcl" + val implRptFile = s"${topName}_impl.rpt" + val implDcpFile = s"${topName}_impl.dcp" val implTcl = s""" - |read_checkpoint -force ${topName}_syn.dcp + |read_checkpoint -force ${synDcpFile} |opt_design |place_design |route_design - |report_utilization -file ${topName}_impl.rpt - |write_checkpoint -force ${topName}_impl.dcp + |report_utilization -file ${implRptFile} + |write_checkpoint -force ${implDcpFile} """.stripMargin Files.write(buildDir.resolve(implTclFile), implTcl.getBytes) + // Generate helper Makefile + val make = s""" + |SV_FILE =${svFile} + | + |VIVADO_SYN_TCL =${synTclFile} + |VIVADO_SYN_REPORT =${synRptFile} + | + |VIVADO_IMPL_TCL =${implTclFile} + |VIVADO_IMPL_REPORT =${implRptFile} + | + |.PHONY: clean + |# Remove generated reports + |clean: + |\trm -f $${VIVADO_SYN_REPORT} $${VIVADO_IMPL_REPORT} + | + |.PHONY: syn + |# Run synthesis to generate synthesis report $${VIVADO_SYN_REPORT} + |syn: $${VIVADO_SYN_REPORT} + | + |$${VIVADO_SYN_REPORT}: $${SV_FILE} $${VIVADO_SYN_TCL} + |\tvivado -mode batch -source ${synTclFile} + | + |.PHONY: impl + |# Run implementation to generate implementation report $${VIVADO_IMPL_REPORT} + |impl: $${VIVADO_IMPL_REPORT} + | + |$${VIVADO_IMPL_REPORT}: $${VIVADO_SYN_REPORT} $${VIVADO_IMPL_TCL} + |\tvivado -mode batch -source ${implTclFile} + """.stripMargin + Files.write(buildDir.resolve("Makefile"), make.getBytes) + (buildDir.toString, svFile, synTclFile, implTclFile) } } + + def runVivadoSynthesis(dir: String): VivadoSynthesisResults = { + ??? + } + + def runVivadoImplementation(dir: String): VivadoImplementationResults = { + ??? + } } From 5153ada5150f65efbb7ae0d7b17c46c172b43d96 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Thu, 14 May 2026 21:12:00 +0200 Subject: [PATCH 03/15] disable Verification layer in generated systemverilog --- src/main/scala/approx/util/Synthesis.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index ae7b9c0..6a9780f 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -27,7 +27,7 @@ object Synthesis { // Generate SystemVerilog source first to get the module name; // regex extraction assumes the top module is the last one defined val moduleNameRegex = "module\\s+([A-Za-z_]\\w*)\\s*\\(".r - val sv = ChiselStage.emitSystemVerilog(gen()) + val sv = ChiselStage.emitSystemVerilog(gen(), firtoolOpts = Array("--disable-layers", "Verification")) val topName = moduleNameRegex.findAllMatchIn(sv) .map(_.group(1)).toList .lastOption From 550ea572e24f82c4b8801a82689bf50f00146591 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Thu, 14 May 2026 21:24:47 +0200 Subject: [PATCH 04/15] fix non-project mode tcl scripts --- src/main/scala/approx/util/Synthesis.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index 6a9780f..582cb30 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -48,7 +48,6 @@ object Synthesis { val synTcl = s""" |read_verilog ${svFile} |synth_design -top ${topName} -part ${part} - |opt_design |report_utilization -file ${synRptFile} |write_checkpoint -force ${synDcpFile} """.stripMargin @@ -59,7 +58,8 @@ object Synthesis { val implRptFile = s"${topName}_impl.rpt" val implDcpFile = s"${topName}_impl.dcp" val implTcl = s""" - |read_checkpoint -force ${synDcpFile} + |read_checkpoint ${synDcpFile} + |link_design |opt_design |place_design |route_design From 03bbf7eb026fbfd8ac237c98dad22c44114b56d2 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Thu, 14 May 2026 22:25:28 +0200 Subject: [PATCH 05/15] move tcl script generation into make --- src/main/scala/approx/util/Synthesis.scala | 119 +++++++++++------- .../scala/approx/util/SynthesisSpec.scala | 6 +- 2 files changed, 73 insertions(+), 52 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index 582cb30..d31e15c 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -10,19 +10,29 @@ import scala.util.{Try, Success, Failure} object Synthesis { + final val VivadoBuildDir = "build/Vivado" + case class VivadoSynthesisResults(buildDir: String, success: Boolean, synReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) case class VivadoImplementationResults(buildDir: String, success: Boolean, implReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + /** Generate a helper Makefile for synthesis and implementation + * + * @param dir the directory where the Makefile should be created + */ + private[Synthesis] def generateHelperMakefile(dir: String) = { + Files.createDirectories(Paths.get(dir)) + Files.write(Paths.get(dir, "Makefile"), "include *.mk".getBytes) + } + /** Generates Vivado synthesis and implementation TCL scripts along with the * corresponding SystemVerilog source for a given Chisel module * * @param gen a function that generates the Chisel module to be synthesized * @param part the target FPGA part number (defaults to "xc7a35t") - * @return a Try containing a tuple of (build directory, SystemVerilog - * filename, synthesis TCL filename, implementation TCL filename) + * @return a Try containing a tuple of (build directory, SV filename) */ - def generateVivadoSources(gen: () => RawModule, part: String = "xc7a35t"): Try[(String, String, String, String)] = { + def generateVivadoSources(gen: () => RawModule, part: String = "xc7a35t"): Try[(String, String)] = { Try { // Generate SystemVerilog source first to get the module name; // regex extraction assumes the top module is the last one defined @@ -34,72 +44,87 @@ object Synthesis { .getOrElse(throw new RuntimeException("Failed to extract top module name from generated SystemVerilog")) // Ensure unique build directory exists - val buildDir = Paths.get(s"build/Vivado/${topName}_${part}") + val buildDir = Paths.get(s"${VivadoBuildDir}/${topName}_${part}") Files.createDirectories(buildDir) // Generate SystemVerilog source file val svFile = s"${topName}.sv" Files.write(buildDir.resolve(svFile), sv.getBytes) - // Generate TCL synthesis script - val synTclFile = s"${topName}_syn.tcl" - val synRptFile = s"${topName}_syn.rpt" - val synDcpFile = s"${topName}_syn.dcp" - val synTcl = s""" + // Generate helper Makefile + val make = s""" + |SV_FILE =${svFile} + | + |VIVADO_SYN_TCL =${topName}_syn.tcl + |VIVADO_SYN_DCP =${topName}_syn.dcp + |VIVADO_SYN_REPORT =${topName}_syn.rpt + | + |VIVADO_IMPL_TCL =${topName}_impl.tcl + |VIVADO_IMPL_DCP =${topName}_impl.dcp + |VIVADO_IMPL_REPORT =${topName}_impl.rpt + | + |# Macros to generate TCL scripts for synthesis and implementation + |define VIVADO_SYN_TCL_CONTENT |read_verilog ${svFile} |synth_design -top ${topName} -part ${part} - |report_utilization -file ${synRptFile} - |write_checkpoint -force ${synDcpFile} - """.stripMargin - Files.write(buildDir.resolve(synTclFile), synTcl.getBytes) - - // Generate TCL implementation script - val implTclFile = s"${topName}_impl.tcl" - val implRptFile = s"${topName}_impl.rpt" - val implDcpFile = s"${topName}_impl.dcp" - val implTcl = s""" - |read_checkpoint ${synDcpFile} + |report_utilization -file $$(VIVADO_SYN_REPORT) + |write_checkpoint -force $$(VIVADO_SYN_DCP) + | + |endef + | + |$$(VIVADO_SYN_TCL): + |\t$$(file >$$@,$$(VIVADO_SYN_TCL_CONTENT)) + | + |define VIVADO_IMPL_TCL_CONTENT + |read_checkpoint $$(VIVADO_SYN_DCP) |link_design |opt_design |place_design |route_design - |report_utilization -file ${implRptFile} - |write_checkpoint -force ${implDcpFile} - """.stripMargin - Files.write(buildDir.resolve(implTclFile), implTcl.getBytes) - - // Generate helper Makefile - val make = s""" - |SV_FILE =${svFile} + |report_utilization -file $$(VIVADO_IMPL_REPORT) + |write_checkpoint -force $$(VIVADO_IMPL_DCP) + | + |endef | - |VIVADO_SYN_TCL =${synTclFile} - |VIVADO_SYN_REPORT =${synRptFile} + |$$(VIVADO_IMPL_TCL): + |\t$$(file >$$@,$$(VIVADO_IMPL_TCL_CONTENT)) | - |VIVADO_IMPL_TCL =${implTclFile} - |VIVADO_IMPL_REPORT =${implRptFile} + |# Helper clean targets for generated files + |.PHONY: clean-vivado-tcl + |# Remove generated TCL scripts + |clean-vivado-tcl: + |\trm -f $$(VIVADO_SYN_TCL) $$(VIVADO_IMPL_TCL) | - |.PHONY: clean + |.PHONY: clean-vivado-rpt |# Remove generated reports - |clean: - |\trm -f $${VIVADO_SYN_REPORT} $${VIVADO_IMPL_REPORT} + |clean-vivado-rpt: + |\trm -f $$(VIVADO_SYN_REPORT) $$(VIVADO_IMPL_REPORT) | - |.PHONY: syn - |# Run synthesis to generate synthesis report $${VIVADO_SYN_REPORT} - |syn: $${VIVADO_SYN_REPORT} + |.PHONY: clean-vivado-dcp + |# Remove generated checkpoints + |clean-vivado-dcp: + |\trm -f $$(VIVADO_SYN_DCP) $$(VIVADO_IMPL_DCP) | - |$${VIVADO_SYN_REPORT}: $${SV_FILE} $${VIVADO_SYN_TCL} - |\tvivado -mode batch -source ${synTclFile} + |$$(VIVADO_SYN_REPORT): $$(SV_FILE) $$(VIVADO_SYN_TCL) + |\tvivado -mode batch -source $$(VIVADO_SYN_TCL) | - |.PHONY: impl - |# Run implementation to generate implementation report $${VIVADO_IMPL_REPORT} - |impl: $${VIVADO_IMPL_REPORT} + |.PHONY: vivado-syn + |# Run synthesis to generate synthesis report $$(VIVADO_SYN_REPORT) + |vivado-syn: $$(VIVADO_SYN_REPORT) | - |$${VIVADO_IMPL_REPORT}: $${VIVADO_SYN_REPORT} $${VIVADO_IMPL_TCL} - |\tvivado -mode batch -source ${implTclFile} + |$$(VIVADO_IMPL_REPORT): $$(VIVADO_SYN_REPORT) $$(VIVADO_IMPL_TCL) + |\tvivado -mode batch -source $$(VIVADO_IMPL_TCL) + | + |.PHONY: vivado-impl + |# Run implementation to generate implementation report $$(VIVADO_IMPL_REPORT) + |vivado-impl: $$(VIVADO_IMPL_REPORT) """.stripMargin - Files.write(buildDir.resolve("Makefile"), make.getBytes) + Files.write(buildDir.resolve("vivado.mk"), make.getBytes) + + // Generate local and common helper Makefiles + generateHelperMakefile(buildDir.toString) - (buildDir.toString, svFile, synTclFile, implTclFile) + (buildDir.toString, svFile) } } diff --git a/src/test/scala/approx/util/SynthesisSpec.scala b/src/test/scala/approx/util/SynthesisSpec.scala index 4506dd4..7a72c7a 100644 --- a/src/test/scala/approx/util/SynthesisSpec.scala +++ b/src/test/scala/approx/util/SynthesisSpec.scala @@ -11,15 +11,11 @@ class SynthesisSpec extends AnyFlatSpec with Matchers { behavior of "Synthesis" it should "generate Vivado synthesis and implementation sources for an RCA adder" in { - val (dir, sv, syn, impl) = Synthesis.generateVivadoSources(() => new approx.addition.RCA(8)) match { + val (dir, sv) = Synthesis.generateVivadoSources(() => new approx.addition.RCA(8)) match { case Success(result) => result case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") } val svPath = Paths.get(dir, sv) - val synPath = Paths.get(dir, syn) - val implPath = Paths.get(dir, impl) Files.exists(svPath) shouldBe true - Files.exists(synPath) shouldBe true - Files.exists(implPath) shouldBe true } } From 45d19ca2aa23420deebee0342a790f5f7909ac27 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Thu, 14 May 2026 22:59:58 +0200 Subject: [PATCH 06/15] enable generating and opening a Vivado project --- src/main/scala/approx/util/Synthesis.scala | 68 ++++++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index d31e15c..4fbd4ff 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -53,20 +53,26 @@ object Synthesis { // Generate helper Makefile val make = s""" - |SV_FILE =${svFile} + |SV_FILE :=${svFile} | - |VIVADO_SYN_TCL =${topName}_syn.tcl - |VIVADO_SYN_DCP =${topName}_syn.dcp - |VIVADO_SYN_REPORT =${topName}_syn.rpt + |VIVADO_PART :=${part} | - |VIVADO_IMPL_TCL =${topName}_impl.tcl - |VIVADO_IMPL_DCP =${topName}_impl.dcp - |VIVADO_IMPL_REPORT =${topName}_impl.rpt + |VIVADO_PROJ_DIR :=${topName} + |VIVADO_PROJ_TCL :=${topName}_proj.tcl + |VIVADO_PROJ_XPR :=$$(VIVADO_PROJ_DIR)/${topName}.xpr + | + |VIVADO_SYN_TCL :=${topName}_syn.tcl + |VIVADO_SYN_DCP :=${topName}_syn.dcp + |VIVADO_SYN_REPORT :=${topName}_syn.rpt + | + |VIVADO_IMPL_TCL :=${topName}_impl.tcl + |VIVADO_IMPL_DCP :=${topName}_impl.dcp + |VIVADO_IMPL_REPORT :=${topName}_impl.rpt | |# Macros to generate TCL scripts for synthesis and implementation |define VIVADO_SYN_TCL_CONTENT - |read_verilog ${svFile} - |synth_design -top ${topName} -part ${part} + |read_verilog $$(SV_FILE) + |synth_design -top ${topName} -part $$(VIVADO_PART) |report_utilization -file $$(VIVADO_SYN_REPORT) |write_checkpoint -force $$(VIVADO_SYN_DCP) | @@ -89,11 +95,49 @@ object Synthesis { |$$(VIVADO_IMPL_TCL): |\t$$(file >$$@,$$(VIVADO_IMPL_TCL_CONTENT)) | + |define VIVADO_PROJ_TCL_CONTENT + |create_project -force $$(VIVADO_PROJ_XPR) -part $$(VIVADO_PART) + |add_files $$(SV_FILE) + |set_property top ${topName} [current_fileset] + |update_compile_order -fileset sources_1 + | + |endef + | + |$$(VIVADO_PROJ_TCL): + |\t$$(file >$$@,$$(VIVADO_PROJ_TCL_CONTENT)) + | + |# Helpers to generate and open a Vivado project for interactive exploration + |$$(VIVADO_PROJ_XPR): $$(VIVADO_PROJ_TCL) + |\tvivado -nolog -nojournal -mode batch -source $$< + | + |.PHONY: generate-vivado-project + |# Generate Vivado project file + |generate-vivado-project: $$(VIVADO_PROJ_XPR) + | + |.PHONY: open-vivado-gui + |# Open Vivado GUI with the generated project + |open-vivado-gui: $$(VIVADO_PROJ_XPR) + |\tvivado -nolog -nojournal $$< + | + |.PHONY: open-vivado-tcl + |# Open Vivado TCL with the generated project + |open-vivado-tcl: $$(VIVADO_PROJ_XPR) + |\tvivado -nolog -nojournal -mode tcl $$< + | |# Helper clean targets for generated files + |.PHONY: clean-vivado + |# Remove all generated Vivado files + |clean-vivado: clean-vivado-project clean-vivado-tcl clean-vivado-rpt clean-vivado-dcp + | + |.PHONY: clean-vivado-project + |# Remove generated Vivado project files + |clean-vivado-project: + |\trm -rf $$(VIVADO_PROJ_DIR) + | |.PHONY: clean-vivado-tcl |# Remove generated TCL scripts |clean-vivado-tcl: - |\trm -f $$(VIVADO_SYN_TCL) $$(VIVADO_IMPL_TCL) + |\trm -f $$(VIVADO_PROJ_TCL) $$(VIVADO_SYN_TCL) $$(VIVADO_IMPL_TCL) | |.PHONY: clean-vivado-rpt |# Remove generated reports @@ -106,14 +150,14 @@ object Synthesis { |\trm -f $$(VIVADO_SYN_DCP) $$(VIVADO_IMPL_DCP) | |$$(VIVADO_SYN_REPORT): $$(SV_FILE) $$(VIVADO_SYN_TCL) - |\tvivado -mode batch -source $$(VIVADO_SYN_TCL) + |\tvivado -nolog -nojournal -mode batch -source $$(VIVADO_SYN_TCL) | |.PHONY: vivado-syn |# Run synthesis to generate synthesis report $$(VIVADO_SYN_REPORT) |vivado-syn: $$(VIVADO_SYN_REPORT) | |$$(VIVADO_IMPL_REPORT): $$(VIVADO_SYN_REPORT) $$(VIVADO_IMPL_TCL) - |\tvivado -mode batch -source $$(VIVADO_IMPL_TCL) + |\tvivado -nolog -nojournal -mode batch -source $$(VIVADO_IMPL_TCL) | |.PHONY: vivado-impl |# Run implementation to generate implementation report $$(VIVADO_IMPL_REPORT) From 93c69493ff84b63220917a29c07b2fc51ac086ce Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Fri, 15 May 2026 10:47:12 +0200 Subject: [PATCH 07/15] implement runVivadoSynthesis --- src/main/scala/approx/util/Synthesis.scala | 55 ++++++++++++++++++- .../scala/approx/util/SynthesisSpec.scala | 34 ++++++++++-- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index 4fbd4ff..a99a837 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -6,19 +6,27 @@ import circt.stage.ChiselStage import java.nio.file.{Files, Paths} +import scala.jdk.CollectionConverters._ +import scala.sys.process._ import scala.util.{Try, Success, Failure} object Synthesis { final val VivadoBuildDir = "build/Vivado" - case class VivadoSynthesisResults(buildDir: String, success: Boolean, synReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + final val VivadoLUTRegex = """\|\s*Slice\s+LUTs\*\s*\|\s*(\d+)\s*\|""".r + final val VivadoFFRegex = """\|\s*Slice\s+Registers\s*\|\s*(\d+)\s*\|""".r + final val VivadoDSPRegex = """\|\s*DSPs\s*\|\s*(\d+)\s*\|""".r - case class VivadoImplementationResults(buildDir: String, success: Boolean, implReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + case class VivadoSynthesisResults(buildDir: String, synReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + + case class VivadoImplementationResults(buildDir: String, implReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) /** Generate a helper Makefile for synthesis and implementation * * @param dir the directory where the Makefile should be created + * + * TODO extend with help and print-all-variables targets */ private[Synthesis] def generateHelperMakefile(dir: String) = { Files.createDirectories(Paths.get(dir)) @@ -172,8 +180,49 @@ object Synthesis { } } + /** Runs Vivado synthesis using the generated Makefile and parses the + * resulting synthesis report to extract and bundle resource utilization + * metrics into a structured result + * + * @param dir the directory containing the generated Makefile and sources + * @return a [[VivadoSynthesisResults]] instance including synthesis success + * status and resource utilization metrics, if available + */ def runVivadoSynthesis(dir: String): VivadoSynthesisResults = { - ??? + // Attempt to launch Vivado synthesis using the generated Makefile assumed + // to exist under dir + println(s"Running Vivado synthesis: make -C $dir vivado-syn") + val stdout = new StringBuilder + val logger = ProcessLogger(line => stdout.append(line).append("\n")) + val exitCode = Process(Seq("make", "-C", dir, "vivado-syn")).!(logger) + if (exitCode != 0) { + println(s"Vivado synthesis failed with exit code $exitCode") + println(s"Vivado output:\n${stdout}") + return VivadoSynthesisResults(dir, None, None, None, None) + } + + // Parse synthesis report to extract resource utilization + val synReportOpt = { + val stream = Files.newDirectoryStream(Paths.get(dir), "*_syn.rpt") + try { // get first matching file, if any + stream.iterator().asScala.toList.headOption + } finally { + stream.close() + } + } + synReportOpt match { + case None => + println(s"Synthesis report not found in directory: $dir") + return VivadoSynthesisResults(dir, None, None, None, None) + case _ => + } + + // Read the synthesis report and extract LUT, FF, and DSP counts using regex + val synRptContent = new String(Files.readAllBytes(synReportOpt.get)) + val lutCount = VivadoLUTRegex.findFirstMatchIn(synRptContent).map(_.group(1).toInt) + val ffCount = VivadoFFRegex .findFirstMatchIn(synRptContent).map(_.group(1).toInt) + val dspCount = VivadoDSPRegex.findFirstMatchIn(synRptContent).map(_.group(1).toInt) + VivadoSynthesisResults(dir, synReportOpt.map(_.toString), lutCount, ffCount, dspCount) } def runVivadoImplementation(dir: String): VivadoImplementationResults = { diff --git a/src/test/scala/approx/util/SynthesisSpec.scala b/src/test/scala/approx/util/SynthesisSpec.scala index 7a72c7a..5ad1039 100644 --- a/src/test/scala/approx/util/SynthesisSpec.scala +++ b/src/test/scala/approx/util/SynthesisSpec.scala @@ -10,12 +10,34 @@ import org.scalatest.flatspec.AnyFlatSpec class SynthesisSpec extends AnyFlatSpec with Matchers { behavior of "Synthesis" - it should "generate Vivado synthesis and implementation sources for an RCA adder" in { - val (dir, sv) = Synthesis.generateVivadoSources(() => new approx.addition.RCA(8)) match { - case Success(result) => result - case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") + val runVivado = System.getenv().containsKey("XILINX_VIVADO") + + if (runVivado) { + val vivadoPath = System.getenv("XILINX_VIVADO") + println(s"Vivado environment detected at ${vivadoPath}") + + it should "generate Vivado synthesis and implementation sources for an RCA adder" in { + val (dir, sv) = Synthesis.generateVivadoSources(() => new approx.addition.RCA(8)) match { + case Success(result) => result + case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") + } + val svPath = Paths.get(dir, sv) + Files.exists(svPath) shouldBe true + } + + it should "run Vivado synthesis and parse results for an RCA adder" in { + val (dir, sv) = Synthesis.generateVivadoSources(() => new approx.addition.RCA(8)) match { + case Success(result) => result + case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") + } + val results = Synthesis.runVivadoSynthesis(dir) + results.synReport shouldBe defined + results.lut shouldBe defined + results.ff shouldBe defined + results.dsp shouldBe defined + results.lut should equal(Some(8)) } - val svPath = Paths.get(dir, sv) - Files.exists(svPath) shouldBe true + } else { + println("Vivado environment not detected; skipping synthesis tests") } } From 355dc601c75520a13b9a18b9d2a706597ee6047e Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Fri, 15 May 2026 11:08:51 +0200 Subject: [PATCH 08/15] implement runVivadoImplementation --- src/main/scala/approx/util/Synthesis.scala | 68 +++++++++++++++---- .../scala/approx/util/SynthesisSpec.scala | 30 ++++++-- 2 files changed, 80 insertions(+), 18 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index a99a837..804535d 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -14,13 +14,14 @@ object Synthesis { final val VivadoBuildDir = "build/Vivado" - final val VivadoLUTRegex = """\|\s*Slice\s+LUTs\*\s*\|\s*(\d+)\s*\|""".r + final val VivadoSynthesisLUTRegex = """\|\s*Slice\s+LUTs\*\s*\|\s*(\d+)\s*\|""".r + final val VivadoImplementationLUTRegex = """\|\s*Slice\s+LUTs\s*\|\s*(\d+)\s*\|""".r final val VivadoFFRegex = """\|\s*Slice\s+Registers\s*\|\s*(\d+)\s*\|""".r final val VivadoDSPRegex = """\|\s*DSPs\s*\|\s*(\d+)\s*\|""".r - case class VivadoSynthesisResults(buildDir: String, synReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + case class VivadoSynthesisResults(buildDir: String, report: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) - case class VivadoImplementationResults(buildDir: String, implReport: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + case class VivadoImplementationResults(buildDir: String, report: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) /** Generate a helper Makefile for synthesis and implementation * @@ -185,8 +186,8 @@ object Synthesis { * metrics into a structured result * * @param dir the directory containing the generated Makefile and sources - * @return a [[VivadoSynthesisResults]] instance including synthesis success - * status and resource utilization metrics, if available + * @return a [[VivadoSynthesisResults]] instance including status and + * resource utilization metrics, if available */ def runVivadoSynthesis(dir: String): VivadoSynthesisResults = { // Attempt to launch Vivado synthesis using the generated Makefile assumed @@ -202,7 +203,7 @@ object Synthesis { } // Parse synthesis report to extract resource utilization - val synReportOpt = { + val synRptOpt = { val stream = Files.newDirectoryStream(Paths.get(dir), "*_syn.rpt") try { // get first matching file, if any stream.iterator().asScala.toList.headOption @@ -210,7 +211,7 @@ object Synthesis { stream.close() } } - synReportOpt match { + synRptOpt match { case None => println(s"Synthesis report not found in directory: $dir") return VivadoSynthesisResults(dir, None, None, None, None) @@ -218,14 +219,55 @@ object Synthesis { } // Read the synthesis report and extract LUT, FF, and DSP counts using regex - val synRptContent = new String(Files.readAllBytes(synReportOpt.get)) - val lutCount = VivadoLUTRegex.findFirstMatchIn(synRptContent).map(_.group(1).toInt) - val ffCount = VivadoFFRegex .findFirstMatchIn(synRptContent).map(_.group(1).toInt) - val dspCount = VivadoDSPRegex.findFirstMatchIn(synRptContent).map(_.group(1).toInt) - VivadoSynthesisResults(dir, synReportOpt.map(_.toString), lutCount, ffCount, dspCount) + val synRptContent = new String(Files.readAllBytes(synRptOpt.get)) + val lutCount = VivadoSynthesisLUTRegex.findFirstMatchIn(synRptContent).map(_.group(1).toInt) + val ffCount = VivadoFFRegex .findFirstMatchIn(synRptContent).map(_.group(1).toInt) + val dspCount = VivadoDSPRegex .findFirstMatchIn(synRptContent).map(_.group(1).toInt) + VivadoSynthesisResults(dir, synRptOpt.map(_.toString), lutCount, ffCount, dspCount) } + /** Runs Vivado implementation using the generated Makefile and parses the + * resulting implementation report to extract and bundle resource utilization + * metrics into a structured result + * + * @param dir the directory containing the generated Makefile and sources + * @return a [[VivadoImplementationResults]] instance including status and + * resource utilization metrics, if available + */ def runVivadoImplementation(dir: String): VivadoImplementationResults = { - ??? + // Attempt to launch Vivado implementation using the generated Makefile + // assumed to exist under dir + println(s"Running Vivado implementation: make -C $dir vivado-impl") + val stdout = new StringBuilder + val logger = ProcessLogger(line => stdout.append(line).append("\n")) + val exitCode = Process(Seq("make", "-C", dir, "vivado-impl")).!(logger) + if (exitCode != 0) { + println(s"Vivado implementation failed with exit code $exitCode") + println(s"Vivado output:\n${stdout}") + return VivadoImplementationResults(dir, None, None, None, None) + } + + // Parse implementation report to extract resource utilization + val implRptOpt = { + val stream = Files.newDirectoryStream(Paths.get(dir), "*_impl.rpt") + try { // get first matching file, if any + stream.iterator().asScala.toList.headOption + } finally { + stream.close() + } + } + implRptOpt match { + case None => + println(s"Implementation report not found in directory: $dir") + return VivadoImplementationResults(dir, None, None, None, None) + case _ => + } + + // Read the implementation report and extract LUT, FF, and DSP counts using regex + val implRptContent = new String(Files.readAllBytes(implRptOpt.get)) + val lutCount = VivadoImplementationLUTRegex.findFirstMatchIn(implRptContent).map(_.group(1).toInt) + val ffCount = VivadoFFRegex .findFirstMatchIn(implRptContent).map(_.group(1).toInt) + val dspCount = VivadoDSPRegex .findFirstMatchIn(implRptContent).map(_.group(1).toInt) + VivadoImplementationResults(dir, implRptOpt.map(_.toString), lutCount, ffCount, dspCount) } } diff --git a/src/test/scala/approx/util/SynthesisSpec.scala b/src/test/scala/approx/util/SynthesisSpec.scala index 5ad1039..f61e0ef 100644 --- a/src/test/scala/approx/util/SynthesisSpec.scala +++ b/src/test/scala/approx/util/SynthesisSpec.scala @@ -31,11 +31,31 @@ class SynthesisSpec extends AnyFlatSpec with Matchers { case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") } val results = Synthesis.runVivadoSynthesis(dir) - results.synReport shouldBe defined - results.lut shouldBe defined - results.ff shouldBe defined - results.dsp shouldBe defined - results.lut should equal(Some(8)) + println(s"Synthesis results: ${results}") + results.report shouldBe defined + results.lut shouldBe defined + results.lut should equal(Some(8)) + results.ff shouldBe defined + results.ff should equal(Some(0)) + results.dsp shouldBe defined + results.dsp should equal(Some(0)) + } + + it should "run Vivado implementation and parse results for an RCA adder" in { + val (dir, sv) = Synthesis.generateVivadoSources(() => new approx.addition.RCA(8)) match { + case Success(result) => result + case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") + } + // implementation indirectly calls synthesis + val implResults = Synthesis.runVivadoImplementation(dir) + println(s"Implementation results: ${implResults}") + implResults.report shouldBe defined + implResults.lut shouldBe defined + implResults.lut should equal(Some(8)) + implResults.ff shouldBe defined + implResults.ff should equal(Some(0)) + implResults.dsp shouldBe defined + implResults.dsp should equal(Some(0)) } } else { println("Vivado environment not detected; skipping synthesis tests") From 27385660f884a9fdcc92d2c160c0170cd3b41de9 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Fri, 15 May 2026 11:24:29 +0200 Subject: [PATCH 09/15] refactor runVivado* with two helpers --- src/main/scala/approx/util/Synthesis.scala | 73 +++++++++++++--------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index 804535d..430ba76 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -4,7 +4,7 @@ import chisel3.RawModule import circt.stage.ChiselStage -import java.nio.file.{Files, Paths} +import java.nio.file.{Files, Path, Paths} import scala.jdk.CollectionConverters._ import scala.sys.process._ @@ -12,12 +12,15 @@ import scala.util.{Try, Success, Failure} object Synthesis { - final val VivadoBuildDir = "build/Vivado" + final val BuildDir = "build" + final val VivadoBuildDir = s"${BuildDir}/Vivado" - final val VivadoSynthesisLUTRegex = """\|\s*Slice\s+LUTs\*\s*\|\s*(\d+)\s*\|""".r + final val VerilogModuleNameRegex = """module\s+([A-Za-z_]\w*)\s*\(""".r + + final val VivadoSynthesisLUTRegex = """\|\s*Slice\s+LUTs\*\s*\|\s*(\d+)\s*\|""".r final val VivadoImplementationLUTRegex = """\|\s*Slice\s+LUTs\s*\|\s*(\d+)\s*\|""".r - final val VivadoFFRegex = """\|\s*Slice\s+Registers\s*\|\s*(\d+)\s*\|""".r - final val VivadoDSPRegex = """\|\s*DSPs\s*\|\s*(\d+)\s*\|""".r + final val VivadoFFRegex = """\|\s*Slice\s+Registers\s*\|\s*(\d+)\s*\|""".r + final val VivadoDSPRegex = """\|\s*DSPs\s*\|\s*(\d+)\s*\|""".r case class VivadoSynthesisResults(buildDir: String, report: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) @@ -34,6 +37,35 @@ object Synthesis { Files.write(Paths.get(dir, "Makefile"), "include *.mk".getBytes) } + /** Run a make target in a given directory + * + * @param dir the directory where the Makefile is located + * @param target the make target to run + * @return a tuple of (exit code, stdout/stderr content) + */ + private[Synthesis] def runMakeTarget(dir: String, target: String): (Int, String) = { + println(s"Running make target: make -C ${dir} ${target}") + val stdout = new StringBuilder + val logger = ProcessLogger(line => stdout.append(line).append("\n")) + val exitCode = Process(Seq("make", "-C", dir, target)).!(logger) + (exitCode, stdout.toString) + } + + /** Get (the first) file in a given directory + * + * @param dir the directory to search + * @param pattern a pattern to match files against + * @return an Option containing the first matching file path, if any + */ + private[Synthesis] def getFileInDir(dir: String, pattern: String): Option[Path] = { + val stream = Files.newDirectoryStream(Paths.get(dir), pattern) + try { + stream.iterator().asScala.toList.headOption + } finally { + stream.close() + } + } + /** Generates Vivado synthesis and implementation TCL scripts along with the * corresponding SystemVerilog source for a given Chisel module * @@ -45,9 +77,8 @@ object Synthesis { Try { // Generate SystemVerilog source first to get the module name; // regex extraction assumes the top module is the last one defined - val moduleNameRegex = "module\\s+([A-Za-z_]\\w*)\\s*\\(".r val sv = ChiselStage.emitSystemVerilog(gen(), firtoolOpts = Array("--disable-layers", "Verification")) - val topName = moduleNameRegex.findAllMatchIn(sv) + val topName = VerilogModuleNameRegex.findAllMatchIn(sv) .map(_.group(1)).toList .lastOption .getOrElse(throw new RuntimeException("Failed to extract top module name from generated SystemVerilog")) @@ -192,10 +223,7 @@ object Synthesis { def runVivadoSynthesis(dir: String): VivadoSynthesisResults = { // Attempt to launch Vivado synthesis using the generated Makefile assumed // to exist under dir - println(s"Running Vivado synthesis: make -C $dir vivado-syn") - val stdout = new StringBuilder - val logger = ProcessLogger(line => stdout.append(line).append("\n")) - val exitCode = Process(Seq("make", "-C", dir, "vivado-syn")).!(logger) + val (exitCode, stdout) = runMakeTarget(dir, "vivado-syn") if (exitCode != 0) { println(s"Vivado synthesis failed with exit code $exitCode") println(s"Vivado output:\n${stdout}") @@ -203,14 +231,7 @@ object Synthesis { } // Parse synthesis report to extract resource utilization - val synRptOpt = { - val stream = Files.newDirectoryStream(Paths.get(dir), "*_syn.rpt") - try { // get first matching file, if any - stream.iterator().asScala.toList.headOption - } finally { - stream.close() - } - } + val synRptOpt = getFileInDir(dir, "*_syn.rpt") synRptOpt match { case None => println(s"Synthesis report not found in directory: $dir") @@ -237,10 +258,7 @@ object Synthesis { def runVivadoImplementation(dir: String): VivadoImplementationResults = { // Attempt to launch Vivado implementation using the generated Makefile // assumed to exist under dir - println(s"Running Vivado implementation: make -C $dir vivado-impl") - val stdout = new StringBuilder - val logger = ProcessLogger(line => stdout.append(line).append("\n")) - val exitCode = Process(Seq("make", "-C", dir, "vivado-impl")).!(logger) + val (exitCode, stdout) = runMakeTarget(dir, "vivado-impl") if (exitCode != 0) { println(s"Vivado implementation failed with exit code $exitCode") println(s"Vivado output:\n${stdout}") @@ -248,14 +266,7 @@ object Synthesis { } // Parse implementation report to extract resource utilization - val implRptOpt = { - val stream = Files.newDirectoryStream(Paths.get(dir), "*_impl.rpt") - try { // get first matching file, if any - stream.iterator().asScala.toList.headOption - } finally { - stream.close() - } - } + val implRptOpt = getFileInDir(dir, "*_impl.rpt") implRptOpt match { case None => println(s"Implementation report not found in directory: $dir") From 56d6283a7a51fb28ea2831acb1b1f2cb69225a46 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Fri, 15 May 2026 22:29:06 +0200 Subject: [PATCH 10/15] add initial print-all-variables target --- src/main/scala/approx/util/Synthesis.scala | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index 430ba76..f9894cf 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -15,7 +15,7 @@ object Synthesis { final val BuildDir = "build" final val VivadoBuildDir = s"${BuildDir}/Vivado" - final val VerilogModuleNameRegex = """module\s+([A-Za-z_]\w*)\s*\(""".r + final val VerilogModuleNameRegex = """module\s+([A-Za-z_]\w*)\s*\(""".r final val VivadoSynthesisLUTRegex = """\|\s*Slice\s+LUTs\*\s*\|\s*(\d+)\s*\|""".r final val VivadoImplementationLUTRegex = """\|\s*Slice\s+LUTs\s*\|\s*(\d+)\s*\|""".r @@ -34,7 +34,19 @@ object Synthesis { */ private[Synthesis] def generateHelperMakefile(dir: String) = { Files.createDirectories(Paths.get(dir)) - Files.write(Paths.get(dir, "Makefile"), "include *.mk".getBytes) + val make = s""" + |include *.mk + | + |.PHONY: print-all-variables + |# Print all Make variables and their values (excluding environment, default, and automatic variables) + |print-all-variables: + | @$$(foreach V,$$(sort $$(.VARIABLES)), \\ + | $$(if $$(filter-out environment% default automatic,$$(origin $$V)), \\ + | $$(info $$(V) = $$($$(V))) \\ + | ) \\ + | ) + |""".stripMargin + Files.write(Paths.get(dir, "Makefile"), make.getBytes) } /** Run a make target in a given directory @@ -202,7 +214,7 @@ object Synthesis { |.PHONY: vivado-impl |# Run implementation to generate implementation report $$(VIVADO_IMPL_REPORT) |vivado-impl: $$(VIVADO_IMPL_REPORT) - """.stripMargin + |""".stripMargin Files.write(buildDir.resolve("vivado.mk"), make.getBytes) // Generate local and common helper Makefiles From 794987a83ae64ac7237686a8cf3431afe550ffaf Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Fri, 15 May 2026 22:41:18 +0200 Subject: [PATCH 11/15] add initial help target --- src/main/scala/approx/util/Synthesis.scala | 47 +++++++++++++--------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index f9894cf..022e090 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -29,8 +29,6 @@ object Synthesis { /** Generate a helper Makefile for synthesis and implementation * * @param dir the directory where the Makefile should be created - * - * TODO extend with help and print-all-variables targets */ private[Synthesis] def generateHelperMakefile(dir: String) = { Files.createDirectories(Paths.get(dir)) @@ -38,13 +36,26 @@ object Synthesis { |include *.mk | |.PHONY: print-all-variables - |# Print all Make variables and their values (excluding environment, default, and automatic variables) + |## Print all variables and their values (excluding environment, default, and automatic variables) |print-all-variables: - | @$$(foreach V,$$(sort $$(.VARIABLES)), \\ - | $$(if $$(filter-out environment% default automatic,$$(origin $$V)), \\ - | $$(info $$(V) = $$($$(V))) \\ - | ) \\ - | ) + |\t@$$(foreach V,$$(sort $$(.VARIABLES)), \\ + |\t\t$$(if $$(filter-out environment% default automatic,$$(origin $$V)), \\ + |\t\t\t$$(info $$(V) = $$($$(V))) \\ + |\t\t) \\ + |\t) + | + |.PHONY: help + |## Print available targets + |help: + |\t@awk '\\ + |\t\t/^## / { help=$$$$0; sub(/^## /, "", help); next } \\ + |\t\t/^[a-zA-Z0-9_-]+:/ { \\ + |\t\t\tif (help != "") { \\ + |\t\t\t\tprintf " %-24s %s\\n", $$$$1, help; \\ + |\t\t\t\thelp="" \\ + |\t\t\t} \\ + |\t\t} \\ + |\t' $$(MAKEFILE_LIST) |""".stripMargin Files.write(Paths.get(dir, "Makefile"), make.getBytes) } @@ -163,41 +174,41 @@ object Synthesis { |\tvivado -nolog -nojournal -mode batch -source $$< | |.PHONY: generate-vivado-project - |# Generate Vivado project file + |## Generate Vivado project file |generate-vivado-project: $$(VIVADO_PROJ_XPR) | |.PHONY: open-vivado-gui - |# Open Vivado GUI with the generated project + |## Open Vivado GUI with the generated project |open-vivado-gui: $$(VIVADO_PROJ_XPR) |\tvivado -nolog -nojournal $$< | |.PHONY: open-vivado-tcl - |# Open Vivado TCL with the generated project + |## Open Vivado TCL with the generated project |open-vivado-tcl: $$(VIVADO_PROJ_XPR) |\tvivado -nolog -nojournal -mode tcl $$< | |# Helper clean targets for generated files |.PHONY: clean-vivado - |# Remove all generated Vivado files + |## Remove all generated Vivado files |clean-vivado: clean-vivado-project clean-vivado-tcl clean-vivado-rpt clean-vivado-dcp | |.PHONY: clean-vivado-project - |# Remove generated Vivado project files + |## Remove generated Vivado project files |clean-vivado-project: |\trm -rf $$(VIVADO_PROJ_DIR) | |.PHONY: clean-vivado-tcl - |# Remove generated TCL scripts + |## Remove generated TCL scripts |clean-vivado-tcl: |\trm -f $$(VIVADO_PROJ_TCL) $$(VIVADO_SYN_TCL) $$(VIVADO_IMPL_TCL) | |.PHONY: clean-vivado-rpt - |# Remove generated reports + |## Remove generated reports |clean-vivado-rpt: |\trm -f $$(VIVADO_SYN_REPORT) $$(VIVADO_IMPL_REPORT) | |.PHONY: clean-vivado-dcp - |# Remove generated checkpoints + |## Remove generated checkpoints |clean-vivado-dcp: |\trm -f $$(VIVADO_SYN_DCP) $$(VIVADO_IMPL_DCP) | @@ -205,14 +216,14 @@ object Synthesis { |\tvivado -nolog -nojournal -mode batch -source $$(VIVADO_SYN_TCL) | |.PHONY: vivado-syn - |# Run synthesis to generate synthesis report $$(VIVADO_SYN_REPORT) + |## Run synthesis to generate synthesis report $$(VIVADO_SYN_REPORT) |vivado-syn: $$(VIVADO_SYN_REPORT) | |$$(VIVADO_IMPL_REPORT): $$(VIVADO_SYN_REPORT) $$(VIVADO_IMPL_TCL) |\tvivado -nolog -nojournal -mode batch -source $$(VIVADO_IMPL_TCL) | |.PHONY: vivado-impl - |# Run implementation to generate implementation report $$(VIVADO_IMPL_REPORT) + |## Run implementation to generate implementation report $$(VIVADO_IMPL_REPORT) |vivado-impl: $$(VIVADO_IMPL_REPORT) |""".stripMargin Files.write(buildDir.resolve("vivado.mk"), make.getBytes) From 8caee531709ec0eaef907cf609bf9ed81a1011cd Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Tue, 19 May 2026 20:29:44 +0200 Subject: [PATCH 12/15] add initial support for yosys synthesis with oss-cad-suite --- src/main/scala/approx/util/Synthesis.scala | 137 +++++++++++++++++- .../scala/approx/util/SynthesisSpec.scala | 33 +++++ 2 files changed, 168 insertions(+), 2 deletions(-) diff --git a/src/main/scala/approx/util/Synthesis.scala b/src/main/scala/approx/util/Synthesis.scala index 022e090..7e1038f 100644 --- a/src/main/scala/approx/util/Synthesis.scala +++ b/src/main/scala/approx/util/Synthesis.scala @@ -13,7 +13,8 @@ import scala.util.{Try, Success, Failure} object Synthesis { final val BuildDir = "build" - final val VivadoBuildDir = s"${BuildDir}/Vivado" + final val VivadoBuildDir = s"${BuildDir}/vivado" + final val YosysBuildDir = s"${BuildDir}/yosys" final val VerilogModuleNameRegex = """module\s+([A-Za-z_]\w*)\s*\(""".r @@ -23,9 +24,10 @@ object Synthesis { final val VivadoDSPRegex = """\|\s*DSPs\s*\|\s*(\d+)\s*\|""".r case class VivadoSynthesisResults(buildDir: String, report: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) - case class VivadoImplementationResults(buildDir: String, report: Option[String], lut: Option[Int], ff: Option[Int], dsp: Option[Int]) + case class YosysSynthesisResults(buildDir: String, report: Option[String], nand: Option[Int], not: Option[Int], ff: Option[Int], others: Map[String, Int]) + /** Generate a helper Makefile for synthesis and implementation * * @param dir the directory where the Makefile should be created @@ -304,4 +306,135 @@ object Synthesis { val dspCount = VivadoDSPRegex .findFirstMatchIn(implRptContent).map(_.group(1).toInt) VivadoImplementationResults(dir, implRptOpt.map(_.toString), lutCount, ffCount, dspCount) } + + /** Generates Yosys synthesis sources and returns the directory and SV file paths + * + * @param gen a function that generates the Chisel module to be synthesized + * @return a Try containing a tuple of (build directory, SV filename) + */ + def generateYosysSources(gen: () => RawModule): Try[(String, String)] = { + Try { + // Generate SystemVerilog source first to get the module name; + // regex extraction assumes the top module is the last one defined + val sv = ChiselStage.emitSystemVerilog(gen(), firtoolOpts = Array("--disable-layers", "Verification")) + val topName = VerilogModuleNameRegex.findAllMatchIn(sv) + .map(_.group(1)).toList + .lastOption + .getOrElse(throw new RuntimeException("Failed to extract top module name from generated SystemVerilog")) + + // Ensure unique build directory exists + val buildDir = Paths.get(s"${YosysBuildDir}/${topName}") + Files.createDirectories(buildDir) + + // Generate SystemVerilog source file + val svFile = s"${topName}.sv" + Files.write(buildDir.resolve(svFile), sv.getBytes) + + // Generate helper Makefile + val make = s""" + |SV_FILE :=${svFile} + | + |YOSYS_SYN_YS :=${topName}_syn.ys + |YOSYS_SYN_NL_JSON :=${topName}_syn.json + |YOSYS_SYN_NL_VLOG :=${topName}_syn.v + |YOSYS_SYN_REPORT :=${topName}_syn.rpt + | + |# Macros to generate Yosys synthesis script + |define YOSYS_SYN_YS_CONTENT + |read_verilog -sv $$(SV_FILE) + |hierarchy -check -top ${topName} + | + |# Generic synthesis flow + |proc; opt + |fsm; opt + |memory; opt + |flatten -noscopeinfo; opt_clean + |techmap; opt + |abc -g NAND; opt_clean + | + |# Statistics in json format + |tee -o $$(YOSYS_SYN_REPORT) stat -json + | + |# Netlist outputs + |write_json $$(YOSYS_SYN_NL_JSON) + |write_verilog -sv $$(YOSYS_SYN_NL_VLOG) + | + |endef + | + |$$(YOSYS_SYN_YS): + |\t$$(file >$$@,$$(YOSYS_SYN_YS_CONTENT)) + | + |# Helper clean targets for generated files + |.PHONY: clean-yosys + |## Remove all generated Yosys files + |clean-yosys: clean-yosys-syn-netlist clean-yosys-syn-rpt + | + |.PHONY: clean-yosys-syn-netlist + |## Remove generated Yosys synthesis files + |clean-yosys-syn-netlist: + |\trm -f $$(YOSYS_SYN_YS) $$(YOSYS_SYN_NL_JSON) $$(YOSYS_SYN_NL_VLOG) + | + |.PHONY: clean-yosys-syn-rpt + |## Remove generated Yosys synthesis report + |clean-yosys-syn-rpt: + |\trm -f $$(YOSYS_SYN_REPORT) + | + |$$(YOSYS_SYN_REPORT): $$(SV_FILE) $$(YOSYS_SYN_YS) + |\tyosys -s $$(YOSYS_SYN_YS) + | + |.PHONY: yosys-syn + |## Run Yosys synthesis to generate report $$(YOSYS_SYN_REPORT) + |yosys-syn: $$(YOSYS_SYN_REPORT) + |""".stripMargin + Files.write(buildDir.resolve("yosys.mk"), make.getBytes) + + // Generate local and common helper Makefiles + generateHelperMakefile(buildDir.toString) + + (buildDir.toString, svFile) + } + } + + /** Runs Yosys synthesis using the generated Makefile and parses the + * resulting synthesis report to extract and bundle resource utilization + * metrics into a structured result + * + * @param dir the directory containing the generated Makefile and sources + * @return a [[YosysSynthesisResults]] instance including status and + * resource utilization metrics, if available + */ + def runYosysSynthesis(dir: String): YosysSynthesisResults = { + // Attempt to launch Yosys synthesis using the generated Makefile assumed + // to exist under dir + val (exitCode, stdout) = runMakeTarget(dir, "yosys-syn") + if (exitCode != 0) { + println(s"Yosys synthesis failed with exit code $exitCode") + println(s"Yosys output:\n${stdout}") + return YosysSynthesisResults(dir, None, None, None, None, Map.empty) + } + + // Parse synthesis report to extract resource utilization + val synRptOpt = getFileInDir(dir, "*_syn.rpt") + synRptOpt match { + case None => + println(s"Synthesis report not found in directory: $dir") + return YosysSynthesisResults(dir, None, None, None, None, Map.empty) + case _ => + } + + // Read the synthesis report and extract NAND, NOT, and FF counts using + // json parsing; hardcoded keys for now + val synRptContent = ujson.read(Files.readString(synRptOpt.get)) + val cellCounts = synRptContent("design")("num_cells_by_type").obj + val nandCount = cellCounts.get("$_NAND_").map(_.num.toInt) + val notCount = cellCounts.get("$_NOT_").map(_.num.toInt) + val ffCount = { + val cnts = cellCounts.collect { case (k, v) if k.startsWith("$_DFF") => v.num.toInt } + if (cnts.isEmpty) None else Some(cnts.sum) + } + val otherCounts = cellCounts.collect { + case (k, v) if k != "$_NAND_" && k != "$_NOT_" && !k.startsWith("$_DFF") => k -> v.num.toInt + }.toMap + YosysSynthesisResults(dir, synRptOpt.map(_.toString), nandCount, notCount, ffCount, otherCounts) + } } diff --git a/src/test/scala/approx/util/SynthesisSpec.scala b/src/test/scala/approx/util/SynthesisSpec.scala index f61e0ef..1e72753 100644 --- a/src/test/scala/approx/util/SynthesisSpec.scala +++ b/src/test/scala/approx/util/SynthesisSpec.scala @@ -11,6 +11,7 @@ class SynthesisSpec extends AnyFlatSpec with Matchers { behavior of "Synthesis" val runVivado = System.getenv().containsKey("XILINX_VIVADO") + val runYosys = System.getenv().containsKey("OSS_CAD_SUITE") if (runVivado) { val vivadoPath = System.getenv("XILINX_VIVADO") @@ -60,4 +61,36 @@ class SynthesisSpec extends AnyFlatSpec with Matchers { } else { println("Vivado environment not detected; skipping synthesis tests") } + + if (runYosys) { + val yosysPath = System.getenv("OSS_CAD_SUITE") + println(s"Yosys environment detected at ${yosysPath}") + + it should "generate Yosys synthesis sources for an RCA adder" in { + val (dir, sv) = Synthesis.generateYosysSources(() => new approx.addition.RCA(8)) match { + case Success(result) => result + case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") + } + val svPath = Paths.get(dir, sv) + Files.exists(svPath) shouldBe true + } + + it should "run Yosys synthesis and parse results for an RCA adder" in { + val (dir, sv) = Synthesis.generateYosysSources(() => new approx.addition.RCA(8)) match { + case Success(result) => result + case Failure(exp) => fail(s"Source generation failed with exception: ${exp.getMessage}") + } + val results = Synthesis.runYosysSynthesis(dir) + println(s"Synthesis results: ${results}") + results.report shouldBe defined + results.nand shouldBe defined + results.nand should equal(Some(76)) + results.not shouldBe defined + results.not should equal(Some(25)) + results.ff should not be defined // no FFs in an RCA + results.others shouldBe empty + } + } else { + println("Yosys environment not detected; skipping synthesis tests") + } } From b778804a656f2e8e8b8bac405ac21da10b1cd2ce Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Tue, 19 May 2026 20:43:14 +0200 Subject: [PATCH 13/15] install oss-cad-suite in ci flow --- .github/workflows/ci.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65abc1b..1ecdbc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,21 @@ jobs: with: jvm: adopt:11 apps: sbt - - name: Setup Dependencies - run: sudo apt-get install verilator + - name: Install OSS CAD Suite + run: | # always fetch the most recent release + wget \ + $(wget -qO- \ + https://api.github.com/repos/YosysHQ/oss-cad-suite-build/releases/latest \ + | grep browser_download_url \ + | grep 'linux-x64.*tgz' \ + | cut -d '"' -f 4) + tar -xzf oss-cad-suite-linux-x64*.tgz + - name: Validate OSS CAD Suite installation + shell: bash + run: | + source oss-cad-suite/environment + which yosys + yosys -V - name: Run tests run: sbt test From 4be5574adaf92c8f779cdc9abc5d91d9cac9747f Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Tue, 19 May 2026 20:50:01 +0200 Subject: [PATCH 14/15] reword detection of oss-cad-suite --- src/test/scala/approx/util/SynthesisSpec.scala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/scala/approx/util/SynthesisSpec.scala b/src/test/scala/approx/util/SynthesisSpec.scala index 1e72753..d79f2ff 100644 --- a/src/test/scala/approx/util/SynthesisSpec.scala +++ b/src/test/scala/approx/util/SynthesisSpec.scala @@ -2,6 +2,7 @@ package approx.util import java.nio.file.{Files, Paths} +import scala.sys.process._ import scala.util.{Try, Success, Failure} import org.scalatest.matchers.should.Matchers @@ -10,12 +11,12 @@ import org.scalatest.flatspec.AnyFlatSpec class SynthesisSpec extends AnyFlatSpec with Matchers { behavior of "Synthesis" - val runVivado = System.getenv().containsKey("XILINX_VIVADO") - val runYosys = System.getenv().containsKey("OSS_CAD_SUITE") + val runVivado = sys.env.contains("XILINX_VIVADO") + // oss-cad-suite environment does not set a clear environment variable + val runYosys = Try { Seq("yosys", "-V").! }.toOption.contains(0) if (runVivado) { - val vivadoPath = System.getenv("XILINX_VIVADO") - println(s"Vivado environment detected at ${vivadoPath}") + println(s"Vivado environment detected") it should "generate Vivado synthesis and implementation sources for an RCA adder" in { val (dir, sv) = Synthesis.generateVivadoSources(() => new approx.addition.RCA(8)) match { @@ -63,8 +64,7 @@ class SynthesisSpec extends AnyFlatSpec with Matchers { } if (runYosys) { - val yosysPath = System.getenv("OSS_CAD_SUITE") - println(s"Yosys environment detected at ${yosysPath}") + println(s"Yosys environment detected") it should "generate Yosys synthesis sources for an RCA adder" in { val (dir, sv) = Synthesis.generateYosysSources(() => new approx.addition.RCA(8)) match { From 880216a2b9bd2dd77742261cae96442987d10823 Mon Sep 17 00:00:00 2001 From: Hans Jakob Damsgaard Date: Tue, 19 May 2026 20:51:00 +0200 Subject: [PATCH 15/15] install oss-cad-suite in ci flow take 2 --- .github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ecdbc1..5b33bef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,14 +29,10 @@ jobs: | grep 'linux-x64.*tgz' \ | cut -d '"' -f 4) tar -xzf oss-cad-suite-linux-x64*.tgz - - name: Validate OSS CAD Suite installation - shell: bash + - name: Run tests run: | source oss-cad-suite/environment - which yosys - yosys -V - - name: Run tests - run: sbt test + sbt test docs: name: Generate and Deploy Docs