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.