前言
在编写 Verilog 的过程中,我们常常会碰到一些重复性的脏活累活:比如调整总线位宽、增加模块端口、扩展 case 分支、插入流水打拍等等。由于 Verilog 本身是硬件描述语言,很难像高级软件语言那样灵活抽象,所以大部分时候我们只能复制粘贴再手动修改参数,既繁琐又容易出错。这时候,如果借助 Python 的 Jinja2 模板引擎,就可以把这些机械性的代码生成工作自动化,让我们从重复劳动中解放出来。
什么是jinjia2
举个例子
假设你有一个很常见的模块,但每个实例的位宽 不一样。以前你可能复制粘贴代码然后手改,容易出错。
模板引擎的思路是:你写一个模板文件,里面把会变的地方用特殊标记标出来(就像填空题的空格),然后用 Python 脚本把实际数值填进去,自动生成最终的.v文件。
比如模板文件里写:
module {{ module_name }} (
input wire clk,
input wire rst_n,
output reg [{{ data_width-1 }}:0] rdata
);然后你告诉 Python:module_name = "reg_slice32",data_width = 32,它就会生成:
module apb_reg_slice (
input wire clk,
input wire rst_n,
output reg [31:0] rdata
);{{ }} 就是空,Python 把值填进去。
jinjia2和其他方法
有人可能会说:那我用 Python 的 f-string 或字符串拼接来生成不就行了?确实可以,但当逻辑变得复杂时,字符串拼接的代码会变得难以阅读和维护。比如你需要根据条件生成不同的代码块,或者循环生成多个实例,字符串拼接会让 Python 脚本里充满大量的 if、for 和换行符 \n,最后生成的 Verilog 结构隐藏在层层字符串中,可读性极差。
Jinja2 模板引擎解决了这个问题:它将 模板(接近最终 Verilog 的文本,带占位符和控制标签)与 数据(从 Excel、JSON 或自定义对象中提取的参数)分离。硬件工程师可以直接编辑模板文件,专注于 RTL 结构,而 Python 脚本只负责准备数据和调用渲染。两者各司其职,互不干扰。
快速开始
安装
Jinja2 是 Python 的一个第三方库,安装非常简单:
pip install jinja2核心语法
Jinja2 的模板就是普通文本文件(比如后缀为 .v.j2),里面可以包含三种特殊标记:
1. {{ 变量 }}:插值表达式,用于输出变量的值。2. {% 控制语句 %}:如if、for等,用于控制模板的逻辑。3. {# 注释 #}:模板注释,渲染时会被忽略。
我们用几个例子来感受一下。
插值:填空
模板文件 example.v.j2:
module {{ module_name }} (
input wire [{{ width-1 }}:0] data_in,
output wire [{{ width-1 }}:0] data_out
);
assign data_out = data_in;
endmodulePython 脚本:
from jinja2 import Template
template = Template(open('example.v.j2').read())
verilog = template.render(module_name='pass_through', width=16)
print(verilog)输出:
module pass_through (
input wire [15:0] data_in,
output wire [15:0] data_out
);
assign data_out = data_in;
endmodule注意 {{ width-1 }} 中的 width 是传入的变量,Jinja2 会先计算表达式 width - 1,再输出结果。如果 width 是字符串 "16",则需要先转换为整数,我们稍后会讲到过滤器。
条件判断:根据参数生成不同代码
模板:
{% if pipe_stages > 0 %}
// 插入流水寄存器
reg [31:0] pipe_reg [0:{{ pipe_stages-1 }}];
always @(posedge clk) begin
pipe_reg[0] <= data_in;
for (int i=1; i<{{ pipe_stages }}; i++) begin
pipe_reg[i] <= pipe_reg[i-1];
end
end
assign data_out = pipe_reg[{{ pipe_stages-1 }}];
{% else %}
assign data_out = data_in;
{% endif %}当 pipe_stages = 3 时,渲染出三级流水;当 pipe_stages = 0 时,直接赋值。
循环:批量生成实例
模板:
{% for i in range(num_instances) %}
sub_module u_sub_{{ i }} (
.clk (clk),
.in (data_in[{{ i }}]),
.out (data_out[{{ i }}])
);
{% endfor %}num_instances = 4 时会生成 4 个实例,名称从 u_sub_0 到 u_sub_3。
过滤器:数据预处理
过滤器用于修改变量的输出形式,语法是 {{ 变量 | 过滤器 }}。常用的有:
• int:将字符串转换为整数(因为从 Excel 或 JSON 读到的数字往往是字符串)。• upper/lower:转换大小写。• default(value):如果变量未定义或为空,则使用默认值。
例如:
localparam AW = {{ addr_width | int }};如果 addr_width 是字符串 "12",int 过滤器会将其转换为整数 12,避免生成 localparam AW = "12"; 这样的非法代码。
使用宏(Macro)复用公共代码
当多个模块拥有相同的端口声明(例如 APB 从机接口)时,可以在模板中定义宏,类似于 Verilog 中的 `define 或函数。
在模板中定义宏:
{% macro apb_slave_ports() %}
input wire pclk,
input wire presetn,
input wire [31:0] paddr,
input wire psel,
input wire penable,
input wire pwrite,
input wire [31:0] pwdata,
output wire [31:0] prdata,
output wire pready
{% endmacro %}然后在任何模块中调用:
module my_slave (
{{ apb_slave_ports() }}
);这样所有 APB 从机的端口定义就统一了,修改宏即可批量更新所有模块。
jinjia2的应用
假设你负责一个模块,这个模块有十几个不同的配置寄存器和状态寄存器,有的可读写(RW),有的只读(RO),位宽各不相同,偏移地址也不同。手写它们的 APB 总线读写逻辑和端口不仅枯燥,而且极易复制粘贴出错。
我们可以用 Jinja2 这样重写这个例子
Python 脚本 (提供结构化数据)
在实际工程中,这些数据通常是从架构师写的 Excel 或 YAML 文件里读出来的,Python 将它们组织成列表和字典的结构:
from jinja2 import Template
# 寄存器配置表:包含了名字、偏移地址、位宽和读写类型
registers = [
{"name": "MAC_CTRL", "offset": "0x00", "width": 32, "type": "RW"},
{"name": "LINK_STAT", "offset": "0x04", "width": 1, "type": "RO"},
{"name": "RX_PKT_CNT", "offset": "0x08", "width": 32, "type": "RO"},
{"name": "INT_EN", "offset": "0x0C", "width": 16, "type": "RW"}
]
# 读取 Jinja2 模板文件并渲染
with open("csr_template.v.j2", "r") as f:
template = Template(f.read())
rendered_rtl = template.render(module_name="mac_csr", regs=registers)
print(rendered_rtl)Jinja2 模板 (csr_template.v.j2)
在这里你可以看到 Jinja2 的强大:{% for %} 循环生成端口和逻辑,{% if %} 区分 RW 和 RO,甚至是过滤器 |lower 自动把大写宏转成小写信号名。
module {{ module_name }} (
input wire clk,
input wire rst_n,
// 简化的 APB 总线接口
input wire [31:0] paddr,
input wire pwrite,
input wire psel,
input wire [31:0] pwdata,
output reg [31:0] prdata,
// 自动生成的硬件交互端口
{% for reg in regs %}
{% if reg.type == 'RW' %}
output reg [{{ reg.width-1 }}:0] {{ reg.name|lower }}_q{% if not loop.last %},{% endif %}
{% elif reg.type == 'RO' %}
input wire [{{ reg.width-1 }}:0] {{ reg.name|lower }}_d{% if not loop.last %},{% endif %}
{% endif %}
{% endfor %}
);
// --- 自动生成的写逻辑 (仅限 RW 寄存器) ---
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
{% for reg in regs if reg.type == 'RW' %}
{{ reg.name|lower }}_q <= {{ reg.width }}'h0;
{% endfor %}
end else if (psel && pwrite) begin
case (paddr[7:0])
{% for reg in regs if reg.type == 'RW' %}
8'h{{ reg.offset[-2:] }}: {{ reg.name|lower }}_q <= pwdata[{{ reg.width-1 }}:0];
{% endfor %}
endcase
end
end
// --- 自动生成的读逻辑 (包含 RW 和 RO) ---
always @(*) begin
prdata = 32'h0; // 默认值
if (psel && !pwrite) begin
case (paddr[7:0])
{% for reg in regs %}
8'h{{ reg.offset[-2:] }}: prdata = {{ "{32'h0, " if reg.width < 32 else "" }}{{ reg.name|lower }}_{{ "q" if reg.type == "RW" else "d" }}{{ "}" if reg.width < 32 else "" }};
{% endfor %}
endcase
end
end
endmodule生成的 Verilog 结果
执行 Python 后,模板引擎一下子就给你吐出了可以直接拿去综合的、完全不出错的代码:
module mac_csr (
input wire clk,
input wire rst_n,
// 简化的 APB 总线接口
input wire [31:0] paddr,
input wire pwrite,
input wire psel,
input wire [31:0] pwdata,
output reg [31:0] prdata,
// 自动生成的硬件交互端口
output reg [31:0] mac_ctrl_q,
input wire [0:0] link_stat_d,
input wire [31:0] rx_pkt_cnt_d,
output reg [15:0] int_en_q
);
// --- 自动生成的写逻辑 (仅限 RW 寄存器) ---
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mac_ctrl_q <= 32'h0;
int_en_q <= 16'h0;
end else if (psel && pwrite) begin
case (paddr[7:0])
8'h00: mac_ctrl_q <= pwdata[31:0];
8'h0C: int_en_q <= pwdata[15:0];
endcase
end
end
// --- 自动生成的读逻辑 (包含 RW 和 RO) ---
always @(*) begin
prdata = 32'h0; // 默认值
if (psel && !pwrite) begin
case (paddr[7:0])
8'h00: prdata = mac_ctrl_q;
8'h04: prdata = {32'h0, link_stat_d};
8'h08: prdata = rx_pkt_cnt_d;
8'h0C: prdata = {32'h0, int_en_q};
endcase
end
end
endmodule写在最后
Jinja2 模板引擎为 Verilog 代码生成提供了一种优雅的解决方案。它将硬件设计逻辑(模板)与数据准备逻辑(Python)解耦,使得代码生成过程清晰、可维护、可扩展。无论是调整总线位宽、增加流水级,还是批量实例化模块,都可以通过修改模板轻松完成,而无需在 Python 脚本中堆砌复杂的字符串拼接。
- -THE END- -
往期精选


FPGA技术江湖广发江湖帖
无广告纯净模式,给技术交流一片净土,从初学小白到行业精英业界大佬等,从军工领域到民用企业等,从通信、图像处理到人工智能等各个方向应有尽有,QQ微信双选,FPGA技术江湖打造最纯净最专业的技术交流学习平台。
FPGA技术江湖微信交流群

加群主微信,备注姓名+学校/公司+专业/岗位进群
FPGA技术江湖QQ交流群

备注姓名+学校/公司+专业/岗位进群
