3. RISC-V指令集架构入门
好,我们进入第三章。这一章的内容,说白了就是数字逻辑的“肌肉记忆”——时序逻辑和状态机。你写RISC-V软核,CPU的流水线、控制单元、总线交互,全得靠这些东西撑起来。我个人习惯把这一章看作“从组合逻辑到数字系统的分水岭”。
本章核心脉络:寄存器 vs 锁存器 → 复位策略 → 状态机模板 → UART实战。一条线串下来,全是硬核干货。
3.1 寄存器与锁存器:一字之差,天壤之别
很多初学者搞不清寄存器和锁存器。我当年也踩过坑——有一次做SPI从机,综合完发现面积比预期大了一倍,查了半天,原来是代码里不小心写了个锁存器。嗯,从那以后我对这俩东西就特别敏感。
| 对比项 | 寄存器 (Flip-Flop) | 锁存器 (Latch) |
|---|---|---|
| 触发方式 | 时钟边沿触发(上升沿/下降沿) | 电平触发(高电平/低电平) |
| 时序特性 | 同步输出,只在时钟沿变化 | 透明模式,电平期间输出随输入变 |
| 综合结果 | D触发器 + 时钟网络 | 锁存器单元(通常不推荐) |
| FPGA资源 | 使用专用FF单元 | 用LUT+反馈路径模拟,不稳定 |
| 设计推荐 | ✅ 时序逻辑首选 | ❌ 除非特殊低功耗场景,否则避免 |
⚠️ 锁存器是怎么“意外”产生的?
最常见的坑:在组合逻辑块(always @(*))里,if-else或case语句没有覆盖所有分支。比如if没有else,综合工具就会推断:当条件不满足时,输出要保持原值——这不就是锁存器吗?
来看一个反面教材:
// ❌ 错误示例:会综合出锁存器
always @(*) begin
if (sel)
q = d; // 没有else分支,sel=0时q保持原值 → 锁存器
end
// ✅ 正确写法:覆盖所有分支
always @(*) begin
if (sel)
q = d;
else
q = 1'b0; // 或者给默认值
end
你想想看,在RISC-V的寄存器堆实现里,如果意外生成了锁存器,整个CPU的时序都会乱套。所以我的原则是:写组合逻辑时,if-else和case一定要给全默认值。
3.2 同步复位 vs 异步复位:选哪个?
复位策略,每个工程师都有自己的偏好。我个人的经验是:没有绝对的好坏,看场景。
同步复位
// 同步复位:只在时钟上升沿检查复位信号
always @(posedge clk) begin
if (rst_n)
q <= d;
else
q <= 1'b0;
end
- 优点:抗毛刺能力强,复位信号可以综合为普通数据路径
- 缺点:需要时钟存在才能复位,复位信号宽度必须大于一个时钟周期
- 适用场景:高速设计、复位信号来自片外且可能抖动
异步复位
// 异步复位:复位信号独立于时钟,立即生效
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
- 优点:复位响应快,不需要时钟也能复位
- 缺点:对毛刺敏感,存在复位释放时的时序问题(recovery/removal检查)
- 适用场景:上电初始化、安全关键电路
💡 我的建议:在RISC-V软核中,我通常采用异步复位、同步释放的策略。既保证了上电时能快速复位,又避免了异步复位释放时的亚稳态问题。代码模板如下:
// 异步复位、同步释放
reg rst_sync1, rst_sync2;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rst_sync1 <= 1'b0;
rst_sync2 <= 1'b0;
end else begin
rst_sync1 <= 1'b1;
rst_sync2 <= rst_sync1;
end
end
// 使用 rst_sync2 作为内部复位
always @(posedge clk or negedge rst_n) begin
if (!rst_sync2)
q <= 1'b0;
else
q <= d;
end
3.3 三段式状态机:写状态机的“标准答案”
状态机是数字系统的“大脑”。在RISC-V中,取指、译码、执行、访存、写回——每个流水级的状态切换,本质上就是一个复杂状态机。
我强烈推荐三段式状态机写法。为什么?因为它把组合逻辑和时序逻辑分得清清楚楚,可读性强,综合结果也稳定。
来看一个完整的UART接收状态机示例:
// UART接收器 - 三段式状态机
module uart_rx_fsm (
input wire clk,
input wire rst_n,
input wire rx, // 串行输入
input wire baud_tick, // 波特率时钟(16倍过采样)
output reg [7:0] rx_data, // 接收数据
output reg rx_done // 接收完成标志
);
// 状态定义
localparam IDLE = 2'b00,
START = 2'b01,
DATA = 2'b10,
STOP = 2'b11;
reg [1:0] state, next_state;
reg [3:0] bit_cnt; // 位计数器
reg [3:0] sample_cnt; // 采样计数器
reg [7:0] shift_reg; // 移位寄存器
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:次态逻辑
always @(*) begin
next_state = state; // 默认保持
case (state)
IDLE: if (!rx) next_state = START;
START: if (baud_tick && sample_cnt == 4'd7) next_state = DATA;
DATA: if (baud_tick && bit_cnt == 4'd8) next_state = STOP;
STOP: if (baud_tick && sample_cnt == 4'd15) next_state = IDLE;
endcase
end
// 第三段:输出逻辑(这里用组合逻辑输出)
always @(*) begin
rx_done = (state == STOP && baud_tick && sample_cnt == 4'd15);
end
// 辅助计数器(单独时序块)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sample_cnt <= 4'd0;
bit_cnt <= 4'd0;
shift_reg <= 8'd0;
rx_data <= 8'd0;
end else begin
case (state)
IDLE: begin
sample_cnt <= 4'd0;
bit_cnt <= 4'd0;
end
START: begin
if (baud_tick) begin
if (sample_cnt == 4'd7) sample_cnt <= 4'd0;
else sample_cnt <= sample_cnt + 1'b1;
end
end
DATA: begin
if (baud_tick && sample_cnt == 4'd15) begin
sample_cnt <= 4'd0;
shift_reg <= {rx, shift_reg[7:1]}; // LSB first
if (bit_cnt == 4'd7) bit_cnt <= 4'd0;
else bit_cnt <= bit_cnt + 1'b1;
end else if (baud_tick) begin
sample_cnt <= sample_cnt + 1'b1;
end
end
STOP: begin
if (baud_tick) begin
if (sample_cnt == 4'd15) begin
sample_cnt <= 4'd0;
rx_data <= shift_reg; // 锁存数据
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
end
endcase
end
end
endmodule
💡 三段式的好处:
- 第一段只管状态跳转,代码极简,不容易出错
- 第二段纯组合逻辑,综合工具容易优化
- 第三段输出可以灵活选择组合输出(响应快)或时序输出(无毛刺)
3.4 UART接收模块:从理论到实战
UART(通用异步收发器)是嵌入式系统最常用的串行通信协议之一。在RISC-V软核中,UART通常是第一个外设——因为调试全靠它打印信息。
UART帧格式
| 字段 | 起始位 | 数据位(LSB first) | 校验位(可选) | 停止位 |
|---|---|---|---|---|
| 位数 | 1 bit | 5~8 bits | 0/1 bit | 1/1.5/2 bits |
| 电平 | 高→低跳变 | 数据位 | 奇/偶校验 | 高电平 |
接收时序要点
- 起始位检测:检测到rx线从高到低跳变,确认是真正的起始位(需要去毛刺)
- 过采样:通常用16倍波特率时钟,在每位的中点采样(第7、8、9个采样点取多数决)
- 数据移位:LSB first,每收到一个bit就移入移位寄存器
- 停止位检查:停止位必须是高电平,否则报帧错误
⚠️ 我曾经踩过的坑:
有一次做UART接收,波特率时钟和主时钟是异步的。我直接用主时钟域采样rx信号,结果偶尔会采到毛刺,导致起始位误判。后来加了两级同步器 + 边沿检测才解决。记住:跨时钟域信号必须先同步!
完整UART接收模块结构
// UART接收顶层模块
module uart_rx (
input wire clk, // 系统时钟
input wire rst_n, // 异步复位
input wire rx, // 串行输入
input wire baud_clk_en, // 波特率时钟使能(16x)
output reg [7:0] data_out, // 并行数据输出
output reg data_valid, // 数据有效标志
output reg frame_err // 帧错误标志
);
// 信号同步
reg rx_sync1, rx_sync2;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_sync1 <= 1'b1;
rx_sync2 <= 1'b1;
end else begin
rx_sync1 <= rx;
rx_sync2 <= rx_sync1;
end
end
// 边沿检测
wire rx_falling = rx_sync2 & !rx_sync1;
// 状态机实例化(使用上面三段式状态机)
// ... 状态机代码同上
endmodule
📌 本章要点回顾:
- 寄存器 vs 锁存器:组合逻辑一定要给全分支,否则综合出锁存器
- 复位策略:异步复位同步释放,兼顾速度和可靠性
- 三段式状态机:状态跳转、次态逻辑、输出逻辑分离,代码清晰易维护
- UART接收:过采样、中点采样、同步器——细节决定成败