Skip to content

Instantly share code, notes, and snippets.

@koehlma
Last active October 25, 2022 18:54
Show Gist options
  • Save koehlma/d20169f77b5055ca5a7fad1650986ada to your computer and use it in GitHub Desktop.
Save koehlma/d20169f77b5055ca5a7fad1650986ada to your computer and use it in GitHub Desktop.
Python interface to the ModelSim simulator.
# -*- coding: utf-8 -*-
# Copyright (C) 2016, Maximilian Köhl <mail@koehlma.de>
"""
Python interface to the ModelSim simulator. The simulator is instrumented using FIFO pipes
such that it becomes fully controllable from within Python using TCL commands.
"""
import contextlib
import enum
import os
import os.path
import re
import subprocess
import tempfile
from collections import namedtuple
from subprocess import Popen, check_output, STDOUT, DEVNULL
__version__ = (0, 1, 1, 'dev')
# the heart of the instrumentation: use blocking FIFO pipes to run TCL commands
SIMULATION_SCRIPT = r'''
set fifo_posi [open "__py_modelsim_posi.fifo" "r"]
set fifo_piso [open "__py_modelsim_piso.fifo" "w"]
puts $fifo_piso "ready"
flush $fifo_piso
gets $fifo_posi command
while {$command != "quit"} {
if {[catch {
set result [eval $command]
puts $fifo_piso "S:$result"
flush $fifo_piso
} error ]} {
puts $fifo_piso [string map {"\n" " "} "E:$error"]
flush $fifo_piso
}
gets $fifo_posi command
}
quit -f
'''
RelativeTime = namedtuple('RelativeTime', ['value', 'unit'])
AbsoluteTime = namedtuple('AbsoluteTime', ['value', 'unit'])
def encode_time(timespec):
if isinstance(timespec, int):
return '{{{} ns}}'.format(timespec)
elif isinstance(timespec, RelativeTime):
return '{{{} {}}}'.format(timespec.value, timespec.unit)
elif isinstance(timespec, AbsoluteTime):
return '{{@{} {}}}'.format(timespec.value, timespec.unit)
raise Exception('unknown time specification format')
def tcl_escape(value):
return '{{{}}}'.format(str(value))
class Library:
"""
ModelSim Verilog library with context manager support.
If the directory argument is omitted a temporary directory is created on entering the
context. Further the library is initialized and all Verilog files are compiled. When
leaving the context, all temporary resources are freed.
"""
def __init__(self, name, *files, directory=None):
self.name = name
self.directory = directory
self.files = list(files)
self.temporary = None
def __enter__(self):
if self.directory is None:
self.temporary = tempfile.TemporaryDirectory()
self.directory = self.temporary.name
self.initialize()
self.compile()
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
if self.temporary is not None:
self.temporary.cleanup()
self.directory = None
self.temporary = None
def initialize(self, *arguments):
"""
Initialize the library using the `vlib` command.
"""
if self.directory is None:
raise Exception('unable to explicitly initialize temporary library')
command = ['vlib', self.name] + list(arguments)
try:
check_output(command, cwd=str(self.directory), stderr=STDOUT)
except subprocess.CalledProcessError as error:
raise Exception('unable to initialize verilog library', error.output)
def compile(self, *arguments):
"""
Compile the Verilog files using the `vlog` command.
"""
if self.directory is None:
raise Exception('unable to explicitly compile temporary library')
command = ['vlog', '-work', self.name] + list(arguments)
command += (str(filename) for filename in self.files)
try:
check_output(command, cwd=str(self.directory), stderr=STDOUT)
except subprocess.CalledProcessError as error:
raise Exception('unable to compile verilog files', error.output)
def simulate(self, toplevel, *arguments, commandline=True, **keywords):
"""
Start the simulator with the given toplevel entity using the `vsim` command.
"""
if self.directory is None:
raise Exception('unable to simulate outside of the library context')
command = ['vsim'] + list(arguments)
if commandline:
command.append('-c')
command.append('{}.{}'.format(self.name, toplevel))
return Popen(command, cwd=str(self.directory), **keywords)
class Object:
"""
Represents a Verilog object within the simulation. Verilog objects support slicing for
arrays and member access for structs using native Python syntax.
"""
def __init__(self, path, simulator):
self.path = path
self.simulator = simulator
def __repr__(self):
return '<Object "{}">'.format(self.path)
def __truediv__(self, segment):
return Object('{}/{}'.format(self.path, segment), self.simulator)
def __getitem__(self, item):
if isinstance(item, int):
return self.simulator.examine(self.path + '({})'.format(item))
elif isinstance(item, slice):
if not isinstance(item.start, int) or not isinstance(item.stop, int):
raise Exception('unsupported slice types on verilog object')
if item.step is not None:
raise Exception('slice steps are not supported on verilog objects')
path = self.path + '({}:{})'.format(item.start, item.stop)
return self.simulator.examine(path)
else:
raise Exception('unsupported key access on verilog object')
def __setitem__(self, item, value):
if isinstance(item, int):
self.simulator.change(self.path + '({})'.format(item), value)
elif isinstance(item, slice):
if not isinstance(item.start, int) or not isinstance(item.stop, int):
raise Exception('unsupported slice types on verilog object')
if item.step is not None:
raise Exception('slice steps are not supported on verilog objects')
for offset, number in enumerate(value):
self[item.start + offset] = number
else:
raise Exception('unsupported key access on verilog object')
def __getattr__(self, name):
return Object(self.path + '.' + name, self.simulator)
@property
def value(self):
"""
The value of the object as returned by the `examine` TCL command.
"""
return self.simulator.examine(self.path)
def force(self, *arguments, **keywords):
"""
Force the value of a Verilog net by the `force` TCL command.
"""
return self.simulator.force(self.path, *arguments, **keywords)
def change(self, *arguments, **keywords):
"""
Change the value of a parameter, variable or register by the `change` TCL command.
"""
return self.simulator.change(self.path, *arguments, **keywords)
def nets(self):
"""
Find Verilog nets starting from the objects's path using the `find` command.
"""
return self.simulator.nets(self.path)
def signals(self):
"""
Find Verilog signals starting from the objects's path using the `find` command.
"""
return self.simulator.signals(self.path)
def instances(self):
"""
Find Verilog instances starting from the objects's path using the `find` command.
"""
return self.simulator.instances(self.path)
class TCLError(Exception):
"""
Error during the execution of a TCL command.
"""
def __init__(self, command, message):
super().__init__(message)
self.command = command
# regular expression for the `examine` command result parser
EXAMINE_REGEX = re.compile(r'(?P<begin>{)|(?P<end>})|(?P<value>[0-9A-Fa-fx]+)')
def parse_examine_result(string, base=2):
"""
Parse the result of an examine command into either a value or a list of values. Values
typically are integers, however they might `None` if the value is undefined.
"""
if not string.find('...') < 0:
raise Exception('examine command returned incomplete result')
stack = []
for match in EXAMINE_REGEX.finditer(string):
if match.lastgroup == 'value':
string = str(match.group('value'))
value = int(string, base) if string.find('x') < 0 else None
if stack:
stack[-1].append(value)
else:
return value
elif match.lastgroup == 'begin':
stack.append([])
elif match.lastgroup == 'end':
head = stack.pop()
if stack:
stack[-1].append(head)
else:
return head
raise Exception('unable to parse result of examine command')
# regular expression for the `find instances` command result parser
INSTANCES_REGEX = re.compile(r'{(?P<path>[^ ]+) \((?P<type>[^)]+)\)}')
def parse_find_instances_result(string, simulator):
"""
Parse the result of an `find instances`.
"""
result = {}
for match in INSTANCES_REGEX.finditer(string):
if match.group('type') not in result:
result[match.group('type')] = []
result[match.group('type')].append(Object(match.group('path'), simulator))
return result
class ForceModes(enum.Enum):
"""
Modes for the `force` command as described in the ModelSim documentation.
"""
FREEZE = '-freeze'
DRIVE = '-drive'
DEPOSIT = '-deposit'
class Simulator:
"""
Python interface to an instrumented ModelSim instance.
"""
def __init__(self, library, toplevel, libraries=None):
self.library = library
self.toplevel = toplevel
self.libraries = libraries or []
self.directory = None
self.running = False
self.process = None
self.posi = None
self.piso = None
self.time = None
# cache for examine results: speedup multiple accesses
self.examine_cache = {}
def __getitem__(self, path):
return Object(path, self)
def __truediv__(self, segment):
return Object('/{}/{}'.format(self.toplevel, segment), self)
def start(self, *arguments, stdout=DEVNULL, stderr=DEVNULL):
"""
Start an instrumented ModelSim instance simulating the toplevel entity.
"""
if self.running:
raise Exception('unable to start simulator: already running')
self.running = True
self.time = 0
self.directory = self.library.directory
posi_name = os.path.join(str(self.directory), '__py_modelsim_posi.fifo')
piso_name = os.path.join(str(self.directory), '__py_modelsim_piso.fifo')
script_name = os.path.join(str(self.directory), '__py_modelsim_script.do')
os.mkfifo(posi_name)
os.mkfifo(piso_name)
with open(script_name, 'wb') as script:
script.write(SIMULATION_SCRIPT.encode())
arguments = ['-do', '__py_modelsim_script.do'] + list(arguments)
for library in self.libraries:
arguments.append('-Lf')
arguments.append(library)
self.process = self.library.simulate(self.toplevel, *arguments,
stdout=stdout, stderr=stderr)
self.posi = open(str(posi_name), 'wb', 0)
self.piso = open(str(piso_name), 'rb')
self.examine_cache = {}
if self.piso.readline().decode().strip() != 'ready':
self.process.kill()
self.cleanup()
raise Exception('unable to start simulator: internal communication error')
def object(self, path):
"""
Return an object for the given path.
"""
return Object(path, self)
def execute(self, command):
"""
Execute the given TCL command by sending it to the simulator. Returns the result
or raises a `TCLError` if the command failed.
"""
if not self.running or self.process.returncode is not None:
raise Exception('unable to execute command: simulator not running')
self.posi.write((command + '\n').encode())
code, data = self.piso.readline().decode().partition(':')[::2]
if code == 'S':
return data.strip()
else:
raise TCLError(command, data)
def examine(self, path, cache=True):
"""
Issue an examine command for the given path. Since we do not log any signals per
default, time-travel is not supported.
"""
if cache and path in self.examine_cache:
return self.examine_cache[path]
else:
command = 'examine {}'.format(tcl_escape(path))
result = parse_examine_result(self.execute(command))
if cache:
self.examine_cache[path] = result
return result
def change(self, path, value):
"""
Change the value of Verilog parameters, registers, memories, and variables. This
allows us for instance to change memory content and thereby simulate MMIO.
"""
if isinstance(value, int):
value = bin(value)[2:]
elif isinstance(value, list):
value = tcl_escape(' '.join(bin(item)[2:] for item in value))
return self.execute('change {} {}'.format(tcl_escape(path), value))
def force(self, path, value, *arguments, mode=None, cancel=None, repeat=None):
"""
Force a Verilog net to a specified value. This allows us to control for instance
the clock signal or stimulate external interrupts.
"""
command = ['force']
if mode is not None:
command.append(mode.value)
if cancel is not None:
command.append('-cancel {}'.format(encode_time(cancel)))
if repeat is not None:
command.append('-repeat {}'.format(encode_time(repeat)))
if isinstance(value, int):
value = bin(value)[2:]
command.append(tcl_escape(path))
command.append(value)
command += arguments
return self.execute(' '.join(command))
def noforce(self, *paths):
"""
Removes the effect of any active force commands on the given objects.
"""
self.examine('noforce {}'.format(' '.join(map(tcl_escape, paths))))
def run(self, time):
"""
Run the simulation for `time` nanoseconds.
"""
self.examine_cache = {}
self.time += time
self.execute('run {}ns'.format(time))
def cleanup(self):
"""
Remove FIFO pipes and the TCL script.
"""
os.unlink(os.path.join(self.directory, '__py_modelsim_posi.fifo'))
os.unlink(os.path.join(self.directory, '__py_modelsim_piso.fifo'))
os.unlink(os.path.join(self.directory, '__py_modelsim_script.do'))
def quit(self):
"""
Quit the simulation and wait until it has terminated, cleanup afterwards.
"""
try:
self.posi.write('quit\n'.encode())
self.process.wait()
finally:
self.cleanup()
def find(self, kind, *arguments):
"""
Find simulation objects using the `find` TCL command.
"""
return self.execute('find {} {}'.format(kind, ' '.join(arguments)))
def nets(self, path=None):
"""
Find Verilog nets using the `find` TCL command.
"""
path = (path or '') + '/*'
nets = self.find('nets', '-internal', tcl_escape(path), '-recursive')
return list(map(self.object, nets.split()))
def signals(self, path=None):
"""
Find Verilog signals using the `find` TCL command.
"""
path = (path or '') + '/*'
signals = self.find('signals', '-internal', tcl_escape(path), '-recursive')
return list(map(self.object, signals.split()))
def instances(self, path=None):
"""
Find Verilog instances using the `find` TCL command.
"""
path = (path or '') + '/*'
instances = self.find('instances', '-recursive', tcl_escape(path))
return parse_find_instances_result(instances, self)
@contextlib.contextmanager
def simulate(toplevel, *files, libraries=None):
"""
Context manager for easy usage of the simulator.
"""
library = Library('simulation')
library.files.extend(files)
with library:
simulator = Simulator(library, toplevel, libraries)
simulator.start()
yield simulator
simulator.quit()
def interactive(toplevel, *files, namespace=None, libraries=None, **keywords):
"""
Launch an interactive interpreter to control the simulator.
"""
from ptpython.repl import embed
with simulate(toplevel, *files, libraries=libraries) as simulator:
namespace = namespace or {}
namespace.update({
'simulator': simulator,
'run': simulator.run,
'execute': simulator.execute,
'examine': simulator.examine,
'change': simulator.change,
'force': simulator.force,
'noforce': simulator.noforce,
'nets': simulator.nets,
'signals': simulator.signals,
'instances': simulator.instances
})
embed(namespace, namespace, **keywords)
@koehlma
Copy link
Author

koehlma commented Mar 19, 2021

I do not remember how exactly this is integrated with ModelSim. The simulate context manager (line 478) should be a good starting point. You first create a Library with your Verilog files. These files will be compiled using the vlog command (line 117) when entering the library context (line 484). Afterwards, you create a Simulator using the created library. I forgot what precisely all the arguments do, but it should be relatively straightforward to reverse engineer what they are doing by tracing how they are used in calls to vlib, vlog, and vsim. With the Simulator object you can run the simulation for a given amount of time using the run method (line 419) and afterwards examine the state of the simulator. Usually, one would do that in a loop, i.e., running the simulator for some time, then examining the state, and then running the simulator again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment