diff --git a/apio/managers/arguments.py b/apio/managers/arguments.py index bd1e9651..aec1826b 100644 --- a/apio/managers/arguments.py +++ b/apio/managers/arguments.py @@ -242,6 +242,7 @@ def process_arguments( # -- Build Scons flag list flags = serialize_scons_flags( { + "fpga_model": config[FPGA], "fpga_arch": config[ARCH], "fpga_size": config[SIZE], "fpga_type": config[TYPE], diff --git a/apio/resources/boards.json b/apio/resources/boards.json index 4fb9237d..1e3a2f4f 100644 --- a/apio/resources/boards.json +++ b/apio/resources/boards.json @@ -788,5 +788,16 @@ "ftdi": { "desc": "ETH4K" } + }, + "Sipeed-Tang-Nano-20k": { + "name":"Sipeed Tang Nano 20k", + "fpga": "GW2AR-LV18QN88C8/I7", + "programmer": { + "type": "openfpgaloader_tangnano20k" + }, + "usb": { + "vid": "0403", + "pid": "6010" + } } } diff --git a/apio/resources/distribution.json b/apio/resources/distribution.json index d3a46ae2..1dcad16a 100644 --- a/apio/resources/distribution.json +++ b/apio/resources/distribution.json @@ -10,6 +10,7 @@ "litterbox": ">=0.2.1,<0.3.0", "tinyfpgab": ">=1.1.0,<1.2.0", "tinyprog": ">=1.0.21,<1.1.0", - "icefunprog": ">=2.0.3,<3.0.0" + "icefunprog": ">=2.0.3,<3.0.0", + "apycula": "0.12" } } diff --git a/apio/resources/fpgas.json b/apio/resources/fpgas.json index c9d9c39e..5cd21e33 100644 --- a/apio/resources/fpgas.json +++ b/apio/resources/fpgas.json @@ -397,5 +397,11 @@ "type": "um5g-85k", "size": "85k", "pack": "CSFBGA285" - } + }, + "GW2AR-LV18QN88C8/I7": { + "arch": "gowin", + "type": "gw2a-18c", + "size": "20k", + "pack": "QN88" + } } diff --git a/apio/resources/gowin/SConstruct b/apio/resources/gowin/SConstruct new file mode 100644 index 00000000..8a85a284 --- /dev/null +++ b/apio/resources/gowin/SConstruct @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +# ---------------------------------------------------------------------- +# -- Generic Scons script for Sintesizing hardware on an FPGA and more. +# -- This file is part of the Apio project +# -- (C) 2016-2019 FPGAwars +# -- Authors Riley Wilton, Juan Gonzáles, Jesús Arroyo +# -- Licence GPLv2 +# ---------------------------------------------------------------------- + +import os +import re +from platform import system + +from SCons.Script import (Builder, DefaultEnvironment, Default, AlwaysBuild, + GetOption, Exit, COMMAND_LINE_TARGETS, ARGUMENTS, + Variables, Help, Glob) + +# -- Load arguments +PROG = ARGUMENTS.get('prog', '') +FPGA_MODEL = ARGUMENTS.get('fpga_model', '') +FPGA_SIZE = ARGUMENTS.get('fpga_size', '') +FPGA_TYPE = ARGUMENTS.get('fpga_type', '') +FPGA_PACK = ARGUMENTS.get('fpga_pack', '') +YOSYS_TOP = ARGUMENTS.get('top_module', '') +VERBOSE_ALL = ARGUMENTS.get('verbose_all', False) +VERBOSE_YOSYS = ARGUMENTS.get('verbose_yosys', False) +VERBOSE_PNR = ARGUMENTS.get('verbose_pnr', False) +TESTBENCH = ARGUMENTS.get('testbench', '') +VERILATOR_ALL = ARGUMENTS.get('all', False) +VERILATOR_NO_STYLE = ARGUMENTS.get('nostyle', False) +VERILATOR_NO_WARN = ARGUMENTS.get('nowarn', '').split(',') +VERILATOR_WARN = ARGUMENTS.get('warn', '').split(',') +VERILATOR_TOP = ARGUMENTS.get('top', '') +VERILATOR_PARAM_STR = '' +for warn in VERILATOR_NO_WARN: + if warn != '': + VERILATOR_PARAM_STR += ' -Wno-' + warn + +for warn in VERILATOR_WARN: + if warn != '': + VERILATOR_PARAM_STR += ' -Wwarn-' + warn + + + +# -- Size. Possible values: 20k +# -- Type. Possible values: gw2ar +# -- Package. Unused. + + +# -- Add the FPGA flags as variables to be shown with the -h scons option +vars = Variables() +vars.Add('fpga_size', 'Set the Gowin FPGA size (25k)', FPGA_SIZE) +vars.Add('fpga_type', 'Set the Gowin FPGA type (gw5a)', FPGA_TYPE) +vars.Add('fpga_pack', 'Set the Gowin FPGA packages', FPGA_PACK) +vars.Add('fpga_model', 'Set a specific Gowin FPGA model', FPGA_MODEL) + +# -- Create environment +env = DefaultEnvironment(ENV=os.environ, + tools=[], + variables=vars) + +# -- Show all the flags defined, when scons is invoked with -h +Help(vars.GenerateHelpText(env)) + +# -- Just for debugging +if 'build' in COMMAND_LINE_TARGETS or \ + 'upload' in COMMAND_LINE_TARGETS or \ + 'time' in COMMAND_LINE_TARGETS: + + # print('FPGA_SIZE: {}'.format(FPGA_SIZE)) + # print('FPGA_TYPE: {}'.format(FPGA_TYPE)) + # print('FPGA_PACK: {}'.format(FPGA_PACK)) + + if 'upload' in COMMAND_LINE_TARGETS: + + if PROG == '': + print('Error: no programmer command found') + Exit(1) + + # print('PROG: {}'.format(PROG)) + +# -- Resources paths +IVL_PATH = os.environ['IVL'] if 'IVL' in os.environ else '' +ICEBOX_PATH = os.environ['ICEBOX'] if 'ICEBOX' in os.environ else '' +CHIPDB_PATH = os.path.join(ICEBOX_PATH, 'chipdb-{0}.txt'.format(FPGA_SIZE)) +YOSYS_PATH = os.environ['YOSYS_LIB'] if 'YOSYS_LIB' in os.environ else '' + +isWindows = 'Windows' == system() +VVP_PATH = '' if isWindows or not IVL_PATH else '-M "{0}"'.format(IVL_PATH) +IVER_PATH = '' if isWindows or not IVL_PATH else '-B "{0}"'.format(IVL_PATH) + +# -- Target name +TARGET = 'hardware' + +# -- Scan required .list files +list_files_re = re.compile(r'[\n|\s][^\/]?\"(.*\.list?)\"', re.M) + + +def list_files_scan(node, env, path): + contents = node.get_text_contents() + includes = list_files_re.findall(contents) + return env.File(includes) + + +list_scanner = env.Scanner(function=list_files_scan) + +# -- Get a list of all the verilog files in the src folfer, in ASCII, with +# -- the full path. All these files are used for the simulation +v_nodes = Glob('*.v') +v_files = [str(f) for f in v_nodes] + +# Construct disjoint lists of .v module and testbench files. +src_synth = [f for f in v_files if f[-5:].upper() != '_TB.V'] +list_tb = [f for f in v_files if f[-5:].upper() == '_TB.V'] + +if len(src_synth) == 0: + print('Error: no verilog module files found (.v)') + Exit(1) + +# -- Get the CST file +CST = '' +CST_list = Glob('*.cst') + +try: + CST = CST_list[0] +except IndexError: + print('\n---> WARNING: no CST file found (.cst)\n') + +# -- Debug +# print('CST Found: {}'.format(CST)) + +# -- Synthesizing Builder +synth_builder = Builder( + action='yosys -D LEDS_NR=6 -p \"synth_gowin {0} -json $TARGET\" {1} $SOURCES'.format( + ('-top '+YOSYS_TOP) if YOSYS_TOP else '', + '' if VERBOSE_ALL or VERBOSE_YOSYS else '-q' + ), + suffix='.json', + src_suffix='.v', + source_scanner=list_scanner) +env.Append(BUILDERS={'Synth': synth_builder}) + +# -- Place and route Builder. +pnr_builder = Builder( + action='nextpnr-himbaechel --device {0} --json $SOURCE --write $TARGET --vopt family={5} --vopt cst={3} {4}'.format( + FPGA_MODEL, FPGA_SIZE, FPGA_PACK, CST, + '' if VERBOSE_ALL or VERBOSE_PNR else '-q', FPGA_TYPE), + suffix='.pnr.json', + src_suffix='.json') +env.Append(BUILDERS={'PnR': pnr_builder}) + +# -- Bitstream Builder. +bitstream_builder = Builder( + action='gowin_pack -d {0} -o $TARGET $SOURCE'.format(FPGA_TYPE.upper()), + suffix='.fs', + src_suffix='.pnr.json') +env.Append(BUILDERS={'Bin': bitstream_builder}) + +# -- Time report builder +# https://github.com/cliffordwolf/icestorm/issues/57 +time_rpt_builder = Builder( + action='echo No timme analysis report implemented for Gowin devices $TARGET $SOURCE > $TARGET', + suffix='.rpt', + src_suffix='.pnr.json') +env.Append(BUILDERS={'Time': time_rpt_builder}) + +# -- Generate the bitstream +blif_target = env.Synth(TARGET, [src_synth]) +asc_target = env.PnR(TARGET, [blif_target, CST]) +bitstream_target = env.Bin(TARGET, asc_target) + +build_target = env.Alias('build', bitstream_target) +AlwaysBuild(build_target) + +if(VERBOSE_ALL or VERBOSE_YOSYS): + AlwaysBuild(blif_target) +if(VERBOSE_ALL or VERBOSE_PNR): + AlwaysBuild(asc_target) + +# -- Upload the bitstream into FPGA +upload_target = env.Alias('upload', bitstream_target, '{0} $SOURCE'.format(PROG)) +AlwaysBuild(upload_target) + +# -- Target time: calculate the time +rpt_target = env.Time(asc_target) +AlwaysBuild(rpt_target) +time_target = env.Alias('time', rpt_target) + +# -- Icarus Verilog builders + +def iverilog_generator(source, target, env, for_signature): + """Constructs dynamically a commands for iverlog targets builders. """ + target_name, _ = os.path.splitext(str(target[0])) # E.g. "my_module" or"my_module_tb" + # Testbenches use the value macro VCD_OUTPUT to know the name of the waves output file. + # We also pass a dummy when the verify command to avoid a warning about the undefined macro. + is_testbench = target_name.upper().endswith("_TB") + is_verify = 'verify' in COMMAND_LINE_TARGETS + vcd_output_flag = ( + f'-D VCD_OUTPUT=dummy_vcd_output' if is_verify + else f'-D VCD_OUTPUT={target_name}' if is_testbench + else "") + # If running a testbench with the sim command, we define the macro INTERACTIVE_SIM that + # allows the testbench to supress assertions so we can examine the waves in gtkwave. + # For example, with an assertion macro like this one that fails when running apio test. + # `define EXPECT(signal, value) \ + # if (signal !== value) begin \ + # $display("ASSERTION FAILED in %m: signal != value"); \ + # `ifndef INTERACTIVE_SIM \ + # $fatal; \ + # `endif \ + # end + is_interactive_sim = is_testbench and 'sim' in COMMAND_LINE_TARGETS + interactive_sim_flag = f'-D INTERACTIVE_SIM' if is_interactive_sim else "" + result = 'iverilog {0} -o $TARGET {1} {2} -D NO_ICE40_DEFAULT_ASSIGNMENTS "{3}/ice40/cells_sim.v" $SOURCES'.format( + IVER_PATH, vcd_output_flag, interactive_sim_flag, YOSYS_PATH) + return result + +iverilog_builder = Builder( + # Action string is computed automatically by the generator. + generator = iverilog_generator, + suffix='.out', + src_suffix='.v', + source_scanner=list_scanner) +env.Append(BUILDERS={'IVerilog': iverilog_builder}) + +dot_builder = Builder( + action='yosys -f verilog -p \"show -format dot -colors 1 -prefix hardware {0}\" {1} $SOURCES'.format( + YOSYS_TOP if YOSYS_TOP else 'unknown_top', + '' if VERBOSE_ALL else '-q' + ), + suffix='.dot', + src_suffix='.v', + source_scanner=list_scanner) +env.Append(BUILDERS={'DOT': dot_builder}) + +svg_builder = Builder( + # Expecting graphviz dot to be installed and in the path. + action='dot -Tsvg $SOURCES -o $TARGET', + suffix='.svg', + src_suffix='.dot', + source_scanner=list_scanner) +env.Append(BUILDERS={'SVG': svg_builder}) + +# NOTE: output file name is defined in the iverilog call using VCD_OUTPUT macro +vcd_builder = Builder( + action='vvp {0} $SOURCE'.format( + VVP_PATH), + suffix='.vcd', + src_suffix='.out') +env.Append(BUILDERS={'VCD': vcd_builder}) + +# --- Verify +vout_target = env.IVerilog(TARGET, src_synth + list_tb) +AlwaysBuild(vout_target) +verify_target = env.Alias('verify', vout_target) + +# --- Graph +# TODO: Launch some portable SVG (or differentn format) viewer. +dot_target = env.DOT(TARGET, src_synth) +AlwaysBuild(dot_target) +svg_target = env.SVG(TARGET, dot_target) +AlwaysBuild(svg_target) +graph_target = env.Alias('graph', svg_target) + +# --- Simulation +# Since the simulation targets are dynamic due to the testbench selection, we +# create them only when running simulation. +if 'sim' in COMMAND_LINE_TARGETS: + assert 'test' not in COMMAND_LINE_TARGETS, COMMAND_LINE_TARGETS + if TESTBENCH: + # Explicit testbench file name is given via --testbench. + sim_testbench = TESTBENCH + else: + # No --testbench flag specified. If there is exactly one testbench then pick + # it, otherwise fail. + if len(list_tb) == 0: + print('Error: no testbench found for simulation.') + Exit(1) + if len(list_tb) > 1: + # TODO: consider to allow specifying the default testbench in apio.ini. + print('Error: found {} testbranches, please use the --testbench flag.'.format(len(list_tb))) + for tb in list_tb: + print('- {}'.format(tb)) + Exit(1) + sim_testbench = list_tb[0] # Pick the only available testbench. + # Here sim_testbench contains the testbench, e.g. my_module_tb.v. + # Construct list of files to build. + src_sim = [] + src_sim.extend(src_synth) # All the .v files. + src_sim.append(sim_testbench) + # Create targets sim target and its dependent. + sim_name, _ = os.path.splitext(sim_testbench) #e.g. my_module_tb + sout_target = env.IVerilog(sim_name, src_sim) + vcd_file_target = env.VCD(sout_target) + # 'do_initial_zoom_fit' does max zoom only if .gtkw file not found. + waves_target = env.Alias('sim', vcd_file_target, 'gtkwave {0} {1} {2}.gtkw'.format( + '--rcvar "splash_disable on" --rcvar "do_initial_zoom_fit 1"', + vcd_file_target[0], sim_name)) + AlwaysBuild(waves_target) + + +# --- Testing +# Since the simulation targets are dynamic due to the testbench selection, we +# create them only when running simulation. +if 'test' in COMMAND_LINE_TARGETS: + assert 'sim' not in COMMAND_LINE_TARGETS, COMMAND_LINE_TARGETS + if TESTBENCH: + # Explicit testbench file name is given via --testbench. We test just that one. + test_tbs= [ TESTBENCH ] + else: + # No --testbench flag specified. We will test all them. + if len(list_tb) == 0: + print('Error: no testbenchs found for simulation.') + Exit(1) + test_tbs= list_tb # All testbenches. + tests = [] # Targets of all tests + for test_tb in test_tbs: + # Create a list of source files. All the modules + the current testbench. + src_test = [] + src_test.extend(src_synth) # All the .v files. + src_test.append(test_tb) + # Create the targets for the 'out' and 'vcd' files of the testbench. + # NOTE: Remove the two AlwaysBuild() calls below for an incremental test. Fast, correct, + # but may confuse the user seeing nothing happens. + test_name, _ = os.path.splitext(test_tb) #e.g. my_module_tb + test_out_target = env.IVerilog(test_name, src_test) + AlwaysBuild(test_out_target) + test_vcd_target = env.VCD(test_out_target) + AlwaysBuild(test_vcd_target) + test_target = env.Alias(test_name, [test_out_target, test_vcd_target]) + tests.append(test_target) + # Create a target for the test command that depends on all the test targets. + tests_target = env.Alias('test', tests) + AlwaysBuild(tests_target) + +# -- Verilator builder +verilator_builder = Builder( + action='verilator --lint-only --timing -Wno-TIMESCALEMOD {0} {1} {2} {3} $SOURCES'.format( + '-Wall' if VERILATOR_ALL else '', + '-Wno-style' if VERILATOR_NO_STYLE else '', + VERILATOR_PARAM_STR if VERILATOR_PARAM_STR else '', + '--top-module ' + VERILATOR_TOP if VERILATOR_TOP else ''), + src_suffix='.v', + source_scanner=list_scanner) +env.Append(BUILDERS={'Verilator': verilator_builder}) + +# --- Lint +lint_out_target = env.Verilator(TARGET, src_synth + list_tb) + +lint_target = env.Alias('lint', lint_out_target) +AlwaysBuild(lint_target) + +Default(bitstream_target) + +# -- These is for cleaning the artifact files. +if GetOption('clean'): + # Identify additional files that may not be associated with targets and + # associate them with a target such that they will be cleaned up as well. + # This cleans for example artifacts of past simulation since the testbench + # target are dynamic and changes with the selected testbench. + for glob_pattern in ['*.out', '*.vcd']: + for node in Glob(glob_pattern): + env.Clean(time_target, str(node)) + + env.Default([time_target, build_target, vout_target, graph_target]) + diff --git a/apio/resources/programmers.json b/apio/resources/programmers.json index 6a99920d..181fb04f 100644 --- a/apio/resources/programmers.json +++ b/apio/resources/programmers.json @@ -73,5 +73,9 @@ "openfpgaloader_usb-blaster": { "command": "openFPGALoader", "args": "-c usb-blaster -v --file-type bin" + }, + "openfpgaloader_tangnano20k": { + "command": "openFPGALoader", + "args": "-b tangnano20k -f" } }