3. 简单约束:关系约束、范围约束与集合约束
好,咱们今天来聊聊约束里最基础、也最常用的三类:关系约束、范围约束和集合约束。说实话,这三兄弟你只要玩熟了,80%的随机场景都能搞定。
我个人习惯把约束分成两类:硬约束和软约束。关系约束和范围约束属于硬约束——你必须满足,不满足仿真就报错。集合约束(distribute)则是软约束,它只是调整概率分布,不是绝对禁止。
3.1 关系约束:>、<、==
关系约束,说白了就是给变量之间拉关系。比如地址必须大于基地址,数据长度必须小于包长度,状态机状态必须等于某个值。
语法很简单,直接写在约束块里就行:
class packet;
rand bit [7:0] addr;
rand bit [7:0] len;
constraint c_addr_range {
addr > 16;
addr < 240;
}
constraint c_len_relation {
len == addr + 8;
len < 200;
}
endclass
这里要注意一个坑:关系约束是双向的。你写 len == addr + 8,SystemVerilog 会同时约束 len 和 addr。不是先定 addr 再算 len,而是两个变量一起解。我曾经在项目里吃过这个亏——以为 addr 先随机,结果发现 addr 被 len 反推到了奇怪的值。
避坑指南:我曾经遇到一个 bug,约束写的是 addr > 100 && addr < 200 && len == addr - 50。结果仿真跑了一晚上,addr 始终在 150 附近,len 始终在 100 附近。后来才发现——约束求解器会尽量让所有变量都落在约束范围内,导致分布不均匀。解决办法是加 solve addr before len,告诉求解器先定 addr。
3.2 范围约束:inside
inside 是我个人最常用的约束之一。它比写一串 > && < 要简洁得多,而且可读性更好。
class packet;
rand int pkt_type;
rand bit [7:0] payload_len;
constraint c_type_range {
pkt_type inside {[0:7]}; // 0到7,包含边界
}
constraint c_payload_range {
payload_len inside {[64:1518]}; // 以太网帧长度范围
}
endclass
你想想看,如果不用 inside,你得写成 pkt_type >= 0 && pkt_type <= 7,多啰嗦。而且 inside 还支持不连续的区间:
constraint c_special {
addr inside {[0:15], [32:47], [64:79]}; // 跳过中间段
}
嗯,这里要注意:inside 的边界是包含的。也就是说 inside {[0:7]} 包含 0 和 7。这和某些语言里的左闭右开不一样,别搞混了。
个人经验:我在做 PCIe 验证时,经常用 inside 来约束配置空间地址。比如 cfg_addr inside {[0x00:0x3F], [0x80:0x9F]},跳过中间的保留区域。这样写既清晰又不容易出错。
3.3 集合约束:distribute
distribute 是约束里的「软刀子」。它不强制变量取某个值,而是调整取值的概率。说白了就是:我想让某些值出现得更频繁一些。
class packet;
rand int pkt_type;
constraint c_dist {
pkt_type dist {
0 := 50, // 权重50
1 := 30, // 权重30
2 := 20 // 权重20
};
}
endclass
这里 := 表示每个值的权重。pkt_type 取 0 的概率是 50%,取 1 是 30%,取 2 是 20%。
还有一种写法是 :/,它表示总权重分配:
constraint c_dist_range {
pkt_type dist {
[0:3] :/ 60, // 0-3 总共占60%权重
[4:7] :/ 40 // 4-7 总共占40%权重
};
}
这里 :/ 是把 60 平均分给 0、1、2、3 四个值,每个值 15%。而 := 是每个值都拿 60,那就变成 240 了。区别很大,别搞混。
关键区别:
:=:每个值独立分配权重:/:总权重在区间内平均分配
3.4 三种约束的实战组合
实际项目中,这三种约束很少单独用。我给大家看一个我去年做过的项目片段:
class ethernet_frame;
rand bit [47:0] dst_mac;
rand bit [47:0] src_mac;
rand bit [15:0] ether_type;
rand bit [7:0] payload[$];
constraint c_mac_range {
dst_mac inside {[48'h000000000001 : 48'h0000000000FF]};
src_mac inside {[48'hAABBCCDDEE01 : 48'hAABBCCDDEEFF]};
}
constraint c_ether_type {
ether_type dist {
16'h0800 := 60, // IPv4
16'h0806 := 20, // ARP
16'h86DD := 15, // IPv6
[16'h880B:16'h880C] :/ 5 // 其他协议
};
}
constraint c_payload_len {
payload.size() > 46;
payload.size() < 1500;
}
endclass
你看,这里关系约束保证了 payload 长度在合理范围,inside 约束了 MAC 地址的取值区间,distribute 则让 IPv4 报文出现得更频繁——毕竟大部分测试场景都需要 IPv4。
避坑指南:我曾经在 distribute 里用了 := 但本意是 :/,结果某个值的概率高得离谱,导致覆盖率死活不收敛。排查了两天才发现是权重分配方式搞错了。所以我的建议是:如果你想让某个区间内的值均匀分布,用 :/;如果你想让每个值独立控制概率,用 :=。
3.5 小结
这三种约束,说白了就是:
- 关系约束:变量之间的硬性关系,必须满足
- 范围约束:变量取值必须在某个集合内,简洁高效
- 集合约束:调整概率分布,让某些场景出现得更频繁
我个人建议,刚开始学约束时,先别急着用 distribute。先把关系约束和 inside 用熟,等你对约束求解器的行为有感觉了,再引入概率分布。否则很容易出现「约束写了但覆盖率就是上不去」的尴尬局面。
下一章咱们聊聊更复杂的约束——条件约束和迭代约束。到时候你会看到,这些基础约束组合起来能玩出多少花样。