辉夜的博客

繁花似锦,辉夜如昼

0%

一生一芯 实验报告

哈尔滨工业大学 于鸿利 22040866

预学习阶段

如何科学的提问

见读后感

Linux系统安装和基本使用

  • 安装一个linux操作系统

    复用了wsl2-ubantu20.04

    首先将windows版本更新到build 21362+(win11即可),然后以管理员权限打开命令行

    输入wsl –update即可将已有的windows subsystem for linux更新到带有图形用户界面的版本

  • 获取一生一芯框架代码

    添加ssh密钥

    按照讲义clone一个本地项目即可

复习C语言

*

搭建verilator仿真环境

  • 认识verilator

    开源的数字电路仿真软件

  • 安装verilator

    make install编译了十几分钟啧啧啧

  • 运行示例

    按照手册中的说明编写了our.v, sim_main.cpp,编译并运行了程序

    1
    verilator -Wall --cc --exe --build sim_main.cpp our.v

    结果是这样的

    1
    2
    Hello World
    our.v:2: Verilog $finish
  • 对双控开关模块进行仿真

    具体而言,需要调整的内容有:

    1. .v文件名要修改为top.v

    2. cpp 文件中要引入头文件

    3. cpp 文件中

      1
      Vour* top = new Vour{contextp};

      的类名要改为Vour

    4. 编译语句中的文件名要做相应的更改

结果是这样的

1
2
3
4
5
6
7
a = 0, b = 1, f = 1
a = 1, b = 0, f = 1
a = 0, b = 0, f = 0
a = 0, b = 1, f = 1
a = 0, b = 1, f = 1
a = 0, b = 1, f = 1
^C

  • 理解RTL仿真的行为

  • 一键仿真

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    #生成的可执行文件名称
    TOPNAME = top
    #在指定文件夹中按扩展名查找所有源文件
    VSRC = $(shell find $(abspath ./vsrc) -name "*.v")
    CSRC = $(shell find $(abspath ./csrc) -name "*.c" -or -name "*.cc" -or -name "*.cpp")
    #nvbroad自动绑定源文件,注意,如果没有$(BUILD_DIR)目录,将无法正确的生成该文件
    SRC_AUTO_BIND = $(abspath $(BUILD_DIR)/auto_bind.cpp)
    CSRC += $(SRC_AUTO_BIND)
    #nvbroad约束文件
    CONSTR = constr/top.nxdc

    #构建目录、verilator目标目录、可执行文件存放路径
    BUILD_DIR = ./build
    OBJ_DIR = $(BUILD_DIR)/obj_dir
    BIN = $(BUILD_DIR)/$(TOPNAME)

    #传递给verilator的参数:以c++形式输出调试信息,并自动依据makefile进行构建
    VERIARG = -Wall --cc -MMD --build

    #传递给preproject连接器的信息,添加SDL2库信息
    LDFLAGS += -lSDL2 -lSDL2_image

    #包含文件路径,加前缀-I传递给g++,INC_PATH在nvbroad.mk中定义
    INCFLAGS = $(addprefix -I, $(INC_PATH))

    #传递给g++的编译参数,包括包含路径和TOP_NAME的定义
    CFLAGS += $(INCFLAGS) -DTOP_NAME="\"V$(TOPNAME)\""

    #包含nvbroad.mk
    include $(NVBOARD_HOME)/scripts/nvboard.mk
    #新建构建目录,防止出现无法新建autobind.cpp的错误
    $(shell mkdir -p $(BUILD_DIR))

    #默认的构建对象
    sim: $(CSRC) $(VSRC) $(NVBOARD_ARCHIVE)
    #提交当前存储区,提交信息为sim RTL
    $(call git_commit, "sim RTL") # DO NOT REMOVE THIS LINE!!!

    #运行verilator,传递给其编译参数
    #指定top模块名称
    #添加所有源文件和nvbroad库
    #传递所有preproject连接器参数,加前缀
    #传递所有编译器参数,加前缀
    #指定生成对象目录,生成可执行文件,输出路径为预先指定的可执行文件存储路径
    verilator $(VERIARG) \
    -top $(TOPNAME) \
    $^ \
    $(addprefix -LDFLAGS , $(LDFLAGS)) \
    $(addprefix -CFLAGS , $(CFLAGS)) \
    --Mdir $(OBJ_DIR) --exe -o $(abspath $(BIN))

    #自动绑定文件,由于其被可执行文件依赖,会先生成该文件。调用python执行nvbroad自带的.py文件,输入约束文件,输出自动绑定文件
    $(SRC_AUTO_BIND): $(CONSTR)
    python3 $(NVBOARD_HOME)/scripts/auto_pin_bind.py $^ $@

  • 运行NVBoard示例

    看到了一组循环显示数字的数码管,还有南京大学的 “北 大楼”(南大的高中同学告诉我的)

  • 在NVBoard上实现双控开关

    我使用如下的代码来完成引脚绑定:

