加载中
加载中
表情图片
评为精选
鼓励
加载中...
分享
加载中...
文件下载
加载中...
修改排序
加载中...
一个简单的自主指令集的32位RISC处理器
smith2024/06/23原创 计算机电子学 IP:广东

这些年RISCV红火, 打算研究一下RV, 我一个同学兼同事写了著名的tinyriscv的项目, 而我对这一块刚刚入门,在学习RV之前, 我打算自己实现一个自己设计的RISC处理器, 看看能够领悟到什么。


先定义一下指令集和寄存器, 其实处理器和DDS发生器的架构有些类似, 只不过内存里面的数据从正弦查找表变成指令, 同时输出的数据会变成ALU的变量,为了方便处理,通常RISC的指令都是32bit的, 即四个字节 如0xff000000


我们的指令定义成下面的格式

C++
// instru format : 32bit // 0x-xx----xx---xx---xxxx //    |      |    |     | //   op    r1     r2   data


op就是操作码, 如加减乘除 , r1是第一个寄存器的索引, r2是第二个寄存器的索引, 剩下的16位是立即数。

而寄存器的索引如下:

C++
// reg index // zo : 0 always 0 // ra : 1 alu parameter 1 // rb : 2 alu parameter 2 // rc : 3 alu parameter 3 // rd : 4 alu output 1 // re : 5 alu output 2 // rf : 6 temp 1 // rg : 7 temp 2 // rh :8    temp 3 // ri : 9    temp 4 // rj : 10   temp 5 // rk : 11   temp 6 // rl : 12   temp 7 // rm : 13   temp 8 // rn : 14   temp 9 // ro : 15   temp 10 // mo : 16   mode and status // the op is as blow

如果看其他的指令集, 如ARM和MIPS, 通常指令是有三个寄存器索引的, 这里我偷懒,把输出的寄存器永远定义成了rd,即第四个寄存器, 如执行add rb, ra 命令, 那么, 其实rb + ra的结果是在rd寄存器里面的。

接着说一下指令,直接列出

C++
parameter NOP = 8'hff;     // nop parameter HALT = 8'h00;    // halt parameter LDH = 8'h01;     // ldh rn, xxxx parameter LDL = 8'h02;     // ldl rn, xxxx parameter MOV = 8'h05;     // mov rn, rm parameter CLR = 8'h13;     // clr rn parameter INC = 8'h14;    // inc rn parameter DEC = 8'h15;    // dec rn parameter ADD = 8'h16;     // add rn, rm parameter SUB = 8'h17;    // dec rn, rm parameter MUL = 8'h18;    // mul rn, rm parameter DIV = 8'h19;    // div rn, rm parameter LSF = 8'h1a;    // lsf rn, rm parameter RSF = 8'h1b;    // rsf rn, rm parameter AND = 8'h20;    // and rn, rm parameter OR  = 8'h21;    // or rn, rm parameter XOR = 8'h22;    // xor rn, rm parameter NOT = 8'h23;   // not rn parameter STACK = 8'h40;   // stack rn parameter POP  = 8'h62;   // pop rm parameter PUSH  = 8'h51;  // push rn parameter STR = 8'h53;     // str rn, [rm] parameter STD = 8'h54;     // std rn, [0000] parameter IFEQ = 8'h33;   // ifeq rn, rm parameter IFNQ = 8'h34;   // ifnq rn, rm parameter IFLG = 8'h35;   // iflg rn, rm parameter IFSM = 8'h36;   // ifsm rn, rm parameter JPR  = 8'h37;   // jpr rn parameter JPD  = 8'h38;   // jpd xxxx parameter LDR = 8'h63;     // ldr rn, [rm]

我们的指令主要分成几类, 一类是寄存器存取指令, 如LDH 、LDL、MOV这些, 一类是算数指令, 如ADD、DEC、MUL、DIV这些, 一类是跳转指令, 即ifeq、 ifnq, jpr(跳到寄存器的地址), jpd等等。还有以内是内存操作指令, 如LDR、STR、STD, 这些指令都非常消耗时间。

从上面的指令和寄存器来看, 我们的指令集定义得非常的简单易懂, 甚至可以用手直接写代码


比如想把mul rb, ra 这条指令翻译成机器码, 那我们只要查上面的定义:

  1. mul是0x18

  2. rb是2

  3. ra是1

  4. 立即数全是空的, 也就是0000

那么吧上面的数字拼起来, 这个指令就是0x18210000, 非常简单吧

有了指令了,接着还要定义处理器的运行状态, 通常CPU不外乎干几件事情:取指令、运行指令、取内存数据、写内存数据、等待、 错误, 那么对应的就是:

C++
parameter INIT = 0; parameter FETCH = 1; parameter EXEC = 2; parameter LOAD = 3; parameter STORE = 4; parameter IDLE = 5; parameter ERROR = 6;

这里额外加了一个初始化状态。


让CPU干活, 可以利用Verilog的case语句, 如下面这样

C++
            case (op) HALT : begin                     state <= ERROR;                 end                 NOP : begin                     exec_done <= 1'b1;                 end                 LDH : begin                      rn[r1][31:16] <= da;                     exec_done <= 1'b1;                 end                 LDL : begin                     rn[r1][15:0] <= da;                     exec_done <= 1'b1;                 end                 CLR : begin                     rn[r1] <= 0;             exec_done <= 1'b1;                        。。。。。。。。


其中加减乘除,虽然有不少FPGA开发的书建议手写乘法器和除法器,但实际上系统已经有很好的乘法器给我们用来, 直接写 *和/ 综合是可以用altera或者Xilinx系统的乘法器, 比自己写快很多

