|
本帖最后由 BOB_Sun 于 2015-8-12 09:47 编辑
[FPGA开发] 【Altera SOC体验之旅】(三)软硬结合,scancode示例
本文转自 http://bbs.eeworld.com.cn/thread-459646-1-1.html
(三)SCAN_CODE模块
本篇主要基于上两篇ps2keyboard控制器和vga控制器的基础上增加了一个基于mips32指令集只有20条指令的单周期cpu模块。实现一个在屏幕上显示PS2键盘扫描码的功能。
特别声明:本篇涉及的代码全部来源于《计算机原理与设计—verilog HDL》以及该书作者李亚民老师。我做的工作是尽可能理解这些代码的意思,并且将之简单解释清楚,文中的插图和文字是我自己书写的真实感悟,并未抄袭任何其他资料。同时,对代码中涉及到的时序、接口信号衔接等问题我也做了细致的比对和验证,最终在DE1-SOC开发板上实现了这些功能。此外,我将在后续的文章中,基于本文的工作增添自己需要的功能如中断机制,定时器等),在这个基本的单周期cpu上做自己的升华。希望感兴趣的各位同仁也能联系我,我们一起把这个diysoc做得越来越好。
第一部分:CPU 介绍
1.20条整数指令介绍:
add/sub/and/or/xor rd, rs, rt #rd < - - rs op rt
sll / srl / sra rd, rt, sa # rd < -- rt shift sa
lui rt, imm #rt < -- imm<<16
addi rt, rs ,imm #rt < -- rs + imm(符号拓展)
andi/ori/xori rt, rs, imm #rt< -- rs op imm(零拓展)
lw rt,offset(rs) # rt < -- memory[rs+offset]
sw rt, offset(rs) #memory[rs+offset] < -- rt
beq rs, rt, label #if (rs = = rs),PC< -- label
bne rs, rt, label #if (rs != rs),PC< -- label
j target #PC < --target
jal target #r31 < -- PC+8 ; PC < -- target
jr rs #PC < -- rs
1)MIPS是32位CPU,所以指令的长度,操作数,寄存器的位数都是32位的。
2)共有32个寄存器,即r0~r31; r0的值恒为0x00000000,r31存返回地址。
3)rs, rt表示源操作寄存器, rd表示目标操作寄存器。例如 add r3,r4,r5 就是 (r4) &(r5)-- > r3, r4中的内容与r5中的内容赋值给r3这个寄存器。
4)sll/srl/sra r1,r2,8 # r2逻辑左移/逻辑右移/算数左移 8位赋值给r1
5) lui r3, 0x4000 把0x4000左移16位赋值给r3,置高16位数据。
通常后面跟一句 ori r3, r3,0x3476 马上做或运算。 两条运算拼起来就可以吧0x40003476这个数据完整赋值给r3了。
6) andi r1,r2,0x4768 立即数操作,操作数一个是如r2的寄存器号,一个是常数0x4768立即数。
7)lw r1,3(r4) lw(load words)读外部存储器 把(r4中的内容+3)这个地址中的数据读到r1中。
8) sw r1,3(r4) sw(store words) 写外部存储器 把r1的数据写到 ((r4)+3)这个地址中去。
9)beq r1,r3, loop1 如果r1等于r3,则程序调到loop1标号出的程序段。可以实现分支语句
10)bne r1,r3,loop1,同理,不相等则跳转。
11) j loop1 直接跳转到loop1那个位置
12)jal loop1 先把当前位置下一条指令的PC存到r31,然后跳转到loop1,然后如果在loop1程序段加一句 j r31, 就可以实现执行完子程序后自动返回到跳转之前的下一条指令执行了。
13)每一条汇编语句都对应了一条机器码,
见http://blog.csdn.net/goodlinux/article/details/6731484。这个格式会指导verilog编写的时候如何解析指令,进行操作。
2下面来讲讲cpu代码和功能实现
上图列出了基本模块。 CPU和指令存储器连在一起,发一个pc地址,马上获得inst 指令。CPU右边端口包括和地址映射有关的io_rdn, 外部存储器的读写数据段d_f_mem,d_t_mem, 外部存储器的写控制端,wvram, 和外部存储器的地址端m_addr。不过具体和PS2,vga_c如何连在一起,会在后面讲如何连接。现在这里主要讲cpu内部的结构。
// instruction format
wire [05:00] opcode = inst[31:26];
wire [04:00] rs = inst[25:21];
wire [04:00] rt = inst[20:16];
wire [04:00] rd = inst[15:11];
wire [04:00] sa = inst[10:06];
wire [05:00] func = inst[05:00];
wire [15:00] imm = inst[15:00];
wire [25:00] addr = inst[25:00];
wire sign = inst[15];
wire [31:00] offset = {{14{sign}},imm,2'b00};
wire [31:00] j_addr = {pc_plus_4[31:28],addr,2'b00};
1)首先对inst进行解读,解析各个为的信息具体的是什么,存在具体的变量中。
// instruction decode
wire i_add = (opcode == 6'h00) & (func == 6'h20); // add
wire i_sub = (opcode == 6'h00) & (func == 6'h22); // sub
wire i_and = (opcode == 6'h00) & (func == 6'h24); // and
wire i_or = (opcode == 6'h00) & (func == 6'h25); // or
wire i_xor = (opcode == 6'h00) & (func == 6'h26); // xor
wire i_sll = (opcode == 6'h00) & (func == 6'h00); // sll
wire i_srl = (opcode == 6'h00) & (func == 6'h02); // srl
wire i_sra = (opcode == 6'h00) & (func == 6'h03); // sra
wire i_jr = (opcode == 6'h00) & (func == 6'h08); // jr
wire i_addi = (opcode == 6'h08); // addi
wire i_andi = (opcode == 6'h0c); // andi
wire i_ori = (opcode == 6'h0d); // ori
wire i_xori = (opcode == 6'h0e); // xori
wire i_lw = (opcode == 6'h23); // lw
wire i_sw = (opcode == 6'h2b); // sw
wire i_beq = (opcode == 6'h04); // beq
wire i_bne = (opcode == 6'h05); // bne
wire i_lui = (opcode == 6'h0f); // lui
wire i_j = (opcode == 6'h02); // j
wire i_jal = (opcode == 6'h03); // jal
2)解析操作码和功能码具体代表什么操作,赋值给操作指令的变量,i_add,i_sub之类的变量。如果i_add置1了,则说明指令希望cpu做加法操作。
always @(*) begin
alu_out = 0; // alu output
dest_rn = rd; // dest reg number
wreg = 0; // write regfile
wmem = 0; // write memory (sw)
rmem = 0; // read memory (lw)
next_pc = pc_plus_4;
case (1'b1)
i_add: begin // add
alu_out = a + b;
wreg = 1; end
i_sub: begin // sub
alu_out = a - b;
wreg = 1; end
i_and: begin // and
alu_out = a & b;
wreg = 1; end
i_or: begin // or
alu_out = a | b;
wreg = 1; end
i_xor: begin // xor
3)组合逻辑,一路Case下去,执行对应的操作,如果i_add置1,则做加法操作,令alu_out =a+b;
// data written to register file
wire [31:0] data_2_rf = i_lw ? d_f_mem : alu_out;
// register file
reg [31:0] regfile [1:31]; // $1 - $31
wire [31:0] a = (rs==0) ? 0 : regfile[rs]; // read port
wire [31:0] b = (rt==0) ? 0 : regfile[rt]; // read port
always @ (posedge clk) begin
if (wreg && (dest_rn != 0)) begin
regfile[dest_rn] <= data_2_rf; // write port
end
end
4)r0~r31寄存器堆的读写。定义了1-31个regfile寄存器。如果wreg写寄存器信号1,且dest_rn目标寄存器号(即rd)不为0,则写入数据。data_2_rf一般情况都是alu_out。但在i_lw(从外部存储器取数)这条指令情况下,为外部存储器读到的数据。
5)下面讲讲pc的变化。
reg [31:0] pc;
always @ (posedge clk or negedge clrn) begin
if (!clrn) pc <= 0;
else pc <= next_pc;
end
一般情况pc=next_pc=pc+4;
在跳转指令的情况下,next_pc由具体inst指令计算得出
i_bne: begin // bne
if (a != b)
next_pc = pc_plus_4 + offset; end
(未完,跟帖中)
|
|