import os
import logging
import datetime
import shutil
import traceback
import re
import shlex
from chiptools.common.filetypes import FileType
from chiptools.common import exceptions
from chiptools.common.exceptions import FileNotFoundError
from chiptools.wrappers import synthesiser
log = logging.getLogger(__name__)
# Options file to be used by XFLOW
XST_MIXED_OPT = """
FLOWTYPE = FPGA_SYNTHESIS;
Program xst
-ifn <design>_xst.scr; # input XST script file
-ofn <design>_xst.log; # output XST log file
-intstyle xflow; # Message Reporting Style: ise, xflow, or silent
ParamFile: <design>_xst.scr
"run";
"-ifn <synthdesign>"; # Input/Project File Name
"-ifmt mixed"; # Input Format
"-ofn <design>"; # Output File Name
"-ofmt ngc"; # Output File Format
"-top <design>"; # Top Design Name
"-generics %(generics)s";
"-p <partname>"; # Target Device
End ParamFile
End Program xst
"""
[docs]class Ise(synthesiser.Synthesiser):
"""
A ISE Synthesiser instance can be used to synthesise the files in the
given Project using the XFLOW utility or individual Xst, Map, Par,
Ngdbuild, Bitgen and Promgen tools provided in a base Xilinx ISE
installation. The ISE synthesis flow can be set to either *'manual'* flow
where the individual ISE binaries are called in sequence or *'xflow'*
where the XFLOW utility is called (effectively the same thing).
To use the ISE class it must be instanced with a Project and Options
object passed as arguments, the *'synthesise'* method may then be called
to initiate the synthesis flow.
In addition to running the synthesis flow, the ISE Synthesiser instance
also uses a Reporter instance to filter the synthesis log messages for
important information relating to the build.
When complete, the output files from synthesis will be stored in an
archive bearing the name of the entity that was synthesised and a unique
timestamp.
"""
name = 'ise'
executables = [
'xwebtalk',
'promgen',
'xst',
'map',
'par',
'ngdbuild',
'bitgen',
'xflow',
]
def __init__(self, project, user_paths, mode='manual'):
"""
Create a new ISE Synthesiser instance using the supplied Project and
Options objects with the optionsal string parameter *mode* set to
either 'manual' or 'xflow' to determine which ISE tool flow to use
during synthesis.
"""
super(Ise, self).__init__(project, self.executables, user_paths)
self.mode = mode
self.xwebtalk = os.path.join(self.path, 'xwebtalk')
self.promgen = os.path.join(self.path, 'promgen')
self.xst = os.path.join(self.path, 'xst')
self.map = os.path.join(self.path, 'map')
self.par = os.path.join(self.path, 'par')
self.ngdbuild = os.path.join(self.path, 'ngdbuild')
self.bitgen = os.path.join(self.path, 'bitgen')
self.xflow = os.path.join(self.path, 'xflow')
[docs] @synthesiser.throws_synthesis_exception
def makeProject(self, projectFilePath, fileFormat='mixed'):
"""
Generate a Xilinx ISE project file listing source files with their
filetypes and libraries.
ISE requires a project file to be written using the following format:
.. code-block: xml
<hdl_language> <compilation_library> <source_file>
Where *hdl_language* specifies whether the designated HDL source file
is written in VHDL or Verilog, *compilation_library* specifies the
library where the HDL is compiled and *source_file* specifies the path
to the source file.
This method generates an appropriate file from the project data that
has been loaded into the ISE Synthesiser instance.
"""
log.info('Creating project file for ISE...')
projectFileString = ''
fileSet = self.project.get_synthesis_fileset()
for libName, fileList in fileSet.items():
for fileObject in fileList:
# We could leave it to the synthesis tool to report missing
# files, but handling them here means we can abort the process
# early and notify the user.
if os.path.isfile(fileObject.path):
if fileObject.fileType == FileType.VHDL:
if fileFormat == 'mixed':
projectFileString += 'vhdl '
elif fileObject.fileType == FileType.Verilog:
if fileFormat == 'mixed':
projectFileString += 'verilog '
elif fileObject.fileType == FileType.SystemVerilog:
if fileFormat == 'mixed':
projectFileString += 'verilog '
elif fileObject.fileType == FileType.NGCNetlist:
base = os.path.dirname(projectFilePath)
newPath = os.path.join(
base, os.path.basename(fileObject.path)
)
if os.path.exists(newPath):
log.warning(
'File already exists: '
+ str(newPath)
+ ' and will be overwritten by: '
+ str(fileObject.path)
)
# Copy the NGC into the local directory
shutil.copyfile(fileObject.path, newPath)
continue
else:
raise exceptions.SynthesisException(
'Unknown file type for synthesis tool: '
+ fileObject.fileType
)
projectFileString += fileObject.library + ' '
projectFileString += fileObject.path + '\n'
else:
raise FileNotFoundError(fileObject.path)
# Write out the synthesis project file
log.debug('Writing: ' + projectFilePath)
with open(projectFilePath, 'w') as f:
f.write(projectFileString)
log.info('...done')
[docs] @synthesiser.throws_synthesis_exception
def synthesise(self, library, entity, fpga_part=None):
"""
Synthesise the target entity in the given library for the currently
loaded project. The following steps are performed during synthesis:
* Create synthesis directories
* Generate an ISE project file
* Generate an ISE UCF constraints file
* Invoke XFLOW or the flow tools individually with appropriate command
line arguments
* Generate reports
* Archive the outputs of the synthesis flow
"""
super(Ise, self).synthesise(library, entity, fpga_part)
# make a temporary working directory for the synth tool
import tempfile
startTime = datetime.datetime.now()
log.info(
'Turning Xilinx WebTalk off as it may prevent the removal of '
+ 'temporary directories'
)
try:
self.ise_webtalk_off()
except Exception:
log.debug(traceback.format_exc())
log.warning(
'Could not disable WebTalk, '
+ 'you may encounter PermissionErrors '
+ 'during temporary directory removal'
)
with tempfile.TemporaryDirectory(
dir=self.project.get_synthesis_directory()
) as workingDirectory:
log.info(
'Created temporary synthesis directory: ' + workingDirectory
)
synthName = (
entity + '_synth_' + startTime.strftime('%d%m%y_%H%M%S')
)
archiveName = synthName + '.tar'
synthesisDirectory = os.path.join(workingDirectory, synthName)
os.makedirs(synthesisDirectory)
if fpga_part is None:
fpga_part = self.project.get_fpga_part()
generics = self.project.get_generics().items()
generics = (
'{' + ' '.join(k + '=' + str(v) for k, v in generics) + '}'
)
projectFilePath = os.path.join(synthesisDirectory, entity + '.prj')
exportDirectory = os.path.join(synthesisDirectory, 'output')
reportDirectory = os.path.join(synthesisDirectory, 'reports')
# Add user constraints and other source files
self.addConstraints(entity, synthesisDirectory)
self.makeProject(projectFilePath)
if self.mode == 'xflow':
try:
# Run the flow
self.ise_xflow(
projectFilePath,
fpga_part,
entity,
generics,
synthesisDirectory,
reportDirectory,
exportDirectory,
)
self.generate_programming_files(entity, synthesisDirectory)
except Exception:
# Archive the outputs
log.error(
'Synthesis error, storing output in error directory...'
)
self.storeOutputs(workingDirectory, 'ERROR_' + archiveName)
raise
elif self.mode == 'manual':
try:
# Run the flow
self.ise_manual_flow(
projectFilePath,
fpga_part,
entity,
generics,
synthesisDirectory,
reportDirectory,
exportDirectory,
)
self.generate_programming_files(entity, synthesisDirectory)
except Exception:
# Archive the outputs
log.error(
'Synthesis error, storing output in error directory...'
)
self.storeOutputs(workingDirectory, 'ERROR_' + archiveName)
raise
else:
raise exceptions.SynthesisException(
'Invalid flow type: ' + self.mode
)
# Check the report
reporter_fn = self.project.get_reporter()
try:
if reporter_fn is not None:
reporter_fn(synthesisDirectory)
except Exception:
log.error(
'The post-synthesis reporter script caused an error:\n'
+ traceback.format_exc()
)
# Archive the outputs
log.info('Synthesis completed, saving output to archive...')
self.storeOutputs(workingDirectory, archiveName)
log.info('...done')
[docs] @synthesiser.throws_synthesis_exception
def generate_programming_files(self, entity, working_directory):
"""
Generate programming files using the output bitfile from the synthesis
run. An MCS file is always generated, but additional files can be
generated by adding 'args_ise_promgen_<format>' configuration items
with the tool arguments to apply when calling promgen for that output
format.
"""
# Get all ISE tool arguments for the PROMGEN stage:
arg_keys = self.project.get_all_tool_argument_keys(self.name)
def filter_args_fn(x):
return x.startswith('args_{0}_promgen'.format(self.name))
arg_keys = list(filter(filter_args_fn, arg_keys))
for key in arg_keys:
mode = key.split('_')[-1]
if len(mode) > 0 and mode != 'promgen':
log.info(
'Generating PROM file using user defined '
'arguments from configuration item: {0}'.format(key)
)
# Run promgen using the mode recovered from the config key
self.ise_make_prom_file(
entity + '.bit',
entity + '.' + mode,
working_directory,
mode=mode,
)
# Always ensure that an MCS file is created.
if 'args_{0}_promgen_mcs'.format(self.name) not in arg_keys:
self.ise_make_prom_file(
entity + '.bit', entity + '.mcs', working_directory, mode='mcs'
)
[docs] @synthesiser.throws_synthesis_exception
def ise_webtalk_off(self):
"""
Call the *xwebtalk* binary with the *-user off* switch to disable
WebTalk
"""
Ise._call(
self.xwebtalk,
['-user', 'off'],
cwd=self.project.get_synthesis_directory(),
quiet=False,
)
[docs] @synthesiser.throws_synthesis_exception
def ise_promgen(self, fin, fout, working_directory, args=None):
"""
Call the *promgen* binary, which accepts the following arguments:
Usage: promgen [-b] [-spi] [-p mcs|exo|tek|hex|bin|ieee1532|ufp] [-o
<outfile> {<outfile>}] [-s <size> {<size>}] [-x <xilinx_prom>
{<xilinx_prom>}] [-c [<hexbyte>]] [-l] [-w] [-bpi_dc serial|parallel]
[-intstyle ise|xflow|silent] [-t <templatefile[.pft]>] [-z
[<version:0,3>]] [-i <version:0,3>] [-data_width 8|16|32]
[-config_mode selectmap8|selectmap16|selectmap32] {-ver <version:0,3>
<file> {<file>}} {-u <hexaddr> <file> {<file>}} {-d <hexaddr> <file>
{<file>}} {-n <file> {<file>}} {-bd <file> [start <hexaddr>] [tag
<tagname> {<tagname>}]} {-bm <file>} {-data_file up|down <hexaddr>
<file> {<file>}} [-r <promfile>]
* *fin* is passed to the *<file>* input parameter
* *fout* is passed to the *-o* input parameter
* *workingDirectory* is the working directory where the tool is invoked
"""
# Get additional tool arguments for this flow stage if this method was
# not called with existing arguments.
if args is None:
args = self.project.get_tool_arguments(self.name, 'promgen')
# Allow the user to override PROM loading of bitfiles
if not any([k in args for k in ['-r', '-u', '-d', '-ver']]):
# Default to upward loading from address 0
args += ['-u', '0', fin]
else:
# User provided custom PROM loading argument which we will honour
pass
# Check that the user has supplied a mode
if not any([k in args for k in ['-p', 'mcs']]):
# Default to MCS if the user did not specify a mode
args += ['-p', 'mcs']
# Always overwrite existing files
if '-w' not in args:
args += ['-w']
# Allow the user to override the output file name
if '-o' not in args:
args += ['-o', fout]
Ise._call(self.promgen, args, cwd=working_directory, quiet=False)
[docs] @synthesiser.throws_synthesis_exception
def ise_make_prom_file(self, fin, fout, working_directory, mode='mcs'):
"""
Generate a programming file using the promgen tool, the user can
supply arguments to this flow stage with args_ise_promgen_<mode>.
"""
# Get additional tool arguments for this flow stage
args = self.project.get_tool_arguments(
self.name, 'promgen_{0}'.format(mode)
)
args = shlex.split(['', args][args is not None])
if not any([k in args for k in ['-p', mode]]):
args += ['-p', mode]
self.ise_promgen(fin, fout, working_directory, args)
[docs] @synthesiser.throws_synthesis_exception
def ise_xst(self, part, entity, generics, working_directory):
"""
Generate an XST settings file and call the *XST* binary
"""
# Get additional tool arguments for this flow stage
xstargs = self.project.get_tool_arguments(self.name, 'xst')
# Format the args as XST expects
xstargs = re.sub(' -', '\n-', xstargs)
# Write XST file
xst_scr = (
'run\n'
+ '-ifn %(entity)s.prj\n'
+ '-ofn %(entity)s.ngc\n'
+ '-ofmt NGC\n'
+ '-p %(part)s\n'
+ '-top %(entity)s\n'
+ '-generics %(synthesis_generics)s\n'
+ xstargs
+ '\n'
)
with open(os.path.join(working_directory, entity + '.xst'), 'w') as f:
f.write(
xst_scr
% dict(
entity=entity,
part=part,
synthesis_generics=generics,
)
)
args = ['-ifn', entity + '.xst']
args += ['-ofn', entity + '.log']
Ise._call(self.xst, args, cwd=working_directory, quiet=False)
[docs] @synthesiser.throws_synthesis_exception
def ise_map(self, part, entity, working_directory):
"""
Call the *MAP* binary, which accepts the following arguments:
map [-h] [-p partname] (infile[.ngd]) [-o (outfile[.ncd])]
http://www.xilinx.com/support/documentation/sw_manuals/xilinx14_1/devref.pdf
* *part* is passed to the *-p* input parameter
* *entity* is used to generate output file names
* *workingDirectory* is the working directory where the tool is invoked
"""
# Get additional tool arguments for this flow stage
args = self.project.get_tool_arguments(self.name, 'map')
args = shlex.split(['', args][args is not None])
# Part name
args += ['-p', part]
# Output file
args += ['-o', entity + '_map.ncd']
args += [entity + '.ngd']
# PCF Output name
args += [entity + '.pcf']
Ise._call(self.map, args, cwd=working_directory, quiet=False)
[docs] @synthesiser.throws_synthesis_exception
def ise_par(self, entity, working_directory):
"""
Call the *PAR* binary, which accepts the following arguments:
par [-ol std|high] [-pl std|high] [-rl std|high] [-xe n|c] [-mt
on|off|1| 2|3|4] [-t <costtable:1,100>] [-p] [-k]
[-r] [-w] [-smartguide <guidefile[.ncd]>] [-x] [-nopad] [-power
on|off|xe] [-act ivityfile <activityfile[.vcd|.saif]>]
[-ntd] [-intstyle ise|xflow|silent|pa] [-ise <projectrepositoryfile>]
[-filter < filter_file[.filter]>] <infile[.ncd]>
<outfile> [<constraintsfile[.pcf]>]
* *entity* is used to generate output file names
* *workingDirectory* is the working directory where the tool is invoked
"""
# Get additional tool arguments for this flow stage
args = self.project.get_tool_arguments(self.name, 'par')
args = shlex.split(['', args][args is not None])
# Infile
args += [entity + '_map.ncd']
# Output file
args += [entity + '.ncd']
# Physical Constraints File (auto generated)
args += [entity + '.pcf']
Ise._call(self.par, args, cwd=working_directory, quiet=False)
[docs] @synthesiser.throws_synthesis_exception
def ise_ngdbuild(self, part, entity, working_directory):
"""
Call the *NGDBUILD* binary, which accepts the following arguments:
Usage: ngdbuild [-p <partname>] {-sd <source_dir>} {-l <library>} [-ur
<rules_file[.urf]>] [-dd <output_dir>] [-r] [-a] [-u] [-nt
timestamp|on|off] [-uc <ucf_file[.ucf]>] [-aul] [-aut] [-bm
<bmm_file[.bmm]>] [-i] [-intstyle ise|xflow|silent] [-quiet]
[-verbose] [-insert_keep_hierarchy] [-filter <filter_file[.filter]>]
<design_name> [<ngd_file[.ngd]>]
* *entity* is used to generate input and output file names
* *-sd* is set to *workingDirectory*
* *-p* is set to *part*
* *workingDirectory* is the working directory where the tool is invoked
"""
# Get additional tool arguments for this flow stage
args = self.project.get_tool_arguments(self.name, 'ngdbuild')
args = shlex.split(['', args][args is not None])
# Constraints
args += ['-uc', entity + '.ucf']
# Search directory
args += ['-sd', working_directory]
# Part name
args += ['-p', part]
# Input design file
args += [entity + '.ngc']
# Output NGD file
args += [entity + '.ngd']
Ise._call(self.ngdbuild, args, cwd=working_directory, quiet=False)
[docs] @synthesiser.throws_synthesis_exception
def ise_bitgen(self, part, entity, working_directory):
"""
Call the *BITGEN* binary, which accepts the following arguments:
Usage: bitgen [-d] [-j] [-b] [-w] [-l] [-m] [-t] [-n] [-u] [-a] [-r
<bitFile>] [-intstyle ise|xflow|silent|pa] [-ise
<projectrepositoryfile>] {-bd <BRAM_data_file> [tag <tagname>]} {-g
<setting_value>} [-filter <filter_file[.filter]>] <infile[.ncd]>
[<outfile>] [<pcffile[.pcf]>]
* *entity* is used to generate input and output file names
* *workingDirectory* is the working directory where the tool is invoked
"""
# Get additional tool arguments for this flow stage
args = self.project.get_tool_arguments(self.name, 'bitgen')
args = shlex.split(['', args][args is not None])
# Input file
args += [entity + '.ncd']
# Output file
args += [entity + '.bit']
Ise._call(self.bitgen, args, cwd=working_directory, quiet=False)
[docs] @synthesiser.throws_synthesis_exception
def ise_xflow(
self,
projectFilePath,
part,
entity,
generics,
workingDirectory,
reportDirectory,
exportDirectory,
):
"""
Call the *XFLOW* binary, which accepts the following arguments:
xflow [-p partname] [flow type] [options file[.opt]] [xflow options]
design_name
XFLOW Flow Types:
Create a bitstream for FPGA device configuration using a routed
design.
-config option_file
Create a file that can be used for formal verification of an FPGA
design.
-ecn option_file
Incorporate logic from the design into physical macrocell locations
in a CPLD
-fit option_file
Generate a file that can be used for functional simulation of an
FPGA or CPLD design
-fsim option_file
Implement the design and output a routed NCD file
-implement option_file[fast_runtime.opt, balanced.opt,
high_effort.opt]
Create a file that can be used to perform static timing analysis
of an FPGA design
-sta option_file
Synthesise the design for implementation in an FPGA, for fitting
in a CPLD or for
compiling for functional simulation.
-syth option_file[xst_vhdl.opt/xst_verilog.opt/xst_mixed.opt]
Generate a file that can be used for timing simulation of an FPGA
or CPLD design.
-tsim option_file
"""
# Additional arguments are not supported for XFLOW, the XST flow should
# be used if more control of the stages is required.
if len(self.project.get_tool_arguments(self.name, 'xflow')) > 0:
log.warning(
'The ISE wrapper does not allow additional arguments'
+ ' to be passed to XFLOW. Use the XST flow if fine control'
+ ' of the synthesis stages is required.'
)
# Write the auto-generated options file
with open(os.path.join(workingDirectory, 'xst_custom.opt'), 'w') as f:
f.write(XST_MIXED_OPT % dict(generics=generics))
# Call the flow
args = ['-p', part]
args += ['-synth', 'xst_custom.opt']
args += ['-implement', 'balanced.opt']
args += ['-config', 'bitgen.opt']
args += ['-wd', workingDirectory]
args += ['-ed', exportDirectory]
args += ['-rd', reportDirectory]
args += [projectFilePath]
Ise._call(self.xflow, args, cwd=workingDirectory, quiet=False)
[docs] @synthesiser.throws_synthesis_exception
def ise_manual_flow(
self,
projectFilePath,
part,
entity,
generics,
workingDirectory,
reportDirectory,
exportDirectory,
):
"""
Execute the manual ISE tool flow in the following order:
#. XST
#. NGDBUILD
#. MAP
#. PAR
#. BITGEN
Refer to the individual documentation for these tools for more
information.
"""
# XST > NGDBUILD > MAP > PAR > BitGen > PromGen
self.ise_xst(part, entity, generics, workingDirectory)
self.ise_ngdbuild(part, entity, workingDirectory)
self.ise_map(part, entity, workingDirectory)
self.ise_par(entity, workingDirectory)
self.ise_bitgen(part, entity, workingDirectory)
[docs] @synthesiser.throws_synthesis_exception
def addConstraints(self, entity, synthesisDirectory):
"""
Load the user constraints file path from the Project instance and
generate a UCF file in the supplied *synthesisDirectory* directory
where the synthesis tools are invoked.
"""
# Add user constraints and other source files
constraintsFiles = self.project.get_constraints()
constraintsData = ''
filesProcessed = []
for fileObject in constraintsFiles:
# Avoid duplicates
if fileObject.path not in filesProcessed:
if fileObject.flow == 'ise' or fileObject.flow is None:
if fileObject.fileType == FileType.UCF:
# Copy the UCF data into the string var
with open(fileObject.path, 'r') as constraintsFile:
constraintsData += constraintsFile.read()
log.info(
'Added constraints file: ' + fileObject.path
)
filesProcessed.append(fileObject.path)
# Write the string var to a single file if we have data
if len(constraintsData) != 0:
newPath = os.path.join(synthesisDirectory, entity + '.ucf')
with open(newPath, 'w') as outFile:
outFile.write(constraintsData)
log.info('Wrote: ' + newPath)