1
2
3
4
5
top=top

a SW0//开关0
b SW1//开关1
f LD0//LED0显示输出

配合如下激励代码来连接面板

1
2
3
4
5
6
7
nvboard_bind_all_pins(&dut);
nvboard_init();
/*verilate*/
while(1) {
dut.eval();
nvboard_update();
}

数字电路基础实验

实验一 选择器

  • 上板实验: 二位四选一选择器

我使用如下的代码来搭建top模块:

1
2
3
4
5
6
7
8
9
10
11
module top(a,s,y);
input [7:0] a;
input [1:0] s;
output [1:0] y;
MuxKeyWithDefault #(4, 2, 2) i0 (y, s, 2'b00, {
2'b00, a[1:0],
2'b01, a[3:2],
2'b10, a[5:4],
2'b11, a[7:6]
});//默认模板,参数分别为:输出端、选择端、默认值、选择-输出键值对
endmodule

可以看到, 双输出的选择器要改变a和y的位宽,调整模板的参数,并且对default output的位宽和键值对的输出值的位宽做相应的调整
我使用如下的代码来完成引脚绑定:

1
2
3
4
5
top=top     //设置TOP_NAME为top

s (SW1, SW0)//这两个开关作为选择端
a (SW9, SW8, SW7, SW6, SW5, SW4, SW3, SW2)//这八个开关作为输入端,每两个开关一组,共四组。
y (LD1, LD0)//用LED显示输出

这里按照实验的建议用开关作为输入,led灯作为输出

我使用如下的代码来完成激励代码的主体部分:

1
2
3
4
5
6
7
8
static TOP_NAME dut;//nvbroad接口,宏自动展开为Vtop
nvboard_bind_all_pins(&dut);//绑定所有引脚
nvboard_init();//初始化面板
/*verilate*/
while(1) {
dut.eval();//获取当前数据
nvboard_update();//重绘面板
}

更新值并且更新版面图像即可,我已经看过了,非常漂亮.:>

实验二 编码器和译码器

c源文件无需更改
verilog文件包含两个模块,数码管模块和编码器模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*top.v*/

module top(x,en,l,h);
/*接口定义,x是输入,en是使能,l是编码结果,h是数码管输出结果*/
input [7:0]x;
input en;
output [3:0]l;
output [6:0]h;

/*线路声明,用y存放编码结果,sign存放指示位*/
reg [2:0]y;
wire sign = en & (|x);//若x至少有一位为1.且使能为1,结果就为1

