fpga: Create fpga_image build template

Change-Id: I85f157838060d8c0187bb66596afdc894d2a4bbb
Reviewed-on: https://pigweed-review.googlesource.com/c/gonk/+/185471
Reviewed-by: Armando Montanez <amontanez@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index ea9870c..8a504ba 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -26,6 +26,10 @@
     ":gonk_python.lint",
     ":gonk_python.tests",
     ":python.install",
+
+    # TODO(tonymd): Enable the verilog build by default when the following tools
+    # are available in CIPD: yosys, nextpnr-ice40, icetime, icepack
+    # ":fpga",
   ]
 }
 
@@ -40,6 +44,10 @@
   ]
 }
 
+group("fpga") {
+  deps = [ "//fpga" ]
+}
+
 # Python Targets
 _gonk_python_packages = [ "//tools" ]
 
diff --git a/README.md b/README.md
index 8bf1a10..477707d 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,24 @@
 
 The build commands are defined in: `//tools/gonk_tools/build_project.py`.
 
+## Verilog:
+
+The Verilog build requires the following to be installed on Linux:
+
+```sh
+sudo apt install fpga-icestorm nextpnr-ice40 yosys
+```
+
+Run this to compile the Gonk Verilog:
+
+```sh
+pw build
+ninja -C out/gn fpga
+```
+
+The bitstream files will be written with the `.bin` extenson under
+`./out/gn/obj/fpga/*/*.bin`.
+
 ## Gonk `spi_flash_test` Example
 
 ### Flash with `dfu-util`
diff --git a/fpga/BUILD.gn b/fpga/BUILD.gn
new file mode 100644
index 0000000..6958491
--- /dev/null
+++ b/fpga/BUILD.gn
@@ -0,0 +1,20 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("//fpga/fpga_image.gni")
+
+group("fpga") {
+}
diff --git a/fpga/fpga_image.gni b/fpga/fpga_image.gni
new file mode 100644
index 0000000..6cbf00d
--- /dev/null
+++ b/fpga/fpga_image.gni
@@ -0,0 +1,141 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+#     https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/exec.gni")
+
+# Builds an FPGA image using the icestorm toolchain.
+#
+# Requires the following tools:
+#   yosys, nextpnr-ice40, icepack, icetime
+#
+# Example:
+#
+#   fpga_image("toplevel") {
+#     sources = [
+#       "gonk/csa_ctl_top.v",
+#       "gonk/pll.v",
+#       "gonk/rst_sync.v",
+#       "gonk/sig_sync.v",
+#       "gonk/spi_m_core.v",
+#       "gonk/spi_m_core_ctl.v",
+#       "gonk/spi_s_core.v",
+#       "gonk/top.v",
+#     ]
+#
+#     pcf = "gonk/top.pcf"
+#   }
+#
+template("fpga_image") {
+  assert(defined(invoker.sources), "fpga_image requires 'sources'")
+  assert(defined(invoker.pcf), "fpga_image requires a 'pcf'")
+
+  # Image basename set to target_name by default but can be overriden.
+  _image_name = "$target_name"
+  if (defined(invoker.image_name)) {
+    _image_name = invoker.image_name
+  }
+
+  _verilog_files = invoker.sources
+  _pcf_file = invoker.pcf
+
+  _json_file = "$target_out_dir/$target_name/${_image_name}.json"
+  _asc_file = "$target_out_dir/$target_name/${_image_name}.asc"
+  _bin_file = "$target_out_dir/$target_name/${_image_name}.bin"
+  _timing_report_text =
+      "$target_out_dir/$target_name/${_image_name}_timing_report.txt"
+  _timing_report_json =
+      "$target_out_dir/$target_name/${_image_name}_timing_report.json"
+
+  # By default, build the bin file and timing report.
+  group("$target_name") {
+    public_deps = [
+      ":${target_name}._bin",
+      ":${target_name}._time",
+    ]
+  }
+
+  pw_exec("${target_name}._json") {
+    inputs = _verilog_files
+    outputs = [ _json_file ]
+
+    program = "yosys"
+    args = [
+      "-q",
+      "-p",
+      "synth_ice40 -json " + rebase_path(_json_file, root_build_dir),
+    ]
+    args += rebase_path(_verilog_files, root_build_dir)
+  }
+
+  pw_exec("${target_name}._asc") {
+    inputs = [ _json_file ]
+    outputs = [ _asc_file ]
+
+    deps = [ ":${invoker.target_name}._json" ]
+
+    program = "nextpnr-ice40"
+    args = [
+      "--freq",
+      "75",
+      "--hx8k",
+      "--package",
+      "tq144:4k",
+      "--json",
+      rebase_path(_json_file, root_build_dir),
+      "--pcf",
+      rebase_path(_pcf_file, root_build_dir),
+      "--asc",
+      rebase_path(_asc_file, root_build_dir),
+      "--opt-timing",
+      "--placer",
+      "heap",
+    ]
+  }
+
+  pw_exec("${target_name}._bin") {
+    inputs = [ _asc_file ]
+    outputs = [ _bin_file ]
+
+    deps = [ ":${invoker.target_name}._asc" ]
+
+    program = "icepack"
+    args = [
+      rebase_path(_asc_file, root_build_dir),
+      rebase_path(_bin_file, root_build_dir),
+    ]
+  }
+
+  pw_exec("${target_name}._time") {
+    inputs = [ _asc_file ]
+    outputs = [
+      _timing_report_text,
+      _timing_report_json,
+    ]
+
+    deps = [ ":${invoker.target_name}._asc" ]
+
+    program = "icetime"
+    args = [
+      "-r",
+      rebase_path(_timing_report_text, root_build_dir),
+      "-j",
+      rebase_path(_timing_report_json, root_build_dir),
+      "-tmd",
+      "hx8k",
+      rebase_path(_asc_file, root_build_dir),
+    ]
+  }
+}