diff --git a/README.md b/README.md index c599f67..3c901cf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ ![](assets/manta.png) ## Manta: An In-Situ Debugging Tool for Programmable Hardware - ![functional_simulation](https://github.com/fischermoseley/manta/actions/workflows/functional_simulation.yml/badge.svg) ![formal_verification](https://github.com/fischermoseley/manta/actions/workflows/formal_verification.yml/badge.svg) ![cross_platform_tests](https://github.com/fischermoseley/manta/actions/workflows/cross_platform_tests.yml/badge.svg) @@ -13,7 +12,6 @@ Manta is a tool for debugging FPGA designs over UART. It has two modes for doing Manta is written in Python, and generates SystemVerilog HDL. It's cross-platform, and its only dependency is pySerial. The SystemVerilog templates are included in the Python source, so only a single python file must be included in your project. ## Downlink - Manta's downlink mode works by taking a JSON file describing the ILA configuration, and autogenerating a debug core with SystemVerilog. This gets included in the rest of the project's HDL, and is synthesized and flashed on the FPGA. It can then be controlled by a host machine connected over a serial port. The host can arm the core, and then when a trigger condition is met, the debug output is wired back to the host, where it's saved as a waveform file. This can then be opened and inspected in a waveform viewer like GTKWave. This is similar to Xilinx's Integrated Logic Analyzer (ILA) and Intel/Altera's SignalTap utility. @@ -22,19 +20,16 @@ This is similar to Xilinx's Integrated Logic Analyzer (ILA) and Intel/Altera's S ## Getting Started - Since Manta is designed to be both cross-platform and unintrusive to your project source, it's packaged as a single python file with the HDL templates built in. This isn't the cleanest thing to develop with, so it's developed as a set of files that are stitched together into a single Python script. This isn't compilation since we're not going to machine code - we're just building a script, not a binary. ### Using a prebuilt script - -Copy `ila.py` into the root of your project directory. Also copy a configuration template (`config_template.yml` or `config_template.json`) if you wish. +Copy `ila.py` into the root of your project directory. You'll also need a configuration file - check out `examples/` if you need some help putting one of those together. ### Building from source - -Clone the repo, and then run `build.py`. This will output an `ila.py`, which you're free to use. +Clone the repo, and then run `build.py`. This will output a `manta.py`, which you're free to use. ## Examples Examples can be found under `examples/`. -## About -Manta was originally developed as part of my Master's Thesis at MIT in 2023, done under the supervision of Joe Steinmeyer. But I think it's a neat tool, so I'm still working on it :) +## About +Manta was originally developed as part of my [Master's Thesis at MIT](dspace.mit.edu) in 2023, done under the supervision of Joe Steinmeyer. But I think it's a neat tool, so I'm still working on it :) \ No newline at end of file diff --git a/build.py b/build.py index e69de29..099c7e3 100644 --- a/build.py +++ b/build.py @@ -0,0 +1,5 @@ +import os + +os.system('echo "#!/usr/bin/python3" > manta') +os.system('cat manta.py >> manta') +os.system('chmod +x manta') \ No newline at end of file diff --git a/config_template.json b/config_template.json deleted file mode 100644 index e69de29..0000000 diff --git a/config_template.yaml b/config_template.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/examples/counter/ila.json b/examples/counter/ila.json index 8bfd5a7..db2d312 100644 --- a/examples/counter/ila.json +++ b/examples/counter/ila.json @@ -1,25 +1,23 @@ { + "downlink": { + "sample_depth": 4096, + "clock_freq": 100000000, "probes": { - "larry" : 1, - "curly" : 1, - "moe" : 1, - "shemp" : 3 + "larry": 1, + "curly": 1, + "moe": 1, + "shemp": 3 }, - "triggers": [ "larry && curly && ~moe" - ], - - "uart": { - "baudrate" : 115200, - "data" : 8, - "parity" : "none", - "stop" : 1, - "port" : "/dev/tty.usbserial-2102926963071", - "timeout" : 1 - }, - - "sample_depth": 4096, - "output_dir" : "src/ila.svh", - "clock_freq" : 100e6 - } \ No newline at end of file + ] + }, + "uart": { + "baudrate": 115200, + "port": "/dev/tty.usbserial-2102926963071", + "data": 8, + "parity": "none", + "stop": 1, + "timeout": 1 + } +} \ No newline at end of file diff --git a/examples/counter/ila.yaml b/examples/counter/ila.yaml new file mode 100644 index 0000000..64ddc4c --- /dev/null +++ b/examples/counter/ila.yaml @@ -0,0 +1,21 @@ +--- +downlink: + sample_depth: 4096 + clock_freq: 100000000 + + probes: + larry: 1 + curly: 1 + moe: 1 + shemp: 3 + + triggers: + - larry && curly && ~moe + +uart: + baudrate: 115200 + port: "/dev/tty.usbserial-2102926963071" + data: 8 + parity: none + stop: 1 + timeout: 1 \ No newline at end of file diff --git a/gen_ila.py b/gen_ila.py index dc9a33c..33e60c1 100644 --- a/gen_ila.py +++ b/gen_ila.py @@ -1,4 +1,5 @@ import json +import yaml from datetime import datetime import os @@ -43,8 +44,6 @@ concat = ', '.join(concat) concat = '{' + concat + '};' ila_template = splice(ila_template, '@CONCAT', concat); - - # add probes to ila module definition probe_verilog = [] for name, width in config['probes'].items(): diff --git a/logo.py b/logo.py deleted file mode 100644 index 20128b1..0000000 --- a/logo.py +++ /dev/null @@ -1,16 +0,0 @@ -logo = """ - (\.-./) - / \\ - .' : '. - _.-'` ' `'-._ | Manta 0.0.1 - .-' : '-. | An In-Situ Debugging Tool for Programmable Hardware - ,'_.._ . _.._', | https://github.com/fischermoseley/manta - '` `'-. ' .-'` `' - '. : .' | fischerm@mit.edu - \_. ._/ - \ |^| - | | ; - \'.___.' / - '-....-' """ - -print(logo) \ No newline at end of file diff --git a/manta.py b/manta.py new file mode 100644 index 0000000..3d92028 --- /dev/null +++ b/manta.py @@ -0,0 +1,324 @@ +from sys import argv +import os +import json +import yaml +from datetime import datetime +import serial + +debug = True +version = '0.0.0' +downlink_template = '' + +def load_config(path): + """Take path to configuration file, and retun the configuration as a python list/dict object.""" + extension = path.split('.')[-1] + + if 'json' in extension: + with open(path, 'r') as f: + config = json.load(f) + + return config + + elif 'yaml' in extension or 'yml' in extension: + with open(path, 'r') as f: + config = yaml.safe_load(f) + + return config + + else: + raise ValueError('Unable to recognize configuration file extension.') + +def check_config(config): + """Takes a list/dict python object representing a core configuration and throws an error if it is misconfigured.""" + + assert 'downlink' in config or 'uplink' in config, 'No downlink or uplink specified.' + + if 'downlink' in config: + + dl = config['downlink'] + assert dl['sample_depth'], 'Downlink core specified, but sample_depth not specified.' + assert dl['clock_freq'], 'Downlink core specified, but clock_freq not specified.' + assert dl['probes'] and len(dl['probes']) > 0, 'Downlink core specified, but no probes specified.' + assert dl['triggers'] and len(dl['triggers']) > 0, 'Downlink core specified, but no triggers specified.' + + # confirm core clock is sufficiently fast + prescaler = dl['clock_freq'] // config['uart']['baudrate'] + assert prescaler >= 2 + + # confirm actual baudrate and target baudrate are within 5% + actual_baudrate = config['downlink']['clock_freq'] / prescaler + baudrate_error = abs(actual_baudrate - config['uart']['baudrate']) / config['uart']['baudrate'] + assert baudrate_error <= 0.05, f'Unable to match target baudrate! Actual baudrate differs from target by {round(100*baudrate_error, 2)}%' + + if debug: + print(f'UART interface on debug core will the following configuration:') + print(f' - target_baudrate: {config["uart"]["baudrate"]}') + print(f' - prescaler: {prescaler}') + print(f' - actual_baudrate: {round(actual_baudrate, 2)}') + + if 'uplink' in config: + raise NotImplementedError('Cannot check configuration validity for uplinks just yet!') + + if 'uart' in config: + uart = config['uart'] + + # confirm number of data bits is valid + assert 'data' in uart, 'Number of data bits in UART interface not specified.' + assert uart['data'] in [8, 7, 6, 5], 'Invalid number of data bits.' + + # confirm number of stop bits is valid + assert uart['stop'] in [1, 1.5, 2], 'Invalid number of stop bits.' + + # confirm parity is valid + assert uart['parity'] in ['none', 'even', 'odd', 'mark', 'space'], 'Invalid parity setting.' + +def gen_downlink_core(config): + buf = downlink_template + dl = config['downlink'] + + # add timestamp + timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + buf = buf.replace('@TIMESTAMP', timestamp) + + # add user + user = os.environ.get('USER', os.environ.get('USERNAME')) + buf = buf.replace('@USER', user) + + # add trigger + trigger = [f'({trigger})' for trigger in dl['triggers']] + trigger = ' || '.join(trigger) + buf = buf.replace('@TRIGGER', trigger) + + # add concat + concat = [name for name in dl['probes']] + concat = ', '.join(concat) + concat = '{' + concat + '};' + buf = buf.replace('@CONCAT', concat) + + # add probes + probe_verilog = [] + for name, width in dl['probes'].items(): + if width == 1: + probe_verilog.append(f'input wire {name},') + + else: + probe_verilog.append(f'input wire [{width-1}:0] {name},') + + probe_verilog = '\n\t'.join(probe_verilog) + buf = buf.replace('@PROBES', probe_verilog) + + # add sample width + sample_width = sum([width for name, width in dl['probes'].items()]) + buf = buf.replace('@SAMPLE_WIDTH', sample_width) + + # add sample depth + buf = buf.replace('@SAMPLE_DEPTH', dl['sample_depth']) + + # uart config + buf = buf.replace('@DATA_WIDTH', str(config['uart']['data'])) + buf = buf.replace('@BAUDRATE', str(config['uart']['baudrate'])) + buf = buf.replace('@CLK_FREQ_HZ', str(config['uart']['clock_freq'])) + + return buf + +def print_help(): + help = f""" +Manta v{version} - An In-Situ Debugging Tool for Programmable Hardware + +Supported commands: + gen [config file] generate the core specified in the config file + run [config file] run the core specified in the config file + terminal [config file] present a minicom-like serial terminal with the UART settings in the config file + ports list all available serial ports + help display this help message + ray display a splash screen (hehe...splash screen) + """ + print(help) + +def print_ray(): + ray = f""" + (\.-./) + / \\ + .' : '. + _.-'` ' `'-._ | Manta v{version} + .-' : '-. | An In-Situ Debugging Tool for Programmable Hardware + ,'_.._ . _.._', | https://github.com/fischermoseley/manta + '` `'-. ' .-'` `' + '. : .' | fischerm [at] mit.edu + \_. ._/ + \ |^| + | | ; + \\'.___.' / + '-....-' """ + + print(ray) + +def setup_serial(ser, config): + ser.baudrate = config['uart']['baudrate'] + ser.port = config['uart']['port'] + ser.timeout = config['uart']['timeout'] + + # setup number of data bits + if config['uart']['data'] == 8: + ser.bytesize = serial.EIGHTBITS + + elif config['uart']['data'] == 7: + ser.bytesize = serial.SEVENBITS + + elif config['uart']['data'] == 6: + ser.bytesize = serial.SIXBITS + + elif config['uart']['data'] == 5: + ser.bytesize = serial.FIVEBITS + + else: + raise ValueError("Invalid number of data bits in UART configuration.") + + # setup number of stop bits + if config['uart']['stop'] == 1: + ser.stopbits = serial.STOPBITS_ONE + + elif config['uart']['stop'] == 1.5: + ser.stopbits = serial.STOPBITS_ONE_POINT_FIVE + + elif config['uart']['stop'] == 2: + ser.stopbits = serial.STOPBITS_TWO + + else: + raise ValueError("Invalid number of stop bits in UART configuration.") + + # setup parity + if config['uart']['parity'] == 'none': + ser.parity = serial.PARITY_NONE + + elif config['uart']['parity'] == 'even': + ser.parity = serial.PARITY_EVEN + + elif config['uart']['parity'] == 'odd': + ser.parity = serial.PARITY_ODD + + elif config['uart']['parity'] == 'mark': + ser.parity = serial.PARITY_MARK + + elif config['uart']['parity'] == 'space': + ser.parity = serial.PARITY_SPACE + + else: + raise ValueError("Invalid parity setting in UART configuration.") + +def read_serial(config): + # obtain bytestream from FPGA + with serial.Serial() as ser: + setup_serial(ser, config) + ser.open() + ser.flushInput() + ser.write(b'\x30') + data = ser.read(4096) + + return data + +def part_select(data, width): + top, bottom = width + + assert top >= bottom + + mask = 2**(top - bottom + 1) - 1 + return (data >> bottom) & mask + +def make_widths(config): + # {probe0, probe1, probe2} + # [12, 1, 3] should produce + # [ (11,0) , (12, 12), (15,13) ] + + widths = list(config['probes'].values()) + + parts = [] + for i, width in enumerate(widths): + if (i == 0): + parts.append( (width - 1, 0) ) + + else: + parts.append( ((parts[i-1][1] + width) , (parts[i-1][1] + 1)) ) + + # reversing this list is a little bit of a hack, should fix/document + return parts[::-1] + +def export_waveform(config, data, path): + extension = path.split('.')[-1] + + if extension == 'vcd': + from vcd import VCDWriter + vcd_file = open(path, 'w') + timestamp = datetime.now().strftime("%d/%m/%Y %H:%M:%S") + + with VCDWriter(vcd_file, timescale='10 ns', date=timestamp, version = 'manta') as writer: + + # add probes to vcd file + vcd_probes = [] + for name, width in config['probes'].items(): + probe = writer.register_var('ila', name, 'wire', size = width) + vcd_probes.append(probe) + + # calculate bit widths for part selecting + widths = make_widths(config) + + # slice data, and dump to vcd file + for timestamp, value in enumerate(data): + for probe_num, probe in enumerate(vcd_probes): + val = part_select(value, widths[probe_num]) + writer.change(probe, timestamp, val) + + vcd_file.close() + + else: + raise NotImplementedError('More file formats to come!') + +if __name__ == '__main__': + # print help menu if no args passed or help menu requested + if len(argv) == 1 or argv[1] == 'help': + print_help() + exit() + + # open minicom-like serial terminal with given config + elif argv[1] == 'terminal': + assert len(argv) == 3, 'Not enough (or too many) config files specified.' + + # TODO: make this work with a looser config file - it should work even if we just have a uart definition + config = load_config(argv[2]) + check_config(config) + + raise NotImplementedError('Miniterm console is still under development!') + + # list available serial ports + elif argv[1] == 'ports': + import serial.tools.list_ports + + for info in serial.tools.list_ports.comports(): + print(info) + + # show splash screen + elif argv[1] == 'ray': + print_ray() + exit() + + # generate the specified core + elif argv[1] == 'gen': + assert len(argv) == 4, 'Wrong number of arguments, only a config file and output file must both be specified.' + config = load_config(argv[2]) + check_config(config) + + with open(argv[3], 'w') as f: + f.write(gen_downlink_core(config)) + + # run the specified core + elif argv[1] == 'run': + assert len(argv) == 4, 'Wrong number of arguments, only a config file and output file must both be specified.' + config = load_config(argv[2]) + check_config(config) + + data = read_serial(config) + export_waveform(config, data, argv[3]) + + else: + print('Option not recognized.') + print_help()