一个简单的自主指令集的32位RISC处理器
smith2024/06/23原创 计算机电子学 IP:广东

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


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


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

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


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

而寄存器的索引如下:

// 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寄存器里面的。

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

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不外乎干几件事情:取指令、运行指令、取内存数据、写内存数据、等待、 错误, 那么对应的就是:

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

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


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

            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系统的乘法器, 比自己写快很多

                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。


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

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


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

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的值

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 12次下载


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

综合的结果如下:

2_104415.png

1_104113.png

占用的资源并不多

运行的视频结果如下:


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

VID_20240623_223854037.mp4 点击下载



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

来自:计算机科学 / 计算机电子学
2
1
已屏蔽 原因:{{ notice.reason }}已屏蔽
{{notice.noticeContent}}
~~空空如也
粥粥
5个月13天前 IP:湖北
933451

外行也来看看热闹

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

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

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

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

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

XXXXXXXXXXXXXXXXXX/jamesbowman/j1

   

 

 


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

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

所属专业
上级专业
同级专业
smith
学者 机友 笔友
文章
189
回复
2353
学术分
4
2015/01/11注册,21分18秒前活动

收音机爱好者

主体类型:个人
所属领域:无
认证方式:手机号
IP归属地:广东
文件下载
加载中...
{{errorInfo}}
{{downloadWarning}}
你在 {{downloadTime}} 下载过当前文件。
文件名称:{{resource.defaultFile.name}}
下载次数:{{resource.hits}}
上传用户:{{uploader.username}}
所需积分:{{costScores}},{{holdScores}}下载当前附件免费{{description}}
积分不足,去充值
文件已丢失

当前账号的附件下载数量限制如下:
时段 个数
{{f.startingTime}}点 - {{f.endTime}}点 {{f.fileCount}}
视频暂不能访问,请登录试试
仅供内部学术交流或培训使用,请先保存到本地。本内容不代表科创观点,未经原作者同意,请勿转载。
音频暂不能访问,请登录试试
支持的图片格式:jpg, jpeg, png
插入公式
评论控制
加载中...
文号:{{pid}}
投诉或举报
加载中...
{{tip}}
请选择违规类型:
{{reason.type}}

空空如也

加载中...
详情
详情
推送到专栏从专栏移除
设为匿名取消匿名
查看作者
回复
只看作者
加入收藏取消收藏
收藏
取消收藏
折叠回复
置顶取消置顶
评学术分
鼓励
设为精选取消精选
管理提醒
编辑
通过审核
评论控制
退修或删除
历史版本
违规记录
投诉或举报
加入黑名单移除黑名单
查看IP
{{format('YYYY/MM/DD HH:mm:ss', toc)}}