HDL4SE:软件工程师学习Verilog语言(十五)

Posted 饶先宏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HDL4SE:软件工程师学习Verilog语言(十五)相关的知识,希望对你有一定的参考价值。

15 RISC-V CPU的FPGA实现

前面我们用软件实现了一个RISC-V的CPU,虽然是用HDL4SE建模实现,但是仍然不是RTL的,没法直接在硬件上运行,充其量算是RISC-V CPU的CModel。本次我们实现一个用verilog写的RISC-V CPU,能够在FPGA上跑起来。然而重点是在于介绍从软件CModel到FPGA中间的开发过程,可以看到,整个过程比直接写verilog还是要强,很多开发设计迭代过程在软件建模中实现了,软件做到RTL时,再用硬件语言实现就水到渠成了。反过来,如果直接看最后的verilog代码,其实很难想象能够直接这么设计出来。

15.1 目标

15.1.1 FPGA开发板

要做FPGA应用开发,需要准备很多前置知识,除了verilog语言的学习之外,还有一些硬件相关的内容。我们选用一个硬件方面要求比较低的FPGA开发板DE1-SOC,它采用了Altera(Intel)的Cyclone V(SOC) FPGA芯片,其中有一个ARM,不过本次我们不使用它,我们只使用它的FPGA部分。
DE1-SOC板子有CLK(50MHz外部输入),GPIO(两组共70个,inout类型),KEY(input 4个,按下松开后自动恢复),SW(input10个,二值输入,开关接入),7段数码管(共阳极,6个),LED灯(10个),64MB SDRAM,视频输入,视频输出(VGA DAC),还有PS/2,IR,AUDIO等接口,CPU那边还能接网络,USB,SD卡等。软件上还有一个自动建立工程的软件,省了很多配置FPGA管脚的工作,比较适合软件工程师入门,应该说做CPU的开发板时比较合适的。
我们这次的目标是在这个FPGA开发板上把RISC-V CPU跑起来,并实现软件的计数器,将计数结果输出到LED上,通过KEY或者SW来控制计数器的动作。

它的外部接口示意图如下(来自友terasIC官网):

我们用DE1-Soc自带的SystemBuilder工具生成FPGA的顶层模块及Altera的工程文件:

生成的工程文件中已经配置好了FPGA的管脚,顶层模型文件如下:


//=======================================================
//  This code is generated by Terasic System Builder
//=======================================================

module de1_riscv(

	 ADC //
	output		          		ADC_CONVST,
	output		          		ADC_DIN,
	input 		          		ADC_DOUT,
	output		          		ADC_SCLK,

	 Audio //
	input 		          		AUD_ADCDAT,
	inout 		          		AUD_ADCLRCK,
	inout 		          		AUD_BCLK,
	output		          		AUD_DACDAT,
	inout 		          		AUD_DACLRCK,
	output		          		AUD_XCK,

	 CLOCK //
	input 		          		CLOCK2_50,
	input 		          		CLOCK3_50,
	input 		          		CLOCK4_50,
	input 		          		CLOCK_50,

	 SDRAM //
	output		    [12:0]		DRAM_ADDR,
	output		     [1:0]		DRAM_BA,
	output		          		DRAM_CAS_N,
	output		          		DRAM_CKE,
	output		          		DRAM_CLK,
	output		          		DRAM_CS_N,
	inout 		    [15:0]		DRAM_DQ,
	output		          		DRAM_LDQM,
	output		          		DRAM_RAS_N,
	output		          		DRAM_UDQM,
	output		          		DRAM_WE_N,

	 I2C for Audio and Video-In //
	output		          		FPGA_I2C_SCLK,
	inout 		          		FPGA_I2C_SDAT,

	 SEG7 //
	output		     [6:0]		HEX0,
	output		     [6:0]		HEX1,
	output		     [6:0]		HEX2,
	output		     [6:0]		HEX3,
	output		     [6:0]		HEX4,
	output		     [6:0]		HEX5,

	 IR //
	input 		          		IRDA_RXD,
	output		          		IRDA_TXD,

	 KEY //
	input 		     [3:0]		KEY,

	 LED //
	output		     [9:0]		LEDR,

	 PS2 //
	inout 		          		PS2_CLK,
	inout 		          		PS2_CLK2,
	inout 		          		PS2_DAT,
	inout 		          		PS2_DAT2,

	 SW //
	input 		     [9:0]		SW,

	 Video-In //
	input 		          		TD_CLK27,
	input 		     [7:0]		TD_DATA,
	input 		          		TD_HS,
	output		          		TD_RESET_N,
	input 		          		TD_VS,

	 VGA //
	output		          		VGA_BLANK_N,
	output		     [7:0]		VGA_B,
	output		          		VGA_CLK,
	output		     [7:0]		VGA_G,
	output		          		VGA_HS,
	output		     [7:0]		VGA_R,
	output		          		VGA_SYNC_N,
	output		          		VGA_VS,

	 GPIO_0, GPIO_0 connect to GPIO Default //
	inout 		    [35:0]		GPIO
);
endmodule

