Automating Port-List Synchronization Between Markdown and Verilog

When iterating on RTL, I keep the interface definition in a Markdown table and the implementation in Verilog. Manually keeping the two in sync is error-prone, so I wrote a pair of lightweight Python utilities that convert the Markdown table to a stub module and vice-versa. A third helper explodes the same table into an Excel sheet that the backend team can import.

Markdown ➜ Verilog stub

The first script reads a GitHub-flavored table and emits a module with correctly sized ports and minimal reg declarations for outputs.

#!/usr/bin/env python3
import sys, re, pathlib

def table_to_verilog(md_file, v_file):
    text = pathlib.Path(md_file).read_text()
    rows = [ln.strip('| \n').split('|') for ln in text.splitlines()
            if ln.strip().startswith('|') and '---' not in ln]

    ports = [(name.strip(), int(width.strip()), dir.strip(), note.strip())
             for name, width, dir, note in rows[1:]]

    decls, regs = [], []
    for n, w, d, c in ports:
        rng = f'[{w-1}:0] ' if w > 1 else ''
        decls.append(f'    {d} {rng}{n}, // {c}')
        if d == 'output':
            regs.append(f'reg {rng}{n};')

    # remove trailing comma
    decls[-1] = decls[-1].replace(',', '')

    v = f'''module top (
{chr(10).join(decls)}
);
{chr(10).join(regs)}

endmodule
'''
    pathlib.Path(v_file).write_text(v)

if __name__ == '__main__':
    table_to_verilog(sys.argv[1], sys.argv[2])

Example run:

$ python md2v.py ports.md top.v

Input table:

Port Width Direction Description
clk 1 input clock
reset_n 1 input async active-low reset
data_i 32 input incoming data
addr_o 8 output byte address

Generated stub:

module top (
    input clk, // clock
    input reset_n, // async active-low reset
    input [31:0] data_i, // incoming data
    output [7:0] addr_o // byte address
);

reg [7:0] addr_o;

endmodule

Verilog ➜ Markdown table

The reverse tool parses the module header and reconstructs the Markdown table, extracting bit-widths from range notation and comments after //.

#!/usr/bin/env python3
import re, sys, pathlib

def v_to_table(v_file, md_file):
    txt = pathlib.Path(v_file).read_text()
    hdr = re.search(r'module\s+\w+\s*\((.*?)\);', txt, re.S).group(1)
    hdr = re.sub(r'//.*?$', '', hdr, flags=re.M)  # drop comments for parsing
    lines = [ln.strip() for ln in hdr.split(',') if ln.strip()]

    table = ['| Port | Width | Direction | Description |',
             '| :--- | :---: | :------: | :--- |']
    for ln in lines:
        m = re.match(r'(input|output)\s+(?:\[(\d+):(\d+)\]\s+)?(\w+)', ln)
        dir_, msb, lsb, name = m.groups()
        width = int(msb) - int(lsb) + 1 if msb else 1
        comment = re.search(r'//\s*(.*)', ln)
        desc = comment.group(1).strip() if comment else ''
        table.append(f'| {name} | {width} | {dir_} | {desc} |')

    pathlib.Path(md_file).write_text('\n'.join(table) + '\n')

if __name__ == '__main__':
    v_to_table(sys.argv[1], sys.argv[2])

Exploding the table for Excel

Physical design wants one row per bit. The script below flattens the Markdown table into ports.xlsx with entries like addr_o[7], addr_o[6], ….

#!/usr/bin/env python3
import pandas as pd, sys, re, pathlib

def explode_to_excel(md_file):
    df = pd.read_csv(md_file, sep='|', skiprows=[1], engine='python').dropna(axis=1, how='all')
    df.columns = [c.strip() for c in df.columns]

    rows = []
    for _, r in df.iterrows():
        w = int(r['Width'])
        name = r['Port']
        rows.extend([f'{name}[{i}]' if w > 1 else name for i in range(w)])
    pd.Series(rows).to_excel('ports.xlsx', index=False, header=False)

if __name__ == '__main__':
    explode_to_excel(sys.argv[1])

Running python explode.py ports.md produces an Excel file ready for pad-frame or pin-assignment import.

Tags: Verilog python Markdown EDA RTL

Posted on Mon, 15 Jun 2026 17:28:31 +0000 by FutonGuy