数字电路基础实验
写Verilog最重要的是心中要有电路图(这个是官方说法),我觉得也可以理解为你要能知道你自己写出来的Verilog代码能够综合出什么东西来。
以下部分是必做题:
实验一 选择器
实验二 译码器和编码器
实验三 加法器与ALU
实验六 移位寄存器及桶形移位器
实验七 状态机及键盘输入
前面三个都很简单,这里我给出做第六第七的思路和代码。
实验六 移位寄存器及桶形移位器
RTL代码
点击查看代码
module top(
input clk,
input rst,
input [7:0] in,
output [6:0] seg0, // 改为 7 位
output [6:0] seg1 // 改为 7 位
);
reg [7:0] tmp;
reg [6:0] di; // 7 位
reg [6:0] gao; // 7 位
always @(posedge clk or posedge rst) begin
if (rst) begin
tmp <= in;
end
else begin
tmp <= {tmp[4]^tmp[3]^tmp[2]^tmp[0], tmp[7:1]};
end
end
always @(*) begin
// 默认值
di = 7'b1111111;
gao = 7'b1111111;
// 低4位(个位)显示
case (tmp[3:0])
4'h0: di = 7'b1000000; // 0
4'h1: di = 7'b1111001; // 1
4'h2: di = 7'b0100100; // 2
4'h3: di = 7'b0110000; // 3
4'h4: di = 7'b0011001; // 4
4'h5: di = 7'b0010010; // 5
4'h6: di = 7'b0000010; // 6
4'h7: di = 7'b1111000; // 7
4'h8: di = 7'b0000000; // 8
4'h9: di = 7'b0010000; // 9
4'hA: di = 7'b0001000; // A
4'hB: di = 7'b0000011; // B
4'hC: di = 7'b1000110; // C
4'hD: di = 7'b0100001; // D
4'hE: di = 7'b0000110; // E
4'hF: di = 7'b0001110; // F
endcase
// 高4位(十位)显示
case (tmp[7:4])
4'h0: gao = 7'b1000000; // 0
4'h1: gao = 7'b1111001; // 1
4'h2: gao = 7'b0100100; // 2
4'h3: gao = 7'b0110000; // 3
4'h4: gao = 7'b0011001; // 4
4'h5: gao = 7'b0010010; // 5
4'h6: gao = 7'b0000010; // 6
4'h7: gao = 7'b1111000; // 7
4'h8: gao = 7'b0000000; // 8
4'h9: gao = 7'b0010000; // 9
4'hA: gao = 7'b0001000; // A
4'hB: gao = 7'b0000011; // B
4'hC: gao = 7'b1000110; // C
4'hD: gao = 7'b0100001; // D
4'hE: gao = 7'b0000110; // E
4'hF: gao = 7'b0001110; // F
endcase
end
assign seg0 = di; // 7位
assign seg1 = gao; // 7位
endmodule
仿真代码
点击查看代码
#include <nvboard.h>
#include <Vtop.h>
static TOP_NAME dut;
void nvboard_bind_all_pins(TOP_NAME* top);
int main() {
nvboard_bind_all_pins(&dut);
nvboard_init();
while(1) {
nvboard_update();
dut.eval();
}
}
管脚约束
点击查看代码
top=top
clk BTNR
rst BTNL
in (SW7, SW6, SW5, SW4, SW3, SW2, SW1, SW0)
seg0 (SEG0G,SEG0F,SEG0E,SEG0D,SEG0C,SEG0B,SEG0A)
seg1 (SEG1G,SEG1F,SEG1E,SEG1D,SEG1C,SEG1B,SEG1A)
之后make run 使用下面拨码开关先进行初始化in,然后点击左按钮复位后即可一直惦记右按钮进行随机数生成了。这里.v代码很简单,就不讲述了。
实验七 状态机及键盘输入
实验七我借鉴了example的键盘部分,但是我会讲讲代码理解。
管脚约束
点击查看代码
top=top
kbd_clk (PS2_CLK)
kbd_data (PS2_DAT)
seg0 (DEC0P, SEG0G, SEG0F, SEG0E, SEG0D, SEG0C, SEG0B, SEG0A)
seg1 (DEC1P, SEG1G, SEG1F, SEG1E, SEG1D, SEG1C, SEG1B, SEG1A)
seg2 (DEC2P, SEG2G, SEG2F, SEG2E, SEG2D, SEG2C, SEG2B, SEG2A)
seg3 (DEC3P, SEG3G, SEG3F, SEG3E, SEG3D, SEG3C, SEG3B, SEG3A)
seg4 (DEC4P, SEG4G, SEG4F, SEG4E, SEG4D, SEG4C, SEG4B, SEG4A)
seg5 (DEC5P, SEG5G, SEG5F, SEG5E, SEG5D, SEG5C, SEG5B, SEG5A)
seg6 (DEC6P, SEG6G, SEG6F, SEG6E, SEG6D, SEG6C, SEG6B, SEG6A)
seg7 (DEC7P, SEG7G, SEG7F, SEG7E, SEG7D, SEG7C, SEG7B, SEG7A)
仿真代码
点击查看代码
#include <nvboard.h>
#include <Vtop.h>
static TOP_NAME dut;
void nvboard_bind_all_pins(TOP_NAME* top);
static void single_cycle() {
dut.clk = 0; dut.eval();
dut.clk = 1; dut.eval();
}
static void reset(int n) {
dut.rst = 1;
while (n -- > 0) single_cycle();
dut.rst = 0;
}
int main() {
nvboard_bind_all_pins(&dut);
nvboard_init();
reset(10);
while(1) {
nvboard_update();
single_cycle();
}
}
RTL代码
- top.v
点击查看代码
module top (
input clk,
input rst,
input kbd_clk, kbd_data,
output [7:0] seg0,
output [7:0] seg1,
output [7:0] seg2,
output [7:0] seg3,
output [7:0] seg4,
output [7:0] seg5,
output [7:0] seg6,
output [7:0] seg7
);
/* ps2_keyboard interface signals */
wire [7:0] data;
wire ready, overflow;
wire nextdata_n = 1'b0;
ps2_keyboard inst(
.clk(clk),
.clrn(~rst),
.ps2_clk(kbd_clk),
.ps2_data(kbd_data),
.data(data),
.ready(ready),
.nextdata_n(nextdata_n),
.overflow(overflow)
);
reg seg_en;
reg [7:0] cnt;
reg [1:0] state;
reg [7:0] curdata;
reg [7:0] ascii;
key2ascii inst2(
.clk(clk),
.kbd_data(curdata),
.ascii(ascii)
);
bcd7seg bcd7seg0(.en(seg_en), .b(curdata[3:0]), .h(seg0)); // 扫描码低4位
bcd7seg bcd7seg1(.en(seg_en), .b(curdata[7:4]), .h(seg1)); // 扫描码高4位
bcd7seg bcd7seg2(.en(seg_en), .b(ascii[3:0]), .h(seg3)); // ASCII码低4位
bcd7seg bcd7seg3(.en(seg_en), .b(ascii[7:4]), .h(seg4)); // ASCII码高4位
bcd7seg bcd7seg6(.en(1), .b(cnt[3:0]), .h(seg6)); // 计数器低4位
bcd7seg bcd7seg7(.en(1), .b(cnt[7:4]), .h(seg7)); // 计数器高4位
always @(posedge clk) begin
if (rst == 0 && ready) begin // 复位无效且有新数据
$display("keyboard: %x", data);
if (state == 2'b00) begin
seg_en <= 1'b1; // 使能显示
cnt <= cnt + 8'b1; // 计数器递增
curdata <= data; // 保存当前扫描码
state <= 2'b01; // 进入下一个状态
end else if (state == 2'b01) begin
if (data == 8'hf0) begin // 检测到释放码
state <= 2'b10;
seg_en <= 1'b0; // 关闭显示
end
end else if (state == 2'b10) begin
state <= 2'b00; // 返回初始状态
end
end
end
//初始化
initial begin
seg_en = 1'b0;
cnt = 0;
state = 0;
end
assign seg2 = 8'b11111111;
assign seg5 = 8'b11111111;
endmodule
- ps2_keyboard.v
点击查看代码
module ps2_keyboard (
input clk, // 系统时钟
input clrn, // 低电平复位信号
input ps2_clk, // PS/2 时钟信号
input ps2_data, // PS/2 数据信号
input nextdata_n, // 低电平有效的"读取下一个数据"信号
output [7:0] data, // 输出的键盘扫描码
output reg ready, // 数据准备好信号
output reg overflow // FIFO 溢出标志
);
reg [9:0] buffer; // 存储接收到的10位PS/2数据帧
reg [7:0] fifo [7:0]; // 8个字节的FIFO缓冲区
reg [2:0] w_ptr, r_ptr; // FIFO写指针和读指针
reg [3:0] count; // 接收位计数器(0-10)
reg [2:0] ps2_clk_sync; // PS/2时钟同步寄存器
always @(posedge clk) begin
ps2_clk_sync <= {ps2_clk_sync[1:0], ps2_clk};
end
wire sampling = ps2_clk_sync[2] & ~ps2_clk_sync[1];
always @(posedge clk) begin
if (clrn == 0) begin
count <= 0;
w_ptr <= 0;
r_ptr <= 0;
overflow <= 0;
ready <= 0;
end else begin
if (ready) begin
if(nextdata_n == 1'b0)
begin
r_ptr <= r_ptr + 3'b1;
if (w_ptr == (r_ptr + 1'b1))
ready <= 1'b0;
end
end
if (sampling) begin
if (count == 4'd10) begin
if ((buffer[0] == 0) &&
(ps2_data) &&
(^buffer[9:1])) begin
fifo[w_ptr] <= buffer[8:1];
w_ptr <= w_ptr + 3'b1;
ready <= 1'b1;
overflow <= overflow | (r_ptr == (w_ptr + 3'b1));
$display("kbd scan code: %x", buffer[8:1]);
end
count <= 0;
end else begin
buffer[count] <= ps2_data;
count <= count + 3'b1;
end
end
end
end
assign data = fifo[r_ptr];
endmodule
- key2ascii.v
点击查看代码
module key2ascii (
input clk,
input [7:0] kbd_data,
output reg [7:0] ascii
);
always @(posedge clk) begin
case (kbd_data)
8'h1c: ascii <= 8'h61; // a
8'h32: ascii <= 8'h62; // b
8'h21: ascii <= 8'h63; // c
8'h23: ascii <= 8'h64; // d
8'h24: ascii <= 8'h65; // e
8'h2b: ascii <= 8'h66; // f
8'h34: ascii <= 8'h67; // g
8'h33: ascii <= 8'h68; // h
8'h43: ascii <= 8'h69; // i
8'h3b: ascii <= 8'h6a; // j
8'h42: ascii <= 8'h6b; // k
8'h4b: ascii <= 8'h6c; // l
8'h3a: ascii <= 8'h6d; // m
8'h31: ascii <= 8'h6e; // n
8'h44: ascii <= 8'h6f; // o
8'h4d: ascii <= 8'h70; // p
8'h15: ascii <= 8'h71; // q
8'h2d: ascii <= 8'h72; // r
8'h1b: ascii <= 8'h73; // s
8'h2c: ascii <= 8'h74; // t
8'h3c: ascii <= 8'h75; // u
8'h2a: ascii <= 8'h76; // v
8'h1d: ascii <= 8'h77; // w
8'h22: ascii <= 8'h78; // x
8'h35: ascii <= 8'h79; // y
8'h1a: ascii <= 8'h7a; // z
8'h45: ascii <= 8'h30; // 0
8'h16: ascii <= 8'h31; // 1
8'h1e: ascii <= 8'h32; // 2
8'h26: ascii <= 8'h33; // 3
8'h25: ascii <= 8'h34; // 4
8'h2e: ascii <= 8'h35; // 5
8'h36: ascii <= 8'h36; // 6
8'h3d: ascii <= 8'h37; // 7
8'h3e: ascii <= 8'h38; // 8
8'h46: ascii <= 8'h39; // 9
default: ascii <= 8'h00;
endcase
end
endmodule
- bcd7seg.v
点击查看代码
module bcd7seg(
input [3:0] b,
input en,
output reg [7:0] h
);
always @(*) begin
if (!en) begin
h = 8'b11111111;
end else case(b)
4'b0000: h = 8'b00000011;//0
4'b0001: h = 8'b10011111;//1
4'b0010: h = 8'b00100101;//2
4'b0011: h = 8'b00001101;//3
4'b0100: h = 8'b10011001;//4
4'b0101: h = 8'b01001001;//5
4'b0110: h = 8'b01000001;//6
4'b0111: h = 8'b00011111;//7
4'b1000: h = 8'b00000001;//8
4'b1001: h = 8'b00001001;//9
4'b1010: h = 8'b00010001;//A
4'b1011: h = 8'b11000001;//b
4'b1100: h = 8'b01100001;//C
4'b1101: h = 8'b10000101;//d
4'b1110: h = 8'b01100001;//E
4'b1111: h = 8'b01110001;//F
default: h = ~8'b11111111;//异常,全暗下来
endcase
end
endmodule
翻了一下nvboard的源码,原来他会经过verilator编译之后在放到nvboard上跑,因为obj_dir里面代码组成成分一模一样,刨析一下代码:
首先先从小模块开始讲,bcd7seg
讲输入进来的数字转化为数码管显示的数字。
key2ascii
将输出进来的键码转换为ascⅡ码,方便后面数码管输出。
ps2_keyboard
异步接受PS/2协议传来的键盘扫描码,将其存入一个FIFO缓冲区,并提供一个同步接口供后续读取。
端口定义解释
clk:主系统时钟,所有逻辑都在这个时钟的上升沿同步。
clrn:低电平复位信号,有效时将所有寄存器和状态清零。
ps2_clk:PS/2设备的时钟线,是异步输入。
ps2_data:PS/2设备的数据线,是异步输入。
nextdata_n:来自主系统的读取信号。低电平时表示“请给出下一个数据”。
data:输出到主系统的键盘扫描码(8位)。
ready:输出信号,高电平表示FIFO中有数据可供读取。
overflow:输出信号,高电平表示FIFO已满并有新数据覆盖了旧数据(数据丢失)。
点击查看代码
reg [2:0] ps2_clk_sync;
always @(posedge clk) begin
ps2_clk_sync <= {ps2_clk_sync[1:0], ps2_clk};
end
wire sampling = ps2_clk_sync[2] & ~ps2_clk_sync[1];
1.同步与边沿检测
将异步的ps2_clk信号同步到clk,并且用sampling检测下降沿信号。ps2_clk_sync在每个clk上升沿将新的ps2_clk值一如,经过三级寄存器同步,减少了亚稳态的风险。
2.PS/2数据帧接收
reg [9:0] buffer
是用来存储一帧数据的,PS/2协议格式每帧11位,
- 一位起始位(总是0)
- 八位数据位(扫描码)
- 一位奇偶校验位
- 一位停止位(总是1)
每个时钟周期count都会自增1,count从0计数到10,当count达到10时,表示一帧数据接收完成。每当sampling有效,就将ps2_data存入buffer的相应位置,并将count+1。
3.帧校验与FIFO写入
点击查看代码
if (count == 4'd10) begin
if ((buffer[0] == 0) && // 起始位为0
(ps2_data) && // 当前采样的是第11位(停止位),应为1
(^buffer[9:1])) begin // 对1-9位进行奇校验(^是异或,奇校验结果为1)
fifo[w_ptr] <= buffer[8:1]; // 校验通过,将数据位(bit8~bit1)写入FIFO
w_ptr <= w_ptr + 3'b1; // 写指针加1
ready <= 1'b1; // 有数据了,拉高ready信号
overflow <= overflow | (r_ptr == (w_ptr + 3'b1)); // 溢出判断
end
count <= 0; // 无论校验是否通过,计数器都清零,准备接收下一帧
end
-
校验:检查起始位、停止位和奇校验位是否正确。只有全部正确,数据才被视为有效。
-
FIFO写入:校验通过后,将8位数据 (buffer[8:1]) 写入FIFO,并更新写指针 w_ptr。
-
溢出判断:(r_ptr == (w_ptr + 3‘b1)) 判断的是“写指针加1后是否等于读指针”。如果是,说明FIFO已经满了(7个数据),如果再写一个,就会覆盖尚未读取的数据,此时置位 overflow 标志。
4.FIFO读取与输出
点击查看代码
assign data = fifo[r_ptr]; // 组合逻辑输出,始终输出r_ptr指向的数据
if (ready) begin
if(nextdata_n == 1'b0) begin // 当主系统想要读取下一个数据时
r_ptr <= r_ptr + 3'b1; // 读指针加1
if (w_ptr == (r_ptr + 3'b1)) // 如果读指针+1后等于写指针
ready <= 1'b0; // 说明FIFO已空,拉低ready
end
end
-
输出:data 端口始终输出当前读指针 r_ptr 所指向的FIFO中的数据。这是一种常见的做法,主系统可以在任何时刻看到当前要读的数据。
-
读取操作:当 ready 为高(有数据)且主系统拉低 nextdata_n 时,在下一个时钟上升沿,读指针 r_ptr 会增加,指向下一个数据。
-
Ready信号清除:如果读指针加1后追上了写指针,说明FIFo已经读空,此时拉低 ready 信号。
4.top模块
其余的只是例化前面模块而已,重要的是这个状态机,处理了按键事件。
点击查看代码
always @(posedge clk) begin
if (rst == 0 && ready) begin // 1. 条件判断:复位无效且有新数据
$display("keyboard: %x", data); // 2. 仿真打印
if (state == 2'b00) begin // 3. 状态机处理
// STATE 00: 初始/等待状态
seg_en <= 1'b1; // 使能显示
cnt <= cnt + 8'b1; // 计数器递增
curdata <= data; // 保存当前扫描码
state <= 2'b01; // 进入“检查释放码”状态
end else if (state == 2'b01) begin
// STATE 01: 检查下一个数据是否为释放码
if (data == 8'hf0) begin // 检测到释放码 (F0)
state <= 2'b10;
seg_en <= 1'b0; // 关闭显示(表示键已释放)
end
end else if (state == 2'b10) begin
// STATE 10: 释放码处理状态
state <= 2'b00; // 返回初始状态,准备接收下一个按键的按下扫描码
end
end
end