/*每当x或en发生变化时,都会进行译码,如果有使能,结果是x的为1的位的序号,高位优先,如果没有使能,结果为0*/
integer i;
always @(x or en) begin
if (en) begin
y = 0;
for( i = 0; i <= 7; i = i+1)
if(x[i] == 1) y = i[2:0];
end
else begin
y = 0;
end
end
/*l是编码的总结果,包含符号位和数据位,实例化一个数码管*/
assign l = {sign,y};
seg seg1(
.b(l),
.h(h)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*seg.v*/
module seg(
/*输入输出定义,输入4位编码值,输出7位数码管的显示控制*/
input [3:0] b,
output reg [6:0] h
);
/*每当b发生变化,都会重新更新输出*/
always @(*) begin
/*首先检验指示位,若指示位为0,默认显示为8(共阳极数码管)*/
if(b[3] == 0)
h = 7'b0000000;
else begin
/*若v指示位不为0,则根据数据位进行译码,默认输出为8,*/
case(b[2:0])
3'd0 : h = ~7'b0111111;
3'd1 : h = ~7'b0000110;
3'd2 : h = ~7'b1011011;
3'd3 : h = ~7'b1001111;
3'd4 : h = ~7'b1100110;
3'd5 : h = ~7'b1101101;
3'd6 : h = ~7'b1111101;
3'd7 : h = ~7'b0000111;
//'d8 : h = ~7'b1111111;
//'d9 : h = ~7'b1101111;
default : h = ~7'b1111111;
endcase
en
end
endmodule

约束文件的绑定是这样的:

1
2
3
4
5
top=top//指定top接口名称
x (SW7,SW6,SW5,SW4,SW3,SW2,SW1,SW0)//八个开关作为输入
en SW8//使能
l (LD3,LD2,LD1,LD0)//四个led灯显示编码结果
h (SEG0G, SEG0F, SEG0E, SEG0D, SEG0C, SEG0B, SEG0A)//数码管0输出结果,注意链接顺序要与编码匹配

第三章 加法器和ALU

首先通过约束文件介绍一下整体功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
top=top
operand (SW7,SW6,SW5,SW4,SW3,SW2,SW1,SW0)//八个开关输入操作数
button (BTNC,BTNU,BTND,BTNL,BTNR)//按钮:上下让指令加减2,左右让指令加减1,上下左右都会让使能归零,中间会让使能取反
//也就是实际使用的时候,每当调整指令,都会看到结果消失,按下中间的按钮结果才会再次出现
instr_seg (SEG7G, SEG7F, SEG7E, SEG7D, SEG7C, SEG7B, SEG7A)//最左端数码管显示指令号
/*为了提升视觉体验,没有用到的数码管全部设成空的*/
empty_seg (SEG6G, SEG6F, SEG6E, SEG6D, SEG6C, SEG6B, SEG6A,SEG5G, SEG5F, SEG5E, SEG5D, SEG5C, SEG5B, SEG5A,SEG4G, SEG4F, SEG4E, SEG4D, SEG4C, SEG4B, SEG4A,SEG3G, SEG3F, SEG3E, SEG3D, SEG3C, SEG3B, SEG3A)
/*四个标识符,对应进位/0/负/溢出*/
CF LD3
ZF LD2
SF LD1
OF LD0
/*右边三个数码管输出结果,如果是负数会显示负号哦~*/
value_seg (SEG2G, SEG2F, SEG2E, SEG2D, SEG2C, SEG2B, SEG2A,SEG1G, SEG1F, SEG1E, SEG1D, SEG1C, SEG1B, SEG1A,SEG0G, SEG0F, SEG0E, SEG0D, SEG0C, SEG0B, SEG0A)
/*左边的几个LED灯显示使能和运算结果的二进制表示,用作调试*/
value (LD13,LD12,LD11,LD10)
en LD15

seg模块复用了之前的模块,但是做了些许调整,以显示9,负号和空:

1
2
3
4
5
6
7
/*seg.v*/
case(b[3:0])
......
4'd9 : h = ~7'b1101111;
4'd10 : h = 7'b0111111;//负号
default : h = 7'b1111111;//空
endcase

接下来是alu的主体部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/*top.v*/
module top(
/*输入输出声明*/
input [7:0]operand,
input [4:0]button,
output [6:0]instr_seg,
output reg CF,
output reg ZF,
output reg SF,
output reg OF,
output reg en,
output [20:0]value_seg,
output [27:0]empty_seg,
output reg [3:0]value);
wire [2:0]instr;
wire [3:0]a;
wire [3:0]b;
wire [7:0]value_num;
wire [3:0]value_sign;
/*将不需要使用的数码管置空*/
assign empty_seg = {28{1'b1}};
/*实例化:指令数码管,负号数码管,结果数码管,按钮控制器*/
seg seg7(
.b({1'b0,instr}),
.h(instr_seg)
);
seg seg0(
.b(value_num[3:0]),
.h(value_seg[6:0])
);
seg seg1(
.b(value_num[7:4]),
.h(value_seg[13:7])
);
seg seg2(
.b(value_sign[3:0]),
.h(value_seg[20:14])
);
button button0(
.button(button),
.instr(instr),
.en(en)
);

/*开关输入的操作数*/
assign {a,b} = operand;
/*为减法运算进行的取反,这里不能加一,为了防止Tmin的符号判断错误*/
wire [3:0]b_comp = {4{instr[0]}}^b;


/*操作数,使能,指令码之一变化,就重新输出结果*/
always @(operand or en or instr) begin
if (en) begin
case (instr)
/*加减法*/
3'd0, 3'd1: begin
/*不能把a+b_comp+{1'b0,instr}直接赋给{CF,value},否则当b=0时,b_comp+1会错误的进位*/
value = b_comp + {1'b0,instr};
{CF,value} = value +a;
/*计算溢出:操作数符号相同,结果与操作数符号不同*/
OF = (a[3] == b_comp[3]) && (value[3] != a[3]);
end
/*逻辑运算*/
3'd2 : begin value = ~a; {CF, OF} =2'd0; end
3'd3 : begin value = a & b; {CF, OF} =2'd0; end
3'd4 : begin value = a | b; {CF, OF} =2'd0; end
3'd5 : begin value = a ^ b; {CF, OF} =2'd0; end
/*关系运算*/
3'd6 : begin value = {3'b000,a<b}; {CF, OF} =2'd0; end
3'd7 : begin value = {3'b000,a==b};{CF, OF} =2'd0; end
endcase
ZF = value==0;
SF = value[3] == 1;
end
else begin
/*若使能为0,所有结果都为0*/
value = 0;
{CF, ZF, SF, OF} =4'd0;
end
end

/*value_show是value的绝对值*/
wire [3:0]value_show;
assign value_show = SF ? (~value+1) : value;

/*value_num高四位是十位数,低四位是个位数,value_sign是负号或空*/
assign value_num[7:4] = value_show / 10;
assign value_num[3:0] = value_show % 10;
assign value_sign[3:0] = 4'd11 - {3'b000, (value[3] == 1)};//为10,当为负.为11,当为正

endmodule

接下来是按钮控制模块,相当于有多个触发条件和触发行为的触发器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module button(
/*输入输出声明:输入中上下左右四个按钮的信号,输出指令编码和使能*/
input [4:0]button,
output reg [2:0]instr,
output reg en);
/*所有的五个按钮的上升沿都会触发指令或使能的改变*/
always @(posedge button[3]or posedge button[2] or posedge button[1]
or posedge button[0] or posedge button[4]) begin
/*按一下中间的按钮可以让使能取反*/
if(button[4]) begin
en <= ~en;
end
else begin
/*上:指令+2;下:指令-2;左:指令+1;右:指令-1;这四个按钮都会让使能归零*/
case(button)
5'b01000: begin instr <= instr+2; en <= 0;end
5'b00100: begin instr <= instr-2; en <= 0;end
5'b00010: begin instr <= instr+1; en <= 0;end
5'b00001: begin instr <= instr-1; en <= 0;end
default: en <= 1;
endcase
end
end
endmodule

总体而言,按中间的按钮会执行当前指令的运算,然后可以调节开关,观察结果.如果按上下左右四个按钮,会看到指令码变化,结果消失,需要重新按一次中间的按钮才会出现结果(就好像计算器的”等于”键)

实验五 寄存器组以及存储器

  • 分析三个输出端口的存储器实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
      always @(posedge clk)
    begin
    if (we)
    ram[inaddr] <= din;
    else
    /*dout0在不进行写入的上升沿读取*/
    dout0 <= ram[outaddr];
    end

    always @(negedge clk)
    begin
    /*dout1在没有写使能的下降沿读取*/
    if (!we)
    dout1 <= ram[outaddr];
    end

    /*dout2永远直接读取输出信号*/
    assign dout2 = ram[outaddr];

    完成PA1第一阶段

    1. 开天辟地的篇章

  • 尝试理解计算机如何计算

    取指执行,以跳转实现控制流,以内存和寄存器实现变量

  • 从状态机视角理解程序运行

先把程序写出来:

1
2
3
4
5
6
7
// PC: instruction    | // label: statement
0: mov r1, 0 | pc0: r1 = 0;
1: mov r2, 0 | pc1: r2 = 0;
2: addi r2, r2, 1 | pc2: r2 = r2 + 1;
3: add r1, r1, r2 | pc3: r1 = r1 + r2;
4: blt r2, 100, 2 | pc4: if(r2<100) goto 2
5: jmp 5 | pc5: goto pc5;

然后是开头:

1
(0, x, x) -> (1, 0, x) -> (2, 0, 0) -> (3, 0, 1)

接下来的状态是:

1
2
(4, 1, 1) -> (2, 1, 1) -> (3, 1, 2) -> (4, 3, 2) -> (2, 3, 2) -> (3, 3, 3) -> (4, 3, 4)->...
->(4, 4851, 98) -> (2, 4851, 98) -> (3, 4851, 99) -> (4, 4950, 99) -> (2, 4950, 99) -> (3, 4950, 100) -> (4, 5050, 100) -> (5, 5050, 100) <-> (5, 5050, 100)