VIM插件 -- 自動生成verilog module的testbench
@(VIM)
1. 動機
軟件語言都有各自好用的IDE,各種自動補全,高亮,語法檢查。而苦逼的ICer大多還操着遠古時期的VIM寫着verilog。也是,硬件語言本身就小眾,即使是xilinx, altera等大廠的vivado, quartus等大牌軟件,自帶的代碼編輯器也不是很友好。好在號稱編輯器之神的VIM提供了豐富的定制化功能,vimscript可以實現諸多媲美成熟IDE的功能。此外,vimscript還配備了很多語言的編程接口,perl,python的腳本都可以在vimscript里運行,這也使得開發插件的門檻降低了不少。
在某次寫verilog的testbench時,覺得從寫完一個模塊,到驗證其功能的路上花了大量時間在一些重復的coding上,testbench中大部分內容都是重復模塊的端口定義,完全可以讓腳本去干,於是花了半天嘗試着用python寫了一套自動生成verilog modual 的testbench的腳本,嵌入到vim中,通過簡單的命令就可以實現繁瑣的testbench的模板生成。順便,,,也鞏固一下python的正則表達式寫法。話不多說,直接上代碼。
2. 代碼
"-------------------------------------------------------------------
" auto testbench (python)
"-------------------------------------------------------------------
:nnoremap <leader>tb :call Autotb()<cr>
function! Autotb()
python << EOF
import vim,re
# 清除所有注釋內容
def clear_commonts(lines):
block_comment_index = [] # record block comment index
# remove // comment
for i in range(len(lines)):
lines[i] = re.sub('//.*$','',lines[i])
all_pairs = re.findall('/\*',lines[i])
if(all_pairs != []):
next_index = 0
new_line = lines[i]
for j in range(len(all_pairs)):
# print(new_line)
find_index = re.search('/\*',new_line).span()
next_index = find_index[1]
new_line = new_line[next_index:]
block_comment_index.append(i)
if(re.search('\*/',new_line)!=None): # find paired */
block_comment_index.append(i)
else:
if(re.search('\*/',lines[i])!=None): # find paired */
block_comment_index.append(i)
# print(block_comment_index)
if(len(block_comment_index)%2!=0):
raise('ERROR: /* and */ not paired')
else:
for i in range(int(len(block_comment_index)/2)):
left_index = block_comment_index[i*2]
right_index = block_comment_index[i*2+1]
if(left_index==right_index): # inline /* */
lines[left_index] = re.sub('/\*.*\*/','',lines[left_index])
else:
for j in range(left_index+1,right_index): # delet comment in block /* */
lines[j] = ''
lines[left_index] = re.sub('/\*.*$','',lines[left_index])
lines[right_index] = re.sub('^.*\*/','',lines[right_index])
return lines
# 找到模塊名
def find_inst_name(line):
pattern = re.compile('(module)\s+(\w+)')
if(pattern.search(line)!=None):
return pattern.search(line).group(2)
else:
return ''
# 文本行轉換為字符串
def lines2string(lines):
string = ''
for l in lines:
string += l
return string
# 獲取輸入輸出端口名以及位寬,符號
def find_port_and_width(s):
signed_flag = 0
l_tmp = re.sub('(input)|(output)|(inout)','',s,1) # count=1 will not sub these word in signal name
l_tmp = re.sub('(wire)|(reg)','',l_tmp,1)
if(re.search('\sunsigned[\s\[]',l_tmp)!=None):
signed_flag = 'unsigned'
elif(re.search('\ssigned[\s\[]',l_tmp)!=None):
signed_flag = 'signed'
else:
signed_flag = ' '
l_tmp = re.sub('(signed)|(unsigned)','',l_tmp,1)
l_tmp = re.sub('\s','',l_tmp)
if(re.search('\[.+\]',l_tmp)!=None):
width = re.search('\[.+\]',l_tmp).group(0)
l_tmp = re.sub('\[.+\]','',l_tmp)
else:
width = '1'
#print(l_tmp)
if(re.search('\w+(?=[;,\s\)])',l_tmp)!=None):
port_name = re.search('\w+(?=[;,\s\)])',l_tmp).group(0)
else:
raise('Port name lost!')
print(width,port_name,signed_flag)
return width,port_name,signed_flag
# 尋找模塊例化中定義段parameter變量
def find_parameter(s):
l_tmp = re.sub('\s','',s)
name_search = re.search('\w+(?=\=)',l_tmp)
val_search = re.search('(?<=\=)[\w\+\-\*/\%\$\'`]+',l_tmp)
if(name_search!=None):
name = name_search.group(0)
else:
name = ''
if(val_search!=None):
val = val_search.group(0)
else:
val = ''
return name,val
# 尋找模塊的輸入輸出
def find_input_output(lines):
inst_dict = {'name':'','input':[],'output':[],'inout':[],'para':[]}
string = lines2string(lines)
inst_dict['name'] = find_inst_name(string)
if inst_dict['name'] == '':
raise('Inst name lost!')
output_pattern = re.compile('\Woutput[\[\s][\w\s\[\]:\+\-\*/\(\)%\{\}\'`]*[,;]') # include 0-9 a-z A-Z [:] \n
input_pattern = re.compile('\Winput[\[\s][\w\s\[\]:\+\-\*/\(\)%\{\}\'`]*[,;]') # include 0-9 a-z A-Z [:] \n
inout_pattern = re.compile('\Winout[\[\s][\w\s\[\]:\+\-\*/\(\)%\{\}\'`]*[,;]') # include 0-9 a-z A-Z [:] \n
input_list = input_pattern.findall(string)
output_list = output_pattern.findall(string)
inout_list = inout_pattern.findall(string)
para_search_end_index = input_pattern.search(string).span()[0]
# print (string[:para_search_end_index])
para_pattern = re.compile('\Wparameter\s+[\s\w\[\]:\+\-\*/\(\)%\{\}\=,\'`]*')
para_search = para_pattern.search(string[:para_search_end_index])
if(para_search!=None):
para_string = para_search.group(0)
#print(para_string)
para_string = ' '+para_string
para_string = re.sub('\sparameter\s','',para_string)
para_list = para_string.split(',')
# print(para_list)
for l in para_list:
inst_dict['para'].append(find_parameter(l))
else:
inst_dict['para'] = []
for l in input_list:
inst_dict['input'].append(find_port_and_width(l))
for l in output_list:
inst_dict['output'].append(find_port_and_width(l))
for l in inout_list:
inst_dict['inout'].append(find_port_and_width(l))
return inst_dict
def max_len(ll,mode=0):
max_len = 1
for l in ll:
if(len(l[mode])>max_len):
max_len = len(l[mode])
return max_len
def build_input_port_tb(in_list,mode='input'):
max_in_len_width = max_len(in_list,0)
max_in_len_signed = max_len(in_list,2)
string = ''
net_type = {'input':'reg','output':'wire','inout':'wire'}
for in_port in in_list:
in_name = in_port[1]
width = in_port[0] if in_port[0]!='1' else ''
signed_type = in_port[2]
added_len_signed = max_in_len_signed - len(signed_type)
added_len_width = max_in_len_width - len(width)
s = '%s %s '%(net_type[mode],signed_type) + added_len_signed*' '
s += '%s'%(width) + added_len_width*' ' + ' %s;\n'%(in_name)
#print(max_in_len_width)
#print(max_in_len_signed)
string += s
return string
def build_para_tb(in_list):
max_in_len = max_len(in_list,0)
string = ''
for in_port in in_list:
in_name = in_port[0]
number = in_port[1]
added_len = max_in_len - len(in_name)
s = 'parameter %s '%(in_name) + added_len*' '+'= %s;\n'%(number)
string += s
return string
# 尋找模塊中的clk和rst
def find_clk_and_rst(in_list):
clk = ''
rst = ''
for l in in_list:
name = l[1]
if(re.search('clk',name,re.I)!=None):
clk = name
break
for l in in_list:
name = l[1]
if(re.search('rst',name,re.I)!=None):
rst = name
break
return clk,rst
def input_initial_tb(in_list,rst):
string = 'initial begin\n'
for l in in_list:
if(l[1]!=rst):
s = ' %s = 0;\n'%l[1]
string += s
rst_kind = re.search('(?<=rst)_?n',rst,re.I)==None #1:rst, 0:rst_n
string += ' %s = %d;\n #4 %s = %d; #2 %s = %d;\n'%(rst,1-rst_kind,rst,rst_kind,rst,1-rst_kind)
string += 'end\n'
return string
# 生成模塊例化代碼
def build_inst(inst_dict):
inst_name = inst_dict['name']
name_len = len(inst_name)
string = inst_name
if(inst_dict['para']==[]):
para_string = ' '
else:
para_string = ' #('
para_max_in_len = max_len(inst_dict['para'],0)
for i,para_port in enumerate(inst_dict['para']):
para_name = para_port[0]
number = para_port[1]
added_len = para_max_in_len - len(para_name)
s = (i!=0)*(name_len+3)*' '+' .%s '%(para_name) + added_len*' '+'( '+ para_name +added_len*' '+' )'+(i!=len(inst_dict['para'])-1)*','+(i==len(inst_dict['para'])-1)*')'+'\n'
para_string += s
string += para_string
string += 'U_'+ inst_name.upper()+'_0\n'
# port inst
port_list = inst_dict['input']+inst_dict['output']+inst_dict['inout']
port_max_in_len = max_len(port_list,1)
port_string = '('
for i,port in enumerate(port_list):
port_name = port[1]
added_len = port_max_in_len - len(port_name)
# print(port_list)
s = (i!=0)*' '+' .%s '%(port_name) + added_len*' '+'( '+ port_name + added_len*' '+' )'+(i!=len(port_list)-1)*','+(i==len(port_list)-1)*');'+'\n'
port_string += s
string += port_string
return string
# 增加dumpvar相關命令,vcs+verdi調試需要
def add_dumpvars():
string = '\ninitial begin\n $fsdbDumpvars();\n $fsdbDumpMDA();\n $dumpvars();\n #1000 $finish;\nend'
return string
# 增加頭部信息
def add_head(name,author='Yicheng Lu'):
import time
now_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
year = now_time[0:4]
string = "// -----------------------------------------------------------------\n// Copyright (c) %s .\n// ALL RIGHTS RESERVED\n// -----------------------------------------------------------------\n// Filename : %s.v\n// Author : %s\n// Created On : %s\n// Last Modified :\n// -----------------------------------------------------------------\n// Description:\n//\n// -----------------------------------------------------------------\n"%(year,name,author,now_time)
return string
# 生成tb
def build_tb(inst_dict):
string = add_head(name=inst_dict['name']+'_tb')
string += '\n`timescale 1ns/1ps\n\n'
string += 'module %s();\n\n'%(inst_dict['name']+'_tb')
string += build_para_tb(inst_dict['para'])
string += '\n'
string += build_input_port_tb(inst_dict['input'],'input')
string += '\n'
string += build_input_port_tb(inst_dict['output'],'output')
string += '\n'
string += build_input_port_tb(inst_dict['inout'],'inout')
clk,rst = find_clk_and_rst(inst_dict['input'])
string += '\nalways #1 %s = ~%s;\n'%(clk,clk)
string += input_initial_tb(inst_dict['input'],rst)
string += '\n'
string += build_inst(inst_dict)
string += '\n'
string += add_dumpvars()
string += '\n'
string += '\nendmodule\n'
return string
lines = []
for l in vim.current.buffer[:]:
lines.append(l+'\n') # string in buffer do not contain '\n'
clear_lines = clear_commonts(lines)
inst_dict = find_input_output(clear_lines)
tb_string = build_tb(inst_dict)
# 調用vim的python接口,生成一個新的窗口,在新窗口構建tb
vim.command("badd %s"%(inst_dict['name']+'_tb.v'))
buffer_num = len(vim.buffers)
print(buffer_num)
vim.command("tabnew")
vim.command("b"+str(buffer_num))
vim.current.buffer[:] = tb_string.split('\n')
#vim.command('NERDTree')
EOF
endfunction
3. 使用方法
以上腳本需要復制粘貼到vim的配置文件.vimrc中即可生效。需要系統裝有python,如果系統裝的python是python3,需要修改
:nnoremap <leader>tb :call Autotb()<cr>
function! Autotb()
python << EOF
為
:nnoremap <leader>tb :call Autotb()<cr>
function! Autotb()
python3 << EOF
使用時,用vim打開某個.v,按\(\color{red}{\tb}\)即可生成當前verilog的testbench模板。
4. 效果
需要生成testbench的verilog文件:

按下\(\color{red}{\tb}\)后:


后面只需要添加特定的激勵即可,初始化和例化都已經自動生成了。
5. 說明
由於只是自己一時興起而寫,雖然用到現在沒出過錯,但不排除某些特殊情況會出錯。此外,對verilog 1995標准的寫法支持仍不夠完善,實測module中帶有參數的1995標准寫法在run該腳本時會有問題,其他情況暫時沒發現問題。對於2001標准的寫法應該是沒有太大問題(如果有發現問題歡迎留言,以便改進)。如果有自己的testbench的風格,可以直接修改代碼中tb生成部分的代碼。