2. 时序逻辑与有限状态机:寄存器建模、阻塞与非阻塞赋值、同步/异步复位、FSM(Moore/Mealy)设计与编码风格
大家好,我是你们的讲师。今天我们来聊聊数字设计里最核心的一块——时序逻辑与有限状态机。说实话,这部分内容你要是吃透了,RISC-V处理器的主干也就拿下了。我当年刚入行时,在这上面栽过不少跟头,今天就把这些经验掰开揉碎了讲给你听。
2.1 寄存器建模:数字电路的“记忆单元”
寄存器是什么?说白了,就是数字电路里用来“记住”东西的单元。组合逻辑的输出只取决于当前输入,而寄存器的输出还取决于它之前的状态。你想想看,没有寄存器,CPU怎么记住你上一条指令是什么?
在Verilog里,我们通常用 always @(posedge clk) 来描述一个寄存器。我个人习惯把这种写法叫做“时钟沿触发的赋值”。
// 一个简单的D触发器建模
reg [7:0] data_reg;
always @(posedge clk) begin
data_reg <= data_in;
end
这里有个小细节:reg 类型并不一定代表寄存器。在组合逻辑的 always 块里,reg 其实综合出来是线网。只有像上面这样,在时钟沿触发的块里赋值,它才是真正的寄存器。
_reg 或 _ff 后缀。比如 pc_reg、state_ff。这样看代码时,一眼就能分清哪些是时序逻辑,哪些是组合逻辑。
2.2 阻塞赋值与非阻塞赋值:一个让我熬夜的坑
阻塞赋值 = 和非阻塞赋值 <=,是Verilog新手最容易搞混的地方。我曾经在一个项目里,因为用错了赋值方式,导致仿真结果和实际芯片行为不一致,整整排查了两天。
核心原则其实很简单:
- 组合逻辑用阻塞赋值
=:它“阻塞”了后面的语句,必须等当前语句执行完,才能执行下一条。这符合组合逻辑的硬件行为——信号是立即传递的。 - 时序逻辑用非阻塞赋值
<=:它不阻塞,所有赋值在时钟沿到来时“同时”发生。这模拟了寄存器的行为——所有寄存器在同一时钟沿采样输入,然后更新输出。
来看一个对比的例子:
// 错误写法:时序逻辑用了阻塞赋值
always @(posedge clk) begin
a = b; // 先执行
c = a; // 此时a已经更新,c拿到的是新值
end
// 综合出来:a和c变成了两个串联的寄存器,而不是并行更新
// 正确写法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
a <= b; // 同时采样
c <= a; // 同时采样,c拿到的是a的旧值
end
// 综合出来:a和c是两个并行的寄存器,行为符合预期
always 块里混用 = 和 <=。仿真能过,但综合出来的电路逻辑完全错了。记住:同一个always块里,要么全用阻塞,要么全用非阻塞,不要混用!
2.3 同步复位与异步复位:选哪个?
复位,就是让电路回到一个已知的初始状态。在RISC-V处理器里,PC寄存器必须在上电时复位到入口地址,否则CPU一启动就乱跳。
两种复位方式,各有千秋:
| 特性 | 同步复位 | 异步复位 |
|---|---|---|
| 触发条件 | 只在时钟沿检查复位信号 | 复位信号有效时立即复位,与时钟无关 |
| 优点 | 抗毛刺能力强,时序分析简单 | 复位响应快,不需要时钟也能复位 |
| 缺点 | 需要时钟才能复位,复位信号必须满足建立时间 | 对毛刺敏感,可能引起亚稳态 |
| 典型写法 | always @(posedge clk) |
always @(posedge clk or negedge rst_n) |
我个人在项目中更倾向于使用异步复位、同步释放的方式。说白了,就是复位信号异步地把寄存器清零,但释放时同步到时钟域,避免复位释放时的亚稳态问题。
// 异步复位、同步释放的经典写法
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_reg <= 8'h00; // 异步复位
end else begin
data_reg <= data_in; // 同步工作
end
end
2.4 有限状态机:数字系统的“大脑”
有限状态机(FSM)是数字设计的灵魂。你想想看,CPU的指令译码、总线仲裁、通信协议,本质上都是一个状态机在驱动。FSM分为两种:Moore机和Mealy机。
2.4.1 Moore机 vs Mealy机
- Moore机:输出只取决于当前状态。说白了,状态变了,输出才变。
- Mealy机:输出取决于当前状态和输入。输入一变,输出可能立刻变,不等时钟沿。
用一张图来理解:
嗯,这里要注意:Mealy机因为输出直接受输入影响,所以通常比Moore机少一个时钟周期的延迟。但代价是输出可能有毛刺,需要额外处理。
2.4.2 FSM编码风格:三段式写法
我强烈推荐使用三段式状态机。为什么?因为可读性强,容易维护,而且综合工具优化得好。我在项目中用过两段式,后来发现调试时简直想骂人——状态和输出混在一起,改一个地方要动好几处。
三段式FSM的模板:
// 第一段:状态寄存器(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:下一状态逻辑(组合逻辑)
always @(*) begin
case (state)
IDLE: if (start) next_state = READ;
else next_state = IDLE;
READ: if (done) next_state = DONE;
else next_state = READ;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑或时序逻辑)
always @(*) begin
case (state)
IDLE: data_out = 8'h00;
READ: data_out = data_in;
DONE: data_out = 8'hFF;
default: data_out = 8'h00;
endcase
end
2.5 实战经验:一个RISC-V指令译码器的FSM设计
在RISC-V处理器中,指令译码就是一个典型的状态机。比如加载指令(LW)需要多个周期:取指、译码、访存、写回。每个阶段就是一个状态。
我曾经在一个项目中,把译码状态机写成了两段式,结果在调试时发现输出总是晚一个周期。后来改成三段式,问题立刻解决。你想想看,三段式把“状态跳转”和“输出”分开,调试时只需要看 state 和 next_state 的波形,一目了然。
- 寄存器建模:
always @(posedge clk)+ 非阻塞赋值 - 复位策略:异步复位、同步释放,控制寄存器必复位
- FSM设计:三段式写法,状态、跳转、输出分离
- Moore vs Mealy:输出延迟不同,根据需求选择
好了,这一章的内容就到这里。记住,时序逻辑是数字电路的骨架,状态机是它的灵魂。把这些基础打牢,后面设计RISC-V处理器时,你会觉得游刃有余。