
你已经使用Logisim设计过数字电路
本次课内容:
RTL代码在仿真时发生了什么?
电路的设计和制造是两个阶段
迭代周期太长, 怎么办?
仿真 = 用软件程序来模拟真实硬件电路的行为
有了仿真程序, 就可以基于程序进行电路的设计和验证了
| C程序 | 数字电路 | |
|---|---|---|
| 状态 | \(\{PC, V\}\) | 时序逻辑电路 |
| 激励事件 | 执行语句 | 处理组合逻辑 |
| 状态转移规则 | 语句的语义 | 组合逻辑电路的逻辑 |
#define _CONCAT(x, y) x ## y
#define CONCAT(x, y) _CONCAT(x, y)
#define DEF_WIRE(name, w) uint64_t name : w
#define DEF_REG(name, w) uint64_t name : w; \
uint64_t CONCAT(name, _next) : w; \
uint64_t CONCAT(name, _update) : 1
typedef struct {
DEF_WIRE(clk, 1);
DEF_WIRE(rst, 1);
DEF_REG (led, 16);
DEF_REG (count, 32);
} Circuit;
Circuit circuit;通过结构体变量circuit实现时序逻辑电路
led和count
_next的中间变量和后缀为_update的更新标志,
用于实现Verilog中非阻塞赋值的语义clk和rst虽然不是时序逻辑电路, 但作为输入,
仍然属于电路状态#define EVAL(c, name, val) do { \
c->CONCAT(name, _next) = (val); \
c->CONCAT(name, _update) = 1; \
} while (0)
#define UPDATE(c, name) do { \
if (c->CONCAT(name, _update)) { \
c->name = c->CONCAT(name, _next); \
} \
} while (0)
static void cycle(Circuit *c) {
c->led_update = 0;
c->count_update = 0;
if (c->rst) {
EVAL(c, led, 1);
EVAL(c, count, 0);
} else {
if (c->count == 0) {
EVAL(c, led, (BITS(c->led, 14, 0) << 1) | BITS(c->led, 15, 15));
}
EVAL(c, count, c->count >= 5000000 ? 0 : c->count + 1);
}
UPDATE(c, led);
UPDATE(c, count);
}cycle()基本上是相应Verilog代码的直接翻译
数字电路的工作, 就是不断重复以下步骤:

static void reset(Circuit *c) {
c->rst = 1;
cycle(c);
c->rst = 0;
}
int main() {
reset(&circuit);
while (1) {
cycle(&circuit);
}
return 0;
}RTL仿真的主要过程: 不断执行cycle()函数
#include <stdio.h>
#include <stdint.h>
#define _CONCAT(x, y) x ## y
#define CONCAT(x, y) _CONCAT(x, y)
#define BITMASK(bits) ((1ull << (bits)) - 1)
// similar to x[hi:lo] in verilog
#define BITS(x, hi, lo) (((x) >> (lo)) & BITMASK((hi) - (lo) + 1))
#define DEF_WIRE(name, w) uint64_t name : w
#define DEF_REG(name, w) uint64_t name : w; \
uint64_t CONCAT(name, _next) : w; \
uint64_t CONCAT(name, _update) : 1
#define EVAL(c, name, val) do { \
c->CONCAT(name, _next) = (val); \
c->CONCAT(name, _update) = 1; \
} while (0)
#define UPDATE(c, name) do { \
if (c->CONCAT(name, _update)) { \
c->name = c->CONCAT(name, _next); \
} \
} while (0)typedef struct {
DEF_WIRE(clk, 1);
DEF_WIRE(rst, 1);
DEF_REG (led, 16);
DEF_REG (count, 32);
} Circuit;
static Circuit circuit;
static void cycle(Circuit *c) {
c->led_update = 0;
c->count_update = 0;
if (c->rst) {
EVAL(c, led, 1);
EVAL(c, count, 0);
} else {
if (c->count == 0) {
EVAL(c, led, (BITS(c->led, 14, 0) << 1) | BITS(c->led, 15, 15));
}
EVAL(c, count, c->count >= 5000000 ? 0 : c->count + 1);
}
UPDATE(c, led);
UPDATE(c, count);
}static void reset(Circuit *c) {
c->rst = 1;
cycle(c);
c->rst = 0;
}
static void display(Circuit *c) {
static uint16_t last_led = 0;
if (last_led != c->led) { // only update display when c->led changes
for (int i = 0; i < 16; i ++) {
putchar(BITS(c->led, i, i) ? 'o' : '.');
}
putchar('\r');
fflush(stdout);
last_led = c->led;
}
}
int main() {
reset(&circuit);
while (1) {
cycle(&circuit);
display(&circuit);
}
return 0;
}为每个电路手工编写仿真程序, 是很麻烦的
RTL仿真器\(\approx\)编译器
#include <stdio.h>
#include "Vlight.h"
#define BITMASK(bits) ((1ull << (bits)) - 1)
#define BITS(x, hi, lo) (((x) >> (lo)) & BITMASK((hi) - (lo) + 1))
static Vlight *top = NULL;
void cycle() { top->clk = 0; top->eval(); top->clk = 1; top->eval(); }
void reset() { top->rst = 1; cycle(); top->rst = 0; }
static void display() {
static uint16_t last_led = 0;
if (last_led != top->led) {
for (int i = 0; i < 16; i ++) {
putchar(BITS(top->led, i, i) ? 'o' : '.');
}
putchar('\r');
fflush(stdout);
last_led = top->led;
}
}
int main() {
top = new Vlight;
reset();
while (1) { cycle(); display(); }
return 0;
}用变量实现时序逻辑电路
// obj_dir/Vlight___024root.h
VL_IN8(clk,0,0);
VL_IN8(rst,0,0);
VL_OUT16(led,15,0);
IData/*31:0*/ light__DOT__count;
// ...用语句实现组合逻辑电路的逻辑
// obj_dir/Vlight___024root__DepSet_hbb020019__0.cpp
if (vlSelf->rst) {
__Vdly__led = 1U;
__Vdly__light__DOT__count = 0U;
} else {
if ((0U == vlSelf->light__DOT__count)) {
__Vdly__led = ((0xfffeU & ((IData)(vlSelf->led)
<< 1U)) | (1U
& ((IData)(vlSelf->led)
>> 0xfU)));
}
__Vdly__light__DOT__count = ((0x4c4b40U <= vlSelf->light__DOT__count)
? 0U : ((IData)(1U)
+ vlSelf->light__DOT__count));
}| 举例 | 效率 | 精确度 | |
|---|---|---|---|
| 指令集模拟器 | sEMU, NEMU, QEMU | +++++ | 指令集(行为正确) |
| 体系结构模拟器 | GEM5 | +++ | 性能(大致时间) |
| RTL仿真器 | VCS, Verilator | ++ | 电路逻辑(精确时间) |
| 晶体管仿真器 | Spice | + | 晶体管(电气特性) |
在处理器芯片设计流程中, 前三类都会使用:
后面的课程会进一步讨论

