1997 字
10 分鐘
BSV 进阶语法:类型类、规则调度与参数化模块

上一篇文章介绍了 rulemethod 和寄存器。这一篇处理 BSV 中更容易让初学者停下来的部分:类型类、规则冲突、接口组合和参数化。

BSV 的语法经常把“数据是什么”和“数据具有什么能力”分开写。理解这一点后,很多 provisos 看起来就不再像一串神秘的条件。

1. 类型参数和类型类#

BSV 的 type 参数表示一个未知的类型:

interface Source#(type t);
method ActionValue#(t) get();
endinterface

但工具还需要知道这个类型能否被存储、复制或打印。provisos 声明类型能力:

module mkRegister#(type t)(Source#(t))
provisos(Bits#(t, width));
Reg#(t) value <- mkRegU;
endmodule

Bits#(t, width) 表示类型 t 可以表示成 width 位。常见类型类包括:

  • Bits:在位向量和类型之间转换;
  • Eq:可以比较相等;
  • FShow:可以格式化打印;
  • Ord:可以排序或比较大小;
  • Literal:可以从字面量构造;
  • Arith:支持算术运算。

一个类型可能同时需要多个约束:

function Bool isZero(t x)
provisos(Bits#(t, width), Eq#(t));
return x == 0;
endfunction

2. typedef、struct 和 tagged union#

结构类型通常带 deriving

typedef struct {
Bit#(32) pc;
Bit#(32) inst;
Bool valid;
} FetchPacket deriving (Bits, FShow);

带标签的 union 适合表示互斥状态:

typedef union tagged {
void Empty;
Bit#(32) Data;
Bit#(32) Error;
} Result deriving (Bits, FShow);

使用时需要匹配标签:

case (result) matches
tagged Empty: return 0;
tagged Data .x: return x;
tagged Error .x: return 32'hdead;
endcase

标签不仅是文档。它会参与编码和模式匹配,避免把一个状态误当作另一个状态。

3. Vector、List 和编译期集合#

固定长度的 Vector 常用于生成并行结构:

Vector#(4, Reg#(Bit#(32))) lanes <- replicateM(mkReg(0));

replicateM 会重复构造模块或寄存器。mapzipWithfold 适合在 elaboration 阶段构造规律结构:

Vector#(4, Bit#(32)) sums = zipWith(add32, xs, ys);

不要把 List 当作软件中的动态链表。要生成可综合硬件,长度和存储结构必须最终能够确定。

4. 受限函数和多态函数#

函数可以是多态的,但必须声明它需要的能力:

function t select(Bool chooseA, t a, t b)
provisos(Bits#(t, width));
return chooseA ? a : b;
endfunction

如果函数只在编译期使用,它可以接受 Integer 参数:

function Integer bytesOf(Integer bits);
return (bits + 7) / 8;
endfunction

Integer 参与生成结构,Bit#(n) 才是硬件数据。两者边界要保持清楚。

5. method 的三种形态#

普通返回值:

method Bit#(32) peek() if (!empty);
return fifo.first;
endmethod

只产生动作:

method Action clear();
valid <= False;
endmethod

同时产生动作并返回值:

method ActionValue#(Bit#(32)) pop() if (!empty);
actionvalue
let value = fifo.first;
fifo.deq;
return value;
endactionvalue
endmethod

ActionValue 的调用使用 <-

let value <- queue.pop;

这提醒读者:调用不只是读取函数结果,还会改变状态。

6. interface 嵌套和 interface 传递#

接口可以包含另一个接口:

interface Cpu;
interface Client#(MemRequest, MemResponse) memory;
method Action start(Bit#(32) pc);
endinterface

模块可以把子模块的接口直接暴露出去:

interface Client#(type req, type resp);
method Action request(req x);
method ActionValue#(resp) response();
endinterface

这样能让顶层模块保持薄,真正的请求和响应规则留在对应子模块中。

7. rule 的隐含条件#

规则的有效条件不只来自括号:

rule transfer;
let x <- source.get;
sink.put(x);
endrule

source.getsink.put 的 guard 也会成为 transfer 的隐含条件。只有两边都可用时,规则才会被调度。

这正是 BSV 适合写流水线的原因之一:规则可以直接组合方法的可用条件,不必手动复制每个 ready 信号的逻辑。

8. 规则冲突和原子性#

考虑两条规则:

rule allocate (freeList.notEmpty);
freeList.deq;
allocated <= allocated + 1;
endrule
rule release (releaseValid);
freeList.enq(releaseTag);
allocated <= allocated - 1;
endrule

两条规则都访问 freeListallocated。即使它们的条件看起来独立,也要分析 FIFO 的 enq/deq 能否同周期发生,以及计数器是否允许同时更新。

BSV 的规则是原子动作,但不是无限并发。调度器会根据冲突、方法约束和规则优先级选择合法的执行组合。

9. ActionValue 的组合#

两个 ActionValue method 可以在一条规则中依次调用:

rule move;
let x <- input.pop;
let y <- transform(x);
output.push(y);
endrule

如果 transform 也有 guard,那个 guard 会加入规则条件。若方法之间存在同一寄存器的读写冲突,编译器会报告调度限制。

10. FIFO 和接口的 ready/valid#

一个显式 FIFO 接口可以写成:

interface Fifo#(type t);
method Bool canEnq();
method Action enq(t x) if (canEnq);
method Bool canDeq();
method ActionValue#(t) deq() if (canDeq);
endinterface

canEnqcanDeq 和 method guard 分开暴露,调用方可以提前观察能力;真正的 method 仍然由 guard 保护。

在流水线里,队列的空满条件会自然传播到规则。要特别留意零容量队列、同周期入队和出队,以及 reset 后第一拍的状态。

11. 参数化模块#

数值参数和类型参数可以同时使用:

module mkPipeline#(Integer stages, type t)(Pipeline#(t))
provisos(Bits#(t, width));
Vector#(stages, Reg#(t)) regs <- replicateM(mkRegU);
endmodule

stages 决定生成多少级寄存器,t 决定每一级保存的数据类型。参数不是运行时寄存器,改变参数会生成另一个硬件结构。

12. Maybe、Option 和有效位#

BSV 常用 tagged union 表示可选值:

typedef union tagged {
void Invalid;
t Valid;
} Maybe#(type t) deriving (Bits, FShow);

使用时需要匹配标签:

case (result) matches
tagged Invalid: return 0;
tagged Valid .value: return value;
endcase

这比单独维护一个 data 和 valid 更不容易把二者混用,但生成的 RTL 仍然会包含数据和有效位。

13. Clock 和 Reset#

默认 module 使用隐含时钟和复位。需要显式域时,可以写:

interface CrossDomain;
interface Clock fastClock;
interface Reset fastReset;
endinterface

跨域传输仍需同步器或异步 FIFO。BSV 的规则调度不能解决物理时钟域之间的亚稳态问题。

复位时要检查所有规则和 method 的 guard。一个寄存器已经复位,不代表包含它的 FIFO 或控制规则已经可用。

14. 自定义类型类#

大型项目可以定义自己的类型类:

typeclass Encodable#(type t);
function Bit#(32) encode(t value);
endtypeclass

实例为具体类型提供实现:

instance Encodable#(Bit#(32));
function Bit#(32) encode(Bit#(32) value);
return value;
endfunction
endinstance

类型类可以让 ALU、指令包和测试工具共享操作,但不要为简单函数过度抽象。读者首先要能看懂数据如何进入硬件。

15. package、import 和 export#

BSV 文件通常放在 package 中:

package Alu;
interface AluIf;
method Bit#(32) execute(Bit#(32) a, Bit#(32) b);
endinterface
endpackage

其他 package 使用 import:

import Alu::*;

大型工程应明确哪些类型和 module 对外导出,避免所有内部 helper 都成为公共接口。

16. schedule 声明#

当模块有多个 method 时,设计者可以声明 method 之间的调度关系。不同 BSV 工具版本和库的具体声明形式可能不同,但要表达的事情通常是:

  • 两个 method 可以同周期执行;
  • 一个 method 必须先于另一个;
  • 两个 method 不能同时执行。

不要只靠编译器警告来理解性能。对于 FIFO、寄存器堆和流水线控制,应该主动分析同周期允许的操作组合。

17. 仿真和综合边界#

$display、文件读写和仿真专用 helper 可以帮助验证:

rule debug;
$display("state=%h", state);
endrule

但这些语句不能成为硬件功能依赖。最终仍要查看生成 Verilog,确认寄存器、组合逻辑、FIFO 和调度器结构符合设计意图。

18. BSV 进阶检查表#

  • 所有类型参数需要哪些 typeclass?
  • provisos 是否完整且没有隐藏过多假设?
  • tagged union 的每个标签是否都被处理?
  • method guard 是否足够表达可用条件?
  • rule 之间的读写冲突是否清楚?
  • ActionValue 的调用是否正确使用 <-
  • FIFO 是否正确处理空、满和同时入出队?
  • 参数化结构的深度和宽度是否在生成阶段固定?
  • 时钟、复位和跨时钟域是否单独验证?
  • 生成的调度逻辑是否满足目标吞吐率?

BSV 的复杂语法最终都服务于一件事:把带条件的并发动作写成可检查、可组合的硬件行为。

BSV 进阶语法:类型类、规则调度与参数化模块
https://nyachen.cn/zh_TW/2026/07/22/riscv-bsv-syntax-advanced/
作者
Chen Miao
發佈於
2026-07-22 22:25:00
許可協議
CC BY-NC-SA 4.0
評論