3、RTLA基础语法:模块定义、端口声明、信号类型、赋值语句、过程块(always/initial)、任务与函数
好,咱们今天聊聊RTLA的基础语法。说实话,这部分内容看起来琐碎,但却是你写任何RTLA代码的根基。我见过不少新手,一上来就急着写复杂逻辑,结果端口声明错了,信号类型搞混了,debug起来那叫一个痛苦。咱们一步步来,把地基打牢。
3.1 模块定义与端口声明
RTLA里的模块,说白了就是一个硬件单元的“黑盒子”。你定义它的输入输出,然后描述内部行为。语法上跟Verilog很像,但更简洁。
module counter #(
parameter WIDTH = 8
) (
input logic clk,
input logic rst_n,
input logic en,
output logic [WIDTH-1:0] count
);
// 内部逻辑
endmodule
我个人习惯把参数声明放在模块名后面,用#()括起来。这样一眼就能看到模块的可配置性。端口声明我推荐一行一个,别挤在一起。你想想看,以后要加个信号,或者改个位宽,一行一个多清爽。
input、output、inout。类型用 logic 基本覆盖所有场景。别再用 wire 和 reg 了,那是老古董。
3.2 信号类型:logic 与 wire
RTLA里,logic 是万能类型。它可以被过程块(always)赋值,也可以被连续赋值(assign)驱动。而 wire 只能用于连续赋值。
为什么会这样?因为 logic 内部隐含了存储和驱动的双重特性。我在项目中遇到过一个问题:一个信号被多个always块赋值,结果仿真时出现了多驱动冲突。后来查了半天,发现是有人用了 wire 类型,但又在两个always里写值。嗯,这里要注意:logic 只能被一个过程块赋值,wire 只能被连续赋值驱动。
| 类型 | 可被过程块赋值 | 可被连续赋值 | 多驱动 |
|---|---|---|---|
| logic | 是 | 是 | 不允许 |
| wire | 否 | 是 | 允许(需解析) |
logic 和 wire 混用,结果综合时报了多驱动错误。后来我统一用 logic,只在需要多驱动(比如三态总线)时才用 wire。这个习惯一直保持到现在。
3.3 赋值语句:连续赋值与过程赋值
赋值语句分两种:连续赋值和过程赋值。连续赋值用 assign,过程赋值在 always 块里用 = 或 <=。
连续赋值,说白了就是组合逻辑。信号一变,输出立刻变。
assign sum = a + b;
过程赋值分阻塞赋值(=)和非阻塞赋值(<=)。我刚开始学的时候也搞混过。记住一句话:组合逻辑用阻塞,时序逻辑用非阻塞。
// 组合逻辑:阻塞赋值
always_comb begin
next_state = state;
case (state)
IDLE: if (start) next_state = RUN;
RUN: if (done) next_state = IDLE;
endcase
end
// 时序逻辑:非阻塞赋值
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
你想想看,如果在时序逻辑里用了阻塞赋值,仿真时会出现意想不到的竞争。我有个同事就因为这个,debug了整整两天。后来发现是赋值方式用错了。
3.4 过程块:always 与 initial
always 块是硬件描述的核心。RTLA 提供了三种变体:
always_comb:组合逻辑,敏感列表自动推导always_ff:时序逻辑,显式指定时钟和复位always_latch:锁存器逻辑,慎用
我个人强烈推荐用 always_comb 和 always_ff,而不是传统的 always @(*) 或 always @(posedge clk)。为什么?因为工具能帮你检查。比如你在 always_comb 里漏写了某个信号,工具会报 warning。这在大型项目中能省不少事。
initial 块主要用于仿真,不可综合。我一般用它来初始化变量或生成测试激励。
initial begin
clk = 0;
rst_n = 0;
#100 rst_n = 1;
end
initial 块在硬件综合时会被忽略。如果你想让寄存器有初始值,用 initial 没用,得用复位信号。
3.5 任务与函数
任务和函数用来封装重复逻辑。函数返回一个值,任务不返回(但可以通过输出参数传递)。
函数:
function automatic logic [7:0] add_one(input logic [7:0] val);
add_one = val + 1;
endfunction
任务:
task automatic reset_module(input logic rst_n);
@(posedge rst_n);
#10;
$display("Module reset done at %t", $time);
endtask
我建议在函数和任务前都加上 automatic 关键字。这样每次调用都会分配独立的存储空间,避免递归或多次调用时的冲突。我曾经在写一个深度优先的状态机时,忘了加 automatic,结果递归调用时变量被覆盖了。嗯,那是个教训。
| 特性 | 函数 | 任务 |
|---|---|---|
| 返回值 | 有 | 无(通过输出参数) |
| 时间控制 | 不允许 | 允许(@, #, wait) |
| 可综合 | 是(纯组合逻辑) | 部分可综合 |
| 调用方式 | 表达式 | 语句 |
3.6 知识体系总览
下面这张图,是我自己梳理的RTLA基础语法结构。你可以把它当作一个快速索引。
好了,这一章的内容就这些。语法这东西,光看没用,得多写。我建议你打开编辑器,照着上面的例子敲一遍,然后改改参数,看看仿真结果。遇到问题别怕,debug 本身就是最好的学习方式。