SystemVerilog实例
❓ A Hard Journey…
加法器
需求
实现一个 2 位加法器: 输入两个非负整数, 输出这两个数的和.
输入:
a
: 宽度为 2, 表示输入的第一个非负整数;b
: 宽度为 2, 表示输入的第二个非负整数.
输出:
c
: 宽度为 2, 表示a + b
, 溢出的部分舍弃.
电路
对于这一类输出仅随着输入变化而变化的信号, 通常使用组合逻辑来实现. 特点是输出完全依赖于输入, 没有内部状态, 和时间无关.
根据真值表, 可以得到输出与输入的关系 (a_0
表示 a
的最低位). 电路图如下:
代码
实际上直接写 a + b
就可以了, EDA 工具会自动完成逻辑转换.
module add2 (
input wire [1:0] a,
input wire [1:0] b,
output wire [1:0] c
);
assign c = a + b;
endmodule
一个很容易犯的错误是在 output wire [1:0] c
后面多写了一个逗号. 不要把这里的 assign c = a + b
理解为赋值, 而是把它看成信号的连接: 通过一系列的逻辑门, 计算得到 a + b
的结果, 再把结果连接到输出信号 c
上.
总结
回顾上面的电路, 最大的特点是输入一变化, 输出就跟着变, 并且与时间无关, 这种电路称为组合电路 (组合逻辑电路).
按钮开关
需求
实现一个控制台灯的按钮开关: 按下开关的时候, 灯会亮起来; 再次按下开关的时候, 灯就熄灭了.
输入:
button
: 1 表示按钮被按下, 0 表示按钮处于弹起状态.
输出:
light
: 1 表示灯亮起, 0 表示灯熄灭.
电路
light
输出与它本身的历史状态有关, 并且正好是取反的关系. 如果依然采用组合逻辑来实现, 写出形如 light <= ~light;
的代码, 对应的电路就出现了环路, 此时 light
会不断在 0
和 1
之间震荡.
这一类输出与历史状态相关, 并且输出在某个信号的上升沿变化的信号, 通常使用时序逻辑来实现. 把 button
连接到触发器的 C
端口, 就实现了上升沿触发的目的; 为了实现每次触发, 让输出的结果取反, 可以把触发器的 Q
经过一个非门再连接到触发器的 D
端口:
这个电路也成了一个环, 引入触发器的作用, 使得只有在时钟上升沿时, 触发器的输入 D 会引发输出 Q 的变化, 而当时钟上升沿结束以后, 输出 Q 也许会导致输入 D 变化, 但是输出 Q 是稳定不变的, 因此不会出现循环振荡.
代码
时序逻辑里, 需要显式的声明一个寄存器 (对应电路里的触发器), 并严格把信号连接到触发器的输入 D 端口.
module button (
input wire button,
output wire light
);
logic light_reg;
assign light = light_reg;
always_ff @ (posedge button) begin
light_reg <= ~light_reg;
end
endmodule
需要显式声明一个触发器, 称为 light_reg
, 然后采用 assign light = light_reg
语句把触发器的输出 Q 端口连接到输出信号 light
上. 接下来实现 light_reg
在 button
上升沿时, 将当前的值取反.
这里把 light_reg
的输出 Q 经过非门连接到 light_reg
的输入 D 中. 换句话说, 出现在 <=
右侧的都是触发器的输出 Q 端口, 而出现在 <=
左侧的都是触发器的输入 D 端口. 这里的 <=
要理解为信号的连接, 而不是软件编程中的赋值.
总结
时序逻辑电路和组合逻辑电路最大的区别在于可以记录历史, 并且在一定的条件 (输入信号 C 的上升沿) 下触发更新 . 根据这个特点, 我们就可以保存状态, 在上升沿事件的“带领”下更新内部状态.
秒表
需求
设计一个秒表: 输出一个数字, 每秒加一; 按下复位按钮恢复到零.
输入:
reset
: 1 表示复位按钮被按下, 需要清零; 0 表示不需要清零.clock
: 频率为 1MHz 的时钟.
输出:
timer
: 4 位的数字, 表示目前经过的秒数.
为了实现秒表, 需要外部的时钟连接到电路的输入 clock
中, 可以在内部逻辑中, 每一次时钟上升沿给计数器加一, 当计数器加到 1,000,000 次就知道经过了 1s 时间.
波形
秒表每秒输出都会加一, 说明内部需要保存状态, 需要用时序逻辑来实现这一部分功能. 可以用一个寄存器 timer_reg
来保存当前的秒数, 把寄存器的输出连接到 timer
输出上, 只要保证每 1s 中让 timer_reg
加一即可.
如何实现每 1s 让 timer_reg
加一? 上面引入了一个频率为 1MHz 的时钟, 每 1us 都有一次时钟上升沿, 为了记忆经过了多少次上升沿又是一个状态, 用一个寄存器 counter_reg
来保存当前经过了多少次上升沿.
每次上升沿 counter_reg
加一, 当加到 1,000,000 时给 timer_reg
加一, 同时让 counter_reg
恢复到 0, 这样就实现了秒表的计时功能.
需求里面的复位按钮有两种实现方法:
- 按下按钮, 输出变成 0, 符合输入一变输出立即跟着变的特点, 所以可以用组合逻辑 实现:
timer = reset ? 0 : timer_reg
; 在时钟上升沿, 如果发现reset == 1
, 设置timer_reg = 0
和counter_reg = 0
, 松开按钮时会从 0 开始计时. - 按下按钮, 在时钟上升沿如果发现
reset == 1
, 就设置timer_reg = 0
和counter_reg = 0
; 从下一个周期开始, 输出的timer = timer_reg
就变成了 0.
电路
两组寄存器 timer_reg
和 counter_reg
. counter_reg
在每个 clock
上升沿进行更新, timer_reg
在每个 clock
的上升沿进行更新, 这些 “可能” 在电路上对应用组合逻辑实现的选择器. 最后把 timer_reg
的输出连接到 timer
输出即可.
代码
module timer (
input wire clock,
input wire reset,
output wire [3:0] timer
);
logic [3:0] timer_reg;
logic [19:0] counter_reg;
// sequential
always_ff @ (posedge clock) begin
if (reset) begin
timer_reg <= 4'b0;
counter_reg <= 20'b0;
end else begin
if (counter_reg == 20'd999999) begin
timer_reg <= timer_reg + 4'b1;
counter_reg <= 20'b0;
end else begin
counter_reg <= counter_reg + 20'b1;
end
end
end
// combinatorial
assign timer = timer_reg;
endmodule
接下来按照上面的思路来实现 timer_reg
和 counter_reg
的逻辑. 由于二者的判断是类似的, 可以直接合并起来. 上面的代码中, 语义上是当 XX 条件发生时, 向 YY 寄存器写入 ZZ, 实际电路则是 ZZ <= XX ? YY : ZZ
, 如果所有写入的条件都不满足, 则保留原来的状态.
总结
经过这个例子, 我们学会了如何用 if-then-else
的方式更新寄存器, 而不用手动去写 ZZ = XX ? YY : ZZ
的代码.
计数器
需求
设计一个计数器: 一个计数按钮, 每按一次计数加一; 一个复位按钮, 按下时计数恢复到零; 同时输出两位十进制的数, 显示目前按了多少次计数按钮.
输入信号:
clock
: 频率为 1MHz 的时钟.reset
: 1 表示复位按钮被按下, 0 表示没有按下.button
: 1 表示计数按钮被按下, 0 表示没有按下.
输出信号:
ones
: 输出次数的个位数, 4 位.tens
: 输出次数的十位数, 4 位.
波形
能否在 button
的时钟上升沿触发, 让寄存器加一? 由于按钮的本身特性, 按下按钮的几 ms 内是不稳定的, 不断在 0
和 1
之间抖动, 最后才趋向稳定.
为了消除这个抖动的影响 (Debounce), 可以记录最近若干次 button
的历史值, 如果连续一段时间都处于一个固定的值, 可以认为按钮处于这个状态.
计数器是一个内部状态, 需要用寄存器来实现. 能否把去抖以后的信号作为时钟信号来驱动? 如果可能的话, 尽量减少用非时钟信号作为上边沿触发, 尽量把相关的时序逻辑都放在同一个时钟域中. 如果涉及不同时钟域之间的信号处理, 之后会介绍一些用于实现跨时钟域 (CDC) 的正确电路实现方法. 建议只用一个时钟, 让这个时钟驱动所有的寄存器.
可以在时钟的上升沿来检测 button_debounced
从 0
变成了 1
, 具体思路是:
- 设置一个寄存器
button_debounced_delay
, 相对button_debounced
有一个周期的延迟; - 当
button_debounced == 1
且button_debounced_delay == 0
时, 就检测到了一个从0
变成1
的过程, 对计数器加一.
电路
模块化设计
代码主要有两部分, 一部分是消抖, 一部分是计数和输出逻辑. 消抖逻辑比较独立, 可以拆出来做成一部分电路, 然后连接到计数和输出逻辑部分.
消抖模块 (Debouncer) :
输入:
clock
: 频率为 1MHz 的时钟.reset
: 1 表示复位按钮被按下, 0 表示没有按下.button
: 1 表示计数按钮被按下, 0 表示没有按下.
输出:
button_debounced
: 消抖后的计数按钮信号, 高有效.
计数模块 (Counter) :
输入:
clock
: 频率为 1MHz 的时钟.reset
: 1 表示复位按钮被按下, 0 表示没有按下.button_debounced
: 消抖后的计数按钮信号, 高有效.
输出:
ones
: 输出次数的个位数, 4 位.tens
: 输出次数的十位数, 4 位.
消抖模块
需要记录下历史输入, 才可以判断是否 10,000 个周期都保持稳定. 用 10,000 个 1 位的寄存器有些浪费, 可以用一个寄存器来记录目前稳定了多少个周期.
- 寄存器
last_button_reg
记录上一个周期button
. - 寄存器
counter_reg
用来保存一个计数, 当button == last_button_reg
时, 说明button
保持稳定, 那么counter_reg = counter_reg + 1
; 否则清零重新计数 - 寄存器
button_debounced_reg
保存当前输出的消抖结果, 当counter_reg = 10000
的时候, 更新button_debounced_reg = last_button_reg
.
最后把 button_debounced_reg
连接到 button_debounced
输出.
计数模块
接收来自消抖模块的输出 button_debounced
, 检测到从 0
变成 1
的时候计数器加一. 能不能用一个完整的寄存器保存计数, 输出设置 ones = counter_reg % 10
和 tens = counter_reg / 10
? 不建议, 因为除法和取模运算会耗费大量的逻辑门, 并且延迟比较大. 如果位数更多, 产生的电路复杂度和延迟可能是不可接受的. 考虑到这里每次对 counter_reg
的操作只有加一和清零, 可以添加 ones_reg
和 tens_reg
寄存器, 实现加一和清零的操作, 并且手动处理进位.
用一个寄存器检测按下的计数按钮, 即 button_debounced
从 0
变成 1
:
- 寄存器
button_debounced_reg
保存上一周期的button_debounced
. - 如果
button_debounced == 1 && button_debounced_reg == 0
, 说明检测到了从0
变成了1
.
最后是 ones_reg
和 tens_reg
, 连接到输出的 ones
和 tens
信号即可.
代码
首先是消抖电路:
module debouncer (
input wire clock,
input wire reset,
input wire button,
output wire button_debounced
);
logic last_button_reg;
logic [15:0] counter_reg;
logic button_debounced_reg;
always_ff @ (posedge clock) begin
if (reset) begin
last_button_reg <= 1'b0;
counter_reg <= 16'b0;
button_debounced_reg <= 1'b0;
end else begin
last_button_reg <= button;
if (button == last_button_reg) begin
if (counter_reg == 16'd10000) begin
button_debounced_reg <= last_button_reg;
end else begin
counter_reg <= counter_reg + 16'b1;
end
end else begin
counter_reg <= 16'b0;
end
end
end
assign button_debounced = button_debounced_reg;
endmodule
接着是计数器部分:
module counter (
input wire clock,
input wire reset,
input wire button_debounced,
output wire [3:0] ones,
output wire [3:0] tens
);
logic [3:0] ones_reg;
logic [3:0] tens_reg;
logic button_debounced_reg;
always_ff @ (posedge clock) begin
if (reset) begin
ones_reg <= 4'b0;
tens_reg <= 4'b0;
button_debounced_reg <= 1'b0;
end else begin
button_debounced_reg <= button_debounced;
if (button_debounced && !button_debounced_reg) begin
if (ones_reg == 4'd9) begin
ones_reg <= 4'b0;
tens_reg <= tens_reg + 4'b1;
end else begin
ones_reg <= ones_reg + 4'b1;
end
end
end
end
assign ones = ones_reg;
assign tens = tens_reg;
endmodule
最后再用一个顶层 module
把两个模块合起来:
module counter_top (
input wire clock,
input wire reset,
input wire button,
output wire [3:0] ones,
output wire [3:0] tens
);
wire button_debounced;
debouncer debouncer_component (
.clock(clock),
.reset(reset),
.button(button),
.button_debounced(button_debounced)
);
counter counter_component (
.clock(clock),
.reset(reset),
.button_debounced(button_debounced),
.ones(ones),
.tens(tens)
);
endmodule
button_debounced
是两个内部模块之间的, 所以声明了一个 wire
把两个模块的输入输出连起来, 其他信号则是直接连接到顶层模块的输入输出信号.
无状态仲裁器 (优先级编码器)
需求
设计一个仲裁器: 假想多个用户同时访问同一个资源, 但是资源同时只能给一个用户使用, 这时需要使用一个仲裁器, 选择出一个幸运儿, 其他用户则需要等待. 假设资源的访问是 “立即” 完成的, 资源正在使用的时候, 使用权不会被其他用户抢走.
输入:
request
: 宽度为 4, 每一位 1 表示对应的用户请求访问资源, 0 表示不请求.
输出:
valid
: 1 表示有用户请求访问资源, 0 表示无用户请求访问资源.user
: 宽度为 2, 如果有用户请求访问资源时, 输出获得资源的用户的编号.
电路
仲裁器的输出完全由输入决定, 没有内部状态, 所以可以用组合逻辑来实现. valid
信号比较简单, 直接把所有输入用或门连接在一起即可. 如何找到请求的用户里, 编号最小的那一个? 可以分情况讨论:
request=0000
, 输出的user
可以是任意值.request=???1
, 此时user=0, valid=1
.request=??10
, 此时user=1, valid=1
.request=?100
, 此时user=2, valid=1
.request=1000
, 此时user=3, valid=1
.
上面五个条件遍历了所有可能的情况. 在实现组合逻辑的时候, 一定要考虑所有情况, 并且每个情况下每个信号都要得到一个结果, 否则不可避免会引入锁存器.
代码
module priority_encoder (
input wire [3:0] request,
output wire valid,
output wire [1:0] user
);
logic valid_comb;
logic [1:0] user_comb;
always_comb begin
valid_comb = 1'b0;
user_comb = 2'd0;
casez (request)
4'b???1: begin
valid_comb = 1'b1;
user_comb = 2'd0;
end
4'b??10: begin
valid_comb = 1'b1;
user_comb = 2'd1;
end
4'b?100: begin
valid_comb = 1'b1;
user_comb = 2'd2;
end
4'b1000: begin
valid_comb = 1'b1;
user_comb = 2'd3;
end
endcase
end
assign valid = valid_comb;
assign user = user_comb;
endmodule
实现组合逻辑电路的一种方法是用 assign
, 如 assign valid = |request
; 但是涉及更复杂的组合逻辑时, 会比较复杂. 可以在 always_comb
块中灵活地使用各种条件语句, 包括 casez
语句, 首先设置了一个默认的结果, 这样如果下面所有的 casez
都不满足, 那么输出的就是默认值.
总结
总结规律:
- 确定输入输出;
- 确定需要哪些寄存器;
- 实现时序逻辑;
- 实现组合逻辑.
循环优先级仲裁器
需求
实现一个循环优先级仲裁器 (round robin arbiter), 根据最后一次获取资源的用户, 决定下一次获取资源的优先级. 当一个用户 A
不再获取资源 (对应位 request
从 1
变成 0
) 时, 重新选择一个可以获取资源的用户, 优先级是从 A
的下一个用户开始为最高优先级, 如果溢出了就绕回.
输入:
request
: 宽度为 4, 每一位 1 表示对应的用户请求访问资源, 0 表示不请求.clock
: 1MHz 的时钟.reset
: 复位信号.
输出:
valid
: 1 表示有用户请求访问资源, 0 表示无用户请求访问资源.user
: 宽度为 2, 如果有用户请求访问资源时, 输出获得资源的用户的编号.
波形
相比上一个例子, 有两个比较大的区别:
- 无状态仲裁器中, 如果出现了优先级更高的用户, 资源的访问权立即切换; 循环优先级仲裁器中, 只有用户放弃了请求才会切换;
- 仲裁时, 优先级根据最后一次获得访问权的用户来决定.
电路
由于优先级和最后一次获得访问权的用户有关, 需要时序逻辑实现. 用 user_reg
记录最后一次获得访问权的用户编号:
- 什么时候更新: 上一个周期没有用户获得访问权, 这个周期
request
不等于零; 当前周期获得访问权的用户对应的request
位由1
变成了0
. - 更新成什么: 按照优先级顺序在
request
里选出目前优先级最高的用户.
由此, 在这一类内部具有状态, 又需要在输入变化的同一个周期输出的情况, 需要用时序逻辑来保存状态, 同时用组合逻辑来实现同周期的输出, 把二者结合起来.
第一部分是修改后的优先级编码器, 额外添加输入 last_user
表示最后一次获得访问权的用户编号.
第二部分是维护 user_reg
状态. 第一个模块是上面提到的修改后的优先级编码器, 第二个模块是整体的循环优先级仲裁器, 内部例化第一个模块.
代码
实现第一部分逻辑, 根据最后一次获取资源的用户编号确定优先级的优先级编码器:
module rr_priority_encoder (
input wire [3:0] request,
input wire [1:0] last_user,
output wire valid,
output wire [1:0] user
);
logic valid_comb;
logic [1:0] user_comb;
always_comb begin
valid_comb = 1'b0;
user_comb = 2'd0;
// naive way
if (last_user == 2'd3) begin
casez (request)
4'b???1: begin
valid_comb = 1'b1;
user_comb = 2'd0;
end
4'b??10: begin
valid_comb = 1'b1;
user_comb = 2'd1;
end
4'b?100: begin
valid_comb = 1'b1;
user_comb = 2'd2;
end
4'b1000: begin
valid_comb = 1'b1;
user_comb = 2'd3;
end
endcase
end else if (last_user == 2'd0) begin
casez (request)
4'b??1?: begin
valid_comb = 1'b1;
user_comb = 2'd1;
end
4'b?10?: begin
valid_comb = 1'b1;
user_comb = 2'd2;
end
4'b100?: begin
valid_comb = 1'b1;
user_comb = 2'd3;
end
4'b0001: begin
valid_comb = 1'b1;
user_comb = 2'd0;
end
endcase
end else if (last_user == 2'd1) begin
casez (request)
4'b?1??: begin
valid_comb = 1'b1;
user_comb = 2'd2;
end
4'b10??: begin
valid_comb = 1'b1;
user_comb = 2'd3;
end
4'b00?1: begin
valid_comb = 1'b1;
user_comb = 2'd0;
end
4'b0010: begin
valid_comb = 1'b1;
user_comb = 2'd1;
end
endcase
end else if (last_user == 2'd2) begin
casez (request)
4'b1???: begin
valid_comb = 1'b1;
user_comb = 2'd3;
end
4'b0??1: begin
valid_comb = 1'b1;
user_comb = 2'd0;
end
4'b0?10: begin
valid_comb = 1'b1;
user_comb = 2'd1;
end
4'b0100: begin
valid_comb = 1'b1;
user_comb = 2'd2;
end
endcase
end
end
assign valid = valid_comb;
assign user = user_comb;
endmodule
接着是循环优先级仲裁器:
module rr_arbiter (
input wire clock,
input wire reset,
input wire [3:0] request,
output wire valid,
output wire [1:0] user
);
logic [1:0] user_reg;
logic valid_reg;
logic [1:0] user_comb;
logic [1:0] priority_encoder_user_comb;
rr_priority_encoder rr_priority_encoder_inst (
.request(request),
.last_user(user_reg),
.valid(valid),
.user(priority_encoder_user_comb)
);
// sequential
always_ff @ (posedge clock) begin
if (reset) begin
user_reg <= 2'd0;
valid_reg <= 1'b0;
end else begin
valid_reg <= valid;
if (!valid_reg && valid) begin
// case 1: non valid -> valid
user_reg <= priority_encoder_user_comb;
end else if (valid_reg && valid && request[user_reg]) begin
// case 2: persist
end else if (valid_reg && valid && !request[user_reg]) begin
// case 3: next user
user_reg <= priority_encoder_user_comb;
end
end
end
// combinatorial
always_comb begin
// default
user_comb = 2'b0;
if (!valid_reg && valid) begin
// case 1: non valid -> valid
user_comb = priority_encoder_user_comb;
end else if (valid_reg && valid && request[user_reg]) begin
// case 2: persist
user_comb = user_reg;
end else if (valid_reg && valid && !request[user_reg]) begin
// case 3: next user
user_comb = priority_encoder_user_comb;
end
end
assign user = user_comb;
endmodule
仿真
描述数字电路的 Verilog 和用来仿真的 Verilog 使用完全不同的编写思路和实现方法. 前者与电路一一对应, 而后者更像是 C 这种过程式的编程语言.
例子
module add2 (
input wire [1:0] a,
input wire [1:0] b,
output wire [1:0] c
);
assign c = a + b;
endmodule
要给这个模块输入数据, 要人为地设置模块的输入:
`timescale 1ns/1ps
module add2_tb ();
reg [1:0] a;
reg [1:0] b;
wire [1:0] c;
initial begin
a = 2'b01;
b = 2'b10;
#1;
$finish;
end
add2 inst (
.a(a),
.b(b),
.c(c)
);
endmodule
由于 c
连接到 add2
模块的输出, 所以要用 wire
; 其他要输入到 add2
模块中, 所以用 reg
. 运行 #1;
命令, 表示等待 1ns
, 然后再运行 $finish;
, 表示仿真结束.
时钟
仿真一个带有时序逻辑的模块, 使用前面的秒表的例子:
module timer (
input wire clock,
input wire reset,
output wire [3:0] timer
);
reg [3:0] timer_reg;
reg [19:0] counter_reg;
// sequential
always @ (posedge clock) begin
if (reset) begin
timer_reg <= 4'b0;
counter_reg <= 20'b0;
end else begin
if (counter_reg == 20'd999999) begin
timer_reg <= timer_reg + 4'b1;
counter_reg <= 20'b0;
end else begin
counter_reg <= counter_reg + 20'b1;
end
end
end
// combinatorial
assign timer = timer_reg;
endmodule
例化 timer
模块, 连接输入输出信号. 时钟信号以一个固定的频率在 0 和 1 之间变化. 如果频率是 50MHz, 那么一个周期每 10ns 变化一次. 这样下去就可以构造出一个时钟信号:
initial begin
reset = 1'b0;
clock = 1'b1;
#10;
clock = 1'b0;
#10;
clock = 1'b1;
#10;
clock = 1'b0;
#10;
end
希望仿真更多时钟周期, 自动生成时钟信号:
initial begin
reset = 1'b0;
clock = 1'b1;
end
always #10 clock = ~clock;
复位
处理好时钟后仿真上面的代码, 会发现 timer
输出一直是 x
, 因为 timer
没有被复位. 需要先设置 reset
为 1, 再设置 reset
为 0:
initial begin
reset = 1'b0;
clock = 1'b1;
#10;
reset = 1'b1;
#10;
reset = 1'b0;
end
always #10 clock = ~clock;
构造输入
目前的仿真顶层模块没有提供要测试的模块的其他输入信号, 还需要针对特定的协议人为构造输入.
reg ps2_clock;
reg ps2_data;
ps2_keyboard dut (
.clock(clock),
.reset(reset),
.ps2_clock(ps2_clock),
.ps2_data(ps2_data)
);
按照 PS/2 的协议, 按顺序给 ps2_clock 和 ps2_data 赋值, 穿插着延迟语句.
ps2_data = 1'b1;
ps2_clock = 1'b1;
#5;
// start bit
ps2_data = 1'b0;
#5;
ps2_clock = 1'b0;
#5;
ps2_clock = 1'b1;
// scancode[0] = 0
ps2_data = 1'b0;
#5;
ps2_clock = 1'b0;
#5;
ps2_clock = 1'b1;
// scancode[1] = 0
ps2_data = 1'b0;
#5;
ps2_clock = 1'b0;
#5;
ps2_clock = 1'b1;
...
// scancode[7] = 1
ps2_data = 1'b1;
#5;
ps2_clock = 1'b0;
#5;
ps2_clock = 1'b1;
// parity = 1
ps2_data = 1'b1;
#5;
ps2_clock = 1'b0;
#5;
ps2_clock = 1'b1;
// stop
ps2_data = 1'b1;
#5;
ps2_clock = 1'b0;
#5;
ps2_clock = 1'b1;
更进一步, 如果想要重复发送 scancode, 只不过内容会更改, 可以把这一步骤封装成 task, 完整写法见 Tsinghua GitLab.
总结
总结一下:
- 单独写一个仿真顶层模块, 例化要测试的模块.
- 测试模块的输入输出接到
reg
或者wire
. - 时序逻辑在
initial
块初始化时钟信号, 用always #10 clock = ~clock;
生成时钟信号. - 复位信号在
initial
块内, 仿真信号由 0 变成 1, 再由 1 变成 0. - 输入信号在
initial
块内, 对对应的reg
信号进行赋值.