计算机组成原理 笔记4


SystemVerilog实例

A Hard Journey…

加法器

需求

实现一个 2 位加法器: 输入两个非负整数, 输出这两个数的和.

输入:

  1. a: 宽度为 2, 表示输入的第一个非负整数;
  2. b: 宽度为 2, 表示输入的第二个非负整数.

输出:

  1. 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 上.

总结

回顾上面的电路, 最大的特点是输入一变化, 输出就跟着变, 并且与时间无关, 这种电路称为组合电路 (组合逻辑电路).

按钮开关

需求

实现一个控制台灯的按钮开关: 按下开关的时候, 灯会亮起来; 再次按下开关的时候, 灯就熄灭了.

输入:

  1. button: 1 表示按钮被按下, 0 表示按钮处于弹起状态.

输出:

  1. light: 1 表示灯亮起, 0 表示灯熄灭.

电路

light 输出与它本身的历史状态有关, 并且正好是取反的关系. 如果依然采用组合逻辑来实现, 写出形如 light <= ~light; 的代码, 对应的电路就出现了环路, 此时 light 会不断在 01 之间震荡.

这一类输出与历史状态相关, 并且输出在某个信号的上升沿变化的信号, 通常使用时序逻辑来实现. 把 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_regbutton 上升沿时, 将当前的值取反.

这里把 light_reg 的输出 Q 经过非门连接到 light_reg 的输入 D 中. 换句话说, 出现在 <= 右侧的都是触发器的输出 Q 端口, 而出现在 <= 左侧的都是触发器的输入 D 端口. 这里的 <= 要理解为信号的连接, 而不是软件编程中的赋值.

总结

时序逻辑电路和组合逻辑电路最大的区别在于可以记录历史, 并且在一定的条件 (输入信号 C 的上升沿) 下触发更新 . 根据这个特点, 我们就可以保存状态, 在上升沿事件的“带领”下更新内部状态.

秒表

需求

设计一个秒表: 输出一个数字, 每秒加一; 按下复位按钮恢复到零.

输入:

  1. reset: 1 表示复位按钮被按下, 需要清零; 0 表示不需要清零.
  2. clock: 频率为 1MHz 的时钟.

输出:

  1. 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, 这样就实现了秒表的计时功能.

需求里面的复位按钮有两种实现方法:

  1. 按下按钮, 输出变成 0, 符合输入一变输出立即跟着变的特点, 所以可以用组合逻辑 实现: timer = reset ? 0 : timer_reg; 在时钟上升沿, 如果发现 reset == 1, 设置 timer_reg = 0counter_reg = 0, 松开按钮时会从 0 开始计时.
  2. 按下按钮, 在时钟上升沿如果发现 reset == 1, 就设置 timer_reg = 0counter_reg = 0; 从下一个周期开始, 输出的 timer = timer_reg 就变成了 0.

电路

两组寄存器 timer_regcounter_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_regcounter_reg 的逻辑. 由于二者的判断是类似的, 可以直接合并起来. 上面的代码中, 语义上是当 XX 条件发生时, 向 YY 寄存器写入 ZZ, 实际电路则是 ZZ <= XX ? YY : ZZ, 如果所有写入的条件都不满足, 则保留原来的状态.

总结

经过这个例子, 我们学会了如何用 if-then-else 的方式更新寄存器, 而不用手动去写 ZZ = XX ? YY : ZZ 的代码.

计数器

需求

设计一个计数器: 一个计数按钮, 每按一次计数加一; 一个复位按钮, 按下时计数恢复到零; 同时输出两位十进制的数, 显示目前按了多少次计数按钮.

输入信号:

  1. clock: 频率为 1MHz 的时钟.
  2. reset: 1 表示复位按钮被按下, 0 表示没有按下.
  3. button: 1 表示计数按钮被按下, 0 表示没有按下.

输出信号:

  1. ones: 输出次数的个位数, 4 位.
  2. tens: 输出次数的十位数, 4 位.

波形

能否在 button 的时钟上升沿触发, 让寄存器加一? 由于按钮的本身特性, 按下按钮的几 ms 内是不稳定的, 不断在 01 之间抖动, 最后才趋向稳定.

为了消除这个抖动的影响 (Debounce), 可以记录最近若干次 button 的历史值, 如果连续一段时间都处于一个固定的值, 可以认为按钮处于这个状态.

计数器是一个内部状态, 需要用寄存器来实现. 能否把去抖以后的信号作为时钟信号来驱动? 如果可能的话, 尽量减少用非时钟信号作为上边沿触发, 尽量把相关的时序逻辑都放在同一个时钟域中. 如果涉及不同时钟域之间的信号处理, 之后会介绍一些用于实现跨时钟域 (CDC) 的正确电路实现方法. 建议只用一个时钟, 让这个时钟驱动所有的寄存器.

可以在时钟的上升沿来检测 button_debounced0 变成了 1, 具体思路是:

  1. 设置一个寄存器 button_debounced_delay, 相对 button_debounced 有一个周期的延迟;
  2. button_debounced == 1button_debounced_delay == 0 时, 就检测到了一个从 0 变成 1 的过程, 对计数器加一.

电路

模块化设计

代码主要有两部分, 一部分是消抖, 一部分是计数和输出逻辑. 消抖逻辑比较独立, 可以拆出来做成一部分电路, 然后连接到计数和输出逻辑部分.

消抖模块 (Debouncer) :

