Write a little test bench.
diff --git a/scripts/python-skywater-pdk/generate_symbols.sh b/scripts/python-skywater-pdk/generate_symbols.sh
index a063313..0b0410a 100755
--- a/scripts/python-skywater-pdk/generate_symbols.sh
+++ b/scripts/python-skywater-pdk/generate_symbols.sh
@@ -5,14 +5,20 @@
 	exit 1
 fi
 
-find $1/skywater-pdk/ -name *.v -delete
+set -x
+set -e
 
+find $1/skywater-pdk/ -name *.tb.v -delete
+find $1/skywater-pdk/ -name *.blackbox.v -delete
+find $1/skywater-pdk/ -name *.symbol.v -delete
+#find $1/skywater-pdk/ -name *.svg -delete
+
+./generate_verilog_blackbox.py $1/skywater-pdk/libraries/*/*/models/*
 ./generate_verilog_blackbox.py $1/skywater-pdk/libraries/*/*/cells/*
+exit
 
 (cd $1/ ; git diff --no-renames --name-only --diff-filter=D -z | xargs -0 git checkout --)
 
-exit
-
 for LIB in $1/skywater-pdk/libraries/*; do
 	LIBNAME=$(basename $LIB)
 
diff --git a/scripts/python-skywater-pdk/generate_verilog_blackbox.py b/scripts/python-skywater-pdk/generate_verilog_blackbox.py
index 7675c3e..4dd09aa 100755
--- a/scripts/python-skywater-pdk/generate_verilog_blackbox.py
+++ b/scripts/python-skywater-pdk/generate_verilog_blackbox.py
@@ -10,6 +10,8 @@
 # SPDX-License-Identifier: Apache-2.0
 
 
+import csv
+import itertools
 import json
 import os
 import pprint
@@ -19,7 +21,6 @@
 
 from collections import defaultdict
 
-
 copyright_header = """\
 /**
  * Copyright 2020 The SkyWater PDK Authors
@@ -392,6 +393,10 @@
             return cls.DATA_OUT_WORD
         if name in ('HI', 'LO'):
             return cls.DATA_OUT_WORD
+        if name in ('UDP_IN',):
+            return cls.DATA_IN_WORD
+        if name in ('UDP_OUT',):
+            return cls.DATA_OUT_WORD
 
         if re.search('^S[0-9]+$', name):
             return cls.DATA_CONTROL
@@ -404,6 +409,10 @@
 
         if re.search('^(DE)(_[NB])?$', name):
             return cls.DATA_CONTROL
+        if re.search('^(DATA_EN)$', name):
+            return cls.DATA_CONTROL
+        if re.search('^(SET_ASYNC)$', name):
+            return cls.DATA_CONTROL
 
         if re.search('^((CLK)|(GCLK)|(GATE))_EN$', name):
             return cls.CLOCK_CONTROL
@@ -417,6 +426,8 @@
             return cls.CLOCK
         if re.search('^[G]_?N?$', name):
             return cls.CLOCK
+        if re.search('^((CK)|(CP))$', name):
+            return cls.CLOCK
 
         if re.search('^SLEEP(_[NB])?$', name):
             return cls.POWER_CONTROL
@@ -431,6 +442,8 @@
             return cls.POWER_NEGATIVE
         if name.startswith('DEST'):
             return cls.POWER_OTHER
+        if 'NOT' in name:
+            return cls.POWER_OTHER
         if 'DIODE' in name:
             return cls.POWER_OTHER
         if 'PWR' in name:
@@ -460,68 +473,6 @@
 
 
 
-def write_verilog(define_data, outfile, drive=None):
-    with open(outfile, 'w') as f:
-        f.write(copyright_header)
-        f.write('\n')
-        f.write(include_header.format(define_data['fullname']))
-        f.write('\n')
-        if not drive:
-            return
-        drive_name, drive_value = drive
-
-        if not 'ports' in define_data:
-            return
-
-        module_signal_defports = []
-        module_signal_ports = []
-        for pname, ptype in define_data['ports']['signal']:
-            module_signal_defports.append("{} {}, ".format(ptype, pname))
-            module_signal_ports.append(pname)
-
-        module_signal_defports = "".join(module_signal_defports)
-        assert module_signal_defports.endswith(", "), module_signal_defports
-        module_signal_defports = module_signal_defports[:-2]
-        module_signal_ports = ", ".join(module_signal_ports)
-
-        module_power_defports = []
-        module_power_ports = []
-        for pname, ptype in define_data['ports']['power']:
-            module_power_defports.append(", {} {}".format('input', pname))
-            module_power_ports.append(", {}".format(pname))
-        module_power_defports = "".join(module_power_defports)
-        module_power_ports = "".join(module_power_ports)
-
-        library_name = "{} {}".format(
-            define_data['library']['name'].upper(), define_data['library']['type'])
-
-        f.write(module_header.format(
-            module_base_name = define_data['fullname'],
-            cell_name = define_data['name'],
-            library_name = library_name,
-            drive_name = drive_name,
-            drive_value = drive_value,
-            description = define_data.get('description', ''),
-            module_signal_defports = module_signal_defports,
-            module_signal_ports = module_signal_ports,
-            module_power_defports = module_power_defports,
-            module_power_ports = module_power_ports,
-        ))
-
-
-def echo_file(fname):
-    with open(fname) as f:
-        sys.stdout.write('\n')
-        sys.stdout.write('File: ')
-        sys.stdout.write(fname)
-        sys.stdout.write('\n')
-        sys.stdout.write('------\n')
-        sys.stdout.write(f.read())
-        sys.stdout.write('------\n')
-        sys.stdout.flush()
-
-
-
 def seek_backwards(f):
     start_pos = f.tell()
     current_pos = f.tell()-1
@@ -549,8 +500,8 @@
         drives.append(drive[1:])
     return drives
 
-def wrap(s):
-    return "\n".join(textwrap.wrap(s, initial_indent=' * ', subsequent_indent=' * '))
+def wrap(s, i=''):
+    return "\n".join(textwrap.wrap(s, initial_indent=' * '+i, subsequent_indent=' * '+i))
 
 
 warning = """\
@@ -572,8 +523,14 @@
     f.write(f'`define {guard}\n')
     f.write('\n')
     f.write(f"/**\n")
-    f.write(wrap(f"{define_data['name']}: {define_data['description']}"))
-    f.write('\n')
+    if '\n' in define_data['description']:
+        f.write(f" * {define_data['name']}:\n")
+        for l in define_data['description'].splitlines():
+            f.write(wrap(l.rstrip(), i='  '))
+            f.write('\n')
+    else:
+        f.write(wrap(f"{define_data['name']}: {define_data['description']}"))
+        f.write('\n')
     f.write(f" *\n")
     f.write(wrap(desc))
     f.write('\n')
@@ -597,7 +554,7 @@
 
 def write_module_header(f, define_data):
     f.write('(* blackbox *)\n')
-    f.write(f"module {define_data['library']}__{define_data['name']} (")
+    f.write(f"module {define_data['verilog_name']} (")
 
 
 def write_verilog_parameters(f, define_data):
@@ -641,22 +598,32 @@
 
     for pclass, pname, pdir, ptype in ports:
         pname = pname.ljust(maxlen['pname'])
+        f.write(f"\n    {pname},")
+    seek_backwards(f)
+    if ports:
+        f.write("\n")
+    f.write(");\n")
+
+    for pclass, pname, pdir, ptype in ports:
+        pname = pname.ljust(maxlen['pname'])
 
         if maxlen['pdir']:
             pdir = pdir.ljust(maxlen['pdir'])+' '
         else:
             pdir = ''
 
+        if pclass == 'power':
+            ptype = ''
+
         if maxlen['ptype']:
             ptype = ptype.ljust(maxlen['ptype'])+ ' '
         else:
             ptype = ''
 
-        f.write(f"\n    {pdir}{ptype}{pname},")
+        f.write(f"\n    {pdir}{ptype}{pname};")
     seek_backwards(f)
     if ports:
         f.write("\n")
-    f.write(");\n")
 
 
 def write_verilog_symbol_ports(f, pports):
@@ -713,10 +680,23 @@
     return ports
 
 
+def outfile(cellpath, define_data, ftype='', extra='', exists=False):
+    fname = define_data['name'].lower().replace('$', '_')
+    if ftype:
+        ftype = '.'+ftype
+    outpath = os.path.join(cellpath, f'{define_data["file_prefix"]}{extra}{ftype}.v')
+    if exists is None:
+        pass
+    elif not exists:
+        #assert not os.path.exists(outpath), "Refusing to overwrite existing file:"+outpath
+        print("Creating", outpath)
+    elif exists:
+        assert os.path.exists(outpath), "Missing required:"+outpath
+    return outpath
+
+
 def write_blackbox(cellpath, define_data):
-    outpath = os.path.join(cellpath, f"{define_data['library']}__{define_data['name']}.blackbox.v")
-    assert not os.path.exists(outpath), outpath
-    print("Creating", outpath)
+    outpath = outfile(cellpath, define_data, 'blackbox')
     with open(outpath, "w+") as f:
         write_verilog_header(
             f,
@@ -731,9 +711,11 @@
 
 
 def write_blackbox_pp(cellpath, define_data):
-    outpath = os.path.join(cellpath, f"{define_data['library']}__{define_data['name']}.pp.blackbox.v")
-    assert not os.path.exists(outpath), outpath
-    print("Creating", outpath)
+    if define_data['type'] == 'cell':
+        ofile = 'pp.blackbox'
+    else:
+        ofile = 'blackbox'
+    outpath = outfile(cellpath, define_data, ofile)
     with open(outpath, "w+") as f:
         write_verilog_header(
             f,
@@ -758,6 +740,7 @@
 
     pports = [None,]
     while len(ports) > 0:
+        assert ports[0][0], ports[0]
         if pports[-1] and pports[-1][0].type != ports[0][0].type:
             pports.append(None)
         pports.append(ports.pop(0))
@@ -768,10 +751,211 @@
     return pports
 
 
+def write_primitive(cellpath, define_data):
+    assert define_data['type'] == 'primitive', define_data
+    outpath = outfile(cellpath, define_data)
+    table_datafile = outpath.replace('.v', '.table.tsv')
+    assert os.path.exists(table_datafile), (table_data, define_data)
+
+    table_data = list(csv.reader(open(table_datafile, 'r', newline=''), delimiter='\t'))
+
+    with open(outpath, "w+") as f:
+        write_verilog_header(
+            f,
+            "Verilog primitive definition.",
+            define_data)
+        f.write(f"primitive {define_data['verilog_name']} (")
+        write_verilog_ports(f, define_data['ports'])
+
+        if table_data[0].count(':') == 2:
+            _, pname, _, _ = define_data['ports'][0]
+            assert pname == 'Q', (define_data['ports'], table_data[0])
+            f.write('\n    reg Q;\n')
+
+        maxlen = [max(len(r[i]) for r in table_data if len(r) > i) for i in range(0, len(table_data[0]))]
+        for i in range(0, len(maxlen)):
+            if table_data[0][i] == ':':
+                continue
+            if maxlen[i] == 1:
+                maxlen[i] += 2
+            if maxlen[i] == 2:
+                maxlen[i] += 1
+
+        f.write('\n')
+        f.write('    table\n')
+        prefix_first = '     // '
+        prefix_rest  = '        '
+        for i, r in enumerate(table_data):
+            if i == 0:
+                f.write(prefix_first)
+            else:
+                f.write(prefix_rest)
+
+            if len(r) != len(maxlen):
+                f.write('// ')
+                f.write(repr(r))
+                continue
+
+            for j, c in enumerate(r[:-1]):
+                f.write(c.center(maxlen[j]))
+                f.write(' ')
+
+            if i == 0:
+                assert r[-1] == 'Comments', (i, r)
+                f.write('\n')
+                continue
+            f.write(' ;')
+            if r[-1]:
+                f.write(' // ')
+                f.write(r[-1])
+            f.write('\n')
+        f.write('    endtable\n')
+        f.write('endprimitive\n')
+        write_verilog_footer(f)
+
+
+def write_testbench(cellpath, define_data):
+    ports_by_class = defaultdict(lambda: list())
+    ports_by_dir = defaultdict(lambda: list())
+    for pclass, pname, pdir, ptype in define_data['ports']:
+        pg = PortGroup.classify(pname, define_data['name'])
+        ports_by_class[pg].append((pclass, pname, pdir, ptype))
+        ports_by_dir[pdir].append((pclass, pname, pdir, ptype))
+
+    pprint.pprint(ports_by_class)
+    pprint.pprint(ports_by_dir)
+
+    vfile = outfile(cellpath, define_data, exists=True)
+    outpath = outfile(cellpath, define_data, 'tb')
+
+    vfile = os.path.relpath(vfile, os.path.dirname(outpath))
+
+    with open(outpath, "w+") as f:
+        write_verilog_header(
+            f,
+            "Autogenerated test bench.",
+            define_data)
+
+        f.write('`include "{}"\n'.format(vfile))
+        f.write('\n')
+
+        f.write('module top();\n')
+        f.write('\n')
+
+        port_args = []
+
+
+        input_port_names = []
+        for pclass, pname, pdir, ptype in ports_by_dir['input']:
+            if PortGroup.classify(pname, define_data['name']) == PortGroup.CLOCK:
+                continue
+            input_port_names.append(pname)
+        maxlen = max(len(i) for i in input_port_names)
+        input_port_names = [n.ljust(maxlen) for n in input_port_names]
+
+        f.write('    // Inputs are registered\n')
+        for pclass, pname, pdir, ptype in ports_by_dir['input']:
+            if PortGroup.classify(pname, define_data['name']) == PortGroup.CLOCK:
+                continue
+            assert pdir == 'input'
+            f.write("    reg {};\n".format(pname))
+            port_args.append('.{0}({0})'.format(pname))
+        f.write('\n');
+
+        f.write('    // Outputs are wires\n')
+        for pclass, pname, pdir, ptype in ports_by_dir['output']:
+            assert pdir == 'output'
+            f.write("    wire {};\n".format(pname))
+            port_args.append('.{0}({0})'.format(pname))
+        f.write('\n');
+
+        f.write("""\
+    initial
+    begin
+        // Initial state is x for all inputs.
+""")
+        indent = "        "
+        for n in sorted(input_port_names):
+            f.write(indent+"{0} = 1'bX;\n".format(n))
+
+        f.write("\n")
+
+        DELTA = 20
+        i = 0
+
+        # Set all the inputs to 0, one at a time
+        # x -> 0
+        for n in sorted(input_port_names):
+            i += DELTA
+            f.write(indent+"#{0:<4d} {1} = 1'b0;\n".format(i, n))
+
+        # Set all the inputs to 1, one at a time
+        # 0 -> 1
+        for n in sorted(input_port_names):
+            i += DELTA
+            f.write(indent+"#{0:<4d} {1} = 1'b1;\n".format(i, n))
+
+        # Set all the inputs to zero, one at a time
+        # 1 -> 0
+        for n in sorted(input_port_names):
+            i += DELTA
+            f.write(indent+"#{0:<4d} {1} = 1'b0;\n".format(i, n))
+
+        # Set all the inputs to input, one at a time
+        # 0 -> 1
+        for n in reversed(sorted(input_port_names)):
+            i += DELTA
+            f.write(indent+"#{0:<4d} {1} = 1'b1;\n".format(i, n))
+
+        # Set all the inputs to x, one at a time
+        # 1 -> 0
+        for n in reversed(sorted(input_port_names)):
+            i += DELTA
+            f.write(indent+"#{0:<4d} {1} = 1'bx;\n".format(i, n))
+
+        f.write("""\
+    end
+
+""")
+
+
+        if PortGroup.CLOCK in ports_by_class:
+            assert len(ports_by_class[PortGroup.CLOCK]) == 1, ports
+
+            clk_port = ports_by_class[PortGroup.CLOCK][0]
+            clk_class, clk_name, clk_dir, clk_type = clk_port
+            assert clk_class == 'signal', clk_port
+            assert clk_dir == 'input', clk_port
+            assert clk_type == '', clk_port
+            port_args.append('.{0}({0})'.format(clk_name))
+
+            f.write("""\
+    // Create a clock
+    reg {0};
+    initial
+    begin
+        {0} = 1'b0;
+    end
+
+    always
+    begin
+        #{1} {0} = ~{0};
+    end
+
+""".format(clk_name, DELTA//4))
+
+        f.write("""\
+    {} dut ({args});
+
+""".format(define_data['verilog_name'], args=", ".join(port_args)))
+
+        f.write('endmodule\n')
+        write_verilog_footer(f)
+    pass
+
+
 def write_symbol(cellpath, define_data):
-    outpath = os.path.join(cellpath, f"{define_data['library']}__{define_data['name']}.symbol.v")
-    assert not os.path.exists(outpath), outpath
-    print("Creating", outpath)
+    outpath = outfile(cellpath, define_data, 'symbol')
 
     # Group the ports to make a nice symbol
     pports = group_ports_for_symbol(define_data, ['signal'])
@@ -790,9 +974,11 @@
 
 
 def write_symbol_pp(cellpath, define_data):
-    outpath = os.path.join(cellpath, f"{define_data['library']}__{define_data['name']}.pp.symbol.v")
-    assert not os.path.exists(outpath), outpath
-    print("Creating", outpath)
+    if define_data['type'] == 'cell':
+        ofile = 'pp.symbol'
+    else:
+        ofile = 'symbol'
+    outpath = outfile(cellpath, define_data, ofile)
 
     # Group the ports to make a nice symbol
     pports = group_ports_for_symbol(define_data, ['signal', 'power'])
@@ -812,7 +998,7 @@
 def write_verilog_wrapper(f, extra, supplies, ports, define_data):
     f.write('\n')
     f.write('`celldefine\n')
-    f.write(f"module {define_data['library']}__{define_data['name']}{extra} (")
+    f.write(f"module {define_data['verilog_name']}{extra} (")
     write_verilog_ports(f, pp_ports(define_data))
     write_verilog_parameters(f, define_data)
     if supplies:
@@ -828,7 +1014,7 @@
     if ports:
         ports_str += ",\n        ".join('.{0}({0})'.format(p[1]) for p in ports);
 
-    f.write(f"    {define_data['library']}__{define_data['name']} cell ")
+    f.write(f"    {define_data['verilog_name']} cell ")
     f.write(param_str)
     f.write('(\n        ')
     f.write(ports_str)
@@ -844,9 +1030,8 @@
 
 
 def write_drive_wrapper(drive, cellpath, define_data):
-    outpath = os.path.join(cellpath, f"{define_data['library']}__{define_data['name']}_{drive}.v")
-    basefile = f"{define_data['library']}__{define_data['name']}"
-    assert not os.path.exists(outpath), outpath
+    outpath = os.path.join(cellpath, f'{define_data["file_prefix"]}_{drive}.v')
+    #assert not os.path.exists(outpath), outpath
     print("Creating", outpath)
 
     with open(outpath, "w+") as f:
@@ -854,7 +1039,7 @@
             f,
             f"Verilog wrapper for {define_data['name']} drive {drive}.",
             define_data)
-        f.write(f'`include "{basefile}.v"\n')
+        f.write(f'`include "{define_data["file_prefix"]}.v"\n')
         f.write('\n')
         f.write('`ifdef USE_POWER_PINS\n')
         f.write('/*********************************************************/\n')
@@ -879,15 +1064,21 @@
     assert os.path.exists(define_json), define_json
     define_data = json.load(open(define_json))
 
-    write_blackbox(cellpath, define_data)
+    if define_data['type'] == 'cell':
+        write_blackbox(cellpath, define_data)
     write_blackbox_pp(cellpath, define_data)
 
-    write_symbol(cellpath, define_data)
+    if define_data['type'] == 'cell':
+        write_symbol(cellpath, define_data)
     write_symbol_pp(cellpath, define_data)
 
-    for d in drive_strengths(define_data['name'], cellpath):
-        write_drive_wrapper(d, cellpath, define_data)
+    if define_data['type'] == 'cell':
+        for d in drive_strengths(define_data['name'], cellpath):
+            write_drive_wrapper(d, cellpath, define_data)
 
+    if define_data['type'] == 'primitive':
+        write_primitive(cellpath, define_data)
+        write_testbench(cellpath, define_data)
     return