C++
                ADD : begin                          rn[4] <= rn[r1] + rn[r2];                         if( clk_cont > 2 )                             exec_done <= 1'b1;                         else                             exec_done <= 1'b0;                         clk_cont <= clk_cont + 1;                 end                 SUB : begin                          rn[4] <= rn[r1] - rn[r2];                         if( clk_cont > 2 )                             exec_done <= 1'b1;                         else                             exec_done <= 1'b0;                         clk_cont <= clk_cont + 1;                 end                 MUL : begin                         rn[4] <= rn[r1] * rn[r2];                         if( clk_cont > 10 )                             exec_done <= 1'b1;                         else                             exec_done <= 1'b0;                         clk_cont <= clk_cont + 1;                 end


CPU有了,还得有内存让它才能加载数据跑起来, 这里定义了一个ram。


C++
 RAM ram( .address(ram_data_addr[9:0]), .clock(clk_low), .data(ram_data_in), .wren(ram_wr), .q(ram_data_out));

注意我这里设计的CPU是冯诺依曼架构的, 也就是说内存既装指令也装数据, 这个其实是个大坑, 调试取数据存数据的指令时, 会有一些时序的影响。这也让代码写得很垃圾。


给这个内存加入一个mif文件,也就是初始的程序, 内容如下:

C++
DEPTH = 256; WIDTH = 32; ADDRESS _RADIX = HEX; DATA_RADIX = HEX; -- Specify initial data values for memory, format is address : data CONTENT BEGIN [00..FF]    :   00000000; -- Initialize from course website data 00  :    ff000000;     --nop 04  :    ff000000;     --nop 08  :    02100005;    --ldl ra, 0005  //ra = 0005 0C  :    02200001;   --ldl rb,  0001    10  :    01201000;   --ldh, rb, 1000  //rd = 1000 0001 14  :    16210000;   --add rb, ra      //rd = 1000 0006 18  :    17210000;   --sub rb, ra      // rd = 0fff fffc 1C  :    18210000;   --mul rb, ra     // rd = 5000 0005 20  :    19210000;   -- div rb, ra   // rd = 0333 3333 24  :    13100000;    --clr ra 28  :    02100000;    --ldl ra 0000 2C  :    544000f0;    --sta rd ,00f0 30  :    13400000;    --clr  rd 34  :    13400000;    -- clr  rd 38  :    634000f0;    -- ldr rd ,00f0 3C  :    38000000; END;

这段程序对CPU的加减乘除进行了测试, 结果存入00f0地址,再取出来到rd,在跳到0000首地址


为了观察结果, 我用LED数码管和LCD把寄存器打出来, 还有通过流水灯显示PC的值

C++
LCD_Display lcd_driver(KEY[0], CLOCK_50, ins_out, rd_out, LCD_RS, LCD_EN, LCD_RW, LCD_DATA) SEG7_LUT_8  uled ( .oSEG0(HEX0), .oSEG1(HEX1), .oSEG2(HEX2), .oSEG3(HEX3), .oSEG4(HEX4), .oSEG5(HEX5), .oSEG6(HEX6), .oSEG7(HEX7), .iDIG(rd_out) );


基本上就是这些, 但实际中还有不少恶心的调试,特别对我这种不喜欢仿真的人来说。

最终的代码如下:

attachment icon mtrisc.v 9.98KB V 16次下载


attachment icon DE2_Top.zip 8.45MB ZIP 7次下载

综合的结果如下:

2_104415.png

1_104113.png

占用的资源并不多

运行的视频结果如下:


实际上可以运行得更快,频率可上100M,但为了看到运行, LCD数字和数码管的变化, 这里把时钟设置成了1M

00:00
00:00
仅供内部学术交流或培训使用,请先保存到本地。本内容不代表科创观点,未经原作者同意,请勿转载。
VID_20240623_223854037.mp4  点击下载



[修改于 10个月1天前 - 2024/06/24 08:50:24]

来自:计算机科学 / 计算机电子学
2
1
新版本公告
~~空空如也
粥粥
10个月1天前 IP:湖北
933451

外行也来看看热闹

引用
评论
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论
3DA502
9个月28天前 修改于 9个月27天前 IP:河南
933555

正好最近再研究简单CPU,这种能徒手现编的CPU有个分类叫做 极简CPU。

这里面有个非常优秀的开源设计 J1,我在高云开发板 1k资源就能跑的开,还能添加几个串口。

唯一比较麻烦的是J1是面向forth语言开发的,是个堆栈型CPU,这是历史上存在的CPU流派,没有寄存器,操作数是若干个硬件堆栈。

forth语言是个很烧脑的东西,抽象等级比汇编高一点点。学起来容易但是用起来很难,很考验智力。用得好的高手,能在1k资源fpga里面,跑起来串口交互和forth编译器。

https://github.com/jamesbowman/j1

   

 

 


引用
评论(3)
2
加载评论中,请稍候...
200字以内,仅用于支线交流,主线讨论请采用回复功能。
折叠评论

想参与大家的讨论?现在就 登录 或者 注册

所属专业
上级专业
同级专业
smith
学者 机友 笔友
文章
191
回复
2359
学术分
4
2015/01/11注册,1时45分前活动

收音机爱好者

主体类型:个人
所属领域:无
认证方式:手机号
IP归属地:广东
插入公式
评论控制
加载中...
文号:{{pid}}
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

笔记
{{note.content}}
{{n.user.username}}
{{fromNow(n.toc)}} {{n.status === noteStatus.disabled ? "已屏蔽" : ""}} {{n.status === noteStatus.unknown ? "正在审核" : ""}} {{n.status === noteStatus.deleted ? '已删除' : ''}}
  • 编辑
  • 删除
  • {{n.status === 'disabled' ? "解除屏蔽" : "屏蔽" }}
我也是有底线的