输入:

  1. clock: 频率为 1MHz 的时钟.
  2. reset: 1 表示复位按钮被按下, 0 表示没有按下.
  3. button: 1 表示计数按钮被按下, 0 表示没有按下.

输出:

  1. button_debounced: 消抖后的计数按钮信号, 高有效.

计数模块 (Counter) :

输入:

  1. clock: 频率为 1MHz 的时钟.
  2. reset: 1 表示复位按钮被按下, 0 表示没有按下.
  3. button_debounced: 消抖后的计数按钮信号, 高有效.

输出:

  1. ones: 输出次数的个位数, 4 位.
  2. tens: 输出次数的十位数, 4 位.

消抖模块

需要记录下历史输入, 才可以判断是否 10,000 个周期都保持稳定. 用 10,000 个 1 位的寄存器有些浪费, 可以用一个寄存器来记录目前稳定了多少个周期.

  1. 寄存器 last_button_reg 记录上一个周期 button.
  2. 寄存器 counter_reg 用来保存一个计数, 当 button == last_button_reg 时, 说明 button 保持稳定, 那么 counter_reg = counter_reg + 1; 否则清零重新计数
  3. 寄存器 button_debounced_reg 保存当前输出的消抖结果, 当 counter_reg = 10000 的时候, 更新 button_debounced_reg = last_button_reg.

最后把 button_debounced_reg 连接到 button_debounced 输出.

计数模块

接收来自消抖模块的输出 button_debounced, 检测到从 0 变成 1 的时候计数器加一. 能不能用一个完整的寄存器保存计数, 输出设置 ones = counter_reg % 10tens = counter_reg / 10? 不建议, 因为除法和取模运算会耗费大量的逻辑门, 并且延迟比较大. 如果位数更多, 产生的电路复杂度和延迟可能是不可接受的. 考虑到这里每次对 counter_reg 的操作只有加一和清零, 可以添加 ones_regtens_reg 寄存器, 实现加一和清零的操作, 并且手动处理进位.

用一个寄存器检测按下的计数按钮, 即 button_debounced0 变成 1:

  1. 寄存器 button_debounced_reg 保存上一周期的 button_debounced.
  2. 如果 button_debounced == 1 && button_debounced_reg == 0, 说明检测到了从 0 变成了 1.

最后是 ones_regtens_reg, 连接到输出的 onestens 信号即可.

代码

首先是消抖电路:

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 把两个模块的输入输出连起来, 其他信号则是直接连接到顶层模块的输入输出信号.

无状态仲裁器 (优先级编码器)

需求

设计一个仲裁器: 假想多个用户同时访问同一个资源, 但是资源同时只能给一个用户使用, 这时需要使用一个仲裁器, 选择出一个幸运儿, 其他用户则需要等待. 假设资源的访问是 “立即” 完成的, 资源正在使用的时候, 使用权不会被其他用户抢走.

输入:

  1. request: 宽度为 4, 每一位 1 表示对应的用户请求访问资源, 0 表示不请求.

输出:

  1. valid: 1 表示有用户请求访问资源, 0 表示无用户请求访问资源.
  2. user: 宽度为 2, 如果有用户请求访问资源时, 输出获得资源的用户的编号.

电路

仲裁器的输出完全由输入决定, 没有内部状态, 所以可以用组合逻辑来实现. valid 信号比较简单, 直接把所有输入用或门连接在一起即可. 如何找到请求的用户里, 编号最小的那一个? 可以分情况讨论:

  1. request=0000, 输出的 user 可以是任意值.
  2. request=???1, 此时 user=0, valid=1.
  3. request=??10, 此时 user=1, valid=1.
  4. request=?100, 此时 user=2, valid=1.
  5. 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 都不满足, 那么输出的就是默认值.

总结

总结规律:

  1. 确定输入输出;
  2. 确定需要哪些寄存器;
  3. 实现时序逻辑;
  4. 实现组合逻辑.

循环优先级仲裁器

需求

实现一个循环优先级仲裁器 (round robin arbiter), 根据最后一次获取资源的用户, 决定下一次获取资源的优先级. 当一个用户 A 不再获取资源 (对应位 request1 变成 0) 时, 重新选择一个可以获取资源的用户, 优先级是从 A 的下一个用户开始为最高优先级, 如果溢出了就绕回.

输入:

  1. request: 宽度为 4, 每一位 1 表示对应的用户请求访问资源, 0 表示不请求.
  2. clock: 1MHz 的时钟.
  3. reset: 复位信号.

输出:

  1. valid: 1 表示有用户请求访问资源, 0 表示无用户请求访问资源.
  2. user: 宽度为 2, 如果有用户请求访问资源时, 输出获得资源的用户的编号.

波形

相比上一个例子, 有两个比较大的区别:

  1. 无状态仲裁器中, 如果出现了优先级更高的用户, 资源的访问权立即切换; 循环优先级仲裁器中, 只有用户放弃了请求才会切换;
  2. 仲裁时, 优先级根据最后一次获得访问权的用户来决定.

电路

由于优先级和最后一次获得访问权的用户有关, 需要时序逻辑实现. 用 user_reg 记录最后一次获得访问权的用户编号:

  1. 什么时候更新: 上一个周期没有用户获得访问权, 这个周期 request 不等于零; 当前周期获得访问权的用户对应的 request 位由 1 变成了 0.
  2. 更新成什么: 按照优先级顺序在 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 信号进行赋值.

文章作者: Chengsx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chengsx !
  目录