diff --git a/README.md b/README.md index 0c7d5f3a..2ab37cf7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # PyFPGA [![License](https://img.shields.io/badge/License-GPL--3.0-darkgreen?style=flat-square)](LICENSE) +![Gowin](https://img.shields.io/badge/Gowin-1.9.11-blue.svg?style=flat-square) ![Diamond](https://img.shields.io/badge/Diamond-3.13-blue.svg?style=flat-square) ![ISE](https://img.shields.io/badge/ISE-14.7-blue.svg?style=flat-square) ![Libero](https://img.shields.io/badge/Libero--Soc-2024.1-blue.svg?style=flat-square) diff --git a/docs/intro.rst b/docs/intro.rst index abc7e945..6180daa4 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -13,7 +13,7 @@ With PyFPGA, you can create your own FPGA development workflow tailored to your * Ensured reproducibility and repeatability. * Lower resource consumption compared to GUI-based workflows. -It currently supports vendor tools such as ``Diamond``, ``Ise``, ``Quartus``, ``Libero``, and ``Vivado``, as well as ``Openflow``, a solution based on *Free/Libre and Open Source Software* (**FLOSS**). +It currently supports vendor tools such as ``Diamond``, ``Gowin``, ``Ise``, ``Quartus``, ``Libero``, and ``Vivado``, as well as ``Openflow``, a solution based on *Free/Libre and Open Source Software* (**FLOSS**). .. ATTENTION:: diff --git a/docs/tools.rst b/docs/tools.rst index 38da3ef3..806f5245 100644 --- a/docs/tools.rst +++ b/docs/tools.rst @@ -12,6 +12,10 @@ Tools - Lattice - LFXP2-5E-5TN144C - device-speed-package + * - Gowin + - Gowin Semiconductor + - GW2AR-LV18QN88C8/I7 + - part * - ISE - Xilinx - XC7K160T-3-FBG484 @@ -48,6 +52,27 @@ Example: prj = Diamond() +Gowin +----- + +`Gowin downloads `_ + +Gowin EDA (``gw_sh``) supports Gowin FPGA families. The default part is +``GW2AR-LV18QN88C8/I7`` (Sipeed Tang Nano 20K). Bitstreams are ``.fs`` files +produced at the end of place-and-route. + +Gowin 1.9.9 has no ``open_project`` / ``-ifdef`` / ``-verilog_param``; pyfpga +runs create+syn+pnr in one ``gw_sh`` invocation and wraps the newer options in +``catch``. Prefer 1.9.11+ when licensed. + +Example: + +.. code:: + + from pyfpga.gowin import Gowin + + prj = Gowin() + ISE --- diff --git a/examples/helpers/gowin.sh b/examples/helpers/gowin.sh new file mode 100755 index 00000000..7d8642a1 --- /dev/null +++ b/examples/helpers/gowin.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +HDIR=../../pyfpga/helpers + +python3 $HDIR/hdl2bit.py -t gowin -o results/gowin-vlog -p GW2AR-LV18QN88C8/I7 \ + -i ../sources/vlog/include1 -i ../sources/vlog/include2 \ + -f ../sources/vlog/blink.v -f ../sources/vlog/top.v \ + -f ../sources/cons/tangnano20k/timing.sdc -f ../sources/cons/tangnano20k/clk.cst \ + -f ../sources/cons/tangnano20k/led.cst \ + --define DEFINE1 1 --define DEFINE2 1 --param FREQ 27000000 --param SECS 1 Top + +python3 $HDIR/hdl2bit.py -t gowin -o results/gowin-vhdl -p GW2AR-LV18QN88C8/I7 --project example \ + -f ../sources/vhdl/blink.vhdl,blink_lib -f ../sources/vhdl/blink_pkg.vhdl,blink_lib -f ../sources/vhdl/top.vhdl \ + -f ../sources/cons/tangnano20k/timing.sdc -f ../sources/cons/tangnano20k/clk.cst \ + -f ../sources/cons/tangnano20k/led.cst \ + --param FREQ 27000000 --param SECS 1 --last cfg Top diff --git a/examples/projects/gowin.py b/examples/projects/gowin.py new file mode 100644 index 00000000..db515ba1 --- /dev/null +++ b/examples/projects/gowin.py @@ -0,0 +1,50 @@ +"""Gowin examples.""" + +import argparse + +from pyfpga.gowin import Gowin + + +parser = argparse.ArgumentParser() +parser.add_argument( + '--board', choices=['tangnano20k'], default='tangnano20k' +) +parser.add_argument( + '--source', choices=['vlog', 'vhdl', 'slog'], default='vlog' +) +parser.add_argument( + '--action', choices=['make', 'prog', 'all'], default='make' +) +args = parser.parse_args() + +prj = Gowin(odir=f'results/gowin/{args.source}/{args.board}') + +if args.board == 'tangnano20k': + prj.set_part('GW2AR-LV18QN88C8/I7') + prj.add_param('FREQ', '27000000') + prj.add_cons('../sources/cons/tangnano20k/timing.sdc') + prj.add_cons('../sources/cons/tangnano20k/clk.cst') + prj.add_cons('../sources/cons/tangnano20k/led.cst') +prj.add_param('SECS', '1') + +if args.source == 'vhdl': + prj.add_vhdl('../sources/vhdl/*.vhdl', 'blink_lib') + prj.add_vhdl('../sources/vhdl/top.vhdl') +if args.source == 'vlog': + prj.add_include('../sources/vlog/include1') + prj.add_include('../sources/vlog/include2') + prj.add_vlog('../sources/vlog/*.v') +if args.source == 'slog': + prj.add_include('../sources/slog/include1') + prj.add_include('../sources/slog/include2') + prj.add_slog('../sources/slog/*.sv') +if args.source in ['vlog', 'slog']: + prj.add_define('DEFINE1', '1') + prj.add_define('DEFINE2', '1') + +prj.set_top('Top') + +if args.action in ['make', 'all']: + prj.make() +if args.action in ['prog', 'all']: + prj.prog() diff --git a/examples/sources/cons/tangnano20k/clk.cst b/examples/sources/cons/tangnano20k/clk.cst new file mode 100644 index 00000000..b6c9602b --- /dev/null +++ b/examples/sources/cons/tangnano20k/clk.cst @@ -0,0 +1,2 @@ +IO_LOC "clk_i" 4; +IO_PORT "clk_i" IO_TYPE=LVCMOS33 PULL_MODE=UP BANK_VCCIO=3.3; diff --git a/examples/sources/cons/tangnano20k/led.cst b/examples/sources/cons/tangnano20k/led.cst new file mode 100644 index 00000000..35cea4c4 --- /dev/null +++ b/examples/sources/cons/tangnano20k/led.cst @@ -0,0 +1,2 @@ +IO_LOC "led_o" 15; +IO_PORT "led_o" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8 BANK_VCCIO=3.3; diff --git a/examples/sources/cons/tangnano20k/timing.sdc b/examples/sources/cons/tangnano20k/timing.sdc new file mode 100644 index 00000000..cb0476f2 --- /dev/null +++ b/examples/sources/cons/tangnano20k/timing.sdc @@ -0,0 +1 @@ +create_clock -name clk_i -period 37.037 -waveform {0 18.518} [get_ports {clk_i}] diff --git a/pyfpga/factory.py b/pyfpga/factory.py index 6da4029a..5743874f 100644 --- a/pyfpga/factory.py +++ b/pyfpga/factory.py @@ -11,6 +11,7 @@ # pylint: disable=too-few-public-methods from pyfpga.diamond import Diamond +from pyfpga.gowin import Gowin from pyfpga.ise import Ise from pyfpga.libero import Libero from pyfpga.openflow import Openflow @@ -20,6 +21,7 @@ TOOLS = { 'diamond': Diamond, + 'gowin': Gowin, 'ise': Ise, 'libero': Libero, 'openflow': Openflow, diff --git a/pyfpga/gowin.py b/pyfpga/gowin.py new file mode 100644 index 00000000..09097673 --- /dev/null +++ b/pyfpga/gowin.py @@ -0,0 +1,44 @@ +# +# Copyright (C) 2019-2024 PyFPGA Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +""" +Implements support for Gowin. +""" + +from pathlib import Path +from pyfpga.project import Project + + +class Gowin(Project): + """Class to support Gowin projects.""" + + def _configure(self): + tool = 'gowin' + command = 'gw_sh' + self.conf['tool'] = tool + self.conf['make_cmd'] = f'{command} {tool}.tcl' + self.conf['make_ext'] = 'tcl' + self.conf['prog_bit'] = ['fs', 'bin'] + self.conf['prog_cmd'] = f'bash {tool}-prog.sh' + self.conf['prog_ext'] = 'sh' + + def _make_custom(self): + if 'part' not in self.data: + self.data['part'] = 'GW2AR-LV18QN88C8/I7' + + def _get_bitstream(self, bitstream=None): + if not bitstream: + for ext in self.conf['prog_bit']: + candidate = ( + Path(self.odir) / + f'{self.data["project"]}/impl/pnr/{self.data["project"]}.{ext}' + ) + if candidate.is_file(): + bitstream = candidate + break + if not bitstream or not Path(bitstream).is_file(): + raise FileNotFoundError(bitstream) + return self._get_absolute(bitstream, self.conf['prog_ext']) diff --git a/pyfpga/helpers/prj2bit.py b/pyfpga/helpers/prj2bit.py index 9e4188dc..d8dc64ad 100644 --- a/pyfpga/helpers/prj2bit.py +++ b/pyfpga/helpers/prj2bit.py @@ -54,6 +54,7 @@ def main(): # Detecting a Project file tool_per_ext = { + '.gprj': 'gowin', '.ldf': 'diamond', '.xise': 'ise', '.prjx': 'libero', diff --git a/pyfpga/templates/gowin-prog.jinja b/pyfpga/templates/gowin-prog.jinja new file mode 100644 index 00000000..8f5ac956 --- /dev/null +++ b/pyfpga/templates/gowin-prog.jinja @@ -0,0 +1,16 @@ +{# +# +# Copyright (C) 2015-2024 PyFPGA Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +#} + +set -e + +# Prefer Gowin Programmer CLI when loaded; otherwise openFPGALoader. +if command -v programmer_cli >/dev/null 2>&1; then + programmer_cli --fsFile {{ bitstream }} +else + openFPGALoader {{ bitstream }} +fi diff --git a/pyfpga/templates/gowin.jinja b/pyfpga/templates/gowin.jinja new file mode 100644 index 00000000..4aaf832b --- /dev/null +++ b/pyfpga/templates/gowin.jinja @@ -0,0 +1,95 @@ +{# +# +# Copyright (C) 2015-2024 PyFPGA Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +#} + +{% if 'cfg' in steps %}# Project configuration ------------------------------------------------------- + +create_project -force -name {{ project }} -dir . -pn {{ part }} -device_version C + +{% if hooks %}{{ hooks.precfg | join('\n') }}{% endif %} + +{% if files %}# Files inclusion +{% for name, attr in files.items() %} +add_file {{ name }} +{% if 'lib' in attr %}set_property library {{ attr.lib }} [get_files {{ name }}]{% endif %} +{% endfor %} +{% endif %} + +{% if constraints %}# Constraints inclusion +{% for name, attr in constraints.items() %} +add_file {{ name }} +{% endfor %} +{% endif %} + +{% if top %}# Top-level specification +set_option -top_module {{ top }} +{% endif %} + +set_option -verilog_std sysv2017 + +{% if includes %}# Verilog Includes +set_option -include_path { {{- includes | join(';') -}} } +{% endif %} + +{% if defines %}# Verilog Defines (Gowin 1.9.11+; 1.9.9 has no -ifdef) +if {[catch {set_option -ifdef { {{ defines.items() | map('join', '=') | join(' ') }} }} err]} { + puts "WARNING: set_option -ifdef unsupported: $err" +} +{% endif %} + +{% if params %}# Verilog Parameters (Gowin 1.9.11+; 1.9.9 has no -verilog_param) +if {[catch {set_option -verilog_param { {{ params.items() | map('join', '=') | join(' ') }} }} err]} { + puts "WARNING: set_option -verilog_param unsupported: $err" +} +{% endif %} + +{% if hooks %}{{ hooks.postcfg | join('\n') }}{% endif %} + +{% endif %} + +{% if 'syn' in steps or 'par' in steps or 'bit' in steps %}# Design flow ----------------------------------------------------------------- + +{% if 'cfg' not in steps %} +# 1.9.11+ only. 1.9.9 has no open_project — run cfg+syn+pnr in one gw_sh. +if {[catch {open_project {{ project }}/{{ project }}.gprj} err]} { + puts "WARNING: open_project failed: $err" +} +{% endif %} + +{% if 'syn' in steps %}# Synthesis + +{% if hooks %}{{ hooks.presyn | join('\n') }}{% endif %} + +run syn + +{% if hooks %}{{ hooks.postsyn | join('\n') }}{% endif %} + +{% endif %} + +{% if 'par' in steps %}# Place and Route + +{% if hooks %}{{ hooks.prepar | join('\n') }}{% endif %} + +set_option -bit_format txt +run pnr + +{% if hooks %}{{ hooks.postpar | join('\n') }}{% endif %} + +{% endif %} + +{% if 'bit' in steps %}# Bitstream generation + +{% if hooks %}{{ hooks.prebit | join('\n') }}{% endif %} + +# There is no bit phase in gowin, the bitstream is created at the end of the 'par' phase +{% if hooks %}{{ hooks.postbit | join('\n') }}{% endif %} + +{% endif %} + +run close + +{% endif %} diff --git a/tests/mocks/gw_sh b/tests/mocks/gw_sh new file mode 100755 index 00000000..fb23dee3 --- /dev/null +++ b/tests/mocks/gw_sh @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2022-2025 PyFPGA Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import argparse +import os +import re +import subprocess + + +parser = argparse.ArgumentParser() + +parser.add_argument('source') + +args = parser.parse_args() + +tool = parser.prog + +tcl = f''' +proc unknown args {{ }} + +source {args.source} +''' + +with open(f'{tool}-mock.tcl', 'w', encoding='utf-8') as file: + file.write(tcl) + +subprocess.run( + f'tclsh {tool}-mock.tcl', + shell=True, + check=True, + universal_newlines=True +) + +pattern = r'create_project\s+-force\s+-name\s+(\S+)\s' +with open(args.source, 'r', encoding='utf-8') as file: + match = re.search(pattern, file.read()) + if match: + project = match.group(1) + os.makedirs(f'{project}/impl/pnr', exist_ok=True) + open(f'{project}/impl/pnr/{project}.fs', 'w', encoding='utf-8').close() + +print(f'INFO:the {tool.upper()} mock has been executed') diff --git a/tests/mocks/openFPGALoader b/tests/mocks/openFPGALoader new file mode 100755 index 00000000..ab69f58d --- /dev/null +++ b/tests/mocks/openFPGALoader @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2022-2025 PyFPGA Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +print('INFO:the OPENFPGALOADER mock has been executed') diff --git a/tests/mocks/programmer_cli b/tests/mocks/programmer_cli new file mode 100755 index 00000000..c7d334d9 --- /dev/null +++ b/tests/mocks/programmer_cli @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2022-2025 PyFPGA Project +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +print('INFO:the PROGRAMMER_CLI mock has been executed') diff --git a/tests/regress.sh b/tests/regress.sh index 6e4c1186..ec3c3ab1 100644 --- a/tests/regress.sh +++ b/tests/regress.sh @@ -16,6 +16,7 @@ echo "########################################################################## declare -A TOOLS +TOOLS["gowin"]="tangnano20k" TOOLS["diamond"]="brevia2" TOOLS["ise"]="s6micro nexys3" TOOLS["libero"]="maker" diff --git a/tests/test_tools.py b/tests/test_tools.py index a023ea32..e8d31a2f 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -14,6 +14,14 @@ def test_diamond(): assert Path(f'{base}-prog.sh').exists(), 'file not found' +def test_gowin(): + tool = 'gowin' + generate(tool, 'GW2AR-LV18QN88C8/I7') + base = f'results/{tool}/{tool}' + assert Path(f'{base}.tcl').exists(), 'file not found' + assert Path(f'{base}-prog.sh').exists(), 'file not found' + + def test_ise(): tool = 'ise' generate(tool, 'DEVICE-PACKAGE-SPEED')