diff --git a/README.rst b/README.rst index 3b30d3e..0f65d2d 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,56 @@ -A tool to program the FPGA on Basys 2 boards. +adepttool +======================= +A set of tools to program the FPGA on Basys 2 boards. + +Installation +---- +Run:: + + ./install.sh + + +Programmer +---- Usage: install Python 3 and libusb1, make sure you have the access rights to the USB device (use the attached udev rule if you must), then run:: - ./basys2_prog.py file.bit + basys2_prog file.bit + + +Project builder +---- +Creates handy `Makefile` for code synth and chip programming. +Usage:: + + basys2_prj [-vvv] + +Flow: + +1. Create project dir ``X``. +2. Create main file ``X.v`` with top module named ``X`` inside. +3. Write other modules. +4. Run ``basys2_prj`` and generate builder. +5. ``make`` +6. ``make prog`` + +EEP +---- +Simple communication with Basys2 board via EPP. [If you implemented such module, of course.] + +Get register:: + + basys2_epp -g ADDR + +Put register:: + + basys2_epp -p ADDR VAL + +``ADDR`` and ``VAL`` should be hexstring, eg. ``ff``, ``0xa1``. + +List devices +---- +Usage:: + + basys2_list + diff --git a/basys2_epp.py b/basys2_epp.py new file mode 100755 index 0000000..e977b9a --- /dev/null +++ b/basys2_epp.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +import argparse +import usb1 +from adepttool.device import get_devices +import sys + +parser = argparse.ArgumentParser(description='Super simple EPP communication with the FPGA on Basys 2.') +parser.add_argument('--device', type=int, help='Device index (Default: 0)', default=0) +parser.add_argument('--port', type=int, help='Port index (Default: 0)', default=0) +parser.add_argument('-p', nargs=2, metavar=("ADDR", "VAL"), help='Put VAL into ADDR. (eg. -p 0e 0x10) ') +parser.add_argument('-g', metavar="ADDR", help='Get value from ADDR. (eg. -g 0a)') + +args = parser.parse_args() + +with usb1.USBContext() as ctx: + devs = get_devices(ctx) + if args.device >= len(devs): + if not devs: + print('No devices found.') + else: + print('Invalid device index (max is {})'.format(len(devs)-1)) + sys.exit(1) + dev = devs[args.device] + dev.start() + port = dev.depp_ports[args.port] + port.enable() + + if args.g is not None: + addr = int(args.g, 16) + print("%x" % port.get_reg(addr, 1)[0]) + elif args.p is not None: + addr, val = args.p + addr, val = int(addr, 16), int(val, 16) + port.put_reg(addr, [val]) + else: + print('DEPP PORT {port.idx}: {port.caps:08x}'.format(port=port)) + + port.disable() + diff --git a/basys2_prj.py b/basys2_prj.py new file mode 100755 index 0000000..9d89f73 --- /dev/null +++ b/basys2_prj.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +from project import Pins, Project +import os +import logging +from argparse import ArgumentParser + + +if __name__ == '__main__': + parser = ArgumentParser(description='Basys2 project builder builder') + parser.add_argument('-v', action='count', default=0) + parser.add_argument('--pins', action="store_true", help='List available pins'); + args = parser.parse_args() + + if args.v < 1: + lvl = logging.ERROR + elif args.v < 2: + lvl = logging.WARN + else: + lvl = logging.INFO + logging.basicConfig(level=lvl) + + if args.pins: + pins = Pins() + pins.print_all() + else: + project = Project(root_dir=os.getcwd()) + project.generate_files() + diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3c00c8d --- /dev/null +++ b/install.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -x + +pip3 install --user -r requirements.txt + +ln -s ${PWD}/basys2_prj.py ~/.local/bin/basys2_prj +ln -s ${PWD}/basys2_prog.py ~/.local/bin/basys2_prog +ln -s ${PWD}/basys2_epp.py ~/.local/bin/basys2_epp +ln -s ${PWD}/list.py ~/.local/bin/basys2_list + +echo 'Remember about exporting ${HOME}/.local/bin in PATH' diff --git a/project/__init__.py b/project/__init__.py new file mode 100644 index 0000000..88baf91 --- /dev/null +++ b/project/__init__.py @@ -0,0 +1,2 @@ +from .project import Project +from .pins import Pins diff --git a/project/contents.py b/project/contents.py new file mode 100644 index 0000000..1ecba16 --- /dev/null +++ b/project/contents.py @@ -0,0 +1,30 @@ + +MAKEFILE = """ + +BUILD=out +TOP={top:} +TAR=$(BUILD)/$(TOP)_par.bit + +all: synth + +FILES={deps:} + +prog: $(TAR) + basys2_prog $(TAR) + +synth: $(BUILD) $(FILES) + cd $(BUILD) && xst -ifn ../$(TOP).xst && ngdbuild $(TOP) -uc ../$(TOP).ucf && map $(TOP) && par -w $(TOP).ncd $(TOP)_par.ncd && bitgen -w $(TOP)_par.ncd -g StartupClk:JTAGCLK + +$(BUILD): + mkdir -p $@ + +clean: + rm -rf $(BUILD) + + +.PHONY: clean synth + +""" + +XST_CMD = "run -ifn ../{prj:} -p xc3s100e-4-cp132 -top {top:} -ofn {top:}" + diff --git a/project/pins.py b/project/pins.py new file mode 100644 index 0000000..c4c005b --- /dev/null +++ b/project/pins.py @@ -0,0 +1,109 @@ +# This file is a general .ucf for Basys2 rev C board +# To use it in a project: +# - remove or comment the lines corresponding to unused pins +# - rename the used signals according to the project + +import logging + + +class Pins: + def __init__(self): + self.log = logging.getLogger(name='Pins') + loc = {} + extra = {} + + #=== CLOCKS + loc['mclk'] = ["B8"] + extra['mclk'] = {"CLOCK_DEDICATED_ROUTE": "FALSE"} + + loc['uclk'] = ["M6"] + extra['uclk'] = {"CLOCK_DEDICATED_ROUTE": "FALSE"} + + #=== SWITCHES + loc['sw'] = ["P11", "L3", "K3", "B4", "G3", "F3", "E2", "N3"] + + #=== BUTTONS + loc['btn'] = ["G12", "C11", "M4", "A7"] + + #=== LEDS + loc['Led'] = ["M5", "M11", "P7", "P6", "N5", "N4", "P4", "G1"] + + #=== 7-segment display + loc['seg'] = (["M12", "L13", "P12", "N11", "N14", "H12", "L14"])[::-1] + loc['an'] = ["F12", "J12", "M13", "K14"] + loc['dp'] = ["N13"] + + #=== EPP ext pins + loc['EppAstb'] = ["F2"] + loc['EppDstb'] = ["F1"] + loc['EppWR'] = ["C2"] + loc['EppWait'] = ["D2"] + loc['EppDB'] = ["N2", "M2", "M1", "L1", "L2", "H2", "H1", "H3"] + + #=== PS2 + loc['PS2C'] = ['B1'] + loc['PS2D'] = ['C3'] + extra['PS2C'] = {'DRIVE': 2, 'PULLUP': None} + extra['PS2D'] = {'DRIVE': 2, 'PULLUP': None} + + #=== VGA + loc['HSYNC'] = ['J14'] + loc['VSYNC'] = ['K13'] + loc['OutRed'] = ['C14', 'D13', 'F13'] + loc['OutGreen'] = ['F14', 'G13', 'G14'] + loc['OutBlue'] = ['H13', 'J13'] + extra['HSYNC'] = {'DRIVE': 2, 'PULLUP': None} + extra['VSYNC'] = {'DRIVE': 2, 'PULLUP': None} + extra['OutRed'] = {'DRIVE': 2, 'PULLUP': None} + extra['OutGreen'] = {'DRIVE': 2, 'PULLUP': None} + extra['OutBlue'] = {'DRIVE': 2, 'PULLUP': None} + + + self.extra = extra + self.loc = loc + + def get_ucf(self, ios): + + def get_extras(extras, name): + l = [] + if name in extras: + for k, v in extras[name].items(): + if v is None: + _s = k + else: + _s = '%s = %s' % (k, v) + l.append(_s) + return " | ".join(l) if l else None + + self.log.info("Generating constraint file...") + + ucf_content = "" + for n, r in ios: + if n not in self.loc: + self.log.error("`%s` external pin not found! Use --pins to see available external pins." % n) + exit(1) + + s = "" + extra_opts = get_extras(self.extra, n) + if r is None: + l = self.loc[n][0] + s += 'NET "%s" LOC = "%s";\n' % (n, l) + if extra_opts is not None: + s += 'NET "%s" ' % n + extra_opts + ";\n" + else: + a, b = r + for idx, l in enumerate(self.loc[n][a:b+1]): + s += 'NET "%s<%d>" LOC = "%s";\n' % (n, idx, l) + if extra_opts is not None: + s += 'NET "%s<%d>" ' % (n, idx) + extra_opts + ";\n" + self.log.info('Wire `%s` entries: \n' % n + s) + ucf_content += s + return ucf_content + + def print_all(self): + l = [(k,len(v)) for k, v in self.loc.items()] + print("{:^12s} {:^6s}".format("NET NAME", "WIDTH")) + for k, w in l: + print("{:>12s} {:^6d}".format(k, w)) + + diff --git a/project/project.py b/project/project.py new file mode 100644 index 0000000..727a88f --- /dev/null +++ b/project/project.py @@ -0,0 +1,73 @@ +import os +import glob +import logging +import re + +from .pins import Pins +from .contents import MAKEFILE, XST_CMD + + +class Project: + + def __init__(self, root_dir): + self.log = logging.getLogger(name='Builder') + + self.root_dir = root_dir + self.log.info("Root dir: {:s}".format(root_dir)) + + self.top_module = os.path.basename(root_dir) + self.log.info("Top module: {:s}".format(self.top_module)) + + self.files = glob.glob(root_dir + '/*.v') + self.log.info("Verilog files found: ") + for fn in self.files: + self.log.info(" - " + fn) + + def _ext_io(self): + with open(self.top_module + '.v', 'r') as f: + content = f.read() + res = re.match(".*module (?P\w+)\s*\((?P[\[\]\s\w_,:]+)\);.*", content, re.M | re.S).groupdict() + if self.top_module != res['name']: + self.log.error("Wrong top module name!") + exit() + + self.log.info('Found external IOs in `%s`:' % self.top_module) + ios = [] + for arg in res['args'].split(','): + res = re.match("(?Pinput|output|inout)\s+(wire)?\s*\[(?P.+):(?P.+)\]\s*(?P\w+)", arg.strip()) + if res is not None: + d = res.groupdict() + a, b = int(d['a']), int(d['b']) + x, y = min(a,b), max(a, b) + ios += [(d['name'], (x, y))] + self.log.info(' - {T:} [{a:}:{b:}] {name:}'.format(**d)) + else: + d = re.match("(?Pinput|output|inout)\s+(wire)?\s*(?P\w+)", arg.strip()).groupdict() + ios += [(d['name'], None)] + self.log.info(' - {T:} {name:}'.format(**d)) + return ios + + + def generate_files(self): + ext_ios = self._ext_io() + + # Generate UCF constraints file + with open(self.top_module + ".ucf", "w") as f: + ucf_str = Pins().get_ucf(ext_ios) + f.write(ucf_str) + + # Generate *.xst file + with open(self.top_module + ".xst", "w") as f: + f.write(XST_CMD.format(prj=self.top_module + ".prj", top=self.top_module)) + + # Generate *.prj file + with open(self.top_module + ".prj", "w") as f: + for fpath in self.files: + fn = os.path.basename(fpath) + f.write('verilog work %s\n' % fn) + + deps = [os.path.basename(fpath) for fpath in self.files] + deps = " ".join(deps) + with open("Makefile", "w") as f: + f.write(MAKEFILE.format(top=self.top_module, deps=deps)) +