基于FPGA平台RISCV架构的SOC应用系统设计2

​ 本系列文章是参加第四届“复微杯”全国大学生电子设计大赛 FPGA 赛道的作品,该平台基于 RISCV,要求在 FPGA 平台可以实现指令执行,设计思路清晰, 具体如下:

  1. 对所用 RISCV 的内核结构熟悉,了解其数据通路;
  2. 应用方案完整,设计思路清晰,能够清楚的表达设计的内容以及价值;
  3. 可以根据硬件上的资源实现片外启动;
  4. 实现串口通信功能;
  5. FPGA 平台实现功能;
  6. 提供完整设计报告及验证报告;

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-2019-1514-1211-76-0
二进制数据0000_0000_010100000000011110010011

​ 经过一个时钟沿后传递到译码模块,得到读寄存器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-2524-2019-1514-1211-76-0
二进制数据1111_11101111010000110100000100011

读寄存器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;}