我们先修改它,验证板子能够正确运行:

module de1_riscv(
/*
  端口部分省略......
*/

	wire wClk = CLOCK_50;
	wire nwReset = KEY[3];
	reg [6:0] led0;
	reg [6:0] led1;
	reg [6:0] led2;
	reg [6:0] led3;
	reg [6:0] led4;
	reg [6:0] led5;
	assign HEX0 = ~led0;
	assign HEX1 = ~led1;
	assign HEX2 = ~led2;
	assign HEX3 = ~led3;
	assign HEX4 = ~led4;
	assign HEX5 = ~led5;

	always @(posedge wClk) begin
		if (!nwReset) begin
			led0 <= 8'h3f;
			led1 <= 8'h3f;
			led2 <= 8'h3f;
			led3 <= 8'h3f;
			led4 <= 8'h3f;
			led5 <= 8'h3f;
		end else begin
			if (SW[8]) begin
				led0 <= 8'h06;
				led1 <= 8'h06;
				led2 <= 8'h06;
				led3 <= 8'h07;
				led4 <= 8'h07;
				led5 <= 8'h07;				
			end
			else if (SW[9]) begin
				led0 <= 8'h3f;
				led1 <= 8'h06;
				led2 <= 8'h5b;
				led3 <= 8'h4f;
				led4 <= 8'h66;
				led5 <= 8'h6d;				
			end
		end
	end
	
endmodule

这段代码用Quartus II综合后下载到FPGA板子中(这些操作不详细说明了),能够在数码管显示数字,按键KEY[3],显示000000,松开后拨动开关SW[9]和SW[8],分别显示54321和777111,这样就表示FPGA板子正常运行起来了。

15.1.2 设计目标

我们这次的目标是,将前面的RISC-V CPU核改写为verilog语言实现,在FPGA开发板上跑起来,运行前面的计数器软件,能够将计数值显示在数码管上,并读出按键信息,控制计数的行为(清零,暂停,继续)。数码管作为一个硬件设备挂在CPU的外部读写口上,通过写地址0xF0000010和0xF0000014来控制数码管显示,读0xF0000000得到输入信息。计数器软件代码如下:

const unsigned int segcode[10] =

	0x3F,
	0x06,
	0x5B,//	8'b01011011, 
	0x4F,// 8'b01001111, 
	0x66,// 8'b01100110, 
	0x6d,// 8'b01101101, 
	0x7d,// 8'b01111101, 
	0x07,// 8'b00000111, 
	0x7f,// 8'b01111111, 
	0x6f,// 8'b01101111, 
;

unsigned int num2seg(unsigned int num)

	return segcode[num % 10];


int main(int argc, char* argv[])

	unsigned long long count, ctemp;
	int countit = 1;
	unsigned int* ledkey = (unsigned int*)0xF0000000;
	unsigned int* leddata = (unsigned int*)0xf0000010;
	count = 0;
	leddata[0] = 0x6f7f077d;
	leddata[1] = 0x6d664f5b;
	do 
		unsigned int key;
		key = *ledkey;
		if (key & 1) 
			count = 0;
		
		else if (key & 2) 
			countit = 0;
		
		else if (key & 4) 
			countit = 1;
		
		if (countit)
			count++;

		ctemp = count;
		leddata[0] = num2seg(ctemp) |
			((num2seg(ctemp / 10ll)) << 8) |
			((num2seg(ctemp / 100ll)) << 16) |
			((num2seg(ctemp / 1000ll)) << 24);
		ctemp /= 10000ll;
		leddata[1] = num2seg(ctemp) |
			((num2seg(ctemp / 10ll)) << 8) |
			((num2seg(ctemp / 100ll)) << 16) |
			((num2seg(ctemp / 1000ll)) << 24);
		ctemp /= 10000ll;
		leddata[2] = num2seg(ctemp) |
			((num2seg(ctemp / 10ll)) << 8);
	 while (1);
	return 1;

这段代码用前面准备的RISC-V工具链编译连接后,生成一个ELF文件,通过工具链中的objcopy生成FPGA能够读的格式(本来支持ihex格式,但是不知道怎么回事,Altera ModelSim仿真时总是读不对,于是就用verilog格式然后在软件仿真开始时转换为MIF文件)。我们打算把代码和数据都放在FPGA的RAM中,利用FPGA的RAM IP能够用数据初始化的功能,将ELF文件生成的数据文件放在FPGA的一个RAM中。
这个过程中还涉及到ELF文件生成过程中的内存映象部署的问题,默认的工具链连接时,把运行起始点放在0x00010074开始的地方,前面64KB就空出来了,我们做这个应用时,希望占用的FPGA资源尽可能少,比如用8KB的RAM,就可以支持这个应用运行,其中4KB是代码和只读数据,4KB是程序的数据区和堆栈(当然这个应用没有调用诸如malloc之类的动态内存管理方面的API,因此,堆空间没有实现)。
为此,我们修改了默认的链接脚本,让程序从0x00000000开始,这样就可以在8KB的地址空间内完成运行。具体连接脚本的修改结果请看git文件库。

15.2 CModel模式改到RTL

跟SystemC一样,HDL4SE用来对数字电路建模,最大的好处是能够利用c/c++的资源,这会给建模带来很多方便,能够快速把数字电路的模型建立起来,并且能够仿真运行,可以验证软件工具链以及实现的算法等。我们做这个事情的时候,假定计算资源和存储资源都是不受限制的,而且可以用c/c++的一些表达方式进行算法描述。这样做出来的模型,往往不是RTL的,只能作为数字电路的CModel来用。从CModel改到RTL,其实就是将建模过程中用过的c/c++的表达方式,逐步改为全部用HDL4SE的建模方式实现。具体到RISC-V的这个模型,我们分几步来完成。

15.2.1 存储器实现

在前面实现RISC-V时,内存是用c的指针实现的,这是前面一节中的模型定义:

MODULE_DECLARE(riscv_core)
    unsigned int *ram;
    unsigned int regs[32];
    unsigned int ramsize;
    unsigned int dstreg;
    unsigned int dstvalue;
    unsigned int ramdstaddr;
    unsigned int ramdstvalue;
    unsigned int ramdstwidth; /* 1, 2, 4 */
END_MODULE_DECLARE(riscv_core)

......

MODULE_INIT(riscv_core)
    int i;
    pobj->ramsize = RAMSIZE * 4;
    pobj->ram = malloc(pobj->ramsize);
    loadExecImage(pobj->ram, pobj->ramsize);
......

可以看到其中的ram是用c语言的指针实现的,这样做无法对应到硬件实现,因此我们第一步就是把ram移到模型的外部,也通过模型的read/write系列接口来访问。
具体做法是,用Altera的IP生成工具生成RAM,字长32位,总共2048个字(8KB),生成的RAM只有一个读写口,它的接口如下:

module ram8kb (
    address,
    byteena,
    clock,
    data,
    wren,
    q);

    input	[10:0]  address;
    input	[3:0]  byteena;
    input	  clock;
    input	[31:0]  data;
    input	  wren;
    output	[31:0]  q;
endmodule

具体的用法见Altera的相关文档,为了在HDL4SE系统中进行仿真,我们照这个接口建立HDL4SE模型:

#define riscv_ram_MODULE_VERSION_STRING "0.4.0-20210825.0610 RISCV RAM cell"
#define riscv_ram_MODULE_CLSID CLSID_HDL4SE_RISCV_RAM

#define M_ID(id) riscv_ram##id

IDLIST
    VID(address),
    VID(byteena),
    VID(clock),
    VID(data),
    VID(wren),
    VID(q),
    VID(lastaddr),
END_IDLIST

MODULE_DECLARE(riscv_ram)
    unsigned int* ram;
    unsigned int ramaddr;
    unsigned int ramwrdata;
    unsigned int ramwren;
    unsigned int rambyteena;
END_MODULE_DECLARE(riscv_ram)

DEFINE_FUNC(riscv_ram_gen_q, "address, byteena, data, wren, lastaddr") 
    unsigned int lastaddr;
    lastaddr = vget(lastaddr);
    if (lastaddr < RAMSIZE)
        vput(q, pobj->ram[vget(lastaddr)]);
    else
        vput(q, 0xdeadbeef);
 END_DEFINE_FUNC


DEFINE_FUNC(riscv_ram_clktick, "") 
    pobj->ramwren = vget(wren);
    pobj->ramwrdata = vget(data);
    pobj->rambyteena = vget(byteena);
    pobj->ramaddr = vget(address);
    vput(lastaddr, vget(address));
 END_DEFINE_FUNC

DEFINE_FUNC(riscv_ram_deinit, "") 
    if (pobj->ram != NULL)
        free(pobj->ram);
 END_DEFINE_FUNC

DEFINE_FUNC(riscv_ram_setup, "") 
    if (pobj->ramwren) 
        unsigned int mask =
              (pobj->rambyteena & 1 ? 0x000000ff : 0)
            | (pobj->rambyteena & 2 ? 0x0000ff00 : 0)
            | (pobj->rambyteena & 4 ? 0x00ff0000 : 0)
            | (pobj->rambyteena & 8 ? 0xff000000 : 0);
        pobj->ram[pobj->ramaddr] =  (pobj->ram[pobj->ramaddr] & (~mask))
                                    | (pobj->ramwrdata & mask);
    
    pobj->ramwren = 0;
 END_DEFINE_FUNC

static int loadExecImage(unsigned char* data, int maxlen)

....


MODULE_INIT(riscv_ram)
    pobj->ram = malloc(RAMSIZE * 4);
    loadExecImage(pobj->ram, RAMSIZE * 4);
    pobj->ramwren = 0;
    PORT_IN(clock, 1);
    PORT_IN(wren, 1);
    PORT_IN(address, 11);
    PORT_IN(data, 32);
    PORT_IN(byteena, 4);
    GPORT_OUT(q, 32, riscv_ram_gen_q);
    REG(lastaddr, 11);
    CLKTICK_FUNC(riscv_ram_clktick);
    SETUP_FUNC(riscv_ram_setup);
    DEINIT_FUNC(riscv_ram_deinit);
END_MODULE_INIT(riscv_ram)

这个模型当然也是大量用了c/c++的描述,不过因为这是FPGA的IP,在FPGA应用时是由Altera生成的,因此这里的描述仅供HDL4SE仿真使用,所以也就无所谓了。
当然我们面临的问题是,这里的存储器访问在读的时候有1拍的延时,而且读写不能同时进行,这样前面建模中使用c/c++在同一个周期中读写的方式要进行修改,这点我们后面的寄存器文件修改后再一起描述。

15.2.2 寄存器文件

RISC-V中有32个32位寄存器,一个PC和31个通用寄存器,这些寄存器当然可以使用HDL4SE的寄存器实现,但是要实现按照寄存器号读写寄存器,其实是一个多路选择电路,为了简化电路,我们把寄存器也用1个端口的RAM实现,当然也放在CPU外面实现(当然,PC寄存器还是放在核内用寄存器实现),这样我们也为寄存器访问增加相应的接口。寄存器文件实际就是一个ram,AlteraIP工具生成的接口如下:

module regfile (
    address,
    byteena,
    clock,
    data,
    wren,
    q);

    input	[4:0]  address;
    input	[3:0]  byteena;
    input	  clock;
    input	[31:0]  data;
    input	  wren;
    output	[31:0]  q;
endmodule

为了HDL4SE仿真运行,我们同样用HDL4SE建模语言建模如下,我们特别增加了每个寄存器的访问接口,可以在仿真时在VCD文件中记录每个寄存器的值:

#define riscv_regfile_MODULE_VERSION_STRING "0.4.0-20210825.1540 RISCV REGFILE cell"
#define riscv_regfile_MODULE_CLSID CLSID_HDL4SE_RISCV_REGFILE

#define M_ID(id) riscv_regfile##id
IDLIST
    VID(address),
    VID(byteena),
    VID(clock),
    VID(data),
    VID(wren),
    VID(q),
    VID(lastaddr),
    VID(x1),
    ......
    VID(x31),
END_IDLIST

#define REGCOUNT 32

MODULE_DECLARE(riscv_regfile)
    unsigned int ram[REGCOUNT];
    unsigned int ramaddr;
    unsigned int ramwrdata;
    unsigned int ramwren;
    unsigned int rambyteena;
END_MODULE_DECLARE(riscv_regfile)

DEFINE_FUNC(riscv_regfile_gen_q, "address, byteena, data, wren, lastaddr") 
    unsigned int lastaddr;
    lastaddr = vget(lastaddr);
    if (lastaddr == 0)
        vput(q, 0);
    else
    if (lastaddr < REGCOUNT)
        vput(q, pobj->ram[vget(lastaddr)]);
    else 
        printf("We have %d registers only, but you want to read %d\\n", REGCOUNT, lastaddr);
    
 END_DEFINE_FUNC


DEFINE_FUNC(riscv_regfile_clktick, "") 
    pobj->ramwren = vget(wren);
    pobj->ramwrdata = vget(data);
    pobj->rambyteena = vget(byteena);
    pobj->ramaddr = vget(address);
    vput(lastaddr, vget(address));
 END_DEFINE_FUNC

DEFINE_FUNC(riscv_regfile_setup, "") 
    if (pobj->ramwren以上是关于HDL4SE:软件工程师学习Verilog语言(十五)的主要内容,如果未能解决你的问题,请参考以下文章

HDL4SE:软件工程师学习Verilog语言(十四)

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言