下面有若干条Verilog的编码建议或描述, 但其中有一些是不正确的, 请尝试找出它们:
#0可以将赋值操作强制延迟到当前仿真时刻的末尾.begin-end语句块中对同一个变量进行多次非阻塞赋值,
结果是未定义的.always块描述组合逻辑元件时, 不能使用非阻塞赋值.always块中对同一个变量进行赋值.$display系统任务,
因为有时候它无法正确输出变量的值.$display无法输出非阻塞赋值语句的结果.
你可能会写一些Verilog代码, 但你大概率无法解释清楚上述问题
你真正需要了解的, 是Verilog的本质
Verilog语言在仿真过程中的语义, 是由Verilog标准手册定义的
Intruduction
It was designed ... for a variety of design tools, including verification
simulation, timing analysis, test analysis, and synthesis.simulation出现356次, synthesis出现4次
手册的第11章揭示了Verilog语言的本质
传统的教材和大部分Verilog资料中都没被提及
花点时间看看手册, 会对Verilog有脱胎换骨的理解
RTFM:
11.1 Execution of a model
The elements that make up the Verilog HDL can be used to describe the behavior, at
varying levels of abstraction, of electronic hardware. An HDL has to be a parallel
programming language. The execution of certain language constructs is defined by
parallel execution of blocks or processes. It is important to understand what
execution order is guaranteed to the user and what execution order is indeterminate.
Although the Verilog HDL is used for more than simulation, the semantics of the
language are defined for simulation, and everything else is abstracted from this
base definition.Verilog标准手册的第11.2节定义了仿真的概念, 我们进行逐条说明:
The Verilog HDL is defined in terms of a discrete event execution model. The discrete
event simulation is described in more detail in this subclause to provide a context
to describe the meaning and valid interpretation of Verilog HDL constructs. These
resulting definitions provide the standard Verilog reference model for simulation,
which all compliant simulators shall implement. However, there is a great deal of
choice in the definitions that follow, and differences in some details of execution
are to be expected between different simulators. In addition, Verilog HDL simulators
are free to use different algorithms from those described in this clause, provided
the user-visible effect is consistent with the reference model.A design consists of connected threads of execution or processes. Processes are
objects that can be evaluated, that may have state, and that can respond to changes
on their inputs to produce outputs. Processes include primitives, modules, initial
and always procedural blocks, continuous assignments, asynchronous tasks, and
procedural assignment statements.过程过程是一些可以被求值的对象, 这些对象拥有自己的状态,
当输入产生变化时, 它们可以响应这些变化, 并产生输出initial和always块,
连续赋值, 异步任务和过程赋值语句
Processes are sensitive to update events. When an update event is executed, all the
processes that are sensitive to that event are evaluated in an arbitrary order. The
evaluation of a process is also an event, known as an evaluation event.求值事件
In addition to events, another key aspect of a simulator is time. The term
simulation time is used to refer to the time value maintained by the simulator to
model the actual time it would take for the circuit being simulated. The term time
is used interchangeably with simulation time in this clause.仿真时间用于对仿真电路所用的实际时间进行建模,
由仿真器维护仿真时间有时候也简称时间Events can occur at different times. In order to keep track of the events and to
make sure they are processed in the correct order, the events are kept on an event
queue, ordered by simulation time. Putting an event on the queue is called
scheduling an event.事件的调度
总结: Verilog语言成分的语义都和事件有关联:
根据Verilog标准手册的第11.3节, 事件队列在逻辑上包含以下5个区域, 分别用于处理对应种类的事件:
#0)可以使对应的过程挂起, 将产生一个\(R_2\)事件$monitor和$strobe在每个仿真时刻都产生一个\(R_4\)事件Verilog标准手册的第11.4节给出了事件处理引擎的参考模型
while (there are events) { // 不断重复如下操作
if (no active events) { // R1为空时
if (there are inactive events) { // 如果R2不为空
activate all inactive events; // 则将R2中的所有事件转移到R1
} else if (there are nonblocking assign update events) { // 否则, 如果R3不为空
activate all nonblocking assign update events; // 则将R3中的所有事件转移到R1
} else if (there are monitor events) { // 否则, 如果R4不为空
activate all monitor events; // 则将R4中的所有事件转移到R1
} else { // 否则
advance T to the next event time; // 将仿真时刻前进一个单位
activate all inactive events for time T;// 将R5中当前仿真时刻的事件按类型转移到R1或R3
}
}
E = any active event; // 从R1中取出一个事件
if (E is an update event) { // 如果是更新事件
update the modified object; // 更新相应的对象
// 将对该事件敏感的过程的求值作为求值事件添加到事件队列中
add evaluation events for sensitive processes to event queue;
} else { // 如果是求值事件
evaluate the process; // 对过程进行求值
add update events to the event queue; // 将赋值行为作为更新事件添加到事件队列中
}
}
事件处理引擎可以帮助大家回答这个问题
i = i + 1为例
addi a0, a0, 1的指令,
最后在处理器上直接执行
根据Verilog标准手册的第11.6节, 赋值操作会转换成行为等价的过程
下面选取一些常见的赋值操作进行说明
#)的情况
assign语句)
对表达式的源操作数都敏感的过程0时刻的求值事件,
实现常量的传播两者的本质区别: 更新操作的时机有所不同
结果: 能看到赋值结果的事件集合并不相同, 从而影响这些事件的行为, 最终影响电路的整体行为
.a(i),
将视为连续赋值语句assign a = i;来处理.b(o),
将视为连续赋值语句assign o = b;来处理用返回值替换调用处的行为按阻塞赋值来处理
Verilog标准手册还定义了如何将更多情况转换成事件来处理:
事件处理的顺序并非100%确定的
根据Verilog标准手册的定义, 不确定性主要有两个来源:
#表达式和@表达式)的语句不必作为一整个事件来处理
硬件电路的行为本身就具有并行性: 多个组件之间天然就是并行工作的
放松:
如果两个事件之间不存在依赖关系,
也不必要求它们按一定的先后顺序来处理定义\(A \to B\)表示事件\(A\)先于事件\(B\)被处理
前序于关系
事件处理引擎其实隐含了一些顺序要求:
顺序规则1(GEN) - 如果在处理\(A\)的过程中生成了\(B\), 则\(A \to
B\)
顺序规则2(EVQ) - 如果在仿真过程中的某一时刻, 有\(A\in R_i\), \(B\in R_j\), 且\(i
< j\), 则\(A \to B\)
此外, Verilog标准手册还显式定义了如下两条顺序规则:
顺序规则3(SEQ) -
begin-end语句块中的语句需要按语句顺序执行
begin-end语句块中的两个语句\(S_i\)和\(S_j\), 若\(i <
j\), 则\(S_i \to S_j\)
顺序规则4(NBA) -
非阻塞赋值操作需要按语句的执行顺序来进行
(1)的语句可分解为\(E_1:
\mathrm{eval(0)}\)和\(E_2:
\mathrm{update(a)}\)两个事件(2)的语句可分解为\(E_3:
\mathrm{eval(1)}\)和\(E_4:
\mathrm{update(a)}\)两个事件
顺序规则1(GEN), 应有\(E_1
\to E_2\), \(E_3 \to E_4\)顺序规则2(EVQ), 应有\(E_3
\to E_2\), \(E_1 \to E_4\)顺序规则3(SEQ), 应有\(E_1
\to E_3\)顺序规则4(NBA), 应有\(E_2
\to E_4\)
结论: 有且仅有\(E_1 \to E_3 \to E_2 \to E_4\)
a将先被赋0, 然后被赋1always @(posedge clk) begin
if (rst) begin led <= 1; count <= 0; end
else begin
if (count == 0) led <= {led[14:0], led[15]};
count <= (count >= 5000000 ? 0 : count + 1);
end
end假设clk上升沿到来, 在这个仿真时刻中,
可能会发生以下事件:
结论:
rst = 1时, 应有\(E_1 \to
E_3 \to E_2 \to E_4\)rst = 0, count = 0时, 应有\(E_5 \to E_7 \to E_6 \to E_8\)rst = 0, count != 0时, 应有\(E_7 \to E_8\)rst = 1时, 应有\(E_1 \to
E_3 \to E_2 \to E_4\)rst = 0, count = 0时, 应有\(E_5 \to E_7 \to E_6 \to E_8\)rst = 0, count != 0时, 应有\(E_7 \to E_8\)static void cycle(Circuit *c) {
c->led_update = 0; c->count_update = 0;
if (c->rst) { EVAL(c, led, 1); EVAL(c, count, 0); }
else {
if (c->count == 0) { EVAL(c, led, (BITS(c->led,14,0)<<1) | BITS(c->led,15,15)); }
EVAL(c, count, c->count >= 5000000 ? 0 : c->count + 1);
}
UPDATE(c, led); UPDATE(c, count);
}EVAL()和UPDATE()分别实现了求值事件\(\mathrm{eval(expr)}\)和更新事件\(\mathrm{update(obj)}\)的语义| 周期仿真(cycle simulation) | 事件仿真(event simulation) | |
|---|---|---|
| 驱动方式 | 仿真周期 | 事件队列 |
| 事件调度 | 静态调度, 求值顺序在仿真开始前(编译时刻)决定 | 动态调度, 求值顺序在仿真过程中(运行时刻)决定 |
| 仿真效率 | 较快, 仿真时没有调度开销; C代码有更多优化机会 | 较慢, 需要维护事件队列 |
| 时序信息 | 不支持 | 支持 |
| 适用电路 | 同步电路 | 同步/异步/混合电路 |
| 使用场景 | 大规模同步电路的功能验证 | 功能验证, 时序验证 |
| 代表工具 | Verilator | VCS, iVerilog |
仿真香山 |
585Hz(1T), 1062Hz(2T), 1830Hz(4T), 2968Hz(8T) | 50Hz(xprop), 86Hz(non-xprop) |
数据竞争(data race)
竞争条件(race condition)
通常, 如果仿真过程中存在数据竞争, 这个电路模型所描述的就不是一个有效的真实电路
always @(posedge clk or negedge rstn) begin
if (!rstn) a = 1'b0;
else a = b; // (1)
end
always @(posedge clk or negedge rstn) begin
if (!rstn) b = 1'b1;
else b = a; // (2)
end假设在某时刻有a = 0, b = 1,
rstn = 1, 且clk的上升沿到来
定义一个新操作\(\mathrm{evalAndUpdate(expr)}\)来表示阻塞赋值的行为
(1)的语句可得到\(E_1:
\mathrm{evalAndUpdate(a = b)}\)(2)的语句可得到\(E_2:
\mathrm{evalAndUpdate(b = a)}\)根据Verilog标准手册的定义, 多个激活事件的处理顺序是任意的
a = 1, b = 1a = 0, b = 0对于存在数据竞争的代码, 当仿真器选择不同的事件处理顺序时, 会导致不同的仿真结果
如果Verilog代码中存在数据竞争, 仿真结果可能是难以预测的
作业: 将上述代码中的阻塞赋值改成非阻塞赋值, 可以消除数据竞争, 分析原因
always @(posedge clk or negedge rstn) begin
if (!rstn) a = 1'b0;
else a = 1;
end
always @(posedge clk) begin
$display("a = %d", a);
end假设在某时刻有a = 0, rstn = 1,
且clk的上升沿到来
注意到$display系统任务的处理事件\(E_2 \in R_1\)
a = 1; 若\(E_2 \to
E_1\), 输出a = 0结论: 上述代码中存在数据竞争
module top(output [15:0] y);
initial begin y = 1234; end
initial begin $display("y = %d", y); end
endmodulemodule top(output [15:0] y);
initial begin $display("y = %d", y); end
initial begin y = 1234; end
endmodule#include <stdio.h>
#include "Vtop.h"
int main() {
static Vtop *top = new Vtop;
while (1) { top->eval(); }
return 0;
}作业: 将$display改成$strobe可消除数据竞争,
分析原因
Verilog代码中存在数据竞争, 当且仅当存在和同一个对象\(\mathrm{obj}\)相关的两个事件\(E_1\)和\(E_2\), 同时满足:
要消除数据竞争, 就需要消除满足上述条件的事件
为了应对这个挑战, 很多Verilog书籍和相关资料都会推荐一些良好的编码规范
“Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!”这篇文章中提出了不少Verilog编码建议
always块建立组合逻辑模型时, 用阻塞赋值.
always块中建立时序和组合逻辑电路时,
用非阻塞赋值
always块中不要既用非阻塞赋值又用阻塞赋值
always块中为同一个变量赋值
always块属于不同的过程,
它们之间的求值顺序是不确定的$strobe系统任务来显示用非阻塞赋值的变量值
$strobe系统任务的事件属于\(R_4\)#0延迟
#0产生的事件属于\(R_2\), 其处理时机位于\(R_1\)和\(R_3\)之间下面有若干条Verilog的编码建议或描述, 但其中有一些是不正确的, 请尝试找出它们:
#0可以将赋值操作强制延迟到当前仿真时刻的末尾.begin-end语句块中对同一个变量进行多次非阻塞赋值,
结果是未定义的.always块描述组合逻辑元件时, 不能使用非阻塞赋值.always块中对同一个变量进行赋值.$display系统任务,
因为有时候它无法正确输出变量的值.$display无法输出非阻塞赋值语句的结果.
理解Verilog的事件模型后, 你已经具备能力来判断这些说法是否正确了