基于FPGA平台RISCV架构的SOC应用系统设计2
本系列文章是参加第四届“复微杯”全国大学生电子设计大赛 FPGA 赛道的作品,该平台基于 RISCV,要求在 FPGA 平台可以实现指令执行,设计思路清晰, 具体如下:
- 对所用 RISCV 的内核结构熟悉,了解其数据通路;
- 应用方案完整,设计思路清晰,能够清楚的表达设计的内容以及价值;
- 可以根据硬件上的资源实现片外启动;
- 实现串口通信功能;
- FPGA 平台实现功能;
- 提供完整设计报告及验证报告;
2 SoC简介
2.1 系统结构
tinyriscv是一个采用三级流水线设计,顺序、单发射、单核的32位RISC-V处理器,全部代码都是采用verilog HDL语言编写,核心设计思想是简单、易懂。采用的是三级流水线,即取指、译码和执行,设计的目标就是要对标ARM的Cortex-M3系列处理器。tinyriscv整体框架如图5所示。
图5 tinyriscv整体框架
本次比赛中jtag模块和spi模块没有用到,另外加了一个led指示灯用来表示程序正在运行,每两秒闪烁一次。输入时钟为100MHz,为了与内核时钟50MHz匹配,将100MHz时钟进行二分频。
可见目前tinyriscv已经不仅仅是一个内核了,而是一个小型的SOC,包含一些简单的外设,如timer、uart_tx等。tinyriscv SOC输入输出信号有两部分,一部分是系统时钟clk和复位信号rst,另一部分其他IO口,包括uart下载与调试信号,GPIO,程序状态IO。
pc_reg:PC寄存器模块 | 用于产生PC寄存器的值,该值会被用作指令存储器的地址信号。 |
---|---|
if_id:取指到译码之间的模块 | 用于将指令存储器输出的指令打一拍后送到译码模块。 |
id:译码模块 | 纯组合逻辑电路,根据if_id模块送进来的指令进行译码。当译码出具体的指令(比如add指令)后,产生是否写寄存器信号,读寄存器信号等。由于寄存器采用的是异步读方式,因此只要送出读寄存器信号后,会马上得到对应的寄存器数据,这个数据会和写寄存器信号一起送到id_ex模块。 |
id_ex:译码到执行之间的模块 | 用于将是否写寄存器的信号和寄存器数据打一拍后送到执行模块。 |
ex:执行模块 | 纯组合逻辑电路,根据具体的指令进行相应的操作,比如add指令就执行加法操作等。此外,如果是lw等访存指令的话,则会进行读内存操作,读内存也是采用异步读方式。最后将是否需要写寄存器、写寄存器地址,写寄存器数据信号送给regs模块,将是否需要写内存、写内存地址、写内存数据信号送给rib总线,由总线来分配访问的模块。 |
div:除法模块 | 采用试商法实现,因此至少需要32个时钟才能完成一次除法操作。 |
ctrl:控制模块 | 产生暂停流水线、跳转等控制信号。 |
clint:核心本地中断模块 | 对输入的中断请求信号进行总裁,产生最终的中断信号。 |
rom:程序存储器模块 | 用于存储程序(bin)文件。地址(0x0000_0000~0x0FFF_FFFF) |
ram:数据存储器模块 | 用于存储程序中的数据。地址(0x1000_0000~0x1FFF_FFFF) |
timer:定时器模块 | 用于计时和产生定时中断信号。目前支持RTOS时需要用到该定时器。地址(0x2000_0000~0x2FFF_FFFF) |
uart_tx:串口发送模块 | 主要用于调试打印。地址(0x3000_0000~0x3FFF_FFFF) |
gpio:简单的IO口模块 | 主要用于点灯调试。地址(0x4000_0000~0x4FFF_FFFF) |
RSA:RSA加速模块 | 用于RSA算法的加速。地址(0x5000_0000~0x5FFF_FFFF) |
表2 各个模块作用简介
2.2 简单指令的验证
图6 RISC-V基本指令格式
如图6所示,RISC-V 指令集包含六种基本指令格式,无论是基本指令集或扩展指令集,还是用户自定义的指令,都需要满足这六种基本指令格式。操作码 opcode,规定了不同的指令类型,rd 字段代表目的寄存器的地址,rs1 和 rs2 代表源寄存器的地址,imm代表源操作数之一来自指令的部分字段,即立即数。opcode 和 funct3 的编码并全部未被占用,而是预留大量未被使用的编码,供用户自定义特有的指令,来满足用户个性化需求。
为了充分理解riscv指令如何在tinyriscv内核流通,本案例将用以下C语言代码进行验证的:
int ans = 0;int add = 5;ans = ans + add;
进行编译后,其主函数对应的汇编代码为:
0x194addi1500x0(0)0x198sw150xfec(-20)80x19caddi1500x5(5)0x1a0sw150xfe8(-24)80x1bclw140xfec(-20)80x1c0lw150xfe8(-24)80x1c4add151415
将编译后的二进制文件与编写好的testbench一起仿真,查看tinyriscv内核中关键信号的数值。C语言代码int add = 5;对应两条汇编语句
0x19caddi1500x5(5)0x1a0sw150xfe8(-24)8
当PC指针为0x19c时,执行指令addi 15 0 0x5(5),该条指令为I类型指令,其数据通路如图7(左)所示,仿真波形如图8(左)所示。
图7 I类型(左)与S类型(右)指令数据通路图
图8 I类型(左)与S指令(右)指令仿真波形
从rom里面取出的数据为0x00500793,转换为二进制如下:
数据类型 | 立即数imm | 读寄存器1rs1 | 功能数func3 | 写寄存器rd | 操作码opcode |
---|---|---|---|---|---|
位数 | 31-20 | 19-15 | 14-12 | 11-7 | 6-0 |
二进制数据 | 0000_0000_0101 | 00000 | 000 | 01111 | 0010011 |
经过一个时钟沿后传递到译码模块,得到读寄存器1地址为0,读寄存器2无效。从0寄存器里面取出的操作数1是0,操作数2是立即数进行32位无符号扩展的数,为5,写寄存器地址是0xf(15)。经过一个时钟沿将操作数传递给ex模块进行运行,这里是立即数加指令,将两操作数相加,其结果为5,并将该结果写入寄存器中。
取完该指令后,pc指针加4,指向地址0x1a0,执行指令
sw150xfe8(-24)8;
即将15寄存器里面的值取出放到8寄存器存放的数值偏移-24的ram地址里面。由于15寄存器里面的值在上一条指令用得到,发生了WAR数据冲突,所以此处流水线暂停一个时钟。S指令数据通路图如7(右)所示,该指令仿真波形如8(右)所示。
从rom里取出数据为0xfef43423,转换成二进制如下:
数据类型 | 立即数imm | 读寄存器1rs1 | 读寄存器2Rs2 | 功能数func3 | 写寄存器rd | 操作码opcode |
---|---|---|---|---|---|---|
位数 | 31-25 | 24-20 | 19-15 | 14-12 | 11-7 | 6-0 |
二进制数据 | 1111_111 | 01111 | 01000 | 011 | 010000 | 0100011 |
读寄存器1的值为0x8,取出的值为0x10004000,读寄存器2的值为0xf(15),取出的值为0x5。写寄存器无效。操作数1的值为0x10004000,操作数2为0xffffffe8(-24),在ex模块里,写ram地址为0x10004000+0xffffffe8=0x10003fe8,写数据为0x5.
3 程序设计
3.1 代码设计框图
程序开始运行前,先由python生成两个互为质数的p和q,并写入到C语言中。程序运行后,软件部分先将p和q通过rib总线传入RSA模块的寄存器中,发出计算指令后inverter模块开始根据p、q生成密钥,并保存到RSA模块的寄存器中,软件再从寄存器中读取生成的密钥,根据需要通过串口发送给电脑或者别的移动设备。当需要进行加密时,先将明文传入RSA寄存器中,发出计算指令后mod模块开始根据明文和密钥进行加密,完成后的密文保存到RSA寄存器中,最后软件部分读取密文,并根据需要通过串口发送到电脑或别的移动设备。解密过程与加密过程类似。
本次程序运行过程中实现将需要加密的明文写入软件中,经过加密后再将密文进行解密,通过对比明文与密文是否一致来判断加解密的正确性,其流程图如图9所示。
图9 C语言算法流程图
3.2 程序设计与介绍
主函数程序见附录1,寄存器及宏定义见附录2。其中主函数中的宏定义
#define set_test_pass() asm("li x27, 0x01")#define set_test_fail() asm("li x27, 0x00")
用来设置寄存器27的值为1或者0,在仿真时通过读取寄存器27里面的值来判断程序是否运行正确。关键函数如下:
void inverter_init(const char* p,const char* q);char *get_e(char *str);char *encrypt(const char* code, char *str);char *decrypt(const char *str1,char *str2);
3.3 函数讲解
对于1024位长度的n,对应的宏定义如下
#define L 255#define L_2 127#define L2 511
生成加密密钥函数如下。其中*p、*q为指向p、q数组的指针,p、q互为质数,地址为RSA_INVERTER_STATUS的信号从高电平变到低电平表示开始一次生成密钥过程,开始后地址为RSA_INVETER_FINISH的信号从低电平变为高电平表示生成密钥结束。
void inverter_init(const char* p,const char* q){uint16_t i;for (i=0;i<=L_2;i++)RSA_REG((RSA_P | ((L_2-i)<<8))) = *p++ ;for (i=0;i<=L_2;i++)RSA_REG((RSA_Q | ((L_2-i)<<8))) = *q++ ;RSA_REG(RSA_INVERTER_STATUS) = 1;RSA_REG(RSA_INVERTER_STATUS) = 0;while(!RSA_REG(RSA_INVETER_FINISH));}
获得公钥私钥函数如下。生成密钥结束后即可得到公钥e和私钥d。
char *get_e(char *str){uint16_t i;for (i=0;i<=L2;i++)str[i] = (RSA_REG((RSA_E | (L2-i) << 8 )));return str;}
对数据code进行加密(解密)函数如下。地址为RSA_ENCRYPT_DECRYPT的信号为高电平表示加密。反之为解密。地址为RSA_MOD_STATUS的信号从高电平变成低电平表示一次加密(解密)过程,地址为RSA_MOD_FINISH的信号从低电平变成高电平表示一次加密(解密)过程结束,最后返回加密后(解密后)的数据。
char *encrypt(const char* code, char *str){uint16_t i;char *str_p = str;i=0;while(*code){RSA_REG((RSA_IN | (i << 8 )))=*code++;i++;}RSA_REG(RSA_ENCRYPT_DECRYPT) = 1;RSA_REG(RSA_MOD_STATUS) = 1;RSA_REG(RSA_MOD_STATUS) = 0;while(!RSA_REG(RSA_MOD_FINISH));for (i=0;i<=L_2;i++)*str_p++ = RSA_REG((RSA_OUT | (i << 8) ));return str;}char *decrypt(const char *str1,char *str2){uint16_t i;char *str2_p = str2;for (i=0;i<=L_2;i++)RSA_REG((RSA_IN | (i << 8) )) = *str1++;RSA_REG(RSA_ENCRYPT_DECRYPT) = 0;RSA_REG(RSA_MOD_STATUS) = 1;RSA_REG(RSA_MOD_STATUS) = 0;while(!RSA_REG(RSA_MOD_FINISH));for (i=0;i<=L_2;i++)*str2_p++ = RSA_REG((RSA_OUT | (i << 8 )));return str2;}