**Verilog ****always**语句详解:从组合逻辑到时序逻辑
always语句是Verilog/SystemVerilog中最核心、最强大的过程块,用于描述硬件的重复行为。它定义了"在什么条件下"以及"如何执行"的硬件行为模型。
一、基本语法与结构
verilog
// 基本形式
always @(敏感事件列表) begin
// 过程语句
// 可以包含:赋值语句、条件语句、循环语句等
end
关键特性:
- 无限循环执行 :
always块一旦被激活,就会不断重复执行 - 敏感事件驱动:执行由敏感事件列表控制
- 内部顺序执行:块内语句按书写顺序执行(但注意赋值的语义)
二、敏感事件列表的三种主要形式
1. 电平敏感(组合逻辑)
verilog
// 形式1:使用 *(推荐,自动推断所有输入信号)
always @(*) begin
y = a & b | c;
end
// 形式2:显式列出所有输入信号
always @(a, b, c) begin
y = a & b | c;
end
// 形式3:SystemVerilog简化写法
always_comb begin
y = a & b | c;
end
特点:
- 当任何输入信号变化时立即执行
- 必须使用阻塞赋值 (
=) - 描述组合逻辑电路
2. 边沿敏感(时序逻辑)
verilog
// 时钟上升沿触发
always @(posedge clk) begin
q <= d;
end
// 时钟下降沿触发
always @(negedge clk) begin
q <= d;
end
// 多信号边沿触发(不推荐,易出错)
always @(posedge clk, posedge reset) begin
// ...
end
特点:
- 只在时钟边沿触发
- 必须使用非阻塞赋值 (
<=) - 描述触发器、寄存器等时序元件
3. 混合敏感(异步控制)
verilog
// 异步复位,时钟上升沿触发
always @(posedge clk or posedge reset) begin
if (reset) begin
q <= 1'b0; // 异步复位
end else begin
q <= d; // 同步工作
end
end
// 异步复位,异步置位
always @(posedge clk or posedge reset or posedge set) begin
if (reset) begin
q <= 1'b0;
end else if (set) begin
q <= 1'b1;
end else begin
q <= d;
end
end
三、always块的四种主要应用模式
1. 纯组合逻辑
verilog
// 多路选择器
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = 1'b0;
endcase
end
// 译码器
always @(*) begin
// 避免锁存器:必须为所有分支赋值
decode = 4'b0000;
if (en) begin
case (addr)
2'b00: decode = 4'b0001;
2'b01: decode = 4'b0010;
2'b10: decode = 4'b0100;
2'b11: decode = 4'b1000;
endcase
end
end
重要 :组合逻辑always块中,必须为所有可能的输入组合 指定输出值,否则会推断出锁存器。
2. 同步时序逻辑(最常见)
verilog
// 带同步复位的D触发器
always @(posedge clk) begin
if (reset) begin
q <= 1'b0;
end else begin
q <= d;
end
end
// 带使能的计数器
always @(posedge clk) begin
if (reset) begin
count <= 0;
end else if (enable) begin
count <= count + 1;
end
// 注意:没有else分支,count保持原值
end
// 状态机
always @(posedge clk) begin
if (reset) begin
state <= IDLE;
end else begin
case (state)
IDLE: if (start) state <= WORK;
WORK: if (done) state <= IDLE;
default: state <= IDLE;
endcase
end
end
3. 异步时序逻辑
verilog
// 异步复位D触发器
always @(posedge clk or posedge async_reset) begin
if (async_reset) begin
q <= 1'b0; // 异步动作:不受时钟控制
end else begin
q <= d; // 同步动作:时钟上升沿触发
end
end
// 异步复位,同步释放(推荐,避免亚稳态)
reg reset_sync;
always @(posedge clk or posedge async_reset) begin
if (async_reset) begin
reset_sync <= 1'b1;
q <= 1'b0;
end else begin
reset_sync <= 1'b0;
q <= d;
end
end
4. 电平敏感锁存器(通常应避免)
verilog
// 不完整的if语句会产生锁存器
always @(*) begin
if (enable) begin
q = d; // 当enable=0时,q没有赋值,保持原值 → 锁存器
end
end
// 明确的锁存器(不推荐在FPGA中使用)
always @(*) begin
if (gate) begin
q = d; // 电平敏感存储
end
end
四、高级用法与注意事项
1. 多个always块操作同一变量
verilog
// 错误:多个always块对同一变量赋值 → 多驱动错误
reg [7:0] data;
always @(posedge clk) begin
if (load_a) data <= input_a;
end
always @(posedge clk) begin
if (load_b) data <= input_b; // 编译错误
end
// 正确:合并到一个always块
reg [7:0] data;
always @(posedge clk) begin
if (load_a) begin
data <= input_a;
end else if (load_b) begin
data <= input_b;
end
end
2. always块中的循环语句
verilog
// 用于组合逻辑的循环(会被展开)
parameter WIDTH = 8;
reg [WIDTH-1:0] parity;
integer i;
always @(*) begin
parity = 0;
for (i = 0; i < WIDTH; i = i + 1) begin
parity = parity ^ data[i]; // 计算奇偶校验
end
end
// 注意:循环边界必须能在编译时确定
3. SystemVerilog增强
verilog
// always_comb: 自动推断敏感列表,检查组合逻辑
always_comb begin
y = a & b;
// 工具会自动检查是否所有输入都在敏感列表中
// 检查是否推断出锁存器
end
// always_ff: 明确指定时序逻辑
always_ff @(posedge clk or posedge reset) begin
if (reset) q <= '0;
else q <= d;
end
// always_latch: 明确指定锁存器
always_latch begin
if (en) q = d;
end
五、常见错误与调试技巧
错误1:组合逻辑中产生锁存器
verilog
// 错误:缺少else分支
always @(*) begin
if (sel)
out = a;
// 当sel=0时,out没有赋值 → 锁存器!
end
// 修复:添加默认值
always @(*) begin
out = 0; // 默认赋值
if (sel)
out = a;
end
错误2:时序逻辑中使用阻塞赋值
verilog
// 错误:可能导致竞争条件
always @(posedge clk) begin
a = b; // 阻塞赋值
c = a; // 使用新的a值
// 仿真与综合可能不一致
end
// 正确:使用非阻塞赋值
always @(posedge clk) begin
a <= b; // 计划赋值
c <= a; // 使用时钟沿前的a值
end
错误3:不完整的敏感列表
verilog
// 错误:缺少信号c
always @(a, b) begin
y = a & b | c; // c变化时,y不会更新
end
// 正确:使用@(*)
always @(*) begin
y = a & b | c;
end
六、性能与优化建议
- 尽量使用时序逻辑:组合逻辑容易产生毛刺,时序逻辑更稳定
- 一个变量一个always块:保持代码清晰
- 优先使用同步设计:异步设计难验证、难综合
- 明确复位策略:同步复位还是异步复位,保持一致
- 避免在always块中使用#延迟:不可综合,仅用于仿真
七、实例:用always块实现一个FIFO控制器
verilog
module fifo_ctrl #(
parameter ADDR_WIDTH = 4
)(
input wire clk, reset,
input wire wr_en, rd_en,
output reg full, empty,
output reg [ADDR_WIDTH-1:0] wptr, rptr
);
localparam DEPTH = 1 << ADDR_WIDTH;
reg [ADDR_WIDTH:0] wptr_reg, rptr_reg; // 额外一位用于判断空满
// 写指针逻辑
always @(posedge clk or posedge reset) begin
if (reset) begin
wptr_reg <= 0;
end else if (wr_en && !full) begin
wptr_reg <= wptr_reg + 1;
end
end
// 读指针逻辑
always @(posedge clk or posedge reset) begin
if (reset) begin
rptr_reg <= 0;
end else if (rd_en && !empty) begin
rptr_reg <= rptr_reg + 1;
end
end
// 空满判断(组合逻辑)
always @(*) begin
full = (wptr_reg[ADDR_WIDTH-1:0] == rptr_reg[ADDR_WIDTH-1:0]) &&
(wptr_reg[ADDR_WIDTH] != rptr_reg[ADDR_WIDTH]);
empty = (wptr_reg == rptr_reg);
wptr = wptr_reg[ADDR_WIDTH-1:0];
rptr = rptr_reg[ADDR_WIDTH-1:0];
end
endmodule
总结表格
| 特性 | 组合逻辑always |
时序逻辑always |
|---|---|---|
| 触发条件 | 电平变化(@(*)) |
时钟边沿(@(posedge clk)) |
| 赋值类型 | 阻塞赋值(=) |
非阻塞赋值(<=) |
| 实现电路 | 组合门电路 | 触发器/寄存器 |
| 输出特性 | 立即变化 | 时钟边沿后变化 |
| 敏感列表 | 所有输入信号 | 时钟、异步控制信号 |
| 常见错误 | 产生锁存器 | 使用阻塞赋值导致竞争 |
黄金法则:
- 描述组合逻辑 →
always @(*)+ 阻塞赋值(=) - 描述时序逻辑 →
always @(posedge clk)+ 非阻塞赋值(<=) - 避免锁存器 → 为组合逻辑的所有分支赋值
- 避免多驱动 → 一个变量只在一个always块中赋值
掌握always语句是Verilog设计的核心,理解其工作原理和适用场景,才能写出可靠、可综合的硬件描述代码。